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