Go Network Programming(Go 语言网络编程)
HTTP 编程
1.1. web工作流程
- Web服务器的工作原理可以简单地归纳为
- 客户机通过TCP/IP协议建立到服务器的TCP连接
- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
- 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
- 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果
1.2. HTTP协议
- 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
- HTTP协议通常承载于TCP协议之上
http.HandleFunc("/go", myHandler)- 作用:注册一个处理函数
myHandler来处理访问路径"/go"的 HTTP 请求。 http.HandleFunc的作用是将指定的路径/go与myHandler关联起来,当用户访问http://IP:Port/go时,会调用myHandler处理请求。
- 作用:注册一个处理函数
http.ListenAndServe("IP:Port", nil)- 作用:启动一个 HTTP 服务器,监听指定的 IP 和端口,并处理 HTTP 请求。
nil代表使用默认的http.DefaultServeMux作为路由器,匹配http.HandleFunc注册的路由。
w http.ResponseWriter- 作用:
w是 HTTP 服务器处理请求时的响应写入对象,负责向客户端返回 HTTP 响应数据。 - 服务器处理 HTTP 请求时,可以通过
w.Write([]byte("Hello"))发送响应内容。
- 作用:
r *http.Request- 作用:
r代表客户端的 HTTP 请求,包含了请求方法(GET、POST 等)、URL、请求头和请求体等信息。 - 例如,
r.URL.Path可以获取请求路径,r.Method可以获取请求方法。
- 作用:
resp, err := http.Get("http://IP:Port")- 作用:向
http://IP:Port发送一个 GET 请求,并返回resp(HTTP 响应对象)和err(错误信息)。 resp包含响应的状态码、头信息和响应体。
- 作用:向
resp.Body.Close()- 作用:关闭 HTTP 响应的
Body,释放网络资源。 - 由于
resp.Body是一个流,使用完后需要手动Close(),否则可能会导致资源泄露。
- 作用:关闭 HTTP 响应的
n, err := resp.Body.Read(buf)- 作用:从
resp.Body读取数据到buf缓冲区,返回读取的字节数n和可能的错误err。 - 适用于读取 HTTP 响应体的内容,例如
buf是一个[]byte切片。
- 作用:从
1.3. HTTP服务端
package main
import (
"fmt"
"net/http"
)
func main() {
//http://127.0.0.1:8000/go
// 单独写回调函数
http.HandleFunc("/go", myHandler)
//http.HandleFunc("/ungo",myHandler2 )
// addr:监听的地址
// handler:回调函数
http.ListenAndServe("127.0.0.1:8000", nil)
}
// handler函数
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功")
// 请求方式:GET POST DELETE PUT UPDATE
fmt.Println("method:", r.Method)
// /go
fmt.Println("url:", r.URL.Path)
fmt.Println("header:", r.Header)
fmt.Println("body:", r.Body)
// 回复
w.Write([]byte("XiaoMo"))
}
1.4. HTTP服务端
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//resp, _ := http.Get("http://www.baidu.com")
//fmt.Println(resp)
resp, err := http.Get("http://127.0.0.1:8000/go")
if err != nil {
fmt.Println("Get error: ", err)
}
defer resp.Body.Close()
// 200 OK
fmt.Println(resp.Status)
fmt.Println(resp.Header)
buf := make([]byte, 1024)
for {
// 接收服务端信息
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
return
} else {
fmt.Println("读取完毕")
res := string(buf[:n])
fmt.Println(res)
break
}
}
}
TCP 协议
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
-
listen, err := net.Listen("tcp", "ip:port")- 创建一个 TCP 服务器,监听指定的
ip:port(如"127.0.0.1:8080")。 - 返回一个
Listener对象(listen)用于接受连接,err返回错误(如端口占用)。
- 创建一个 TCP 服务器,监听指定的
-
conn, err := listen.Accept()- 阻塞等待客户端连接。当有连接时,返回一个
Conn对象(conn)表示连接,用于后续通信。
- 阻塞等待客户端连接。当有连接时,返回一个
-
conn.Close()- 关闭当前连接(释放资源)。
-
reader := bufio.NewReader(conn)- 为连接
conn创建一个带缓冲的读取器(reader),提高读取效率。
- 为连接
-
n, err := reader.Read(buf[:])- 从连接中读取数据到字节切片
buf,返回读取的字节数n和错误err。
- 从连接中读取数据到字节切片
-
conn.Write()- 向连接写入数据(需传入字节切片,如
conn.Write([]byte("Hello")))。
- 向连接写入数据(需传入字节切片,如
-
conn, err := net.Dial("tcp", "ip:port")- 作为客户端连接到指定
ip:port的 TCP 服务器,返回连接对象conn。
- 作为客户端连接到指定
-
inputReader := bufio.NewReader(os.Stdin)- 创建一个带缓冲的标准输入读取器(从终端读取用户输入)。
-
input, _ := inputReader.ReadString('\n')- 读取用户输入(直到遇到换行符
\n),结果存储在input中。
- 读取用户输入(直到遇到换行符
-
inputInfo := strings.Trim(input, "\r\n")- 去除输入字符串
input中的换行符(\r\n),得到纯净的输入内容。
- 去除输入字符串
TCP服务端
一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端程序的处理流程:
1.监听端口
2.接收客户端请求建立链接
3.创建goroutine处理链接。
我们使用Go语言的net包实现的TCP服务端代码如下:
// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
将上面的代码保存之后编译成server或server.exe可执行文件。
TCP客户端
一个TCP客户端进行TCP通信的流程如下:
1.建立与服务端的链接
2.进行数据收发
3.关闭链接
使用Go语言的net包实现的TCP客户端代码如下:
// tcp/client/main.go
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}
将上面的代码编译成client或client.exe可执行文件,先启动server端再启动client端,在client端输入任意内容回车之后就能够在server端看到client端发送的数据,从而实现TCP通信。
WebSocket 编程
1.1 webSocket是什么
- WebSocket是一种在单个TCP连接上进行全双工通信的协议
- WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
- 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
- 需要安装第三方包:
- cmd中:go get -u -v github.com/gorilla/websocket
1.2 举个聊天室的小例子
1. router := mux.NewRouter()
-
作用:创建一个新的 HTTP 路由管理器(使用
gorilla/mux包)。 -
功能:
- 用于定义 URL 路径与处理函数的映射(如
/home、/api)。 - 支持路径参数(如
/users/{id})、查询参数、中间件等高级路由功能。
- 用于定义 URL 路径与处理函数的映射(如
-
示例:
router := mux.NewRouter() router.HandleFunc("/", homeHandler) // 绑定根路径到处理函数
2. http.ListenAndServe("127.0.0.1:8080", router)
- 作用:启动一个 HTTP 服务器。
- 参数:
"127.0.0.1:8080":监听本地的 8080 端口(127.0.0.1表示仅本地访问)。router:路由处理器(如果是nil,则使用默认的http.DefaultServeMux)。
- 返回值:
- 返回
error,如果服务器启动失败(如端口被占用)。
- 返回
- 注意:
- 若需允许外网访问,应绑定到
0.0.0.0(如"0.0.0.0:8080")。
- 若需允许外网访问,应绑定到
3. json.Marshal(c.data)
-
作用:将 Go 数据结构 序列化为 JSON 字节数组。
-
参数:
c.data:需要转换的 Go 变量(如结构体、map)。
-
返回值:
[]byte:JSON 格式的字节流。error:转换失败时返回错误。
-
示例:
data := map[string]string{"name": "Alice"} jsonBytes, err := json.Marshal(data) // 得到 `{"name":"Alice"}`
4. c.ws.RemoteAddr().String()
- 作用:获取 WebSocket 客户端 的 远程地址(IP + 端口)。
- 上下文:
c.ws是*websocket.Conn类型的 WebSocket 连接对象。
- 返回值:
- 字符串格式的地址(如
"192.168.1.100:12345")。
- 字符串格式的地址(如
- 用途:
- 常用于日志记录或客户端识别。
5. websocket.Upgrader
-
作用:将 HTTP 连接 升级为 WebSocket 连接 的配置器。
-
关键字段:
upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, // 允许跨域 } -
使用场景:
-
在 HTTP 处理器中升级请求:
func wsHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) // 升级为 WebSocket if err != nil { log.Println("Upgrade failed:", err) return } defer conn.Close() }
-
在同一级目录下新建四个go文件connection.go|data.go|hub.go|server.go
运行
go run server.go hub.go data.go connection.go
运行之后执行local.html文件
server.go文件代码
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
go h.run()
router.HandleFunc("/ws", myws)
if err := http.ListenAndServe("127.0.0.1:8080", router); err != nil {
fmt.Println("err:", err)
}
}
hub.go文件代码
package main
import "encoding/json"
var h = hub{
c: make(map[*connection]bool),
u: make(chan *connection),
b: make(chan []byte),
r: make(chan *connection),
}
type hub struct {
c map[*connection]bool
b chan []byte
r chan *connection
u chan *connection
}
func (h *hub) run() {
for {
select {
case c := <-h.r:
h.c[c] = true
c.data.Ip = c.ws.RemoteAddr().String()
c.data.Type = "handshake"
c.data.UserList = user_list
data_b, _ := json.Marshal(c.data)
c.sc <- data_b
case c := <-h.u:
if _, ok := h.c[c]; ok {
delete(h.c, c)
close(c.sc)
}
case data := <-h.b:
for c := range h.c {
select {
case c.sc <- data:
default:
delete(h.c, c)
close(c.sc)
}
}
}
}
}
data.go文件代码
package main
type Data struct {
Ip string `json:"ip"`
User string `json:"user"`
From string `json:"from"`
Type string `json:"type"`
Content string `json:"content"`
UserList []string `json:"user_list"`
}
connection.go文件代码
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
type connection struct {
ws *websocket.Conn
sc chan []byte
data *Data
}
var wu = &websocket.Upgrader{ReadBufferSize: 512,
WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}
func myws(w http.ResponseWriter, r *http.Request) {
ws, err := wu.Upgrade(w, r, nil)
if err != nil {
return
}
c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
h.r <- c
go c.writer()
c.reader()
defer func() {
c.data.Type = "logout"
user_list = del(user_list, c.data.User)
c.data.UserList = user_list
c.data.Content = c.data.User
data_b, _ := json.Marshal(c.data)
h.b <- data_b
h.r <- c
}()
}
func (c *connection) writer() {
for message := range c.sc {
c.ws.WriteMessage(websocket.TextMessage, message)
}
c.ws.Close()
}
var user_list = []string{}
func (c *connection) reader() {
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
h.r <- c
break
}
json.Unmarshal(message, &c.data)
switch c.data.Type {
case "login":
c.data.User = c.data.Content
c.data.From = c.data.User
user_list = append(user_list, c.data.User)
c.data.UserList = user_list
data_b, _ := json.Marshal(c.data)
h.b <- data_b
case "user":
c.data.Type = "user"
data_b, _ := json.Marshal(c.data)
h.b <- data_b
case "logout":
c.data.Type = "logout"
user_list = del(user_list, c.data.User)
data_b, _ := json.Marshal(c.data)
h.b <- data_b
h.r <- c
default:
fmt.Print("========default================")
}
}
}
func del(slice []string, user string) []string {
count := len(slice)
if count == 0 {
return slice
}
if count == 1 && slice[0] == user {
return []string{}
}
var n_slice = []string{}
for i := range slice {
if slice[i] == user && i == count {
return slice[:count]
} else if slice[i] == user {
n_slice = append(slice[:i], slice[i+1:]...)
break
}
}
fmt.Println(n_slice)
return n_slice
}
local.html文件代码
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<style>
p {
text-align: left;
padding-left: 20px;
}
</style>
</head>
<body>
<div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
<h1>XiaoMo聊天室</h1>
<div style="width: 800px;border: 1px solid gray;height: 300px;">
<div style="width: 200px;height: 300px;float: left;text-align: left;">
<p><span>当前在线:</span><span id="user_num">0</span></p>
<div id="user_list" style="overflow: auto;">
</div>
</div>
<div id="msg_list" style="width: 598px;border: 1px solid gray; height: 300px;overflow: scroll;float: left;">
</div>
</div>
<br>
<textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
<input type="button" value="发送" onclick="send()">
</div>
</body>
</html>
<script type="text/javascript">
var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
var ws = new WebSocket("ws://127.0.0.1:8080/ws");
ws.onopen = function () {
var data = "系统消息:建立连接成功";
listMsg(data);
};
ws.onmessage = function (e) {
var msg = JSON.parse(e.data);
var sender, user_name, name_list, change_type;
switch (msg.type) {
case 'system':
sender = '系统消息: ';
break;
case 'user':
sender = msg.from + ': ';
break;
case 'handshake':
var user_info = {'type': 'login', 'content': uname};
sendMsg(user_info);
return;
case 'login':
case 'logout':
user_name = msg.content;
name_list = msg.user_list;
change_type = msg.type;
dealUser(user_name, change_type, name_list);
return;
}
var data = sender + msg.content;
listMsg(data);
};
ws.onerror = function () {
var data = "系统消息 : 出错了,请退出重试.";
listMsg(data);
};
function confirm(event) {
var key_num = event.keyCode;
if (13 == key_num) {
send();
} else {
return false;
}
}
function send() {
var msg_box = document.getElementById("msg_box");
var content = msg_box.value;
var reg = new RegExp("\r\n", "g");
content = content.replace(reg, "");
var msg = {'content': content.trim(), 'type': 'user'};
sendMsg(msg);
msg_box.value = '';
}
function listMsg(data) {
var msg_list = document.getElementById("msg_list");
var msg = document.createElement("p");
msg.innerHTML = data;
msg_list.appendChild(msg);
msg_list.scrollTop = msg_list.scrollHeight;
}
function dealUser(user_name, type, name_list) {
var user_list = document.getElementById("user_list");
var user_num = document.getElementById("user_num");
while(user_list.hasChildNodes()) {
user_list.removeChild(user_list.firstChild);
}
for (var index in name_list) {
var user = document.createElement("p");
user.innerHTML = name_list[index];
user_list.appendChild(user);
}
user_num.innerHTML = name_list.length;
user_list.scrollTop = user_list.scrollHeight;
var change = type == 'login' ? '上线' : '下线';
var data = '系统消息: ' + user_name + ' 已' + change;
listMsg(data);
}
function sendMsg(msg) {
var data = JSON.stringify(msg);
ws.send(data);
}
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
</script>

浙公网安备 33010602011771号