網絡編程離不開的基礎是網絡體系結構,通常說的是 OSI協議參考模型,它是一個七層網絡協議模型。而我們通常使用的 Internet 是基於 TCP/IP 協議的,TCP/IP 協議是 OSI 協議的一個4層簡化模型,他們之間的對應關係如下圖:
自下而上,簡單介紹下相應各層在整體架構中的作用:
- 網絡接口層:
Network Interface Layer
是 TCP/IP 的最底層,負責將二進制轉換爲數據幀,並進行數據幀的發送和接收。 - 網絡層:
Internet Layer
負責在主機之間的通信中選擇數據包的傳輸路徑。網絡互連層定義了分組格式和協議,即 IP協議(Internet Protocol)。 - 傳輸層:
在 TCP/IP 模型中,傳輸層的功能是使源端主機和目標端主機上的對等實體可以進行會話。在傳輸層定義了兩種服務質量不同的協議。
即:傳輸控制協議 TCP(transmission control protocol
)和用戶數據報協議 UDP(user datagram Protocol
)。 - 應用層:
TCP/IP 模型將 OSI 參考模型中的會話層和表示層的功能合併到應用層實現。應用層面向不同的網絡應用引入了不同的應用層協議。其中,有基於 TCP 協議的,如文件傳輸協議(File Transfer Protocol,FTP)、虛擬終端協議(TELNET)、超文本鏈接協議(Hyper Text Transfer Protocol,HTTP),也有基於 UDP 協議的。
我們在這裏主要討論的是第三層傳輸層的具體內容,即 TCP 和 UDP。
一、網絡編程基礎知識
1. 套接字
套接字(Socket)是一種特殊的 I/O 接口,也是一種文件描述符。可以用於本地和網絡通信,對於網絡通信而言,每一個 Socket 都可以用網路地址結構(協議,本地地址,本地端口)來表示。Socket 通過一個特殊的函數創建,並返回一個整形的 Socket 描述符。
套接字分爲三種:
* 流式套接字(SOCK_STREAM):TCP 通信使用的就是流式套接字。
* 數據包套接字(SOCK_DGRAM):UDP 通信使用的就是數據包套接字。
* 原始套接字(SOCKRAW):原始套接字允許對底層協議進行直接的訪問,主要用於協議的開發。
套接字有幾個相關函數:
套接字的創建:
C
int socket(int family, int type, int protocol);
功能:創建一個套接字。
參數:family
指定相應的協議族,type
指定套接字類型,protocol
爲0,(原始套接字除外)。
返回值:成功返回非負的描述符,失敗返回-1。獲取套接字選項:
C
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:獲取套接字選項
參數:sockfd
套接字描述符;level
選項所屬協議層;optname
選項名稱;optval
保存選項的緩存區;optlen
選項值長度。
返回值:成功返回0,失敗返回-1,並設置 errno。設置套接字選項:
C
int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:設置套接字選項
參數:sockfd
套接字描述符;level
選項所屬協議層;optname
選項名稱;optval
保存選項的緩存區;optlen
選項值長度。
返回值:成功返回0,失敗返回-1,並設置 errno。
相應的套接字選項及說明如下:
選項名稱 | 說明 | 數據類型 |
---|---|---|
LEVEL | SOL_SOCKET | |
SO_BROADCAST | 允許發送廣播數據報 | int |
SO_DEBUG | 使能調試跟蹤 | int |
SO_DONTROUTE | 旁路路由表查詢 | int |
SO_ERROR | 獲取待處理錯誤並消除 | int |
SO_KEEPALIVE | 週期性測試連接是否存活 | int |
SO_LINGER | 若有數據待發送則延遲關閉 | linger{} |
SO_OOBINLINE | 讓接收到的帶外數據繼續在線存放 | int |
SO_RCVBUF | 接收緩衝區大小 | int |
SO_SNDBUF | 發送緩衝區大小 | int |
SO_RCVTIMEO | 接收超時 | timeval{} |
SO_SNDTIMEO | 發送超時 | timeval{} |
SO_REUSEADDR | 允許重用本地地址 | int |
SO_REUSEPORT | 允許重用本地地址 | int |
SO_TYPE | 取得套接口類型 | int |
LEVEL | IPPROTO_IP | |
IP_HDRINCL | IP頭部包括數據 | int |
IP_OPTIONS | IP頭部選項 | 見後面說明 |
IP_TOS | 服務類型和優先權 | int |
IP_TTL | 存活時間 | int |
IP_ADD_MEMBERSHIP | 加入多播組 | ip_mreq{} |
IP_DROP_MEMBERSHIP | 離開多播組 | ip_mreq{} |
LEVEL | IPPROTO_TCP | |
TCP_KEEPALIVE | 控測對方是否存活前連接閒置秒數 | int |
TCP_MAXRT | TCP最大重傳時間 | int |
TCP_MAXSEG | TCP最大分節大小 | int |
TCP_NODELAY | 禁止Nagle算法 | int |
2. IP/端口和網絡字節序
IP 地址用來標識網絡中的一臺主機,端口用來標識主機內部的某個套接字。下面介紹幾個相關的函數:
地址格式轉換函數:
C
int inet_addr(const char *strptr);
int inet_pton(int family, const char *src, void *dst);
char* inet_ntop(int family, void *src, char *dst, size_t len);函數
inet_addr()
/inet_pton
用來要轉換的字符串轉換爲 32 二進制IP地址(網絡字節序)。函數inet_ntop
是相應的反向操作。
family
指的是地址族,用來區分 IPv4(AF_INET)和 IPv6(AF_INET6)。
inet_addr()
成功返回相應的地址,失敗返回-1;inet_pton
成功返回0,失敗返回-1;inet_ntop
成功返回 dst,失敗返回NULL。地址結構
地址信息有兩個相關的結構體:
C
struct sockaddr {
unsigned short sa_family; //地址族
char sa_data[14]; //14字節的協議地址
}
struct sockaddr_in {
shor int sin_family; //地址族
unsigned short int sin_port; //端口號
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8]; //填充0
}這兩個數據類型大小相同,通常用
sockaddr_in
來保存某個網絡地址,使用是強轉成sockaddr
。sa_family
常見值有:AF_INET
IPv4 協議AF_INET6
IPv6 協議AF_LOCAL
UNIX域協議AF_LINK
鏈路地址協議AF_KEY
密鑰套接字
網絡字節序
計算機的多字節整形存儲方式有兩種:大端(高位字節存儲在地位地址)小端(高位字節存儲在高位地址)。爲了保證網絡通信的一致性,數據以大端方式傳輸,所以需要相應的轉換函數:
C
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
二、TCP 網絡編程
TCP 嚮應用層提供可靠的面向鏈接的全雙工數據流傳輸服務。它能提供高可靠性通信(數據無誤,數據無丟失,數據無失序,數據無重複到達)。
1. 3次握手協議和兩次揮手
TCP 的面向連接指的是:當計算機雙方通信的時候必須先建立連接,然後進行數據通信,最後關閉連接。TCP 在建立連接時有三個步驟:
- 第一步 : (客戶端 -> 服務端)客戶端向服務端發送一個包含 SYN 標誌的 TCP 報文,並進入 SYN_SEND 狀態,等待確認。
- 第二步 : (服務端 -> 客戶端)服務端在收到客戶端的 SYN 報文之後,返回一個 SYN+ACK 的報文,表示服務端收到客戶端的 SYN,服務端進入 SYN_RECV 狀態。
- 第三步 : (客戶端 -> 服務端)客戶端在收到服務端的 SYN+ACK 報文之後,向確認端發送確認 ACK 報文,客戶端和服務端都進入 ESTABLISHED 狀態。
在發送方發送一個數據包之後,會啓動一個定時器,當數據包到達目的地之後,接收方會返回一個數據包,其中含有一個確認序號,如果發送方的定時器在確認信息到達之前超時,發送方會重發數據。
由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這個原則是當一方完成它的數據發送任務後就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN後仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
- 第一步 客戶端A發送一個FIN,用來關閉客戶A到服務器B的數據傳送。
- 第二步 服務器B收到這個FIN,它發回一個ACK,確認序號爲收到的序號加1。和SYN一樣,一個FIN將佔用一個序號。
- 第三步 服務器B關閉與客戶端A的連接,發送一個FIN給客戶端A。
- 第四步 客戶端A發回ACK報文確認,並將確認序號設置爲收到序號加1。
2. TCP 的數據包頭
- 源端口/目的端口:16bit,標識本地和遠端的端口號。
- 順序號:32bit,標識發送的數據包的順序。
- 確認號:32bit,希望收到的下一包數據的序列號。
- TCP頭長:4bit,表明 TCP 頭中包含多少個32bit。
- 6bit 未用。
- URG:表示緊急指針字段有效。
- ACK:置位表示確認號字段有效;
- PSH:表示當前報文需要請求推(push)操作;
- RST:置位表示復位TCP連接;
- SYN:用於建立TCP連接時同步序號;
- FIN:用於釋放TCP連接時標識發送方比特流結束。
- 窗口:16位,表示源主機在請求接收端等待確認之前需要接收的字節數。它用於流量控制。
- 校驗位:16位。用於檢查TCP數據包頭和數據的一致性。
- 緊急指針:16位。當URG碼有效時只向緊急數據字節。
- 可選項:存在時表示TCP包頭後還有另外的4字節數據。包括最大 TCP 載荷/窗口比例/選擇重發數據包等選項。
3. 服務器端 TCP 網絡編程:
TCP編程模型可分爲服務器端和客戶端兩種,一個完整的服務器端 TCP 編程模型有6步:
- 創建一個套接字
socket()
連接到指定的地址和端口
C
int bind(int sockfd, const struct sockaddr *myaddr, socket_t addrlen);
功能:將一個本地協議賦予一個套接字。
參數:sockfd
套接字描述符,myaddr
指定的地址結構的指針,addrlen
該地址結構長度。
返回值:成功返回0,失敗返回-1;設置爲監聽模式
C
int listen(int sockfd, int backlog);
功能:將一個未連接的套接字轉換爲一個被動套接字
參數:sockfd
套接字描述符,backlog
相應的套接字排隊的最大連接個數。
返回值:成功返回0,失敗返回-1;等待接收客戶端的連接請求
C
int accept(int sockfd, const struct sockaddr *cliaddr, socket_t addrlen);
功能:從已完成的鏈接隊列隊頭返回下一個已完成連接,如果隊列爲空,進程進入睡眠(默認爲阻塞模式)。
參數:sockfd
套接字描述符,cliaddr
/addrlen
用來存儲對端的地址結構和長度
返回值:成功返回非負的描述符,失敗返回-1;發送和接收數據:
send()
/recv()
C
int recvform(int sockfd, void *buff, size_t nbytes, int flags);
int sendto(int sockfd, void *buff, size_t nbytes, int flags);
功能:類似標準的read()
和write()
函數。
參數:前三個函數同read()
/write()
,flags
一般是0。
返回值:成功返回讀或寫的字節數,失敗返回-1。關閉套接字
close()
4. 客戶端的 TCP 編程模型
- 創建一個套接字:
socket()
- 建立連接
C
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socket_t addrlen);
功能:建立與 TCP 服務器的連接
參數:sockfd
套接字描述符,servaddr
/addrlen
用來存儲服務器端的地址結構和長度
返回值:成功返回0,失敗返回-1; - 發送和接收數據:
send()
/recv()
- 關閉套接字
close()
三、UDP編程模型
UDP 即用戶數據報協議,是一種面向無連接的不可靠的傳輸協議,具有消耗資源少,處理速度快的特點。
UDP 的數據包頭比較簡單,包含源和目的的地址(各16bit)8位空位,8位協議位,16位 UDP 長度。
UDP 網絡編程和 TCP 網絡編程的區別在於:UDP 是不可靠的數據報協議,客戶端不與服務器建立連接,直接使用 sendto()
函數向服務器發送數據,而服務器同樣不用接收連接,使用 recvform
返回相應的數據。
1. UDP 網絡編程模型
UDP 服務器端編程模型
- 創建一個套接字:同 TCP。
- 連接到指定的地址和端口
bind()
發送和接收數據 用
sendto()
/recvform()
C
int recvform(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
int sendto(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *to, socklen_t *addrlen);
功能:類似標準的read()
和write()
函數。
參數:前三個函數同read()
/write()
,flags
一般是0,from
/to
分別是對端的地址結構指針,addrlen
是對端的地址結構長度。
返回值:成功返回讀或寫的字節數,失敗返回-1。關閉套接字
close()
UDP 客戶端編程模型
- 創建一個套接字:同 TCP。
- 發送和接收數據 用
sendto()
/recvform()
- 關閉套接字
close()
2. 組播與廣播編程
之前我們處理的都是單播程序:一個進程就與另一個進程通信。TCP 只支持單播尋址,而 UDP 還支持其他的尋址類型。
- 單播(unicast):向標識的單獨接口遞送數據,TCP 僅支持此種
- 任播(anycast):向標識的一組接口中的一個遞送數據
- 組播(multicast):向標識的一組中的所有接口遞送數據
- 廣播(broadcast):向全體遞送數據
廣播和組播都需要使用UDP,都不能使用TCP。IPv4地址可以使用{子網id,主機id}來表示,-1表示所有位都爲1的字段,廣播可以分爲幾種:
子網定向廣播地址,{子網id,-1},指定子網上所有接口的廣播地址
192.168.1.0/24 該子網上的廣播地址192.168.1.255
受限廣播地址{-1,-1}即
255.255.255.255
。- 廣播的流程
發送廣播的流程是:
- 創建 UDP 套接字
- 指定目標地址和端口
- 設置套接字選項允許發送廣播包
C
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); - 發送廣播包
接收廣播包的流程是:
- 創建 UDP 套接字
- 綁定目標地址和端口
接收廣播包
- 廣播的缺點
- 廣播的流程
當使用單播時:
發送端UDP 套接字承載了目的 IP,比如 192.168.32.3,和目的端口 比如 7433,在傳輸層,對他冠以一個 UDP 首部,在 IPv4 中標識爲 17, 其次在網絡層,標識爲 IPv4, 確定了外出接口,接口層的以太網接口將相應的目的IP映射成 相應的以太網地址,並標識相應的幀類型爲 IPv4(0x0800)。
該數據在接口層傳輸,相應的主機接口首先判斷該幀的以太網地址是否和本機一致:不一致忽略,一致讀取整個幀,單播對非目的主機不造成任何額外開銷。目的主機接口讀取整個幀後,比較相應的幀類型 0x0800,傳遞給相應網絡層協議 IPv4,網絡層比較相應的 IP 是否和本機一致,一致後接收,隨後查看相應的協議字段 17,數據被傳送給傳輸層 UDP,UDP 查看相應的目的端口,把數據置於相應的套接字接收隊列。當使用廣播時:
發送端UDP 套接字承載了目的 IP,比如 192.168.32.255,和目的端口 比如 7433,在傳輸層,對他冠以一個 UDP 首部,在 IPv4 中標識爲 17, 其次在網絡層,標識爲 IPv4, 確定了外出接口,接口層的以太網接口將相應的目的IP映射成所在以太網的子網的以太網地址,此時這個以太網地址爲全1的地址,並標識相應的幀類型爲 IPv4(0x0800)。
該數據在接口層傳輸,該子網內所有的主機接口都會判斷該幀的以太網地址是和本機一致,讀取整個幀,單播對非目的主機不造成任何額外開銷。目的主機接口讀取整個幀後,比較相應的幀類型 0x0800,傳遞給相應網絡層協議 IPv4,網絡層比較相應的 IP 是否和本機一致,一致後接收,隨後查看相應的協議字段 17,數據被傳送給傳輸層 UDP,UDP 查看相應的目的端口,如果有相應端口的應用進程,執行相應的接收程序,如果沒有相應的進程,則 UDP 丟棄當前數據包。所以廣播存在的問題是:子網上所有使用 IP 協議的主機都必須沿相應的協議傳輸到 UDP 層判斷自己是否參加了相應的廣播。所有不使用 IP 協議的主機也都必須在接口層接收所用的數據,並在網絡層讀取判斷。
組播地址
IPv4 地址可以分爲五類:- A 類:最高位0,主機號佔 24 位,地址範圍爲:1.0.0.1 到 126.255.255.254
- B 類:最高兩位10,主機號佔 16 位,地址範圍爲:128.0.0.1 到 191.255.255.254
- C 類:最高三位110,主機號佔 8 位,地址範圍爲:192.0.1.1 到 223.255.255.254
- D 類:最高四位1110,地址範圍爲:224.0.0.1 到 239.255.255.254
- E 類:保留
其中,D類地址爲組播地址,每一個組播地址代表一個多播組。
組播的流程
發送組播的流程是:- 創建 UDP 套接字
- 指定目標地址和端口
- 發送組播包
接收組播的流程是:
- 創建 UDP 套接字
- 加入多播組
C
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); - 綁定地址和端口
- 發送組播包
四、本地套接字
套接字的引用本來就只支持本地通信,目前很多前臺後臺進程依舊使用 UNIX 域套接字進行通信,本地套接字的特點是使用簡單,效率高。本地套接字也分爲流式套接字和用戶數據報兩種類型。具體的編程方法和相應 TCP / UDP 套接字基本一致,區別僅是使用的協議和地址不同,這裏就不再詳細描述。