介绍
最初开发这个包的目的是提炼一套可以在公司内多个项目间共用的网络层,因为在项目中我发现不同的网络应用一直重复一些相同或相类似的东西,比如最常用到的就是会话管理,不管是做游戏的前端连接层还是做服务器和服务器之间的RPC层或者是游戏的网关,虽然协议不一样但是它们都会需要会话的管理。会话管理看似简单,但是任何简单的问题一旦涉及到并发都会变得复杂起来,所以看似简单的会话管理每次实现起来都得再配套做单元测试甚至线上实际运行几个版本才能放心。所以我决定提取这些公共的部分,避免那些容易引入BUG的重复劳动。
但是在提取这些公共部分的时候并没有期初想象的那么容易,因为不同的应用场景有不同的需求,比如有的场景需要异步,有的场景需要同步,有的协议需要握手过程,有的则需要keepalive。从代码的提交历史里面可以看出这个包前后经过了很多次大的调整,因为要做一个能满足所有需求的通用网络层真的很难。
重构过程就像在简化公式,经过不断的提炼目前link变得十分的简单,同时它的定位也很清楚。link不是一个完整网络层也不是一个框架,它只是一个脚手架,它可以帮助你快速的实现出你所需要的网络层或者通讯框架,帮你约束网络层的实现方式,不至于用不合理的方式实现网络层,除此之外它不会管更多的事情。
link是协议无关的,使用link只需要理解少数几个概念就可以上手了。
核心
link包的核心是Session
,Session
的字面含义是会话
,就是一次对话过程。每一个连接的生命周期被表达为一个会话过程,这个过程中通讯双方的消息有来有往。
会话过程所用的具体通讯协议通过Codec
接口解耦。通过Codec
接口可以自定义通讯的IO实现方式,如:TCP、UDP、UNIX套接字、共享内存等,也可以自定义流的实现方式,如:压缩、加密、校验等,也可以实现自定义的协议格式,如:JSON、Gob、XML、Protobuf等。
在实际项目中,通常不会只有一个会话,所以link提供了几种不同的Session
管理方式。
Manager
是最基础的Session
管理方式,它负责创建和管理一组Session
。Manager
是不与通讯形式关联的,与通讯有关联的Manager
叫Server
,它的行为比Manager
更具体,它负责从net.Listener
上接收新连接并创建Session
,然后在独立的goroutine中处理来自新连接的消息。
link还提供了Channel
用于对Session
进行按需分组,Channel
用key-value的形式管理Session
,Channel
的key类型通过代码生成的形式来实现自定义。
示例
link在codec
目录下实现了集中简单的协议类型,可作为示例。
示例,创建一个使用Json作为消息格式的TCP服务端:
package main
import (
"log"
"net"
"github.com/funny/link"
"github.com/funny/link/codec"
)
type AddReq struct {
A, B int
}
type AddRsp struct {
C int
}
type Server struct{}
func main() {
json := codec.Json()
json.Register(AddReq{})
json.Register(AddRsp{})
listen, err := net.Listen("tcp", "")
checkErr(err)
server := link.NewServer(listen, json, 1024, new(Server))
go server.Serve()
addr := server.Listener().Addr()
clientSession, err := link.Dial(addr.Network(), addr.String(), json, 1024)
checkErr(err)
clientSessionLoop(clientSession)
}
func (*Server) HandleSession(session *link.Session) {
for {
req, err := session.Receive()
checkErr(err)
err = session.Send(&AddRsp{
req.(*AddReq).A + req.(*AddReq).B,
})
checkErr(err)
}
}
func clientSessionLoop(session *link.Session) {
for i := 0; i < 10; i++ {
err := session.Send(&AddReq{
i, i,
})
checkErr(err)
log.Printf("Send: %d + %d", i, i)
rsp, err := session.Receive()
checkErr(err)
log.Printf("Receive: %d", rsp.(*AddRsp).C)
}
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
补充说明
channel.go
这个文件里实现了Channel
类型用于管理一组Session
,通常用于发送广播和维护在线列表。
之前版本的Channel
类型,用的是Session.ID()
做key,这个设计会导致实际项目种出现类似这样的操作逻辑:
取用户ID -> 从自己维护的映射关系中取用户ID对应的Session ID -> 到Channel里取Session
而新的Channel
类型可以自定义key类型,在上述场景中就可以直接用用户ID
做key来索引Session
:
取用户ID -> 到Channel里取Session
除了直观的可以看出少了一次map操作之外,其实额外维护一份Session ID
映射关系也不是一件容易的事情,你需要重复Channel
内部做的所有事情,而又不能重用Channel
的代码。
使用Channel.Fetch()
进行遍历发送广播的时候,请注意存在IO阻塞的可能,如果IO阻塞会影响业务处理,就需要使用异步发送。
相关项目
参与项目
欢迎提交通过github的issues功能提交反馈或提问。
技术群:474995422