文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 数据协议
  4. 4. 为什么需要数据协议
  5. 5. 服务端代码
  6. 6. 客户端代码
  7. 7. 执行结果分析
  8. 8. 总结

文档更新说明

  • 最后更新 2019年05月17日
  • 首次更新 2019年05月17日

前言

  最近在复习网络相关知识, 复习到了TCP协议, 所以写了个demo熟悉一下. 为了方便我用了golang实现的, 其实用什么语言都一样, 最重要的是理解核心知识.
  在这个例子中, 我会设计一个简单的tcp数据协议, 然后客户端根据数据协议发送数据, 服务端根据协议接收数据并打印出来, 整个过程我会模拟tcp数据流特点, 存在粘包问题, 所以我们服务端接收数据的时候要进行数据完整性判断, 把不完整的数据, 粘包的数据在个地方存起来, 等数据完整了再继续读取.   

数据协议

  我们的数据协议比较简单, 如下:

1
2
3
--------------------
| 头部4字节 | Body内容 |
--------------------

上面就是我们的数据协议了, 非常简单, 头部4字节表示body的长度N, 接着后面N个字节就是body的内容.当然我们可以设计复杂一些, 比如头部可以带上版本号啊, 消息类型什么的, 这个我就不弄了.

为什么需要数据协议

  我们知道tcp数据的传输是以流的形式传输, 接收方接收到数据的时候并不一定是完整的, 比如我发送两句话,分别是”今天好热”, 和”啊我喜欢中国”, 这个时候服务端收到的数据可能是”今天好热啊我喜欢”, “中国”, 这样服务端其实很容易产生歧义, 也就是我们平时说的粘包问题, 因此我们需要一个数据协议, 来让通讯双方都能理解数据的含义.
  比如上面的话我们设计成”[4]今天好热”, “[6]啊我喜欢中国”. 前面部分的数字表示这句话的长度, 后面就是这句话的内容. 这样尽管我们收到数据”[4]今天好热[6]啊我喜欢, “中国”, 都能清楚知道”今天好热”是一句话, 后面”啊我爱中国”是另一句话了.   

服务端代码

  下面是我们的代码, 我在这里简单解释一下:
  因为我们在本地传输数据少量数据, 所以比较难模拟粘包问题, 因此我们每次都从缓冲区取8个字节数据出来, 就假设是tcp数据传输的时候每次就传这么多吧, 因为8个字节很可能取到的不是一条完整的额消息, 可能是1.5条消息, 所以我们需要按照上面的数据协议对流进行处理, 并且把不够一条消息的部分数据存回我们定义的缓存数组里, 等到数据接收完毕之后再解析使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package main
import (
"encoding/binary"
"fmt"
"net"
)
func process(conn net.Conn) {
defer conn.Close()
// 我们规定的消息投长度, 根据这个长度指定的字节数, 一次性读取我们的字节
msgHeadLen := 4
// 已经接收的还未处理的数据总长度, 含头部部分+body部分
totalCount := 0
// 最大容量, 表示我们接收到的数据可以存放的地方大小
maxBuf := make([]byte, 1024)
// 表示buffer放入了多少内容, 即内容偏移
bufOffset := 0
for {
// 假设tcp每次传输到的数据只有8个字节, 因此我们每次最大只读取8个字节, 来模拟粘包问题
buf := make([]byte, 8)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read err:", err)
return
}
fmt.Printf("Read %d bit\n", n)
totalCount += n
// 先将数据放入大容量buffer里
for i := 0; i < n; i++ {
maxBuf[bufOffset+i] = buf[i]
fmt.Printf("byte:%v add into maxBuf[%d]\n", buf[i], bufOffset+i)
}
// 数据已经存放起来了, 所以这块buf内容可以释放了
buf = nil
bufOffset += n
// 当我们接受到的内容长度还不够头部长度那么长时, 说明数据不完整, 继续循环等待接下来的tcp数据流
if totalCount < msgHeadLen {
fmt.Println("totalCount < msgHeadLen")
continue
}
// 读取数据头部, 确认body部分长度
// 注意这里我们用大端读取, 因为网络传输的数据我们规定使用大端传输.
bodyLen := binary.BigEndian.Uint32(maxBuf[0:msgHeadLen])
fmt.Printf("msg body len is:%v\n", bodyLen)
// 确定body是否已经足够长了, 如果不够则继续循环等待读取接下来的数据.
if bufOffset < msgHeadLen+int(bodyLen) {
fmt.Println("bufOffset < msgHeadLen+int(bodyLen)")
continue
}
// 开始处理数据
// 这里只是简单把数据部分一个一个字节打印出来
fmt.Printf("msg head = %x. body = %x\n", maxBuf[0:msgHeadLen], maxBuf[msgHeadLen:msgHeadLen+int(bodyLen)])
for i := msgHeadLen; i < msgHeadLen+int(bodyLen); i++ {
fmt.Printf("%v ", maxBuf[i])
}
fmt.Println("\nprint content done")
// 因为我们可能读取到1.5条数据, 所以剩下的不完整数据, 需要返回缓冲数组, 等下一个循环读取到数据, 再拼一起构成新的完整数据
newOffset := 0
for i := msgHeadLen + int(bodyLen); i < totalCount; i++ {
maxBuf[newOffset] = maxBuf[i]
fmt.Printf("byte:%v add into maxBuf[%d]\n", maxBuf[i], newOffset)
newOffset++
}
bufOffset = newOffset
totalCount = newOffset
fmt.Println("newOffser = ", bufOffset)
fmt.Println()
}
}
func main() {
fmt.Println("server start...")
listen, err := net.Listen("tcp", "127.0.0.1:8081")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}

客户端代码

  下面是我们的客户端代码, 为了模拟真实的数据传输, 我们将数据按照定义好的数据协议格式发送出去, 而且故意把数据分成多段, 一段一段发出, 看看服务端是如何处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main
import (
"fmt"
"net"
"time"
)
func main() {
fmt.Printf("Dial...\n")
conn, err := net.Dial("tcp", "127.0.0.1:8081")
if err != nil {
fmt.Println("err dialing:", err.Error())
return
}
fmt.Printf("Dial Success...\n")
defer conn.Close()
// 下面就是我们的数据, 分成好几段发出吗, 完整的数据是这样
// 头部:0, 0, 0, 2 内容:1, 2
// 头部:0, 0, 0, 5 内容:101, 102, 103, 104, 105
// 头部:0, 0, 0, 1 内容:108
_, err = conn.Write([]byte{0, 0, 0, 2, 1, 2, 0, 0, 0, 5, 101})
func() {
time.Sleep(3 * time.Second)
conn.Write([]byte{102, 103, 104, 105, 0, 0})
}()
func() {
time.Sleep(6 * time.Second)
conn.Write([]byte{0})
}()
func() {
time.Sleep(9 * time.Second)
conn.Write([]byte{1, 108})
}()
if err != nil {
fmt.Println("err conn.write:", err)
return
}
time.Sleep(1000 * time.Second)
}

执行结果分析

  下面看一下服务端是否能把数据解析出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
server start...
Read 8 bit // 第一次读取到8个字节
byte:0 add into maxBuf[0]
byte:0 add into maxBuf[1]
byte:0 add into maxBuf[2]
byte:2 add into maxBuf[3]
byte:1 add into maxBuf[4]
byte:2 add into maxBuf[5]
byte:0 add into maxBuf[6]
byte:0 add into maxBuf[7]
msg body len is:2
msg head = 00000002. body = 0102
1 2 //这里读取到第一条消息, 内容是1,2, 正确
print content done
byte:0 add into maxBuf[0]
byte:0 add into maxBuf[1]
newOffser = 2 // 读取了8个字节解析了第一条消息, 一共用去了6个字节, 剩下2个字节放回缓冲数组
Read 3 bit // 第二次读取到3个字节, 加上上面剩余的2个一共就有5个字节
byte:0 add into maxBuf[2]
byte:5 add into maxBuf[3]
byte:101 add into maxBuf[4]
msg body len is:5
bufOffset < msgHeadLen+int(bodyLen) // 程序解析得到第二条消息的body长度是5, 完整的数据就是4+5=9个字节, 而我们才5个字节明显不够, 继续等待新数据的到来
Read 6 bit // 第三次读取, 读取到6个字节
byte:102 add into maxBuf[5]
byte:103 add into maxBuf[6]
byte:104 add into maxBuf[7]
byte:105 add into maxBuf[8]
byte:0 add into maxBuf[9]
byte:0 add into maxBuf[10]
msg body len is:5
msg head = 00000005. body = 6566676869
101 102 103 104 105 // 解析得到第二条消息的内容101,102,103,104,105, 正确
print content done
byte:0 add into maxBuf[0]
byte:0 add into maxBuf[1]
newOffser = 2 //将剩余的2个字节放回缓冲数组
Read 1 bit // 第4次读取到1个字节, 加上上面剩余的2个一共3个
byte:0 add into maxBuf[2]
totalCount < msgHeadLen // 3个字节很明显不够一条消息的头部长度, 继续等待读取
Read 2 bit // 第5次读取到2个字节, 加上上面剩余的3个, 一共5个字节
byte:1 add into maxBuf[3]
byte:108 add into maxBuf[4]
msg body len is:1
msg head = 00000001. body = 6c
108 // 解析得到第三条消息内容是108, 正确
print content done
newOffser = 0 // 刚好5个字节都用完了, 剩余0个字节.

总结

  上面服务端已经正确处理了客户端发送的3条消息了, 只不过我们可以看出, 服务端实际上一共读取了5次数据, 而客户端是分4次发出的.
  我们模拟出了出现粘包的情况, 也就是接收到超过一条消息的长度, 我们的处理方法是读取第一条, 剩余的放回缓冲数组继续等待新数据的到来. 还有接收到的数据还不够数据部那么长时的处理方法, 我们的处理方法是继续等待新数据的到来.

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 数据协议
  4. 4. 为什么需要数据协议
  5. 5. 服务端代码
  6. 6. 客户端代码
  7. 7. 执行结果分析
  8. 8. 总结