Go Network Programming(Go 语言网络编程)

HTTP 编程

1.1. web工作流程

  • Web服务器的工作原理可以简单地归纳为
    • 客户机通过TCP/IP协议建立到服务器的TCP连接
    • 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
    • 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
    • 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

1.2. HTTP协议

  • 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
  • HTTP协议通常承载于TCP协议之上
  1. http.HandleFunc("/go", myHandler)
    • 作用:注册一个处理函数 myHandler 来处理访问路径 "/go" 的 HTTP 请求。
    • http.HandleFunc 的作用是将指定的路径 /gomyHandler 关联起来,当用户访问 http://IP:Port/go 时,会调用 myHandler 处理请求。
  2. http.ListenAndServe("IP:Port", nil)
    • 作用:启动一个 HTTP 服务器,监听指定的 IP 和端口,并处理 HTTP 请求。
    • nil 代表使用默认的 http.DefaultServeMux 作为路由器,匹配 http.HandleFunc 注册的路由。
  3. w http.ResponseWriter
    • 作用:w 是 HTTP 服务器处理请求时的响应写入对象,负责向客户端返回 HTTP 响应数据。
    • 服务器处理 HTTP 请求时,可以通过 w.Write([]byte("Hello")) 发送响应内容。
  4. r *http.Request
    • 作用:r 代表客户端的 HTTP 请求,包含了请求方法(GET、POST 等)、URL、请求头和请求体等信息。
    • 例如,r.URL.Path 可以获取请求路径,r.Method 可以获取请求方法。
  5. resp, err := http.Get("http://IP:Port")
    • 作用:向 http://IP:Port 发送一个 GET 请求,并返回 resp(HTTP 响应对象)和 err(错误信息)。
    • resp 包含响应的状态码、头信息和响应体。
  6. resp.Body.Close()
    • 作用:关闭 HTTP 响应的 Body,释放网络资源。
    • 由于 resp.Body 是一个流,使用完后需要手动 Close(),否则可能会导致资源泄露。
  7. 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)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

  1. listen, err := net.Listen("tcp", "ip:port")

    • 创建一个 TCP 服务器,监听指定的 ip:port(如 "127.0.0.1:8080")。
    • 返回一个 Listener 对象(listen)用于接受连接,err 返回错误(如端口占用)。
  2. conn, err := listen.Accept()

    • 阻塞等待客户端连接。当有连接时,返回一个 Conn 对象(conn)表示连接,用于后续通信。
  3. conn.Close()

    • 关闭当前连接(释放资源)。
  4. reader := bufio.NewReader(conn)

    • 为连接 conn 创建一个带缓冲的读取器(reader),提高读取效率。
  5. n, err := reader.Read(buf[:])

    • 从连接中读取数据到字节切片 buf,返回读取的字节数 n 和错误 err
  6. conn.Write()

    • 向连接写入数据(需传入字节切片,如 conn.Write([]byte("Hello")))。
  7. conn, err := net.Dial("tcp", "ip:port")

    • 作为客户端连接到指定 ip:port 的 TCP 服务器,返回连接对象 conn
  8. inputReader := bufio.NewReader(os.Stdin)

    • 创建一个带缓冲的标准输入读取器(从终端读取用户输入)。
  9. input, _ := inputReader.ReadString('\n')

    • 读取用户输入(直到遇到换行符 \n),结果存储在 input 中。
  10. 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})、查询参数、中间件等高级路由功能。
  • 示例

    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>
posted @ 2025-03-31 16:17  XiaoMo247  阅读(46)  评论(0)    收藏  举报