文档章节

TCP 粘包问题及解决方案

吃一堑消化不良
 吃一堑消化不良
发布于 2016/10/09 11:24
字数 1726
阅读 238
收藏 1
点赞 0
评论 0

一、TCP粘包问题

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

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

1、应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
2、TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
3、由于链路层最大发送单元MTU,在IP层会进行数据的分片。
这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。

4、发送端需要等缓冲区满才发送出去,造成粘包(由Nagle算法造成的发送端的粘包)
5、接收方不及时接收缓冲区的包,造成多个包接收(接收端接收不及时造成的接收端粘包)

二、粘包的问题的解决

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

1、发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。这里需要封装两个函数:

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

这两个函数的参数列表和返回值与read、write一致,作用是读取/写入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字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。


2、包尾加上\r\n标记。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;
}

 

3、包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。

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");
}

 

4、使用更加复杂的应用层协议。

本文转载自:http://www.cnblogs.com/QG-whz/p/5537447.html

共有 人打赏支持
吃一堑消化不良
粉丝 27
博文 187
码字总数 112458
作品 0
浦东
程序员
TCP粘包, UDP丢包, nagle算法

一、TCP粘包 1. 什么时候考虑粘包 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议,UDP不会...

Playboy002 ⋅ 2015/09/14 ⋅ 0

TCP协议粘包和拆包解析

TCP是一个"流"协议,没有边界的一段数据.像打开自来水管一样,连成一片,没有边界. TCP协议并不了解上层的业务在做什么东西,有什么含义,它会根据自己的缓冲区的实际情况进行数据包的划分. 所以,...

ParkJun ⋅ 2016/03/02 ⋅ 0

Netty5入门学习笔记002-TCP粘包/拆包问题的解决之道(上)

TCP网络通信时候会发生粘包/拆包的问题,接下来探讨其解决之道。 什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?主要原因是...

山东小木 ⋅ 2014/12/17 ⋅ 11

Node.js 中 TCP 粘包、分包解决方案 - Stick

StickPackage,NodeJs 中 TCP 粘包、分包解决方案! 配置介绍 提供对TCP粘包处理的解决方案 默认缓冲512个字节,当接收数据超过512字节,自动以512倍数扩大缓冲空间 本默认采用包头两个字节表...

匿名 ⋅ 2017/10/11 ⋅ 0

tcp协议数据传输“粘包”分析

这两天看csdn有一些关于socket粘包,socket缓冲区设置的问题,发现自己不是很清楚,所以查资料了解记录一下: 一 .两个简单概念长连接与短连接: 1.长连接 Client方与Server方先建立通讯连接...

长平狐 ⋅ 2013/12/25 ⋅ 0

关于tcp协议可靠性的个人理解

1、首先,因为tcp协议是可靠的,所以不存在丢包问题,也不存在包顺序错乱问题(udp会存在这个问题,这个时候需要自己使用序号之类的机制保证了,这里只讨论tcp)。 2、tcp传输会有粘包的问题...

libaineu2004 ⋅ 2017/12/13 ⋅ 0

Socket/WebSocket应用及IM粘包 分包等

> Socket/WebSocket应用 WebSocket的frame?google的protobuf在IM中的使用? IM、金融、股价、视频会议等这样一些应用来说,所需要的不过是高实时、低延时。比较好的可选方案呢?比较流行的是...

shareus ⋅ 04/28 ⋅ 0

TCP通信中的粘包问题

TCP通信中的粘包问题 尹德位 2015 西安 关键词 : TCP 网络通信 粘包 Linux C/S 一 粘包问题概述 二 粘包回避设计 第一章 粘包问题概述 1.1 描述背景 采用TCP协议进行网络数据传送的软件设计...

cnyinlinux ⋅ 2015/11/29 ⋅ 3

JAVA网络编程:解决TCP网络传输“粘包”问题

当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API)。TCP/IP传输层有两个并列的协议:TCP和UDP。其中TCP(transport control protocol,传输控制协议...

HenrySun ⋅ 2016/07/23 ⋅ 0

Netty 之入门应用

说明 系列文章:http://www.jianshu.com/p/594441fb9c9e 本文完全参考自《Netty权威指南(第2版)》,李林峰著。 Netty 环境搭建 例程使用构建工程,在pom文件中,加入Netty的依赖。 服务端程...

被称为L的男人 ⋅ 2017/09/02 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

从 Confluence 5.3 及其早期版本中恢复空间

如果你需要从 Confluence 5.3 及其早期版本中的导出文件恢复到晚于 Confluence 5.3 的 Confluence 中的话。你可以使用临时的 Confluence 空间安装,然后将这个 Confluence 安装实例升级到你现...

honeymose ⋅ 14分钟前 ⋅ 0

用ZBLOG2.3博客写读书笔记网站能创造今日头条的辉煌吗?

最近两年,著名的自媒体网站今日头条可以说是火得一塌糊涂,虽然从目前来看也遇到了一点瓶颈,毕竟发展到了一定的规模,继续增长就更加难了,但如今的今日头条规模和流量已经非常大了。 我们...

原创小博客 ⋅ 今天 ⋅ 0

MyBatis四大核心概念

本文讲解 MyBatis 四大核心概念(SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper)。 MyBatis 作为互联网数据库映射工具界的“上古神器”,训有四大“神兽”,谓之:Sql...

waylau ⋅ 今天 ⋅ 0

以太坊java开发包web3j简介

web3j(org.web3j)是Java版本的以太坊JSON RPC接口协议封装实现,如果需要将你的Java应用或安卓应用接入以太坊,或者希望用java开发一个钱包应用,那么用web3j就对了。 web3j的功能相当完整...

汇智网教程 ⋅ 今天 ⋅ 0

2个线程交替打印100以内的数字

重点提示: 线程的本质上只是一个壳子,真正的逻辑其实在“竞态条件”中。 举个例子,比如本题中的打印,那么在竞态条件中,我只需要一个方法即可; 假如我的需求是2个线程,一个+1,一个-1,...

Germmy ⋅ 今天 ⋅ 0

Springboot2 之 Spring Data Redis 实现消息队列——发布/订阅模式

一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式,这里利用redis消息“发布/订阅”来简单实现订阅者模式。 实现之前先过过 redis 发布订阅的一些基础概念和操...

Simonton ⋅ 今天 ⋅ 0

error:Could not find gradle

一.更新Android Studio后打开Project,报如下错误: Error: Could not find com.android.tools.build:gradle:2.2.1. Searched in the following locations: file:/D:/software/android/andro......

Yao--靠自己 ⋅ 昨天 ⋅ 0

Spring boot 项目打包及引入本地jar包

Spring Boot 项目打包以及引入本地Jar包 [TOC] 上篇文章提到 Maven 项目添加本地jar包的三种方式 ,本篇文章记录下在实际项目中的应用。 spring boot 打包方式 我们知道,传统应用可以将程序...

Os_yxguang ⋅ 昨天 ⋅ 0

常见数据结构(二)-树(二叉树,红黑树,B树)

本文介绍数据结构中几种常见的树:二分查找树,2-3树,红黑树,B树 写在前面 本文所有图片均截图自coursera上普林斯顿的课程《Algorithms, Part I》中的Slides 相关命题的证明可参考《算法(第...

浮躁的码农 ⋅ 昨天 ⋅ 0

android -------- 混淆打包报错 (warning - InnerClass ...)

最近做Android混淆打包遇到一些问题,Android Sdutio 3.1 版本打包的 错误如下: Android studio warning - InnerClass annotations are missing corresponding EnclosingMember annotation......

切切歆语 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部