blog icon indicating copy to clipboard operation
blog copied to clipboard

简化 WebSocket, TCP 等长连接的开发

Open eyasliu opened this issue 4 years ago • 0 comments

简化 WebSocket, TCP 等长连接的开发

简介

根据之前的项目经验梳理了一下对于服务器端长连接(websocket, tcp)的开发模式,并且总结出了一套解决方案

HTTP 开发模式

先看看HTTP的开发模式有哪些爽点

  • 协议稳定,无论是http 还是 https,他们的协议都是固定不变的,自己无需处理数据包问题
  • 每次请求响应都是一个独立连接(即使因 keep-alive 复用同一个连接,在开发时也是无感知的 )
  • 请求的连接状态,虽然http连接本身是无状态的,但是浏览器会自动处理 Cookie,或者往Header 带 Token,就相当于给这个请求赋予了状态
  • 根据请求路径做路由映射,指定路由处理函数
  • 框架丰富,大部分框架封装好了上下文对象,解析参数,验证参数,响应数据等等操作都特别方便
  • 易调试,因 HTTP 协议本身特别简单,纯粹,它的调试也简单,工具很多

还有其他爽点,可是这些在 WebSocket, TCP 就不适用了。

长连接的痛点

在长连接的开发中,相比于http的开发,会遇到这些问题:

  • 协议不固定,websocket还好,在协议本身已经定义了数据边界,但是数据包的内容依然需要自己解析。TCP 就更不固定了,还需要自己定义数据编解码协议,处理粘包半包问题,才能解析到数据包,解析到了数据包还需要解析数据内容
  • 请求响应这种模式其实在长连接也很常见,只不过都是发生在同一个连接中,响应需要自己手动往连接发数据
  • 会涉及到和其他连接的交互
  • 主动往连接推送数据,或者获取其他连接状态并给其他连接推送数据,或者广播数据
  • 长连接的状态维护,长连接本身是不带任何状态,都要开发者维护,通过业务协议为连接赋予状态

解决方案

Command Service

协议

参考 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 对于连接的状态维护

eyasliu avatar Jan 26 '21 10:01 eyasliu