## 0. 概述
?? 在第五章的TCP客戶同時處理兩個輸入:標準輸入和TCP套接字。我們遇到的問題是在客戶阻塞于fgets調用期間,服務器進程會被殺死。服務器TCP雖然正確的給客戶TCP發送了一個FIN,但是既然客戶進程正阻塞于從標準輸入讀入的過程,它將看不到這個EOF,直到從套接字讀時為止(可能經過很長時間)。這樣的進程需要一種預先告知內核的能力,使得內核一旦發現進程指定的一個或多個I/O條件就緒(也就是說輸入已準備好被讀取,或者描述符已能承接更多的輸出),它就通知進程。這個能力稱為I/O復用,是由select和poll這兩個函數支持。
?? I/O復用使用于以下場合:
1) 當客戶處理多個描述符(通常是交互式輸入和網絡套接字)時,必須使用I/O復用。
2) 一個客戶同時處理多個套接字是可能的,不過比較少見。
3) 如果一個TCP服務器既要處理監聽套接字,又要處理已連接套接字,一般就要使用I/O復用。
4)如果一個服務器既要處理TCP,又要處理UDP,一般就要使用I/O復用。
5) 如果一個服務器要處理多個服務或者多個協議,一般就要使用I/O復用。
## 1. I/O模型
? 一個輸入操作通常包括兩個不同的階段:
(1) 等待數據準備好
(2) 從內核向進程復制數據
? 對于一個套接字上的輸入操作,第一步通常涉及等待數據從網絡中到達。當所等待分組到達時,它被復制到內核中的某個緩沖區。第二步就是把數據從內核緩沖區復制到應用進程緩沖區。
### 1) 阻塞式I/O模型

? 進程調用recvfrom,其系統調用直到數據報到達且被復制到應用進程的緩沖區中或者發生錯誤才返回。我們說進程在從調用recvfrom開始到它返回的整段時間內飾被阻塞的。recvfrom成功返回后,應用進程開始處理數據報。
### 2) 非阻塞式I/O模型

? 進程把一個套接字設置成非阻塞是在通知內核:當所請求的I/O操作非得把本進程投入睡眠才能完成,不要把本進程投入睡眠,而是返回一個錯誤。
?? 當一個應用進程像這樣對一個非阻塞描述符循環調用recvfrom時,我們稱之為輪詢。應用進程持續輪詢內核,以查看某個操作是否就緒。這么做往往耗費大量CPU時間。
### 3) I/O復用模型

? 我們阻塞于select調用(而非阻塞于recvfrom處),等待數據報套接字變為可讀。當select返回套接字可讀這一條件時,我們調用recvfrom把所讀數據報復制到應用進程緩沖區。使用select的優勢在于我們可以等待多個描述符就緒。
### 4) 信號驅動式I/O模型

? 讓內核在描述符就緒時發送SIGIO信號通知我們。
?? 無論如何處理SIGIO信號,這種模型的優勢在于等待數據報到達期間進程不被阻塞。主循環可以繼續執行,只要等待來自信號處理函數的通知:既可以是數據已準備好被處理,也可以是數據報已準備好被讀取。
### 5) 異步I/O模型

? 這些函數的工作機制是:告知內核啟動某個操作,并讓內核在整個操作(包括將數據從內核復制到我們自己的緩沖區)完成后通知我們。
## 2. select函數
### 1) 基礎知識
?? 該函數允許進程指示內核等待多個事件中的任何一個發生,并只在有一個或多個事件發生或經歷一段指定的時間后才喚醒它。
~~~
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout );
返回:若有就緒描述符則為其數目,若超時則為0,若出錯則為-1
~~~
?? 作為一個例子,我們可以調用select,告知內核僅在下列情況發生時才返回:
(1)集合{1,4,5}中的任何描述符準備好讀
(2)集合{2,7}中的任何描述符準備好寫
(3)集合{1,4}中的任何描述符有異常條件待處理
(4)已經經歷了10.2秒
?? timeout告知內核等待所指定描述符中的任何一個就緒可花多長時間。其timeval結構用于指定這段時間的秒數和微秒數:
~~~
struct timeval{
long tv_sec;
long tv_usec;
};
~~~
?? 這個參數有以下三種可能:
1) 永遠等待下去:僅在有一個描述符準備好I/O時才返回。為此,我們把這參數設置為空指針。
2) 等待一段固定時間:在有一個描述符準備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
3) 根本不等待:檢查描述符后立即返回,這稱為輪詢。為此,該參數必須指向一個timeval結構,而且其中的定時器值必須為0.
?? 中間的三個參數readset,writeset和exceptset指定我們要讓內核測試讀,寫和異常條件的描述符。
?? maxfdp1參數指定待測試的描述符個數,它的值是待測試的最大描述符加1.
?? 關于fd_set結構體數據四個關鍵的宏:
~~~
void FD_ZERO( fd_set *fset );
void FD_SET( int fd, fd_set *fdset );
void FD_CLR( int fd, fd_set *fdset );
int FD_ISSET( int fd, fd_set *fset );
~~~
?? 假設我們要將描述符1(對應于stdout,標準輸出),4,5(分別對應socket中服務器socket描述符和客戶端的一個socket描述符)放入select函數中,當任何一個寫就緒時候就返回,那么我們大概可以這樣寫:
~~~
fd_set rset;
FD_ZERO( &rset );
FD_SET( 1, &rset );
FD_SET( 4, &rset );
FD_SET( 5, &rset );
select( maxfdp1, NULL, &rset, NULL,NULL);
~~~
?? 描述符集的初始化非常重要,因為作為自動變量分配的一個描述符集如果沒有初始化,那么可能發生不可預期的后果。
測試用力如下:
~~~
#include <stdio.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <time.h>
int main(int argc, char **argv)
{
fd_set rset;
FD_ZERO(&rset);
FD_SET(1, &rset);
FD_SET(4, &rset);
FD_SET(5, &rset);
return 0;
}
~~~
當我們調試程序,查看rset:
~~~
(gdb) p rset
$3 = {__fds_bits = {50, 0 <repeats 15 times>}}
(gdb) p rset.__fds_bits
$4 = {50, 0 <repeats 15 times>}
~~~
?? 其中,50=110010,即第1,4,5位均被置為1.
### 2) 描述符就緒條件
(1)滿足下列四個條件中的任何一個時,一個套接字準備好讀(即可從描述符中讀取數據)
a) 該套接字接收緩沖區中的數據字節數大于等于套接字接收緩沖區低水位標記的當前大小。對這樣的套接字執行讀操作不會阻塞并將返回一個大于0的值(也就是返回準備好讀入的數據,即進程可以從緩沖區中讀取數據)
b) 該連接的讀半部關閉(也就是接收了FIN的TCP連接)。對這樣的套接字的讀操作將不阻塞并返回0(因為這時候服務器執行close套接字需要一段時間,而這段時間內,客戶端可繼續從服務器讀取數據,只是讀取的是EOF而已)
c) 該套接字是一個監聽套接字且已完成的連接數不為0.(這樣服務端才能執行accept函數,讀取客戶端發送過來的數據)
d) 其上有一個套接字錯誤待處理。對這樣的套接字的讀操作將不阻塞并返回-1,同時把errno設置成確切的錯誤條件。
(2)滿足下列四個條件中的任何一個時,一個套接字準備好寫(即可向描述符中寫入數據)
a) 該套接字發送緩沖區中的可用空間字節數大于等于套接字發送緩沖區低水位標記的當前大小,并且或者該套接字已連接,或者該套接字不需要連接。
b) 該連接的寫半部關閉。對這樣的套接字的寫操作將產生SIGPIPE信號。(就是如果服務器不啟動,而客戶端啟動向服務器發送數據,則服務端向客戶端發送RST,并且向客戶端寫入數據(相當于客戶端讀取數據),則產生SIGPIPE信號,進程強行終止)
c) 使用非阻塞式connect的套接字已建立連接,或者connect已經以失敗告終。(只有成功connect,才能進行數據的寫入)
d) 其上有一個套接字錯誤待處理。
?? 注意:當某個套接字上發生錯誤時,它將由select標記為即可讀又可寫。
?? 接收低水位標記和發送低水位標記的目的在于:允許應用進程控制在select返回可讀或可寫條件之前有多少數據可讀或有多大空間可用于寫。舉例來說,如果我們知道除非至少存在64個字節的數據,否則我們的應用進程沒有任何有效工作可做,那么可以把接收低水位標記設置為64,以防少于64個字節的數據準備好讀時select喚醒我們。
### 3) 使用select的str_cli函數的實現
客戶的套接字上的三個條件處理如下:
(1)如果對端TCP發送數據,那么該套接字變為可讀,并且read返回一個大于0的值(即讀入數據的字節數)
(2)如果對端TCP發送一個FIN(對端進程終止),那么該套接字變為可讀,并且read返回0(EOF)。
(3)如果對端TCP發送一個RST(對端主機崩潰并重新啟動),那么該套接字變為可讀,并且read返回-1,而errno中含有確切的錯誤碼。
~~~
void str_cli( FILE *fp, int sockfd )
{
int maxfdp1;
fd_set rset;
char sendline[ MAXLINE ], recvline[ MAXLINE ];
FD_ZERO(&rset);
for( ; ; ){
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, *rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
if ( FD_ISSET(sockfd,&rset)){
if ( Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli:server terminated prematurely");
Fputs(recvline, stdout);
}
if ( FD_ISSET(fileno(fp), &rset)){
if ( Fgets(sendline, MAXLINE, fp) == NULL)
return;
Writen(sockfd, sendline, strlen(sendline));
}
}
}
~~~
### 4) 使用select版本的str_cli函數仍不正確,但問題出在哪里
?? 如果我們批量輸入的情況下,對標準輸入中的EOF的處理:str_cli函數就此返回到main函數,而main函數隨后終止。然而在批量方式下,標準輸入中的EOF并不意味著我們同時也完成了從套接字的讀入;可能仍有請求在去往服務器的路上,或者仍有應答在返回客戶的路上。
(1)在fgets函數處返回單個輸入行寫給服務器,隨后select再次被調用以等待新的工作,而不管stdio緩沖區中還有額外的輸入行待消費。究其原因在于select不知道stdio使用了緩沖區---它只是從read系統調用的角度指出是否有數據可讀,而不是從fgets之類調用的角度考慮。
(2)而在readline調用中,這回select不可見的數據不是隱藏在stdio緩沖區,而是隱藏在readline自己的緩沖區中。所以也可能導致程序終止時緩沖區中還有未讀取的數據。
## 3. shutdown函數,poll函數以及TCP回射服務器程序的修訂版
### 1) shutdown函數
?? 終止網絡連接的通常方法是調用close函數,不過close有兩個限制,卻可以使用shutdown來避免:
(1)close把描述符的引用計數減1,僅在該計數變為0時才關閉套接字。使用shutdown可以不管引用計數就激發TCP的正常連接終止序列。
(2)close終止讀和寫兩個方向的數據傳送。這導致有些數據存于緩沖區內,并未被發送/接收成功。
~~~
#include <sys/socket.h>
int shutdown( int sockfd, int howto );
返回:若成功則為0,若出錯則為-1
~~~
該函數依賴于howto參數的值:
SHUT_RD:關閉連接的讀這一半----套接字中不再有數據可接收,而且套接字接收緩沖區中的現有數據都被丟棄。進程不能再對這樣的套接字調用任何讀函數。對一個TCP套接字這樣調用shutdown函數后,由該套接字接收的來自對端的任何數據都被確認,然后悄然丟棄。
SHUT_WR:關閉連接的寫這一半----對于TCP套接字,這稱為半關閉。當前留在套接字發送緩沖區中的數據將被發送掉,后跟TCP的正常連接終止序列。
SHUT_RDWR:連接的讀半部和寫半部都關閉----這與調用shutdown兩次等效:第一個調用指定SHUT_RD,第二次調用指定SHUT_WR.
### 2)str_cli函數的修訂版
(1)我們使用read和write函數處理緩沖區而非文本,可以保證緩沖區的數據完全的讀取。
(2)如果執行了err_quit函數,則說明服務器過早的終止。
(3)使用shutdown(sockfd,SHUT_WR)的作用是:終止寫入,并且把緩沖區所有的數據全部發送出去
~~~
void str_cli( FILE *fp, int sockfd )
{
int maxfdp1, stdineof;
fd_set rset;
char buf[ MAXLINE ];
int n;
stdineof = 0;
FD_ZERO(&rset);
for( ; ; ){
if ( stdineof == 0 )
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, *rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
/*read和write是對緩沖區進行操作*/
if ( FD_ISSET(sockfd,&rset)){
if ( ( n = Read(sockfd,buf,MAXLINE)) == 0){
if ( stdineof == 1 )
return;
else
err_quit("str_cli:server terminated prematurely");
}
Write(fileno(stdout),buf,n);
}
if ( FD_ISSET(fileno(fp), &rset)){
//說明數據已經從緩沖區中讀取完畢,即全部數據都發送給進程
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0){
stdineof = 1;
Shutdown(sockfd,SHUT_WR);
FD_CLR(fileno(fp),&rset);
continue;
}
//因為執行了Shutdown(sockfd,SHUT_WR);說明所有存在緩沖區的數據,均被發送到了sockfd
Writen(sockfd, sendline, strlen(sendline));
}
}
}
~~~
### 3) TCP回射服務器程序(修訂版)
服務端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <sys/select.h>
#define MAXLINE 1024
#define SA struct sockaddr
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9877);
bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
listen(listenfd, 5);
maxfd = listenfd;
maxi = -1;
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; ){
rset = allset;
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)){
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0){
client[i] = connfd;
break;
}
if (i == FD_SETSIZE){
printf("too many clients\n");
exit(-1);
}
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
if (i > maxi)
maxi = i;
if (--nready <= 0)
continue;
}
for (i = 0; i <= maxi; i++){
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)){
if ((n = read(sockfd, buf, MAXLINE)) == 0){
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
write(sockfd, buf, n);
if (--nready <= 0)
break;
}
}
}
}
~~~
客戶端:
~~~
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <fcntl.h>
#define MAXLINE 1024
#define SA struct sockaddr
void str_cli(FILE *fp, int sockfd);
int main(int argc, char **argv)
{
int sockfd[5], n;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
int i;
for (i = 0; i < 5; i++){
sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr));
}
str_cli(stdin, sockfd[0]);
return 0;
}
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
int n;
while (fgets(sendline, MAXLINE, fp) != NULL){
write(sockfd, sendline, strlen(sendline));
if (( n = read(sockfd, recvline, MAXLINE)) == 0){
printf("str_cli:server terminated prematurely\n");
return;
}
recvline[n] = '\0';
fputs(recvline, stdout);
}
}
~~~
程序輸出:
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ ./srv
~~~
客戶端1:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
hello world
hello world
what
what
^C
~~~
客戶端2:
~~~
leichaojian@ThinkPad-T430i:~$ ./cli 127.0.0.1
heihei
heihei
^C
~~~