最簡單的回射客戶/服務器程序、time_wait 狀態
下面通過最簡單的客戶端/服務器程序的實例來學習socket API。
echoser.c 程序的功能是從客戶端讀取字符然後直接回射回去。
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
/************************************************************************* > File Name: echoser.c > Author: Simba > Mail: [email protected] > Created Time: Fri 01 Mar 2013 06:15:27 PM CST ************************************************************************/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { int listenfd; //被動套接字(文件描述符),即只可以accept if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */ /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt error"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) //listen應在socket和bind之後,而在accept之前 ERR_EXIT("listen error"); struct sockaddr_in peeraddr; //傳出參數 socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值 int conn; // 已連接套接字(變為主動套接字,即可以主動connect) if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) ERR_EXIT("accept error"); printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = read(conn, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); write(conn, recvbuf, ret); } close(conn); close(listenfd); return 0; } |
下面介紹程序中用到的socket API,這些函數都在sys/socket.h中。
int socket(int family, int type, int protocol);
socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,family參數指定為AF_INET。對於TCP協議,type參數指定為SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定為SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的介紹從略,指定為0即可。
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。bind()成功返回0,失敗返回-1。
bind()的作用是將參數sockfd和myaddr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽myaddr所描述的地址和端口號。struct
sockaddr
*是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。我們的程序中對myaddr參數是這樣初始化的:
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
首先將整個結構體清零(也可以用bzero函數),然後設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為5188。
int listen(int sockfd, int backlog);
典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的accept()返回並接受這個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未accept的客戶端就處於連接等待狀態,listen()聲明sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接等待狀態,如果接收到更多的連接請求就忽略。listen()成功返回0,失敗返回-1。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成後,服務器調用accept()接受連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。cliaddr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result
argument),傳入的是調用者提供的緩沖區cliaddr的長度以避免緩沖區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給cliaddr和addrlen參
數傳NULL,表示不關心客戶端的地址。
在上面的程序中我們通過peeraddr打印連接上來的客戶端ip和端口號。
在while循環中從accept返回的文件描述符conn讀取客戶端的請求,然後直接回射回去。
echocli.c 的作用是從標準輸入得到一行字符,然後發送給服務器後從服務器接收,再打印在標準輸出。
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
/************************************************************************* > File Name: echoclic > Author: Simba > Mail: [email protected] > Created Time: Fri 01 Mar 2013 06:15:27 PM CST ************************************************************************/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect error"); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(sock, sendbuf, strlen(sendbuf)); read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sock); return 0; } |
由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。註意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
客戶端需要調用connect()連接服務器,connect和bind的參數形式一致,區別在於bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返回0,出錯返回-1。
先編譯運行服務器:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser
然後在另一個終端裏用netstat命令查看:
simba@ubuntu:~$ netstat -anp | grep 5188
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 4425/echoser
可以看到server程序監聽5188端口,IP地址還沒確定下來。現在編譯運行客戶端:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli
回到server所在的終端,看看server的輸出:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser
recv connect ip=127.0.0.1 port=59431
可見客戶端的端口號是自動分配的。
再次netstat 一下
simba@ubuntu:~$ netstat -anp | grep 5188
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 4425/echoser
tcp 0 0 127.0.0.1:59431 127.0.0.1:5188 ESTABLISHED 4852/echocli
tcp 0 0 127.0.0.1:5188 127.0.0.1:59431 ESTABLISHED 4425/echoser
應用程序中的一個socket文件描述符對應一個socket pair,也就是源地址:源端口號和目的地址:目的端口號,也對應一個TCP連接。
上面第一行即echoser.c 中的listenfd;第二行即echocli 中的sock; 第三行即echoser.c 中的conn。4425和4852分別是進程id。
現在來做個測試,先把echoser.c 中40~42行的代碼註釋起來。
首先啟動server,然後啟動client,然後用Ctrl-C使server終止,這時馬上再運行server,結果是:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser
bind error: Address already in use
這是因為,雖然server的應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的server端口。我們用netstat命令查看一下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:5188 127.0.0.1:37381 FIN_WAIT2 -
tcp 1 0 127.0.0.1:37381 127.0.0.1:5188 CLOSE_WAIT 2302/echocli
server終止時,socket描述符會自動關閉並發FIN段給client,client收到FIN後處於CLOSE_WAIT狀態,但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的TCP連接處於FIN_WAIT2狀態。
Many implementations prevent this infinite wait in
the FIN_WAIT_2 state as follows: If the application that does the
active close does a complete close, not a half-close indicating that it
expects
to receive data, a timer is set. If the connection is idle when the
timer expires, TCP moves the connection into the CLOSED state. In Linux,
the variable net.ipv4.tcp_fin_timeout can be adjusted to control the number of seconds
to which the timer is set. Its default value is 60s.
現在用Ctrl-C把client也終止掉,再觀察現象:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188
(No info could be read for "-p": geteuid()=1000 but you should be root.)
tcp 0 0 127.0.0.1:5188 127.0.0.1:37381 TIME_WAIT -
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser
bind error: Address already in use
client終止時自動關閉socket描述符,server的TCP連接收到client發的FIN段後處於TIME_WAIT狀態。TCP協議規定,主動關閉連接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximum
segment lifetime)的時間後才能回到CLOSED狀態,需要有MSL
時間的主要原因是在這段時間內如果最後一個ack段沒有發送給對方,則可以重新發送(in which
case the other end will time out and retransmit its final FIN)。因為我們先Ctrl-C終止了server,所以server是主動關閉連接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同,在Linux上一般經過半分鐘後就可以再次啟動server了。
MSL: the maximum amount of time any
segment can exist in the network before being discarded. We know that
this time limit is bounded, because TCP segments are transmitted as IP
datagrams, and the IP datagram has the TTL field
or Hop Limit field that limits its effective lifetime.
Given the MSL value for an
implementation, the rule is: When TCP performs an active close and sends
the final ACK, that connection must stay in the TIME_WAIT state for
twice the MSL. This lets TCP resend the final ACK in case
it is lost. The final ACK is resent not because the TCP retransmits ACKs
(they do not consume sequence numbers and are not retransmitted by
TCP), but because the other side will retransmit its FIN (which does
consume a sequence
number). Indeed, TCP will always retransmit FINs until it receives a
final ACK.
在server的TCP連接沒有完全斷開之前不允許重新監聽是不合理的,因為,TCP連接沒有完全斷開指的是connfd(127.0.0.1:5188)沒有完全斷開,而我們重新監聽的是listenfd(0.0.0.0:5188),雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是wildcard
address(比如一臺機器可能有內網和外網兩張網卡)。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端口號相同但IP地址不同的多個socket描述符。將原來註釋的40~42行代碼打開,問題解決。
參考:
《Linux C 編程一站式學習》
《TCP/IP詳解 卷一》
《UNP》
http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html
http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html
最簡單的回射客戶/服務器程序、time_wait 狀態