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

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

socket

用於指定通訊的協議類型,它的返回值爲socket descriptorbash

函數定義爲 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失敗則該套接字不可再用,必須關閉,想要重鏈接必須再調用socketsocket

connect在那些狀況下會出錯?

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

對於4.4BSD內核發送SYN,沒有響應再等6s發送,無響應等24s,若是總共等了75s仍然沒有就返回ETIMEDOUT錯誤函數

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

這是種硬錯誤。收到RST多是:沒有服務器監聽鏈接的端口;TCP想取消鏈接;TCP收到一個根本不存在的鏈接上的分節ui

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

這是種軟錯誤spa

bind

將本地協議地址賦予一個套接字。操作系統

本地協議地址:好比 IPv4或IPv6地址與端口的組合3d

調用bind的端口和地址能夠都指定或者都不指定,或者只指定一個。若是端口號不指定,內核會在bind被調用時選擇一個臨時的端口。指針

函數定義爲 int bind(int sockfd,const struct *myaddr,socklen_t addrlen);第一個參數就是就是socket返回的套接字描述符,第二個參數是指向特定於協議的地址結構的指針,第三個是該地址結構的長度。因爲地址結構是個常量,因此若是是內核指定端口,沒法返回,因此要獲取內核指定的臨時端口,必須調用getsockname返回協議地址

listen

作兩件事

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

socket建立的套接字默認是用來主動發起請求的,即用來調用connect函數,listen則是將這個套接字變成被動套接字,用來接收請求

內核維護的監聽套接字隊列

backlog的同一個取值根據操做系統不一樣,實際的數目會有差異

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

三次握手正常完成的這項會從未完成鏈接對列移到已完成隊列的隊尾。當進程調用accept時,已完成隊列的頭部將返回給進程,若是已完成隊列爲空,進程將被投入睡眠,睡眠針對的是默認的阻塞模式,直到TCP在該隊列中放入一項才喚醒。

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

accept

用於從已完成鏈接隊列隊頭返回下一個已完成鏈接。若是accept成功,返回值是有內核自動生成的一個全新的描述符,表明與客戶端創建的TCP鏈接。

一個服務器一般只建立一個監聽套接字,他在這個服務的聲明週期內一直存在。可是會爲每一個客戶端的鏈接創建一個以鏈接套接字,對客戶端的服務完成時,就關閉這個鏈接套接字

accept生成新的描述符處理已鏈接的請求過程

首先處於監聽狀態的服務器監聽客戶端發來的鏈接請求

第二步accept返回結果,鏈接被內核接受,新的套接字(connfd)建立

第三步併發服務器會調用fork,此時listenfd和connfd在父進程和子進程之間共享

最後父進程關閉已鏈接套接字,子進程關閉監聽套接字,由子進程處理與客戶端的鏈接,父進程則繼續監聽下一個客戶端鏈接請求

父進程中調用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

相關文章
相關標籤/搜索