朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型
? ? ? ? 在《樸素、Select、Poll和Epoll網絡編程模型實現和分析——樸素模型》中我們分析了樸素模型的一個缺陷——一次只能處理一個連接。本文介紹的Select模型則可以解決這個問題。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 和樸素模型一樣,我們首先要創建一個監聽socket,然后調用listen去監聽服務器端口。不同的是,我們要對make_socket方法傳遞1,因為我們要創建一個異步的socket。
	listen_sock = make_socket(1);if (listen(listen_sock, SOMAXCONN) < 0) {perror("listen error");exit(EXIT_FAILURE);} 
? ? ? ? 下一步我們需要清空Select模型使用的fd_set結構體對象,并把監聽socket設置進去
	FD_ZERO(&active_fd_set);FD_SET(listen_sock, &active_fd_set); 
? ? ? ??active_fd_set用于記錄活動的socket信息。之后我們使用到的read_fd_set則是其一個拷貝,因為我們只關心讀行為
	fd_set active_fd_set, read_fd_set; 
? ? ? ? 和樸素模型類似,我們也需要使用一個死循環讓服務器不要停止
	/* Initialize the set of active sockets. */while (1) {timeout.tv_sec = 0;timeout.tv_usec = 500;/* Service all the sockets with input pending. */read_fd_set = active_fd_set;switch(select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout)) {case -1 : {perror("select error\n");exit(EXIT_FAILURE);}break;case 0 : {//perror("select timeout\n");}break;default: { 
? ? ? ? select函數第一個參數我們傳遞了FD_SETSIZE,它在我的系統上是1024,它代表需要關注的socket的最大個數。第二參數是用于記錄需要關注的發生讀事件的fd_set對象。我們讓select函數按異步方式執行,故最后一個參數設置為500微秒的超時時間。整個select函數意思是我們需要等待socket發生可讀事件,如果等待時間超過超時設置,則函數返回0,如果出錯則返回-1,如果等待到事件則返回其他值。
			default: {for (index = 0; index < FD_SETSIZE; ++index) {if (FD_ISSET(index, &read_fd_set)) {if (listen_sock == index) {/* Connection request on original socket. */int new_sock;new_sock = accept(listen_sock, NULL, NULL);if (new_sock < 0) {perror("accept error");exit(EXIT_FAILURE);}request_add(1);//set_block_filedes_timeout(new_sock);FD_SET(new_sock, &active_fd_set);} else {if (0 == server_read(index)) {server_write(index);}close(index);FD_CLR(index, &active_fd_set);}}}}}}return 0;
} 
? ? ? ? default中才是我們程序的重點。
? ? ? ? 我們使用一個for循環遍歷每個socket。如果該socket通過FD_ISSET宏判斷不處于我們關注的可讀事件fd_set中,則忽略它。
? ? ? ? 如果處在可讀fd_set中,則看看其是否是監聽socket。
 ? ? ? ? 如果是監聽socket,則使用accpet方法獲取接入的socket。并使用request_add讓請求數量加一。還要使用FD_SET宏將該socket加入到活動狀態的fd_set中。之后該活動狀態的fd_set將被賦值給需要關注可讀事件的fd_set中。
? ? ? ? 如果不是監聽socket,則是接入的socket。于是我們調用《樸素、Select、Poll和Epoll網絡編程模型實現和分析——樸素模型》一文中介紹的server_read和server_write方法讀取內容并回包。最后我們還要關閉socket,并使用FD_CLR宏將該socket從活動狀態的fd_set中去掉。之后的select函數將不會在關注該socket了。
? ? ? ? 整個過程非常簡單。但是這其中卻包含了很多值得思考的問題。
? ? ? ? 首先我拋出一個問題,我在default中使用了一個從0到FD_SETSIZE的遍歷行為。并且將遍歷的游標——index作為socket去操作——使用server_read和server_write去讀取。于是問題就來了,使用make_socket創建的socket值和使用accept接收到的socket的值怎么和游標產生關聯?代碼中似乎沒有任何讓它們產生關聯的邏輯,而且它們的關系是嚴格的“相等”的關系!那么只有一個假設,就是make_socket和accept返回的socket值在FD_SETSIZE和0之間。但是目前我沒有找到文檔對這個問題進行說明,而我也沒深入研究這兩個函數考證到其值就是在這個范圍之內,那么為什么我還要這么去用呢?
? ? ? ? 我們先記下這個問題,深入到linux的源碼中取解釋這個使用的正確性。
? ? ? ? 我們先看下fd_set的定義
/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;/* Some versions of <linux/posix_types.h> define this macros.  */
#undef	__NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT.  */
#define __NFDBITS	(8 * (int) sizeof (__fd_mask))
#define	__FD_ELT(d)	((d) / __NFDBITS)
#define	__FD_MASK(d)	((__fd_mask) 1 << ((d) % __NFDBITS))/* 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;/* Maximum number of file descriptors in `fd_set'.  */
#define	FD_SETSIZE		__FD_SETSIZE 
? ? ? ? 以上我是在我ubuntu系統的/usr/include/x86_64-linux-gnu/sys/select.h文件中找到的定義。我們看到fd_set的主體就是一個long int型數組__fds_bits。該數組的個數是兩個數的商。被除數__FD_SETSIZE就是我們程序中使用的FD_SETSIZE,也就是1024。除數__NFDBITS是64。于是fd_set中數組元素的個數是1024/64=16。注意一下這個值是16,而我們程序中關注的socket的最大個數是FD_SETSIZE——1024,這是為什么?其實這就是該結構設計的一個精妙之處。fd_set的__fds_bits是一個16個元素的long int型數組,其總長度就是16*64=1024位。于是可以使用每一位表示一個socket。
? ? ? ? 我們到/usr/include/x86_64-linux-gnu/bits/select.h 文件中看看linux是如何讓socket和這個空間中每一位進行對應的。我們查看FD_SET宏
#define __FD_SET(d, set) \((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) 
? ? ? ??__FDS_BITS宏定義在fd_set定義中。它就是返回fd_set的__fds_bits數組首地址。數組的游標是通過__FD_ELT對socket進行處理的結果。__FD_ELT在上面我們已經見過,它是對socket值除以__NFBITS——64的值。通過游標我們取到數組元素值之后,我們再用其與__FD_MASK對socket進行操作的值進行或操作。__FD_MASK的定義也在上面給出,它是將socket的值與__NFBITS——64相除,取得余數,然后讓1左移該余數次。這樣我們就將該socket映射到fd_set內存的一位中。我們知道,只要在知道除數、商和余數的情況下,可以很方便的推算出被除數是多少。可以說linux內核對這塊的設計真是做到了極致,不浪費一點點空間。
? ? ? ? 有了上面的認識,我們就知道select模型最大只能支持FD_SETSIZE個數的socket,而且socket的值也只能在FD_SETSIZE之內。如果socket()或accept()函數返回的socket值大于FD_SETSIZE,則select模型將出現錯誤——上面的計算將溢出。基于這種反向推理,我們可以放心大膽的使用0到FD_SETSIZE的值去當socket的值去計算。我看網上有很多select例子需要使用一個數組去維護接入的socket,如果在不考慮效率的前提下是不必要的。但是如果你追求極致的Select模型性能,還是建議使用一個數組去維護Socket,這樣不至于出現大量的浪費操作。這塊分析我們將在后文中給出。
? ? ? ? 這兒再多言一句,正是因為這種位操作,我們才需要在使用fd_set之前調用FD_ZERO去清空所有空間
# if __WORDSIZE == 64
#  define __FD_ZERO_STOS "stosq"
# else
#  define __FD_ZERO_STOS "stosl"
# endif# define __FD_ZERO(fdsp) \do {									      \int __d0, __d1;							      \__asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS			      \: "=c" (__d0), "=D" (__d1)			      \: "a" (0), "0" (sizeof (fd_set)		      \/ sizeof (__fd_mask)),	      \"1" (&__FDS_BITS (fdsp)[0])			      \: "memory");					      \} while (0) 
? ? ? ? 如果socket不再需要監測,則我們使用__FD_CLR在fd_set中去除其對應的位
#define __FD_CLR(d, set) \((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) 
? ? ? ? 我們再來看下Select模型的處理能力。我們采用和《樸素、Select、Poll和Epoll網絡編程模型實現和分析——樸素模型》一文中相同的環境和壓力,看下服務器的數據輸出
? ? ? ? 再看下客戶端的輸出
 ? ? ? ? 可見當前環境下,select模型的處理能力大概是每秒7000多連接。(和下一章介紹的Poll模型差距不大,而且如果使用數組維護Socket還可以提高性能)
總結
以上是生活随笔為你收集整理的朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 朴素、Select、Poll和Epoll
 - 下一篇: 朴素、Select、Poll和Epoll