socket基本的API使用

weixin_33872660發表於2018-04-04

1.socket初探

2.socket分析

3.socket核心原始碼分析

//1.生成核心socket;2。與檔案描述符繫結
socket(AF_UNIX, SOCK_STREAM, 0);
//建立連線,包含三次握手
connect(sockfd, (struct sockaddr *)&address, len);
//繫結一個IP地址和埠到socket套接字上
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
//半連線佇列、全連線佇列 
int listen(int sockfd, int backlog)
//返回一個new的socket檔案描述符(不佔用埠號)
accept(server_sockfd,(struct sockaddr *)&client_address, client_len);
//斷開連線,包含四次揮手(TCP將嘗試傳送已排隊等待傳送到對端的任何資料,傳送完畢後發生的是正常的TCP連線終止序列。TIME_WAIT原因)
int close(int sockfd); 
//可選擇性的斷開連線

系統呼叫的過程:

1.int socket(int domain,int type,int protocol)

作用:根據使用者定義的網路型別、協議型別、和具體的協議標號,生成一個套接字檔案描述符供使用者使用,實現各種初始化工作(檔案系統初始化、socket初始化等)

  • //[sock_create]
  • [ ] 分配socket結構:1.在socket檔案系統中建立i節點;2.建立socket專用inode;
  • [ ] 根據inode取得socket物件:
  • [ ] 使用協議族來初始化socket:1) 註冊AF_INET協議域 2)套接字型別(如AF_INET域下存在流套接字(SOCK_STREAM),資料包套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),在這三種型別的套接字上建立的協議分別是TCP, UDP,ICMP/IGMP);3) 使用協議域來初始化socket
  • [ ] 分配sock結構:
  • [ ] 建立socket結構與sock結構的關係:
  • [ ] 使用tcp協議初始化sock:
  • //[sock_map_fd]
  • [ ] socket與檔案系統關聯;

2.int bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen)

  • [ ] bind()的Socket層實現
  • [ ] bind()的tcp層實現、埠的衝突處理
  • Q: 什麼情況下會出現衝突呢?

同時符合以下條件才會衝突:

  1. 繫結的裝置相同(不允許自動選擇裝置)

  2. 繫結的IP地址相同(不允許自動選擇IP)

3 以下條件有一個成立:

3.1 要繫結的socket不允許重用

3.2 已繫結的socket不允許重用

3.3 已繫結的socket處於監聽狀態   

3.4 relax引數為false

埠區間(0--65535)

我們可以指定系統自動分配埠號時,埠的區間:

/proc/sys/net/ipv4/ip_local_port_range,預設為:32768 61000

也可以指定要保留的埠區間:

/proc/sys/net/ipv4/ip_local_reserved_ports,預設為空

系統自動選擇埠時:不優先選擇沒被使用過的埠。只要沒有衝突,直接重用埠。

  • 一個網路應用程式只能繫結一個埠( 一個套接字只能 繫結一個埠 )
  • 一般情況下伺服器需要繫結埠號,而客戶端可以不繫結埠號,在send的時候,系統隨機分配一個埠號。
  • 埠複用技術//設定socket的SO_REUSEADDR選項,即可實現埠複用
  • SO_REUSEADDR可以用在以下四種情況下。 (摘自《Unix網路程式設計》卷一,即UNPv1)

1、當有一個有相同本地地址和埠的socket1處於TIME_WAIT狀態時,而你啟動的程式的socket2要佔用該地址和埠,你的程式就要用到該選項。

2、SO_REUSEADDR允許同一port上啟動同一伺服器的多個例項(多個程式)。但每個例項繫結的IP地址是不能相同的。在有多塊網路卡或用IP Alias技術的機器可以測試這種情況。

3、SO_REUSEADDR允許單個程式繫結相同的埠到多個socket上,但每個socket繫結的ip地址不同。這和2很相似,區別請看UNPv1。

4、SO_REUSEADDR允許完全相同的地址和埠的重複繫結。但這隻用於UDP的多播,不用於TCP

  • 埠複用最常用的用途應該是防止伺服器重啟時之前繫結的埠還未釋放或者程式突然退出而系統沒有釋放埠
  • 當在一個應用或是程式中多個socket同時繫結到相同的埠時,這些套接字並不是所有都能讀取資訊,只有最後一個套接字會正常接收資料。
    淺析套接字中SO_REUSEPORT和SO_REUSEADDR的區別

3.int listen(int sockfd,int backlog)

backlog的定義

Now it specifies the queue length for completely established sockets waiting to be accepted,instead of the number of incomplete connection requests. The maximum length of the queuefor incomplete sockets can be set using the tcp_max_syn_backlog sysctl. When syncookiesare enabled there is no logical maximum length and this sysctl setting is ignored.If the socket is of type AF_INET, and the backlog argument is greater than the constant SOMAXCONN(128 default), it is silently truncated to SOMAXCONN.

全連線佇列的最大長度:

  • backlog儲存的是完成三次握手、等待accept的全連線佇列
  • 負載不高時,backlog不用太大。(For complete connections)
  • 系統最大的、未處理的全連線數量為:min(backlog,somaxconn),net.core.somaxconn預設為128。這個值最終儲存於sk->sk_max_ack_backlog

半連線佇列的最大長度:

  • tcp_max_syn_backlog預設值為256。(For incomplete connections)
  • 當使用SYN Cookie時,這個引數變為無效。
  • 半連線佇列的最大長度為backlog、somaxconn、tcp_max_syn_backlog的最小值。
  1. 檢查套介面的狀態、當前連線的狀態是否合法,然後呼叫inet_csk_listen_start()啟動監聽。
  2. 啟動監聽時,做的工作主要包括:
  1. 建立半連線佇列的例項,初始化全連線佇列。

  2. 初始化sock的一些變數,把它的狀態設為TCP_LISTEN。

  3. 檢查埠是否可用,防止bind()後其它程式修改了埠資訊。

  4. 把sock連結進入監聽雜湊表listening_hash中。

  • listen_sock結構用於儲存SYN_RECV狀態的連線請求塊,所以也叫半連線佇列
  1. 銷燬連線請求塊中的listen_sock例項,釋放半連線佇列
  2. inet_hash()用於把sock鏈入監聽雜湊表listening_hash,或者已建立連線的雜湊表ehash。

4.int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)

It extracts the first connection request on the queue of pending connections (backlog), creates a newconnected socket, and returns a new file descriptor referring to that socket.If no pending connections are present on the queue, and the socket is not marked as non-blocking,accept() blocks the caller until a connection is present. If the socket is marked non-blocking and no pending connections are present on the queue, accept() fails with the error EAGAIN.

  • 在sys_socketcall()中會呼叫sys_accept4():
  1. 建立了一個新的socket和inode,以及它所對應的fd、file。
  2. 呼叫Socket層操作函式inet_accept()。
  3. 儲存對端地址到指定的使用者空間地址
  • SOCK_STREAM套介面的Socket層操作函式集例項為inet_stream_ops,連線接收函式為inet_accept():
  1. 呼叫TCP層的操作函式,獲取已建立的連線sock。
  2. 把新socket和sock關聯起來。
  3. 把新socket的狀態設為SS_CONNECTED。
  • SOCK_STREAM套介面的TCP層操作函式集例項為tcp_prot,其中連線接收函式為inet_csk_accept().inet_csk_accept()用於從backlog佇列(全連線佇列)中取出一個ESTABLISHED狀態的連線請求塊,返回它所對應的連線sock,同時更新backlog佇列的全連線數,釋放取出的連線控制塊.
  1. 非阻塞的,且當前沒有已建立的連線,則直接退出,返回-EAGAIN。
  2. 阻塞的,且當前沒有已建立的連線:
    2.1 使用者沒有設定超時時間,則無限期阻塞。
    2.2 使用者設定了超時時間,超時後會退出。

accept()是如何避免驚群現象(當核心接收到一個客戶連線後,只會喚醒等待佇列上的第一個程式或執行緒)的:

初始化等待任務時,flags|=WQ_FLAG_EXCLUSIVE。傳入的nr_exclusive為1,表示只允許喚醒一個等待任務。
所以這裡只會喚醒一個等待的程式,不會導致驚群現象。

Nginx中使用mutex互斥鎖解決這個問題,具體措施有使用全域性互斥鎖,每個子程式在epoll_wait()之前先去申請鎖,申請到則繼續處理,獲取不到則等待,並設定了一個負載均衡的演算法(當某一個子程式的任務量達到總設定量的7/8時,則不會再嘗試去申請鎖)來均衡各個程式的任務量。使用mutex鎖住多個執行緒是不會驚群的,在某個執行緒解鎖後,只會有一個執行緒會獲得鎖,其它的繼續等待.

參考:accept與epoll驚群

5.int connect(int sockfd,struct sockaddr *,int addrlen)

  • SOCK_STREAM套介面的socket層操作函式集例項為inet_stream_ops,其中主動建立連線的函式為inet_stream_connect()。
  1. 檢查socket地址長度和使用的協議族。

  2. 檢查socket的狀態,必須是SS_UNCONNECTE或SS_CONNECTING。

  3. 呼叫tcp_v4_connect()來傳送SYN包。

  4. 等待後續握手的完成:

    如果socket是非阻塞的,那麼就直接返回錯誤碼-EINPROGRESS。

    如果socket為阻塞的,就呼叫inet_wait_for_connect(),通過睡眠來等待。在以下三種情況下會被喚醒:

    (1) 使用SO_SNDTIMEO選項時,睡眠時間超過設定值,返回0。connect()返回錯誤碼-EINPROGRESS。

    (2) 收到訊號,返回剩餘的等待時間。connect()返回錯誤碼-ERESTARTSYS或-EINTR。

    (3) 三次握手成功,sock的狀態從TCP_SYN_SENT或TCP_SYN_RECV變為TCP_ESTABLISHED,sock I/O事件的狀態變化處理函式sock_def_wakeup()就會喚醒程式。connect()返回0。

  5. 程式的睡眠: connect()的超時時間為sk->sk_sndtimeo,在sock_init_data()中初始化為MAX_SCHEDULE_TIMEOUT,表示無限等待,可以通過SO_SNDTIMEO選項來修改。

  6. 程式的喚醒:三次握手中,當客戶端收到SYNACK、發出ACK後,連線就成功建立了。此時連線的狀態從TCP_SYN_SENT變為TCP_ESTABLISHED,sock的狀態發生變化,會呼叫sock_def_wakeup()來處理連線狀態變化事件,喚醒程式,connect()就能成功返回了。

close()與shutdown()

int close(int sockfd); //返回成功為0,出錯為-1.
int shutdown(int sockfd,int howto); //返回成功為0,出錯為-1.

1.SHUT_RD:值為0,關閉連線的讀這一半。

2.SHUT_WR:值為1,關閉連線的寫這一半。

3.SHUT_RDWR:值為2,連線的讀和寫都關閉。

  • close函式會關閉套接字ID,如果有其他的程式共享著這個套接字,那麼它仍然是開啟的,這個連線仍然可以用來讀和寫,並且有時候這是非常重要的,特別是對於多程式併發伺服器來說。在多程式併發伺服器中,父子程式共享著套接字,套接字描述符引用計數記錄著共享著的程式個數,當父程式或某一子程式close掉套接字時,描述符引用計數會相應的減一,當引用計數仍大於零時,這個close呼叫就不會引發TCP的四路握手斷連過程。

  • shutdown會切斷程式共享的套接字的所有連線,不管這個套接字的引用計數是否為零,那些試圖讀得程式將會接收到EOF標識,那些試圖寫的程式將會檢測到SIGPIPE訊號,同時可利用shutdown的第二個引數選擇斷連的方式。利用shutdown()可以避免用close()過程出現死鎖現象

//close()

/* First  Sample client fragment, 
 * 多餘的程式碼及變數的宣告已略       */  
   s=connect(...);  
   if( fork() ){   /*      The child, it copies its stdin to the socket              */  
       while( gets(buffer) >0)  
           write(s,buf,strlen(buffer));  
           close(s);  
           exit(0);  
   }  
   else {          /* The parent, it receives answers  */  
        while( (n=read(s,buffer,sizeof(buffer)){  
            do_something(n,buffer);  
            /* Connection break from the server is assumed  */  
            /* ATTENTION: deadlock here                     */  
         wait(0); /* Wait for the child to exit          */  
         exit(0);  
    }  

//shutdown()

if( fork() ) {  /* The child                    */  
      while( gets(buffer)  
         write(s,buffer,strlen(buffer));  
      shutdown(s,1); /* Break the connection 
                        *for writing, The server will detect EOF now. Note: reading from 
                  *the socket is still allowed. The server may send some more data 
                  *after receiving EOF, why not? */  
      exit(0);  
 }  

相關文章