一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close

爬蜥發表於2018-07-30

一個TCP請求的基本過程是怎樣的?

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close

socket

用於指定通訊的協議型別,它的返回值為socket descriptor

函式定義為 int socket(int family,int type,int protocol),在 sys/socket.h中定義。

  • family:指定協議族,比如 AF_INET表示IPv4協議,AF_INET6表示IPv6協議
  • type:表明套接字型別,比如 SCOK_STREAM 表示位元組流套接字,SCOK_DGRAM表示資料包套接字
  • protocol:表示某個協議型別的常量值,一般為0,表示對所有family和type的用系統預設值。IPROTO_TCP表示TCP協議,IPROTO_UDP表示UDP協議

connect

客戶端用來建立與TCP伺服器的連線,它的呼叫將激發TCP的三路握手,即會使當前套接字從CLOSED狀態轉移到SYN_SENT狀態,若成功再轉移到ESTABLISHED狀態。只有連線建立或者出錯才會返回。

connect失敗則該套接字不可再用,必須關閉,想要重連線必須再呼叫socket

connect在那些情況下會出錯?

  1. 客戶端沒有收到SYN的響應,返回ETIMEDOUT錯誤。

對於4.4BSD核心傳送SYN,沒有響應再等6s傳送,無響應等24s,如果總共等了75s仍然沒有就返回ETIMEDOUT錯誤

  1. 客戶端收到SYN響應為RST,返回ECONNREFUESED錯誤。

這是種硬錯誤。收到RST可能是:沒有伺服器監聽連線的埠;TCP想取消連線;TCP收到一個根本不存在的連線上的分節

  1. 路由器引發了‘destination unreachable’ ICMP錯誤。

這是種軟錯誤

bind

將本地協議地址賦予一個套接字。

本地協議地址:比如 IPv4或IPv6地址與埠的組合

呼叫bind的埠和地址可以都指定或者都不指定,或者只指定一個。如果埠號不指定,核心會在bind被呼叫時選擇一個臨時的埠。

函式定義為 int bind(int sockfd,const struct *myaddr,socklen_t addrlen);第一個引數就是就是socket返回的套接字描述符,第二個引數是指向特定於協議的地址結構的指標,第三個是該地址結構的長度。由於地址結構是個常量,所以如果是核心指定埠,無法返回,所以要獲取核心指定的臨時埠,必須呼叫getsockname返回協議地址

listen

做兩件事

  1. 指示核心應該接受指向此套接字的連線請求,對應TCP狀態轉移為套接字從CLOSED狀態變成LISTEN狀態
  2. 規定核心應該為相應套接字排隊的最大連線個數

socket建立的套接字預設是用來主動發起請求的,即用來呼叫connect函式,listen則是將這個套接字變成被動套接字,用來接收請求

核心維護的監聽套接字佇列

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close

backlog的同一個取值根據作業系統不同,實際的數目會有差別

  1. 未完成佇列:由某個客戶端發出的SYN包到達了伺服器,而伺服器正在等待完成相應的TCP三次握手的過程;
  2. 已完成的佇列:每個已完成TCP三次握手的客戶端對應的其中一項

三次握手正常完成的這項會從未完成連線對列移到已完成佇列的隊尾。當程式呼叫accept時,已完成佇列的頭部將返回給程式,如果已完成佇列為空,程式將被投入睡眠,睡眠針對的是預設的阻塞模式,直到TCP在該佇列中放入一項才喚醒。

當客戶SYN到達時,如果佇列是滿的,TCP會忽略這個包,使得客戶端會重傳

accept

用於從已完成連線佇列隊頭返回下一個已完成連線。如果accept成功,返回值是有核心自動生成的一個全新的描述符,代表與客戶端建立的TCP連線。

一個伺服器通常只建立一個監聽套接字,他在這個服務的宣告週期內一直存在。但是會為每個客戶端的連線建立一個以連線套接字,對客戶端的服務完成時,就關閉這個連線套接字

accept生成新的描述符處理已連線的請求過程

首先處於監聽狀態的伺服器監聽客戶端發來的連線請求

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close
第二步accept返回結果,連線被核心接受,新的套接字(connfd)建立

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close
第三步併發伺服器會呼叫fork,此時listenfd和connfd在父程式和子程式之間共享

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close
最後父程式關閉已連線套接字,子程式關閉監聽套接字,由子程式處理與客戶端的連線,父程式則繼續監聽下一個客戶端連線請求

一文告訴你java NIO底層用到的那些connect、bind、listen、accept、close

父程式中呼叫fork之前所開啟的所有描述符在fork返回之後與子程式共享。

併發伺服器

併發伺服器的存在是不希望一個服務一個客戶端過長時間,而導致整個伺服器被單個客戶端長期佔用,Unix中編寫併發伺服器最簡單的辦法就是 fork一個子程式來服務每個客戶,一般實現如下:

for(;;){
  connfd=Accept(listenfd,..)
// fork呼叫一次會返回兩次。在子程式中返回值一次,返回值為0;在呼叫程式,即父程式,中返回一次,返回值為新建的子程式的程式ID;
  if((pid=Fork())==0){
      Close(listenfd); //子程式不監聽,直接關閉
      doSomething(connfd); //處理客戶端請求
      Close(connfd); //處理客戶端請求完畢,關閉連線
      exit(0);
  }
  Close(connfd) //由子程式處理,父程式就可以斷開連線
}
複製程式碼

父程式中關閉了新建立的連線,為什麼子程式還能處理連線請求?

每個檔案或套接字都有一個引用計數。在檔案表中維護,它表示的是當前開啟著的引用該檔案或者套接字的描述符的個數。socket返回後與listenfd關聯的檔案表項的引用計數值為1,accept返回的connfd也是如此。fork之後,兩個檔案描述符在父子程式之間共享,因此引用計數均變成2,這樣當父程式關閉connfd的時候,只是引用計數從2變成了1,而真正的資源清理和釋放只有在變為0才發生。

close

用來關閉套接字,如果檔案的引用計數此時恰好為0,就會傳送FIN包,終止TCP連線。

如果想直接終止可以用shutdown

相關文章