TCP粘包原因与解决

转自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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值