1. IP 協議
1.1 IP 分片
(1)IP 分片和重組主要依靠 IP 頭部三個字段:數據報標識、標志和片偏移
以太網幀的 MTU 是 1500 字節;
一個每個分片都有自己的 IP 頭部,它們都具有相同的標識值,有不同的片偏移(數據部分的長度必須是 8 的整數倍),除了最后一個分片外,其他分片都設置 MF 標志;
每個分片的總長度字段被設置為該分片的長度;
1.2 路由機制
執行 route
可以查看路由表
第一項的目標地址是 default,即默認路由項,包含一個“G”標志,說明路由下一跳目標是網關;
- 查找路由表中和數據報目標的 IP 地址完全匹配的 IP 地址;
- 查找路由表中和數據報目標的 IP 地址具有相同網絡號的 IP 地址(網絡號為 IP 地址&&子網掩碼,匹配長度越長越優先);
- 選擇默認路由項;
ICMP 重定向報文會改變路由表緩沖,生成更合理的路由方式;
2. TCP 協議
TCP 是一對一的,UDP 適合多播和廣播;
UDP 中應用程序每次執行一個寫操作,就會發送一個 UDP 數據報;接收方必須及時針對每一個 UDP 數據報執行讀操作,否則會丟包,如果沒有足夠的應用程序緩沖區,UDP 數據可能被截斷;
2.1 TCP 狀態轉移
(1)服務器通過 listen
系統調用進入 LISTEN 狀態,監聽到某個連接請求時(收到 SYN),將該連接放入內核等待隊列,發送 SYN + ACK,收到 ACK 后,進入 ESTABLISHED 狀態;
(2)客戶端通過 connect
系統調用,該函數首先發送 SYN,使狀態轉移到 SYN_SENT 狀態;
connect
調用失敗的兩個場景:(立即返回初始的 CLOSED 狀態)
- 目標端口不存在,或其處于 TIME_WAIT 狀態,服務器返回 RST 段
- 目標端口存在,但
connect
在超時時間內未收到服務器的確認報文段
connect
調用成功,轉移至 ESTABLISHED 狀態;
(3)客戶端在 FIN_WAIT_2 狀態時,未等服務器關閉連接強行退出;
此時客戶端連接由內核接管,稱為孤兒連接;tcp_max_orphans
和 tcp_fin_timeout
定義了內核能接管的孤兒連接數目及其在內存中生成的時間;
(4)TIME_WAIT 狀態存在的原因:
可靠的終止 TCP 連接
保證讓遲來的 TCP 報文段被丟棄
(5)異常終止連接;
應用可以通過 socket 選項 SO_LINGER 來發送 RST 報文段;
2.2 TCP 數據流
(1)交互數據:僅包含很少字節,對實時性要求高,如 telnet,ssh 等;
客戶端每個鍵入就會發送一個 TCP 數據報;客戶端會立即確認,服務器可能使用延遲確認;
(2)成塊數據:長度通常為最大數據長度,對傳輸效率要求高,如 ftp;
2.3 帶外數據
緊急指針指向帶外數據最后一個字節的下一個位置,因此僅能識別帶外數據的最后一個字節;
2.4 擁塞控制
控制發送窗口(SWND),即發送端能連續發送的 TCP 報文段的數量
3. HTTP 協議
HTTP 請求中的頭部字段可按任意順序排序,目標主機名是必須包含的頭部字段;
3.1 請求
(1)請求行;格式為:請求方法 ------ 目標資源 URL ------ 版本號(------ 代表空格或 \t(可能為多個));
(2)多個頭部字段行;一個頭部字段是一行,請求行和每個頭部字段行必須以< CR >< LF >結尾(回車符及換行符)
(3)空行;所有頭部字段之后,需要包含一個空行(回車符及換行符),標識頭部字段的結束
(4)可選消息體;若消息體為空,頭部字段必須包含該消息體長度的字段”Content-Length“
3.2 應答
(1)狀態行;
(2)頭部字段行;
HTTP 是一種無狀態的協議,使用額外的手段來保持 HTTP 連接狀態;
Cookie 是服務器發送給客戶端的特殊信息,客戶端每次向服務器發送請求時帶上這個信息,就可以幫助服務器區分不同客戶;
(3)空行;
(4)請求數據;
4. 整體流程
本地名稱查詢:/etc/hosts 存儲了目標主機名及其對應 IP 地址
(1)如果在 /etc/hosts 未找到目標機器名對應的 IP 地址,會使用 DNS 服務;這個順序可由 /etc/host.conf 配置
(2)DNS 服務:首先讀取 /etc/resolv.conf,獲取 DNS 服務器的 IP 地址,發送 UDP 報文段;
到達 IP 層時,會查詢路由表,獲得下一跳的 IP 地址,再調用網絡驅動程序,根據這個地址獲得其 MAC 地址(如果沒有,會使用 ARP 協議)
5. socket
5.1 socket 地址族
5.1.1 字節序
(1)大端序:高位在內存低地址處(網絡字節序,JAVA 虛擬機采用大端序)
------- 小端序:高位在內存高地址處(主機字節序)
(2)字節序的轉換函數,long 函數通常轉換 IP 地址,short 通常轉換端口號;
轉換函數源碼:通過移位實現;
5.1.2 通用 socket 地址
(1)socket 結構體
sa_family 表示地址族類型;
sa_data 存放 socket 地址值,不同協議族有不同含義和長度,14 字節的 sa_data 無法完全容納多數協議族的地址值;
協議族 | 地址族 | 描述 | 地址含義及長度 |
---|---|---|---|
PF_UNIX | AF_UNIX | UNIX 本地域協議族 | 文件路徑名,長度可達108字節 |
PF_INET | AF_INET | TCP/IPv4協議族 | 16 位端口號和32位 P地址,共6字節 |
PF_INET6 | AF_INET6 | TCP/IPv6 協議族 | 16 位端口號,32位流標識,128位IP地址,32位范圍ID,共26字節 |
(2)Linux 定義了新的通用 socket 地址結構體
_ss_align 用于內存對齊作用;
5.1.3 專用 socket 地址
通用地址涉及位操作;
#include <sys/un.h>
struct sockaddr_un
{sa_family_t sin_family; /*AF_UNIX*/char sun_path[108]; /*文件路徑名*/
};
----------------------------------------------------------------
struct sockaddr_in
{sa_family_t sin_family; /*AF_INET*/u_int16_t sin_port /*端口號, 要用網絡字節序表示*/struct in_addr sin_addr; /*IPv4 地址結構體*/
};
struct in_addr
{u_int32_t s_addr; /*IPv4地址,要用網絡字節序表示*/
};
----------------------------------------------------------------
struct sockaddr_in6
{sa_family_t sin6_family; /*AF_INET6*/u_int16_t sin6_port; /*端口號,要用網絡字節序表示*/u_int32_t sin6_flowinfo; /*流信息,應設置為 0*/struct in6_addr sin6_addr; /*IPv6 地址結構體*/u_int32_t sin6_scope_id; /*暫未使用*/
};
struct in6_addr
{unsigned char sa_addr[16]; /*IPv6地址,要用網絡字節序表示*/
};
所有專用 socket 地址(以及 sockaddr_storage)類型在實際使用時都需要轉化為通用 socket 類型 sockaddr(強制轉換即可)
如何進行的轉換?
5.1.4 IP 地址轉換
注意:點分十進制字符串是主機字節序,整數為網絡序;
#include <arpa/inet.h>
/*
點分十進制轉換為整數
失敗返回 INADDR_NONE
*/
in_addr_t inet_addr( const char* strptr );
/*
點分十進制轉換為整數,將結果存儲與 inp 指向結構中,
成功返回 1,失敗返回 0
*/
int inet_aton( const char* cp, struct in_addr* inp );
/*整數轉換為點分十進制*/
char* inet_ntoa( struct in_addr in );
----------------------------------------------------
/*
點分十進制字符串轉換為整數
成功返回1
失敗返回0,設置errno
*/
int inet_pton( int af, const char* src. void* dst );
/*
整數轉換為點分十進制字符串
成功返回目標存儲單元地址
失敗返回 NULL 并設置 errno
*/
const char* inet_ntop( int af, const void* src. char* dst, socklen_t cnt );
這里注意:inet_ntoa
函數內部使用靜態變量存儲轉換結果,函數返回值指向該靜態內存,是不可重入的(會互相干擾)
轉換結果存儲在 dst 指向內存中;af 指定地址族,可以是 AF_INET 或 AF_INET6;參數 cnt 指定目標存儲單元大小,取值參考下面;
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16 /*用于 IPv4 */
#define INET6_ADDRSTRLEN 46 /*用于 IPv6 */
5.2 操作 socket
5.2.1 創建
指定地址族;socket 是一個文件描述符;
#include <sys/types.h>
#include <sys/socket.h>
/*
成功返回 socket 文件描述符
失敗返回 -1 并設置 errno
*/
int socket( int domain, int type, int protocol );
(1)domain 指明協議族,PF_**;
(2)type 指定服務類型,SOCK_STREAM(流服務)和 SOCK_UGRAM(數據報)對 TCP/IP 而言,前者表示使用 TCP,后者為 UDP;
服務類型可以和標志進行與運算:SOCK_NONBLOCK(新創建的 socket 設為非阻塞) 和 SOCK_CLOEXEC (fork 創建子進程時在子進程關閉該 socket);
(3)protocol;應該設置為 0;
5.2.2 命名
socket 命名:將一個 socket 與 socket 地址綁定;命名后客戶端才知道如何連接它;客戶端通常不需要命名 socket,采用匿名方式(操作系統自動分配);
#include <sys/types.h>
#include <sys/socket.h>
/*
成功返回 0
失敗返回 -1 并設置 errno
EACCES 表示被綁定的地址是受包含的地址,僅超級用戶能夠訪問,例如普通用戶綁定知名端口
EADDRINUSE 被綁定的地址正在使用中,比如綁定到一個處于 TIME_WAIT 的 socket 地址
*/
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );
addrlen 指出 socket 地址的長度;
5.2.3 監聽 socket
創建監聽隊列以存放待處理的客戶連接;
#include <sys/socket.h>
/*
成功返回 0
失敗返回 -1 并設置 errno
*/
int listen( int sockfd, int backlog );
backlog 參數提示內核監聽隊列的最大長度,表示處于完全連接狀態(ESTABLISHED)的 socket 上限,典型值為 5;超過該值,服務器不再受理新的連接,客戶端收到 ECONNREFUSED 錯誤;
處于半連接狀態的 socket 上限由 /proc/sys/net/ipv4/tcp_max_syn_backlog 內核參數定義
5.2.4 接收連接
簡單的從 listen
監聽隊列中接收一個連接,不關心連接處于哪種狀態,例如建立連接后,斷開客戶端網絡或直接退出客戶端(處于 CLOSE_WAIT狀態),都能調用成功;
#include <sys/types.h>
#include <sys/socket.h>
/*
成功返回一個新的連接 socket,唯一標識了被接收的連接
失敗返回 -1 并設置 errno
*/
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );
sockfd 是 listen
系統調用監聽 socket;
addr 獲取遠端 socket 地址,該地址長度由 addrlen 指出;
accept
調用成功返回的連接 socket 至少完成了三次握手的前兩個;
5.2.5 發起連接
/*
成功返回 0
失敗返回 -1 并設置 errno
*/
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t *addrlen );
serv_addr 為服務器監聽,socket,addrlen 為這個地址長度;
常見 errno:
ECONNREFUSED,目標端口不存在,連接被拒絕
ETIMEDOUT,連接超時
connect
成功返回后,TCP 三次握手已完成
5.2.6 關閉連接
關閉連接只是將其引用計數減1,等其為0時,才真正關閉;
#include <unistd.h>
int close( int fd );
立即終止連接(不是將引用計數減1)
#include <sys/socket.h>
/*
成功返回 0
失敗返回 -1 并設置 errno
*/
int shutdown( int sockfd, int howto );
howto 取值為:
- SHUT_RD,關閉讀,不能對其執行讀操作,接收緩沖區的數據都被丟棄
- SHUT_WR,關閉寫,之前發送緩沖區數據會在真正關閉前發送出去,處于半連接狀態;
- SHUT_RDWR,都關閉
5.3 數據讀寫
對文件的讀寫操作 read
和 write
可以使用,但有專門的系統調用,增加了對數據讀寫的控制;
5.3.1 TCP 數據讀寫
/*
失敗返回 -1 并設置 errno
*/
ssize_t recv( int sockfd, void *buf, size_t len, int flags );
ssize_t send( int sockfd, const void *buf, size_t len, int flags );
buf 和 len 分別為緩沖區的位置和大小,recv
成功返回實際讀到的數據,返回 0 意味對方關閉連接;
send
成功返回實際寫入的數據的長度
flags 參數只對 send
和 recv
的當前調用生效,一般設為 0;
5.3.2 UDP 數據讀寫
/*
失敗返回 -1 并設置 errno
*/
ssize_t recvfrom( int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen );
ssize_t sendto( int sockfd, const void *buf, size_t len, int flags const struct sockaddr* dest_addr, socklen_t addrlen );
recvfrom
和sendto
也可用于面向連接的 socket 數據讀寫,只需最后兩個參數設為 NULL;
5.3.3 通用數據讀寫
#include <sys/socket.h>
ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags );
ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );
struct msghdr
{void* msg_name; /* socket地址 */socklen_t msg_namelen; /* socket地址長度 */struct iovec* msg_iov; /* 分散的內存塊,數組首地址 */int msg_iovlen; /* 分散的內存塊數量 */void* msg_control; /* 指向輔助數據的起始位置 */socklen_t msg_controllen; /* 輔助數據的大小 */int msg_flags; /* 復制函數中的 flags 參數,并在調用過程中更新 */
};
struct iovec
{void* iov_base; /* 內存起始地址 */size_t iov_len; /* 這塊內存長度 */
};
(1)對 TCP 協議,msg_name 必須設為 NULL;
(2)iovec 封裝了一塊內存,msg_iovlen 指定了這樣的對象有幾個;
對 recvmsg
而言,數據被讀取并存放在 msg_iovlen 塊分散內存中,這些內存由 msg_iov 指向的數組指定,稱為分散讀;
對 sendmsg
而言,msg_iovlen 塊分散內存中的數據將被一并發送,稱為集中寫;
(3)msg_flags 無需設定,會復制 recvmsg/sendmsg
的 flags 參數;
5.4 帶外標記
判斷下一個被讀取到的數據是否是帶外數據,若是,返回 1 ,之后可以利用 MSG_OOB 標志的 recv
調用來接收,否則,返回 0;
#include <sys/socket.h>
int sockatmark( int sockfd );
5.5 地址信息函數
#include <sys/socket.h>
/*
獲取 sockfd 對應本端 socket 地址
成功返回 0
失敗返回 -1 并設置 errno
*/
int getsockname( int sockfd, struct sockaddr* address, socklen_t* address_len );
/* 獲取 sockfd 對應遠端 socket 地址*/
int getpeername( int sockfd, struct sockaddr* address, socklen_t* address_len );
如果實際 socket 地址長度大于 address 指向內存,那么該 socket 地址會被截斷;
5.6 socket 選項
#include <sys/socket.h>
/*
成功返回 0
失敗返回 -1 并設置 errno
*/
int getsockopt( int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len );
/* 獲取 sockfd 對應遠端 socket 地址*/
int setsockopt( int sockfd, int level, int option_name, const void* option_value, socklen_t* restrict option_len );
level | option name | 數據類型 | 是否需要特定的設定時機 | 說明 |
---|---|---|---|---|
SOL_SOCKET(通用選項) | SO_REUSEADDR | int | 是 | 重用本地地址 |
SOL_SOCKET | SO_RCVBUF | int | 是 | TCP 接收緩沖區大小 |
SOL_SOCKET | SO_SNDBUF | int | 是 | TCP 發送緩沖區大小 |
SOL_SOCKET | SO_RCVLOWAT | int | 是 | TCP 接收緩沖區低水位標記 |
SOL_SOCKET | SO_SNDLOWAT | int | 是 | TCP 發送緩沖區低水位標記 |
SOL_SOCKET | SO_LINGER | linger | 是 | 若有數據待發送,則延遲關閉 |
對服務器而言,有些 socket 選項只能在 listen
之前設定,對監聽 socket 設定的選項,對返回的連接 socket 將自動繼承這些選項;
對客戶端而言,這些選項應該在調用 connect
之前設定
(1)設置 TCP 接收/發送緩沖區大小時,系統會將其值加倍,并不得小于某個最小值;
TCP 接收緩沖區的最小值為 256 字節,發送緩沖區的最小值為 2048 字節
(2)SO_RCVLOWAT 和 SO_SNDLOWAT 一般被 IO 復用調用用來判斷 socket 是否可讀或可寫;
(3)SO_LINGER 控制 close
關閉 TCP 的行為;
默認調用 close
會立即返回,TCP 模塊負責把該 socket 對應 TCP 發送緩沖區中殘留的數據發送給對方;
struct linger
{int l_onoff; /* 開啟(非0)還是關閉(0) */int l_linger; /* 滯留時間 */
};
l_onoff 為 0;該選項無效,close
采用默認行為;
l_onoff 不為 0,l_linger 為 0;close
立即返回,丟棄發送緩沖區數據,同時給對方發送一個 RST 報文段(異常終止一個連接);
l_onoff 不為 0,l_linger 大于 0;對于阻塞的 socket,close
等待 l_linger 時間,知道發送完所有殘留數據并得到對方確認,否則,將返回 -1 并設置 errno 為 EWOULDBLOCK;如果 socket 是非阻塞的,close
立即返回,此時需要根據其返回值和 errno 來判斷殘留數據是否發送完畢;
5.7 網絡信息
網絡信息 API 作用:實現主機名代替 IP,服務名代替端口號
5.7.1 根據主機名/IP 獲取主機完整信息
如果在 /etc/hosts 未找到目標機器名對應的 IP 地址,會使用 DNS 服務;
#include <netdb.h>
struct hostent* gethostbyname( const char* name );
struct hostent* gethostbyaddr( const void* addr, size_t len, int type );
struct hostent
{char* h_name; /*主機名*/char** h_aliases; /*主機別名列表*/int h_addrtype; /*地址族*/int h_length; /*地址長度*/char** h_addr_list; /*主機 IP 地址列表(網絡字節序給出)*/
};
addr 是目標主機的 IP 地址,len 是 addr 所指 IP 地址長度;
type 是 addr 所指 IP 地址類型,取值為 AF_INFT / AF_INFT6;
5.7.2 根據名稱/端口號獲取服務的完整信息
實際上讀取 /etc/services 文件獲取服務信息;
#include <netdb.h>
struct servent* getservbyname( const char* name, const char* proto );
struct servent* getservbyport( int port, const char* proto );
struct servent
{char* s_name; /*服務名*/char** s_aliases; /*服務別名列表*/int s_port; /*端口號族*/char* s_proto; /*服務類型,見下*/
};
proto 指定服務類型,取值為 ”tcp“、 ”udp“、NULL(所有類型的服務)
注意:上述四個函數是不可重入的(線程不安全),可重入版本函數名結尾加 _r
5.7.3 getaddrinfo
通過主機名/服務名獲得 IP/端口號,內部調用 gethostbyname
或 getservbyname
#include <netdb.h>
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints,struct addrinfo** result );
struct addrinfo
{int ai_flags; /*見下*/int ai_family; /*地址族*/int ai_socktype; /*服務類型,SOCK_STREAM 或 SOCK_DGRAM */int ai_protocol; /*具體網絡協議,通常設置為 0 */ socklen_t ai_addrlen; /*socket 地址 ai_addr 的長度*/char* ai_canonname; /*主機的別名*/struct sockaddr* ai_addr; /*指向 socket 地址*/struct addrinfo* ai_next; /*指向下一個addrinfo對象*/
};
result 指向一個鏈表,存儲 getaddrinfo
返回結果
hints 具有提示作用,當使用時,設置其前四個字段,其余必須被設置為 NULL;
getaddrinfo
將給 result 隱式分配堆內存,必須通過下述函數進行釋放
void freeaddrinfo( struct addrinfo* res );
5.7.4 getnameinfo
通過socket 地址同時獲得 主機名和服務名,內部調用 gethostbyaddr
和 getservbyport
/*
成功返回 0
失敗返回錯誤碼
*/
void getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* hostsocklen_t hostlen, char* serv, socklen_t servlen, int flags );
將錯誤碼轉化為字符串形式;
const char* gai_strerror( int error );
6. 參考
《Linux高性能服務器》