Go websocket 聊天室demo以及k8s 部署

Posted dz45693

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go websocket 聊天室demo以及k8s 部署相关的知识,希望对你有一定的参考价值。

本来打算练习go websocket  做一个示例,结果在网上找了一个聊天室的示例【Go websocket 聊天室的详细实现和详细分析_上官二狗的博客-CSDN博客_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(&MessageContent: "/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(&MessageContent: "/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(&MessageSender: 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.UpgraderCheckOrigin: func(r *http.Request) bool  return true ).Upgrade(res, req, nil)
    if err != nil 
        http.NotFound(res, req)
        return
    

    //每一次连接都会新开一个client,client.id通过uuid生成保证每次都是不同的
    client := &Clientid: 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
 
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.URLScheme: "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

windows技术爱好者

以上是关于Go websocket 聊天室demo以及k8s 部署的主要内容,如果未能解决你的问题,请参考以下文章

[Go WebSocket] 单房间的聊天室

go语言聊天室实现(二)gorilla/websocket中的聊天室示例

Java——WebSocket的使用(Demo:聊天室)

详解 WebSocket 原理,附完整的聊天室实战 Demo

在线聊天室 --- WebSocket练手Demo

WebSocket聊天室demo