TCP粘包原因与解决

来源:互联网 发布:京挑客 网站域名 编辑:程序博客网 时间:2024/05/29 03:08

转自http://www.tuicool.com/articles/vaE3iq


流协议与粘包

粘包的表现

Host A 发送数据给 Host B; 而Host B 接收数据的方式不确定


粘包产生的原因

说明

TCP

字节流,无边界

对等方,一次读操作,不能保证完全把消息读完

UDP

数据报,有边界

对方接受数据包的个数是不确定的

产生粘包问题的原因分析

1、 SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)

2、 tcp传送的端 mss大小限制

3、 链路层也有MTU大小限制 ,如果数据包大于>MTU要在IP层进行分片,导致消息分割。

4、tcp的流量控制和拥塞控制,也可能导致粘包

5、tcp延迟发送机制等

结论:tcp/ip协议,在传输层没有处理粘包问题。

粘包解决方案(本质上是要在应用层维护消息与消息的边界)

定长包

包尾加\r\n(ftp)

包头加上包体长度(如下)

更复杂的应用层协议

编程实践-readn && writen

管道,FIFO以及某些设备(特别是终端和网络)有下列两种性质:

1)一次read操作所返回的数据可能少于所要求的数据,即使还没到达文件尾端也可能这样,但这不是一个错误,应当继续读该设备;

2)一次write操作的返回值也可能少于指定输入的字节数.这可能是由于某个因素造成的,如:内核缓冲区满...但这也不是一个错误,应当继续写余下的数据(通常,只有非阻塞描述符,或捕捉到一个信号时,才发生这种write的中途返回)

在读写磁盘文件时从未见到过这种情况,除非是文件系统用完了空间,或者接近了配额限制,不能将所要求写的数据全部写出!

通常,在读,写一个网络设备,管道或终端时,需要考虑这些特性.于是,我们就有了下面的这两个函数:readn和writen,功能分别是读\写指定的N字节数据,并处理返回值可能小于要求值的情况:

ssize_t readnint fd, void *buf, size_t count);ssize_t writen(int fd, const void *buf, size_t count);

返回值:

读\写的字节数;若出错,返回-1

实现:

这两个函数只是按需多次调用read和write系统调用直至读\写了N个数据

ssize_t readn(int fd,void *buf,size_t count){size_t nLeft = count;ssize_t nRead = 0;char *ptr = static_cast<char *>(buf);while (nLeft > 0){if ((nRead = read(fd,ptr,nLeft)) < 0){//一点东西都没读if (nLeft == count){return -1;  //error}else{break;  //error, return amount read so far}}else if (nRead == 0){break;  //EOF}nLeft -= nRead;ptr += nRead;}return count - nLeft;}ssize_t writen(int fd, const void *buf, size_t count){size_t nLeft = count;ssize_t nWritten;const char *ptr = static_cast<const char *>(buf);while (nLeft > 0){if ((nWritten = write(fd,ptr,nLeft)) < 0){//一点东西都没写if (nLeft == count){return -1;  //error}else{break;  //error, return amount write so far}}else if (nWritten == 0){break;  //EOF}nLeft -= nWritten;ptr += nWritten;}return count - nWritten;}

报头加上报文长度编程实践

报文结构 :

struct TransStruct{    int m_length;   //报头:保存数据m_text的真实数据长度    char m_text[BUFSIZ];    //报文:保存真正要发送的数据};

发报文时:前四个字节长度 + 报文

收报文时:先读前四个字节,求出长度;根据长度读数据

//server端完整代码及解析#include "commen.h"//echo 服务器writen,readn 版int main(){int sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd == -1){err_exit("socket error");}//添加地址复用int optval = 1;if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1){err_exit("setsockopt SO_REUSEADDR error");}//绑定struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8002);serverAddr.sin_addr.s_addr = INADDR_ANY;//绑定本机的任意一个IP地址if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1){err_exit("bind error");}//启动监听套接字if (listen(sockfd,SOMAXCONN) == -1){err_exit("listen error");}struct sockaddr_in peerAddr;socklen_t peerLen = sizeof(peerAddr);while (true){//接受链接int peerSockfd = accept(sockfd, (struct sockaddr *)&peerAddr,&peerLen);if (peerSockfd == -1){err_exit("accept error");}//打印客户信息cout << "Client:" << endl;cout << "\tsin_port: " << ntohs(peerAddr.sin_port) << endl;cout << "\tsin_addr: " << inet_ntoa(peerAddr.sin_addr) << endl;cout << "\tsocket: " << peerSockfd << endl;//每有一个客户端连接进来,就fork一个子进程,//相应的业务处理由子进程完成,父进程继续监听pid_t pid = fork();if (pid == -1){close(sockfd);close(peerSockfd);err_exit("fork error");}else if (pid == 0)  //子进程,处理业务{close(sockfd);  //子进程关闭监听套接字,因为子进程不负责监听任务struct TransStruct recvBuf;ssize_t readCount = 0;while (true){memset(&recvBuf,0,sizeof(recvBuf));//首先,从客户端读取报头长度if ((readCount = readn(peerSockfd,&(recvBuf.m_length),4)) == -1){err_exit("readn error");}else if (readCount == 0)//如果链接关闭{peerClosePrint("client connect closed");}//根据报文实际长度,读取数据if ((readCount = readn(peerSockfd,&(recvBuf.m_text),recvBuf.m_length)) == -1){err_exit("readn error");}else if (readCount == 0){peerClosePrint("client connect closed");}//将整体报文回写回客户端if (writen(peerSockfd,&recvBuf,recvBuf.m_length+4) == -1){err_exit("writen error");}recvBuf.m_text[recvBuf.m_length] = 0;//写至终端fputs(recvBuf.m_text,stdout);}}else if (pid > 0)   //父进程{close(peerSockfd);}}close(sockfd);return 0;}

//client端完整代码实现及解析#include "commen.h"int main(){int sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd == -1){err_exit("socket error");}//填写好服务器地址及其端口号struct sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8002);serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1){err_exit("connect error");}int readCount = 0;struct TransStruct sendBuf;struct TransStruct recvBuf;//从键盘输入数据while (fgets(sendBuf.m_text,sizeof(sendBuf.m_text),stdin) != NULL){//保存的是真实报文的长度sendBuf.m_length = strlen(sendBuf.m_text);//向server发送数据....+4的原因:需要添加报首的4个字节报头的长度if (writen(sockfd,&sendBuf,sendBuf.m_length+4) == -1){err_exit("write socket error");}//首先,从server端接收将要发送的数据报的长度if ((readCount = readn(sockfd,&(recvBuf.m_length),4)) == -1){err_exit("read socket error");}else if (readCount == 0){peerClosePrint("client connect closed");}//然后,根据从server端读来的报文长度,读取报文if ((readCount = readn(sockfd,&(recvBuf.m_text),recvBuf.m_length)) == -1){err_exit("read socket error");}else if (readCount == 0){peerClosePrint("client connect closed");}recvBuf.m_text[recvBuf.m_length] = 0;//将其回写到终端fputs(recvBuf.m_text,stdout);memset(&sendBuf,0,sizeof(sendBuf));memset(&recvBuf,0,sizeof(recvBuf));}close(sockfd);return 0;}


附 -commen.h 完整代码及解析
#ifndef COMMEN_H_INCLUDED#define COMMEN_H_INCLUDED#include <unistd.h>#include <signal.h>#include <errno.h>#include <fcntl.h>#include <sys/types.h>#include <sys/wait.h>#include <sys/stat.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/msg.h>#include <sys/sem.h>#include <sys/socket.h>#include <arpa/inet.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#include <stdio.h>#include <stdlib.h>#include <iostream>using namespace std;//报文结构struct TransStruct{int m_length;   //报头:保存数据m_text的真实数据长度char m_text[BUFSIZ];//报文:保存真正要发送的数据};//出错退出void err_exit(std::string str){perror(str.c_str());exit(EXIT_FAILURE);}//对端关闭链接退出void peerClosePrint(std::string str = "peer connect closed"){cout << str << endl;_exit(0);}//信号捕获函数:上一篇博客中的代码需要使用的void onSignal(int signalNumber){switch (signalNumber){case SIGUSR1:cout << "child receive SIGUSR1" << signalNumber << endl;_exit(0);case SIGUSR2:cout << "parent receive SIGUSR2: " << signalNumber << endl;_exit(0);default:cout << "RECV OTHRER SIGNAL" << endl;}}//经典的readn函数(来源:APUE)ssize_t readn(int fd,void *buf,size_t count){size_t nLeft = count;ssize_t nRead = 0;char *ptr = static_cast<char *>(buf);while (nLeft > 0){if ((nRead = read(fd,ptr,nLeft)) < 0){//一点东西都没读if (nLeft == count){return -1;  //error}else{break;  //error, return amount read so far}}else if (nRead == 0){break;  //EOF}nLeft -= nRead;ptr += nRead;}return count - nLeft;}//经典的writen函数(来源:APUE)ssize_t writen(int fd, const void *buf, size_t count){size_t nLeft = count;ssize_t nWritten;const char *ptr = static_cast<const char *>(buf);while (nLeft > 0){if ((nWritten = write(fd,ptr,nLeft)) < 0){//一点东西都没写if (nLeft == count){return -1;  //error}else{break;  //error, return amount write so far}}else if (nWritten == 0){break;  //EOF}nLeft -= nWritten;ptr += nWritten;}return count - nWritten;}#endif // COMMEN_H_INCLUDED















0 0
原创粉丝点击