回射客戶-服務器模型(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進行讀取並將已讀取的數據移除緩衝區,這樣我們就能夠實現數據的按行讀取。



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