第 16 章 關于 I/O 流分離的其他內容
?
16.1 分離 I/O 流
????????「分離 I/O 流」是一種常用表達。有 I/O 工具可區分二者,無論采用哪種方法,都可以認為是分離了 I/O 流。
2次 I/O 流分離:
- 第一種是第 10 章的「TCP I/O 過程」分離。通
shutdown(sock,SHUT_WR);
- 第二種分離是在第 15 章。通過 2 次 fdopen 函數的調用,創建讀模式 FILE 指針(FILE 結構體指針)和寫模式 FILE 指針。換言之,我們分離了輸入工具和輸出工具,因此也可視為「流」的分離。下面是分離的理由。
分離「流」的好處:
????????首先是第 10 章「流」的分離目的:
- 通過分開輸入過程(代碼)和輸出過程降低實現難度
- 與輸入無關的輸出操作可以提高速度
????????下面是第 15 章「流」分離的目的:
- 為了將 FILE 指針按讀模式和寫模式加以區分
- 可以通過區分讀寫模式降低實現難度
- 通過區分 I/O 緩沖提高緩沖性能
「流」分離帶來的 EOF 問題:
????????第 7 章介紹過 EOF 的傳遞方法和半關閉的必要性。有一個語句:
shutdown(sock,SHUT_WR);
????????當時說過調用 shutdown 函數的基于半關閉的 EOF 傳遞方法。第十章添加了半關閉的相關代碼。但是還沒有講采用 fdopen 函數怎么半關閉。那么是否是通過 fclose 函數關閉流呢?我們先試試:
????????服務端代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int serv_sock, clnt_sock;FILE *readfp;FILE *writefp;struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;char buf[BUF_SIZE] = {0,};serv_sock = socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);serv_adr.sin_port = htons(atoi(argv[1]));bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));listen(serv_sock, 5);clnt_adr_sz = sizeof(clnt_adr);clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);readfp = fdopen(clnt_sock, "r");writefp = fdopen(clnt_sock, "w");fputs("FROM SERVER: Hi~ client? \n", writefp);fputs("I love all of the world \n", writefp);fputs("You are awesome! \n", writefp);fflush(writefp);fclose(writefp);fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);return 0;
}
? ? ? ? 客戶端代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int sock;char buf[BUF_SIZE];struct sockaddr_in serv_addr;FILE *readfp;FILE *writefp;sock = socket(PF_INET, SOCK_STREAM, 0);memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = inet_addr(argv[1]);serv_addr.sin_port = htons(atoi(argv[2]));connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));readfp = fdopen(sock, "r");writefp = fdopen(sock, "w");while (1){if (fgets(buf, sizeof(buf), readfp) == NULL)break;fputs(buf, stdout);fflush(stdout);}fputs("FROM CLIENT: Thank you \n", writefp);fflush(writefp);fclose(writefp);fclose(readfp);return 0;
}
運行結果:
????????
????????從運行結果可以看出,服務端最終沒有收到客戶端發送的信息。
????????原因是:服務端代碼的?fclose(writefp);
?這一句,完全關閉了套接字而不是半關閉。這才是這一章需要解決的問題。
16.2 文件描述符的的復制和半關閉
終止「流」時無法半關閉原因:
????????下面的圖描述的是服務端代碼中的兩個FILE 指針、文件描述符和套接字中的關系:
????????從圖中可以看到,兩個指針都是基于同一文件描述符創建的。因此,針對于任何一個 FILE 指針調用 fclose 函數都會關閉文件描述符,如圖所示:
????????那如何進入可以進入但是無法輸出的半關閉狀態呢?如下圖所示:
????????只需要創建 FILE 指針前先復制文件描述符即可。復制后另外創建一個文件描述符,然后利用各自的文件描述符生成讀模式的 FILE 指針和寫模式的 FILE 指針。這就為半關閉創造好了環境,因為套接字和文件描述符具有如下關系:?
????????銷毀所有文件描述符候才能銷毀套接字。
????????也就是說,針對寫模式 FILE 指針調用 fclose 函數時,只能銷毀與該 FILE 指針相關的文件描述符,無法銷毀套接字,如下圖:
????????那么調用 fclose 函數候還剩下 1 個文件描述符,因此沒有銷毀套接字。那此時的狀態是否為半關閉狀態?不是!只是準備好了進入半關閉狀態,而不是已經進入了半關閉狀態。仔細觀察,還剩下一個文件描述符。而該文件描述符可以同時進行 I/O 。因此,不但沒有發送 EOF ,而且仍然可以利用文件描述符進行輸出。?
復制文件描述符:
????????與調用 fork 函數不同,調用 fork 函數將復制整個進程,此處討論的是同一進程內完成對描述符的復制。如圖:
????????復制完成后,兩個文件描述符都可以訪問文件,但是編號不同。?
dup 和 dup2:
????????下面給出文件描述符的復制方法:
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
/*
成功時返回復制的文件描述符,失敗時返回 -1
fildes : 需要復制的文件描述符
fildes2 : 明確指定的文件描述符的整數值。
*/
????????dup2 函數明確指定復制的文件描述符的整數值。向其傳遞大于 0 且小于進程能生成的最大文件描述符值時,該值將成為復制出的文件描述符值。下面是dup的代碼示例:
#include <stdio.h>
#include <unistd.h>int main(int argc, char *argv[])
{int cfd1, cfd2;char str1[] = "Hi~ \n";char str2[] = "It's nice day~ \n";cfd1 = dup(1); //復制文件描述符 1cfd2 = dup2(cfd1, 7); //再次復制文件描述符,定為數值 7printf("fd1=%d , fd2=%d \n", cfd1, cfd2);write(cfd1, str1, sizeof(str1));write(cfd2, str2, sizeof(str2));close(cfd1);close(cfd2); //終止復制的文件描述符,但是仍有一個文件描述符write(1, str1, sizeof(str1));close(1);write(1, str2, sizeof(str2)); //無法完成輸出return 0;
}
? ? ? ? ?運行結果:
????????復制文件描述符后「流」的分離?:
????????下面更改sep_clnt.c和sep_serv.c???可以使得讓它正常工作,正常工作是指通過服務器的半關閉狀態接收客戶端最后發送的字符串。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024int main(int argc, char *argv[])
{int serv_sock, clnt_sock;FILE *readfp;FILE *writefp;struct sockaddr_in serv_adr, clnt_adr;socklen_t clnt_adr_sz;char buf[BUF_SIZE] = {0,};serv_sock = socket(PF_INET, SOCK_STREAM, 0);memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);serv_adr.sin_port = htons(atoi(argv[1]));bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));listen(serv_sock, 5);clnt_adr_sz = sizeof(clnt_adr);clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);readfp = fdopen(clnt_sock, "r");writefp = fdopen(dup(clnt_sock), "w"); //復制文件描述符fputs("FROM SERVER: Hi~ client? \n", writefp);fputs("I love all of the world \n", writefp);fputs("You are awesome! \n", writefp);fflush(writefp);shutdown(fileno(writefp), SHUT_WR); //對 fileno 產生的文件描述符使用 shutdown 進入半關閉狀態fclose(writefp);fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);return 0;
}
? ? ? ? 運行結果:
?
? ? ? ? ?運行結果證明了 服務器端在半關閉狀態下向客戶端發送了EOF。