WebSocket连接在建立阶段需要基于HTTP协议,但建立后会有独立的TCP通信通道。具体来说:
-
握手阶段(基于HTTP) WebSocket连接通过HTTP协议发起握手请求,请求头包含Upgrade: websocket等关键信息,服务端响应101状态码完成协议切换
-
通信阶段(独立协议) 握手成功后,客户端和服务端会保持TCP长连接,使用WebSocket协议帧进行双向通信,此时不再依赖HTTP协议
这种设计的好处:
- 复用现有HTTP端口(80/443)
- 利用成熟的HTTP基础设施(身份验证、负载均衡等)
- 更容易通过防火墙和代理
如果希望完全脱离HTTP协议,可以使用更底层的TCP Socket+自定义协议,但会失去上述优势,且需要自行处理协议解析、心跳维护等复杂逻辑。
go
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许跨域请求
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
// 升级HTTP连接到WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error("WebSocket升级失败:", err)
return
}
defer conn.Close()
// 读取客户端消息
for {
}
}
区别
特性 | websocket | socket |
---|---|---|
协议层级 | 应用层协议 (基于HTTP握手) | 传输层协议 |
连接建立 | 通过HTTP协议升级建立 | 直接三次握手建立 |
数据格式 | 自带帧结构(支持文本/二进制) | 原始字节流 |
通信模式 | 全双工通信 | 全双工通信 |
消息边界 | 自动处理消息边界 | 需应用层处理分包 |
头部开销 | 每个帧有2-14字节头部 | 无额外头部 |
典型应用 | 实时网页应用(聊天、推送) | 传统C/S应用(文件传输、数据库连接) |
WebSocket是一种应用层协议,建立在TCP之上,提供全双工通信。它通过HTTP握手建立连接,之后保持长连接,适合实时应用。而TCP Socket是传输层的接口,更底层,需要自己处理数据格式和协议。
总结两者的适用场景,比如需要实时性且不想处理底层协议时用WebSocket,需要完全控制通信细节时用TCP Socket。
实现
websocket处理消息
go
// 自动处理协议升级(HTTP -> WebSocket)
conn, _, err := websocket.DefaultDialer.Dial(url, header)
// 支持消息类型标识(TextMessage表示文本帧)
err = conn.WriteMessage(websocket.TextMessage, []byte("Hello..."))
// 内置消息边界处理(ReadMessage自动处理帧分割)
_, message, err := conn.ReadMessage()
原生TCP处理方式
go
// 需要手动处理消息边界(假设头部4字节标识消息长度)
func readTCPMessage(conn net.Conn) ([]byte, error) {
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(header)
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
return data, err
}
如果使用原生TCP Socket实现相同功能,需要:
- 手动实现HTTP握手协议
- 自行设计消息格式(如添加长度前缀)
- 处理TCP粘包/拆包问题
- 实现心跳保持机制
- 自行处理连接重连逻辑
通信协议
设计结构化的websocket通信协议。
通常,一个良好的协议需要定义消息格式、类型、错误处理、心跳机制等。用户可能希望消息包含元数据(如时间戳、消息ID)和有效载荷,同时支持不同的操作类型。
消息类型的设计。可能需要定义如Request、Response、Notification、Heartbeat等类型,每种类型有不同的处理逻辑。例如,客户端发送请求,服务器响应,或者服务器主动推送通知。
错误处理的设计。协议应包含错误码和错误信息,这样客户端可以识别和处理不同错误。例如,在用户代码中,处理速率限制时返回特定错误,可以将其纳入协议的错误体系中。
心跳机制的设计。心跳机制对于维持连接很重要,特别是在长时间空闲时防止连接被关闭。用户当前的代码中没有心跳,需要建议添加定期发送心跳消息的逻辑。
消息头部的设计。可以考虑包含通用字段,如消息ID、时间戳、类型、状态码等。有效载荷部分则根据具体类型携带不同数据。
版本控制和兼容性设计。需要考虑版本控制和兼容性,为协议添加版本字段,以便未来升级。
go
// 新增协议结构体(添加到文件顶部)
type Message struct {
ID string `json:"id"` // 消息唯一标识
Type string `json:"type"` // 消息类型:request/response/notification
Timestamp int64 `json:"ts"` // 毫秒时间戳
Status int `json:"status"` // 状态码(0=成功)
Payload json.RawMessage `json:"data"` // 实际业务数据
}
// 消息类型常量(添加到包级别)
const (
MsgTypeRequest = "request"
MsgTypeResponse = "response"
MsgTypeNotification = "notification"
MsgTypeHeartbeat = "heartbeat"
)
// 在wsconn.ReadLoop中处理消息(修改消息处理逻辑)
func (c *Conn) ReadLoop(ctx context.Context) error {
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
return err
}
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
// 处理协议错误
_ = c.SendError(ctx, "invalid_message_format", err)
continue
}
switch msg.Type {
case MsgTypeHeartbeat:
// 更新心跳时间
c.lastHeartbeat = time.Now().Unix()
case MsgTypeRequest:
go c.handleRequest(ctx, msg)
default:
// 返回未知类型错误
_ = c.SendError(ctx, "unsupported_message_type", nil)
}
}
}
// 新增发送方法(添加到Conn结构体)
func (c *Conn) SendResponse(ctx context.Context, reqID string, data interface{}) error {
payload, _ := json.Marshal(data)
msg := Message{
ID: uuid.New().String(),
Type: MsgTypeResponse,
Timestamp: time.Now().UnixMilli(),
Status: 0,
Payload: payload,
}
return c.conn.WriteJSON(msg)
}
// 新增错误处理方法(添加到Conn结构体)
func (c *Conn) SendError(ctx context.Context, reqID string, err error) error {
errorMsg := map[string]interface{}{
"code": 1001, // 自定义错误码
"message": err.Error(),
}
payload, _ := json.Marshal(errorMsg)
msg := Message{
ID: reqID,
Type: MsgTypeResponse,
Timestamp: time.Now().UnixMilli(),
Status: 1,
Payload: payload,
}
return c.conn.WriteJSON(msg)
}