Socket和Websocket实践

Posted by     "" on Thursday, March 4, 2021

Socket 和 WebSocket 实践:从理论到代码的完全指南

引言

在当今的互联网应用中,实时通信已经成为一个不可或缺的特性。无论是聊天软件、在线协作工具,还是股票行情推送,背后都离不开 Socket 或 WebSocket 技术的支持。

然而,很多开发者对 Socket 和 WebSocket 的理解存在混淆——它们听起来相似,但实际上是不同层面的东西。本文将带你理清这两个概念,并通过实践代码让你真正掌握它们的用法。

一、概念辨析:Socket ≠ WebSocket

在深入实践之前,我们需要先建立一个清晰的认知:Socket 和 WebSocket 解决的是不同层级的问题

1.1 Socket:网络通信的基石

Socket(套接字)是操作系统提供的一个编程接口,它封装了 TCP/IP 协议栈的细节,让应用程序能够通过网络发送和接收数据。

可以把 Socket 想象成电话线的基础设施——它提供了通信的物理基础,但如何使用(说哪种语言、传输什么内容)完全由你决定。

Socket 的特点:

  • 是操作系统层面的 API
  • 支持 TCP 和 UDP 两种传输协议
  • 灵活性极高,可以自定义任何通信规则
  • 适用于任何编程语言和平台

1.2 WebSocket:Web 时代的双向通道

WebSocket 则是一个应用层协议,它建立在 TCP 之上,专门为 Web 应用设计。它的核心目标是:让浏览器和服务器之间建立一个可以双向实时通信的长连接

WebSocket 的特点:

  • 基于 HTTP 协议完成握手,然后升级为 WebSocket 协议
  • 全双工通信,客户端和服务器都可以主动发送消息
  • 保持长连接,避免频繁建立连接的开销
  • 浏览器原生支持,API 简单易用

1.3 一图看懂三者关系

为了更好地理解,这里用一个比喻来总结:

概念 类比 说明
Socket 高速公路 + 交通规则 提供通信的基础设施
WebSocket 快递专线 基于高速的高效双向通道
Socket.IO 顺丰快递 封装了重连、心跳等功能的库

二、Socket 编程实践(以 Python 为例)

Socket 编程是所有网络编程的基础。这里用 Python 的 socket 库来演示一个简单的 TCP 通信。

2.1 服务端实现

import socket
import threading

def handle_client(client_socket, address):
    """处理客户端连接"""
    print(f"新连接来自: {address}")
    
    while True:
        # 接收客户端消息(最多 1024 字节)
        data = client_socket.recv(1024)
        if not data:
            break
        
        message = data.decode('utf-8')
        print(f"收到消息: {message}")
        
        # 回复客户端
        response = f"服务器已收到: {message}"
        client_socket.send(response.encode('utf-8'))
    
    print(f"连接断开: {address}")
    client_socket.close()

def start_server(host='127.0.0.1', port=8888):
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((host, port))
    server_socket.listen(5)
    
    print(f"服务器启动,监听 {host}:{port}")
    
    while True:
        client_socket, address = server_socket.accept()
        # 为每个客户端创建新线程
        client_thread = threading.Thread(
            target=handle_client, 
            args=(client_socket, address)
        )
        client_thread.start()

if __name__ == "__main__":
    start_server()

2.2 客户端实现

import socket
import time

def start_client(host='127.0.0.1', port=8888):
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    print(f"已连接到服务器 {host}:{port}")
    
    for i in range(5):
        message = f"Hello {i}"
        client_socket.send(message.encode('utf-8'))
        
        response = client_socket.recv(1024)
        print(f"服务器回复: {response.decode('utf-8')}")
        
        time.sleep(1)
    
    client_socket.close()
    print("连接关闭")

if __name__ == "__main__":
    start_client()

2.3 运行效果

# 终端1 - 启动服务端
$ python server.py
服务器启动,监听 127.0.0.1:8888
新连接来自: ('127.0.0.1', 54321)
收到消息: Hello 0
收到消息: Hello 1
...

# 终端2 - 启动客户端
$ python client.py
已连接到服务器 127.0.0.1:8888
服务器回复: 服务器已收到: Hello 0
服务器回复: 服务器已收到: Hello 1
...

三、WebSocket 实践

WebSocket 在现代 Web 开发中应用广泛。下面分别用 Python 和 Go 来实现 WebSocket 服务。

3.1 使用 Python + websockets 库

安装依赖

pip install websockets

服务端实现

import asyncio
import websockets

async def handler(websocket):
    """处理 WebSocket 连接"""
    print("客户端已连接")
    
    try:
        async for message in websocket:
            print(f"收到消息: {message}")
            
            # 回复客户端
            response = f"服务器已收到: {message}"
            await websocket.send(response)
    except websockets.exceptions.ConnectionClosed:
        print("客户端已断开")

async def main():
    async with websockets.serve(handler, "localhost", 8000):
        print("WebSocket 服务器运行在 ws://localhost:8000")
        await asyncio.Future()  # 永久运行

if __name__ == "__main__":
    asyncio.run(main())

客户端实现

import asyncio
import websockets

async def client():
    uri = "ws://localhost:8000"
    async with websockets.connect(uri) as websocket:
        print("已连接到服务器")
        
        # 发送消息
        await websocket.send("Hello WebSocket!")
        print("消息已发送")
        
        # 接收回复
        response = await websocket.recv()
        print(f"收到回复: {response}")

if __name__ == "__main__":
    asyncio.run(client())

3.2 使用 Go + Gorilla WebSocket

Go 语言在并发处理方面有天然优势,非常适合实现 WebSocket 服务。

安装依赖

go get github.com/gorilla/websocket

服务端实现

package main

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

// 配置 WebSocket 升级器
var upgrader = websocket.Upgrader{
    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.Print("升级失败:", err)
        return
    }
    defer conn.Close()
    
    fmt.Println("客户端已连接")
    
    for {
        // 读取客户端消息
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("读取失败:", err)
            break
        }
        
        fmt.Printf("收到消息: %s\n", message)
        
        // 回复客户端
        response := fmt.Sprintf("服务器已收到: %s", message)
        err = conn.WriteMessage(messageType, []byte(response))
        if err != nil {
            log.Println("发送失败:", err)
            break
        }
    }
    
    fmt.Println("客户端已断开")
}

func main() {
    http.HandleFunc("/ws", handleWebSocket)
    
    fmt.Println("WebSocket 服务器启动在 ws://localhost:8080/ws")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

前端 HTML 客户端

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket 客户端</title>
</head>
<body>
    <h1>WebSocket 测试</h1>
    <input type="text" id="message" placeholder="输入消息">
    <button onclick="sendMessage()">发送</button>
    <div id="output"></div>
    
    <script>
        // 创建 WebSocket 连接
        const ws = new WebSocket('ws://localhost:8080/ws');
        
        ws.onopen = () => {
            console.log('已连接到服务器');
            log('连接成功');
        };
        
        ws.onmessage = (event) => {
            console.log('收到消息:', event.data);
            log(`收到: ${event.data}`);
        };
        
        ws.onerror = (error) => {
            console.error('WebSocket 错误:', error);
            log('发生错误');
        };
        
        ws.onclose = () => {
            console.log('连接已关闭');
            log('连接已关闭');
        };
        
        function sendMessage() {
            const input = document.getElementById('message');
            const message = input.value;
            
            if (message && ws.readyState === WebSocket.OPEN) {
                ws.send(message);
                log(`发送: ${message}`);
                input.value = '';
            }
        }
        
        function log(message) {
            const output = document.getElementById('output');
            output.innerHTML += `<div>${message}</div>`;
        }
    </script>
</body>
</html>

3.3 WebSocket 的握手过程

WebSocket 连接的建立有一个重要的环节:HTTP 协议升级。当客户端发起连接时,会发送这样一个请求:

GET /ws HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器如果支持 WebSocket,会返回:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这个握手过程完成后,连接就升级为 WebSocket 协议,之后就可以进行双向通信了。

四、进阶:实现一个简单的聊天室

将上面的知识综合起来,实现一个支持多人的聊天室。

4.1 Go 版本聊天室服务端

package main

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

type Client struct {
    conn *websocket.Conn
    send chan []byte
}

type Hub struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
    mu         sync.RWMutex
}

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.mu.Lock()
            h.clients[client] = true
            h.mu.Unlock()
            log.Println("客户端已注册")
            
        case client := <-h.unregister:
            h.mu.Lock()
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
            h.mu.Unlock()
            log.Println("客户端已注销")
            
        case message := <-h.broadcast:
            h.mu.RLock()
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
            h.mu.RUnlock()
        }
    }
}

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    
    client := &Client{
        conn: conn,
        send: make(chan []byte, 256),
    }
    hub.register <- client
    
    // 启动写协程
    go client.writePump()
    // 启动读协程
    go client.readPump(hub)
}

func (c *Client) readPump(hub *Hub) {
    defer func() {
        hub.unregister <- c
        c.conn.Close()
    }()
    
    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            break
        }
        hub.broadcast <- message
    }
}

func (c *Client) writePump() {
    defer c.conn.Close()
    
    for message := range c.send {
        err := c.conn.WriteMessage(websocket.TextMessage, message)
        if err != nil {
            break
        }
    }
}

func main() {
    hub := NewHub()
    go hub.Run()
    
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        serveWs(hub, w, r)
    })
    
    // 提供静态文件服务
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "index.html")
    })
    
    fmt.Println("聊天室启动在 http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

五、实际应用场景与选型建议

5.1 何时使用原生 Socket?

  • 物联网设备通信:设备资源受限,需要极简协议
  • 游戏服务器:追求极致性能,需要自定义协议
  • 跨语言 RPC 框架:gRPC、Thrift 等底层都是 Socket
  • 文件传输:大文件传输场景

5.2 何时使用 WebSocket?

  • Web 聊天应用:微信网页版、在线客服
  • 实时行情推送:股票、加密货币价格
  • 在线协作工具:Google Docs、Figma
  • 多人游戏:浏览器中的联机小游戏
  • 实时监控仪表盘:服务器性能监控、日志流

5.3 备选方案

除了 WebSocket,还有一些其他实时通信方案值得了解:

技术 通信方向 适用场景
SSE 服务器 → 客户端 通知推送、新闻流、AI 流式输出
WebRTC P2P 双向 音视频通话、文件分享
MQTT 发布/订阅 物联网、移动推送

六、常见问题与最佳实践

6.1 心跳保活

WebSocket 连接可能因为网络问题而断开,需要实现心跳机制:

// 客户端心跳
const ws = new WebSocket('ws://example.com/ws');
let pingInterval;

ws.onopen = () => {
    pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send('ping');
        }
    }, 30000); // 每 30 秒发送一次心跳
};

ws.onclose = () => {
    clearInterval(pingInterval);
};

6.2 断线重连

function connectWebSocket() {
    const ws = new WebSocket('ws://example.com/ws');
    
    ws.onclose = () => {
        console.log('连接断开,3秒后重连...');
        setTimeout(connectWebSocket, 3000);
    };
    
    return ws;
}

6.3 安全建议

  • 生产环境务必使用 wss://(WebSocket over TLS),相当于 HTTPS 对应 HTTP
  • 在握手阶段验证用户身份(检查 Origin 头、使用 Token)
  • 设置合理的消息大小限制,防止 DoS 攻击
  • 实现速率限制,避免单个连接占用过多资源

总结

特性 Socket WebSocket
层级 操作系统 API 应用层协议
协议基础 TCP/UDP HTTP → WebSocket
浏览器支持 不支持 原生支持
适用场景 通用网络编程 Web 实时通信
开发复杂度 较高 中等

选择合适的技术取决于你的具体场景。如果你开发的是 Web 应用需要实时通信,WebSocket 是首选;如果你需要底层控制或非 HTTP 环境,原生 Socket 更合适。