網絡I/O一直是Linux網絡編程中極其重要的一部分,除了前面講到的send、recv等,socket編程接口還給出了不少高級了I/O函數,這些函數大體分爲三類:用於建立文件描述符的函數、用於讀寫控制的函數和用於控制I/O行爲和屬性的函數。編程
pipe函數是用來建立一個管道,管道是較爲原始的進程間通訊手段,分爲無名管道和有名管道,而無名管道只能用於有親緣關係的進程之間傳遞消息。pipe創建的管道是單工的,其參數是一個包含兩個元素的整形數組fd[2],建立成功後fd[0]表明管道可讀的一端,fd[1]表明可寫的一端,這兩個的本質都是文件描述符,當進程間有數據要傳輸時,數據發送的一端須要關閉fd[0],接收端要關閉fd[1],才能正常傳送數據。須要注意的是無名管道只能用低級文件編程庫中的讀寫函數進行操做,如read和write,當咱們向一個空管道執行read時,函數會阻塞,直到有數據寫入才繼續執行,同理對滿的管道執行write也會進入阻塞狀態。可是若是對於這兩個文件描述符設置爲非阻塞模式,則他們會有不一樣的行爲。若是fd[1]的引用計數減小至0,即沒有寫端進程向管道中寫,則fd[0]上的read操做將會讀取到EOF標誌,返回0;反之若是fd[0]上的引用計數減小至0,即沒有讀端程序調用read,則此時fd[1]上的write操做將失敗並引起SIGPIPE信號。爲了便於使用,API中還有一個函數用來建立雙向管道,是socketpair函數,使用這個函數建立的雙向管道只能使用AF_UNIX協議,即UNIX本地域協議族,它建立的兩個文件描述符是既可讀又可寫的。數組
dup函數和dup2函數用於複製文件描述符,區別在於dup函數是將一個文件描述符複製到當前系統可用的最小整數值,而dup2則是不小於其參數的最小整數值,注意,經過這兩個函數複製的文件描述符不繼承其原來的屬性。咱們來看一個CGI服務器的例子:服務器
1 /************************************************************************* 2 > File Name: 6-1.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Thu 01 Feb 2018 11:29:09 PM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char **argv) { 12 if(argc <= 2) { 13 printf("usage: %s ip_address port_number\n", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 struct sockaddr_in address; 19 bzero(&address, sizeof(address)); 20 address.sin_family = AF_INET; 21 inet_pton(AF_INET, ip, &address.sin_addr); 22 address.sin_port = htons(port); 23 24 int sock = socket(AF_INET, SOCK_STREAM, 0); 25 assert(sock >= 0); 26 27 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 28 assert(ret != -1); 29 30 ret = listen(sock, 5); 31 assert(ret != -1); 32 33 struct sockaddr_in client; 34 socklen_t client_addrlength = sizeof(client); 35 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 36 if(connfd < 0) { 37 printf("errno is: %d\n", errno); 38 } 39 else { 40 close(STDOUT_FILENO); 41 int newfd = dup(connfd); 42 printf("abcd\n"); 43 close(connfd); 44 } 45 close(sock); 46 return 0; 47 }
使用telnet客戶端鏈接服務器發現有abcd的回顯,經過這個例子咱們能夠看到,咱們關閉了標準輸出文件描述符後再調用dup,會將要複製的connfd複製到當前未使用的最小的文件描述符也就是標準輸出文件描述符上,實現了輸出的重定向。網絡
readv和writev函數和前面提過的readmsg和writemsg函數相似,也是用來對數據的集中寫和分散讀,至關於前面兩個函數的簡化版。舉一個例子來講明,在Web服務器解析完HTTP請求後若是客戶端請求的文件存在而且有權限時,就須要返回一個HTTP首部狀態碼和狀態信息,而後再返回該文件,可是咱們考慮效率問題,若是每次咱們都須要將兩個不相關的存儲空間合併到一塊兒再發送勢必會很影響效率,因此咱們能夠事先將HTTP不一樣的頭部存儲好,找到文件後使用sendv函數直接發送便可。咱們創建一個test.txt文件模擬一下,服務器代碼以下:socket
1 /************************************************************************* 2 > File Name: 6-2.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 01:30:46 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 #define BUFFER_SIZE 1024 12 //用於定義HTTP的成功和失敗的狀態碼和狀態信息 13 static const char* status_line[2] = {"200 OK", "500 Internal server error"}; 14 15 int main(int argc, char **argv) { 16 if(argc <= 3) { 17 printf("usage: %s ip_address port_number filename\n", basename(argv[0])); 18 return 1; 19 } 20 const char* ip = argv[1]; 21 int port = atoi(argv[2]); 22 const char* file_name = argv[3]; 23 24 struct sockaddr_in address; 25 bzero(&address, sizeof(address)); 26 address.sin_family = AF_INET; 27 address.sin_port = htons(port); 28 inet_pton(AF_INET, ip, &address.sin_addr); 29 30 int sock = socket(AF_INET, SOCK_STREAM, 0); 31 assert(sock >= 0); 32 33 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 34 assert(ret != -1); 35 36 ret = listen(sock, 5); 37 assert(ret != -1); 38 39 struct sockaddr_in client; 40 socklen_t client_addrlength = sizeof(client); 41 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 42 if(connfd < 0) { 43 printf("errno is: %d\n", errno); 44 } 45 else { 46 char header_buf[BUFFER_SIZE]; 47 memset(header_buf, 0, sizeof(header_buf)); 48 char *file_buf; 49 struct stat file_stat; //用於獲取文件屬性的結構體 50 bool file_is_valid = true; 51 int len = 0; 52 if(stat(file_name, &file_stat) < 0) { //獲取文件信息 53 file_is_valid = false; 54 } 55 else { 56 if(S_ISDIR(file_stat.st_mode)) { //若是是目錄 57 file_is_valid = false; 58 } 59 else if(file_stat.st_mode & S_IROTH) { //若是當前用戶對文件有讀的權限 60 int fd = open(file_name, O_RDONLY); 61 file_buf = new char[file_stat.st_size + 1]; 62 memset(file_buf, 0, sizeof(file_buf)); 63 if(read(fd, file_buf, file_stat.st_size + 1) < 0) { 64 file_is_valid = false; 65 } 66 } 67 else file_is_valid = false; 68 //若是文件合法則返回正確的狀態信息以及文件內容,不然返回錯誤 69 if(file_is_valid) { 70 ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[0]); 71 len += ret; 72 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "Content-Length: %d\r\n", (int)file_stat.st_size); 73 len += ret; 74 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n"); 75 //將頭部信息和文件信息裝入不一樣的iovec中調用writev集中寫 76 struct iovec iv[2]; 77 iv[0].iov_base = header_buf; 78 iv[0].iov_len = strlen(header_buf); 79 iv[1].iov_base = file_buf; 80 iv[1].iov_len = strlen(file_buf); 81 ret = writev(connfd, iv, 2); 82 } 83 else { 84 ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[1]); 85 len += ret; 86 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n"); 87 send(connfd, header_buf, strlen(header_buf), 0); 88 } 89 } 90 close(connfd); 91 delete []file_buf; 92 } 93 close(sock); 94 return 0; 95 }
使用telnet鏈接鏈接服務器端,發現正常回顯了HTTP首部和數據。函數
接下來還有一個較爲經常使用的函數sendfile,它的做用是在兩個文件描述符之間直接傳遞數據,是一個零拷貝函數。所謂零拷貝函數,首先要知道內核空間和用戶空間的概念和區別。Linux操做系統的內核使用了內存中的低地址區域,這裏是咱們在編程時不能訪問的,不少緩衝區都是在這裏定義,而用戶空間就是其他的內存空間,咱們在編寫程序時能夠進行操做。平時咱們調用recv函數會將網絡I/O數據拷貝到定義的用戶緩衝區內,這樣就會在內核空間和用戶空間之間進行數據拷貝,這樣就會致使進程再內核態和用戶態之間進行頻繁轉換,下降效率。而零拷貝函數能夠直接在內核態完成數據的傳遞,效率較高。其函數原型以下:spa
1 #include<sys/sendfile.h> 2 ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
值得注意的是,out_fd是待寫入的文件描述符,必須是一個socket,而in_fd是待讀出的文件描述符,但它必須指向真實的文件,不能使管道或者socket。咱們用一個例子來看一下:操作系統
1 /************************************************************************* 2 > File Name: 6-3.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 03:25:03 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc <= 3) { 13 printf("usage: %s ip_address port_number file_name", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 const char* file_name = argv[3]; 19 20 int filefd = open(file_name, O_RDONLY); 21 assert(filefd > 0); 22 struct stat stat_buf; 23 fstat(filefd, &stat_buf); 24 25 struct sockaddr_in address; 26 bzero(&address, sizeof(address)); 27 address.sin_family = AF_INET; 28 inet_pton(AF_INET, ip, &address.sin_addr); 29 address.sin_port = htons(port); 30 31 int sock = socket(AF_INET, SOCK_STREAM, 0); 32 assert(sock >= 0); 33 34 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 35 assert(ret != -1); 36 37 ret = listen(sock, 5); 38 assert(ret != -1); 39 40 struct sockaddr_in client; 41 socklen_t client_addrlength = sizeof(client); 42 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 43 if(connfd < 0) { 44 printf("errno is: %d\n", errno); 45 } 46 else { 47 sendfile(connfd, filefd, NULL, stat_buf.st_size); 48 close(connfd); 49 } 50 close(sock); 51 return 0; 52 }
在上例中未在用戶空間內申請任何緩衝區即完成了文件的傳送,效率要比原始作法高得多。code
splice函數用來在兩個文件描述符間移動數據,也是零拷貝操做,可是其in_fd和out_fd中必須至少有一個管道文件描述符,調用成功時返回一共轉移的字節數,以一個splice實現的簡單回射服務器爲例:server
1 /************************************************************************* 2 > File Name: 6-4.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 03:45:40 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc <= 2) { 13 printf("usage: %s ip_address port_number\n", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 19 struct sockaddr_in address; 20 bzero(&address, sizeof(address)); 21 address.sin_family = AF_INET; 22 inet_pton(AF_INET, ip, &address.sin_addr); 23 address.sin_port = htons(port); 24 25 int sock = socket(AF_INET, SOCK_STREAM, 0); 26 assert(sock >= 0); 27 28 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 29 assert(ret != -1); 30 31 ret = listen(sock, 5); 32 assert(ret != -1); 33 34 struct sockaddr_in client; 35 socklen_t client_addrlength = sizeof(client); 36 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 37 if(connfd < 0) { 38 printf("errno is: %d\n", errno); 39 } 40 else { 41 int pipefd[2]; 42 assert(ret != -1); 43 ret = pipe(pipefd); 44 ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 45 assert(ret != -1); 46 ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 47 assert(ret != -1); 48 close(connfd); 49 } 50 close(sock); 51 return 0; 52 }
因爲咱們不能將數據從connfd的輸入直接變成connfd的輸出,因此咱們藉助了一個管道,將connfd的輸入與管道的輸入鏈接,將管道的輸出與connfd的回射鏈接,這樣就作成了一個高效率的回射服務器。
tee函數是在兩個管道文件描述符之間複製數據,也是零拷貝操做,而它不消耗數據,原始數據仍能夠用於後續操做,函數原型與返回值與splice相似,咱們以一個能夠同時輸出數據到終端和文件的程序爲例:
1 /************************************************************************* 2 > File Name: 6-5.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 04:28:26 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc != 2) { 13 printf("usage: %s <file>\n", basename(argv[0])); 14 return 1; 15 } 16 int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666); 17 assert(filefd > 0); 18 19 int pipefd_stdout[2]; 20 int ret = pipe(pipefd_stdout); 21 assert(ret != -1); 22 23 int pipefd_file[2]; 24 ret = pipe(pipefd_file); 25 assert(ret != -1); 26 27 //將標準輸入重定向到管道pipefd_stdout中 28 ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); 29 assert(ret != -1); 30 31 //將pipefd_stdout中的數據拷貝一份到pipefd_file中 32 ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK); 33 assert(ret != -1); 34 35 //分別將兩個管道的輸出端和標準輸出文件與建立的文件相鏈接 36 ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE); 37 assert(ret != -1); 38 39 ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 40 assert(ret != -1); 41 42 close(filefd); 43 close(pipefd_file[0]); 44 close(pipefd_file[1]); 45 close(pipefd_stdout[0]); 46 close(pipefd_stdout[1]); 47 return 0; 48 }
此程序有一個問題,使用splice將pipefd_stdout[0]鏈接到STDOUT_FILENO時出錯,errno的值爲EINVAL,在網上查了很久資料都沒有收穫,簡單說一下個人見解:返回EINVAL的緣由主要有四種,目標文件系統不支持splice,目標文件以追加方式打開,兩個文件描述符都不是管道文件和某個offset參數被用於不支持隨機訪問的設備,而咱們能夠輕易排除一、三、4,標準輸出文件默認應該是以追加方式打開的,這樣在輸出時纔不會覆蓋以前的數據,因此splice出錯。
Linux提供了tee命令用於完成上述程序的操做,在tee函數的幫助文檔裏也有一個例子來完成上述操做,可用man 2 tee來查看。