Go语言基础 -- 网络编程

1. TCP 编程

1.1 TCP协议

TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

1.2 服务端

一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理

TCP服务端程序的处理流程:

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 创建goroutine处理链接。

服务端代码

server/main.go

package main

import (
	"bufio"
	"fmt"
	"net"
)

func process(conn net.Conn) {
	defer conn.Close() // 关闭连接

	for {
		reader := bufio.NewReader(conn)
		var buffer [128]byte
		length, err := reader.Read(buffer[:])
		fmt.Println("读取从客户端发来的字节数:", length)
		if err != nil {
			fmt.Println("读数据错误", err.Error())
			break
		}
		recvStr := string(buffer[:length])
		fmt.Println("读取到客户端发来的消息:", recvStr)
		conn.Write([]byte(recvStr))
	}

}

func main() {
	// 监听地址和端口
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("网络连接错误", err.Error())
		return
	}

	for {
		// 建立连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("连接错误", err.Error())
			continue
		}

		// 每来一个连接.启动一个goroutine 处理连接
		go process(conn)
	}
}

1.3 客户端

一个TCP客户端进行TCP通信的流程如下:

1.建立与服务端的链接
2.进行数据收发
3.关闭链接

客户端代码

client/main.go

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("建立连接错误", err.Error())
		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" {
			return
		}
		_, err = conn.Write([]byte(inputInfo))
		if err != nil {
			return
		}
		buffer := [512]byte{}
		length, err := conn.Read(buffer[:])
		fmt.Println("读取从客户端发来的字节数:", length)
		if err != nil {
			fmt.Println("读取客户端发来的数据错误:", err)
			return
		}
		fmt.Println("读取从客户端发来消息:", string(buffer[:length]))
	}
}

1.4 TCP黏包问题

1.4.1 黏包问题演示

服务端

server/main.go

func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    var buf [1024]byte
    for {
        n, err := reader.Read(buf[:])
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read from client failed, err:", err)
            break
        }
        recvStr := string(buf[:n])
        fmt.Println("收到client发来的数据:", recvStr)
    }
}

func main() {

    listen, err := net.Listen("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue
        }
        go process(conn)
    }
}

客户端

client/main.go

// socket_stick/client/main.go

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        conn.Write([]byte(msg))
    }
}

输出结果

image-20231026151542850

可以看到客户端分10次发送的数据,在服务端并没有成功的输出10次,而是出现了多条数据“粘”到了一起的现象,称之为黏包现象

1.4.2 为什么会出现粘包

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

“粘包”可发生在发送端也可发生在接收端:

1.由Nagle算法造成的发送端的粘包:
	Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。

2.接收端接收不及时造成的接收端粘包:
	TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。

1.4.3 解决办法

出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

封包

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

示例:

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

proto/proto.go

package proto

import (
    "bufio"
    "bytes"
    "encoding/binary"
)

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
    // 读取消息的长度,转换成int32类型(占4个字节)
    var length = int32(len(message))
    var pkg = new(bytes.Buffer)
    // 写入消息头
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {
        return nil, err
    }
    // 写入消息实体
    err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    if err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
    // 读取消息的长度
    lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
    lengthBuff := bytes.NewBuffer(lengthByte)
    var length int32
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {
        return "", err
    }
    // Buffered返回缓冲中现有的可读取的字节数。
    if int32(reader.Buffered()) < length+4 {
        return "", err
    }

    // 读取真正的消息数据
    pack := make([]byte, int(4+length))
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    return string(pack[4:]), nil
}

接下来在服务端和客户端分别使用上面定义的proto包的DecodeEncode函数处理数据。

服务端

package main

import (
	"bufio"
	"fmt"
	"io"
	"learn.mod/proto"
	"net"
)
func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		msg, err := proto.Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("decode msg failed, err:", err)
			return
		}
		fmt.Println("收到client发来的数据:", msg)
	}
}

func main() {

	listen, err := net.Listen("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn)
	}
}

客户端

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        data, err := proto.Encode(msg)
        if err != nil {
            fmt.Println("encode msg failed, err:", err)
            return
        }
        conn.Write(data)
    }
}

2. UDP 编程

2.1 UDP协议

UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

2.2 服务端

server/main.go

package main

import (
	"fmt"
	"net"
)

func main() {
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		var data [1024]byte
		n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
		if err != nil {
			fmt.Println("read udp failed, err:", err)
			continue
		}
		fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
		_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
		if err != nil {
			fmt.Println("write to udp failed, err:", err)
			continue
		}
	}
}

2.3 客户端

client/main.go

package main

import (
	"fmt"
	"net"
)

func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("连接服务端失败,err:", err)
		return
	}
	defer socket.Close()
	sendData := []byte("Hello server")
	_, err = socket.Write(sendData) // 发送数据
	if err != nil {
		fmt.Println("发送数据失败,err:", err)
		return
	}
	data := make([]byte, 4096)
	n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
	if err != nil {
		fmt.Println("接收数据失败,err:", err)
		return
	}
	fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

3. HTTP 编程

3.1 web工作流程

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

3.2 HTTP协议

  • 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
  • HTTP协议通常承载于TCP协议之上

3.3 服务端

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("www.5lmh.com"))
}

3.4 服务端

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    //resp, _ := http.Get("http://www.baidu.com")
    //fmt.Println(resp)
    resp, _ := http.Get("http://127.0.0.1:8000/go")
    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
        }
    }
}

4. WebSocket编程

1. webSocket是什么

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议
  • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
  • WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

2. 包下载

go get -u -v github.com/gorilla/websocket

3. 建立连接

1. 前端

<!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>www.5lmh.comy演示聊天室</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:8890/ws");
    // 建立连接成功后执行的回调函数 onopen
    ws.onopen = function () {
        var data = "系统消息:建立连接成功";
        listMsg(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>

2. 后端

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
	"net/http"
)

// 读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512,
	WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

func chatHandler(w http.ResponseWriter, r *http.Request){
	// 建立连接成功
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil{
		return
	}
	fmt.Println("连接建立成功",ws)


}

func main() {
	// 1. 创建路由
	var router = mux.NewRouter()
	
	// 2. 创建路由映射关系及回调函数
	router.HandleFunc("/ws",chatHandler)
	
	// 3. 启动服务器, 并且设置监听地址
	if err := http.ListenAndServe("127.0.0.1:8890",router);err != nil{
		fmt.Println("err: ",err)
		return
	}
}

4. 接收消息

1. 前端

<!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>www.5lmh.comy演示聊天室</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:8890/ws");
    ws.onopen = function () {
        var data = "系统消息:建立连接成功";
        listMsg(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('');
    }
    
    // 发送消息
    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 sendMsg(msg) {
        var data = JSON.stringify(msg);
        ws.send(data);  // 调用 ws.send() 发送消息
    }
</script>

2. 后端

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
	"net/http"
)

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"`
}

var wu = &websocket.Upgrader{ReadBufferSize: 512,
	WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

func chatHandler(w http.ResponseWriter, r *http.Request) {
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}

	// 开启死循环, 不停的读取 buffer 中的消息, 如果前端发送了消息, 就可以收到
	for {
		_,message,err := ws.ReadMessage()
		if err != nil{
			fmt.Println("err: ",err)
			break
		}
		d1 := Data{}
		fmt.Println("后端接收到消息",message,json.Unmarshal(message,&d1))
		fmt.Println(d1)
	}
}

func main() {
	var router = mux.NewRouter()
	router.HandleFunc("/ws", chatHandler)
	if err := http.ListenAndServe("127.0.0.1:8890", router); err != nil {
		fmt.Println("err: ", err)
		return
	}
}

5. 发送消息

1. 前端

<!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>www.5lmh.comy演示聊天室</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:8890/ws");
    ws.onopen = function () {
        var data = "系统消息:建立连接成功";
        listMsg(data);
    };
    // 接收到服务器发送的消息, 触发 onmessage 回调函数
    ws.onmessage = function (e) {
        console.log("接收到服务器发送的消息: ",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);
    };
    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 sendMsg(msg) {
        var data = JSON.stringify(msg);
        console.log("前端向后端发送的消息: ", data)
        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>

2. 后端

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
	"net/http"
)

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"`
}

var wu = &websocket.Upgrader{ReadBufferSize: 512,
	WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

func chatHandler(w http.ResponseWriter, r *http.Request) {
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	// 一定要在开启死循环读取 buffer 中的消息前, 开启协程发送消息, 否则一直卡在死循环里, 无法开启这个协程
	go func() {
		for i := 0; i < 50; i++ {
			i := 0
			m1 := Data{
				Content: fmt.Sprintf("你是?%v", i),
				Type:    "user",
			}
			bmessage, _ := json.Marshal(&m1)
			ws.WriteMessage(websocket.TextMessage, bmessage)
			i++
			fmt.Println(m1)
		}
		ws.Close()
	}()

	// 开启死循环, 不停的读取 buffer 中的消息, 如果前端发送了消息, 就可以收到
	for {
		_, message, err := ws.ReadMessage()
		if err != nil {
			fmt.Println("err: ", err)
			break
		}
		d1 := Data{}
		fmt.Println("后端接收到消息", message, json.Unmarshal(message, &d1))
		fmt.Println(d1)
	}

}

func main() {
	var router = mux.NewRouter()
	router.HandleFunc("/ws", chatHandler)
	if err := http.ListenAndServe("127.0.0.1:8890", router); err != nil {
		fmt.Println("err: ", err)
		return
	}
}

9. 聊天室案例

0. 前端

<!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>www.5lmh.comy演示聊天室</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:9988/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>

1. 目录结构创建

在同一级目录下新建四个go文件

connection.go    // 连接相关
data.go			// 通讯协议(数据)
hub.go			// 
server.go        // 主入口(服务端)

2. 服务端启动

server.go

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"net/http"
)

func main() {
	// 1. 创建路由
	router := mux.NewRouter()

	// 2. 注册路由映射关系
	router.HandleFunc("/ws", websocketHandler)

	// 3. 启动服务端
	if err := http.ListenAndServe("127.0.0.1:9988", router); err != nil {
		fmt.Println("err", err)
	}
}

3. 建立连接

1. 初始化连接信息

connection.go

package main

import (
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn
	sc   chan []byte
	data *Data
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 建立连接
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}

	// 2. 建立连接成功后, 对当前连接进行初始化
	c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
	h.r <- c

}

2. 建立连接成功, 告知前端

server.go

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"net/http"
)

func main() {
	router := mux.NewRouter()

	// 开启一个协程, 用来处理服务端对连接的 状态保持 等操作
	go h.run()
	
	router.HandleFunc("/ws", websocketHandler)
	if err := http.ListenAndServe("127.0.0.1:9988", router); err != nil {
		fmt.Println("err", err)
	}
}

hub.go

package main

import "encoding/json"

var h = hub{
	allConnStatus: make(map[*connection]bool),
	r:             make(chan *connection),
}

type hub struct {
	allConnStatus map[*connection]bool // 存放全局连接的状态 { 0x12456446: true }, 用于表示当前连接是否在线
	r             chan *connection     // 来握手的连接
}

func (h *hub) run() {
	// 死循环, 不停的处理握手、挥手、以及异常退出
	for {
		select {
		case c := <-h.r: // 对握手成功的所有连接进行初始化, 并返回前端连接建立成功的消息
			h.allConnStatus[c] = true              // 设置当前连接为在线状态
			c.data.Ip = c.ws.RemoteAddr().String() // 设置当前连接的 IP 地址
			c.data.Type = "handshake"              // 设置当前消息类型为 成功建立连接
			c.data.UserList = user_list            // 设置在线成员列表
			data_byte, _ := json.Marshal(c.data)   // 将消息压入当前连接的 channel
			c.sc <- data_byte
		}
	}
}

connection.go

package main

import (
	"fmt"
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn // 当前连接对象
	sc   chan []byte     // 需要返回给前端的消息内容 json 序列化后的 byte 数组
	data *Data           // 消息体
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 握手
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}

	// 2. 每来一个连接, 握手成功, 就实例化一个 connection 并对其初始化, 保存它自己的 ws 连接对象
	c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
	h.r <- c

	// 3. 每来一个建立连接, 开一个协程, 调用当前 connection 的 writer 方法, 返回对应连接消息
	go c.writer()

}

func (c *connection) writer() {
	for message := range c.sc {
		fmt.Println("返回前端的消息:>>>", message, c)
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

// 3. 在线成员列表
var user_list = []string{}

4. 进入聊天室

connection.go

package main

import (
	"encoding/json"
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn // 当前连接对象
	sc   chan []byte     // 需要返回给前端的消息内容 json 序列化后的 byte 数组
	data *Data           // 消息体
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(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()

	// 4. 死循环不停的读取前端是否发送了消息, 如果发送了消息, 根据对应的逻辑进行处理
	c.reader()
}

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 字符串
		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_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		}
	}
}

hub.go

package main

import (
	"encoding/json"
)

var h = hub{
	allConnStatus: make(map[*connection]bool),
	responseData:  make(chan []byte),
	r:             make(chan *connection),
}

type hub struct {
	allConnStatus map[*connection]bool 
	responseData  chan []byte          // 服务端返回的消息
	r             chan *connection     
}

func (h *hub) run() {
	for {
		select {
		case c := <-h.r:   // 握手
			h.allConnStatus[c] = true             
			c.data.Ip = c.ws.RemoteAddr().String() 
			c.data.Type = "handshake"             
			c.data.UserList = user_list            
			data_byte, _ := json.Marshal(c.data)   
			c.sc <- data_byte
		case data := <-h.responseData:    // 读取服务端是否给前端发送了消息
			for c := range h.allConnStatus {  // 读取全局缓存的曾经建立的所有连接, 挨个发送消息
				select {
				case c.sc <- data:    // 则将消息体压入 c.sc channel 中, 有协程在监控 c.sc 的 channel,一有消息就会发送给前端
				default:
				}
			}
		}
	}
}

5. 用户发送消息

connection.go

package main

import (
	"encoding/json"
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn // 当前连接对象
	sc   chan []byte     // 需要返回给前端的消息内容 json 序列化后的 byte 数组
	data *Data           // 消息体
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(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()

	// 4. 死循环不停的读取前端是否发送了消息, 如果发送了消息, 根据对应的逻辑进行处理
	c.reader()
}

func (c *connection) writer() {
	for message := range c.sc {
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

// 3. 在线成员列表
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_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		case "user":
			c.data.Type = "user"
			data_byte, _ := json.Marshal(c.data)  // 将用户发送的消息发送到各个用户
			h.responseData <- data_byte
		}
	}
}

6. 主动退出聊天室

connections.go

package main

import (
	"encoding/json"
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn // 当前连接对象
	sc   chan []byte     // 需要返回给前端的消息内容 json 序列化后的 byte 数组
	data *Data           // 消息体
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(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()

	// 4. 死循环不停的读取前端是否发送了消息, 如果发送了消息, 根据对应的逻辑进行处理
	c.reader()
}

func (c *connection) writer() {
	for message := range c.sc {
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

// 3. 在线成员列表
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_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		case "user":
			c.data.Type = "user"
			data_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		case "logout":
			c.data.Type = "logout"
			c.data.Content = c.data.User
			user_list = del(user_list, c.data.User) // 从全局缓存的在线用户列表中, 将当前用户删掉
			data_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		}
	}
}

// 将当前用户从全局缓存的用户列表中删除
func del(user_list []string, user string) []string {
	count := len(user_list)
	if count == 0 {
		return user_list
	}

	if count == 1 && user_list[0] == user {
		return []string{}
	}

	new_user_list := []string{}
	for i := range user_list {
		// 如果当前用户在切片的最后一个
		if user_list[i] == user && i == count {
			return user_list[:count]
		} else if user_list[i] == user {
			new_user_list = append(user_list[:i], user_list[i+1:]...)
			break
		}
	}

	return new_user_list
}

7. 异常退出(连接断开)

connection.go

package main

import (
	"encoding/json"
	"github.com/gorilla/websocket"
	"net/http"
)

// 连接信息
type connection struct {
	ws   *websocket.Conn // 当前连接对象
	sc   chan []byte     // 需要返回给前端的消息内容 json 序列化后的 byte 数组
	data *Data           // 消息体
}

// 1. 创建读写 buffer
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

// 2. websocket 处理函数
func websocketHandler(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()

	// 5. 连接断开
	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_byte, _ := json.Marshal(c.data)
		h.responseData <- data_byte
		h.u <- c   // 压入当前连接到 退出 channel 中, 释放资源
	}()
}

func (c *connection) writer() {
	for message := range c.sc {
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

// 3. 在线成员列表
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_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		case "user":
			c.data.Type = "user"
			data_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		case "logout":
			c.data.Type = "logout"
			c.data.Content = c.data.User
			user_list = del(user_list, c.data.User) 
			data_byte, _ := json.Marshal(c.data)
			h.responseData <- data_byte
		}
	}
}

// 将当前用户从全局缓存的用户列表中删除
func del(user_list []string, user string) []string {
	count := len(user_list)
	if count == 0 {
		return user_list
	}

	if count == 1 && user_list[0] == user {
		return []string{}
	}

	new_user_list := []string{}
	for i := range user_list {
		// 如果当前用户在切片的最后一个
		if user_list[i] == user && i == count {
			return user_list[:count]
		} else if user_list[i] == user {
			new_user_list = append(user_list[:i], user_list[i+1:]...)
			break
		}
	}

	return new_user_list
}

hub.go

package main

import (
	"encoding/json"
)

var h = hub{
	allConnStatus: make(map[*connection]bool),
	responseData:  make(chan []byte),
	r:             make(chan *connection),
	u:             make(chan *connection),
}

type hub struct {
	allConnStatus map[*connection]bool // 存放全局连接的状态 { 0x12456446: true }, 用于表示当前连接是否在线
	responseData  chan []byte          // 服务端返回的消息
	r             chan *connection     // 来握手的连接
	u             chan *connection
}

func (h *hub) run() {
	for {
		select {
		case c := <-h.r:
			h.allConnStatus[c] = true              
			c.data.Ip = c.ws.RemoteAddr().String() 
			c.data.Type = "handshake"              
			c.data.UserList = user_list           
			data_byte, _ := json.Marshal(c.data)   
			c.sc <- data_byte
		case c := <-h.u:
			if _, ok := h.allConnStatus[c]; ok {
				delete(h.allConnStatus, c) // 从全局缓存的连接对象中删除当前退出的连接对象
				close(c.sc)                // 关闭当前连接对象的 channel
			}
		case data := <-h.responseData:
			for c := range h.allConnStatus { 
				select {
				case c.sc <- data:
				default:
				}
			}
		}
	}
}

posted @ 2021-10-04 11:52  河图s  阅读(49)  评论(0)    收藏  举报