📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第40篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包
- 并发编程(五):并发模式
- 并发编程(六):原子操作与内存模型
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket 👈 当前位置
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控
- 第三阶段项目实战:微服务聊天应用
📖 文章导读
在本文中,您将了解:
- WebSocket协议的基本原理与HTTP的区别
- 如何在Go中建立和管理WebSocket连接
- 实现WebSocket消息的发送、接收和处理
- 构建广播系统实现多客户端实时通信
- 心跳机制的实现确保连接稳定性
- 完整实现一个基于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协议有以下局限性:
- 单向通信:客户端发起请求,服务器响应,服务器不能主动推送数据
- 连接不持久:每次请求都需要建立新的TCP连接(虽然HTTP/1.1引入了Keep-Alive,但本质上仍是请求-响应模式)
- 头部开销大:HTTP请求和响应都包含大量头部信息
相比之下,WebSocket提供以下优势:
- 双向通信:服务器可以主动向客户端推送数据
- 持久连接:一次握手后保持TCP连接,减少连接建立的开销
- 较低的延迟:没有HTTP请求的额外开销,适合实时数据传输
- 更高效的数据传输:相比HTTP轮询,大大减少了数据传输量
1.3 WebSocket工作原理
WebSocket连接的建立过程如下:
-
握手阶段:客户端发起HTTP请求,请求升级到WebSocket协议
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 -
服务器响应:如果服务器支持WebSocket,则返回升级确认
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= -
建立连接:握手成功后,HTTP连接升级为WebSocket连接,此后的通信遵循WebSocket协议
-
数据传输:使用帧(frames)进行数据传输,帧可以是文本帧或二进制帧
-
关闭连接:任何一方都可以发送关闭帧来关闭连接
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 设计广播系统
我们将设计一个广播系统,包含以下组件:
- 客户端结构体:用于存储每个WebSocket连接和相关信息
- 消息队列:用于存储要广播的消息
- 客户端管理器:用于管理所有已连接的客户端
下面是一个简单的实现:
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.

最低0.47元/天 解锁文章
1551

被折叠的 条评论
为什么被折叠?



