Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
eyasliu opened this issue Jan 26, 2021 · 0 comments
Open

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

eyasliu opened this issue Jan 26, 2021 · 0 comments

Comments

@eyasliu
Copy link
Owner

eyasliu commented Jan 26, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant