基於TCP協議的服務器/客戶端程序

作爲傳輸層的主要協議,TCP協議不僅可以支持本地的數據通信,還可以支持跨網絡的進程間通信。

在偌大的互聯網中,我們可以通過“IP地址+端⼜號”標識互聯網中唯一的一個進程。然而,“IP地址+端⼜號”就稱爲socket,這就是網絡socket編程

在TCP協議中,建⽴連接的兩個進程各⾃有⼀個socket來標識,那麼這兩個socket組成 的socketpair就唯⼀標識⼀個連接。

socket本⾝有“插座”的意思,因此⽤來描述⽹絡連接的⼀ 對⼀關係。


要寫出一個基於TCP的網絡服務程序,我們應該具有以下的知識:

  1. 發送主機通常將發送緩衝區中的數據按內存地址從低到⾼的順序發出,接收主機把從⽹絡上接到的字節依次保存在接收緩衝區中,也是按內存地址從低到⾼的順序保存。
  2. TCP/IP協議規定,⽹絡數據流應採⽤⼤端字節序,即低地址⾼字節。
  3. socket API是⼀層抽象的⽹絡編程接⼜,適⽤於各種底層⽹絡協議,如IPv4、 IPv6,以及後⾯要講的UNIX Domain Socket。

       IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址⽤sockaddr_in結構體表⽰,包括16位端⼜號和32位IP地址,IPv6地址⽤sockaddr_in6結構體表⽰,包括16位端⼜號、 128位IP地址和⼀些 控制字段。 UNIX Domain Socket的地址格式定義在sys/un.h中,⽤sockaddr_un結構體表⽰。各 種socket地址結構體的開頭都是相同的,前16位表⽰整個結構體的長度(並不是所有UNIX的實現 都有長度字段,如Linux就沒有),後16位表⽰地址類型。 IPv4、 IPv6和UNIXDomain Socket的地 址類型分別定義爲常數AF_INET、 AF_INET6、 AF_UNIX。這樣,只要取得某種sockaddr結構體的 ⾸地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的 內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例 如bind、 accept、 connect等函數,這些函數的參數應該設計成void *類型以便接受各種類型的指 針,但是sock API的實現早於ANSI C標準化,那時還沒有void *類型,因此這些函數的參數都 ⽤struct sockaddr *類型表⽰,在傳遞參數之前要強制類型轉換⼀下.結構如圖所示:


通常⽤點分⼗進制的字符串表⽰IP 地址,以下函數可以在字符串表⽰ 和in_addr表⽰之間轉換。
字符串轉in_addr的函數:


in_addr轉字符串的函數:


以下是一次客戶端,服務端從請求連接到斷開連接的依次完整會話過程






       服務器調⽤socket()、 bind()、 listen() 完成初始化後,調⽤accept()阻塞等待,處於監聽端⼜的狀態,客戶端調⽤socket()初始化後,調⽤connect()發出SYN段並阻塞等待服務器應答,服務器應答⼀個SYN-ACK段,客戶端收到後從connect()返回,同時應答⼀個ACK段,服務器收到後 從accept()返回。


數據傳輸的過程: 建⽴連接後,TCP協議提供全雙⼯的通信服務,但是⼀般的客戶端/服務器程序的流程是由客戶端主 動發起請求,服務器被動處理請求,⼀問⼀答的⽅式。因此,服務器從accept()返回後⽴刻調 ⽤read(),讀socket就像讀管道⼀樣,如果沒有數據到達就阻塞等待,這時客戶端調⽤write()發送 請求給服務器,服務器收到後從read()返回,對客戶端的請求進⾏處理,在此期間客戶端調 ⽤read()阻塞等待服務器的應答,服務器調⽤write()將處理結果發回給客戶端,再次調⽤read()阻塞 等待下⼀條請求,客戶端收到後從read()返回,發送下⼀條請求,如此循環下去。如果客戶端沒有更多的請求了,就調⽤close() 關閉連接,就像寫端關閉的管道⼀樣,服務器 的ead()返回0,這樣服務器就知道客戶端關閉了連接,也調⽤close()關閉連接。注意,任何⼀⽅調⽤close() 後,連接的兩個傳輸⽅向都關閉,不能再發送數據了。如果⼀⽅調⽤shutdown() 則連接處 於半關閉狀態,仍可接收對⽅發來的數據。


有了上面的分析過程,不難寫出一下程序:

(一)server.c 的作⽤是接受client的請求,並與client進⾏簡單的數據通信,整體爲⼀個阻塞式的⽹絡聊天⼯具。

<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>


static void Usage(const char* proc)
{
	printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		Usage(argv[0]);
	//	exit(1);
		return 4;
	}
	int listen_sock=socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock<0)
	{
		perror("socket");
		return 1;
	}
	struct sockaddr_in local;
	local.sin_family=AF_INET;
	local.sin_port=htons(atoi(argv[2]));
	local.sin_addr.s_addr=inet_addr(argv[1]);
	if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
	{
		perror("bind");
		return 2;
	}
	listen(listen_sock,5);//max listen bind 5
	struct sockaddr_in peer;
	socklen_t len=sizeof(peer);
	int fd=accept(listen_sock,(struct sockaddr*)&peer,&len);
	if(fd<0)
	{
		perror("accept");
		return 3;
	}



	char buf[1024];
	while(1)
	{
		memset(buf,'\0',sizeof(buf));
		ssize_t _s=read(fd,buf,sizeof(buf)-1);
		if(_s>0)
		{
			buf[_s]='\0';
			printf("client :");
			printf("%s\n",buf);
			write(fd,buf,strlen(buf));
			printf("server :%s\n",buf);
		}else
		{
			printf("read done....\n");
			break;
		}
	}
	return 0;
}
</span></span>

socket()打開⼀個⽹絡通訊端⼜,如果成功的話,就像open()⼀樣返回⼀個⽂件描述符,應⽤程序可以像讀寫⽂件⼀樣⽤read/write在⽹絡上收發數據,如果socket()調⽤出錯則返回-1。對 於IPv4,family參數指定爲AF_INET。對於TCP協議,type參數指定爲SOCK_STREAM,表⽰⾯向流的傳輸協議。如果是UDP協議,則type參數指定爲SOCK_DGRAM,表⽰⾯向數據報的傳輸協 議。 protocol參數的介紹從略,指定爲0即可。

bind()的作⽤是將參數sockfd和myaddr綁定在⼀起,使sockfd這個⽤於⽹絡通訊的⽂件描述符監聽myaddr所描述的地址和端⼜號。

listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接等待狀態,如果接收到更多的連接請求就忽略。

三⽅握⼿完成後,服務器調⽤accept()接受連接,如果服務器調⽤accept()時還沒有客戶端的連接請 求,就阻塞等待直到有客戶端連接上來。 cliaddr是⼀個傳出參數,accept()返回時傳出客戶端的地 址和端⼜號。 addrlen參數是⼀個傳⼊傳出參數(value-result argument),傳⼊的是調⽤者提供的 緩衝區cliaddr 的長度以避免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調⽤者提供的緩衝區)。如果給cliaddr 參數傳NULL,表⽰不關⼼客戶端的地址。

(二)client.c的作⽤是鏈接server,並向server發起通信請求。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<netinet/in.h>

static void Usage(const char* proc)
{
	printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		Usage(argv[0]);
	//	exit(1);
		return 4;
	}
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("socket");
		return 1;
	}
	struct sockaddr_in remote;
	remote.sin_family=AF_INET;
	remote.sin_port=htons(atoi(argv[2]));
	remote.sin_addr.s_addr=inet_addr(argv[1]);
	if(connect(sock,(struct sockaddr*)&remote,sizeof(remote))<0)
	{
		perror("connect");
		return 2;
	}
	while(1)
	{
		char buf[1024];
		memset(buf,'\0',sizeof(buf));
		printf("client :");
		fflush(stdout);
		ssize_t _s=read(0,buf,sizeof(buf)-1);
		if(_s>0)
		{
			buf[_s-1]='\0';
			write(sock,buf,strlen(buf));
			memset(buf,'\0',sizeof(buf));
			_s=read(sock,buf,sizeof(buf));
			printf("server :");
			printf("%s\n",buf);

		}else
		{
			printf("read done....\n");
			break;
		}


	}
	return 0;
}
</span></span>
    由於客戶端不需要固定的端⼜號,因此不必調⽤bind(),客戶端的端⼜號由內核⾃動分配。注意, 客戶端不是不允許調⽤bind(),只是沒有必要調⽤bind()固定⼀個端⼜號,服務器也不是必須調⽤bind(),但如果服務器不調⽤bind(),內核會⾃動給服務器分配監聽端⼜,每次啓動服務器時端⼜ 號都不⼀樣,客戶端要連接服務器就會遇到⿇煩。

    客戶端需要調⽤connect()連接服務器,connect和bind的參數形式⼀致,區別在於bind的參數是⾃⼰的地址,⽽connect的參數是對⽅的地址。 connect()成功返回0,出錯返回-1。

我們來看一下server和client是怎樣通信的(運行截圖):

(一)先運行服務器:

服務器阻塞等待客戶機請求鏈接。


(二)重啓一個終端,然後運行客戶機:


(三)然後客戶機發送數據:


(四)服務器接到數據,並回顯給客戶機:




這裏有一個問題,當我們ctrl+Z暫停服務器,然後再起一個服務時,它會提醒我們Address already in use。這就像服務器掛了


client終⽌時⾃動關閉socket描述符,server的TCP連接收到client發的FIN段後處於TIME_WAIT狀 態。 TCP協議規定,主動關閉連接的⼀⽅要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間後才能回到CLOSED狀態,因爲我們先Ctrl-C終⽌了server,所以server是主動關閉連接的⼀⽅,在TIME_WAIT期間仍然不能再次監聽同樣的server端 ⼜。 MSL在RFC1122中規定爲兩分鐘,但是各操作系統的實現不同,在Linux上⼀般經過半分鐘後 就可以再次啓動server了。


解決方法是:在socket調用和bind調用之間加上一段對socket的設置:
   int opt = 1;
   setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))


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