WebSocket #
一、WebSocket概述 #
WebSocket是一种在单个TCP连接上进行全双工通信的协议。
1.1 WebSocket特点 #
- 全双工通信
- 低延迟
- 服务端主动推送
- 保持连接
1.2 应用场景 #
- 实时聊天
- 在线协作
- 实时数据推送
- 游戏
- 股票行情
二、基本使用 #
2.1 安装gorilla/websocket #
bash
go get github.com/gorilla/websocket
2.2 WebSocket升级 #
go
package main
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func main() {
e := echo.New()
e.GET("/ws", func(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
for {
messageType, message, err := ws.ReadMessage()
if err != nil {
break
}
err = ws.WriteMessage(messageType, message)
if err != nil {
break
}
}
return nil
})
e.Logger.Fatal(e.Start(":8080"))
}
2.3 客户端示例 #
html
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Demo</title>
</head>
<body>
<script>
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = function() {
console.log('Connected');
ws.send('Hello, Server!');
};
ws.onmessage = function(event) {
console.log('Received:', event.data);
};
ws.onclose = function() {
console.log('Disconnected');
};
ws.onerror = function(error) {
console.error('Error:', error);
};
</script>
</body>
</html>
三、消息类型 #
3.1 文本消息 #
go
ws.WriteMessage(websocket.TextMessage, []byte("Hello"))
3.2 二进制消息 #
go
ws.WriteMessage(websocket.BinaryMessage, []byte{0x01, 0x02, 0x03})
3.3 JSON消息 #
go
type Message struct {
Type string `json:"type"`
Content interface{} `json:"content"`
}
func sendJSON(ws *websocket.Conn, msg Message) error {
data, err := json.Marshal(msg)
if err != nil {
return err
}
return ws.WriteMessage(websocket.TextMessage, data)
}
func readJSON(ws *websocket.Conn) (*Message, error) {
_, data, err := ws.ReadMessage()
if err != nil {
return nil, err
}
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
return nil, err
}
return &msg, nil
}
四、连接管理 #
4.1 客户端结构 #
go
type Client struct {
ID string
Conn *websocket.Conn
Send chan []byte
}
func NewClient(conn *websocket.Conn) *Client {
return &Client{
ID: uuid.New().String(),
Conn: conn,
Send: make(chan []byte, 256),
}
}
func (c *Client) Read() {
defer c.Conn.Close()
for {
_, message, err := c.Conn.ReadMessage()
if err != nil {
break
}
handleMessage(c, message)
}
}
func (c *Client) Write() {
defer c.Conn.Close()
for message := range c.Send {
err := c.Conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
break
}
}
}
4.2 Hub中心 #
go
type Hub struct {
Clients map[*Client]bool
Broadcast chan []byte
Register chan *Client
Unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
Clients: make(map[*Client]bool),
Broadcast: make(chan []byte),
Register: make(chan *Client),
Unregister: make(chan *Client),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
close(client.Send)
}
case message := <-h.Broadcast:
for client := range h.Clients {
select {
case client.Send <- message:
default:
close(client.Send)
delete(h.Clients, client)
}
}
}
}
}
五、聊天室实现 #
5.1 完整示例 #
go
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Message struct {
Type string `json:"type"`
Sender string `json:"sender"`
Content string `json:"content"`
Timestamp int64 `json:"timestamp"`
}
type Client struct {
ID string
Name string
Conn *websocket.Conn
Room *Room
Send chan []byte
}
type Room struct {
ID string
Name string
Clients map[*Client]bool
Mutex sync.RWMutex
}
type ChatServer struct {
Rooms map[string]*Room
Mutex sync.RWMutex
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func NewChatServer() *ChatServer {
return &ChatServer{
Rooms: make(map[string]*Room),
}
}
func (s *ChatServer) GetOrCreateRoom(roomID string) *Room {
s.Mutex.Lock()
defer s.Mutex.Unlock()
if room, ok := s.Rooms[roomID]; ok {
return room
}
room := &Room{
ID: roomID,
Name: "Room " + roomID,
Clients: make(map[*Client]bool),
}
s.Rooms[roomID] = room
return room
}
func (r *Room) AddClient(client *Client) {
r.Mutex.Lock()
r.Clients[client] = true
r.Mutex.Unlock()
}
func (r *Room) RemoveClient(client *Client) {
r.Mutex.Lock()
delete(r.Clients, client)
r.Mutex.Unlock()
}
func (r *Room) Broadcast(message []byte, sender *Client) {
r.Mutex.RLock()
defer r.Mutex.RUnlock()
for client := range r.Clients {
if client != sender {
select {
case client.Send <- message:
default:
close(client.Send)
delete(r.Clients, client)
}
}
}
}
func (c *Client) ReadPump() {
defer func() {
c.Room.RemoveClient(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 {
_, message, err := c.Conn.ReadMessage()
if err != nil {
break
}
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
continue
}
msg.Sender = c.Name
msg.Timestamp = time.Now().Unix()
data, _ := json.Marshal(msg)
c.Room.Broadcast(data, c)
}
}
func (c *Client) WritePump() {
ticker := time.NewTicker(30 * time.Second)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.Conn.WriteMessage(websocket.TextMessage, message)
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
server := NewChatServer()
e.GET("/ws/:room", func(c echo.Context) error {
roomID := c.Param("room")
name := c.QueryParam("name")
if name == "" {
name = "Anonymous"
}
conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
room := server.GetOrCreateRoom(roomID)
client := &Client{
ID: time.Now().String(),
Name: name,
Conn: conn,
Room: room,
Send: make(chan []byte, 256),
}
room.AddClient(client)
joinMsg := Message{
Type: "join",
Sender: "System",
Content: name + " joined the room",
Timestamp: time.Now().Unix(),
}
data, _ := json.Marshal(joinMsg)
room.Broadcast(data, nil)
go client.WritePump()
go client.ReadPump()
return nil
})
e.Static("/", "static")
e.Logger.Fatal(e.Start(":8080"))
}
5.2 客户端页面 #
html
<!DOCTYPE html>
<html>
<head>
<title>Chat Room</title>
<style>
#messages { height: 400px; overflow-y: scroll; border: 1px solid #ccc; }
#message { width: 80%; }
</style>
</head>
<body>
<div id="messages"></div>
<input type="text" id="message" placeholder="Type a message...">
<button onclick="send()">Send</button>
<script>
const room = 'default';
const name = prompt('Enter your name:') || 'Anonymous';
const ws = new WebSocket(`ws://localhost:8080/ws/${room}?name=${name}`);
ws.onmessage = function(event) {
const msg = JSON.parse(event.data);
const div = document.createElement('div');
div.textContent = `${msg.sender}: ${msg.content}`;
document.getElementById('messages').appendChild(div);
document.getElementById('messages').scrollTop = 999999;
};
function send() {
const input = document.getElementById('message');
const msg = {
type: 'message',
content: input.value
};
ws.send(JSON.stringify(msg));
input.value = '';
}
document.getElementById('message').addEventListener('keypress', function(e) {
if (e.key === 'Enter') send();
});
</script>
</body>
</html>
六、总结 #
WebSocket要点:
| 要点 | 说明 |
|---|---|
| Upgrader | HTTP升级到WebSocket |
| ReadMessage | 读取消息 |
| WriteMessage | 发送消息 |
| Ping/Pong | 心跳检测 |
| Hub | 连接管理中心 |
| Room | 房间管理 |
准备好学习性能优化了吗?让我们进入下一章!
最后更新:2026-03-28