0x00 回顾TCP
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。用一个图介绍连接、数据传输、断开连接,即如下图所示:
三次握手四次挥手
0x01 IO多路复用原始的TCP socket开发中,偏向于底层,基本利用系统调用和操作系统交互。涉及几个概念:
0x02 go中TCP网络编程
type ServiceManager interface {
Handler(conn Connection)
}
func (s *ServiceManage) Handler(conn wolsocket.Connection) {
// 处理连接
agent, err := s.acceptConnect(conn)
if err != nil {
// todo: 错误处理
glog.Warningf("handler connect error: %s", err.Error())
return
}
// 处理数据
for {
data, err := agent.Connection.Read()
if err != nil {
break
}
....
}
return
}
0x03 读写细节
conn.Read
-
socket有部分数据:如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等待所有期望数据全部读取后再返回。
-
socket有足够数据:如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil
-
有数据,socket关闭:第一次Read成功读出了所有的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error;
conn.Write
应用层面的read和write并发安全性
-
每次Write操作都是受lock保护,直到此次数据全部write完。因此在应用层面,要想保证多个goroutine在一个conn上write操作的Safe,需要一次write完整写入一个“业务包”;一旦将业务包的写入拆分为多次write,那就无法保证某个Goroutine的某“业务包”数据在conn发送的连续性。
-
Read操作,也是lock保护的。多个goroutine对同一conn的并发读不会出现读出内容重叠的情况,但内容断点是依 runtime调度来随机确定的。存在一个业务包数据,1/3内容被goroutine-1读走,另外2/3被另外一个goroutine-2读 走的情况。比如一个完整包:world,当goroutine的read slice size < 5时,存在可能:一个goroutine读到 “worl”,另外一个goroutine读出”d”。
socket关闭
-
从client的结果来看,在己方已经关闭的socket上再进行read和write操作,会得到”use of closed network connection” error;
-
从server的执行结果来看,在对方关闭的socket上执行read操作会得到EOF error,但write操作会成功,因为数据会成功写入己方的内核socket缓冲区中,即便最终发不到对方socket缓冲区了,因为己方socket并未关闭。因此当发现对方socket关闭后,己方应该正确合理处理自己的socket,再继续write已经无任何意义了
0x04 连接保活
必要性
保活方式
-
keep-alive:tcp层保活。当我们了解tcp socket时,一般看到keep-alive会以为采用该方式保活挺好。但是实际上该方式存在问题,很多时候并不能起到保活的作用。比如:socks协议只管转发TCP层具体的数据包,而不会转发TCP协议内的实现细节的包(也做不到),一旦使用sokets代理就直接失效了,所以考虑到真实复杂的网络环境,还是不要用。
-
应用层heartbeat:业务层,发送心跳包保活:client发送/server发送。真实的场景中使用client发送的方式实现。
超时检测
-
定时器模型:常用做法是利用go中timer功能,为每一个conn维护一个timer,保证可以预期超时检查conn timestamp的更新情况。
-
go中read block模型:该方式简单好用,灵活利用go socket conn中read block的特性。即在每一次read之前设置
SetReadDeadline
保证read可以阻塞超时,达到连接超时的检测效果。func (c *Connection) Read() ([]byte, error) {
// 设置read超时
c.Conn.SetReadDeadline(time.Now().Add(70 * time.Second))
// 先读取长度
lenData := make([]byte, CONNECTION_SIZE_BUF)
_, err := io.ReadFull(c.Conn, lenData)
if err != nil {
return nil, fmt.Errorf("socket read data length error: %s", err.Error())
}
.....
return d, nil
}
0x05 数据封装
byte封装
func (c *Connection) Read() ([]byte, error) {
// 设置read超时
c.Conn.SetReadDeadline(time.Now().Add(70 * time.Second))
// 先读取长度
lenData := make([]byte, CONNECTION_SIZE_BUF)
_, err := io.ReadFull(c.Conn, lenData)
if err != nil {
return nil, fmt.Errorf("socket read data length error: %s", err.Error())
}
// 从byte中解析出l值
magic := binary.BigEndian.Uint16(lenData[0:2])
if magic != CONNECTION_MAGIC {
return nil, fmt.Errorf("socket read data magic error: %x", magic)
}
l := binary.BigEndian.Uint32(lenData[2:CONNECTION_SIZE_BUF])
if l > CONN_MAX_DATA_LEN {
return nil, fmt.Errorf("data len big: %d", l)
}
// 准备读取数据
d := make([]byte, l)
realLen, err := io.ReadFull(c.Conn, d)
if err != nil {
return nil, fmt.Errorf("socket read data(len=%d) error: %s", l, err.Error())
}
if realLen != int(l) {
return nil, fmt.Errorf("data len is error: reallen(%d) != len(%d)", realLen, l)
}
return d, nil
}