Socket编程实践(5) --TCP粘包问题与解决
TCP粘包问题
由于TCP协议是基于字节流且无边界的传输协议, 因此很有可能产生粘包问题, 问题描述如下
对于Host A 发送的M1与M2两个各10K的数据块, Host B 接收数据的方式不确定, 有以下方式接收:
先接收M1, 再接收M2(正确方式)
先接收M2, 再接收M1(错误)
一次性收到20k数据(错误)
分两次收到,第一次15k,第二次5k(错误)
分两次收到,第一次5k,第二次15k(错误)
其他任何可能(错误)
粘包产生的原因
1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)
2、tcp传送的端 mss大小限制
3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割。
4、tcp的流量控制和拥塞控制,也可能导致粘包
5、tcp延迟发送机制等
TCP与UDP关于粘包问题的对比
|
TCP |
UDP |
|
字节流 |
数据报 |
|
无边界 |
有边界 |
|
对等方的一次读操作并不能保证完全把消息读完 |
对方接收数据包的个数是不确定的 |
粘包解决方案(本质上是要在应用层维护消息与消息的边界)
(1)定长包
该方式并不实用: 如果所定义的长度过长, 则会浪费网络带宽, 而又如果定义的长度过短, 则一条消息又会拆分成为多条, 仅在TCP的应用一层就增加了合并的开销, 何况在其他层(因此我在博客中并未给出定长包的示例, 而是将之(一个不太完善的实现)与使用自定义报头的示例放到了一起, 感兴趣的读者可以下载下来查看);
(2)包尾加\r\n(FTP使用方案)
如果消息本身含有\r\n字符,则也分不清消息的边界;
(3)报文长度+报文内容
(4)更复杂的应用层协议
readn / writen实现
Socket, 管道以及某些设备(特别是终端和网络)有下列两种性质:
1)一次read操作所返回的数据可能少于所要求的数据,即使还没到达文件尾端也可能这样,但这不是一个错误,应当继续读该设备;
2)一次write操作的返回值也可能少于指定输入的字节数.这可能是由于某个因素造成的,如:内核缓冲区满...但这也不是一个错误,应当继续写余下的数据(通常,只有非阻塞描述符,或捕捉到一个信号时,才发生这种write的中途返回)
在读写磁盘文件时从未见到过这种情况,除非是文件系统用完了空间,或者接近了配额限制,不能将所要求写的数据全部写出!
通常,在读/写一个网络设备,管道或终端时,需要考虑这些特性.于是,我们就有了下面的这两个函数:readn和writen,功能分别是读/写指定的count字节数据,并处理返回值可能小于要求值的情况:
/**实现:
这两个函数只是按需多次调用read和write系统调用直至读/写了count个数据
**/
/**返回值说明:
== count: 说明正确返回, 已经真正读取了count个字节
== -1 : 读取出错返回
< count: 读取到了末尾
**/
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nLeft = count;
ssize_t nRead = 0;
char *pBuf = (char *)buf;
while (nLeft > 0)
{
if ((nRead = read(fd, pBuf, nLeft)) < 0)
{
//如果读取操作是被信号打断了, 则说明还可以继续读
if (errno == EINTR)
continue;
//否则就是其他错误
else
return -1;
}
//读取到末尾
else if (nRead == 0)
return count-nLeft;
//正常读取
nLeft -= nRead;
pBuf += nRead;
}
return count;
}
/**返回值说明:
== count: 说明正确返回, 已经真正写入了count个字节
== -1 : 写入出错返回
**/
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nLeft = count;
ssize_t nWritten = 0;
char *pBuf = (char *)buf;
while (nLeft > 0)
{
if ((nWritten = write(fd, pBuf, nLeft)) < 0)
{
//如果写入操作是被信号打断了, 则说明还可以继续写入
if (errno == EINTR)
continue;
//否则就是其他错误
else
return -1;
}
//如果 ==0则说明是什么也没写入, 可以继续写
else if (nWritten == 0)
continue;
//正常写入
nLeft -= nWritten;
pBuf += nWritten;
}
return count;
}
报文长度+报文内容实践
发报文时:前四个字节长度+报文内容一次性发送;
收报文时:先读前四个字节,求出报文内容长度;根据长度读数据。
发送结构:
struct Packet
{
unsigned int msgLen; //数据部分的长度(网络字节序)
char text[1024]; //报文的数据部分
};
//server端echo部分的改进代码
void echo(int clientfd)
{
struct Packet buf;
int readBytes;
//首先读取首部
while ((readBytes = readn(clientfd, &buf.msgLen, sizeof(buf.msgLen))) > 0)
{
//网络字节序 -> 主机字节序
int lenHost = ntohl(buf.msgLen);
//然后读取数据部分
readBytes = readn(clientfd, buf.text, lenHost);
if (readBytes == -1)
err_exit("readn socket error");
else if (readBytes != lenHost)
{
cerr << "client connect closed..." << endl;
return ;
}
cout << buf.text;
//然后将其回写回socket
if (writen(clientfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)
err_exit("write socket error");
memset(&buf, 0, sizeof(buf));
}
if (readBytes == -1)
err_exit("read socket error");
else if (readBytes != sizeof(buf.msgLen))
cerr << "client connect closed..." << endl;
}
//client端发送与接收代码
...
struct Packet buf;
memset(&buf, 0, sizeof(buf));
while (fgets(buf.text, sizeof(buf.text), stdin) != NULL)
{
/**写入部分**/
unsigned int lenHost = strlen(buf.text);
buf.msgLen = htonl(lenHost);
if (writen(sockfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)
err_exit("writen socket error");
/**读取部分**/
memset(&buf, 0, sizeof(buf));
//首先读取首部
ssize_t readBytes = readn(sockfd, &buf.msgLen, sizeof(buf.msgLen));
if (readBytes == -1)
err_exit("read socket error");
else if (readBytes != sizeof(buf.msgLen))
{
cerr << "server connect closed... \nexiting..." << endl;
break;
}
//然后读取数据部分
lenHost = ntohl(buf.msgLen);
readBytes = readn(sockfd, buf.text, lenHost);
if (readBytes == -1)
err_exit("read socket error");
else if (readBytes != lenHost)
{
cerr << "server connect closed... \nexiting..." << endl;
break;
}
//将数据部分打印输出
cout << buf.text;
memset(&buf, 0, sizeof(buf));
}
...
完整实现代码:
http://download.csdn.net/detail/hanqing280441589/8460557
按行读取实践
recv/send函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
与read相比,recv只能用于套接字文件描述符,而且多了一个flags
recv的flags参数常用取值:
MSG_OOB(带外数据: 通过紧急指针发送的数据[需设置TCP头部紧急指针位有效])
This flag requests receipt of out-of-band data that would not be received
in the normal data stream. Some protocols place expedited data at the head of
the normal data queue, and thus this flag cannot be used with such protocols.
MSG_PEEK(可以读数据,但不从缓存区中读走[仅仅是一瞥],利用此特点可以方便的实现按行读取数据;一个一个字符的读,多次调用系统调用read方法,效率不高)
This flag causes the receive operation to return data from the beginning of
the receive queue without removing that data from the queue. Thus, a subsequent
receive call will return the same data.
/**示例: 通过MSG_PEEK封装一个recv_peek函数(仅查看数据, 但不取走)**/
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (true)
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果recv是由于被信号打断, 则需要继续(continue)查看
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
/**使用recv_peek实现按行读取readline(只能用于socket)**/
/** 返回值说明:
== 0: 对端关闭
== -1: 读取出错
其他: 一行的字节数(包含'\n')
**/
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nRead = 0;
int returnCount = 0;
char *pBuf = (char *)buf;
int nLeft = maxline;
while (true)
{
ret = recv_peek(sockfd, pBuf, nLeft);
//如果查看失败或者对端关闭, 则直接返回
if (ret <= 0)
return ret;
nRead = ret;
for (int i = 0; i < nRead; ++i)
//在当前查看的这段缓冲区中含有'\n', 则说明已经可以读取一行了
if (pBuf[i] == '\n')
{
//则将缓冲区内容读出
//注意是i+1: 将'\n'也读出
ret = readn(sockfd, pBuf, i+1);
if (ret != i+1)
exit(EXIT_FAILURE);
return ret + returnCount;
}
// 如果在查看的这段消息中没有发现'\n', 则说明还不满足一条消息,
// 在将这段消息从缓冲中读出之后, 还需要继续查看
ret = readn(sockfd, pBuf, nRead);;
if (ret != nRead)
exit(EXIT_FAILURE);
pBuf += nRead;
nLeft -= nRead;
returnCount += nRead;
}
//如果程序能够走到这里, 则说明是出错了
return -1;
}
readline实现思想:
在readline函数中,我们先用recv_peek”偷窥” 一下现在缓冲区有多少个字符并读取到pBuf,然后查看是否存在换行符'\n'。如果存在,则使用readn连同换行符一起读取(作用相当于清空socket缓冲区); 如果不存在,也清空一下缓冲区, 且移动pBuf的位置,回到while循环开头,再次窥看。注意,当我们调用readn读取数据时,那部分缓冲区是会被清空的,因为readn调用了read函数。还需注意一点是,如果第二次才读取到了'\n',则先用returnCount保存了第一次读取的字符个数,然后返回的ret需加上原先的数据大小。
按行读取echo代码:
void echo(int clientfd)
{
char buf[512] = {0};
int readBytes;
while ((readBytes = readline(clientfd, buf, sizeof(buf))) > 0)
{
cout << buf;
if (writen(clientfd, buf, readBytes) == -1)
err_exit("writen error");
memset(buf, 0, sizeof(buf));
}
if (readBytes == -1)
err_exit("readline error");
else if (readBytes == 0)
cerr << "client connect closed..." << endl;
}
client端读取与发送代码
...
char buf[512] = {0};
memset(buf, 0, sizeof(buf));
while (fgets(buf, sizeof(buf), stdin) != NULL)
{
if (writen(sockfd, buf, strlen(buf)) == -1)
err_exit("writen error");
memset(buf, 0, sizeof(buf));
int readBytes = readline(sockfd, buf, sizeof(buf));
if (readBytes == -1)
err_exit("readline error");
else if (readBytes == 0)
{
cerr << "server connect closed..." << endl;
break;
}
cout << buf;
memset(buf, 0, sizeof(buf));
}
...
完整代码实现:
http://download.csdn.net/detail/hanqing280441589/8460883
Socket编程实践(5) --TCP粘包问题与解决的更多相关文章
- 网络编程——TCP协议、UDP协议、socket套接字、粘包问题以及解决方法
网络编程--TCP协议.UDP协议.socket套接字.粘包问题以及解决方法 TCP协议(流式协议) 当应用程序想通过TCP协议实现远程通信时,彼此之间必须先建立双向通信通道,基于该双向通道实现数 ...
- Socket编程实践(6) --TCP服务端注意事项
僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 sign ...
- Java网络编程基础之TCP粘包拆包
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想象河里的流水,他们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实 ...
- 什么是TCP粘包?怎么解决这个问题
在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接.在TCP的socket编程中,发送端和接收端都有成对的socke ...
- 网络编程-SOCKET开发之----2. TCP粘包现象产生分析
1. 粘包现象及产生原因 1)概念 指TCP协议中,发送方发送的若干个包数据到接收方接收时粘成一包.发送方粘包:发送方把若干个要发送的数据包封装成一个包,一次性发送,减少网络IO延迟:接收方粘包:接收 ...
- 网络编程 TCP协议:三次握手,四次回收,反馈机制 socket套接字通信 粘包问题与解决方法
TCP协议:传输协议,基于端口工作 三次握手,四次挥手 TCP协议建立双向通道. 三次握手, 建连接: 1:客户端向服务端发送建立连接的请求 2:服务端返回收到请求的信息给客户端,并且发送往客户端建立 ...
- Socket编程实践(1) --TCP/IP简述
ISO的OSI OSI(open system interconnection)开放系统互联模型是由ISO国际标准化组织定义的网络分层模型,共七层, 从下往上为: OSI七层参考模型 物理层(Phys ...
- 02网络编程( socket套接字+TCP粘包 )
目录 02 网络编程 一.socket套接字编程 二.简易代码模板 2.1 服务端 2.2 客户端 三.通信循环及代码优化 四.黏包现象 五.struct模块 六.简易版本报头 七.上传文件数据 * ...
- TCP粘包/拆包问题
无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...
随机推荐
- Luogu P3740 [HAOI2014]贴海报_线段树
线段树版的海报 实际上这个与普通的线段树相差不大,只是貌似数据太水,暴力都可以过啊 本来以为要离散的,结果没打就A了 #include<iostream> #include<cstd ...
- vue.js-路由
1:编写router.js import Router from "vue-router" import Vue from "vue" import rou ...
- vue+node.js+webpack开发微信公众号功能填坑——组件按需引入
初次开发微信公众号,整体框架是经理搭建,小喽喽只是实现部分功能,整体页面效果 整个页面使用两个组件:布局 FlexBox,搜索框 Search,demo文档 http://vue.ydui.org/d ...
- 高效update方案
--方案1:如果有索引,先把索引删除后,再update,最后把索引重新创建一下因为索引对update影响很大. --方案2:1.create table newA as select id,name, ...
- [ Java学习基础 ] Java异常处理
一.异常概述 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的.比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error:如果你用Sys ...
- 让你的代码减少三倍!使用kotlin开发Android(五) 监听器
本文同步自 博主的私人博客wing的地方酒馆 在前面的博客中,有一个栗子,是点击按钮转跳的监听器. button.setOnClickListener { val user = User(" ...
- 使用redis构建文章投票系统
首先,我得说明这篇博客基本上就是<<redis in action>>第一章内容的读书笔记. 需求 首先,说明一下,我们的需求 用户可以发表文章,发表时,自己就默认的给自己的文 ...
- 使用Android Studio向GitHub提交代码
使用Android Studio向GitHub提交代码 在GitHub上创建一个仓库 填写仓库名称.描述.类型(公有.私有)等信息,点击创建 到此,成功创建了我们的仓库,并且初始化创建了README. ...
- Dynamics CRM EXCEL导入数据字段类型为选项集时的注意事项
在开始先展示下CRM的导入数据涉及选项集字段时的一个问题 下图是选项集字段的属性 下图是我要导入的excel中的列值,可以看出列明和字段名是一致的,而列值却不是选项集中已有的选项 在导入校验时,只要字 ...
- 关于ROS学习的一些反思
距离发布上一篇ROS的博客已经过去两年了,才发现原来自己已经这么久可没有写过关于ROS的文章,想来很是惭愧.这两年时间,自己怀着程序员的梦想,研究过RTOS,探索过Linux,编写过Android应用 ...