• Go 快速起步:创建 WebSocket 服务器(聊天室)


    先了解 WebSocket 协议

            和 HTTP 协议一样,WebSocket  协议也建立在 TCP/IP 协议基础上,但不一样的是 HTTP 协议 为单向协议,即只能客户端向服务器请求资源,服务器才能将数据传送给浏览器。 

            而 WebSocket 协议 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/UA 都能主动的向对方发送或接收数据。

            WebSocket 可以认为是与 HTTP 属于同一个层级的兄弟协议,但实际上它又有些依赖 HTTP 协议,因为 WebSocket 的初始连接是通过 HTTP 协议的三次握手实现,完成连接之后再向服务器请求升级成为 websocket 协议;

    简单即时聊天室的实现思路

    1. 引入了 github.com/gorilla/websocket 包,先创建 http 服务器,然后升级为 websocket 服务器;
    2. 当用户打开聊天页面,则建立连接,并将用户信息推送给ws服务端,服务端注册该用户到连接池;
    3. 用户点击发送消息时,循环用户连接池拿到用户的连接,主动将消息推送给每个用户;
    4. 这个思路比较简单,只通过 ReadMessage 接口通讯,所以需要为每个消息约定一下消息类型,比如注册、注销、消息);

    简单版的 serv.go

    1. package main
    2. // go get -u -v github.com/gorilla/websocket 安装第三方包
    3. import (
    4. "encoding/json"
    5. "fmt"
    6. "github.com/gorilla/websocket"
    7. "net/http"
    8. )
    9. func main() {
    10. // http请求,跳转到一个简单的前端页面
    11. http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
    12. http.ServeFile(writer, request, "home.html")
    13. })
    14. // http请求,连接成功后申请升级为websocket服务
    15. http.HandleFunc("/ws", indexHandler)
    16. err := http.ListenAndServe("0.0.0.0:8888", nil)
    17. if err != nil {
    18. return
    19. }
    20. }
    21. // 消息体
    22. type replyMsg struct {
    23. Type string `json:"type,omitempty"`
    24. Uid string `json:"uid,omitempty"`
    25. Msg string `json:"msg,omitempty"`
    26. }
    27. var (
    28. upgrader = websocket.Upgrader{
    29. // 允许所有CORS跨域请求
    30. CheckOrigin: func(r *http.Request) bool {
    31. return true
    32. },
    33. }
    34. // 用户列表
    35. userMaps = make(map[string]*websocket.Conn)
    36. )
    37. func indexHandler(w http.ResponseWriter, r *http.Request) {
    38. var (
    39. replyMsg replyMsg
    40. conn *websocket.Conn
    41. err error
    42. data []byte
    43. )
    44. // 主动升级成websocket( Upgrade:websocket)
    45. if conn, err = upgrader.Upgrade(w, r, nil); err != nil {
    46. return
    47. }
    48. for {
    49. // 监听新消息请求
    50. if _, data, err = conn.ReadMessage(); err != nil {
    51. goto Err
    52. }
    53. // json -> 结构体
    54. err := json.Unmarshal([]byte(data), &replyMsg)
    55. if err != nil {
    56. goto Err
    57. }
    58. fmt.Println(replyMsg.Type)
    59. switch {
    60. // 监听加入群聊的请求
    61. case replyMsg.Type == "register" && replyMsg.Uid != "":
    62. // 注册用用户集合中
    63. userMaps[replyMsg.Uid] = conn
    64. // 监听用户关闭页面
    65. case replyMsg.Type == "logout" && replyMsg.Uid != "":
    66. // 剔除群聊
    67. delete(userMaps, replyMsg.Uid)
    68. case replyMsg.Type == "message":
    69. }
    70. // 循环推送消息给每个用户
    71. for _, v := range userMaps {
    72. err = v.WriteMessage(websocket.TextMessage, data)
    73. if err != nil {
    74. goto Err
    75. }
    76. }
    77. }
    78. Err:
    79. conn.Close()
    80. }

    并发即时聊天室的实现思路

    1. 引入了 github.com/gorilla/websocket 包,先创建 http 服务器,然后升级为 websocket 服务器;
    2. 启动服务时,马上起一个协程作为处理中心,监听注册、注销、消息 3 个总的 channel(是的,通过channel通信);
    3. 当用户打开聊天页面,则建立连接,并新建一个 Client 实例,将实例指针推给处理中心的注册 channel 后,自己再起两个协程监听读、写操作;
    4. 用户点击发送消息时,自己起的读协程将消息推给处理中心的广播 channel,循环将消息推给每个 client 的写 channel,对应 client  的写通道最将消息主动推给每个用户;
    5. 这个思路比较复杂,但主要的抽象出来一个  Client 充当 客户端 和 websocket处理中心 的中介,每个 Client 都运行两个 goroutine :
        5.1 读,循环监听是否该用户是否要发言,有则推给 websocket  的广播渠道 推给每个客户端
        5.2 写,循环读取 websocket 是否有新消息,有则主动推送消息给客户端

    升级版的 serv.go

    1. package main
    2. // go get -u -v github.com/gorilla/websocket --安装官方的websocket包
    3. import (
    4. "github.com/gorilla/websocket"
    5. "log"
    6. "net/http"
    7. )
    8. var (
    9. // 升级成 WebSocket 协议
    10. upgrader = websocket.Upgrader{
    11. // 允许CORS跨域请求
    12. CheckOrigin: func(r *http.Request) bool {
    13. return true
    14. },
    15. }
    16. conn *websocket.Conn
    17. err error
    18. )
    19. // CenterHandler 处理中心,关联着每个 Client 的注册、注销、广播通道,相当于每个用户的中心通讯的中介。
    20. type CenterHandler struct {
    21. // 广播通道,有数据则循环每个用户广播出去
    22. broadcast chan []byte
    23. // 注册通道,有用户进来 则推到用户集合map中
    24. register chan *Client
    25. // 注销通道,有用户关闭连接 则将该用户剔出集合map中
    26. unregister chan *Client
    27. // 用户集合,每个用户本身也在跑两个协程,监听用户的读、写的状态
    28. clients map[*Client]bool
    29. }
    30. // 处理中心的一个接口,监控状态
    31. func (ch *CenterHandler) monitoring() {
    32. for {
    33. select {
    34. // 注册,新用户连接过来会推进注册通道,这里接收推进来的用户指针
    35. case client := <-ch.register:
    36. ch.clients[client] = true
    37. // 注销,关闭连接或连接异常会将用户推出群聊
    38. case client := <-ch.unregister:
    39. delete(ch.clients, client)
    40. // 消息,监听到有新消息到来
    41. case message := <-ch.broadcast:
    42. println("消息来了,message:" + string(message))
    43. // 推送给每个用户的通道,每个用户都有跑协程起了writePump的监听
    44. for client := range ch.clients {
    45. client.send <- message
    46. }
    47. }
    48. }
    49. }
    50. // Client 抽象出来的 Client,里面有这个 websocket 连接的 读 和 写 操作
    51. type Client struct {
    52. handler *CenterHandler
    53. conn *websocket.Conn
    54. // 每个用户自己的循环跑起来的状态监控
    55. send chan []byte
    56. }
    57. // 写,主动推送消息给客户端
    58. func (c *Client) writePump() {
    59. defer func() {
    60. c.handler.unregister <- c
    61. c.conn.Close()
    62. }()
    63. for {
    64. // 广播推过来的新消息,马上通过websocket推给自己
    65. message, _ := <-c.send
    66. if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
    67. return
    68. }
    69. }
    70. }
    71. // 读,监听客户端是否有推送内容过来服务端
    72. func (c *Client) readPump() {
    73. defer func() {
    74. c.handler.unregister <- c
    75. c.conn.Close()
    76. }()
    77. for {
    78. // 循环监听是否该用户是否要发言
    79. _, message, err := c.conn.ReadMessage()
    80. if err != nil {
    81. // 异常关闭的处理
    82. if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
    83. log.Printf("error: %v", err)
    84. }
    85. break
    86. }
    87. // 要的话,推给广播中心,广播中心再推给每个用户
    88. c.handler.broadcast <- message
    89. }
    90. }
    91. func main() {
    92. // 应用一运行,就初始化 CenterHandler 处理中心对象
    93. handler := CenterHandler{
    94. broadcast: make(chan []byte),
    95. register: make(chan *Client),
    96. unregister: make(chan *Client),
    97. clients: make(map[*Client]bool),
    98. }
    99. // 起个协程跑起来,监听注册、注销、消息 3 个 channel
    100. go handler.monitoring()
    101. // http请求,跳转到一个简单的前端页面
    102. http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
    103. http.ServeFile(writer, request, "home.html")
    104. })
    105. // websocket 请求,建立双工通讯连接
    106. http.HandleFunc("/ws", func(writer http.ResponseWriter, request *http.Request) {
    107. // 由 http 升级成为 websocket 服务
    108. if conn, err = upgrader.Upgrade(writer, request, nil); err != nil {
    109. log.Println(err)
    110. return
    111. }
    112. // 为每个连接创建一个 Client 实例,(实际上这里应该还有绑定用户真实信息的操作)
    113. client := &Client{&handler, conn, make(chan []byte, 256)}
    114. // 推给监控中心注册到用户集合中
    115. handler.register <- client
    116. // 每个 client 都挂起 2 个新的协程,监控读、写状态
    117. go client.writePump()
    118. go client.readPump()
    119. })
    120. if err := http.ListenAndServe(":8888", nil); err != nil {
    121. log.Fatal("ListenAndServe:", err)
    122. }
    123. }

    最后,附上一个简单的前端文件(home.html)及效果截图

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    5. <title>聊天室-demo</title>
    6. <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    7. </head>
    8. <body>
    9. <h1>聊天室-demo</h1>
    10. <div class="show-data"></div>
    11. <input class="content">
    12. <button class="send-btn">发送</button>
    13. <script>
    14. // 测试用户
    15. var username = "用户_" + Math.floor(Math.random() * 10000);
    16. var ws_url = "ws://localhost:8888/ws";
    17. var ws = new WebSocket(ws_url);
    18. // 新连接
    19. ws.onopen = function (ev) {
    20. ws.send('{"type":"register","uid":"' + username + '","msg":" 上线了"}');
    21. };
    22. // 发送内容
    23. $(".send-btn").click(function () {
    24. var content = $(".content").val();
    25. content = '{"type":"msg","uid":"' + username + '","msg":"' + content + '"}'
    26. ws.send(content);
    27. $(".content").val("");
    28. });
    29. // 监听接收新消息
    30. ws.onmessage = function (ent) {
    31. var r = JSON.parse(ent.data);
    32. console.log(r)
    33. if(r.uid == username){
    34. r.uid = username + "(你)"
    35. }
    36. $(".show-data").append(r.uid + ":" + r.msg + "<br>");
    37. }
    38. // 退出页面
    39. ws.onclose = function (ent) {
    40. ws.send('{"type":"logout","uid":"' + username + '","msg":" 下线了"}');
    41. }
    42. </script>
    43. </body>
    44. </html>

    开两个窗户的即时聊天,体验比前端轮询好的多~

     

    >>>对比 && 总结 GatewayWorker 开发实时聊天功能的基本思路

    >>>对比 && 总结 Swoole 快速起步:创建 WebSocket 服务器(聊天室)

  • 相关阅读:
    【数据结构与算法】之深入解析“分割数组的最多方案数”的求解思路与算法示例
    CentOS单机安装k8s并部署.NET 6程序 压测 记录
    进程终止(你真的学会递归了吗?考验你的递归基础)
    【Leetcode】 第387场周赛 — 题解
    python 两个文件对比,以文件1为标准将文件2中有相等的内容整行取出
    gprof 分析程序执行时间和函数调用次数
    全同态加密知识体系整理
    已解决(Python最新xlrd库读取xlsx报错)SyntaxError: invalid syntax
    利用容器技术优化DevOps流程
    Excel表格数据导入MySQL数据库
  • 原文地址:https://blog.csdn.net/Phplayers/article/details/125471726