zoukankan      html  css  js  c++  java
  • 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(&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;
                 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;
                 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技术爱好者
  • 相关阅读:
    c# 文件上传
    iOSswift基础篇1
    copyWithZone 的使用方法
    客户端登陆接收大量数据导致数据丢失问题解决方法
    设计模式观察者模式(KVO)
    SQLite 之 C#版 System.Data.SQLite 使用
    设计模式MVC(C++版)
    JS 创建自定义对象的方法
    手机号、邮箱、身份证号 格式 验证
    在.cs文件中添加客户端方法
  • 原文地址:https://www.cnblogs.com/majiang/p/15780349.html
Copyright © 2011-2022 走看看