下面經過最簡單的客戶端/服務器程序的實例來學習socket API。ubuntu
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; }
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; }
先編譯運行服務器:
socket
huangcheng@ubuntu:~$./serv
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地址還沒肯定下來。如今編譯運行客戶端:
tcp
huangcheng@ubuntu:~$ ./cli
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。spa
如今來作個測試,先serv.c中的把33~35行的代碼註釋掉。
操作系統
首先啓動server,而後啓動client,而後用Ctrl-C使server終止,這時立刻再運行server,結果是:code
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/cliserver終止時,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