哈爾濱網(wǎng)站建設(shè)價格網(wǎng)站制作流程和方法
《TCP/IP網(wǎng)絡(luò)編程》學習筆記 | Chapter 16:關(guān)于 I/O 流分離的其他內(nèi)容
- 《TCP/IP網(wǎng)絡(luò)編程》學習筆記 | Chapter 16:關(guān)于 I/O 流分離的其他內(nèi)容
- 分離 I/O 流
- 2 次 I/O 流分離
- 分離「流」的好處
- 「流」分離帶來的 EOF 問題
- 文件描述符的的復制和半關(guān)閉
- 終止「流」時無法半關(guān)閉原因
- 復制文件描述符
- 復制文件描述符后「流」的分離
- 習題
- (1)下列關(guān)于FILE結(jié)構(gòu)體指針和文件描述符的說法錯誤的是?
- (2)EOF的發(fā)送相關(guān)描述中錯誤的是?
《TCP/IP網(wǎng)絡(luò)編程》學習筆記 | Chapter 16:關(guān)于 I/O 流分離的其他內(nèi)容
分離 I/O 流
「分離 I/O 流」是一種常用表達。有 I/O 工具可區(qū)分二者,無論采用哪種方法,都可以認為是分離了 I/O 流。
2 次 I/O 流分離
之前有兩種分離方法:
第一種是第 10 章的「TCP I/O 過程」分離。通過調(diào)用 fork 函數(shù)復制出一個文件描述符,以區(qū)分輸入和輸出中使用的文件描述符。雖然文件描述符本身不會根據(jù)輸入和輸出進行區(qū)分,但我們分開了 2 個文件描述符的用途,因此,這也屬于「流」的分離。
第二種分離是在第 15 章。通過 2 次 fdopen 函數(shù)的調(diào)用,創(chuàng)建讀模式 FILE 指針(FILE 結(jié)構(gòu)體指針)和寫模式 FILE 指針。換言之,我們分離了輸入工具和輸出工具,因此也可視為「流」的分離。
分離「流」的好處
首先是第 10 章「流」的分離目的:
- 通過分開輸入過程(代碼)和輸出過程降低實現(xiàn)難度
- 與輸入無關(guān)的輸出操作可以提高速度
下面是第 15 章「流」分離的目的:
- 為了將 FILE 指針按讀模式和寫模式加以區(qū)分
- 可以通過區(qū)分讀寫模式降低實現(xiàn)難度
- 通過區(qū)分 I/O 緩沖提高緩沖性能
「流」分離帶來的 EOF 問題
第 7 章介紹過 EOF 的傳遞方法和半關(guān)閉的必要性。有一個語句:
shutdown(sock,SHUT_WR);
當時說過調(diào)用 shutdown 函數(shù)的基于半關(guān)閉的 EOF 傳遞方法。第十章的 echo_mpclient.c 添加了上述代碼,實現(xiàn)了半關(guān)閉。
但是還沒有講采用 fdopen 函數(shù)怎么半關(guān)閉。那么是否 可以針對輸出模式的 FILE 指針調(diào)用 fclose 函數(shù)呢?我們先試試。
服務器端:
#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];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;
}
運行結(jié)果:
從運行結(jié)果可以看出,服務端最終沒有收到客戶端發(fā)送的信息。那么這是什么原因呢?
原因是:服務端代碼的 fclose(writefp); 這一句,完全關(guān)閉了套接字而不是半關(guān)閉。這才是這一章需要解決的問題。
文件描述符的的復制和半關(guān)閉
終止「流」時無法半關(guān)閉原因
上述服務器端程序2個FILE指針、文件描述符及套接字之間的關(guān)系:
讀模式FILE指針和寫模式FILE指針都是基于同一個文件描述符創(chuàng)建的。任意一個FILE指針調(diào)用fclose函數(shù)時都會關(guān)閉文件描述符,也就是終止套接字。
解決辦法:創(chuàng)建FILE指針前先復制文件描述符即可。
銷毀所有文件描述符后才能銷毀套接字。
針對寫模式FILE指針調(diào)用fclose函數(shù)時,只能銷毀與該FILE指針相關(guān)的文件描述符,無法銷毀套接字。
如上圖所示,調(diào)用 fclose 函數(shù)候還剩下 1 個文件描述符,因此沒有銷毀套接字。那此時的狀態(tài)是否為半關(guān)閉狀態(tài)?不是!只是準備好了進入半關(guān)閉狀態(tài),而不是已經(jīng)進入了半關(guān)閉狀態(tài)。仔細觀察,還剩下一個文件描述符。而該文件描述符可以同時進行 I/O 。因此,不但沒有發(fā)送 EOF ,而且仍然可以利用文件描述符進行輸出。
復制文件描述符
與調(diào)用 fork 函數(shù)不同,調(diào)用 fork 函數(shù)將復制整個進程,此處討論的是同一進程內(nèi)完成對完成描述符的復制。當然,文件描述符的值不能重復,因此各使用一個整數(shù)值。此處的復制的含義:“為了訪問同一文件或套接字,創(chuàng)建另一個文件描述符”。
下面給出兩個文件描述符的復制方法:
#include <unistd.h>int dup(int fildes);
int dup2(int fildes, int fildes2);
成功時返回復制的文件描述符,失敗時返回 -1。
參數(shù):
- fildes : 需要復制的文件描述符。
- fildes2 : 明確指定的文件描述符的整數(shù)值。向其傳遞大于 0 且小于進程能生成的最大文件描述符值時,該值將成為復制出的文件描述符值。
示例程序:
#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); // 再次復制文件描述符,定為數(shù)值 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;
}
運行結(jié)果:
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 16>gcc dup.c -o dupC:\Users\81228\Documents\Program\TCP IP Project\Chapter 16>dup
fd1=3 , fd2=7
Hi~
It's nice day~ Hi~
復制文件描述符后「流」的分離
下面更改 sep_serv.c 可以使得讓它正常工作,正常工作是指通過服務器的半關(guān)閉狀態(tài)接收客戶端最后發(fā)送的字符串。
#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];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);fclose(writefp);fgets(buf, sizeof(buf), readfp);fputs(buf, stdout);fclose(readfp);return 0;
}
注意,無論復制出多少文件描述符,均應調(diào)用shutdown函數(shù)發(fā)送EOF并進入半關(guān)閉狀態(tài)。
調(diào)用shutdown函數(shù)時,無論復制出多少文件描述符都進入半關(guān)閉狀態(tài),并發(fā)送EOF。
習題
(1)下列關(guān)于FILE結(jié)構(gòu)體指針和文件描述符的說法錯誤的是?
a. 與FILE結(jié)構(gòu)體指針相同,文件描述符也分輸入描述符和輸出描述符。
b. 復制文件描述符時將生成相同值的描述符,可以通過這2個描述符進行I/O。
c. 可以利用創(chuàng)建套接字時返回的文件描述符進行I/O ,也可以不通過文件描述符,直接通過FILE結(jié)構(gòu)體指針完成。
d. 可以從文件描述符生成FILE結(jié)構(gòu)體指針,而且可以利用這種FILE結(jié)構(gòu)體指針進行套接字I/O。
e. 若文件描述符為讀模式,則基于該描述符生成的FILE結(jié)構(gòu)體指針同樣是讀模式;若文件描述符為寫模式,則基于該描述符生成的FILE結(jié)構(gòu)體指針同樣是寫模式。
答:a、b、e。
(2)EOF的發(fā)送相關(guān)描述中錯誤的是?
a. 終止文件描述符時發(fā)送EOF。
b. 即使未完全終止文件描述符,關(guān)閉輸出流時也會發(fā)送EOF。
c. 如果復制文件描述符,則包括復制的文件描述符在內(nèi),所有文件描述符都終止時才會發(fā)送EOF。
d. 即使復制文件描述符,也可以通過調(diào)用shutdown函數(shù)進入半關(guān)閉狀態(tài)并發(fā)送EOF。
答:a。