日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

基于TCP协议的网络程序(基础学习)

發(fā)布時(shí)間:2023/12/10 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于TCP协议的网络程序(基础学习) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

下圖是基于TCP協(xié)議的客戶端/服務(wù)器程序的一般流程:

圖?37.2.?TCP協(xié)議通訊流程


服務(wù)器調(diào)用socket()、bind()、listen()完成初始化后,調(diào)用accept()阻塞等待,處于監(jiān)聽(tīng)端口的狀態(tài),客戶端調(diào)用socket()初始化后,調(diào)用connect()發(fā)出SYN段并阻塞等待服務(wù)器應(yīng)答,服務(wù)器應(yīng)答一個(gè)SYN-ACK段,客戶端收到后從connect()返回,同時(shí)應(yīng)答一個(gè)ACK段,服務(wù)器收到后從accept()返回。

數(shù)據(jù)傳輸?shù)倪^(guò)程:

建立連接后,TCP協(xié)議提供全雙工的通信服務(wù),但是一般的客戶端/服務(wù)器程序的流程是由客戶端主動(dòng)發(fā)起請(qǐng)求,服務(wù)器被動(dòng)處理請(qǐng)求,一問(wèn)一答的方式。因此,服務(wù)器從accept()返回后立刻調(diào)用read(),讀socket就像讀管道一樣,如果沒(méi)有數(shù)據(jù)到達(dá)就阻塞等待,這時(shí)客戶端調(diào)用write()發(fā)送請(qǐng)求給服務(wù)器,服務(wù)器收到后從read()返回,對(duì)客戶端的請(qǐng)求進(jìn)行處理,在此期間客戶端調(diào)用read()阻塞等待服務(wù)器的應(yīng)答,服務(wù)器調(diào)用write()將處理結(jié)果發(fā)回給客戶端,再次調(diào)用read()阻塞等待下一條請(qǐng)求,客戶端收到后從read()返回,發(fā)送下一條請(qǐng)求,如此循環(huán)下去。

如果客戶端沒(méi)有更多的請(qǐng)求了,就調(diào)用close()關(guān)閉連接,就像寫(xiě)端關(guān)閉的管道一樣,服務(wù)器的read()返回0,這樣服務(wù)器就知道客戶端關(guān)閉了連接,也調(diào)用close()關(guān)閉連接。注意,任何一方調(diào)用close()后,連接的兩個(gè)傳輸方向都關(guān)閉,不能再發(fā)送數(shù)據(jù)了。如果一方調(diào)用shutdown()則連接處于半關(guān)閉狀態(tài),仍可接收對(duì)方發(fā)來(lái)的數(shù)據(jù)。

在學(xué)習(xí)socket API時(shí)要注意應(yīng)用程序和TCP協(xié)議層是如何交互的: *應(yīng)用程序調(diào)用某個(gè)socket函數(shù)時(shí)TCP協(xié)議層完成什么動(dòng)作,比如調(diào)用connect()會(huì)發(fā)出SYN段 *應(yīng)用程序如何知道TCP協(xié)議層的狀態(tài)變化,比如從某個(gè)阻塞的socket函數(shù)返回就表明TCP協(xié)議收到了某些段,再比如read()返回0就表明收到了FIN段

2.1.?最簡(jiǎn)單的TCP網(wǎng)絡(luò)程序?請(qǐng)點(diǎn)評(píng)

下面通過(guò)最簡(jiǎn)單的客戶端/服務(wù)器程序的實(shí)例來(lái)學(xué)習(xí)socket API。

server.c的作用是從客戶端讀字符,然后將每個(gè)字符轉(zhuǎn)換為大寫(xiě)并回送給客戶端。

/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h>#define MAXLINE 80 #define SERV_PORT 8000int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;listenfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));listen(listenfd, 20);printf("Accepting connections ...\n");while (1) {cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);write(connfd, buf, n);close(connfd);} }

下面介紹程序中用到的socket API,這些函數(shù)都在sys/socket.h中。

int socket(int family, int type, int protocol);

socket()打開(kāi)一個(gè)網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個(gè)文件描述符,應(yīng)用程序可以像讀寫(xiě)文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()調(diào)用出錯(cuò)則返回-1。對(duì)于IPv4,family參數(shù)指定為AF_INET。對(duì)于TCP協(xié)議,type參數(shù)指定為SOCK_STREAM,表示面向流的傳輸協(xié)議。如果是UDP協(xié)議,則type參數(shù)指定為SOCK_DGRAM,表示面向數(shù)據(jù)報(bào)的傳輸協(xié)議。protocol參數(shù)的介紹從略,指定為0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

服務(wù)器程序所監(jiān)聽(tīng)的網(wǎng)絡(luò)地址和端口號(hào)通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號(hào)后就可以向服務(wù)器發(fā)起連接,因此服務(wù)器需要調(diào)用bind綁定一個(gè)固定的網(wǎng)絡(luò)地址和端口號(hào)。bind()成功返回0,失敗返回-1。

bind()的作用是將參數(shù)sockfd和myaddr綁定在一起,使sockfd這個(gè)用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽(tīng)myaddr所描述的地址和端口號(hào)。前面講過(guò),struct sockaddr *是一個(gè)通用指針類型,myaddr參數(shù)實(shí)際上可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,而它們的長(zhǎng)度各不相同,所以需要第三個(gè)參數(shù)addrlen指定結(jié)構(gòu)體的長(zhǎng)度。我們的程序中對(duì)myaddr參數(shù)是這樣初始化的:

bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT);

首先將整個(gè)結(jié)構(gòu)體清零,然后設(shè)置地址類型為AF_INET,網(wǎng)絡(luò)地址為INADDR_ANY,這個(gè)宏表示本地的任意IP地址,因?yàn)榉?wù)器可能有多個(gè)網(wǎng)卡,每個(gè)網(wǎng)卡也可能綁定多個(gè)IP地址,這樣設(shè)置可以在所有的IP地址上監(jiān)聽(tīng),直到與某個(gè)客戶端建立了連接時(shí)才確定下來(lái)到底用哪個(gè)IP地址,端口號(hào)為SERV_PORT,我們定義為8000。

int listen(int sockfd, int backlog);

典型的服務(wù)器程序可以同時(shí)服務(wù)于多個(gè)客戶端,當(dāng)有客戶端發(fā)起連接時(shí),服務(wù)器調(diào)用的accept()返回并接受這個(gè)連接,如果有大量的客戶端發(fā)起連接而服務(wù)器來(lái)不及處理,尚未accept的客戶端就處于連接等待狀態(tài),listen()聲明sockfd處于監(jiān)聽(tīng)狀態(tài),并且最多允許有backlog個(gè)客戶端處于連接待狀態(tài),如果接收到更多的連接請(qǐng)求就忽略。listen()成功返回0,失敗返回-1。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握手完成后,服務(wù)器調(diào)用accept()接受連接,如果服務(wù)器調(diào)用accept()時(shí)還沒(méi)有客戶端的連接請(qǐng)求,就阻塞等待直到有客戶端連接上來(lái)。cliaddr是一個(gè)傳出參數(shù),accept()返回時(shí)傳出客戶端的地址和端口號(hào)。addrlen參數(shù)是一個(gè)傳入傳出參數(shù)(value-result argument),傳入的是調(diào)用者提供的緩沖區(qū)cliaddr的長(zhǎng)度以避免緩沖區(qū)溢出問(wèn)題,傳出的是客戶端地址結(jié)構(gòu)體的實(shí)際長(zhǎng)度(有可能沒(méi)有占滿調(diào)用者提供的緩沖區(qū))。如果給cliaddr參數(shù)傳NULL,表示不關(guān)心客戶端的地址。

我們的服務(wù)器程序結(jié)構(gòu)是這樣的:

while (1) {cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);...close(connfd); }

整個(gè)是一個(gè)while死循環(huán),每次循環(huán)處理一個(gè)客戶端連接。由于cliaddr_len是傳入傳出參數(shù),每次調(diào)用accept()之前應(yīng)該重新賦初值。accept()的參數(shù)listenfd是先前的監(jiān)聽(tīng)文件描述符,而accept()的返回值是另外一個(gè)文件描述符connfd,之后與客戶端之間就通過(guò)這個(gè)connfd通訊,最后關(guān)閉connfd斷開(kāi)連接,而不關(guān)閉listenfd,再次回到循環(huán)開(kāi)頭listenfd仍然用作accept的參數(shù)。accept()成功返回一個(gè)文件描述符,出錯(cuò)返回-1。

client.c的作用是從命令行參數(shù)中獲得一個(gè)字符串發(fā)給服務(wù)器,然后接收服務(wù)器返回的字符串并打印。

/* client.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h>#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;char *str;if (argc != 2) {fputs("usage: ./client message\n", stderr);exit(1);}str = argv[1];sockfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));write(sockfd, str, strlen(str));n = read(sockfd, buf, MAXLINE);printf("Response from server:\n");write(STDOUT_FILENO, buf, n);close(sockfd);return 0; }

由于客戶端不需要固定的端口號(hào),因此不必調(diào)用bind(),客戶端的端口號(hào)由內(nèi)核自動(dòng)分配。注意,客戶端不是不允許調(diào)用bind(),只是沒(méi)有必要調(diào)用bind()固定一個(gè)端口號(hào),服務(wù)器也不是必須調(diào)用bind(),但如果服務(wù)器不調(diào)用bind(),內(nèi)核會(huì)自動(dòng)給服務(wù)器分配監(jiān)聽(tīng)端口,每次啟動(dòng)服務(wù)器時(shí)端口號(hào)都不一樣,客戶端要連接服務(wù)器就會(huì)遇到麻煩。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客戶端需要調(diào)用connect()連接服務(wù)器,connect和bind的參數(shù)形式一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對(duì)方的地址。connect()成功返回0,出錯(cuò)返回-1。

先編譯運(yùn)行服務(wù)器:

$ ./serverAccepting connections ...

然后在另一個(gè)終端里用netstat命令查看:

$ netstat -apn|grep 8000tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8148/server

可以看到server程序監(jiān)聽(tīng)8000端口,IP地址還沒(méi)確定下來(lái)。現(xiàn)在編譯運(yùn)行客戶端:

$ ./client abcd Response from server: ABCD

回到server所在的終端,看看server的輸出:

$ ./serverAccepting connections ...received from 127.0.0.1 at PORT 59757

可見(jiàn)客戶端的端口號(hào)是自動(dòng)分配的。現(xiàn)在把客戶端所連接的服務(wù)器IP改為其它主機(jī)的IP,試試兩臺(tái)主機(jī)的通訊。

再做一個(gè)小實(shí)驗(yàn),在客戶端的connect()代碼之后插一個(gè)while(1);死循環(huán),使客戶端和服務(wù)器都處于連接中的狀態(tài),用netstat命令查看:

$ ./server & [1] 8343 $ Accepting connections ... ./client abcd & [2] 8344 $ netstat -apn|grep 8000 tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8343/server tcp 0 0 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED8344/client tcp 0 0 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED8343/server

應(yīng)用程序中的一個(gè)socket文件描述符對(duì)應(yīng)一個(gè)socket pair,也就是源地址:源端口號(hào)和目的地址:目的端口號(hào),也對(duì)應(yīng)一個(gè)TCP連接。

表?37.1.?client和server的socket狀態(tài)

socket文件描述符 源地址:源端口號(hào) 目的地址:目的端口號(hào) 狀態(tài)
server.c中的listenfd 0.0.0.0:8000 0.0.0.0:* LISTEN
server.c中的connfd 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED
client.c中的sockfd 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED

2.2.?錯(cuò)誤處理與讀寫(xiě)控制?請(qǐng)點(diǎn)評(píng)

上面的例子不僅功能簡(jiǎn)單,而且簡(jiǎn)單到幾乎沒(méi)有什么錯(cuò)誤處理,我們知道,系統(tǒng)調(diào)用不能保證每次都成功,必須進(jìn)行出錯(cuò)處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。

為使錯(cuò)誤處理的代碼不影響主程序的可讀性,我們把與socket相關(guān)的一些系統(tǒng)函數(shù)加上錯(cuò)誤處理代碼包裝成新的函數(shù),做成一個(gè)模塊wrap.c:

#include <stdlib.h> #include <errno.h> #include <sys/socket.h>void perr_exit(const char *s) {perror(s);exit(1); }int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {int n;again:if ( (n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))goto again;elseperr_exit("accept error");}return n; }void Bind(int fd, const struct sockaddr *sa, socklen_t salen) {if (bind(fd, sa, salen) < 0)perr_exit("bind error"); }void Connect(int fd, const struct sockaddr *sa, socklen_t salen) {if (connect(fd, sa, salen) < 0)perr_exit("connect error"); }void Listen(int fd, int backlog) {if (listen(fd, backlog) < 0)perr_exit("listen error"); }int Socket(int family, int type, int protocol) {int n;if ( (n = socket(family, type, protocol)) < 0)perr_exit("socket error");return n; }ssize_t Read(int fd, void *ptr, size_t nbytes) {ssize_t n;again:if ( (n = read(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n; }ssize_t Write(int fd, const void *ptr, size_t nbytes) {ssize_t n;again:if ( (n = write(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n; }void Close(int fd) {if (close(fd) == -1)perr_exit("close error"); }

慢系統(tǒng)調(diào)用accept、read和write被信號(hào)中斷時(shí)應(yīng)該重試。connect雖然也會(huì)阻塞,但是被信號(hào)中斷時(shí)不能立刻重試。對(duì)于accept,如果errno是ECONNABORTED,也應(yīng)該重試。詳細(xì)解釋見(jiàn)參考資料。

TCP協(xié)議是面向流的,read和write調(diào)用的返回值往往小于參數(shù)指定的字節(jié)數(shù)。對(duì)于read調(diào)用,如果接收緩沖區(qū)中有20字節(jié),請(qǐng)求讀100個(gè)字節(jié),就會(huì)返回20。對(duì)于write調(diào)用,如果請(qǐng)求寫(xiě)100個(gè)字節(jié),而發(fā)送緩沖區(qū)中只有20個(gè)字節(jié)的空閑位置,那么write會(huì)阻塞,直到把100個(gè)字節(jié)全部交給發(fā)送緩沖區(qū)才返回,但如果socket文件描述符有O_NONBLOCK標(biāo)志,則write不阻塞,直接返回20。為避免這些情況干擾主程序的邏輯,確保讀寫(xiě)我們所請(qǐng)求的字節(jié)數(shù),我們實(shí)現(xiàn)了兩個(gè)包裝函數(shù)readn和writen,也放在wrap.c中:

ssize_t Readn(int fd, void *vptr, size_t n) {size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0;elsereturn -1;} else if (nread == 0)break;nleft -= nread;ptr += nread;}return n - nleft; }ssize_t Writen(int fd, const void *vptr, size_t n) {size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n; }

如果應(yīng)用層協(xié)議的各字段長(zhǎng)度固定,用readn來(lái)讀是非常方便的。例如設(shè)計(jì)一種客戶端上傳文件的協(xié)議,規(guī)定前12字節(jié)表示文件名,超過(guò)12字節(jié)的文件名截?cái)?#xff0c;不足12字節(jié)的文件名用'\0'補(bǔ)齊,從第13字節(jié)開(kāi)始是文件內(nèi)容,上傳完所有文件內(nèi)容后關(guān)閉連接,服務(wù)器可以先調(diào)用readn讀12個(gè)字節(jié),根據(jù)文件名創(chuàng)建文件,然后在一個(gè)循環(huán)中調(diào)用read讀文件內(nèi)容并存盤(pán),循環(huán)結(jié)束的條件是read返回0。

字段長(zhǎng)度固定的協(xié)議往往不夠靈活,難以適應(yīng)新的變化。比如,以前DOS的文件名是8字節(jié)主文件名加“.”加3字節(jié)擴(kuò)展名,不超過(guò)12字節(jié),但是現(xiàn)代操作系統(tǒng)的文件名可以長(zhǎng)得多,12字節(jié)就不夠用了。那么制定一個(gè)新版本的協(xié)議規(guī)定文件名字段為256字節(jié)怎么樣?這樣又造成很大的浪費(fèi),因?yàn)榇蠖鄶?shù)文件名都很短,需要用大量的'\0'補(bǔ)齊256字節(jié),而且新版本的協(xié)議和老版本的程序無(wú)法兼容,如果已經(jīng)有很多人在用老版本的程序了,會(huì)造成遵循新協(xié)議的程序與老版本程序的互操作性(Interoperability)問(wèn)題。如果新版本的協(xié)議要添加新的字段,比如規(guī)定前12字節(jié)是文件名,從13到16字節(jié)是文件類型說(shuō)明,從第17字節(jié)開(kāi)始才是文件內(nèi)容,同樣會(huì)造成和老版本的程序無(wú)法兼容的問(wèn)題。

現(xiàn)在重新看看上一節(jié)的TFTP協(xié)議是如何避免上述問(wèn)題的:TFTP協(xié)議的各字段是可變長(zhǎng)的,以'\0'為分隔符,文件名可以任意長(zhǎng),再看blksize等幾個(gè)選項(xiàng)字段,TFTP協(xié)議并沒(méi)有規(guī)定從第m字節(jié)到第n字節(jié)是blksize的值,而是把選項(xiàng)的描述信息“blksize”與它的值“512”一起做成一個(gè)可變長(zhǎng)的字段,這樣,以后添加新的選項(xiàng)仍然可以和老版本的程序兼容(老版本的程序只要忽略不認(rèn)識(shí)的選項(xiàng)就行了)。

因此,常見(jiàn)的應(yīng)用層協(xié)議都是帶有可變長(zhǎng)字段的,字段之間的分隔符用換行的比用'\0'的更常見(jiàn),例如本節(jié)后面要介紹的HTTP協(xié)議。可變長(zhǎng)字段的協(xié)議用readn來(lái)讀就很不方便了,為此我們實(shí)現(xiàn)一個(gè)類似于fgets的readline函數(shù),也放在wrap.c中:

static ssize_t my_read(int fd, char *ptr) {static int read_cnt;static char *read_ptr;static char read_buf[100];if (read_cnt <= 0) {again:if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {if (errno == EINTR)goto again;return -1;} else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1; }ssize_t Readline(int fd, void *vptr, size_t maxlen) {ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = my_read(fd, &c)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {*ptr = 0;return n - 1;} elsereturn -1;}*ptr = 0;return n; }

習(xí)題?請(qǐng)點(diǎn)評(píng)

1、請(qǐng)讀者自己寫(xiě)出wrap.c的頭文件wrap.h,后面的網(wǎng)絡(luò)程序代碼都要用到這個(gè)頭文件。

2、修改server.c和client.c,添加錯(cuò)誤處理。

2.3.?把client改為交互式輸入?請(qǐng)點(diǎn)評(píng)

目前實(shí)現(xiàn)的client每次運(yùn)行只能從命令行讀取一個(gè)字符串發(fā)給服務(wù)器,再?gòu)姆?wù)器收回來(lái),現(xiàn)在我們把它改成交互式的,不斷從終端接受用戶輸入并和server交互。

/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;sockfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));while (fgets(buf, MAXLINE, stdin) != NULL) {Write(sockfd, buf, strlen(buf));n = Read(sockfd, buf, MAXLINE);if (n == 0)printf("the other side has been closed.\n");elseWrite(STDOUT_FILENO, buf, n);}Close(sockfd);return 0; }

編譯并運(yùn)行server和client,看看是否達(dá)到了你預(yù)想的結(jié)果。

$ ./client haha1 HAHA1 haha2 the other side has been closed. haha3 $

這時(shí)server仍在運(yùn)行,但是client的運(yùn)行結(jié)果并不正確。原因是什么呢?仔細(xì)查看server.c可以發(fā)現(xiàn),server對(duì)每個(gè)請(qǐng)求只處理一次,應(yīng)答后就關(guān)閉連接,client不能繼續(xù)使用這個(gè)連接發(fā)送數(shù)據(jù)。但是client下次循環(huán)時(shí)又調(diào)用write發(fā)數(shù)據(jù)給server,write調(diào)用只負(fù)責(zé)把數(shù)據(jù)交給TCP發(fā)送緩沖區(qū)就可以成功返回了,所以不會(huì)出錯(cuò),而server收到數(shù)據(jù)后應(yīng)答一個(gè)RST段,client收到RST段后無(wú)法立刻通知應(yīng)用層,只把這個(gè)狀態(tài)保存在TCP協(xié)議層。client下次循環(huán)又調(diào)用write發(fā)數(shù)據(jù)給server,由于TCP協(xié)議層已經(jīng)處于RST狀態(tài)了,因此不會(huì)將數(shù)據(jù)發(fā)出,而是發(fā)一個(gè)SIGPIPE信號(hào)給應(yīng)用層,SIGPIPE信號(hào)的缺省處理動(dòng)作是終止程序,所以看到上面的現(xiàn)象。

為了避免client異常退出,上面的代碼應(yīng)該在判斷對(duì)方關(guān)閉了連接后break出循環(huán),而不是繼續(xù)write。另外,有時(shí)候代碼中需要連續(xù)多次調(diào)用write,可能還來(lái)不及調(diào)用read得知對(duì)方已關(guān)閉了連接就被SIGPIPE信號(hào)終止掉了,這就需要在初始化時(shí)調(diào)用sigaction處理SIGPIPE信號(hào),如果SIGPIPE信號(hào)沒(méi)有導(dǎo)致進(jìn)程異常退出,write返回-1并且errno為EPIPE。

另外,我們需要修改server,使它可以多次處理同一客戶端的請(qǐng)求。

/* server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, 20);printf("Accepting connections ...\n");while (1) {cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);while (1) {n = Read(connfd, buf, MAXLINE);if (n == 0) {printf("the other side has been closed.\n");break;}printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);Write(connfd, buf, n);}Close(connfd);} }

經(jīng)過(guò)上面的修改后,客戶端和服務(wù)器可以進(jìn)行多次交互了。我們知道,服務(wù)器通常是要同時(shí)服務(wù)多個(gè)客戶端的,運(yùn)行上面的server和client之后,再開(kāi)一個(gè)終端運(yùn)行client試試,新的client能得到服務(wù)嗎?想想為什么。

2.4.?使用fork并發(fā)處理多個(gè)client的請(qǐng)求?請(qǐng)點(diǎn)評(píng)

怎么解決這個(gè)問(wèn)題?網(wǎng)絡(luò)服務(wù)器通常用fork來(lái)同時(shí)服務(wù)多個(gè)客戶端,父進(jìn)程專門(mén)負(fù)責(zé)監(jiān)聽(tīng)端口,每次accept一個(gè)新的客戶端連接就fork出一個(gè)子進(jìn)程專門(mén)服務(wù)這個(gè)客戶端。但是子進(jìn)程退出時(shí)會(huì)產(chǎn)生僵尸進(jìn)程,父進(jìn)程要注意處理SIGCHLD信號(hào)和調(diào)用wait清理僵尸進(jìn)程。

以下給出代碼框架,完整的代碼請(qǐng)讀者自己完成。

listenfd = socket(...); bind(listenfd, ...); listen(listenfd, ...); while (1) {connfd = accept(listenfd, ...);n = fork();if (n == -1) {perror("call to fork");exit(1);} else if (n == 0) {close(listenfd);while (1) {read(connfd, ...);...write(connfd, ...);}close(connfd);exit(0);} elseclose(connfd); }

2.5.?setsockopt?請(qǐng)點(diǎn)評(píng)

現(xiàn)在做一個(gè)測(cè)試,首先啟動(dòng)server,然后啟動(dòng)client,然后用Ctrl-C使server終止,這時(shí)馬上再運(yùn)行server,結(jié)果是:

$ ./serverbind error: Address already in use

這是因?yàn)?#xff0c;雖然server的應(yīng)用程序終止了,但TCP協(xié)議層的連接并沒(méi)有完全斷開(kāi),因此不能再次監(jiān)聽(tīng)同樣的server端口。我們用netstat命令查看一下:

$ netstat -apn |grep 8000tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -

server終止時(shí),socket描述符會(huì)自動(dòng)關(guān)閉并發(fā)FIN段給client,client收到FIN后處于CLOSE_WAIT狀態(tài),但是client并沒(méi)有終止,也沒(méi)有關(guān)閉socket描述符,因此不會(huì)發(fā)FIN給server,因此server的TCP連接處于FIN_WAIT2狀態(tài)。

現(xiàn)在用Ctrl-C把client也終止掉,再觀察現(xiàn)象:

$ netstat -apn |grep 8000tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -$ ./serverbind error: Address already in use

client終止時(shí)自動(dòng)關(guān)閉socket描述符,server的TCP連接收到client發(fā)的FIN段后處于TIME_WAIT狀態(tài)。TCP協(xié)議規(guī)定,主動(dòng)關(guān)閉連接的一方要處于TIME_WAIT狀態(tài),等待兩個(gè)MSL(maximum segment lifetime)的時(shí)間后才能回到CLOSED狀態(tài),因?yàn)槲覀兿菴trl-C終止了server,所以server是主動(dòng)關(guān)閉連接的一方,在TIME_WAIT期間仍然不能再次監(jiān)聽(tīng)同樣的server端口。MSL在RFC1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實(shí)現(xiàn)不同,在Linux上一般經(jīng)過(guò)半分鐘后就可以再次啟動(dòng)server了。至于為什么要規(guī)定TIME_WAIT的時(shí)間請(qǐng)讀者參考UNP 2.7節(jié)。

在server的TCP連接沒(méi)有完全斷開(kāi)之前不允許重新監(jiān)聽(tīng)是不合理的,因?yàn)?#xff0c;TCP連接沒(méi)有完全斷開(kāi)指的是connfd(127.0.0.1:8000)沒(méi)有完全斷開(kāi),而我們重新監(jiān)聽(tīng)的是listenfd(0.0.0.0:8000),雖然是占用同一個(gè)端口,但I(xiàn)P地址不同,connfd對(duì)應(yīng)的是與某個(gè)客戶端通訊的一個(gè)具體的IP地址,而listenfd對(duì)應(yīng)的是wildcard address。解決這個(gè)問(wèn)題的方法是使用setsockopt()設(shè)置socket描述符的選項(xiàng)SO_REUSEADDR為1,表示允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符。在server代碼的socket()和bind()調(diào)用之間插入如下代碼:

int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

有關(guān)setsockopt可以設(shè)置的其它選項(xiàng)請(qǐng)參考UNP第7章。

2.6.?使用select?請(qǐng)點(diǎn)評(píng)

select是網(wǎng)絡(luò)程序中很常用的一個(gè)系統(tǒng)調(diào)用,它可以同時(shí)監(jiān)聽(tīng)多個(gè)阻塞的文件描述符(例如多個(gè)網(wǎng)絡(luò)連接),哪個(gè)有數(shù)據(jù)到達(dá)就處理哪個(gè),這樣,不需要fork和多進(jìn)程就可以實(shí)現(xiàn)并發(fā)服務(wù)的server。

/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char **argv) {int i, maxi, maxfd, listenfd, connfd, sockfd;int nready, client[FD_SETSIZE];ssize_t n;fd_set rset, allset;char buf[MAXLINE];char str[INET_ADDRSTRLEN];socklen_t cliaddr_len;struct sockaddr_in cliaddr, servaddr;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, 20);maxfd = listenfd; /* initialize */maxi = -1; /* index into client[] array */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1; /* -1 indicates available entry */FD_ZERO(&allset);FD_SET(listenfd, &allset);for ( ; ; ) {rset = allset; /* structure assignment */nready = select(maxfd+1, &rset, NULL, NULL, NULL);if (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) { /* new client connection */cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {client[i] = connfd; /* save descriptor */break;}if (i == FD_SETSIZE) {fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset); /* add new descriptor to set */if (connfd > maxfd)maxfd = connfd; /* for select */if (i > maxi)maxi = i; /* max index in client[] array */if (--nready == 0)continue; /* no more readable descriptors */}for (i = 0; i <= maxi; i++) { /* check all clients for data */if ( (sockfd = client[i]) < 0)continue;if (FD_ISSET(sockfd, &rset)) {if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {/* connection closed by client */Close(sockfd);FD_CLR(sockfd, &allset);client[i] = -1;} else {int j;for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);}if (--nready == 0)break; /* no more readable descriptors */}}} }

總結(jié)

以上是生活随笔為你收集整理的基于TCP协议的网络程序(基础学习)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。