Socket编程--TCP粘包问题
2016-08-14 20:41
405 查看
TCP是个流协议,它存在粘包问题
产生粘包的原因是:
TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
由于链路层最大发送单元MTU,在IP层会进行数据的分片。
应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
粘包问题的解决
①:发送定长包
这里需要封装两个函数:
这两个函数的参数列表和返回值与
有了这两个函数之后,我们就可以使用定长包来发送数据了,我抽取其关键代码来讲诉:
每个消息都以固定的512字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。
②:粘包解决方案二:使用结构体,显式说明数据部分的长度
在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。发送端的对等方接收报文时,先读取前四个字节,获取数据的长度,由长度来进行数据的读取。定义一个结构体
读写过程如下所示,这里抽取关键代码进行说明:
下面是读取数据的过程,先读取msgLen字段,该字段指示了有效数据data的长度。依据该字段再读出data。
③:粘包解决方案三:按行读取
ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:
与read函数相比,recv函数的区别在于两点:
recv函数只能够用于套接口IO。
recv函数含有flags参数,可以指定一些选项。
recv函数的flags参数常用的选项是:
MSG_OOB 接收带外数据,即通过紧急指针发送的数据
MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据
为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。
产生粘包的原因是:
TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
由于链路层最大发送单元MTU,在IP层会进行数据的分片。
应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
粘包问题的解决
①:发送定长包
这里需要封装两个函数:
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字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。
②:粘包解决方案二:使用结构体,显式说明数据部分的长度
在这个方案中,我们需要定义一个‘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函数的区别在于两点:
recv函数只能够用于套接口IO。
recv函数含有flags参数,可以指定一些选项。
recv函数的flags参数常用的选项是:
MSG_OOB 接收带外数据,即通过紧急指针发送的数据
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; }
相关文章推荐
- 网络编程与并发-TCP/UDP套接字、粘包问题、Socket编程、并发编程、FTP作业
- Socket编程(4)TCP粘包问题及解决方案
- Linux下的socket编程实践(四)TCP的粘包问题和常用解决方案
- 网络编程与并发-TCP/UDP套接字、粘包问题、Socket编程、并发编程、FTP作业
- JAVA Socket编程学习10--解决TCP粘包分包问题
- Socket编程实践(5) --TCP粘包问题与解决
- Socket编程实践(5) --TCP粘包问题与解决
- Socket编程 TCP粘包问题及解决方案
- C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性
- iOS经典讲解之socket编程”粘包“问题
- Socket/TCP粘包、多包和少包, 断包 问题
- golang中tcp socket粘包问题和处理
- golang中tcp socket粘包问题和处理
- 【转】TCP Socket服务器编程,粘包
- linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案
- Tcp编程常见问题及解决方法总结(粘包,拆包)
- socket编程之解决流协议的粘包问题(一 )
- Socket学习之解决TCP半包粘包问题
- Java网络编程(五)socket的半包,粘包与分包的问题
- C语言socket编程的分包和粘包的有关问题解决