Go websocket 聊天室demo以及k8s 部署
本来打算练习go websocket 做一个示例,结果在网上找了一个聊天室的示例【
介绍
首先需要有一个客户端 client 的 manager ,manager 里应该保存所有的client 信息
所以在我们的程序里定义了 ClientManager 这个结构体
用 clients 这个 map 结构来保存所有的连接信息
遍历 clients 通过使用 broadcast 这个 channel 把 web 端传送来的消息分发给所有的客户端client
其次每个成功建立长连接的 client 开一个 read 协程和 wrtie 协程
read 协程不断读取 web 端输入的 meaasge,并把 message 传递给 boradcast ,让 manager 遍历 clients 把 message 通过 broadcast channel ,传递给各个客户端 client 的 send channel
write 协程不断的将 send channel 里的消息发送给 web 端
结构图大致如下:
服务代码
main.go
package main import ( "encoding/json" "fmt" "net" "net/http" "github.com/gorilla/websocket" uuid "github.com/satori/go.uuid" ) //客户端管理 type ClientManager struct { //客户端 map 储存并管理所有的长连接client,在线的为true,不在的为false clients map[*Client]bool //web端发送来的的message我们用broadcast来接收,并最后分发给所有的client broadcast chan []byte //新创建的长连接client register chan *Client //新注销的长连接client unregister chan *Client } //客户端 Client type Client struct { //用户id id string //连接的socket socket *websocket.Conn //发送的消息 send chan []byte } //会把Message格式化成json type Message struct { //消息struct Sender string `json:"sender,omitempty"` //发送者 Recipient string `json:"recipient,omitempty"` //接收者 Content string `json:"content,omitempty"` //内容 ServerIP string `json:"serverIp,omitempty"` //实际不需要 验证k8s SenderIP string `json:"senderIp,omitempty"` //实际不需要 验证k8s } //创建客户端管理者 var manager = ClientManager{ broadcast: make(chan []byte), register: make(chan *Client), unregister: make(chan *Client), clients: make(map[*Client]bool), } func (manager *ClientManager) start() { for { select { //如果有新的连接接入,就通过channel把连接传递给conn case conn := <-manager.register: //把客户端的连接设置为true manager.clients[conn] = true //把返回连接成功的消息json格式化 jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()}) //调用客户端的send方法,发送消息 manager.send(jsonMessage, conn) //如果连接断开了 case conn := <-manager.unregister: //判断连接的状态,如果是true,就关闭send,删除连接client的值 if _, ok := manager.clients[conn]; ok { close(conn.send) delete(manager.clients, conn) jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()}) manager.send(jsonMessage, conn) } //广播 case message := <-manager.broadcast: //遍历已经连接的客户端,把消息发送给他们 for conn := range manager.clients { select { case conn.send <- message: default: close(conn.send) delete(manager.clients, conn) } } } } } //定义客户端管理的send方法 func (manager *ClientManager) send(message []byte, ignore *Client) { for conn := range manager.clients { //不给屏蔽的连接发送消息 if conn != ignore { conn.send <- message } } } //定义客户端结构体的read方法 func (c *Client) read() { defer func() { manager.unregister <- c _ = c.socket.Close() }() for { //读取消息 _, message, err := c.socket.ReadMessage() //如果有错误信息,就注销这个连接然后关闭 if err != nil { manager.unregister <- c _ = c.socket.Close() break } //如果没有错误信息就把信息放入broadcast jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message), ServerIP: LocalIp(), SenderIP: c.socket.RemoteAddr().String()}) manager.broadcast <- jsonMessage } } func (c *Client) write() { defer func() { _ = c.socket.Close() }() for { select { //从send里读消息 case message, ok := <-c.send: //如果没有消息 if !ok { _ = c.socket.WriteMessage(websocket.CloseMessage, []byte{}) return } //有消息就写入,发送给web端 _ = c.socket.WriteMessage(websocket.TextMessage, message) } } } func main() { fmt.Println("Starting application...") //开一个goroutine执行开始程序 go manager.start() //注册默认路由为 /ws ,并使用wsHandler这个方法 http.HandleFunc("/ws", wsHandler) http.HandleFunc("/health", healthHandler) //监听本地的8011端口 fmt.Println("chat server start.....") _ = http.ListenAndServe(":8080", nil) } func wsHandler(res http.ResponseWriter, req *http.Request) { //将http协议升级成websocket协议 conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(res, req, nil) if err != nil { http.NotFound(res, req) return } //每一次连接都会新开一个client,client.id通过uuid生成保证每次都是不同的 client := &Client{id: uuid.Must(uuid.NewV4(), nil).String(), socket: conn, send: make(chan []byte)} //注册一个新的链接 manager.register <- client //启动协程收web端传过来的消息 go client.read() //启动协程把消息返回给web端 go client.write() } func healthHandler(res http.ResponseWriter, _ *http.Request) { _, _ = res.Write([]byte("ok")) } func LocalIp() string { address, _ := net.InterfaceAddrs() var ip = "localhost" for _, address := range address { if ipAddress, ok := address.(*net.IPNet); ok && !ipAddress.IP.IsLoopback() { if ipAddress.IP.To4() != nil { ip = ipAddress.IP.String() } } } return ip }
我这里还要验证k8s, 所以加了IP信息,图简单 只有server端部署到k8s,客服端可以通过go程序,html页面访问
Dockerfile
FROM golang:1.15.6 RUN mkdir -p /app WORKDIR /app ADD main /app/main EXPOSE 8080 CMD ["./main"]
build.sh【我这里就不走jenkins, 直接把代码 拖到 k8s master上 运行build文件 编译 推镜像到harbor】
#!/bin/bash
#cd $WORKSPACE
export GOPROXY=https://goproxy.io
#根据 go.mod 文件来处理依赖关系。
go mod tidy
# linux环境编译
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main
# 构建docker镜像,项目中需要在当前目录下有dockerfile,否则构建失败
docker build -t chatserver .
docker tag chatserver 192.168.100.30:8080/go/chatserver:2022
docker login -u admin -p '123456' 192.168.100.30:8080
docker push 192.168.100.30:8080/go/chatserver:2022
docker rmi chatserver
docker rmi 192.168.100.30:8080/go/chatserver:2022
deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: chatserver namespace: go labels: app: chatserver version: v1 spec: replicas: 1 minReadySeconds: 10 selector: matchLabels: app: chatserver version: v1 template: metadata: labels: app: chatserver version: v1 spec: imagePullSecrets: - name: regsecret containers: - name: chatserver image: 192.168.100.30:8080/go/chatserver:2022 ports: - containerPort: 8080 imagePullPolicy: Always --- apiVersion: v1 kind: Service metadata: name: chatserver namespace: go labels: app: chatserver version: v1 spec: ports: - port: 8080 targetPort: 8080 name: grpc-port protocol: TCP selector: app: chatserver --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: chatserver namespace: go annotations: #ingress使用那种软件 kubernetes.io/ingress.class: nginx #配置websocket 需要的配置 nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header Upgrade "websocket"; proxy_set_header Connection "Upgrade"; spec: rules: - host: chatserver.go.com http: paths: #代理websocket服务 - path: / backend: serviceName: chatserver servicePort: 8080
客服端
main.go
package main import ( "flag" "fmt" "net/url" "github.com/gorilla/websocket" ) //定义连接的服务端的网址 var addr = flag.String("addr", "chatserver.go.com", "http service address") func main() { u := url.URL{Scheme: "ws", Host: *addr, Path: "/ws"} var dialer *websocket.Dialer //通过Dialer连接websocket服务器 conn, _, err := dialer.Dial(u.String(), nil) if err != nil { fmt.Println(err) return } //go timeWriter(conn) //打印接收到的消息或者错误 for { _, message, err := conn.ReadMessage() if err != nil { fmt.Println("read:", err) return } fmt.Printf("received: %s\n", message) } }
chatroom.html 【可以用 bee server 提供静态文件服务器】
<html> <head> <title>Golang Chat</title> <script type="application/javascript" src="jquery-1.12.4.js"></script> <script type="text/javascript"> $(function() { var conn; var msg = $("#msg"); var log = $("#log"); function appendLog(msg) { var d = log[0] var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight; msg.appendTo(log) if (doScroll) { d.scrollTop = d.scrollHeight - d.clientHeight; } } $("#form").submit(function() { if (!conn) { return false; } if (!msg.val()) { return false; } conn.send(msg.val()); msg.val(""); return false }); if (window["WebSocket"]) { conn = new WebSocket("ws://chatserver.go.com/ws"); conn.onclose = function(evt) { appendLog($("<div><b>Connection Closed.</b></div>")) } conn.onmessage = function(evt) { appendLog($("<div/>").text(evt.data)) } } else { appendLog($("<div><b>WebSockets Not Support.</b></div>")) } }); </script> <style type="text/css"> html { overflow: hidden; } body { overflow: hidden; padding: 0; margin: 0; width: 100%; height: 100%; background: gray; } #log { background: white; margin: 0; padding: 0.5em 0.5em 0.5em 0.5em; position: absolute; top: 0.5em; left: 0.5em; right: 0.5em; bottom: 3em; overflow: auto; } #form { padding: 0 0.5em 0 0.5em; margin: 0; position: absolute; bottom: 1em; left: 0px; width: 100%; overflow: hidden; } </style> </head> <body> <div id="log"></div> <form id="form"> <input type="submit" value="发送" /> <input type="text" id="msg" size="64"/> </form> </body> </html>
运行效果
确认链接是长连接,不同的客服端链接到不同的服务器上,但是也留下了一个问题待处理,比如A给B发送消息,A链接到服务器1,B链接到服务2,之间消息如何简单方便处理了?
下载地址 https://github.com/dz45693/gochat.git