blog
blog copied to clipboard
简化 WebSocket, TCP 等长连接的开发
简化 WebSocket, TCP 等长连接的开发
简介
根据之前的项目经验梳理了一下对于服务器端长连接(websocket, tcp)的开发模式,并且总结出了一套解决方案
HTTP 开发模式
先看看HTTP的开发模式有哪些爽点
- 协议稳定,无论是http 还是 https,他们的协议都是固定不变的,自己无需处理数据包问题
- 每次请求响应都是一个独立连接(即使因 keep-alive 复用同一个连接,在开发时也是无感知的 )
- 请求的连接状态,虽然http连接本身是无状态的,但是浏览器会自动处理 Cookie,或者往Header 带 Token,就相当于给这个请求赋予了状态
- 根据请求路径做路由映射,指定路由处理函数
- 框架丰富,大部分框架封装好了上下文对象,解析参数,验证参数,响应数据等等操作都特别方便
- 易调试,因 HTTP 协议本身特别简单,纯粹,它的调试也简单,工具很多
还有其他爽点,可是这些在 WebSocket, TCP 就不适用了。
长连接的痛点
在长连接的开发中,相比于http的开发,会遇到这些问题:
- 协议不固定,websocket还好,在协议本身已经定义了数据边界,但是数据包的内容依然需要自己解析。TCP 就更不固定了,还需要自己定义数据编解码协议,处理粘包半包问题,才能解析到数据包,解析到了数据包还需要解析数据内容
- 请求响应这种模式其实在长连接也很常见,只不过都是发生在同一个连接中,响应需要自己手动往连接发数据
- 会涉及到和其他连接的交互
- 主动往连接推送数据,或者获取其他连接状态并给其他连接推送数据,或者广播数据
- 长连接的状态维护,长连接本身是不带任何状态,都要开发者维护,通过业务协议为连接赋予状态
解决方案
协议
参考 HTTP 的开发模式。首先要定义一个请求响应的协议规范
// Request 请求协议
type Request struct {
Cmd string // 消息命令,用于路由映射
Seqno string // 消息编号,在短时间内不能重复
RawData []byte // 消息原始数据
}
// Response 响应协议
type Response struct {
Cmd string // 消息命令
Seqno string // 消息编号,请求的 Seqno 原样赋值
Code int // 响应状态码
Msg string // 响应消息
Data interface{} // 响应的数据
}
Request
是请求数据,Response
是响应数据、服务器推送数据,整个框架将会围绕该协议做进一步的封装。
路由映射,处理上下文
一旦有固定的协议了,那么就可以很方便的做很多事了,比如 HTTP 那么爽,围绕 HTTP 框架的功能,让我们把 HTTP 框架有的东西也给弄过来。
模拟HTTP的开发模式,绑定路由,路由分组,中间件请求处理,参数解析,验证,格式化响应等等。
此外,还有长连接特有的功能,主动推送,获取其他连接状态
srv := cs.New()
srv.Use(cs.Recover()) // 使用中间件
srv.Handle("register", func(c *cs.Context) { // 绑定路由
var body struct{
UID int64 `json:"" v:"required#uid必填"`
Name string `json:"" v:"required#name必填"`
}
if err := c.Parse(&body); err != nil {
panic(err) // panic 给 recover 中间件处理
}
c.Set("uid", body.UID) // 给当前连接设置状态
c.OK(map[string]int64{ // 响应数据
"timestamp": time.Now().Unix(),
})
c.Push(&cs.Response{ // 主动给当前连接推送消息
Cmd: "welcome",
Data: "welcome to my server"
})
c.Broadcast(&cs.Response{ // 广播,给所有连接推消息
Cmd: "user_online",
Data: body,
})
for _, sid := range c.GetAllSID() {
if c.GetState(sid, "uid") != nil { // 获取其他连接的状态,然后给其他连接推消息
c.PushSID(sid, &cs.Response{
Cmd: "friend_online",
Data: map[string]interface{}{
"name": body.Name,
}
})
}
}
})
瞧,只要把基础协议一固定,什么都好做了。而且功能比 HTTP 更强,更方便了。
适配器
如果把协议定的很死,那么就少了很多的灵活性,局限就很大了。所以上面的协议,可以理解为业务协议,实际上数据包的协议完全由适配器自己去决定,然后把数据包转成上方的协议就可以完美使用刚刚封装的框架。
比如 tcp 的一个数据包协议为 [数据长度 4byte] + [命令2byte] + [版本号1byte] + [数据不固定长度]
, 就可以根据该协议提取出 命令,、数据,转化为上方的协议,&cs.Request{Cmd: parsedCmd, Data: parseDataBytesArray}
多适配器共享
该框架做到后面,我发现要将多个适配器共享一套代码变得特别容易,比如 TCP 和 Websocket 共用同一套业务逻辑,只需要实现好适配器就可以了,真是意外惊喜
wsAdapter := xwebsocket.New()
tcpAdapter := xtxp.New("127.0.0.1:5566")
srv := cs.New(httpAdapter, tcpAdapter)
srv.Handle(cs.CmdConnected, func (c *cs.Context) {})
HTTP 主动推送
再到后面,发现要实现 HTTP 主动推送也是特别简单的,基于 SSE 的主动推送,加上 HTTP 的请求响应模式,在加上 cookie 对于连接的状态维护