linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
来源:互联网 发布:淘宝旺旺mac版 编辑:程序博客网 时间:2024/06/07 14:15
我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
一、粘包问题可以用下图来表示:
假设主机A发送了两个数据包M1和M2给主机B,由于主机B一次接收的字节数是不确定的,故可能存在上图的4种情况,
1、分两次接收两个数据包,一次一个,没有粘包问题;
2、一次接收了两个数据包,存在粘包问题;
3、第一个接收了M1和M2的一部分,第二次接收M2的另一部分,存在粘包问题;
4、第一次接收了M1的一部分,第二次接收M1的另一部分和M2,存在粘包问题;
当然实际的情况可能不止以上4种,可以得知的是在互联网上通信很容易造成粘包问题。
二、粘包问题产生的原因
如下图所示,产生的原因主要有3个,当应用程序write 写入的大小大于套接口发送缓冲区大小时;当进行MSS大小的tcp分段时;当以太网帧的payload大于MTU进行ip分片时;都很容易产生粘包问题。
三、粘包问题的解决方案
本质上是要在应用层维护消息与消息的边界
1、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度
4、更复杂的应用层协议
对于条目2,缺点是如果消息本身含有\r\n字符,则也分不清消息的边界,条目4不在本文讨论范围内。
对于条目1,即我们需要发送和接收定长包。因为TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用(套接字标志为阻塞),如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回,但如果socket文件描述符有O_NONBLOCK标志,则write不阻塞,直接返回20。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,如下所示。
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
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;
while (nleft > 0)
{
ssize_t readn(int fd, void * buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;
while (nleft > 0)
{
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nread == 0) //对方关闭或者已经读到eof
return count - nleft;
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void * buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char *)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。
此时条目3是比较好的解决办法,其实也可以算是自定义的一种简单应用层协议。比如我们可以自定义一个包体结构
struct packet {
int len;
char buf[1024];
};
先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。服务器端在前面的fork程序的基础上把do_service函数更改如下:
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
{
struct packet recvbuf;
int n;
while (1)
{
memset(&recvbuf, 0, sizeof(recvbuf));
int ret = readn(conn, &recvbuf.len, 4);
if (ret == -1)
ERR_EXIT("read error");
else if (ret < 4) //客户端关闭
{
printf("client close\n");
break;
}
n = ntohl(recvbuf.len);
ret = readn(conn, recvbuf.buf, n);
if (ret == -1)
ERR_EXIT("read error");
if (ret < n) //客户端关闭
{
printf("client close\n");
break;
}
fputs(recvbuf.buf, stdout);
writen(conn, &recvbuf, 4 + n);
}
}
客户端程序的修改与上类似,不再赘述。
参考:
《Linux C 编程一站式学习》
《TCP/IP详解 卷一》
《UNP》- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- UNIX网络编程——tcp流协议产生的粘包问题和解决方案
- Linux编程之socket:tcp流协议产生的粘包问题及解决方法
- tcp流协议产生的粘包问题和解决方案
- tcp流协议产生的粘包问题和解决方案
- Linux下的socket编程实践(四)TCP的粘包问题和常用解决方案
- 【Linux网络】Linux Socket编程 TCP协议
- Linux网络编程之[基于socket通信的tcp协议的编程模型]
- socket编程之解决流协议的粘包问题(一 )
- socket编程之解决流协议的粘包问题(二)
- Socket编程 TCP粘包问题及解决方案
- 错误小记,以后改正
- shell编程测试文件权限
- csdn怎么弄得很麻烦了
- C++编写发送自定义TCP数据包程序
- 经过一年时间的沉淀 再次回首 TCP Socket服务器编程
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- C#提高知识-001:反射的应用和原理(一)
- Some English courses in Guildford
- asdasd
- LeetCode题解:Two Sum
- Use thrust reduce_by_key with raw pointers instead of device pointers
- Leetcode: Best Time to Buy and Sell Stock II
- Leetcode: Same Tree
- Leetcode: Maximum Depth of Binary Tree