最近學習route組件,了解了些關於tcp通信中I/O復用的知識。比如:select,poll,epoll。目前系統主要是用select。本來以為select是個好東西,解決了單進程單線程的server可以連接多個客戶端的問題。后來,同事跟我說read函數是阻塞的,那么連接建立后,server會阻塞在read處,其他連接就沒法正常工作了。然后這個問題就一直困擾着。想起了之前在知乎上有個問題是:怎么設計tcp連接?有個點贊很多的是建多線程,一個線程一個連接。但是,也有不少人批判這個設計,說這樣太費資源,指出I/O多路復用中使用類似於select在一個線程中可以實現連接多個客戶端。當時也是沒想明白,而且公司route組件(老版本)的設計是一個線程一個連接,apache也是一個進程一個連接,壞處就是連接數量很少,畢竟進程切換是耗cpu的。后來就查閱資料,然后通過代碼測試,read的阻塞可以不會干擾其他連接的,一個server連N客戶端跟連一個客戶端一個麻溜溜的。測試代碼如下:
1 for (;;) { 2 memset(szBuf, 0, sizeof(szBuf)); 3 FD_ZERO(&fset); 4 FD_SET(fd, &fset); 5 tv.tv_sec = 5; 6 tv.tv_usec = 0; 7 8 for (int i = 0; i < BACKLOG; i++) { 9 if (fd_A[i] != 0) 10 FD_SET(fd_A[i], &fset); 11 } 12 13 ret = select(maxfd+1, &fset, NULL, NULL, &tv); 14 15 if (ret < 0) { 16 printf("select調用發生錯誤\n"); 17 break; 18 } 19 else if (ret == 0) { 20 printf("select timeout\n"); 21 continue; 22 } 23 else { 24 printf("select normal\n"); 25 } 26 27 for (int i = 0; i < BACKLOG; i++) { 28 if (fd_A[i] && FD_ISSET(fd_A[i], &fset)) { 29 printf("recv before\n"); 30 if ((ret = recv(fd_A[i], szBuf, sizeof(szBuf), 0)) == 0) { 31 close(fd_A[i]); 32 FD_CLR(fd_A[i], &fset); 33 fd_A[i] = 0; 34 conn_amount--; 35 } 36 else { 37 printf("fd_A[%d]:%s", i, szBuf); 38 } 39 } 40 } 41 42 if (FD_ISSET(fd, &fset)) { 43 newfd = accept(fd, (struct sockaddr *)&cli_addr, &cli_len); 44 if (newfd <= 0) { 45 printf("accept出錯\n"); 46 continue; 47 } 48 else 49 printf("accept normal\n"); 50 。。。
當客戶端connect連接上的時候,會輸出"select normal" "accept normal",沒有走到read/recv這塊;如果5秒內客戶端沒其他操作,server就會在select處超時(select的超時時間設置的是5秒)。接着客戶端調用send,然后代碼會走到select->read這塊,並沒有進去accept。因為在調用accept,recv/read之前我用了FD_ISSET來判斷。
fd_set是一組文件描述符(fd)的集合,它用一位來表示一個fd。至於fd有多大,操作系統定義了常量FD_SETSIZE。在很久 以前是32,現在一般是1024。select函數用於檢查fd_set集合中是否有可讀的,同時也會更新fd_set集合。FD_ISSET用於測試指定的文件描述符是否在該集合中。假設現在客戶端1是成功連接的,如果客戶端2發起連接,那么select后客戶端1對應fd使用FD_ISSET后返回值是false的,那么就不去調用recv/read函數。如果客戶端1發送數據過來,select檢測到后,使用FD_ISSET判斷連接1返回true,可以用recv/read不會阻塞;使用FD_ISSET判斷連接2的返回是false的,不去調用recv/read函數。
同時,客戶端在send的時候一次發2k數據,在server接收一次1k的,第一次沒取完,select會再次檢測到該fd可讀,再收一次,正好2k,select才不會檢測到該fd可讀。這個例子是一個簡單的非阻塞(NIO)的例子,難點就是對於半包問題要處理好。很多時候我們接收到的數據要完整了才行進行decode。
