1. 程式人生 > 其它 >Go websocket 聊天室demo以及k8s 部署

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
 
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,之間訊息如何簡單方便處理了?

windows技術愛好者