【Go语言学习系列40】Web开发(五):WebSocket

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第40篇,当前位于第三阶段(进阶篇)

🚀 第三阶段:进阶篇
  1. 并发编程(一):goroutine基础
  2. 并发编程(二):channel基础
  3. 并发编程(三):select语句
  4. 并发编程(四):sync包
  5. 并发编程(五):并发模式
  6. 并发编程(六):原子操作与内存模型
  7. 数据库编程(一):SQL接口
  8. 数据库编程(二):ORM技术
  9. Web开发(一):路由与中间件
  10. Web开发(二):模板与静态资源
  11. Web开发(三):API开发
  12. Web开发(四):认证与授权
  13. Web开发(五):WebSocket 👈 当前位置
  14. 微服务(一):基础概念
  15. 微服务(二):gRPC入门
  16. 日志与监控
  17. 第三阶段项目实战:微服务聊天应用

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • WebSocket协议的基本原理与HTTP的区别
  • 如何在Go中建立和管理WebSocket连接
  • 实现WebSocket消息的发送、接收和处理
  • 构建广播系统实现多客户端实时通信
  • 心跳机制的实现确保连接稳定性
  • 完整实现一个基于WebSocket的实时聊天应用

Go WebSocket开发

Web开发(五):WebSocket

在前面的Web开发系列中,我们已经讨论了路由、中间件、模板渲染、API开发以及认证授权等技术。然而,这些技术主要用于构建传统的请求-响应模式的Web应用。现代Web应用通常需要更加实时的交互体验,例如聊天应用、协作工具或在线游戏等,这些场景都需要服务器能够主动推送数据给客户端。WebSocket正是为此类需求设计的协议,它实现了浏览器和服务器之间的全双工通信。

本文将详细介绍在Go语言中如何使用WebSocket,从基本概念到实际应用,帮助你掌握实时Web应用开发的核心技能。

1. WebSocket基础概念

1.1 WebSocket协议简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

WebSocket在2011年被IETF标准化为RFC 6455,并由W3C定义了对应的JavaScript API。

1.2 WebSocket vs HTTP

传统的HTTP协议有以下局限性:

  1. 单向通信:客户端发起请求,服务器响应,服务器不能主动推送数据
  2. 连接不持久:每次请求都需要建立新的TCP连接(虽然HTTP/1.1引入了Keep-Alive,但本质上仍是请求-响应模式)
  3. 头部开销大:HTTP请求和响应都包含大量头部信息

相比之下,WebSocket提供以下优势:

  1. 双向通信:服务器可以主动向客户端推送数据
  2. 持久连接:一次握手后保持TCP连接,减少连接建立的开销
  3. 较低的延迟:没有HTTP请求的额外开销,适合实时数据传输
  4. 更高效的数据传输:相比HTTP轮询,大大减少了数据传输量

1.3 WebSocket工作原理

WebSocket连接的建立过程如下:

  1. 握手阶段:客户端发起HTTP请求,请求升级到WebSocket协议

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13
    
  2. 服务器响应:如果服务器支持WebSocket,则返回升级确认

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    
  3. 建立连接:握手成功后,HTTP连接升级为WebSocket连接,此后的通信遵循WebSocket协议

  4. 数据传输:使用帧(frames)进行数据传输,帧可以是文本帧或二进制帧

  5. 关闭连接:任何一方都可以发送关闭帧来关闭连接

1.4 WebSocket应用场景

WebSocket特别适合以下场景:

  • 聊天应用:用户间的实时消息传递
  • 协作工具:多用户同时编辑文档、白板等
  • 实时游戏:玩家间的实时交互
  • 实时监控:数据仪表盘实时更新
  • 股票行情:价格实时更新
  • 体育赛事:比分实时显示
  • 位置服务:实时位置追踪

2. Go语言中的WebSocket实现

在Go语言中,有多个WebSocket库可供选择,最常用的是Gorilla WebSocket库。接下来,我们将使用这个库实现WebSocket功能。

2.1 安装Gorilla WebSocket

首先,安装Gorilla WebSocket库:

go get github.com/gorilla/websocket

2.2 创建WebSocket服务器

下面是一个简单的WebSocket服务器示例:

package main

import (
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

// 配置WebSocket升级器
var upgrader = websocket.Upgrader{
   
   
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    // 允许所有CORS请求,生产环境应当配置更严格的检查
    CheckOrigin: func(r *http.Request) bool {
   
   
        return true
    },
}

// WebSocket处理器
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
   
   
    // 将HTTP连接升级为WebSocket连接
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
   
   
        log.Println("升级连接失败:", err)
        return
    }
    defer conn.Close()
    
    log.Println("客户端已连接:", conn.RemoteAddr())
    
    // 无限循环,持续读取客户端消息
    for {
   
   
        // 读取消息
        messageType, message, err := conn.ReadMessage()
        if err != nil {
   
   
            log.Println("读取消息错误:", err)
            break
        }
        
        // 打印收到的消息
        log.Printf("收到消息: %s", message)
        
        // 简单地将消息回送给客户端
        err = conn.WriteMessage(messageType, message)
        if err != nil {
   
   
            log.Println("写入消息错误:", err)
            break
        }
    }
}

func main() {
   
   
    http.HandleFunc("/ws", handleWebSocket)
    
    // 提供一个简单的客户端HTML页面
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   
   
        http.ServeFile(w, r, "websocket.html")
    })
    
    log.Println("服务器启动在 :8080 端口")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

2.3 创建WebSocket客户端页面

创建一个简单的HTML客户端(websocket.html):

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket测试</title>
    <style>
        body {
     
     
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        #messages {
     
     
            height: 300px;
            border: 1px solid #ccc;
            margin-bottom: 10px;
            padding: 10px;
            overflow-y: auto;
        }
        input[type="text"] {
     
     
            width: 70%;
            padding: 8px;
        }
        button {
     
     
            padding: 8px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>WebSocket测试</h1>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="输入消息">
    <button onclick="sendMessage()">发送</button>
    
    <script>
        var socket;
        var messagesDiv = document.getElementById('messages');
        var messageInput = document.getElementById('messageInput');
        
        // 建立WebSocket连接
        function connect() {
     
     
            // 创建一个WebSocket连接
            socket = new WebSocket('ws://' + window.location.host + '/ws');
            
            // 连接打开时的处理
            socket.onopen = function(event) {
     
     
                addMessage('系统', '连接已建立');
            };
            
            // 收到消息时的处理
            socket.onmessage = function(event) {
     
     
                addMessage('服务器', event.data);
            };
            
            // 连接关闭时的处理
            socket.onclose = function(event) {
     
     
                addMessage('系统', '连接已关闭');
                // 尝试重连
                setTimeout(function() {
     
     
                    connect();
                }, 2000);
            };
            
            // 发生错误时的处理
            socket.onerror = function(error) {
     
     
                addMessage('错误', '发生错误');
                console.error('WebSocket错误:', error);
            };
        }
        
        // 发送消息
        function sendMessage() {
     
     
            if (!socket || socket.readyState !== WebSocket.OPEN) {
     
     
                addMessage('错误', '连接未建立');
                return;
            }
            
            var message = messageInput.value;
            if (message) {
     
     
                socket.send(message);
                addMessage('我', message);
                messageInput.value = '';
            }
        }
        
        // 在消息区域添加一条消息
        function addMessage(sender, message) {
     
     
            var messageElement = document.createElement('div');
            messageElement.textContent = sender + ': ' + message;
            messagesDiv.appendChild(messageElement);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
        
        // 回车键发送消息
        messageInput.addEventListener('keyup', function(event) {
     
     
            if (event.key === 'Enter') {
     
     
                sendMessage();
            }
        });
        
        // 页面加载完成后建立连接
        window.onload = connect;
    </script>
</body>
</html>

这个简单的例子实现了客户端和服务器之间的实时消息交换。接下来,我们将深入探讨更高级的WebSocket功能和模式。

3. WebSocket消息广播系统

在许多WebSocket应用中,我们不仅需要处理单个客户端的连接,还需要向多个客户端广播消息。例如,在聊天室应用中,当一个用户发送消息时,所有在线用户都应该收到这条消息。接下来,我们将实现一个简单的广播系统。

3.1 设计广播系统

我们将设计一个广播系统,包含以下组件:

  1. 客户端结构体:用于存储每个WebSocket连接和相关信息
  2. 消息队列:用于存储要广播的消息
  3. 客户端管理器:用于管理所有已连接的客户端

下面是一个简单的实现:

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"

    "github.com/gorilla/websocket"
)

// 配置WebSocket升级器
var upgrader = websocket.Upgrader{
   
   
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
   
   
        return true
    },
}

// 定义消息结构
type Message struct {
   
   
    Type    string      `json:"type"`
    Content string      `json:"content"`
    Sender  string      `json:"sender"`
    Time    time.Time   `json:"time"`
}

// 客户端结构体
type Client struct {
   
   
    ID       string
    Conn     *websocket.Conn
    Send     chan []byte
    Hub      *Hub
    Username string
}

// Hub管理所有已连接的客户端
type Hub struct {
   
   
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
    mutex      sync.Mutex
}

// 创建新的Hub
func newHub() *Hub {
   
   
    return &Hub{
   
   
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
        clients:    make(map[*Client]bool),
    }
}

// Hub运行方法,管理客户端连接和消息广播
func (h *Hub) run() {
   
   
    for {
   
   
        select {
   
   
        case client := <-h.register:
            h.mutex.Lock()
            h.clients[client] = true
            h.mutex.Unlock()
            
            // 通知所有客户端有新用户加入
            joinMsg := Message{
   
   
                Type:    "system",
                Content: client.Username + " 加入了聊天",
                Time:    time.Now(),
            }
            msgBytes, _ := json.Marshal(joinMsg)
            h.broadcast <- msgBytes
            
        case client := <-h.unregister:
            h.mutex.Lock()
            if _, ok := h.clients[client]; ok {
   
   
                delete(h.clients, client)
                close(client.Send)
                
                // 通知所有客户端有用户离开
                leaveMsg := Message{
   
   
                    Type:    "system",
                    Content: client.Username + " 离开了聊天",
                    Time:    time.Now(),
                }
                msgBytes, _ := json.Marshal(leaveMsg)
                h.broadcast <- msgBytes
            }
            h.mutex.Unlock()
            
        case message := <-h.broadcast:
            h.mutex.Lock()
            for client := range h.clients {
   
   
                select {
   
   
                case client.Send <- message:
                    // 消息发送成功
                default:
                    // 客户端已无法接收消息
                    close(client.Send)
                    delete(h.clients, client)
                }
            }
            h.mutex.Unlock()
        }
    }
}

// 客户端读取消息的goroutine
func (c *Client) readPump() {
   
   
    defer func() {
   
   
        c.Hub.unregister <- c
        c.Conn.Close()
    }()
    
    // 设置最大消息大小
    c.Conn.SetReadLimit(512)
    
    // 设置读取超时
    c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    c.Conn.SetPongHandler(func(string) error {
   
    
        c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
        return nil 
    })
    
    // 持续读取客户端消息
    for {
   
   
        _, rawMessage, err := c.Conn.ReadMessage()
        if err != nil {
   
   
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
   
   
                log.Printf("error: %v", err)
            }
            break
        }
        
        // 创建新消息并广播
        message := Message{
   
   
            Type:    "message",
            Content: string(rawMessage),
            Sender:  c.Username,
            Time:    time.Now(),
        }
        
        messageBytes, _ := json.Marshal(message)
        c.Hub.broadcast <- messageBytes
    }
}

// 客户端写入消息的goroutine
func (c *Client) writePump() {
   
   
    ticker := time.NewTicker(54 * time.Second)
    defer func() {
   
   
        ticker.Stop()
        c.Conn.Close()
    }()
    
    for {
   
   
        select {
   
   
        case message, ok := <-c.Send:
            // 设置写入超时
            c.Conn.SetWriteDeadline(time.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值