UNIX网络编程——tcp流协议产生的粘包问题和解决方案
我们在前面曾经说过,发送端可以是一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,如下所示。
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函数更改如下:
void do_service(int conn)
{
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);
}
}
客户端程序的修改与上类似,不再赘述。
UNIX网络编程——tcp流协议产生的粘包问题和解决方案的更多相关文章
- tcp流协议产生的粘包问题和解决方案
我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...
- Socket 编程中,TCP 流的结束标志与粘包问题
因为 TCP 本身是无边界的协议,因此它并没有结束标志,也无法分包. socket和文件不一样,从文件中读,读到末尾就到达流的结尾了,所以会返回-1或null,循环结束,但是socket是连接两个主机 ...
- UNIX网络编程——TCP 滑动窗口协议
什么是滑动窗口协议? 一图胜千言,看下面的图.简单解释下,发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口.发送方的窗口大小由接受方确定,目的在于控制发送速度,以免接受方的缓存不够大, ...
- UNIX网络编程——基于UDP协议的网络程序
一.下图是典型的UDP客户端/服务器通讯过程 下面依照通信流程,我们来实现一个UDP回射客户/服务器: #include <sys/types.h> #include <sys/so ...
- UNIX网络编程——TCP连接的建立和断开、滑动窗口
一.TCP段格式: TCP的段格式如下图所示: 源端口号与目的端口号:源端口号和目的端口号,加上IP首部的源IP地址和目的IP地址唯一确定一个TCP连接. 序号:序号表示在这个报文段中的第一个数据字节 ...
- UNIX网络编程——TCP/IP简介
一.ISO/OSI参考模型 OSI(open system interconnection)开放系统互联模型是由ISO(International Organization for Standardi ...
- JAVA基础知识之网络编程——-TCP/IP协议,socket通信,服务器客户端通信demo
OSI模型分层 OSI模型是指国际标准化组织(ISO)提出的开放系统互连参考模型(Open System Interconnection Reference Model,OSI/RM),它将网络分为七 ...
- unix网络编程——TCP套接字编程
TCP客户端和服务端所需的基本套接字.服务器先启动,之后的某个时刻客户端启动并试图连接到服务器.之后客户端向服务器发送请求,服务器处理请求,并给客户端一个响应.该过程一直持续下去,直到客户端关闭,给服 ...
- UNIX网络编程---TCP客户/服务器程序示例(五)
一.概述 客户从标准输入读入一行文本,并写给服务器 服务器从网络输入读入这行文本,并回射给客户 客户从网络输入读入这行回射文本,并显示在标准输出上 二.TCP回射服务器程序:main函数 这里给了函数 ...
随机推荐
- hdu5634 BestCoder Round #73 (div.1)
Rikka with Phi Accepts: 5 Submissions: 66 Time Limit: 16000/8000 MS (Java/Others) Memory Limit: ...
- bzoj1132[POI2008]Tro 计算几何
1132: [POI2008]Tro Time Limit: 20 Sec Memory Limit: 162 MBSubmit: 1722 Solved: 575[Submit][Status] ...
- bzoj1858[Scoi2010]序列操作 线段树
1858: [Scoi2010]序列操作 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 3079 Solved: 1475[Submit][Statu ...
- bzoj省选十连测推广赛
A.普通计算姬 题意:给丁一棵树,每个点有一个权值,用sum(x)表示以x为根的子树的权值和,要求支持两种操作: 1 u v :修改点u的权值为v. 2 l r : 求∑sum[i] l&l ...
- Mysql锁机制--并发事务带来的更新丢失问题
Mysql 系列文章主页 =============== 刚开始学习 Mysql 锁的时候,觉得 Mysql 使用的是行锁,再加上其默认的可重复读的隔离级别,那就应该能够自动解决并发事务更新的问题.可 ...
- 【转载】RAID写惩罚(Write Penalty)与IOPS计算
浅谈RAID写惩罚(Write Penalty)与IOPS计算 Character is what you are in the dark. 暗处最能反映一个人真正品格. ---------Apri ...
- jenkins部署.net平台自动化构建
在引入自动化部署工具的时候,我们对比了jenkins和gitlab CI,jenkins有非常丰富的插件,配置起来方便.gitlab CI更倾向于脚本配置,当然jenkins也可以使用pipeline ...
- java web中filter分析
摘自博客园,博主孤傲苍狼 一.Filter简介 Filter也称之为过滤器,它是Servlet技术中最激动人心的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp ...
- Spring + mybatis 集成
具体项目可参照:https://github.com/LuoXiaoyi/springmvc 一.环境准备:Spring4.3.5 + Mybatis3.4.6 + Mybatis-Spring 1. ...
- jQuery – AJAX load() 方法
jQuery load() 方法 jQuery load() 方法是简单但强大的 AJAX 方法. load() 方法从服务器加载数据,并把返回的数据放入被选元素中. 语法: $(selector). ...