窺探 socket 監聽的祕密

socket 用 listen 函數監聽,listen 從英語上理解就是一個 "聽" 函數,實際上它也就是這個意思。

我們來看 unix 網絡編程這本書是怎樣對它的解釋:listen 函數把一個未連接的套接字轉換成一個被動套接字,指示內核應該接受指向該套接字的鏈接請求。

該函數有 2 個參數,第一個我就不說了,第二參數規定了內核爲相應套接字排隊的最大連接個數。只看這些理論搞的人稀裏糊塗,我們還是來測一下。

[mapan@localhost test]$ ls
client.cpp  makefile  server.cpp
[mapan@localhost test]$ 
[mapan@localhost test]$ cat server.cpp 
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
void main()
{
   int listenfd,connfd;
   socklen_t  clilen;
   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=INADDR_ANY;
   servaddr.sin_port=htons(8888);
   bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));  
   listen(listenfd,1);
   getchar();
   connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);
   close(connfd);
   close(listenfd);
}
[mapan@localhost test]$ cat client.cpp 
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define MAXLINE 4096
void main()
{
   int sockfd;
   struct sockaddr_in servaddr;
   sockfd=socket(AF_INET,SOCK_STREAM,0);
   bzero(&servaddr,sizeof(servaddr));
   servaddr.sin_family=AF_INET;
   servaddr.sin_port=htons(8888);
   servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
   int ret=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
   getchar();
   close(sockfd);
}
[mapan@localhost test]$ cat makefile 
all:server client
server.o:server.cpp
  g++ -c server.cpp
client.o:client.cpp
  g++ -c client.cpp
server:server.o
  g++ -o server server.o
client:client.o
  g++ -o client client.o
clean:
  rm -f server client *.o
[mapan@localhost test]$

請注意上面的服務端中,我是沒有調用 accept 函數的,直接調用 getchar() 了, 跑起來。

[mapan@localhost test]$ make
g++ -c server.cpp
g++ -o server server.o
g++ -c client.cpp
g++ -o client client.o
[mapan@localhost test]$ ./server 
服務度開啓,然後新打開一個窗口開啓客戶端。
[mapan@localhost TCP]$ cd ../test/
[mapan@localhost test]$ 
[mapan@localhost test]$ ./client 127.0.0.1

查看網絡:

[mapan@localhost test]$ netstat -na | grep 8888
tcp        0      0 0.0.0.0:8888                0.0.0.0:*                   LISTEN      
tcp        0      0 127.0.0.1:34846             127.0.0.1:8888              ESTABLISHED 
tcp        0      0 127.0.0.1:8888              127.0.0.1:34846             ESTABLISHED 
[mapan@localhost test]$

看,已經建立起一個連接了。但是我們沒有調用 accept 函數,連接還是建立起來了,這說說明 accept 函數和 TCP 三次握手沒啥關係,這也是一個知識盲點。好,在開啓一個新窗口運行客戶端,查看網絡狀態。(新開窗口運行客戶端同上,這裏就不用代碼演示了)

[mapan@localhost test]$ netstat -na | grep 8888
tcp        0      0 0.0.0.0:8888                0.0.0.0:*                   LISTEN      
tcp        0      0 127.0.0.1:34846             127.0.0.1:8888              ESTABLISHED 
tcp        0      0 127.0.0.1:34848             127.0.0.1:8888              ESTABLISHED 
tcp        0      0 127.0.0.1:8888              127.0.0.1:34846             ESTABLISHED 
tcp        0      0 127.0.0.1:8888              127.0.0.1:34848             ESTABLISHED

看,又建立起一個連接。在運行一個客戶端,看網絡狀態。

[mapan@localhost test]$ netstat -na | grep 8888
tcp        0      0 0.0.0.0:8888                0.0.0.0:*                   LISTEN      
tcp        0      0 127.0.0.1:8888              127.0.0.1:34850             SYN_RECV    
tcp        0      0 127.0.0.1:34846             127.0.0.1:8888              ESTABLISHED 
tcp        0      0 127.0.0.1:34848             127.0.0.1:8888              ESTABLISHED 
tcp        0      0 127.0.0.1:8888              127.0.0.1:34846             ESTABLISHED 
tcp        0      0 127.0.0.1:8888              127.0.0.1:34848             ESTABLISHED 
tcp        0      0 127.0.0.1:34850             127.0.0.1:8888              ESTABLISHED

當第三個客戶端連接進來的時候,出現了一個 SYN_RECV,這標明第三個客戶端沒有與服務端建立連接。

我們 listen 函數設置的監聽隊列爲 1,那麼監聽隊列塞了 2 個之後就沒有往裏面塞了。這下大概懂了 listen 函數第二個參數的意義了吧,當參數爲 1 的時候只能監聽 2 個套接字,這應該是從 0 開始數的。

爲什麼是大概呢?其實 unix 網絡編程上是這樣說的:listen 函數的第二個參數是 ESTABLISHED 和 SYN_RECV 之和,只是在監聽隊列沒有滿的情況下,SYN_RECV 狀態不容易重現。這時候在服務度輸入一個字符會有啥效果呢?

答案告訴你,就是那個 SYN_RECV 狀態變成 ESTABLISHED 了,這也是 accept 函數的作用。accept 函數會將已完成連接隊列中的對頭項返回給進程,所以 SYN_RECV 變成 ESTABLISHED 了。這個現象留給大家去實踐一下吧,只有自己實踐出來的東西纔是自己的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MrTG7wpQbuzHXMAzNR8ggg