許多初學者都是從阻塞式IO網絡編程開始的。若是一個IO操做是同步的,意味着當你調用相關的函數時,除非IO操做已經完成,不然函數不會當即返回,或者達到超時時間後纔會返回。舉例來講,當你使用TCP協議中的connect()函數時,你所在的操做系統將一個SYN包放入發往TCP另外一端的數據隊列中。除非從TCP另外一端收到SYN ACK包,不然你的應用程序不會得到響應,或者通過足夠的時間後放棄了鏈接網絡纔會得到響應。程序員
這裏有一個使用阻塞式IO的簡單的客戶端的例子:它打開一個www.google.com的鏈接,發送一個簡單的HTTP請求,而後輸出應答到stdout。編程
Example: A simple blocking HTTP client數組
/* For sockaddr_in */#include <netinet/in.h>/* For socket functions */ #include <sys/socket.h>/* For gethostbyname */#include <netdb.h> #include <unistd.h> #include <string.h> #include <stdio.h> int main(int c, char **v) { const char query[] = "GET / HTTP/1.0\r\n" "Host: www.google.com\r\n" "\r\n"; const char hostname[] = "www.google.com"; struct sockaddr_in sin; struct hostent *h; const char *cp; int fd; ssize_t n_written, remaining; char buf[1024]; h = gethostbyname(hostname); if (!h) { fprintf(stderr, "Couldn't lookup %s: %s", hostname, hstrerror(h_errno)); return 1; } if (h->h_addrtype != AF_INET) { fprintf(stderr, "No ipv6 support, sorry."); return 1; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("socket"); return 1; } /* Connect to the remote host. */ sin.sin_family = AF_INET; sin.sin_port = htons(80); sin.sin_addr = *(struct in_addr*)h->h_addr; if (connect(fd, (struct sockaddr*) &sin, sizeof(sin))) { perror("connect"); close(fd); return 1; } /* Write the query. */ /* XXX Can send succeed partially? */ cp = query; remaining = strlen(query); while (remaining) { n_written = send(fd, cp, remaining, 0); if (n_written <= 0) { perror("send"); return 1; } remaining -= n_written; cp += n_written; } /* Get an answer back. */ while (1) { ssize_t result = recv(fd, buf, sizeof(buf), 0); if (result == 0) { break; } else if (result < 0) { perror("recv"); close(fd); return 1; } fwrite(buf, 1, result, stdout); } close(fd); return 0; }
上面全部的網絡函數調用都是阻塞的:gethostbyname在成功或者失敗抵達www.google.com前不會返回;connect在鏈接成功以前不會返回;recv在得到數據或者關閉以前不會返回;send在刷新完輸出數據到kernel' s write buffers以前不會返回。
服務器
即便到如今,阻塞式IO也不是毫無可取之處的。若是你的程序在IO時沒有其它的事情要作,阻塞式IO是一個不錯的選擇。可是,假如你須要寫一個能當即同時處理不少鏈接的程序,舉例來講,假設你須要從2個鏈接讀取輸入數據,你是沒法肯定先從哪個鏈接先讀入的。由於假如第2個鏈接的數據先到達,你的程序就不會讀取第2個鏈接的數據除非第1個鏈接的數據已經達到而且已經讀取完畢。網絡
有時候程序員們經過多線程的方式來解決這個問題,或者採用多進程的方式。這種方式的實現的最簡單的方法之一就是:一個線程(進程)處理一個鏈接。由於每個鏈接都有它本身的線程(進程),因此當一個鏈接阻塞了的時候並不會阻塞或者說影響到其它的鏈接線程(進程)的處理過程。
多線程
這裏有另一個例子,一個較爲複雜的服務器。它監聽40713端口,每次讀取一行輸入數據,而且writes out the ROT13 obfuscation of line each as it arrives。 它使用Unix的fork()函數爲每個新的鏈接建立一個新的進程。異步
Example: Forking ROT13 serversocket
/* For sockaddr_in */#include <netinet/in.h>/* For socket functions */ #include <sys/socket.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #define MAX_LINE 16384charrot13_char(char c) { /* We don't want to use isalpha here; setting the locale would change * which characters are considered alphabetical. */ if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) return c + 13; else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) return c - 13; else return c; } void child(int fd) { char outbuf[MAX_LINE+1]; size_t outbuf_used = 0; ssize_t result; while (1) { char ch; result = recv(fd, &ch, 1, 0); if (result == 0) { break; } else if (result == -1) { perror("read"); break; } /* We do this test to keep the user from overflowing the buffer. */ if (outbuf_used < sizeof(outbuf)) { outbuf[outbuf_used++] = rot13_char(ch); } if (ch == '\n') { send(fd, outbuf, outbuf_used, 0); outbuf_used = 0; continue; } } } void run(void) { int listener; struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = 0; sin.sin_port = htons(40713); listener = socket(AF_INET, SOCK_STREAM, 0); #ifndef WIN32 { int one = 1; setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); } #endif if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) { perror("bind"); return; } if (listen(listener, 16)<0) { perror("listen"); return; } while (1) { struct sockaddr_storage ss; socklen_t slen = sizeof(ss); int fd = accept(listener, (struct sockaddr*)&ss, &slen); if (fd < 0) { perror("accept"); } else { if (fork() == 0) { child(fd); exit(0); } } } } int main(int c, char **v) { run(); return 0; }
至此,咱們有了最完美的處理多個鏈接的解決方案了嗎?我該中止繼續寫寫下去作點別的事情嗎?還沒完。首先,建立進程(線程)在某些平臺上會是一筆不小的開銷。在實際實踐中,你可能更想使用一個線程池來代替它。可是從根本上講,多線程並不可以達到你所指望的那種擴展性。若是你程序須要同時處理成千上萬個鏈接,對於每一個CPU僅能嘗試處理不多的線程的狀況,處理成千上萬個線程效率並不高。ide
若是多線程(進程)並非解決多個鏈接的答案,那麼該是什麼呢?在Unix中,你可使你的套接字變成非阻塞的。Unix系統下像這樣 調用:
函數
fcntl(fd, F_SETFL, O_NONBLOCK);
fd是套接字描述符。一旦你使套接字變成非阻塞的,從如今開始,不論你何時經過fd調用網絡函數都會在操做完成後當即返回,或者當即返回一個特定的錯誤碼來代表」我如今不能處理任何事情,請再次嘗試「。所以,咱們的2個鏈接的例子能夠改寫成這樣:
Bad Example: busy-polling all sockets
/* This will work, but the performance will be unforgivably bad. */ int i, n; char buf[1024]; for (i=0; i < n_sockets; ++i) fcntl(fd[i], F_SETFL, O_NONBLOCK);while (i_still_want_to_read()) { for (i=0; i < n_sockets; ++i) { n = recv(fd[i], buf, sizeof(buf), 0); if (n == 0) { handle_close(fd[i]); } else if (n < 0) { if (errno == EAGAIN) ; /* The kernel didn't have any data for us to read. */ else handle_error(fd[i], errno); } else { handle_input(fd[i], buf, n); } } }
如今咱們使用的是非阻塞套接字,上面的代碼僅僅是能工做。它的性能極差,2個緣由:第一,當沒有數據能夠讀取時,循環體將會不停地無限循環,它將佔用掉全部的CPU時間片;第2,在處理多個鏈接時,無論有沒有數據,你都須要執行一次kernel call。所以,咱們須要一個方法來讓kenel作到:」一直等待這些套接字直到它有數據給我,而且告訴我是哪些套接字有數據了「。
這個問題的傳統的解決方案是使用select()函數。select()可以管理三種類型事件的套接字集合(使用位數組實現的):一種是讀取事件,一種是寫入事件,一種是異常事件。它一直等待直到一個集合中的套接字就緒,而且通知集合中僅包含就緒的套接字。
下面是使用select的一個例子:
Example: Using select
/* If you only have a couple dozen fds, this version won't be awful */ fd_set readset; int i, n; char buf[1024]; while (i_still_want_to_read()) { int maxfd = -1; FD_ZERO(&readset); /* Add all of the interesting fds to readset */ for (i=0; i < n_sockets; ++i) { if (fd[i]>maxfd) maxfd = fd[i]; FD_SET(fd[i], &readset); } /* Wait until one or more fds are ready to read */ select(maxfd+1, &readset, NULL, NULL, NULL); /* Process all of the fds that are still set in readset */ for (i=0; i < n_sockets; ++i) { if (FD_ISSET(fd[i], &readset)) { n = recv(fd[i], buf, sizeof(buf), 0); if (n == 0) { handle_close(fd[i]); } else if (n < 0) { if (errno == EAGAIN) ; /* The kernel didn't have any data for us to read. */ else handle_error(fd[i], errno); } else { handle_input(fd[i], buf, n); } } } }