linux下TCP socket編程入門案例(二)——非阻塞的TCP server&client


上一篇【阻塞的TCP server&client】中,介紹瞭如何使用socket函數編寫第一個socket通信小程序。這篇文章在第一個demo的基礎上,將使用select函數實現非阻塞的TCP server&client。

相關概念介紹

阻塞與非阻塞

在理解這個概念前,你要知道在linux系統中,一切皆是文件,不管是普通文件、輸入輸出設備、目錄,或者是套接字,都被linux當做文件處理。

1)阻塞是指,當試圖對某個文件描述符進行讀寫時,如果當前沒有東西可讀,或者暫時不可寫,程序就進入等待狀態,直到有東西可讀或者可寫爲止
而對於非阻塞狀態,如果沒有東西可讀,或者不可寫,讀寫函數馬上返回,而不會等待(這也與設置的超時時間有關)。
2)非阻塞,就是進程或線程執行某個函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況,如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率較高。

兩者區別

①阻塞好控制,不發送完數據程序不會往下走,但是對性能有影響;
非阻塞不太好控制,可能和能力有關,但是性能會得到很大提升。
②阻塞式的編程方便,非阻塞的編程不方便,需要開發人員處理各種返回;
③阻塞處理簡單,非阻塞處理複雜;
④阻塞效率低,非阻塞效率高;
⑤阻塞模式,常見的通信模型爲多線程模型,服務端accept之後,對每個socket創建一個線程去recv。邏輯上簡單,適用於併發量小(客戶端數目少),連續傳輸大數據量的情況下,比如文件服務器。還有就是在客戶端接收服務器消息的時候也經常用,因爲客戶端就一個socket,用阻塞模式不影響效率,而且編程邏輯上要簡單得多。
非阻塞模式,常見的通信模型爲select模型IOCP模型,適用於高併發,數據量小的情況,比如聊天室;客戶端多的情況下,如果採用阻塞模式,需要開很多線程,影響效率。


注意,不要和同步、異步的概念搞混了。下面的解釋來源於網絡:

I.所謂同步,就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。
按照這個定義,其實絕大多數函數都是同步調用(例如sin, isdigit等)。但是一般而言,我們在說同步、異步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。最常見的例子就是 SendMessage。該函數發送一個消息給某個窗口,在對方處理完消息之前,這個函數不返回。當對方處理完畢以後,該函數才把消息處理函數所返回的 LRESULT值返回給調用者。
II.異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的部件在完成後,通過狀態、通知和回調來通知調用者。以 CAsycSocket類爲例(注意,CSocket從CAsyncSocket派生,但是其功能已經由異步轉化爲同步),當一個客戶端通過調用 Connect函數發出一個連接請求後,調用者線程立刻可以向下運行。當連接真正建立起來以後,socket底層會發送一個消息通知該對象。這裏提到執行部件和調用者通過三種途徑返回結果:狀態、通知和回調。可以使用哪一種依賴於執行部件的實現,除非執行部件提供多種選擇,否則不受調用者控制。如果執行部件用狀態來通知,那麼調用者就需要每隔一定時間檢查一次,效率就很低(有些初學多線程編程的人,總喜歡用一個循環去檢查某個變量的值,這其實是一種很嚴重的錯誤)。如果是使用通知的方式,效率則很高,因爲執行部件幾乎不需要做額外的操作。至於回調函數,其實和通知沒太多區別。

同步/異步和阻塞/非阻塞的區別

1.阻塞調用是指調用結果返回之前,當前線程會被掛起。函數只有在得到結果之後纔會返回;
對同步調用來說,很多時候當前線程還是激活的,只是從邏輯上當前函數沒有返回而已
2.非阻塞是指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。

select模型

好了,對這些有了瞭解後,可以幫助你更好的理解select模型。
函數原型:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
返回值:成功返回做好準備的文件描述符的個數;超時爲0;錯誤爲 -1.
參數
maxfdp:是一個整數值,是指集合中所有文件描述符的範圍即所有文件描述符的最大值加1,不能錯。+1的原因:[0,maxfd],描述符是從0開始的,因此如果最大的描述符爲n的話,共有n+1個描述符
fd_set *readset:指向fd_set結構的指針,監視這些文件描述符的讀變化;
fd_set *writeset:監視集合中文件描述符的寫變化,只要有一個文件可寫,函數就返回一個大於0的值;
fd_set *exceptset:同上,監視集合中錯誤異常文件;
struct timeval *timeout:select的超時時間。這個參數至關重要,它可以使select處於三種狀態,
第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一直等到監視文件描述符集合中某個文件描述符發生變化爲止;
第二,若將時間值設爲0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;
第三,timeout的值大於0,這就是等待的超時時間,即 select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。

I.傳遞給select函數的參數會告訴內核:

  • 我們所關心的文件描述符
  • 對每個描述符,我們所關心的狀態。(是想從一個文件描述符中讀或者寫,還是關注一個描述符中是否出現異常)
  • 要等待多長時間。(可以等待無限長的時間;等待固定的一段時間;或者根本就不等待)

II.從select函數返回後,內核會告訴我們:

  • 對我們的要求已經做好準備的描述符的個數
  • 對於三種條件(讀,寫,異常)哪些描述符已經做好準備

對fd_set類型的變量,可以使用以下幾個宏控制它
int FD_ZERO(int fd, fd_set *set); //將一個 fd_set類型變量的所有位都置爲 0

int FD_CLR(int fd, fd_set *set); //清除某個位

int FD_SET(int fd, fd_set *set); //將變量的某個位置位

int FD_ISSET(int fd, fd_set *set); //測試某個位是否被置位

理解select模型,關鍵是理解fd_set。爲了方便說明,以fd_set長度爲1B(1字節)爲例,fd_set的每一位(bit)可以對應一個文件描述符(fd)。則1B的fd_set可以對應8個fd.

1)執行fd_set set,FD_ZERO(&set);則set用位表示是0000,0000
2)若fd=3,執行FD_SET(fd,&set);後set變爲0000,0100(第3位爲1)
3)若再加入fd=2,fd=1,則set變爲0000,0111
4)執行select(5,&set,0,0,0)阻塞等待
5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變爲0000,0011。注意:沒有事件發生的fd=3位置被清空。

注意,
a.可監控的文件描述符個數取決與sizeof(fd_set)的值;
b.將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,一是用於在select返回後,把array作爲源數據和fd_set進行FD_ISSET判斷。二是select返回後會把以前加入的但並無事件發生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用於select的第一個參數。
c.必須在select前循環array(加入fd,取maxfd),select返回後循環array(使用FD_ISSET判斷是否有(讀/寫/異常)事件發生)。

編碼實現

代碼改進

在阻塞的TCP server&client中,加入select函數
即在listen之後,將套接字描述符全部加入fd_set,然後按照下面的順序編寫代碼
1)FD_ZERO()清空fd_set;
2)FD_SET()將要測試的fd加入fd_set;
3)select()測試fd_set中所有的fd;
4)FD_ISSET()測試是否有符合條件的描述符

實現

server.cpp
/*
 * server.cpp --非阻塞TCP server
 *
 *  Created on: Nov 23, 2019
 *      Author: xb
 */
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>

#define CONCURRENT_MAX 3	//服務端同時支持的最大連接數
#define SERVER_PORT 9999	//端口
#define BUFFER_SIZE 1024	//緩衝區大小

int main(int argc, char* argv[]) {

	int client_fd[CONCURRENT_MAX] = {0};//用於存放客戶端套接字描述符
	int server_sock_fd;//服務器端套接字描述符
	char send_msg[BUFFER_SIZE];//數據傳輸緩衝區
	char recv_msg[BUFFER_SIZE];
	struct sockaddr_in server_addr;

	memset(&server_addr,0,sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//限定只接受本地連接請求
	server_addr.sin_addr.s_addr = INADDR_ANY;

	/*測試*/
	/*for(int a = 0; a < CONCURRENT_MAX; a++){
		printf("client_fd[%d] = %d\n",a,client_fd[a]);
	}*/

	//1.創建socket
	server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_sock_fd < 0) {
		printf("create socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//2.綁定socket和端口號
	if (bind(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(server_addr)) < 0) {
		printf("bind socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//3.監聽listen
	if (listen(server_sock_fd, 5) < 0) {
		printf("listen socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//fd_set
	fd_set server_fd_set; //文件描述符集合
	int max_fd = -1;
	struct timeval tv;

	while (1) {
		tv.tv_sec = 10;/* 超時時間10s */
		tv.tv_usec = 0;
		/*
		1)FD_ZERO()清空fd_set;
		2)FD_SET()將要測試的fd加入fd_set;
		3)select()測試fd_set中所有的fd;
		4)FD_ISSET()測試是否有符合條件的描述符
		*/
		FD_ZERO(&server_fd_set); 
		//STDIN_FILENO:接收鍵盤輸入
		FD_SET(STDIN_FILENO, &server_fd_set);
		if (max_fd < STDIN_FILENO) {
			max_fd = STDIN_FILENO;
		}
		//printf("STDIN_FILENO=%d\n", STDIN_FILENO);//STDIN_FILENO = 0
		//服務器端socketfd
		FD_SET(server_sock_fd, &server_fd_set);
		// printf("server_sock_fd=%d\n", server_sock_fd);
		if (max_fd < server_sock_fd) {
			max_fd = server_sock_fd;
		}

		//客戶端連接
		for (int i = 0; i < CONCURRENT_MAX; i++) {
			//printf("client_fd[%d]=%d\n", i, client_fd[i]);
			if (client_fd[i] != 0) {
				FD_SET(client_fd[i], &server_fd_set);
				if (max_fd < client_fd[i]) {
					max_fd = client_fd[i];
				}
			}
		}
		/*
			int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
			maxfdp1:集合中所有文件描述符的範圍,即最大值+1
			readset:監視文件描述符的讀變化
			writeset:監視寫變化
			exceptset:監視錯誤異常文件
			timeout:超時時間

			函數返回值:<0:發生錯誤;
						=0:沒有滿足條件的文件描述符,等待超時;
						>0:文件描述符滿足條件
		*/
		int result = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv);
		if (result < 0) {
			printf("select error:%s(errno:%d)\n",strerror(errno),errno);
			continue;
		} else if (result == 0) {
			printf("select 超時\n");
			continue;
		} else {
			//result爲位狀態發生變化的文件描述符的個數
			//STDIN_FILENO:系統API接口庫,是打開文件的句柄
			/*  服務器端輸入信息  */
			if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
				bzero(send_msg, BUFFER_SIZE);//清空
				scanf("%s",&send_msg);
				//輸入"q"則關閉服務端
				if (strcmp(send_msg, "q") == 0) {
					close(server_sock_fd);
					exit(0);
				}
				for (int i = 0; i < CONCURRENT_MAX; i++) {
					if (client_fd[i] != 0) {
						printf("發送消息給客戶端:");
						printf("client_fd[%d]=%d\n", i, client_fd[i]);
						send(client_fd[i], send_msg, strlen(send_msg), 0);
					}
				}
			}
			/*  處理新的連接請求  */
			if (FD_ISSET(server_sock_fd, &server_fd_set)) {
				struct sockaddr_in client_address;
				socklen_t address_len;
				int client_sock_fd = accept(server_sock_fd,(struct sockaddr *) &client_address, &address_len);
				printf("new connection client_sock_fd = %d\n", client_sock_fd);
				if (client_sock_fd > 0) {
					int index = -1;//判斷連接數量是否達到最大值
					for (int i = 0; i < CONCURRENT_MAX; i++) {
						//如果還有空閒的連接數量,就分配給新的連接
						if (client_fd[i] == 0) {
							index = i;
							client_fd[i] = client_sock_fd;
							break;
						}
					}
					if (index >= 0) {
						printf("新客戶端[%d] [%s:%d]連接成功\n", index,
								inet_ntoa(client_address.sin_addr),
								ntohs(client_address.sin_port));
					} else {
						bzero(send_msg, BUFFER_SIZE);
						strcpy(send_msg, "服務器已連接的客戶端數量達到最大值,連接失敗!\n");
						send(client_sock_fd, send_msg, strlen(send_msg), 0);
						printf("客戶端連接數量達到最大值,新客戶端[%s:%d]連接失敗\n",
								inet_ntoa(client_address.sin_addr),
								ntohs(client_address.sin_port));
					}
				}
			}
			/*  處理某個客戶端發過來的消息  */
			for (int i = 0; i < CONCURRENT_MAX; i++) {
				if (client_fd[i] != 0) {
					if (FD_ISSET(client_fd[i], &server_fd_set)) {
						bzero(recv_msg, BUFFER_SIZE);
						int n = recv(client_fd[i], recv_msg,BUFFER_SIZE, 0);
						// >0,接收消息成功
						if (n > 0) {
							if (n > BUFFER_SIZE) {
								n = BUFFER_SIZE;
							}
							recv_msg[n] = '\0';
							printf("收到客戶端[%d]發來的消息:%s\n", i, recv_msg);
						} else if (n < 0) {
							printf("從客戶端[%d]接收消息出錯!\n", i);
						} else { //=0,對端連接關閉
							FD_CLR(client_fd[i], &server_fd_set);
							client_fd[i] = 0;
							printf("客戶端[%d]斷開連接\n", i);
						}
					}
				}
			}
		}
	}

	return 0;
}
client.cpp
/*
 * client.cpp -- 非阻塞 TCP client
 *
 *  Created on: Nov 23, 2019
 *      Author: xb
 */
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

#define BUFFER_SIZE 1024

int main(int argc, char* argv[]) {

	char recv_msg[BUFFER_SIZE];
	char send_msg[BUFFER_SIZE];//數據收發緩衝區
	struct sockaddr_in server_addr;
	int server_sock_fd;

	memset(&server_addr,0,sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(9999);
	server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	//創建套接字
	if ((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
		return -1;
	}

	//連接服務器
	if (connect(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(struct sockaddr_in)) < 0) {
		printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        return -1;
	}

		fd_set client_fd_set;
		struct timeval tv;

		while (1) {
			tv.tv_sec = 2;
			tv.tv_usec = 0;
			FD_ZERO(&client_fd_set);
			FD_SET(STDIN_FILENO, &client_fd_set);
			FD_SET(server_sock_fd, &client_fd_set);

			select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
			if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
				bzero(send_msg, BUFFER_SIZE);
				fgets(send_msg, BUFFER_SIZE, stdin);
				if (send(server_sock_fd, send_msg, BUFFER_SIZE, 0) < 0) {
					printf("send message error: %s(errno:%d)\n",strerror(errno),errno);
				}
			}
			if (FD_ISSET(server_sock_fd, &client_fd_set)) {
				bzero(recv_msg, BUFFER_SIZE);
				int n = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
				if (n > 0) {
					printf("recv %d byte\n",n);
					if (n > BUFFER_SIZE) {
						n = BUFFER_SIZE;
					}
					recv_msg[n] = '\0';
					printf("收到服務器發送的信息:%s\n", recv_msg);
				} else if (n < 0) {
					printf("接收消息出錯!\n");
				} else {
					printf("服務器已關閉!\n");
					close(server_sock_fd);
					exit(0);
				}
			}
		}

	return 0;
}

使用g++編譯:
g++ server.cpp -o server
g++ client.cpp -o client
程序運行結果:
1)啓動server
在這裏插入圖片描述
2)啓動client
在這裏插入圖片描述
3)發送信息給server
在這裏插入圖片描述
4)發送信息給client
在這裏插入圖片描述
第4個client嘗試連接server時
在這裏插入圖片描述
相比較阻塞的TCP server&client,非阻塞的可以連接更多客戶端。阻塞的則只能連接一個,新的連接請求會被阻塞,直到上一個連接關閉。

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