《UNIX網路程式設計》筆記 - 套接字選項/UDP套接字

熊紀元發表於2019-05-12

套接字選項

繼承自監聽套接字的選項

  • SO_DEBUG
  • SO_NOROUTE
  • SO_KEEPALIVE
  • SO_LINGER
  • SO_OOBINLINE
  • SO_RCVBUF
  • SO_RCVLOWWAT
  • SO_SNDBUF
  • SO_SNDLOWAT
  • TCP_MAXSEG
  • TCP_NODELAY

這幾個屬性是從監聽套接字繼承的,要想設定已連線套接字的這些屬性,需要在監聽套接字上設定。

SO_KEEPALIVE

如果設定了這個選項,當2小時內套接字的任一方向都沒有資料交換時,TCP會自動傳送一個探活資料包(keep-alive probe),接下來會有幾種可能:

  1. 對端正常響應,這時不會通知應用程式。接下來2小時內如果仍沒有資料,傳送另一個探活資料包。
  2. 對端響應RST,表示對端已經崩潰並重新啟動,這時將SO_ERROR設定為ECONNRESET,關閉套接字。
  3. 對端沒有任何響應,這時會繼續傳送探活資料包試圖獲得響應(不同系統的重試次數和間隔不同),如果仍沒有任何響應則放棄。這時會將SO_ERROR設定為ETIMEOUT,關閉套接字。
  4. 套接字收到一個ICMP錯誤(比如host unreachable),這時會將SO_ERROR設定為對應的錯誤,然後關閉套接字。

SO_LINGER

這個選項表示如何關閉面向連線的協議,預設是呼叫close時立即返回,但如果傳送緩衝區有殘留的資料,會嘗試將其傳送給對端。

設定時通過以下結構體來控制引數:

struct  linger {
	int     l_onoff;                /* option on/off */
	int     l_linger;               /* linger time, 單位為秒*/
};
複製程式碼
  1. l_onoff為0時表示選項關閉,l_linger的值被忽略,呼叫close時會立刻返回
  2. l_onoff為非0而l_linger為0時,呼叫close關閉某個連線時TCP會中止該連線,即丟棄傳送緩衝區的所有資料並向對端傳送一個RST,而不是進行正常的四次揮手。這樣能夠避免TCP的TIME_WAIT狀態,但是也可能出現2MSL內建立出該連線的化身的情況,導致來自剛才被終止的連線上的舊的資料被髮送到新的化身上。
  3. l_onoff為非0而且l_linger也為非0時,關閉套接字時核心將會拖延一段時間。如果此時傳送緩衝區中有資料,程式將會進入睡眠,直到:(a) 所有資料都已傳送並被對端確認;(b) 拖延時間到。如果套接字被設定為非阻塞型,close會立即返回,即使拖延時間為非0的情況也是。在使用SO_LINGER選項時,應該檢查close的返回值,如果在資料傳送完並被確認前拖延時間到的話,close會返回EWOULDBLOCK,且傳送緩衝區的資料都會被丟棄。

SO_REUSEADDR

  1. SO_REUSEADDR允許一個監聽套接字繫結到其眾所周知埠,即使以前建立的將該埠作為本地埠的連線仍存在。比如監聽的程式中途關閉了但其建立的子程式和連線仍存在,這時監聽程式重啟時嘗試重新繫結埠時需要指定了SO_REUSEADDR才行,否則會繫結失敗。
  2. 允許在同一個埠上啟動同一伺服器的多個例項,只要每個例項繫結不同的本地地址即可

TCP_MAXSEG

獲取或設定TCP最大分節大小(MSS),表示TCP能夠傳送的最大資料量,通常由對端的SYN分節指定,除非我們選擇一個更小的值。如果在連線建立之前查詢,返回的是預設值。

TCP_NODELAY

開啟該選項將禁用Nagle演算法,預設情況其是啟用的。nagle演算法可以減少大量小的資料包在網路中傳輸的情況。

TCP_CORK

linux2.4之後的版本才支援選項,開啟該選項將啟用cork演算法,預設是禁用的。cork選項可以禁止小的資料包在網路中傳輸。

fcntl(file control)

fcntl顧名思義,可以對描述符進行各種控制操作,主要通過cmd和arg兩個引數來控制。

int fcntl(int fd, int cmd, .../* args */)
複製程式碼

可選的cmd有:

  • F_SETFL 設定flag
  • F_GETFL 獲取flag
  • F_SETOWN 設定套接字屬主
  • F_GETOWN 獲取套接字屬主

使用方式:

int flags;
//先獲取當前flags
if ((flags = fcntl(fd, F_GETFL, 0)) < 0) {
    //error
}
//增加O_NONBLOCK選項
flags |= O_NONBLOCK;
//設定flags
if ((flags = fcntl(fd, F_SETFL, flag)) < 0) {
    //error
}
//設定flags
if (fcntl(fd, F_SETFL, flags) < 0) {
    //error
}
//關閉O_NONBLOCK選項
flags &= ~O_NONBLOCK
//設定flags
if (fcntl(fd, F_SETFL, flags) < 0) {
    //error
}
複製程式碼

在使用fcntl時,必須先獲取當前的標誌,然後與新的標誌或之後再設定標誌,否則會清除描述符的其他標誌。

UDP套接字

sendto和recvfrom

ssize_t sendto(int fd, const void *buff, size_t nbytes,
    int flags, const struct sockaddr *to, socklen_t *addrlen);
ssize_t recvfrom(int fd, void *buff, size_t nbytes,
    int flags, struct sockaddr *from, socklen_t *addrlen);
複製程式碼

函式返回傳送或者接收的位元組數,最後兩個引數指定了對端地址。如果recvfrom的後兩個引數是空指標,則表示不關心資料傳送者的協議地址。

非同步錯誤

在一個UDP套接字上呼叫sendto時,如果對端不可用,對端會返回一個ICMP訊息(比如"port unreachable"),但這個錯誤不會返回給應用程式,sendto仍能夠正常返回。

我們稱這裡的這個ICMP錯誤為非同步錯誤,這個錯誤由sendto觸發,但是sendto本身卻成功返回;原因是sendto的成功僅表示在網路介面輸出佇列中具備足夠的空間存放sendto形成的IP資料包,而真正的錯誤在隨後實際發出資料包的時候才發生,所以我們稱這個錯誤是非同步的。

對於非同步錯誤,處理的基本規則是:對於一個UDP套接字,由它引發的非同步錯誤不會返回給它,除非它已連線。這裡的已連線指的是成功呼叫了connect函式。為什麼這麼規定呢?因為一個UDP套接字可能會往多個對端傳送和讀取資料,而sendtorecvfrom只能返回單純的errno,不能返回對端的ip和埠號資訊。所以我們決定:只有在UDP已經只繫結到一個對端時,這些非同步錯誤才返回給程式。

connect

除非udp套接字事先成功呼叫了connect,否則sendtorecvfrom發生的異常不會返回給應用程式。

對一個UDP套接字呼叫connect函式並不會像TCP套接字那樣進行三次握手,而是先檢查傳入的地址是否合法(是否可達等)然後儲存ip和port到傳入的套接字地址結構體中,接著直接返回。

一旦一個UDP套接字已連線,會發生三個變化:

  1. sendto不能再指定最後兩個引數(目標地址和地址長度),必須設定為空指標;或者改用write或者send
  2. recvfrom不能再指定最後兩個引數(目標地址和地址長度),必須設定為空指標;或者改用read/recv/recvmsg
  3. 由已連線UDP套接字引發的非同步錯誤會返回給程式。

多次呼叫connect

和TCP套接字不同,UDP套接字可以多次呼叫connect函式,通過這樣做可以達到兩個目的:

  1. 為套接字指定新的對端ip和port
  2. 斷開套接字(將套接字地址結構體的地址族設定為AF_UNSPEC即可)

connect與效能

對於未連線的UDP套接字,每次呼叫sendto函式時都會隱式地進行套接字連線和斷開連線;所以如果確定UDP套接字要傳送的對端只有一個時,可以通過顯式連線來提高效率。

相關文章