回射客户-服务器模型(4)

由于TCP是一种基于字节流的传输,属于无边界传输,所以它不能够处理消息与消息之间的边界问题,因此存在粘包问题。

如下图所示:


M1和M2是从主机A传送到主机B的两条消息,那么中间可能有几种传输情况:

a. 两条消息刚好分别完整的传输;

b. 先传输M1和M2的一部分,然后M2的另一部分单独传输;

c. 先传输M1的一部分,然后M1的另一部分和M2一起传输;


粘包产生的原因


有这几个原因会导致粘包问题:

a. 应用进程缓冲区的大小大于套接口缓冲区的大小,因此缓冲区中的数据一次性发送不完,产生粘包;

b. TCP层传递的数据段的最大限制为MSS,因此高层的数据如果超过这个值,也需要进行分割处理;

c. 链路层最大传输单元为MTU,因此,当上层的数据包的大小超过该值,需要分片;

d. 其他的导致粘包问题,比如TCP的流量控制,拥塞控制,延迟发送机制等都有可能导致粘包问题;


粘包解决方案

a. 本质上是要在应用层维护消息与消息的边界;

b. 发送定长包(让数据以定长的方式发送和接收);

c. 包尾加\r\n等分割符;

d. 设计更复杂的应用层协议;


下面我们以定长包的方式发送和接收数据包,主要封装了readn和writen函数。

服务器端:echosrv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

struct package{
	int len;
	char buf[1024];
};

ssize_t readn(int fd, void* buf, size_t count)
{
	//由于不能保证一次能够读取count个字节
	//因此我们需要循环进行读取
	//直到读取的字节数为count
	size_t nleft = count;
	ssize_t nread;
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)//如果信号中断
				continue;
			return -1;
		}
		if(nread == 0)
			//表示对等方关闭,这里直接返回
			return count-nleft;
		nleft -= nread;//每次读取后剩余的字节数
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	//我们每次希望写入的字节数为count
	size_t nleft = count;
	ssize_t nwritten;
	char* bufp = (char*)buf;
 	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			//什么都没发生
			continue;
		nleft -= nwritten;//每次写后剩余要写的字节数
		bufp += nwritten;
	}
	return count;
}

void do_service(int conn)
{
	struct package recvbuf;
	int n;
	while(1)
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		//包头与包体分开读取
		//先读取包头4个字节,进而确定包体的长度
		int ret = readn(conn,  &recvbuf.len, 4);
		if(ret == -1)
			ERR_EXIT("read failure");
		else if(ret < 4)
		{
			printf("client close\n");
			break;
		}

		//再读取包体
		//包体的长度n存放在结构体的len中
		n = ntohl(recvbuf.len);
		ret = readn(conn, recvbuf.buf, n);
		if(ret == -1)
			ERR_EXIT("read failure");
		else if(ret < n)
		{
			printf("client close\n");
			break;
		}
		fputs(recvbuf.buf, stdout);
		writen(conn, &recvbuf, 4+n);
	}
}

int main(void)
{
	//创建一个套接字
	int listenfd;
	if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		//初始化地址
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//地址族
		servaddr.sin_port = htons(5188);

		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本机的任意地址,推荐使用(转换成网络字节序)
		//也可以自己显式指定
//		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		//或者
//		inet_aton("127.0.0.1", &servaddr.sin_addr);
		
		//绑定之前开启地址重复利用
		int on = 1;
		if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
			ERR_EXIT("setsockopt_failure");

		//接下来进行绑定,将该套接字与一个本地地址进行绑定
		//需要将IPv4地址结构强制转换为通用地址结构
		if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("bind_failure");//绑定失败
		
		//接下来是监听,将socket从close状态转为监听状态才能够接受连接
		if(listen(listenfd, SOMAXCONN) < 0)
			ERR_EXIT("listen_failure"); 

		//定义一个对方的地址
		struct sockaddr_in peeraddr;
		socklen_t peerlen = sizeof(peeraddr);
		int conn; //一个新的套接字,称为已连接套接字(主动套接字)

		pid_t pid;
		while(1)
		{
			if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)				
				ERR_EXIT("accept_failure");
			//输出客户端的地址和端口		
			printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
			//一旦获得一个连接,就创建一个进程
			pid = fork();
			if(pid == -1)
				ERR_EXIT("fork_failure");
			if(pid == 0)
			{
				//让子进程处理已有的通信过程
				//不再需要监听套接口
				close(listenfd);
				do_service(conn);
				//一旦do_service函数返回,那么该进程就没有存在的价值了
				exit(EXIT_SUCCESS);//此时,为客户端开辟的进程也销毁了
			}
			else
				//父进程进行accept
				//不再需要连接套接口了,即conn(父子进程共享文件描述符)
				close(conn);
				
		}

		//实现一个回射客户/服务器模型
		//即客户端从标准输入获取数据,发送给服务器端,服务器端再回射过去
		
		return 0;
}


客户端:echocli.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

struct package{
	int len;
	char buf[1024];
};

ssize_t readn(int fd, void* buf, size_t count)
{
	size_t nleft = count; //剩余要读取的字节数
	ssize_t nread; //每次读取的字节数
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nread == 0)//对等方关闭
			return count-nleft;
		nleft -= nread;
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	size_t nleft = count; //剩余要读取的字节数
	ssize_t nwritten; //每次读取的字节数
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			continue;
		nleft -= nwritten;
		bufp += nwritten;
	}
	return count;	
}

int main(void)
{
	int sock;//创建一个套接字
	if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//地址族
		servaddr.sin_port = htons(5188);

		//自己显式指定服务器端地址
		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		
		//客户端不需要绑定(bind),也不需要监听(listen)
		//直接连接过去就可以
		if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("connect_failure");

		//如果连接成功,就可以进行通信
		struct package sendbuf;
		struct package recvbuf;
		memset(&sendbuf, 0, sizeof(sendbuf));
		memset(&recvbuf, 0, sizeof(recvbuf));
		int n;
		while(fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
		{
			n = strlen(sendbuf.buf);
			sendbuf.len = htonl(n);
			writen(sock, &sendbuf, 4+n);
			//先读取包头四个字节
			int ret = readn(sock, &recvbuf.len, 4);
			if(ret == -1)
				ERR_EXIT("read error");
			else if(ret == 0)
			{
				printf("peer close\n");
				break;
			}
			//再读取包体,头部长度存储在recvbuf.len中
			n = ntohl(recvbuf.len);
			ret = readn(sock, recvbuf.buf, n);
			if(ret == -1)
				ERR_EXIT("read error");
			else if(ret == 0)
			{
				printf("peerclose\n");
				break;
			}
			//显示出来
			fputs(recvbuf.buf, stdout);
			//这里需要清空缓冲区
			memset(&sendbuf, 0, sizeof(sendbuf));
			memset(&recvbuf, 0, sizeof(recvbuf));			
		}
		//关闭套接口
		close(sock);
		
		return 0;
}
说明:对于写writen,我们发送一个包的总长度为4+n,包括4字节的包头和n字节的包体;

            对于读readn,我们分别读取包头和包体,因为我们在发送的时候,将包体的长度n存放在了结构体的len变量中,因此需要先               读取包头,然后才能够获取包体的长度;


下面介绍另外一种方法解决粘包问题,我们封装一个readline函数,即按行读取消息。

先看代码:

服务器端:ehcosrv.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)


ssize_t readn(int fd, void* buf, size_t count)
{
	//由于不能保证一次能够读取count个字节
	//因此我们需要循环进行读取
	//直到读取的字节数为count
	size_t nleft = count;
	ssize_t nread;
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)//如果信号中断
				continue;
			return -1;
		}
		if(nread == 0)
			//表示对等方关闭,这里直接返回
			return count-nleft;
		nleft -= nread;//每次读取后剩余的字节数
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	//我们每次希望写入的字节数为count
	size_t nleft = count;
	ssize_t nwritten;
	char* bufp = (char*)buf;
 	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			//什么都没发生
			continue;
		nleft -= nwritten;//每次写后剩余要写的字节数
		bufp += nwritten;
	}
	return count;
}

ssize_t recv_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;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	//读取过程不一定要读取maxline个字节
	//只要遇到\n就可以返回
	int ret;
	int nread;
	char* bufp = buf;
	int nleft = maxline;
	while(1)
	{
		ret = recv_peek(sockfd, bufp, nleft);
		if(ret < 0)
			return ret;
		else if(ret == 0)
			return ret;
		nread = ret;
		int i;
		for(i = 0; i < nread; ++i)
		{
			if(bufp[i] == '\n')
			{
				//我们的recv_peek只是偷窥一下数据
				//并没有一走数据
				//所以这里用readn从缓冲区中移除已偷窥的数据
				ret = readn(sockfd, bufp, i+1);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//没有遇到\n
		if(nread > nleft)
			exit(EXIT_FAILURE);
		//把读到的数据nread个字节从缓冲区中移走
		nleft -= nread;
		ret = readn(sockfd, bufp, nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		//继续下一次的偷窥,需偏移
		bufp += nread;
	}
	return -1;
}

void do_service(int conn)
{
	char recvbuf[1024];
	int n;
	while(1)
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		int ret = readline(conn,  recvbuf, 1024);
		if(ret == -1)
			ERR_EXIT("read failure");
		if(ret == 0)
		{
			printf("client close\n");
			break;
		}
		fputs(recvbuf, stdout);
		writen(conn, recvbuf, strlen(recvbuf));
	}
}

int main(void)
{
	//创建一个套接字
	int listenfd;
	if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		//初始化地址
		struct sockaddr_in servaddr;
		memset(&servaddr, 0, sizeof(servaddr));
		servaddr.sin_family = AF_INET;//地址族
		servaddr.sin_port = htons(5188);

		servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本机的任意地址,推荐使用(转换成网络字节序)
		//也可以自己显式指定
//		servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
		//或者
//		inet_aton("127.0.0.1", &servaddr.sin_addr);
		
		//绑定之前开启地址重复利用
		int on = 1;
		if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
			ERR_EXIT("setsockopt_failure");

		//接下来进行绑定,将该套接字与一个本地地址进行绑定
		//需要将IPv4地址结构强制转换为通用地址结构
		if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
			ERR_EXIT("bind_failure");//绑定失败
		
		//接下来是监听,将socket从close状态转为监听状态才能够接受连接
		if(listen(listenfd, SOMAXCONN) < 0)
			ERR_EXIT("listen_failure"); 

		//定义一个对方的地址
		struct sockaddr_in peeraddr;
		socklen_t peerlen = sizeof(peeraddr);
		int conn; //一个新的套接字,称为已连接套接字(主动套接字)

		pid_t pid;
		while(1)
		{
			if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)				
				ERR_EXIT("accept_failure");
			//输出客户端的地址和端口		
			printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
			//一旦获得一个连接,就创建一个进程
			pid = fork();
			if(pid == -1)
				ERR_EXIT("fork_failure");
			if(pid == 0)
			{
				//让子进程处理已有的通信过程
				//不再需要监听套接口
				close(listenfd);
				do_service(conn);
				//一旦do_service函数返回,那么该进程就没有存在的价值了
				exit(EXIT_SUCCESS);//此时,为客户端开辟的进程也销毁了
			}
			else
				//父进程进行accept
				//不再需要连接套接口了,即conn(父子进程共享文件描述符)
				close(conn);
				
		}

		//实现一个回射客户/服务器模型
		//即客户端从标准输入获取数据,发送给服务器端,服务器端再回射过去
		
		return 0;
}


客户端:echocli.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define ERR_EXIT(m)	\
	do	\
	{	\
		perror(m);	\
		exit(EXIT_FAILURE);	\
	}while(0)

ssize_t readn(int fd, void* buf, size_t count)
{
	size_t nleft = count; //剩余要读取的字节数
	ssize_t nread; //每次读取的字节数
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nread == 0)//对等方关闭
			return count-nleft;
		nleft -= nread;
		bufp += nread;
	}
	return count;
}

ssize_t writen(int fd, void* buf, size_t count)
{
	size_t nleft = count; //剩余要读取的字节数
	ssize_t nwritten; //每次读取的字节数
	char* bufp = (char*)buf;
	while(nleft > 0)
	{
		if((nwritten = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			return -1;
		}
		if(nwritten == 0)
			continue;
		nleft -= nwritten;
		bufp += nwritten;
	}
	return count;	
}

ssize_t recv_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;
	}
}

ssize_t readline(int sockfd, void*buf, size_t maxline)
{
	char* bufp = buf;
	int nleft = maxline;
	int nread;
	int ret;
	while(1)
	{
		ret = recv_peek(sockfd, bufp, nleft);
		if(ret < 0)
			return ret;
		if(ret == 0)
			return ret;
		nread = ret;
		int i;
		for(i = 0; i < nread; ++i)
		{
			if(bufp[i] == '\n')
			{
				ret = readn(sockfd, bufp, i+1);
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//没有遇到\n
		if(nread > nleft)
			exit(EXIT_FAILURE);
		nleft -= nread;
		ret = readn(sockfd, bufp, nread);
		if(ret != nread)
			exit(EXIT_FAILURE);
		//继续下一次偷窥
		bufp += nread;
	}
	return -1;
}


int main(void)
{
	int sock;//创建一个套接字
	if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
//	if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0
		ERR_EXIT("socket_failure");
		
	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;//地址族
	servaddr.sin_port = htons(5188);

	//自己显式指定服务器端地址
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	
	//客户端不需要绑定(bind),也不需要监听(listen)
	//直接连接过去就可以
	if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
		ERR_EXIT("connect_failure");

	//连接成功,查看本地的端口和地址
	struct sockaddr_in localaddr;
	socklen_t addrlen = sizeof(localaddr);
	if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
		ERR_EXIT("getsockname error");
	
	printf("IP=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));

	//如果连接成功,就可以进行通信
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};
	while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
	{
		writen(sock, sendbuf, strlen(sendbuf));
		//先读取包头四个字节
		int ret = readline(sock, recvbuf, 1024);
		if(ret == -1)
			ERR_EXIT("read error");
		else if(ret == 0)
		{
			printf("peer close\n");
			break;
		}
		//显示出来
		fputs(recvbuf, stdout);
		//这里需要清空缓冲区
		memset(sendbuf, 0, sizeof(sendbuf));
		memset(recvbuf, 0, sizeof(recvbuf));			
	}
	//关闭套接口
	close(sock);
	
	return 0;
}

说明:我们首先利用recv函数封装了一个recv_peek函数,recv函数的特点就是,它仅仅从套接口缓冲区中接收数据到buffer中,但是并不会将数据从该缓冲区中移除,而read函数在读取完数据后会将缓冲区中的数据移除。因此,我们这里就可以先用recv_peek函数先对缓冲区中的内容进行“偷窥”,然后就能知道所“偷窥”内容中有没有分隔符(我们这里是\n),“偷窥”后再利用readn进行读取并将已读取的数据移除缓冲区,这样我们就能够实现数据的按行读取。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章