网络通信中,端与端之间只能传输二进制数据流。TCP/IP协议的解析已经完全交给了硬件设备完成,即便是软路由等用服务器上装软件来替代硬件设备也已经相当成熟。我们需要面对的都是应用层的通信问题。而大部分情况下也无需考虑通信细节,因为总有各种框架比如长连接的websocket框架,处理HTTP协议的网站框架。或者直接提供网络访问包比如访问数据库的包,各种消息队列服务包。总之网络通信只剩下序列化对象,发送出去,另一端接收,反序列化成对象。甚至有些框架序列化的步骤都给省略了。
前人造出了各种轮子,所以我们只需要装几个沙发蒙张皮就可以卖车了。但深入研究才是根本。有时我们需要自行构建一套简单的协议来实现客户端与服务器或者不同程序之间通信。当然按照一些有影响力的协议来实现会更具有通用性,更规范安全。不过实现难度可就大大提高了。
golang建立网络连接还是相当容易的。以下代码为了尽可能清爽,省略错误处理等。客户端和服务器端都采用定长数据包,且采用 请求1 > 响应1 >> 请求2 > 响应2 这样的模式通信。
func main() {
go server()
client()
time.Sleep(time.Second * 4)
}
//客户端实现
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
buf := make([]byte, 50000)
for i := 0; i < 5; i++ {
data := make([]byte, 10000)
copy(data, []byte("PING"+string.))
con.Write(data)
n, _ := io.ReadFull(con, buf)
msg := string(buf[:n])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
}
con.Close()
}
//服务器端实现
func server() {
s, _ := net.Listen("tcp", "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
//固定PING数据包长度为10000
buf := make([]byte, 10000)
for {
//读满数据
n, err := io.ReadFull(c, buf)
if err != nil {
fmt.Println(err)
break
}
msg := string(buf[:n])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
//固定PONG数据包长度为50000
data := make([]byte, 50000)
copy(data, []byte("PONG"))
c.Write(data)
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
PING 10000
PONG 50000
EOF
disconnect
定长数据包是最容易处理的。如果实际应用中数据包长度平均,可以加一部分填补空数据进去形成定长数据。虽然浪费了一定的网络带宽,但有得有失也是一种解决方案。
不定长的数据包才是实际应用中经常遇到的。因此必须要解决数据流通信的包完整性问题和粘包问题。包的完整性当然是接收时得接收够数据。粘包问题是接收当前数据包时不能多截取到下一个包的数据。解决方案中网站这种打开连接传数据,传完数据关连接是最方便的。不过应用中除了网站很少采用这种模式通信。那么另一种解决方案就是在每个包末尾加个结束符号,当读取到结束符号时,即代表包完整结束,否则一直读取数据。
//客户端实现
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
r := bufio.NewReaderSize(con, 5000) //缓冲区大小大于数据包
data := make([]byte, 100)
for i := 0; i < 5; i++ {
data = append(data, data...)
copy(data, []byte("PING"))
con.Write(data)
con.Write([]byte{' '}) //以空格作为结束符
//接收以空格符作为结束的数据包
s, _ := r.ReadSlice(' ')
msg := string(s[:len(s)-1])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
}
con.Close()
}
//服务器端实现
func server() {
s, _ := net.Listen("tcp", "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
//缓冲区大小大于数据包
r := bufio.NewReaderSize(c, 5000)
data := make([]byte, 90)
for {
//接收以空格符作为结束的数据包
s, err := r.ReadSlice(' ')
if err != nil {
fmt.Println(err)
break
}
msg := string(s[:len(s)-1])
fmt.Printf("%v %v\n", msg[0:4], len(msg))
data = append(data, data...)
copy(data, []byte("PONG"))
c.Write(data)
c.Write([]byte{' '})//以空格作为结束符
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 200
PONG 180
PING 400
PONG 360
PING 800
PONG 720
PING 1600
PONG 1440
PING 3200
PONG 2880
EOF
disconnect
这种方式当数据包中的数据含有结束符时就会出错。因此我们需要对数据进行一次编码,比如Base64。Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,是一种基于64个可打印字符来表示二进制数据的方法。编码后的数据比原始数据略长,为原来的1.3倍。在电子邮件中,根据RFC822规定,每76个字符,还需要加上一个回车换行。可以估算编码后数据长度大约为原长的135.1%。下面是通信例子中更改的部分。
//发送时
//将连接con用base64包装一下,对data进行编码
w := base64.NewEncoder(base64.RawStdEncoding, con)
w.Write(data)
w.Close()
//写入结束符
con.Write([]byte{' '})
//接收时
s, _ := r.ReadSlice(' ')
//去掉结束符
s = s[:len(s)-1]
//解码接收到的数据
de := base64.RawStdEncoding
d := make([]byte, de.DecodedLen(len(s)))
de.Decode(d, s)
msg := string(d)
以上方案只能适用简单场景,耗费带宽或者性能不高。
结构化数据包应该是最常采用的方案。数据包拥有固定长度的消息头,不定长度的消息体,消息头内含消息体的长度。每次解析先读取固定长度消息头,通过消息头内的消息体长度再次读取完整的消息体。消息头内还可以包含各种命令,状态等数据,能在解析消息体之前先做一步业务处理。定义消息头的数据结构和含义的一整套规则可以统称为xxx协议。比如下面这个是websocket协议的包结构定义。关于websocket的详细资料可以在度娘上轻松找到。这里只是借来做个例子。
FIN
标识是否为此消息的最后一个数据包,占 1 bit
RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
Opcode
数据包类型(frame type),占4bits
0x0:标识一个中间数据包
0x1:标识一个text类型数据包
0x2:标识一个binary类型数据包
0x3-7:保留
0x8:标识一个断开连接类型数据包
0x9:标识一个ping类型数据包
0xA:表示一个pong类型数据包
0xB-F:保留
MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
处理websocket协议太麻烦了,在此定义一种新协议A,就一条规则:首4字节为命令,后4字节为消息体长度。代码例子如下
//客户端实现
func client() {
con, _ := net.Dial("tcp", "127.0.0.1:6666")
data := make([]byte, 10)
head := make([]byte, 8)
for i := 0; i < 4; i++ {
data = append(data, data...)
//4字节的命令
copy(data, []byte("PING"))
//4字节的消息体长度
binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
con.Write(data)
io.ReadFull(con, head)
//取出命令
cmd := string(head[0:4])
//取出消息体长度
bodylen := int(binary.BigEndian.Uint32(head[4:8]))
//按长度, 再次读取消息体
buf := make([]byte, bodylen)
io.ReadFull(con, buf)
msg := buf
fmt.Printf("%v %v\n", cmd, len(msg)+8)
}
con.Close()
}
//服务器端实现
func server() {
s, _ := net.Listen("tcp", "127.0.0.1:6666")
for {
c, _ := s.Accept()
go func() {
fmt.Println("someone connected")
data := make([]byte, 9)
head := make([]byte, 8)
for {
_, err := io.ReadFull(c, head)
if err != nil {
fmt.Println(err)
break
}
cmd := string(head[0:4])
bodylen := int(binary.BigEndian.Uint32(head[4:8]))
buf := make([]byte, bodylen)
_, err2 := io.ReadFull(c, buf)
if err2 != nil {
fmt.Println(err2)
break
}
msg := buf
fmt.Printf("%v %v\n", cmd, len(msg)+8)
data = append(data, data...)
copy(data, []byte("PONG"))
binary.BigEndian.PutUint32(data[4:8], uint32(len(data)-8))
c.Write(data)
}
fmt.Println("disconnect")
}()
}
}
output:
someone connected
PING 20
PONG 18
PING 40
PONG 36
PING 80
PONG 72
PING 160
PONG 144
EOF
disconnect