07tcp粘包的原因以及處理的方法,網絡三大編程函數的使用

1流協議與粘包

在這裏插入圖片描述
說明
tcp 字節流 無邊界
udp 消息、數據報 有邊界
對等方,一次讀操作,不能保證完全把消息讀完。
對方接受數據包的個數是不確定的。

產生粘包問題的原因
	1、SQ_SNDBUF 套接字本身有緩衝區 (發送緩衝區、接受緩衝區)
	2、tcp傳送的端 mss大小限制
	3、鏈路層也有MTU大小限制,如果數據包大於>MTU要在IP層進行分片,導致消息分割。
	4、tcp的流量控制和擁塞控制,也可能導致粘包
	5、tcp延遲發送機制 等等
結論:tcp/ip協議,在傳輸層沒有處理粘包問題。

粘包解決方案   
	本質上是要在應用層維護消息與消息的邊界
	定長包
	包尾加\r\n(ftp)
	包頭加上包體長度
	更復雜的應用層協議

例1:包頭加上包體長度編程實踐

包頭加上包體長度
	發報文時,前四個字節長度(轉成網絡字節序)+包體
	收報文時,先讀前四個字節,求出長度;根據長度讀數據。

7client_stick1.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>


#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>

/*

	包頭+包體編程
*/

/*

	*******readn、writen、readline屬於同一個系列,稱爲網絡編程三大函數*******
	
1.1 read與write原型

  ssize_t  read(int fd, void* buf, size_t count);
  ssize_t 是有符號整數
  返回值如下:
	a)成功返回讀取的字節數,這裏可能等於 count 或者小於 count
	 	(當 count > 文件 size 的時候,返回實際讀到的字節數);	
	b)剛開始讀就遇到EOF 則返回 0;
	c)讀取失敗返回 -1, 並設置相應的 errno

	ssize_t write(int fd, const void *buf, size_t count); 
	返回值如下:
	
	a)成功返回寫入的字節數,這裏同上;
	b)寫入失敗返回 -1,並設置相應的 errno;
	c)當返回值爲0 時,表示什麼也沒有寫進去,這種情況在socket編程中出現可能是
	因爲連接已關閉,在寫磁盤文件的時候一般不會出現。
	


 1.2 爲什麼要封裝一個readn 函數和 writen 函數,現有的read 函數和 write 含有有什麼缺陷? 

    這個是因爲在調用read(或 write)函數的時候,讀(寫)一次的返回值可能不是我們想到讀
		的字節數(即read函數中的 count 參數),這經常在讀取管道,或者網絡數去時出現。


 1.3 readn 函數 和 writen 函數

	1.3.1 readn保證在沒有遇到EOF的情況下,一定可以讀取n個字節。它的返回值有三種:  
		a) >0,表示成功讀取的字節數,如果小於n,說明中間遇到了EOF;
		b)==0 表示一開始讀取就遇到EOF;
		c) -1 表示錯誤(這裏的errno絕對不是EINTR)

	1.3.2 writen函數保證一定寫滿n個字節,返回值:
		a)n 表示寫入成功n個字節
		b)-1 寫入失敗(這裏也沒有EINTR錯誤)
*/

/*
	tcp粘包處理:包頭+包體長度
	

*/


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

struct packet
{
	int len;		//長度在前,首地址與結構體的首地址是一致的
	char buf[1024];	//數據在後
} packet;

/*
	使用說明:
		//1一次全部讀走 //2次讀完數據 //出錯分析 //對方已關閉
	   
	思想:
		tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要循環
		讀指定長度的數據。
	按照count大小讀數據,若讀取的長度ssize_t<count 說明讀到了一個結束符,
	對方已關閉
	
	函數功能:
		從一個文件描述符中讀取count個字符到buf中
	參數:
		@buf:接受數據內存首地址
		@count:接受數據長度
	返回值:
		@ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗
*/
ssize_t readn(int fd, void *buf, size_t count)
{
	size_t nleft = count;	//剩下需要讀取的數據個數
	ssize_t nread;			//成功讀取的字節數
	char * bufp = (char*)buf;//將參數接過來
	while (nleft > 0) 
	{
		//如果errno被設置爲EINTR爲被信號中斷,如果是被信號中斷繼續,
		//不是信號中斷則退出。
		 if ((nread = read(fd, bufp, nleft)) < 0) 
		 {
		 	//異常情況處理
		 	if (errno == EINTR) //讀數據過程中被信號中斷了
		 		continue;	//再次啓動read
		 		//nread = 0;//等價於continue
		 	return -1;	
		 }else if (nread == 0)	//到達文件末尾EOF,數據讀完(讀文件、讀管道、socket末尾、對端關閉)
		 	break;
		 bufp += nread;	//將字符串指針向後移動已經成功讀取個數的大小。
		 nleft -=nread;	//需要讀取的個數=需要讀取的個數-已經成功讀取的個數
	}
	return (count - nleft);//返回已經讀取的數據個數
}

/*
	思想:tcpip是流協議,不能1次把指定長度數據,全部寫完 
	按照count大小寫數據
	若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。
	
	
	函數功能:
		向文件描述符中寫入count個字符
	函數參數:
		@buf:待寫數據首地址
		@count:待寫長度
	返回值:
		@ssize_t:返回寫的長度 -1失敗
*/
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 ((nwritten < 0) && (errno == EINTR)) //讀數據過程中被信號中斷了
		 		continue;			//再次啓動write
		 		//nwritten = 0;		//等價continue
		 	else
		 		return -1;
		 }
		 
		 bufp += nwritten;	//移動緩衝區指針
		 nleft -=nwritten;	//記錄剩下未讀取的數據
	}
	return count;//返回已經讀取的數據個數
}


void test()
{
	int sockfd = 0;
	const char *serverip = "192.168.66.128";
	
	//創建socket
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		ERR_EXIT("socket()");
	//定義socket結構體 man 7 ip
	
	struct sockaddr_in srvsddr;
	srvsddr.sin_family = AF_INET;
	srvsddr.sin_port = htons(8001);//轉化爲網絡字節序
	srvsddr.sin_addr.s_addr = inet_addr(serverip);
	//進程-》內核
	if( connect(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) < 0)
		ERR_EXIT("connect()");
	size_t n;
	struct packet sendbuf;
	struct packet recvbuf;
	memset(&sendbuf, 0, sizeof(sendbuf));
	memset(&recvbuf, 0, sizeof(recvbuf));
	
	while( fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL )
	{
		//獲取字符串的大小
		n = strlen(sendbuf.buf);
		//將n轉換爲網絡字節序
		sendbuf.len = htonl(n);
		//將獲取的包發送
		writen(sockfd, &sendbuf, 4 + n);
		
		//第一次 先讀取數據包的長度
		ssize_t ret = readn(sockfd, &recvbuf.len, 4);
		if (ret == -1) 
		{
			ERR_EXIT("readn");
		}else if (ret < 4)
		{
			printf("client close\n");
			break;
		}
		//轉換爲本地字節存儲
		n = ntohl(recvbuf.len);
		//第二次 根據數據長度讀取所有數據
		ret = readn(sockfd, recvbuf.buf, n);
		if (ret == -1) 
		{
			ERR_EXIT("readn");
		}else if (ret < n)
		{
			//因爲告訴了數據的長度,所以ret等於n纔是正確的
			printf("client close\n");//對於網絡來說,意味着對端關閉了
			break;
		}
		
		fputs(recvbuf.buf, stdout);
		memset(&sendbuf, 0, sizeof(sendbuf));
		memset(&recvbuf, 0, sizeof(recvbuf));
	}
	//異常處理
	close(sockfd);
	return ;
}

int main()
{
	test();
	return 0;
}

8server_stick1.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>

/*

	包頭+包體編程 
*/


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

struct packet
{
	int len;		//長度在前,首地址與結構體的首地址是一致的
	char buf[1024];	//數據在後
};


/*
	使用說明:
		//1一次全部讀走 //2次讀完數據 //出錯分析 //對方已關閉
	   
	思想:
		tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要循環
		讀指定長度的數據。
	按照count大小讀數據,若讀取的長度ssize_t<count 說明讀到了一個結束符,
	對方已關閉
	
	函數功能:
		從一個文件描述符中讀取count個字符到buf中
	參數:
		@buf:接受數據內存首地址
		@count:接受數據長度
	返回值:
		@ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗
*/
ssize_t readn(int fd, void *buf, size_t count)
{
	size_t nleft = count;	//剩下需要讀取的數據個數
	ssize_t nread;			//成功讀取的字節數
	char * bufp = (char*)buf;//將參數接過來
	while (nleft > 0) 
	{
		//如果errno被設置爲EINTR爲被信號中斷,如果是被信號中斷繼續,
		//不是信號中斷則退出。
		 if ((nread = read(fd, bufp, nleft)) < 0) 
		 {
		 	//異常情況處理
		 	if (errno == EINTR) //讀數據過程中被信號中斷了
		 		continue;	//再次啓動read
		 		//nread = 0;//等價於continue
		 	return -1;	
		 }else if (nread == 0)	//到達文件末尾EOF,數據讀完(讀文件、讀管道、socket末尾、對端關閉)
		 	break;
		 bufp += nread;	//將字符串指針向後移動已經成功讀取個數的大小。
		 nleft -=nread;	//需要讀取的個數=需要讀取的個數-已經成功讀取的個數
	}
	return (count - nleft);//返回已經讀取的數據個數
}

/*
	思想:tcpip是流協議,不能1次把指定長度數據,全部寫完 
	按照count大小寫數據
	若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。
	
	
	函數功能:
		向文件描述符中寫入count個字符
	函數參數:
		@buf:待寫數據首地址
		@count:待寫長度
	返回值:
		@ssize_t:返回寫的長度 -1失敗
*/
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 ((nwritten < 0) && (errno == EINTR)) //讀數據過程中被信號中斷了
		 		continue;			//再次啓動write
		 		//nwritten = 0;		//等價continue
		 	else
		 		return -1;
		 }
		 
		 bufp += nwritten;	//移動緩衝區指針
		 nleft -=nwritten;	//記錄剩下未讀取的數據
	}
	return count;//返回已經讀取的數據個數
}

void do_service(int conn)
{
	struct packet recvbuf;
	int n;
	while (1) 
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		//讀取包頭 4個字節
		int ret = readn(conn, &recvbuf.len, 4);
		if (ret == -1) 
			ERR_EXIT("read()");
		//如果讀取的個數小於4,則客服端已經關閉
		else if (ret < 4)
		{
			printf("client close\n");
			break;
		}
		//將網絡數據轉換爲本地數據結構,比如網絡數據爲大端,而本地數據爲小端
		n = ntohl(recvbuf.len);
		//根據包頭裏包含的數據大小讀取數據
		ret = readn(conn, recvbuf.buf, n);
		if (ret < -1) 
			ERR_EXIT("read");
		else if (ret < n)
		{
			printf("client close\n");
			break;
		}
		//將數據打印輸出
		fputs(recvbuf.buf, stdout);
		//將接收到的數據再直接發出去
		writen(conn, &recvbuf, 4 + n);//注意寫數據的時候,多加4個字節	 
	}	
}

void test()
{
	int sockfd = 0;
	int conn = 0;
	const char *serverip = "192.168.66.128";
	
	//創建socket
	//創建socket
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		ERR_EXIT("socket()");
	
	//定義socket結構體 man 7 ip
	struct sockaddr_in srvsddr;
	srvsddr.sin_family = AF_INET;
	srvsddr.sin_port = htons(8001);//轉化爲網絡字節序
	//第一種
	#if 0
	srvsddr.sin_addr.s_addr = inet_addr(serverip);
	#endif
	//第二種
	#if 0
	//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在網絡字節序
	//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //綁定本機的任意一個地址
	#endif
	//第三種
	//建議使用這種
	#if 1
	int ret;
	ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
	if (ret == 0)
	{
		ERR_EXIT("inet_pton()");
	}	
	#endif
	
	//設置端口複用
	//使用SO_REUSEADDR選項可以使得不必等待TIME_WAIT狀態消失就可以重啓服務器
	int optval = 1;
    if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
		ERR_EXIT("setsockopt()");
		
	if(bind(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) <0 )
		ERR_EXIT("bind()");
		
	if(listen(sockfd, SOMAXCONN) < 0)
		ERR_EXIT("listen()");
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen = sizeof(peeraddr);//值-結果參數
	pid_t pid;
	
	while (1) 
	{
		if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) 
			ERR_EXIT("accept()");
			
		printf("客戶端的ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
				
		pid = fork(); 
		if (pid == -1) 
		{
			ERR_EXIT("fork()");
		}
		if (pid == 0) 
		{
			//子進程不需要監聽socket
			close(sockfd);
			do_service(conn);
			exit(EXIT_SUCCESS);
		}else 
		{
			close(conn);//父進程不需要連接socket
		} 
	}
	return ;
}

int main()
{
	test();
	return 0;
}

在這裏插入圖片描述

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