• Go websocket 聊天室demo2


    上文Go websocket 聊天室demo以及k8s 部署   后面有一个问题, 如果2个客服端 分别来链接到不同的服务 如何发布消息了?

    如图:

     cliant A ->ServerA ----推送消息到kafka---->推送消息到 服务A和B---->服务AB都去寻找自己的client集合------>发送消息给具体的客户端【有可能是广播 也可能是指定具体的用户】

    代码:

    package main
    
    import (
        "context"
        "encoding/json"
        "fmt"
        "log"
        "net"
        "net/http"
        "time"
    
        "github.com/Shopify/sarama"
        "github.com/gorilla/websocket"
        uuid "github.com/satori/go.uuid"
    )
    
    //客户端管理
    type ClientManager struct {
        //客户端 map 储存并管理所有的长连接client,在线的为true,不在的为false
        clients map[string]*Client
        //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
        //服务器IP
        ip string
    }
    
    //会把Message格式化成json
    type Message struct {
        //消息struct
        Sender    string `json:"sender,omitempty"`    //发送者
        Recipient string `json:"recipient,omitempty"` //接收者
        Content   string `json:"content,omitempty"`   //内容
    }
    
    //创建客户端管理者
    var manager = ClientManager{
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
        clients:    make(map[string]*Client),
    }
    
    func (manager *ClientManager) start() {
        for {
            select {
            case conn := <-manager.register:
                manager.clients[conn.id] = conn
                //把返回连接成功的消息json格式化
                jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected. " + conn.ip, Sender: conn.id})
                //manager.send(jsonMessage)
                syncProducer(jsonMessage)
                //如果连接断开了
            case conn := <-manager.unregister:
                //判断连接的状态,如果是true,就关闭send,删除连接client的值
                if _, ok := manager.clients[conn.id]; ok {
                    close(conn.send)
                    delete(manager.clients, conn.id)
                    jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected. " + conn.ip, Sender: conn.id})
                    //manager.send(jsonMessage)
                    syncProducer(jsonMessage)
                }
                //广播
            case message := <-manager.broadcast:
                manager.send(message)
            }
        }
    }
    
    //定义客户端管理的send方法
    func (manager *ClientManager) send(message []byte) {
        obj := &Message{}
        _ = json.Unmarshal(message, obj)
        for id, conn := range manager.clients {
            if obj.Sender == id {
                //continue
            }
            if obj.Recipient == conn.id || len(obj.Recipient) < 1 {
                conn.send <- message
            }
        }
    }
    
    //定义客户端结构体的read方法
    func (c *Client) read() {
        defer func() {
            manager.unregister <- c
            _ = c.socket.Close()
        }()
    
        for {
            //读取消息
            _, str, err := c.socket.ReadMessage()
            //如果有错误信息,就注销这个连接然后关闭
            if err != nil {
                manager.unregister <- c
                _ = c.socket.Close()
                break
            }
            //如果没有错误信息就把信息放入broadcast
            message := &Message{}
            _ = json.Unmarshal(str, message)
            message.Sender = c.id
            jsonMessage, _ := json.Marshal(&message)
            fmt.Println(fmt.Sprintf("read Id:%s, msg:%s", c.id, string(jsonMessage)))
            //manager.broadcast <- jsonMessage
            syncProducer(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)
                fmt.Println(fmt.Sprintf("write Id:%s, msg:%s", c.id, string(message)))
            }
        }
    }
    
    func main() {
        fmt.Println("Starting application...")
        //开一个goroutine执行开始程序
        go manager.start()
        initial()
        //注册默认路由为 /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), ip: LocalIp()}
        //注册一个新的链接
        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
    }
    
    /////kafka
    
    var topic = "chat"
    var producer sarama.SyncProducer
    
    func initial() {
        config := sarama.NewConfig()
        // Version 必须大于等于  V0_10_2_0
        config.Version = sarama.V0_10_2_1
        config.Consumer.Return.Errors = true
        fmt.Println("start connect kafka")
        // 开始连接kafka服务器
        address := []string{"192.168.100.30:9092"}
        client, err := sarama.NewClient(address, config)
        if err != nil {
            fmt.Println("connect kafka failed; err", err)
            return
        }
    
        groupId := LocalIp()
        group, err := sarama.NewConsumerGroupFromClient(groupId, client)
        if err != nil {
            fmt.Println("connect kafka failed; err", err)
            return
        }
        go ConsumerGroup(group, []string{topic})
    
        config = sarama.NewConfig()
        // 等待服务器所有副本都保存成功后的响应
        config.Producer.RequiredAcks = sarama.WaitForAll
        // 随机的分区类型:返回一个分区器,该分区器每次选择一个随机分区
        config.Producer.Partitioner = sarama.NewRandomPartitioner
        // 是否等待成功和失败后的响应
        config.Producer.Return.Successes = true
        config.Producer.Timeout = 5 * time.Second
        producer, err = sarama.NewSyncProducer(address, config)
        if err != nil {
            log.Printf("sarama.NewSyncProducer err, message=%s \n", err)
        }
    }
    
    //同生产步消息模式
    func syncProducer(data []byte) {
        msg := &sarama.ProducerMessage{
            Topic:     topic,
            Partition: 0,
            Value:     sarama.ByteEncoder(data),
        }
        partition, offset, err := producer.SendMessage(msg)
        if err != nil {
            fmt.Println(fmt.Sprintf("Send message Fail %v", err))
        }
        fmt.Printf("send message success topic=%s Partition = %d, offset=%d content:=%s \n", topic, partition, offset ,string(data))
    }
    
    func ConsumerGroup(group sarama.ConsumerGroup, topics []string) {
        // 检查错误
        go func() {
            for err := range group.Errors() {
                fmt.Println("group errors : ", err)
            }
        }()
        ctx := context.Background()
        fmt.Println("start get msg")
        // for 是应对 consumer rebalance
        for {
            // 需要监听的主题
            handler := ConsumerGroupHandler{}
            // 启动kafka消费组模式,消费的逻辑在上面的 ConsumeClaim 这个方法里
            err := group.Consume(ctx, topics, handler)
    
            if err != nil {
                fmt.Println("consume failed; err : ", err)
                return
            }
        }
    }
    
    type ConsumerGroupHandler struct{}
    
    func (ConsumerGroupHandler) Setup(sess sarama.ConsumerGroupSession) error {
        //sess.MarkOffset(topic, 0, 0, "")
        return nil
    }
    
    func (ConsumerGroupHandler) Cleanup(sess sarama.ConsumerGroupSession) error {
        return nil
    }
    
    // 这个方法用来消费消息的
    func (h ConsumerGroupHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
        // 获取消息
        for msg := range claim.Messages() {
            fmt.Printf("kafka receive message %s---Partition:%d, Offset:%d, Key:%s, Value:%s\n", msg.Topic, msg.Partition, msg.Offset, string(msg.Key), string(msg.Value))
            //发送消息
            manager.send(msg.Value)
    
            // 将消息标记为已使用
            sess.MarkMessage(msg, "")
            sess.Commit()
        }
    
        return nil
    }

    客服端:

    <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");
                var recipient =$("#recipient")
                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;
                    }
                    content= JSON.stringify({"content":msg.val(),"recipient" :recipient.val()})
                    conn.send(content);
                    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">
           <label>接受者<label><input type="text" id ="recipient"/> <input type="submit" value="发送" /> <input type="text" id="msg" size="64"/>
    </form>
    </body>
    </html>

     运行效果:

    windows技术爱好者
  • 相关阅读:
    5.7填数字游戏求解
    5.6判断回文数字
    5.5百钱买百鸡问题
    5.4三色球问题
    5.3哥德巴赫猜想的近似证明
    5.2求两个数的最大公约数和最小公倍数
    5.1舍罕王的失算
    4.19递归反向输出字符串
    Elasticsearch 安装
    linux 安装nginx步骤
  • 原文地址:https://www.cnblogs.com/majiang/p/15781258.html
Copyright © 2020-2023  润新知