課程內容#
套接字是什麼?網絡編程是做什麼的?
- 了解 TCP/IP 五層模型、OSI 七層模型
- 類比
- 套接字 —— 快遞員
- 运输層 —— 快遞公司:TCP—— 某豐快遞公司,UDP—— 某通快遞公司
- 交通運輸道路 —— 因特網
- 通訊地址 ——IP
—— 运输层协议 ——#
類比快遞公司
對於開發者,只能選擇 TCP 或 UDP 協議,修改協議參數
TCP#
傳輸控制協議;面向連接,可靠的數據傳輸協議
- 連接:三次握手 [詳見附加知識點]
- 可靠的本質:確認與重傳 [需要有序號]
- 如果丟了就會重傳
- [PS] 雙方都保存一些描述雙方狀態的變量
- 頭部格式
-
- 源端口號:從哪個端口發出;目標端口號:發送到哪個端口
- 不同的端口對應不同的應用
- 如果把計算機比作一個大樓,端口號就是大樓裡的房間號
- IP 地址由 IP 層給出
- 序列號:標誌第幾次通信;確認應答號:期望對方下次通信的序列號
- 首部長度:單位為字 [一般為 4 字節]
- 功能比特位[關注標黃處]
- ACK:確認
- RST:重置連接 [拒絕下次連接]
- SYN:建立請求 [三次握手中前兩次握手會用到]
- FIN:關閉連接 [四次揮手的第一、第三次揮手會用到,同時還可以放一些數據,詳見附加知識點]
- 窗口大小:告訴對方還可以發多少數據,用來抑制對方的發送速率
- 校驗和:確認數據是否正確。如果有問題,直接銷毀,然後請求重發
- [PS]
- 設計了這麼多主要為了可靠
- 現實中的快遞公司無法達到可靠,因為運輸的物品是唯一的
UDP#
用戶數據報協議;無連接,不可靠的數據傳輸協議
- 無連接:不需要握手
- 不可靠:不管對方是否收到
- 優勢:靈活、成本低
- 頭部格式
-
- 相對 TCP,簡單得多
——socket——#
類比快遞員,但只為一個任務服務
進程和運輸層之間的接口,進程發送網絡數據必須經它交給運輸層去交付
【生與死】#
socket:創建套接字#
-
- domain:域名類型
- AF_INET,對應 ipv4 [常用]
- AF_INET6,對應 ipv6
- type:類型
- SOCK_STREAM,對應字節流 [TCP]
- SOCK_DGRAM,對應數據報 [UDP]
- protocol:協議
- domain 和 type 可能唯一確定 protocol,如 AF_INET 與 SOCK_STREAM 確定 IPPROTO_TCP
- [PS] 只能選一個的話,可用0代替
- 返回值:文件描述符
- 出錯則返回 - 1
- socket 也是一個文件,一切皆文件
close:關閉連接#
- int close(int fd);
- 四次揮手 [詳見附加知識點]
- 兩端都需要調用 close,調用方發送 FIN,接收端的 recv 的返回值為 0
【服】#
bind:綁定 IP 和端口#
只針對收數據方
-
- sockfd:文件描述符
- addr:IP 地址和端口
- 綁定 IP:可以接收來自該 IP 地址的數據 [本機]
- 若為空,則可以接收來自任意 IP 地址的數據
- 可用在內網和外網的交接處,作為防火牆使用
- 綁定端口:服務於哪個端口 [共 $2^{16}=65536$ 個端口]
- 綁定 IP:可以接收來自該 IP 地址的數據 [本機]
- addrlen:地址長度
- 返回值:成功,0;否則,-1
+ 相關結構體:sockaddr、sockaddr_in#
sockaddr
-
- sin_family:地址協議族,一般使用 AF_INET,對應 ipv4
- sa_data:同時包含 IP 地址和端口
- ❗ 使用並不方便,轉用下列更友好的方式👇,再使用 (struct sockaddr*) 強轉即可
sockaddr_in
-
- sin_port:端口號 [需要網絡字節序,見下]
- sin_addr:IP地址
- 其中,sin_addr 對應一個新的結構體in_addr
-
- 存儲 32 位無符號整型,一般使用inet_addr函數將點分十進制轉換為 in_addr 結構體:
-
- 點分十進制表示 [字符串形式] 更方便
- inet_ntoa,則反之
-
-
+ 主機字節序 & 網絡字節序#
- 主機字節序:大端、小端
- 常見為小端機,低位字節排放在內存的低地址端
- 網絡字節序:對於 4 個字節的 32bit 值,先傳輸 0~7bit,...,最後傳輸 24~31bit
- 整形字節序的轉換函數
-
- htonl:32 位主機字節序到網絡字節序的轉換
- htons:16 位主機字節序到網絡字節序的轉換
- ntohl、ntohs,則反之
-
litsen:設為監聽態#
將套接字從主動(默認)切換為被動 [首先需要 bind 綁定端口]
-
- 注意:第二個參數的真實含義是完成隊列的長度
- ① TCP 連接過程存在兩個隊列
- 未完成隊列:客戶端發送 SYN 過來,服務器回應 SYN+ACK 之後,服務器當前處於 SYN_RECV 狀態,此時的連接處在未完成隊列中
- 完成隊列:客戶端回應 ACK 之後,兩邊都處於 ESTABLISHED 狀態,此時連接從未完成隊列轉移到完成隊列中
- 👉 當服務器調用 accept 時,才將連接從完成隊列中移除
- ② 注意事項:設置合適的 backlog;服務端要儘快 accept 新的連接
- ① TCP 連接過程存在兩個隊列
accept:接受連接#
生成一個新的快遞員 [還可繼續建立多個連接]
-
- ① 傳入的 sockfd 必須經過 socket ()、bind ()、listen () 處理
- ② addr 為傳出參數,用來存儲客戶端地址
- 返回值
- 成功,則返回一個新的 sockfd,原先的 sockfd 仍可用來 accept
- 失敗,則返回 - 1
- [PS] 一般新的 scokfd 使用完畢就將其關閉;listen 狀態的 socket 不關閉
【客】#
connect:建立連接#
主動套接字,最多只能連一個
-
- 與 accept 不同的是:
- sockfd 不需要經過 bind ()、listen () 處理
- 不會返回新的 socket
⭐ connect 和 accept 是一對,分別在客戶端和服務端執行,在此期間,完成了三次握手
【傳輸】#
send:發送數據#
本質同write
-
- ❗ sendto 多傳入了 dest_addr 和 addrlen,它是用於 UDP 的
- 因為沒有建立連接,從而需要指定目的 IP 和端口
- flag 一般置為 0
recv:接收數據#
本質同read
-
- 當對方斷開時,返回值為 0
- ❗ recvfrom 多傳入了 src_addr 和 addrlen,它是用於 UDP 的
- src_addr 存儲發送數據端的地址信息
- 默認是阻塞的
—— 附加 ——#
kill#
給一個進程發送信號
- man 2 kill
- 原型
- 基於進程 ID 和信號位掩碼
- 描述
- 設置 pid 有各種形式
- 均需要存在和權限檢查
- 返回值
- 0,成功;-1,出錯
- kill -l 查看信號列表
-
- 64 種信號
-
signal#
信號的處理方式
- man signal
- 原型
- 需要定義一個 sighandler_t 類型的函數
- 描述
- 其行為會隨 UNIX 版本變化
- handler 有三種類型:忽略、默認、自定義
- 自定義類型涉及涉及捕鼠器原理:夾住一個老鼠的時候,後面一個老鼠可能被丟失
- 需要重新設置 [由系統操作]
- 返回值
- 根據 handler 而定
代碼演示#
服務端#
tcp_server.h
-
- 在指定端口上創建一個處於監聽狀態的快遞員
tcp_server.c
-
- 參照序號閱讀
- 注意:head.h 中添加 socket 相關的頭文件,可在 man 手冊中查找,這裡不贅述
1.server.c
-
-
- accept 可以獲取客戶端的地址信息
- 創建子進程單獨用於傳輸數據
- 每一步都要注意有錯誤檢測
- + 斷開連接情況(FIN,recv 返回為 0)的處理
- 收發策略不同
- 有多少發多少,能收多少收多少
- send 使用 strlen,recv 使用 sizeof
客戶端#
tcp_client.h
-
- 主動連接指定 IP [點分十進制的 ipv4 字符串] 和端口
tcp_client.c
-
- 填表基於輸入
1.client.c
-
-
- 加入了對信號的捕捉
- bzero 的使用,初始化 buff 變量
效果展示#
- 左:服務端,右:客戶端 [可多用戶]
- 建立連接、地址捕獲、數據傳輸、斷開連接
- 使用 netstat 可查看端口的監聽狀態
-
- 添加 - alnt 選項
-
- [PS] 需要在雲主機的控制台 —— 安全組中開放端口 8888
附加知識點#
- IP:公共的地址服務,盡力而為交付服務。另一層含義,其不可靠 [有可能出車禍]
三次握手、四次揮手#
-
- 三次握手 [SYN、ACK]
-
- 第一次握手:客戶端發送 SYN 包到服務器 [客戶端進入 SYN_SEND 狀態,等待服務器確認]
- 第二次握手:服務器收到,必須確認客戶端,設置一個 ACK,同時自己也設置一個 SYN,即 SYN+ACK 包 [服務端從 LISTEN 進入 SYN_RECV 狀態]
- 第三次握手:客戶端收到服務器的 SYN+ACK 包,向服務器發送 ACK 確認包,發送完畢後,客戶端進入 ESTABLISHED 狀態,服務器收到 ACK 後也進入 ESTABLISHED 狀態
- 注意:每次的 ACK 序號,在需要確認的包的序號上加一,表示確認
-
- 四次揮手 [FIN、ACK]
-
- 第一次揮手:假設客戶端想要關閉連接,客戶端發送一個 FIN 包,表示自己已經沒有數據可以發送了 [此時仍然可以接收數據][客戶端進入 FIN_WAIT_1 狀態]
- 第二次揮手:服務端回覆一個 ACK 包,表明自己接收到了客戶端關閉連接的請求,但自己還需要做些準備來關閉連接 [服務端進入 CLOSE_WAIT 狀態]
- 客戶端接收到這個 ACK 後,進入 FIN_WAIT_2 狀態,等待服務端關閉連接
- 第三次揮手:服務端準備好關閉連接時,向客戶端再發送 FIN [服務端進入 LAST_ACK 狀態,等待客戶端的確認]
- 第四次揮手:客戶端接收到來自服務器端的關閉請求,發送一個 ACK 包 [客戶端進入 TIME_WAIT 狀態,為可能出現的超時重傳的 FIN 包,等待2 個 MSL時間]
- 服務端接收到這個 ACK 之後,關閉連接,進入 CLOSED 狀態
- 客戶端等待了2 個 MSL後,如果沒有收到服務端的 FIN,則認為服務端已經正常關閉連接,於是自己也關閉連接,進入 CLOSED 狀態;否則,再次發送 ACK
-
- 參考三次握手與四次揮手—— 博客 [注:第四次揮手客戶端等待的是超時重傳的 FIN 而不是 ACK]
附加:2 個 MSL 的含義#
TIME_WAIT 是如何引起的,有什麼作用,在編程時有什麼弊端,怎麼解決?
- 引起原因:TCP 的四次揮手時,已經完成前三次揮手,在第四次揮手時,客戶端收到來自服務端的 FIN,它在發送一個 ACK 後,就會進入 TIME_WAIT 狀態
- 此時客戶端需要等待兩個最大數據段生命周期(Maximum segment lifetime,MSL)的時間之後,才會進入 CLOSED 狀態
- 存在原因
- ①阻止延遲數據段
- 每一個 TCP 數據段都包含唯一的序列號,這個序列號能夠保證 TCP 協議的可靠
- 為了保證新 TCP 連接的數據段不會與還在網絡中傳輸的歷史連接的數據段重複,TCP 連接在分配新的序列號之前需要至少靜默數據段在網絡中能夠存活的最長時間,即 MSL
- 從而防止延遲的數據段被其他使用相同源地址、源端口、目的地址以及目的端口的 TCP 連接收到
- ②保證連接關閉
- 如果客戶端等待的時間不夠長,當服務端還沒有收到 ACK 消息,而客戶端重新與服務端建立 TCP 連接時,會發生:
- 服務端因為沒有收到 ACK 消息,所以仍然認為當前連接是合法的
- 客戶端重新發送 SYN 消息請求握手時,會收到服務端的 RST 消息,連接建立的過程被終止
- 所以要保證 TCP 連接的遠程被正確關閉,即等待被動關閉連接的一方收到 FIN 對應的 ACK 消息
- 如果客戶端等待的時間不夠長,當服務端還沒有收到 ACK 消息,而客戶端重新與服務端建立 TCP 連接時,會發生:
- ①阻止延遲數據段
- 編程影響
- 對於高並發的場景容易出現過多的 TIME_WAIT
- 而 MSL 的時長一般是 60s,這是難以接受的,可能一個 TCP 連接只為了通信幾秒鐘,但 TIME_WAIT 就需要等待 2 分鐘
- 解決方式
- 基於一個時間戳變量,記錄發送數據包、最近一次接收數據包的時間
- 然後配合兩個參數
- reuse:允許主動關閉連接的一方,再次向對方發起連接的時候,復用處於 TIME_WAIT 狀態的連接
- recycle:內核會快速回收處於 TIME_WAIT 的連接,只需等待 RTO 時間 [數據包重傳的超時時間]
- 參考
C 語言下的 socket 編程#
- 服務端:socket、sockaddr [_in]、bind、listen;accept、send/recv;close
- 客戶端:socket、sockaddr [_in]、connect;send/recv;close
-
- 基於 TCP 的流式套接字、基於 UDP 的數據報套接字
- UDP 服務端也需要 bind IP 與端口,但不需要 listen,使用 sendto、recvfrom 來發、收信息
- sockaddr [_in]:保存 socket 信息的結構體,使用 [_in] 填寫信息,再轉換為 sockaddr
- 服務端需要兩個套接字,一個用來監聽,一個用來接收客戶端 connect 發送的套接字
輸入 kaikeba.com 並按下回車#
-> 到 TCP 建立連接,本地發送第一個 request 報文 -> 到收到第一個 request 報文為止,發生了哪些事情?
- [宏觀層面] DNS👉TCP 連接 [應用層、傳輸層、網絡層、數據鏈路層]👉服務器處理請求👉返回響應結果
- DNS
- 本地 hosts、本地 DNS 解析器緩存
- 本地 DNS
- 迭代 / 遞歸:根 DNS 服務器,頂級 DNS,權威 DNS
- 直到找到域名對應的 IP
- TCP 連接
- 應用層:發送 HTTP 請求 —— 請求方法、URL、HTTP 版本
- 傳輸層:與服務器進行三次握手
- 網路層:ARP 協議查詢 IP 對應的 MAC 地址,如果在一個局域網內,就直接根據 MAC 地址發送請求;否則使用路由表,查找下一個跳轉的地址,再訪問對應的 MAC 地址
- 數據鏈路層:以太網協議
- 廣播:向一個局域網中的所有機器發送請求,比較 MAC 地址
- Web 服務器
- 解析用戶請求,知道了需要調度哪些資源文件,並調用數據庫信息,返回給瀏覽器客戶端
- 返回響應結果
- 一般會有一個 HTTP 狀態碼,比如 200、301、404 等,通過這個狀態碼我們可以知道服務器端的處理是否正常,並能了解具體的錯誤
- ⭐ 推薦視頻:TCP-IP Explained (2000)——Youtube
- [主要從 IP 層展開]
- 涉及對象:TCP 包、ICMP Ping 包、UDP 包、死亡之 Ping、路由器、路由器交換機...
- 大致流程
- 本地:封裝包、本地傳輸、本地路由器選擇、交換機選擇、代理檢查、防火牆檢查、本地傳輸、路由器選擇
- ——> 網絡傳輸 ——>
- 響應端:防火牆檢查 [監管端口]、代理檢查請求包、返回相應信息給請求端、同上述本地過程 [封裝包、...、路由器選擇]
端口復用相關#
一個端口可以同時綁定不同的服務嗎?
- 可以。接收數據時,根據五元組 {傳輸協議,源 IP,源端口,目的 IP,目的端口} 判斷數據屬性
- 例如:
- 使用 TCP 和 UDP 傳輸協議監聽同一個端口後,接收數據互不影響,不衝突
- 同樣,accept 產生新的 socket,還是用的同一個端口
- 產生了多個不同的 socket,這些 socket 裡包含的目的 IP 和端口是不變的,變化的只是源 IP 和端口 [端口復用]
- [PS] TCP 類型 socket 只給 TCP 類型發數據
父子進程的 socket 關係#
父進程克隆出的子進程裡的 socket 和父進程的 socket 的關係
- 是同一個,對應同一個文件
- 當有數據到來時,兩個進程誰先收到數據則誰有該數據,另一個進程繼續等待
- 所以一般地,子進程不需要的資源就不要繼承,如:可使用 close 直接關閉子進程中繼承自父進程的 socket
Tips#
- 系統 / 網絡編程要考慮所有可能出錯的地方
- 信號知識擴展:實現自己的 sleep 函數
- 編譯時要記得考慮所有相關的源文件 [*.c]