在上一篇【阻塞的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,非阻塞的可以連接更多客戶端。阻塞的則只能連接一個,新的連接請求會被阻塞,直到上一個連接關閉。