文章目錄
- 讀取和設置 socket 選項
- SO_REUSEADDR
- SO_RCVBUF 和 SO_SNDBUF
- SO_RCVLOWAT 和 SO_SNDLOWAT
- SO_LINGER 選項
- 網絡信息API
- gethostbyname 和 gethostbyaddr
- getservbyname 和 getservbyport
- getaddrinfo
- getnameinfo
讀取和設置 socket 選項
正如 fcntl
系統調用是控制文件描述符屬性的通用 POSIX
方法;socket文件描述符的屬性也有兩個系統調用專門 讀取 和 設置 :
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
// sockfd 指定被操作的socket。
// level 指定操作哪個協議的選項(屬性),如:IPv4、IPv6、TCP等。
// option_name 指定選項的名字。
// option_value 和 option_len 參數分別指定操作選項值和長度。
// 成功返回0,失敗返回-1并置errno。
level | option | 數據類型 | 說明 | 必須在 listen/connect 調用之前設置 |
---|---|---|---|---|
SOL_SOCKET | SO_DEBUG | int | 打開調試信息 | YES |
(通用 socket 選項,與協議無關) | SO_REUSEADDR | int | 重用本地地址。如:可以令服務器處于 TIME_WAIT 的端口立刻被使用。 | |
SO_TYPE | int | 獲取 socket 類型 | ||
SO_ERROR | int | 獲取并清除 socket 錯誤狀態 | ||
SO_DONTROUTE | int | 不查看路由表,直接將數據發送給本地局域網內的主機。類同 send 系統調用的 MSG_DONTROUTE。 | YES | |
SO_RCVBUF | int | TCP接收緩沖區大小(最小值是256字節) | YES | |
SO_SNDBUF | int | TCP發送緩沖區大小(最小值是2048字節) | YES | |
SO_RCVLOWAT | int | TCP接收緩沖區低水位標記 | YES | |
SO_SNDLOWAT | int | TCP發送緩沖區低水位標記 | YES | |
SO_RCVLOWAT | int | 接收數據超時 | ||
SO_SNDLOWAT | int | 發送數據超時 | ||
SO_KEEPALIVE | int | 發送周期性保活報文以維持連接 | YES | |
SO_OBBINLINE | int | 接收到的帶外數據將 在線存留 在普通數據的輸入隊列中,此時我們不能使用帶 MSG_OOB 的讀操作來讀取帶外數據,而應該像讀取普通數據那樣讀取帶外數據。 | YES | |
SO_LINGER | intger | 若有數據待發送,則延遲關閉。 | YES | |
IPPROTO_IP | IP_TOS | int | 服務類型 | |
(IPv4選項) | IP_TTL | int | 存活時間 | |
IPPROTO_IPV6 | IPV6_NEXTHOP | sockaddr_in6 | 下一跳IP地址 | |
(IPv6選項) | IPV6_RECVPKTINFO | int | 接受分組信息 | |
IPV6_DONTFRAG | int | 禁止分片 | ||
IPV6_RECVTCLASS | int | 接受通信類型 | ||
IPPROTO_TCP | TCP_MAXSEG | int | TCP最大報文段大小 | YES |
(TCP選項) | TCP_NODELAY | int | 禁止Nagle算法 | YES |
對 服務器 而言,部分socket選項
只能在調用 listen 系統調用前
針對 socket
設置才有效。這是因為連接 socket
只能由 accept
調用返回,而 accept
從 listen 監聽隊列
中接受的連接至少是個 半連接 ,這說明 服務器 已經往 客戶端 上發送出了 TCP同步報文段(執行完了三次握手中的前兩次)。但 部分 socket
選項只能在 TCP同步報文中設置 ,如:TCP最大報文段選項。對此有兩種解決方案:
- 對于服務器而言,執行
listen系統調用
時設置這些socket選項
,那么accept
返回的連接socket
將自動繼承這些選項(注1)
。 - 對于客戶端而言,這些
socket選項
應該在調用connect函數
之前設置,因為connect
調用成功之后三次握手已完成。
注1
這些選項包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OBBINLINE、SO_RCVBUF、SO_RCVLOWAT、TCP_MAXSEG、TCP_NODELAY 。
SO_REUSEADDR
服務器程序可以通過設置本選項來強制使用被處于 TIME_WAIT
狀態的連接占用的 socket
地址。此外,我們也可以通過修改內核參數 /proc/sys/net/ipv4/tcp_tw_recycle
來快速回收被關閉的 socket
,甚至使 TCP
連接根本就不進入 TIME_WAIT
狀態。
int sock = socket( PF_INET, SOCK_STREAM, 0); // TCP協議,IPv4版本,基于流服務,0:使用默認協議
assert( sock >= 0); // 檢測創建sock是否成功
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse) );
// 操作描述符為sock的socket套接字,操作的屬性是通用socket選項
// 選項類型為SO_REUSEADDR,SO_REUSEADDR的數據類型為int
// 1即為啟用SO_REUSEADDR選項
SO_RCVBUF 和 SO_SNDBUF
這兩個選項分別用來表示 TCP
接收緩沖區大小和發送緩沖區大小。不過,當我們 通過setsockopt
來設置 TCP
的接收、發送緩沖區大小時,系統都會將其值加倍,并且不小于某個值。
一般來講,TCP
接收緩沖區的最小值是 256
字節,發送緩沖區的最小值是 2048
字節(不同操作系統可能有差異)。我們可以直接修改內核參數 /proc/sys/net/ipv4/tcp_rmem
和 /proc/sys/net/ipv4/tcp_wmem
來強制緩沖區沒有最小值限制。
/* 先設置 TCP 接收緩沖區的大小,然后立即讀取 */
setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf) );
getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t* )&len );
// sock 為目標套接字的文件描述符
// recvbuf 為設定的緩沖區大小
// len=sizeof( recvbuf );
SO_RCVLOWAT 和 SO_SNDLOWAT
該兩個選項分別表示接收、發送緩沖區的低水位標記。一般被 I/O復用系統調用
用來判斷 socket
是否可讀或可寫:
- 當
接收緩沖區中
可讀數據的總數大于其低水位標記時,I/O復用系統調用
將通知應用程序可以從對應的socket
上讀取數據 - 當
發送緩沖區
中的空閑空間大于其低水位標記時,I/O復用系統調用
將通知應用程序可以往對應的socket
上寫入數據。
默認情況下,兩者均為 1字節
。
SO_LINGER 選項
該選項用于控制 close系統調用
在關閉 TCP
連接時的行為。默認情況下,當我們使用 close系統調用
來關閉一個 socket
時,close
將立即返回,TCP
模塊負責把該 socket
對應的 TCP
發送緩沖區中殘留的數據發送給對方。
設置(獲取)SO_LINGER
選項的值時,我們需要給 setsockopt(getsockopt)
系統調用傳遞一個 linger
類型的結構體:
#include <sys/socket.h>struct linger{int l_onoff; // 開啟(非0)/關閉(0)該選項int l_linger; // 滯留時間
};
根據 linger
結構體中兩個成員變量的不同值,close
系統調用可能產生如下行為:
- l_onoff 等于 0。 此時
SO_LINGER
選項不起作用,close
用默認行為來關閉socket
。 - l_onoff 不為 0,l_linger 等于 0。 此時
close系統調用
立即返回,TCP模塊
將丟棄被關閉的socket
對應的TCP發送緩沖區
中殘留的數據,同時給對方發送一個復位報文段
。這為服務器提供了異常終止一個連接的方法。 - l_onoff 不為 0,l_linger 大于 0。 此時 close 的行為取決于兩個條件:
- 被關閉的
socket
對應的發送緩沖區
中是否還有殘留的數據; - 該
socket
是阻塞的還是非阻塞的,阻塞:close
將等待一段長為l_linger
的時間,直到TCP模板
發送完所有殘留數據并得到對方的確認。如果沒發送完并得到確認,則close
返回-1
并設置errno
為EWOULDBLOCK
。非阻塞:close
將立即返回,此時需要根據其返回值
和errno
來判斷殘留數據是否發送完畢。
- 被關閉的
網絡信息API
gethostbyname 和 gethostbyaddr
- gethostbyname: 根據主機名稱獲取主機的完整信息。通常先在本地的
/etc/hosts
配置文件中查找主機,沒有找到再去訪問DNS
服務器。 - gethostbyaddr: 根據IP地址獲取主機的完整信息。
#include<netdb.h>
struct hostent* gethostbyname( const char* name );
struct hostent* gethostbyaddr( const void* addr, size_t len, int type );
// len IIP地址長度
// type 指定addr所指IP地址的類型,可以是AF_INET或AFINET6
兩者的返回類型都是 hostent
結構體類型的指針:
#include<netdb.h>
struct hostent{char* h_name; // 主機名char** h_aliases; // 主機別名列表,可有多個int h_addrtype; // 地址類型(地址族)int h_length; // 地址長度char** h_addr_list; // 按網絡字節序列出的主機IP地址列表
};
getservbyname 和 getservbyport
根據名稱/端口號獲得某個服務的完整信息。實際上都是通過讀取 /etc/services
文件來獲取服務的信息的。
#include<netdb.h>
struct servent* getservbyname( const char* name, const char* proto );
struct servent* getservbyport( int port, const char* proto );
// name 目標服務的名字
// port 目標服務對應的端口號
// proto 服務類型,tcp表流服務、udp表數據報服務、NULL表獲取所有類型的服務
兩者的返回類型都是 servent
結構體類型的指針:
#include<netdb.h>
struct servent{char* s_name; // 服務名稱char** s_aliases; // 服務別名列表,可有多個int s_port; // 端口號char* s_proto; // 服務類型,通常是 tcp 或 udp
};
不可重入
gethostbyname
、gethostbyaddr
、getservbyname
和 getservbyport
都是不可重入的,即非線程安全的。但是 netdb.h
頭文件給出了它們的可重入版本:在原函數名尾部加上 _r(re-entrant)
。
getaddrinfo
既能通過主機名獲取 IP
地址(內部使用的是 gethostbyname
),也能通過服務名獲取端口號(內部使用的是 getservbyname
),是否可重入取決于內部調用的函數( gethostbyname
、 getservbyname
)是否是它們的可重入版本。
#include<netdb.h>
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );
// hostname:可以接收主機名(服務名)/字符串表示的IP地址【IPv4使用點分十進制字符串、IPv6使用十六進制字符串】
// service:可以接收服務名/字符串表示的十進制端口號
// hints:可以為NULL,表示允許反饋任何可用的結果。
// result:指向一個用于存儲反饋結果的鏈表
getaddrinfo
將隱式分配堆內存,因此調用結束后,必須釋放這塊內存:
#include <netdb.h>
void freeaddrinfo( struct addrinfo* res );
getaddrinfo
反饋的每一條結果都是 addinfo
結構體類型的對象:
struct addrinfo
{int ai_flags; // 標志int ai_family; // 地址族int ai_socktype; // 服務類型,SOCK_STREAM或SOCK_DGRAMint ai_protocol; // 具體的網絡協議,等同于socket系統調用的第三個參數,常被設為0以表自動匹配對應協議。socklen_t ai_addrlen; // socket地址ai_addr的長度char* ai_canonname; // 主機的別名struct sockaddr* ai_addr; // 指向socket地址struct addrinfo* ai_next; // 指向下一個 sockinfo 結構的對象
};
ai_flags成員可以取下表中的標志的按位或:
選項 | 含義 |
---|---|
AI_PASSIVE | 套接字地址將用于調用 bind 函數,服務器通常需要設置以表接受任何本地 socket 地址上的服務請求。客戶端不能設置。 |
AI_CANONNAME | 返回主機的別名 |
AI_NUMERICHOST | hostname 參數必須是IP地址字符串,避免了DNS查詢。 |
AI_NUMERICSERV | 強制 service 參數必須是十進制端口號字符串,不能是服務名。 |
AI_V4MAPPED | 如果對 IPv6 地址的 getaddrinfo 請求失敗,則將 IPv4 映射為 IPv6 地址格式。 |
AI_ALL | 必須和 AI_V4MAPPED 同時使用,否則將被忽略。同時返回 符合條件 和 由IPv4轉換而來 的 IPv6地址 。 |
AI_ADDRCONFIG | 只有至少配置了一個IPv4/IPv6地址(除了回路地址)后,getaddrinfo 才會解析。和 AI_V4MAPPED 互斥。 |
當我們使用 hints
參數時,可以設置 addrinfo
中前四個成員,其他成員必須設置為 NULL
。
getnameinfo
內部使用 gethostbyaddr
和 getservbyport
,是否可重入取決于內部調用的函數版本是否可重入:
#include<netdb.h>
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags );
// 將返回的主機名(服務名)存儲在 host(serv) 參數指向的緩存中
// flags控制getnameinfo的行為
getaddrinfo
和 getnameinfo
成功時返回0,失敗返回錯誤碼。Linux下 strerror
函數能將數值錯誤碼 error
轉換為易讀的字符串形式:
#include<netdb.h>
const char* gai_strerror( int error );