① TCP是个流协议,它存在粘包问题

TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的。这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的。TCP的发送方无法保证对等方每次接收到的是一个完整的数据包。主机A向主机B发送两个数据包,主机B的接收情况可能是

产生粘包问题的原因有以下几个:

  • 第一 。应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
  • 第二种情况是,TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
  • 第三种情况由于链路层最大发送单元MTU,在IP层会进行数据的分片。

这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。

② 粘包的问题的解决思路
粘包问题的最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪。我们通过使用某种方案给出边界,例如:

  • 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
  • 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
  • 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。
  • 使用更加复杂的应用层协议。

③ 粘包解决方案一:使用定长包
这里需要封装两个函数:

ssize_t readn(int fd, void *buf, size_t count)
ssize_t writen(int fd, void *buf, size_t count)

这两个函数的参数列表和返回值与readwrite一致。它们的作用的读取/写入count个字节后再返回。其实现如下:

ssize_t readn(int fd, void *buf, size_t count)
{
int left = count ; //剩下的字节
char * ptr = (char*)buf ;
while(left>0)
{
int readBytes = read(fd,ptr,left);
if(readBytes< 0)//read函数小于0有两种情况:1中断 2出错
{
if(errno == EINTR)//读被中断
{
continue;
}
return -1;
}
if(readBytes == 0)//读到了EOF
{
//对方关闭呀
printf("peer close\n");
return count - left;
}
left -= readBytes;
ptr += readBytes ;
}
return count ;
} /*
writen 函数
写入count字节的数据
*/
ssize_t writen(int fd, void *buf, size_t count)
{
int left = count ;
char * ptr = (char *)buf;
while(left >0)
{
int writeBytes = write(fd,ptr,left);
if(writeBytes<0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(writeBytes == 0)
continue;
left -= writeBytes;
ptr += writeBytes;
}
return count;
}

有了这两个函数之后,我们就可以使用定长包来发送数据了,我抽取其关键代码来讲诉:

char readbuf[512];
readn(conn,readbuf,sizeof(readbuf)); //每次读取512个字节

同理的,写入的时候也写入512个字节

char writebuf[512];
fgets(writebuf,sizeof(writebuf),stdin);
writen(conn,writebuf,sizeof(writebuf);

每个消息都以固定的512字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。

④ 粘包解决方案二:使用结构体,显式说明数据部分的长度

在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。发送端的对等方接收报文时,先读取前四个字节,获取数据的长度,由长度来进行数据的读取。定义一个结构体

struct packet
{
unsigned int msgLen ; //4个字节字段,说明数据部分的大小
char data[512] ; //数据部分
}

读写过程如下所示,这里抽取关键代码进行说明:

//发送数据过程
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
while(fgets(writebuf.data,sizeof(writebuf.data),stdin)!=NULL)
{
int n = strlen(writebuf.data); //计算要发送的数据的字节数
writebuf.msgLen =htonl(n); //将该字节数保存在msgLen字段,注意字节序的转换
writen(conn,&writebuf,4+n); //发送数据,数据长度为4个字节的msgLen 加上data长度
memset(&writebuf,0,sizeof(writebuf));
}

下面是读取数据的过程,先读取msgLen字段,该字段指示了有效数据data的长度。依据该字段再读出data。

  memset(&readbuf,0,sizeof(readbuf));
int ret = readn(conn,&readbuf.msgLen,4); //先读取四个字节,确定后续数据的长度
if(ret == -1)
{
err_exit("readn");
}
else if(ret == 0)
{
printf("peer close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
int readBytes = readn(conn,readbuf.data,dataBytes); //读取出后续的数据
if(readBytes == 0)
{
printf("peer close\n");
break;
}
if(readBytes<0)
{
err_exit("read");
}

⑤ 粘包解决方案三:按行读取
ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

与read函数相比,recv函数的区别在于两点:

  1. recv函数只能够用于套接口IO。
  2. recv函数含有flags参数,可以指定一些选项。

recv函数的flags参数常用的选项是:

  1. MSG_OOB 接收带外数据,即通过紧急指针发送的数据
  2. MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据

为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。

/*
* 封装了recv函数
返回值说明:-1 读取出错
*/
ssize_t read_peek(int sockfd,void *buf ,size_t len)
{
while(1)
{
//从缓冲区中读取,但不清除缓冲区
int ret = recv(sockfd,buf,len,MSG_PEEK);
if(ret == -1 && errno == EINTR)//文件读取中断
continue;
return ret;
}
}

下面是按行读取的代码:

/*
*读取一行内容
* 返回值说明:
== 0 :对端关闭
== -1 : 读取错误
其他:一行的字节数,包含\n
*
**/
ssize_t readLine(int sockfd ,void * buf ,size_t maxline)
{
int ret ;
int nRead = 0;
int left = maxline ;
char * pbuf = (char *) buf;
int count = 0;
while(true)
{
//从socket缓冲区中读取指定长度的内容,但并不删除
ret = read_peek(sockfd,pbuf,left);
// ret = recv(sockfd , pbuf , left , MSG_PEEK);
if(ret<= 0)
return ret;
nRead = ret ;
for(int i = 0 ;i< nRead ; ++i)
{
if(pbuf[i]=='\n') //探测到有\n
{
ret = readn (sockfd , pbuf, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret + returnCount;
}
}
//如果嗅探到没有\n
//那么先将这一段没有\n的读取出来
ret = readn(sockfd , pbuf , nRead);
if(ret != nRead)
exit(EXIT_FAILURE);
pbuf += nRead ;
left -= nRead ;
count += nRead;
}
return -1;
}

⑥ 实例程序
下面的链接中包含了上面提到的几种方案的代码,各个函数封装在common.h头文件中,TCP粘包解决方案

文章链接:http://www.cnblogs.com/QG-whz/p/5537447.html

原文:https://www.cnblogs.com/QG-whz/p/5537447.html

TCP粘包问题及解决方案的更多相关文章

  1. Socket编程(4)TCP粘包问题及解决方案

    ① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...

  2. TCP粘包拆包基本解决方案

    上个小节我们浅析了在Netty的使用的时候TCP的粘包和拆包的现象,Netty对此问题提供了相对比较丰富的解决方案 Netty提供了几个常用的解码器,帮助我们解决这些问题,其实上述的粘包和拆包的问题, ...

  3. TCP粘包问题的解决方案01——自定义包体

      粘包问题:应用层要发送数据,需要调用write函数将数据发送到套接口发送缓冲区.如果应用层数据大小大于SO_SNDBUF,那么,可能产生这样一种情况,应用层的数据一部分已经被发送了,还有一部分还在 ...

  4. TCP粘包问题的解决方案02——利用readline函数解决粘包问题

      主要内容: 1.read,write 与 recv,send函数. recv函数只能用于套接口IO ssize_t recv(int sockfd,void * buff,size_t len,i ...

  5. UNIX网络编程——tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...

  6. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

  7. tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...

  8. TCP 粘包及其解决方案(zz)

    首先,我们回顾一下 TCP 和 UDP 的头部信息: 具体说明看:http://www.cnblogs.com/aomi/p/7776582.html 我们知道,TCP 和 UDP 是 TCP/IP ...

  9. 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案

    引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...

随机推荐

  1. 【Python之路】第二十二篇--Django【基础篇】

    1 Django流程介绍 MTV模式       著名的MVC模式:所谓MVC就是把web应用分为模型(M),控制器(C),视图(V)三层:他们之间以一种插件似的,松耦合的方式连接在一起. 模型负责业 ...

  2. mysql replace 使用注意,update的时候 删除从表数据

    使用REPLACE插入一条记录时,如果不重复,REPLACE就和INSERT的功能一样,如果有重复记录,REPLACE就使用新记录的值来替换原来的记录值. 使用REPLACE的最大好处就是可以将DEL ...

  3. When an HTTP server receives a request for a CGI script

    cgicc: Overview of the Common Gateway Interface https://www.gnu.org/software/cgicc/doc/cgi_overview. ...

  4. Spring Data HelloWorld(三)

    在 Spring Data 环境搭建(二) 的基础之上 我们改动  http://www.cnblogs.com/fzng/p/7253068.html 定义个一个接口  继承Repository类 ...

  5. DRF(5) - 频率组件、url注册器、响应器、分页器

    一.频率组件 1.使用DRF简单频率控制实现对用户进行访问频率控制 1)导入模块,定义频率类并继承SimpleRateThrottle # 导入模块 from rest_framework.throt ...

  6. POJ 3613 Cow Relays (floyd + 矩阵高速幂)

    题目大意: 求刚好经过K条路的最短路 我们知道假设一个矩阵A[i][j] 表示表示 i-j 是否可达 那么 A*A=B  B[i][j]  就表示   i-j 刚好走过两条路的方法数 那么同理 我们把 ...

  7. HDU1712:ACboy needs your help(分组背包)

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=1712 解释看这里:http://www.cnblogs.com/zhangmingcheng/p/3940 ...

  8. python之路 前段之html,css

    一.HTML 超级文本标记语言是标准通用标记语言下的一个应用,也是一种规范,一种标准, 它通过标记符号来标记要显示的网页中的各个部分.网页文件本身是一种文本文件,通过在文本文件中添加标记符,可以告诉浏 ...

  9. oracle 数据泵

      Oracle数据库导入导出工具,可以使用exp/imp,但这是比较早期的工具.本文主要介绍数据泵expdp/impdp工具的使用.   1.建立数据泵目录 使用数据泵需要先建directory c ...

  10. LigerUI v1.2.4 LigerGrid默认样式 工具条背景白色

    修改Aqua的ligerui-grid.css .l-panel-topbar 样式 修改为: .l-panel-topbar{padding: 0;background: #CEDFEF url(' ...