前言
这篇文章是KIM源码阅读笔记的第三期,记录的是有关tcp和websocket部分的具体实现。
tcp和websocket
这两个包下的代码分别模拟了tcp和websocket连接,代码的可复用程度较高,个人感觉可以单独抽出来作为模块和库使用。
它们基本结构类似,都由server-connection-client组成。
server
两者的server都由新建、命名和升级这三部分组成,只是命名上略有差别。
(虽然不太理解为什么Upgrader被设计成一个空的结构体)
以下是tcp的server:
// Server is a websocket implement of the Server
type Upgrader struct {
}
// NewServer NewServer
func NewServer(listen string, service kim.ServiceRegistration, options ...kim.ServerOption) kim.Server {
return kim.NewServer(listen, service, new(Upgrader), options...)
}
func (u *Upgrader) Name() string {
return "tcp.Server"
}
func (u *Upgrader) Upgrade(rawconn net.Conn, rd *bufio.Reader, wr *bufio.Writer) (kim.Conn, error) {
conn := NewConnWithRW(rawconn, rd, wr)
return conn, nil
}
它们更通用的结构部分已经被抽象为一个server.go,放在了项目的主目录下:
...
// 定义了基础服务的抽象接口
type Service interface {
ServiceID() string
ServiceName() string
GetMeta() map[string]string
}
// 定义服务注册的抽象接口
type ServiceRegistration interface {
Service
PublicAddress() string
PublicPort() int
DialURL() string
GetTags() []string
GetProtocol() string
GetNamespace() string
String() string
}
// Server 定义了一个tcp/websocket不同协议通用的服务端的接口
type Server interface {
ServiceRegistration
// SetAcceptor 设置Acceptor
SetAcceptor(Acceptor)
//SetMessageListener 设置上行消息监听器
SetMessageListener(MessageListener)
//SetStateListener 设置连接状态监听服务
SetStateListener(StateListener)
// SetReadWait 设置读超时
SetReadWait(time.Duration)
// SetChannelMap 设置Channel管理服务
SetChannelMap(ChannelMap)
// Start 用于在内部实现网络端口的监听和接收连接,
// 并完成一个Channel的初始化过程。
Start() error
// Push 消息到指定的Channel中
// string channelID
// []byte 序列化之后的消息数据
Push(string, []byte) error
// Shutdown 服务下线,关闭连接
Shutdown(context.Context) error
}
...
connection
connection的编写框架也一致,都是由以下几部分组成:
- 帧定义
- get set方法
- 新建连接(两种参数形式)
- 读、写帧
- 冲刷缓冲区
只是对帧的定义不同:
- websocket可调用go的一个websocket库——gobwas中的
type Frame struct {
Header Header //头部
Payload []byte //负载
}
及对应函数
(header里面包含了opCode):
func NewFrame(op OpCode, fin bool, p []byte) Frame {
return Frame{
Header: Header{
Fin: fin,
OpCode: op,
Length: int64(len(p)),
},
Payload: p,
}
}
- tcp要自己写
- 自定义结构体:
type Frame struct {
OpCode kim.OpCode //操作码
Payload []byte //负载
}
- set就是赋值,get就是取值;
- 新建连接就是给连接加上(指定或非指定)的reader和writer:
func NewConn(conn net.Conn) kim.Conn {
return &TcpConn{
Conn: conn,
rd: bufio.NewReaderSize(conn, 4096),
wr: bufio.NewWriterSize(conn, 1024),
}
}
- 读帧的核心逻辑就是获取对应的属性数据opcode和payload,然后拼在一起:
func (c *TcpConn) ReadFrame() (kim.Frame, error) {
opcode, err := endian.ReadUint8(c.rd)
if err != nil {
return nil, err
}
payload, err := endian.ReadBytes(c.rd)
if err != nil {
return nil, err
}
return &Frame{
OpCode: kim.OpCode(opcode),
Payload: payload,
}, nil
}
此处的endian被KIM的作者封装在wire下,它其实是对更底层的机器指令的读取方式(大端小端,项目中默认为大端)以及io操作的封装。
client
通过对比tcp和websocket的实现代码可知,两者client部分中,对应结构体结构和函数的命名都是一致的,只是具体实现略有差异。
connect
两者的共性:
- 都有
CAS(compare and swap)原子操作,保证并发安全 - 都有
DialAndHandshake(拨号并握手) - 都有对应的错误处理。比如
- 拨号并握手失败则把CAS改回去;
- 对连接为空的情况报错
- 监控心跳,心跳停止时报错
两者的不同之处:
- websocket比tcp多了一个地址的转换
_, err := url.Parse(addr)
- 连接方式略有不同: tcp是利用rawconn新建了一个连接,相当于同名,但是指向的内存地址不同:
rawconn, err := c.Dialer.DialAndHandshake(kim.DialerContext{
Id: c.id,
Name: c.name,
Address: addr,
Timeout: kim.DefaultLoginWait,
})
...
if rawconn == nil {
return fmt.Errorf("conn is nil")
}
c.conn = NewConn(rawconn)
而websocket是直接用原conn对象赋值:
conn, err := c.Dialer.DialAndHandshake(kim.DialerContext{
Id: c.id,
Name: c.name,
Address: addr,
Timeout: kim.DefaultLoginWait,
})
...
if conn == nil {
return fmt.Errorf("conn is nil")
}
c.conn = conn
- 检测心跳的方式不一样:
tcp:
err := c.heartbeatloop()
websocket:
err := c.heartbeatloop(conn)
而这归根结底是因为两个方法调用的ping() 略有区别: 以下是tcp的ping():流程是打日志、向接收方写入帧、冲刷缓冲区
func (c *Client) ping() error {
logger.WithField("module", "tcp.client").Tracef("%s send ping to server", c.id)
err := c.conn.WriteFrame(kim.OpPing, nil)
if err != nil {
return err
}
return c.conn.Flush()
}
而websocket还要加锁以保证并发安全和设置超时时间保证连接的有效性,且它使用了gobwas中的函数,里面有相应的参数要求:
//client.go里的ping()
func (c *Client) ping(conn net.Conn) error {
c.Lock()
defer c.Unlock()
err := conn.SetWriteDeadline(time.Now().Add(c.options.WriteWait))
if err != nil {
return err
}
logger.Tracef("%s send ping to server", c.id)
return wsutil.WriteClientMessage(conn, ws.OpPing, nil)
}
对应库的函数原型:
func WriteClientMessage(w io.Writer, op ws.OpCode, p []byte) error {
return WriteMessage(w, ws.StateClientSide, op, p)
}
send
共同点:
- 都要执行原子性的加载地址操作:
atomic.LoadInt32(&c.state)
- 都要加锁保证并发安全
不同点:
- websocket要设置writedeadline
- websocket调用了通用库里面的方法,客户端的消息需要使用掩码
return wsutil.WriteClientMessage(c.conn, ws.OpBinary, payload)
剩下的内容也是类似:大体框架和思路类似,某些细节因协议本身规则不同略有差异,这里就不一一细说了,感兴趣的uu直接看大佬的源码就好。
至此,关于tcp和websocket协议的具体实现这一部分就结束了,下期将会关注业务部分代码的实现。
参考资料
- Github链接: KIM github链接