一、 網絡編程概述
1.1 概述
在現代軟件開發與系統交互場景里,基于?Socket?的網絡多進程通信占據核心地位,其適用場景廣泛且深入到各類數字化交互中:
- 直播場景:主播端通過 Socket 建立的網絡連接,將音視頻流以數據包形式,依托 TCP 或 UDP 協議(依據場景選擇,如低延遲需求可能用 UDP 結合糾錯策略 ),實時傳輸到平臺服務器,再由服務器通過多進程通信分發給百萬級觀眾端,實現直播內容的跨地域傳播。
- 文件發送與資源下載:發送端把文件拆分為網絡數據包,借由 TCP 可靠傳輸(保障文件完整性 ),按序發送給接收端;資源下載同理,客戶端與服務器通過 Socket 握手建立連接,以流式傳輸逐步獲取資源,像瀏覽器下載軟件安裝包,背后就是基于 Socket 網絡通信的文件傳輸邏輯。
- 即時聊天:聊天消息(文字、表情、語音片段 )作為字符串或二進制數據,通過網絡載體發送,例如 QQ、微信等應用,利用自定義協議封裝消息,基于 TCP 保證消息不丟包、不亂序,讓雙方主機能解析還原對話內容,實現實時交互。
????????對于?IOT(物聯網)?,智能家居是典型場景:智能空調基于 TCP/UDP 協議,與家庭網關建立連接,采集室內溫度(數據上發 ),同時接收手機 APP 發送的 MQTT 協議指令(如下發 “設置 26℃” ),完成制冷溫度調節(下發控制 ),依托現有網絡實現設備互聯與智能控制。本質上,就是把物理設備數據與控制指令,通過網絡協議轉化為可傳輸、解析的信息,打通 “設備 - 網絡 - 控制端” 鏈路 。
1.2 計算機網絡發展
起源背景與 ARPA 成立
1957 年蘇聯發射 Sputnik(斯普特尼克)人造衛星,這一事件觸動美國對軍事、科技主導權的擔憂 —— 若蘇聯憑借衛星實現通信、情報優勢,美國將陷入被動。因此,1958 年 1 月 7 日,美國緊急撥款成立?ARPA(Advanced Research Projects Agency,美國高級研究計劃署 )?,核心目標是突破蘇聯可能的技術封鎖,研發先進網絡、通信技術,為軍事及科研提供支撐,后續 ARPA 推動的網絡研究,成為現代計算機網絡的重要起源 。
計算機網絡核心需求(區別傳統通信 )
- 非語音優先:傳統電話網聚焦語音通話,而計算機網絡設計初衷是?數據傳輸與多設備協同?,比如科研計算數據共享、軍事信息交互,不局限于 “通話” 單一功能,更強調多類型數據(文件、指令、傳感器數據等 )的傳遞 。
- 簡單可靠:早期網絡用于科研、軍事,需在復雜環境(如跨地域、惡劣條件 )下穩定運行,所以追求?結構簡潔、傳輸容錯性強?。例如,即便部分節點故障,仍能通過冗余路由傳遞數據,保障整體網絡不癱瘓 。
- 異構設備兼容:要連接不同廠商、架構的計算機(如早期大型機、小型機 ),讓 IBM 主機與 DEC 終端能互傳數據,就得突破硬件、系統差異,制定通用通信規則,這也是網絡協議誕生的驅動力之一 。
- 節點平等與路由冗余:網絡里沒有 “中心主機優先”,所有節點(如實驗室的計算終端、軍事監測設備 )地位等同?,保障數據交互的去中心化;同時,“冗余路由” 是為了避免單點故障,像軍事網絡,若一條通信鏈路被破壞,能自動切換到備用鏈路,確保信息持續傳輸 。
- 唯一標識(IP 地址 ):網絡里每臺設備必須有?獨一無二的 “身份”?,才能精準收發數據,這就是 IP 地址的核心作用。
IP 地址體系(IPv4 & IPv6 )
- IPv4:采用?32 位二進制?表示地址(如?
192.168.1.1
?,轉換為二進制是 32 個 0/1 組合 ),分成 4 個 8 位段(即 “點分十進制” ),每個段取值?0 - 255
?。但由于互聯網早期分配策略與地址消耗速度,IPv4 資源極度稀缺?—— 中國僅拿到?156.x.x.x
?、188.x.x.x
?等少量 “高級資源段”(實際是 A 類、B 類地址中的部分分配 ),導致局域網常用?NAT(網絡地址轉換 )?技術,讓多設備共享一個公網 IPv4,通過 “區域唯一 IP + 多級路由”(如家庭路由器分配?192.168.xxx
?內網地址,再映射到公網 IP )緩解地址不足問題 。 - IPv6:為解決 IPv4 枯竭,推出?128 位二進制?地址(格式如?
2001:0db8:85a3:0000:0000:8a2e:0370:7334
?),地址空間從 IPv4 的約 43 億個,暴增至近乎 “無限”(理論上約?\(2^{128}\)?個 ),能為全球設備(包括未來海量物聯網終端 )分配獨立公網地址,徹底解決地址短缺,推動萬物互聯更高效落地 。
1.3 TCP/IP & UDP 協議
協議本質與應用邏輯
基于?Socket(套接字 )?,TCP/IP 和 UDP 是網絡數據傳輸的 “規則框架”,讓不同設備(如手機與服務器、智能手表與云端 )能跨網絡通信。兩者都以?“報文頭 + 數據體”?形式封裝信息:
- “報文頭”?類似快遞面單:包含 “消息類型”(如 TCP 的連接標識、UDP 的端口號 )、源 IP、目的 IP 等,告訴網絡 “數據從哪來、到哪去、按什么規則傳” ;
- “數據體”?是實際內容:可以是聊天消息、文件片段、傳感器數值等,依托 “面單(報文頭 )” 的指引,在網絡中流轉 。
TCP/IP vs UDP 核心差異(以傳輸文件、直播為例 )
- TCP(傳輸控制協議 ):
- 可靠傳輸:通過 “三次握手” 建立連接(確保雙方收發能力正常 )、“確認應答”(發出去的數據,對方必須回傳確認,丟包就重發 )、“擁塞控制”(網絡擁堵時自動降低發送速度 ),像?傳輸重要文件(如合同、安裝包 )?,必須保證數據完整,就依賴 TCP 。缺點是?傳輸延遲高?,不適合對實時性要求極致的場景 。
- UDP(用戶數據報協議 ):
- 無連接、低延遲:無需提前握手,直接發數據,省去連接耗時,像?直播推流、游戲實時交互?(如 FPS 游戲里的子彈射擊指令 ),追求 “瞬間響應” ,哪怕偶爾丟包(畫面卡頓一下、游戲子彈偶爾沒同步 ),也優先保證實時性。但缺點是?不可靠?,數據發出去不保證對方收到,需應用層自己處理丟包、亂序問題 。
網絡分層模型(OSI 七層 & TCP/IP 四層 )
- OSI 七層結構(理論參考 ):從下到上為?物理層(網線、光纖傳輸電信號 / 光信號 )→ 數據鏈路層(以太網協議,處理網卡收發包 )→ 網絡層(IP 協議,負責尋址、路由 )→ 傳輸層(TCP/UDP,負責端到端數據可靠 / 快速傳輸 )→ 會話層(管理應用程序間的連接會話 )→ 表示層(數據加密、壓縮、格式轉換 )→ 應用層(HTTP、FTP 等具體應用協議 )?。它是理想的 “標準化分層”,便于理解網絡各環節職責,但實際應用中較難嚴格分層實現 。
- TCP/IP 四層結構(實際主流 ):簡化為?網絡接口層(對應 OSI 物理層 + 數據鏈路層,處理硬件、鏈路通信 )→ 網絡層(IP 協議,管理地址、路由 )→ 傳輸層(TCP/UDP,負責端到端傳輸 )→ 應用層(HTTP、MQTT 等應用協議,直接對應用戶功能 )?。更貼合實際開發與協議設計,比如我們寫代碼調用 HTTP(應用層 ),底層自動通過 TCP(傳輸層 )、IP(網絡層 )等完成通信,無需關注七層細節 。
組包和解包:
1.4 小端字節序和大選字節序
二、UDP編程
1. UDP 協議概述
- 無連接特性:UDP 協議面向【無連接】,通信前無需像 TCP 那樣建立連接,直接發送數據報,降低通信初始開銷 。
- 角色平等性:不區分客戶端和服務器,僅有發送端和接收端概念,雙方都可主動發數據,靈活適用于廣播、組播場景 。
- 傳輸可靠性:因無連接,數據傳輸無確認、重傳機制,存在丟包、亂序風險,數據安全性依賴應用層保障 。
- 傳輸效率:無需連接管理、確認等額外流程,數據傳遞速度較快 。
- 典型應用場景:適用于廣播、組播,以及對數據完整性要求不絕對嚴格的場景,如直播(容忍偶爾丟包不影響觀看流暢性 )、網絡游戲(優先保證實時交互,輕微數據丟失可通過游戲邏輯彌補 )。
2. 本地數據轉網絡相關函數
2.1?htons
:本地 16 位數據轉網絡數據【小轉大】
- 函數原型:
#include <arpa/inet.h> uint16_t htons(uint16_t hostshort);
- 功能:將無符號?
short
?類型本地小端字節序數據(如主機存儲的端口號 ),轉換為網絡大端字節序,適配網絡傳輸要求(網絡協議通常以大端序解析數據 ),常用于【端口號小轉大】 。 - 參數:
uint16_t hostshort
?,uint16_t
?是無符號?short
?類型(16 位二進制位 ),參數內容為本地主機 16 位二進制數據(如端口號實際值 )。 - 返回值:轉換后符合網絡大端字節序的 16 位無符號?
short
?類型數據 。
2.2?htonl
:本地 32 位數據轉網絡數據【小轉大】
- 函數原型:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong);
- 功能:把無符號?
int
?類型本地小端字節序數據(如 32 位 IP 地址 ),轉為網絡大端字節序,常用于【32 位?int
?類型 IP 地址小轉大】 。 - 參數:
uint32_t hostlong
?,uint32_t
?是無符號?int
?類型(32 位二進制位 ),參數為本地主機 32 位二進制數據(如 IP 地址實際值 )。 - 返回值:轉換后符合網絡大端字節序的 32 位無符號?
int
?類型數據 。
自定義實現示例(補充代碼邏輯 )
以下是手動實現?my_htons
、my_htonl
?及測試函數的代碼,展示字節序轉換邏輯:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>uint16_t my_htons(uint16_t host_value) {uint16_t net_value = 0;// 小端轉大端:交換高低 8 位net_value |= (host_value << 8) | (host_value >> 8);return net_value;
}uint32_t my_htonl(uint32_t host_value) {uint32_t net_value = 0;// 小端轉大端:重新排列 4 個字節順序net_value |= (host_value & 0xFF) << 24;net_value |= ((host_value >> 8) & 0xFF) << 16;net_value |= ((host_value >> 16) & 0xFF) << 8;net_value |= (host_value >> 24);return net_value;
}void test_htons(void) {uint16_t host_value = 7385;uint16_t net_value = htons(host_value);printf("htons net_value : %d\n", net_value);net_value = my_htons(host_value);printf("my_htons net_value : %d\n", net_value);
}void test_htonl(void) {uint32_t host_value = 0xC0A80D48; // 示例 IP 地址轉換前值uint32_t net_value = htonl(host_value);printf("htonl net_value : %d\n", net_value);net_value = my_htonl(host_value);printf("my_htonl net_value : %d\n", net_value);
}int main(int argc, char const *argv[]) {test_htons();test_htonl();return 0;
}
- 邏輯說明:
my_htons
?通過位移操作,交換 16 位數據的高低 8 位,實現小端到網絡大端(大端通常等價于 “高字節在前” )轉換 。my_htonl
?拆分 32 位數據的 4 個字節,重新按大端序(高字節對應高位 )拼接,完成小端到網絡大端轉換 。
2.3?ntohs
:網絡 16 位數據轉本地數據【大轉小】
- 函數原型:
#include <arpa/inet.h> uint16_t ntohs(uint16_t netshort);
- 功能:將無符號?
short
?類型網絡大端字節序數據(如網絡接收的端口號 ),轉換為本地小端字節序,適配主機存儲解析,常用于【端口號大轉小】 。 - 參數:
uint16_t netshort
?,表示網絡大端序的 16 位無符號?short
?類型數據(如網絡傳來的端口號 )。 - 返回值:轉換后符合本地小端字節序的 16 位無符號?
short
?類型數據 。
2.4?ntohl
:網絡 32 位數據轉本地數據【大轉小】
- 函數原型:
#include <arpa/inet.h> uint32_t ntohl(uint32_t netlong);
- 功能:把無符號?
int
?類型網絡大端字節序數據(如網絡接收的 32 位 IP 地址 ),轉為本地小端字節序,常用于【32 位?int
?類型 IP 地址大轉小】 。 - 參數:
uint32_t netlong
?,表示網絡大端序的 32 位無符號?int
?類型數據(如網絡傳來的 IP 地址 )。 - 返回值:轉換后符合本地小端字節序的 32 位無符號?
int
?類型數據 。
3. IPv4 地址相關操作
3.1 IPv4 地址描述
IPv4 地址有兩種表示形式:
- 點分十進制字符串:如?
192.168.13.72
?,直觀易讀,要求字符串長度最小 16 字節(保障數據完整性 ),用于人機交互展示 。 - 4 字節?
int
?類型數值:網絡傳輸、程序內部操作時常用,以緊湊二進制形式存儲,減少數據量,提升傳輸、處理效率 。
3.2 點分十進制 IP 轉網絡 4 字節?int
?類型 IP 地址
- 函數?
inet_pton
:- 原型:
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst);
- 功能:將【點分十進制】IPv4/IPv6 字符串(如?
192.168.13.20
?),轉換為對應協議的 32 位(IPv4 )或 128 位(IPv6 )整數 IP 描述形式,存入指定內存 。 - 參數:
int af
:協議族,AF_INET
?對應 IPv4,AF_INET6
?對應 IPv6 。const char *src
:點分十進制格式的 IP 地址字符串(如?192.168.13.20
?)。void *dst
:存儲轉換后 IP 地址二進制數據的內存地址(如?uint32_t
?變量地址,用于存 IPv4 轉換結果 )。
- 返回值:轉換成功返回?
1
;地址族不支持返回?0
;輸入無效(非合法 IP 格式 )返回?-1
,并設置?errno
?為?EINVAL
?。
- 原型:
代碼示例(inet_pton
?應用 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main(int argc, char const *argv[]) {// 點分十進制 IPv4 地址char *str = "192.168.13.72"; // 存儲轉換后的 4 字節 int 類型 IP 數據uint32_t ip_value = 0; // 調用 inet_pton 轉換,AF_INET 表示 IPv4 協議int ret = inet_pton(AF_INET, str, &ip_value); if (ret != 1) {perror("inet_pton error!");exit(EXIT_FAILURE);}// 轉換后為網絡大端序 4 字節 IP 數據,符合網絡傳輸要求printf("ip_value : %d\n", ip_value); return 0;
}
- 說明:
192.168.13.72
?轉換后,ip_value
?存儲大端序二進制數據(如示例中對應?1208854720
?),可用于網絡通信的 IP 地址設置 。
3.3 網絡 4 字節?int
?類型 IP 地址轉點分十進制 IP 地址
- 函數?
inet_ntop
:- 原型:
#include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 功能:將 32 位(IPv4 )或 128 位(IPv6 )整數形式的 IP 地址,按協議要求轉換為【點分十進制】(IPv4 )或對應格式(IPv6 )的字符串,支持 IPv4、IPv6 。
- 參數:
int af
:協議族(AF_INET
?對應 IPv4,AF_INET6
?對應 IPv6 )。const void *src
:存儲 IP 地址二進制數據的內存地址(如?uint32_t
?變量地址,存 IPv4 大端序數據 )。char *dst
:存儲轉換后點分十進制字符串的緩沖區,IPv4 要求最小 16 字節空間 。socklen_t size
:dst
?緩沖區的字節長度 。
- 返回值:轉換成功返回指向?
dst
?的指針(即點分十進制字符串首地址 );失敗返回?NULL
?。
- 原型:
代碼示例(inet_ntop
?應用 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main(int argc, char const *argv[]) {// 網絡大端序 4 字節 IP 數據(對應 192.168.13.72 轉換后的值 )uint32_t net_ip = 1208854720; // 存儲點分十進制 IP 的緩沖區char ip_addr[16] = ""; // 調用 inet_ntop 轉換,AF_INET 表示 IPv4 協議const char *ip = inet_ntop(AF_INET, &net_ip, ip_addr, 16); if (NULL == ip) {perror("inet_ntop error!");exit(EXIT_FAILURE);}printf("ip_addr : %s\n", ip_addr);printf("ip : %s\n", ip); return 0;
}
- 說明:
net_ip
?是網絡大端序的 4 字節 IP 數據,經?inet_ntop
?轉換后,ip_addr
?存儲點分十進制字符串(如?192.168.13.72
?),實現 “網絡二進制 IP” 到 “可讀字符串” 的轉換 。
3.4 組播
1. 什么是組播?
組播是一種點對多的網絡通信方式:
- 發送端將數據發送到一個特定的「組播地址」,只有主動加入該組的接收端才能收到數據。
- 與廣播(所有設備都能收到)相比,組播更高效(僅目標組接收);與單播(一對一)相比,組播節省帶寬(一份數據同時發給多個接收端)。
2. 組播的核心特點
- 組播地址:使用 IPv4 的?D 類地址(范圍:
224.0.0.0
?~?239.255.255.255
),專門用于標識組播組。
- 例如:
224.0.1.1
(通用組播地址)、239.255.255.250
(SSDP 協議常用)。- 組管理:依賴?IGMP 協議(互聯網組管理協議),接收端通過 IGMP 向路由器聲明「加入 / 離開組播組」,路由器據此轉發組播數據。
- 無連接:基于 UDP 協議(無可靠性保證,但實時性好)。
3. 組播與廣播的區別
特性 組播 廣播 接收范圍 僅加入組播組的設備 局域網內所有設備 地址類型 D 類地址(224.0.0.0~239.255.255.255) 受限廣播(255.255.255.255)或子網廣播 路由支持 可跨網段(需路由器支持) 通常被路由器阻止(僅限本地子網) 效率 高(按需接收) 低(無用數據多) 4. 組播通信流程
- 發送端:創建 UDP 套接字,將數據發送到指定的組播地址和端口(無需加入組)。
- 接收端:
- 創建 UDP 套接字,綁定到組播端口。
- 通過?
setsockopt
?調用?IP_ADD_MEMBERSHIP
?選項,加入目標組播組。- 從套接字接收組播數據。
5. 組播地址與公網通信的關系
(1)組播地址是否為 “公網 IP”?
組播地址不是傳統意義上的 “公網 IP”(公網 IP 通常指互聯網中可唯一路由的單播地址)。它是專門為組播設計的特殊地址,僅用于標識 “組播組”,不對應具體的物理設備或網絡接口。
?根據用途,組播地址可細分為:
- 本地鏈路組播(224.0.0.0 ~ 224.0.0.255):僅在本地局域網內有效,不會被路由器轉發(如?
224.0.0.1
?表示本地子網所有主機)。- 全球組播(224.0.1.0 ~ 238.255.255.255):理論上可在公網中路由,用于跨網絡組播(需網絡設備支持)。
- 管理權限組播(239.0.0.0 ~ 239.255.255.255):類似私網 IP,僅在特定管理域內使用,不允許跨公網路由。
(2)跨國主機能否通過同一組播組通信?
- 理論上:若兩國主機加入同一全球組播地址(如?
224.0.1.100
),且中間網絡路徑(路由器、交換機)均支持組播路由協議(如 PIM、IGMPv3),則可能收到數據。- 實際中:幾乎無法實現,原因包括:
- 多數公網運營商(ISP)為簡化路由、節省帶寬,不支持全局組播路由,甚至屏蔽組播數據包。
- 跨地域組播路由配置復雜,易引發網絡風暴(冗余數據泛濫)。
- 部分國家 / 地區對跨網組播有管控,以保障網絡安全。
(3)替代方案
若需跨地域 “一對多” 通信,更常用的方案是:
?
- 單播 + 服務器轉發(如即時通訊群聊,由服務器向多終端單播轉發);
- P2P 協議(節點間直接通信,無需依賴組播)。
(4)具體代碼示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define MULTICAST_GROUP "239.0.0.1" // 組播地址(D類地址) #define MULTICAST_PORT 9000 // 組播端口 #define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in local_addr; // 本地地址(綁定用)struct ip_mreq mreq; // 組播組信息(用于加入組)char buffer[BUFFER_SIZE];// 1. 創建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket創建失敗");exit(EXIT_FAILURE);}// 2. 配置本地地址(綁定到組播端口)memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口local_addr.sin_port = htons(MULTICAST_PORT); // 綁定到組播端口// 3. 綁定套接字(必須綁定端口才能接收組播數據)if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {perror("綁定失敗");close(sockfd);exit(EXIT_FAILURE);}// 4. 配置組播組信息,準備加入組mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP); // 組播地址mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本地任意接口// 5. 加入組播組(關鍵步驟)if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {perror("加入組播組失敗");close(sockfd);exit(EXIT_FAILURE);}printf("已加入組播組 %s:%d,等待接收數據...\n", MULTICAST_GROUP, MULTICAST_PORT);// 6. 循環接收組播數據while (1) {socklen_t addr_len = sizeof(local_addr);ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE-1, 0,(struct sockaddr*)&local_addr, &addr_len);if (recv_len < 0) {perror("接收數據失敗");break;}buffer[recv_len] = '\0';printf("收到組播數據:%s\n", buffer);}// 7. 離開組播組(可選)setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));close(sockfd);return 0; }
3.5 廣播
接收端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define BROADCAST_PORT 8888
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in local_addr, sender_addr;socklen_t sender_len = sizeof(sender_addr);char buffer[BUFFER_SIZE];// 創建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket創建失敗");exit(EXIT_FAILURE);}// 配置本地地址memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口local_addr.sin_port = htons(BROADCAST_PORT);// 綁定套接字到本地地址和端口if (bind(sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {perror("綁定失敗");close(sockfd);exit(EXIT_FAILURE);}printf("等待接收廣播消息...\n");// 接收廣播消息ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,(struct sockaddr *)&sender_addr, &sender_len);if (recv_len < 0) {perror("接收消息失敗");close(sockfd);exit(EXIT_FAILURE);}buffer[recv_len] = '\0';printf("從 %s:%d 收到消息: %s\n", inet_ntoa(sender_addr.sin_addr),ntohs(sender_addr.sin_port),buffer);// 關閉套接字close(sockfd);return 0;
}
發送端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define BROADCAST_PORT 8888
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in broadcast_addr;char buffer[BUFFER_SIZE] = "這是一條UDP廣播消息";int broadcast_enable = 1;// 創建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket創建失敗");exit(EXIT_FAILURE);}// 設置廣播選項if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable)) < 0) {perror("設置廣播選項失敗");close(sockfd);exit(EXIT_FAILURE);}// 配置廣播地址memset(&broadcast_addr, 0, sizeof(broadcast_addr));broadcast_addr.sin_family = AF_INET;broadcast_addr.sin_port = htons(BROADCAST_PORT);// 設置廣播地址為255.255.255.255if (inet_pton(AF_INET, "255.255.255.255", &broadcast_addr.sin_addr) <= 0) {perror("無效的廣播地址");close(sockfd);exit(EXIT_FAILURE);}// 發送廣播消息printf("發送廣播消息: %s\n", buffer);if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr)) < 0) {perror("發送消息失敗");close(sockfd);exit(EXIT_FAILURE);}// 關閉套接字close(sockfd);return 0;
}
4. UDP 編程
4.1 UDP 編程流程
-
UDP 發送端:
socket()
?→?sendto()
?→?close()
socket()
:創建網絡通信套接字,建立通信 “通道” 。sendto()
:指定目標地址、端口,發送 UDP 數據報 。close()
:關閉套接字,釋放資源 。
-
UDP 接收端:
socket()
?→?bind()
?→?recvfrom()
?→?close()
socket()
:創建套接字 。bind()
:綁定本地 IP、端口,讓套接字監聽指定地址的數據 。recvfrom()
:阻塞等待接收 UDP 數據報,同時獲取發送端地址信息 。close()
:關閉套接字 。
4.2 UDP 編程相關函數
4.2.1?socket
:申請創建 Socket 套接字
- 函數原型:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
- 功能:創建網絡通信套接字,返回對應文件描述符,作為后續網絡操作(收發數據等 )的標識 。
- 參數:
int domain
:協議族,如?AF_INET
(IPv4 協議 )、AF_INET6
(IPv6 協議 )。int type
:套接字類型,SOCK_DGRAM
?表示 UDP 套接字(無連接、基于數據報 );SOCK_STREAM
?表示 TCP 套接字(面向連接、基于流 );SOCK_RAW
?表示原始套接字(可直接操作網絡層協議 )。int protocol
:協議類型,IPPROTO_UDP
?表示 UDP 協議;IPPROTO_TCP
?表示 TCP 協議 。
- 返回值:成功返回套接字文件描述符(非負整數 );失敗返回?
-1
?。
代碼示例(socket
?創建 UDP 套接字 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>int main(int argc, char const *argv[]) {// 存儲套接字文件描述符int socket_fd = -1; // 創建 UDP 套接字:AF_INET(IPv4 ) + SOCK_DGRAM(UDP 類型 ) + IPPROTO_UDP(UDP 協議 )socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (-1 == socket_fd) {perror("socket error!");exit(EXIT_FAILURE);}printf("socket_fd : %d\n", socket_fd); getchar(); close(socket_fd); return 0;
}
補充說明:struct sockaddr
?和?struct sockaddr_in
?結構體
-
struct sockaddr
:通用套接字地址結構體,系統定義的標準格式,用于兼容不同協議地址:struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET )char sa_data[14]; // 協議地址(14 字節,存具體地址、端口等 ) };
-
struct sockaddr_in
:針對 IPv4 協議的地址結構體,更直觀描述 IPv4 通信地址:struct sockaddr_in {sa_family_t sin_family; // 地址族(固定為 AF_INET )in_port_t sin_port; // 16 位端口號(網絡字節序 )struct in_addr sin_addr; // 32 位 IPv4 地址(網絡字節序 )char sin_zero[8]; // 填充字段,使結構體長度與 sockaddr 一致 };
struct in_addr {uint32_t s_addr; // 32位無符號整數,存儲IPv4地址(網絡字節序)
};
- 該結構體專門用于存儲 IPv4 地址的 32 位二進制表示,
s_addr
字段的值必須是網絡字節序(大端序),通常通過inet_pton
等函數轉換得到。
結構體使用說明
sockaddr
與sockaddr_in
的關系:
兩者內存大小相同(均為 16 字節),sockaddr_in
是AF_INET
協議族的專用結構體,字段更清晰;而sockaddr
是通用結構體,作為系統調用(如bind
、sendto
)的參數類型。使用時需將sockaddr_in*
強制轉換為sockaddr*
。- 初始化注意事項:
sin_zero
字段需用bzero
或memset
清零,保證與sockaddr
結構體大小一致;sin_family
必須設為AF_INET
;sin_port
和sin_addr.s_addr
必須轉換為網絡字節序。
4.2.2?bind
:綁定套接字到本地地址和端口
- 函數原型:
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:將創建的套接字與本地 IP 地址和端口綁定,確保數據能通過該地址和端口收發(服務器必須綁定,客戶端可選)。
- 參數:
sockfd
:socket
函數返回的套接字文件描述符。addr
:指向sockaddr
(或sockaddr_in
)結構體的指針,包含本地 IP 和端口信息(需轉換為網絡字節序)。addrlen
:addr
結構體的字節長度(如sizeof(struct sockaddr_in)
)。
- 返回值:成功返回
0
;失敗返回-1
。
4.2.3?sendto
:發送 UDP 數據報
- 函數原型:
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
- 功能:向指定目標地址發送 UDP 數據報(無需提前建立連接)。
- 參數:
sockfd
:套接字文件描述符。buf
:待發送數據的緩沖區地址。len
:待發送數據的字節長度。flags
:發送標志(通常為0
,表示默認行為)。dest_addr
:指向目標地址結構體(sockaddr_in
)的指針,包含對方 IP 和端口。addrlen
:目標地址結構體的字節長度。
- 返回值:成功返回發送的字節數;失敗返回
-1
。
4.2.4?recvfrom
:接收 UDP 數據報
- 函數原型:
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- 功能:接收 UDP 數據報,并獲取發送方的地址信息。
- 參數:
sockfd
:套接字文件描述符。buf
:存儲接收數據的緩沖區地址。len
:緩沖區的最大容量(字節)。flags
:接收標志(通常為0
)。src_addr
:輸出參數,用于存儲發送方的地址信息(需提前分配內存)。addrlen
:輸入輸出參數,輸入時為src_addr
的容量,輸出時為實際地址長度。
- 返回值:成功返回接收的字節數;失敗返回
-1
。
4.2.5?close
:關閉套接字
- 函數原型:
#include <unistd.h> int close(int fd);
- 功能:關閉套接字,釋放相關資源。
- 參數:
fd
為套接字文件描述符。 - 返回值:成功返回
0
;失敗返回-1
。
三、UDP 客戶端與服務器完整通信示例
服務器代碼(udp_server.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>#define PORT 8888 // 服務器端口號
#define BUFFER_SIZE 1024 // 緩沖區大小int main() {int sockfd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);char buffer[BUFFER_SIZE];// 1. 創建UDP套接字sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 初始化服務器地址結構體memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET; // IPv4server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定所有本地IPserver_addr.sin_port = htons(PORT); // 端口轉換為網絡字節序// 3. 綁定套接字到本地地址和端口if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("UDP server started on port %d...\n", PORT);while (1) {// 4. 接收客戶端數據ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,(struct sockaddr*)&client_addr, &client_addr_len);if (recv_len == -1) {perror("recvfrom failed");continue;}buffer[recv_len] = '\0'; // 手動添加字符串結束符// 打印客戶端信息和接收的數據char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("Received from %s:%d: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);// 若收到"exit",則退出服務器if (strcmp(buffer, "exit") == 0) {printf("Server exiting...\n");break;}// 5. 回復客戶端(echo功能)const char* reply = "Message received";if (sendto(sockfd, reply, strlen(reply), 0,(struct sockaddr*)&client_addr, client_addr_len) == -1) {perror("sendto failed");}}// 6. 關閉套接字close(sockfd);return 0;
}
客戶端代碼(udp_client.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>#define SERVER_IP "127.0.0.1" // 服務器IP(本地回環地址)
#define SERVER_PORT 8888 // 服務器端口號
#define BUFFER_SIZE 1024 // 緩沖區大小int main() {int sockfd;struct sockaddr_in server_addr;socklen_t server_addr_len = sizeof(server_addr);char buffer[BUFFER_SIZE];// 1. 創建UDP套接字sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 初始化服務器地址結構體memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);// 轉換服務器IP為網絡字節序if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) != 1) {perror("invalid server IP");close(sockfd);exit(EXIT_FAILURE);}printf("Enter message to send (type 'exit' to quit):\n");while (1) {// 3. 從標準輸入獲取數據printf("> ");fgets(buffer, BUFFER_SIZE, stdin);// 移除fgets添加的換行符buffer[strcspn(buffer, "\n")] = '\0';// 4. 發送數據到服務器if (sendto(sockfd, buffer, strlen(buffer), 0,(struct sockaddr*)&server_addr, server_addr_len) == -1) {perror("sendto failed");continue;}// 若發送"exit",則退出客戶端if (strcmp(buffer, "exit") == 0) {printf("Client exiting...\n");break;}// 5. 接收服務器回復ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,NULL, NULL); // 不關心服務器地址,設為NULLif (recv_len == -1) {perror("recvfrom failed");continue;}buffer[recv_len] = '\0';printf("Server reply: %s\n", buffer);}// 6. 關閉套接字close(sockfd);return 0;
}
https://github.com/0voice