目錄
- select參數解釋
- select使用規范
- select使用缺點
- 基本流程
- 實例代碼
- 通信效果演示
- 往期文章
select參數解釋
extern int select (int __nfds, fd_set *__restrict __readfds,fd_set *__restrict __writefds,fd_set *__restrict __exceptfds,struct timeval *__restrict __timeout);
__nfds:一般設置為所有需要使用select函數檢測時間的fd中的最大值+1
__readfds:需要監聽可讀事件的fd集合
__writefds:需要監聽可寫事件的fd集合
__exceptfds:需要監聽可寫事件的fd集合
__timeout:超時時間,在這個設定的時間內檢測這些fd事件,超過這個超時時間,select將立即返回
fd_set
這個結構體信息如下:
/* fd_set for select and pselect. */
typedef struct{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;
可以簡化看做:
typedef struct {long int __fds_bits[16];
} fd_set;
long int 占8字節,也就是8 * 8 = 64個bit,所以__fds_bits數組總共就占64 * 16 = 1024個fd狀態。每個bit位0表示無事件,1表示有事件。
select使用規范
1、select在調用前后可能會修改readfds
、writefds
、exceptfds
中的內容,所以如果在下次調用時復用這些fd_set,則要在下次調用前使用FD_ZERO
將fd_set
清空,然后調用FD_SET
將要檢測的fd添加到fd_set中
2、linux系統下select函數也會修改timeval結構體的值,所以想要復用也必須給其重新設置值
3、select函數的timeval結構體中的tv_sec
和tv_usec
如果都被設置為0,代表著檢測事件的總時間被設置為0,行為就變成了select檢測相關集合中的fd,如果沒有需要的事件,則立即返回
4、如果select函數的timeval
參數設置為NULL,則select會一直阻塞下去,直到我們需要的事件被觸發
5、Linux下,select函數第一個參數必須設置為需要檢測事件fd中最大值+1,所以每次產生一個新fd都需要和maxfd作比較
select使用缺點
1、select函數需要將fd集合從用戶態拷貝到內核態,在fd較多時開銷較大。并且每次檢測時也是在內核中遍歷這個fd_set。
2、單個進程能夠監視的文件描述符數量上存在最大限制,linux上我們算過了,只有1024個
3、select函數每次調用之前都要對傳入的參數重新設定,比較麻煩
4、在linux上select函數的實現原理是底層的poll函數,所以select和poll本質上沒有區別
基本流程
實例代碼
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <sys/time.h>
#include <vector>
#include <errno.h>
#include <stdio.h>
#include <string.h>
using namespace std;
int main() {// 創建一個監聽socketint listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd == -1) {cout << "create listen error" << endl;return -1;}// 初始化服務器地址struct sockaddr_in bindaddr;bindaddr.sin_family = AF_INET;bindaddr.sin_addr.s_add當斷開各個客戶端時,服務端的select函數對各個客戶端fd進行檢測時,仍然會觸發可讀事件r = htonl(INADDR_ANY);bindaddr.sin_port = htons(3000);if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {cout << "bind listen socket error" << endl;close(listenfd);return -1;}// 啟動監聽if (listen(listenfd,SOMAXCONN) == -1) {cout << "listen error" << endl;close(listenfd);return -1;}// 存儲客戶端socket的數組vector<int> clientfds;int maxfd;while (true) {fd_set readset;// 對標志位清零FD_ZERO(&readset);// 將監聽的socket加入到待檢測的可讀事件中// 第listenfd位被置為1FD_SET(listenfd, &readset);maxfd = listenfd;// 將客戶端socket加入到待檢測的可讀事件中for (int i = 0; i < clientfds.size(); i++) {if (clientfds[i] != -1) {FD_SET(clientfds[i], &readset);maxfd = max(maxfd, clientfds[i]);}}// 設置超時時間為1stimeval time;time.tv_sec = 1;time.tv_usec = 0;// 暫且只檢測可讀事件,不檢測可寫和異常事件int ret = select(maxfd + 1, &readset, NULL, NULL, &time);if (ret == -1) {// 出錯if (errno != EINTR)break;} else if (ret == 0) {// select函數超時continue;} else {// 檢測到某個socket有事件// 是否是監聽socket的可讀事件if (FD_ISSET(listenfd, &readset)) {// 如果是監聽socket的可讀事件,表示現在有新的連接到來struct sockaddr_in clientaddr;socklen_t clientaddrlen = sizeof(clientaddr);// 接受客戶端連接int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);if (clientfd == -1) {cout << "client socket error" << endl;break;} else {cout << "accept a client connection , fd:" << clientfd << endl;clientfds.push_back(clientfd);}} else {// 從client socket上接受數據// 假設對端發送過來的數據長度不超過63個字符char recvbuf[64];for (int i = 0; i < clientfds.size(); i++) {if (clientfds[i] != -1 && FD_ISSET(clientfds[i], &readset)) {memset(recvbuf, 0, sizeof(recvbuf));// 接受數據int length = recv(clientfds[i], recvbuf, 64, 0);if (length <= 0) {cout << "recv data error, clientfd:" << clientfds[i] << endl;close(clientfds[i]);// 不直接刪除該元素而是將位置元素標記clientfds[i] = -1;continue;} else {cout << "clientfd:" << clientfds[i] << "recv data: " << recvbuf << endl;}}}}}}// 處理之后,關閉所有客戶端for (int i = 0; i < clientfds.size(); i++) {if (clientfds[i] != -1)close(clientfds[i]);}// 關閉監聽close(listenfd);return 0;
}
通信效果演示
這里不需要寫客戶端程序,直接用nc命令模擬,指定一下服務端的ip地址和端口號就可以通信了,127.0.0.1就是系統的回環地址,直接是用于本機內的socket的通信。這里我們開了兩個客戶端,分別發送hello和hello world。
由于nc命令發送的數據是按換行符區分的,所以數據包最后一個都是以\n結束。
當斷開各個客戶端時,服務端的select函數對各個客戶端fd進行檢測時,仍然會觸發可讀事件。
不過此時對這些fd進行調用recv函數的話會返回0,表示對端關閉了連接。然后服務端這邊將fd進行置-1,然后關閉連接即可。
往期文章
C++網絡編程快速入門(一):TCP網絡通信基本流程以及基礎函數使用