下面通過最簡單的客戶端/服務器程序的實例來學習socket API。
serv.c 程序的功能是從客戶端讀取字符然後直接回射回去:
#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)); struct sockaddr_in localaddr; char serv_ip[20]; socklen_t local_len = sizeof(localaddr); memset(&localaddr, 0, sizeof(localaddr)); if( getsockname(conn,(struct sockaddr *)&localaddr,&local_len) != 0 ) ERR_EXIT("getsockname error"); inet_ntop(AF_INET, &localaddr.sin_addr, serv_ip, sizeof(serv_ip)); printf("host %s:%d\n", serv_ip, ntohs(localaddr.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; }
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
cli.c 的作用是從標准輸入得到一行字符,然後發送給服務器後從服務器接收,再打印在標准輸出:
#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"); struct sockaddr_in localaddr; char cli_ip[20]; socklen_t local_len = sizeof(localaddr); memset(&localaddr, 0, sizeof(localaddr)); if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 ) ERR_EXIT("getsockname error"); inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip)); printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port)); 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(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。
查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/
先編譯運行服務器:
huangcheng@ubuntu:~$./serv
然後在另一個終端裡用netstat命令查看:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並非所有進程都能被檢測到,所有非本用戶的進程信息將不會顯示,如果想看到所有信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2998/serv
可以看到server程序監聽5188端口,IP地址還沒確定下來。現在編譯運行客戶端:
huangcheng@ubuntu:~$ ./cli
回到server所在的終端,看看server的輸出:
huangcheng@ubuntu:~$ ./serv recv connect ip=127.0.0.1 port=42107
可見客戶端的端口號是自動分配的。再次netstat 一下:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並非所有進程都能被檢測到,所有非本用戶的進程信息將不會顯示,如果想看到所有信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2998/serv tcp 0 0 127.0.0.1:5188 127.0.0.1:42107 ESTABLISHED 2998/serv tcp 0 0 127.0.0.1:42107 127.0.0.1:5188 ESTABLISHED 3198/cli
應用程序中的一個socket文件描述符對應一個socket pair,也就是源地址:源端口號和目的地址:目的端口號,也對應一個TCP連接。
上面第一行即serv.c 中的listenfd;第二行即serv.c 中的sock; 第三行即cli 中的conn。2998和3198分別是進程id。
現在來做個測試,先serv.c中的把33~35行的代碼注釋掉。
首先啟動server,然後啟動client,然後用Ctrl-C使server終止,這時馬上再運行server,結果是:
huangcheng@ubuntu:~$ ./serv bind error: Address already in use
這是因為,雖然server的應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的server端口。我們用netstat命令查看一下:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並非所有進程都能被檢測到,所有非本用戶的進程信息將不會顯示,如果想看到所有信息,則必須切換 到 root 用戶) tcp 0 0 127.0.0.1:5188 127.0.0.1:42108 FIN_WAIT2 - tcp 1 0 127.0.0.1:42108 127.0.0.1:5188 CLOSE_WAIT 3260/cli
server終止時,socket描述符會自動關閉並發FIN段給client,client收到FIN後處於CLOSE_WAIT狀態,但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的TCP連接處於FIN_WAIT2狀態。
現在用Ctrl-C把client也終止掉,再觀察現象:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並非所有進程都能被檢測到,所有非本用戶的進程信息將不會顯示,如果想看到所有信息,則必須切換到 root 用戶) tcp 0 0 127.0.0.1:5188 127.0.0.1:42108 TIME_WAIT -
huangcheng@ubuntu:~$ ./serv bind error: Address already in use
client終止時自動關閉socket描述符,server的TCP連接收到client發的FIN段後處於TIME_WAIT狀態。TCP協議規定,主動關閉連接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximumsegment lifetime)的時間後才能回到CLOSED狀態,需要有MSL 時間的主要原因是在這段時間內如果最後一個ack段沒有發送給對方,則可以重新發送。因為我們先Ctrl-C終止了server,所以server是主動關閉連接的一方,在TIME_WAIT期間仍然不能再次監聽同樣的server端口。MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同,在Linux上一般經過半分鐘後就可以再次啟動server了。至於為什麼要規定TIME_WAIT的時間請大家參考UNP 2.7節。
在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描述符。將原來注釋的33~35行代碼打開,問題解決。
先運行服務器,在運行客戶端:
huangcheng@ubuntu:~$ ./serv recv connect ip=127.0.0.1 port=42107 host 127.0.0.1:5188 huangcheng ctt
huangcheng@ubuntu:~$ ./cli host 127.0.0.1:42107 huangcheng huangcheng ctt ctt