【计算机网络实战】简易IM(三)KIM源码细节之tcp和websocket

320 阅读4分钟

前言

这篇文章是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要自己写
  1. 自定义结构体:
type Frame struct {
	OpCode  kim.OpCode //操作码
	Payload []byte     //负载
}
  1. set就是赋值,get就是取值;
  2. 新建连接就是给连接加上(指定或非指定)的reader和writer:
func NewConn(conn net.Conn) kim.Conn {
	return &TcpConn{
		Conn: conn,
		rd:   bufio.NewReaderSize(conn, 4096),
		wr:   bufio.NewWriterSize(conn, 1024),
	}
}
  1. 读帧的核心逻辑就是获取对应的属性数据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协议的具体实现这一部分就结束了,下期将会关注业务部分代码的实现。

参考资料