個人主頁:chian-ocean
文章專欄-NET
深入了解UDP套接字:構建高效網絡通信
- 個人主頁:chian-ocean
- 文章專欄-NET
- 前言:
- UDP
- UDP 特點:
- UDP的應用
- 套接字地址
- IP地址(Internet Protocol Address)
- IP地址的點分十進制表示
- 端口
- 端口的作用
- 知名端口
- UDP套接字`API`
- 創建套接字(`socket`)
- 參數說明:
- `sockaddr`結構體
- 網絡字節序(Network Byte Order)
- 為什么需要網絡字節序?
- 網絡字節序與主機字節序的區別
- 網絡字節序和主機字節序的轉換
- `recvfrom` 和 `sendto` 函數
- 1. `recvfrom` 函數
- 參數:
- 2. `sendto` 函數
- 參數:
- 返回值:
- UDP網絡編程
- 網絡服務端
- 代碼結構及功能說明:
- 網絡客戶端
- `Linux OS`
- 代碼結構與功能說明:
- `WIndows OS`
- 代碼結構與功能說明:
- 源碼
前言:
TCP/IP中有兩個具有代表性的傳輸層協議,他們分別是TCP和UDP。TCP提供可靠的通信傳輸,而UDP則經常被用于讓廣播和細節控制交給應用層傳輸。總之,根據通信特征,選擇合適的傳輸層協議是非常重要的。
UDP
UDP(User Datagram Protocol,用戶數據報協議) 是一種簡單的、無連接的網絡協議,屬于傳輸層協議。
UDP 特點:
- 無連接:UDP 不需要先建立連接(無三次握手過程),每次發送數據時,發送方只需要提供目標地址和端口信息。
- 不可靠性:UDP 不保證數據包的順序、完整性或可靠性。數據報可能會丟失、重復或亂序。應用層需要自行處理這些問題(如果需要的話)。
- 面向數據報:每個數據報是獨立的,發送時與接收時無關,不能保證數據順序,也不做數據流的管理。
UDP的應用
- 包總量較少的通信(DNS,SNMP等)
- 視頻、音頻等多媒體通信(實時通信)
- 限定與LAN等特定網絡中的應用通信
- 廣播通信(廣播、多播)
套接字地址
? 應用在使用TCP或者UDP的時候,會時使用OS提供的類庫,始終庫一般稱作為API(Application Programming Interface,應用程序編程接口)。
? 在使用TCP或者UDP通信的時候,又會廣泛的使用到套接字(socket)的API。套接字原本是BSD UNIX 開發的,后面應用到了Windows的Winsock以及嵌入式操作系統中。
? 應用程序應用套接字,可以設置端口號的IP地址、端口號、并且實現書庫的接受和發送。
IP地址(Internet Protocol Address)
IP 地址(Internet Protocol Address,互聯網協議地址)是計算機網絡中用來唯一標識一臺設備(如計算機、路由器、服務器、打印機等)在網絡中的位置的地址。IP 地址可以類比為每臺計算機的“身份證”,它確保數據在網絡中能夠正確地傳輸到目標設備。
- IP地址( IPv4地址 ) 由32位正整數來表示。TCP/IP通信要求將這樣的IP地址分配給每一個參與通信的主機。
IP地址的點分十進制表示
IP 地址的點分十進制表示法 是將 IPv4 地址(32 位二進制)分成四個字節(每個字節 8 位),并將每個字節轉換為十進制數,用 點(.
) 分隔開來。這個格式使得人們更容易理解和記住 IP 地址。
二進制 IP 地址:10101100.00010100.00000001.00000001
十進制轉換:
10101100
→172
00001000
→20
00000001
→1
00000001
→1
端口
在計算機網絡中,端口是用來標識應用程序或網絡服務的數字標識符,它幫助計算機識別不同的網絡服務和應用程序。端口號與 IP 地址 一起構成了唯一的通信標識符,用于網絡中數據包的正確傳遞。
端口的作用
- 區分不同的服務和應用程序:
- 計算機網絡中,不同的服務和應用程序通過不同的端口號來區分。每個網絡應用程序都會監聽某個端口,操作系統通過端口號將接收到的數據分配給相應的應用程序。
- 實現多路復用:
- 多路復用指的是通過不同的端口號使同一臺計算機能夠同時處理多個網絡連接。比如,Web 服務器通常通過端口 80(HTTP)接收請求,同時,FTP 服務器可以通過端口 21 處理文件傳輸。
- 標識網絡協議:
- 端口號與 TCP 和 UDP 等傳輸層協議配合使用,用于區分不同協議的數據流。TCP 和 UDP 使用不同的端口號來處理數據包。
知名端口
端口號的 0 到 1023 范圍是 知名端口,通常由 IANA(互聯網數字分配局)分配并保留給常見的協議和服務。這些端口號通常不允許由普通用戶或應用程序使用,因為它們被特定的系統服務和協議占用。
- 端口 0:端口 0 是一個特殊的保留端口,通常不能用于正常通信。它通常用于表示 “無效” 或 “未定義的端口”。
- 端口 1 到 1023:這些端口被廣泛用于操作系統和標準網絡協議,因此普通用戶和應用程序不能使用它們。舉例來說:
- 22:SSH(安全外殼協議)
- 80:HTTP(Web 服務)
- 443:HTTPS(加密的 Web 服務)
- 25:SMTP(郵件傳輸協議)
在試圖綁定80端口號,會出錯返回(Permission denied
)。
UDP套接字API
創建套接字(socket
)
int socket(int domain, int type, int protocol);
參數說明:
- domain:指定協議族,通常使用
AF_INET
表示 IPv4,AF_INET6
表示 IPv6,AF_UNIX
表示 UNIX 域套接字。 - type:指定套接字類型,通常使用:
SOCK_STREAM
:流式套接字,適用于 TCP 協議(面向連接)。SOCK_DGRAM
:數據報套接字,適用于 UDP 協議(無連接)。
- protocol:指定協議類型,通常設為 0,表示自動選擇合適的協議。
demo:
int sockfd_ = socket(AF_INET,SOCK_DGRAM,0);//創建套接字
if(sockfd_ < 0)
{lg(fatal,"Socket error,errno: %d,error: %s",errno,strerror(errno));//打印日志
}
lg(info,"Socket Success");//打印日志
sockaddr
結構體
在 C 語言 的網絡編程中,sockaddr
是一個結構體,表示網絡通信中的地址信息。它用于存儲與套接字(socket)相關的地址信息,例如 IP 地址和端口號。sockaddr
是一個通用的結構體,用于支持不同類型的地址族(例如 IPv4、IPv6 和 Unix 域套接字)的地址。
sockaddr_in
(用于 IPv4 地址)
sockaddr_in
是sockaddr
的一個擴展,專門用于存儲 IPv4 地址的信息。它包含了 IP 地址、端口號等信息。
struct sockaddr_in {short sin_family; // 地址族,通常是 AF_INETunsigned short sin_port; // 端口號(網絡字節序)struct in_addr sin_addr; // IP 地址(網絡字節序)char sin_zero[8]; // 填充字節,通常不使用
};
網絡字節序(Network Byte Order)
網絡字節序(Network Byte Order)是指在網絡通信中,數據以 大端字節序(Big Endian) 的格式進行傳輸的約定。它規定了多字節數據(如整數、浮點數等)在傳輸過程中,應該按照高位字節存儲在低地址位置(大端格式),從而確保不同計算機之間的數據傳輸能夠正確解析。
為什么需要網絡字節序?
不同的計算機架構(如 x86 和 PowerPC)使用不同的字節序來存儲多字節數據。常見的字節序有:
- 大端字節序(Big Endian):高位字節存儲在內存的低地址位置。
- 小端字節序(Little Endian):低位字節存儲在內存的低地址位置。
為了確保網絡中不同架構的計算機能夠正確地交換數據,網絡協議規定所有的數據傳輸必須使用 網絡字節序,即 大端字節序。這使得不同平臺之間的通信可以統一,并避免字節序不一致帶來的數據解釋錯誤。
網絡字節序與主機字節序的區別
- 主機字節序:是計算機系統內部使用的字節序,依賴于計算機的體系結構。常見的計算機體系結構有:
- x86/x64 架構:使用小端字節序(Little Endian)。
- 某些 RISC 架構:可能使用大端字節序(Big Endian)。
- 網絡字節序:是網絡通信的標準格式,統一為大端字節序。
網絡字節序和主機字節序的轉換
這些函數主要用于將不同字節序的數據進行轉換。由于不同的計算機架構使用不同的字節序(例如,大端字節序和小端字節序),而網絡傳輸規定采用大端字節序(網絡字節序),因此這些函數的作用就是將數據從主機字節序轉換為網絡字節序,或者從網絡字節序轉換為主機字節序。
uint32_t htonl(uint32_t hostlong);//將主機字節序的 32 位長整型轉換為網絡字節序。
uint16_t htons(uint16_t hostshort);//將主機字節序的 16 位短整型轉換為網絡字節序。
uint32_t ntohl(uint32_t netlong);//將網絡字節序的 32 位長整型轉換為主機字節序。
uint16_t ntohs(uint16_t netshort);//將網絡字節序的 16 位短整型轉換為主機字節序。
//將點分十進制的 IPv4 地址字符串(如 `"192.168.1.1"`)轉換為網絡字節序的二進制格式,并存儲在 in_addr 結構體中。
int inet_aton(const char *cp, struct in_addr *inp);//將點分十進制的 IPv4 地址字符串(如 `"192.168.1.1"`)轉換為 32 位的網絡字節序整數格式。
in_addr_t inet_addr(const char *cp);//將點分十進制的網絡地址字符串轉換為網絡字節序的整數(通常是 IP 地址的網絡部分)。
in_addr_t inet_network(const char *cp);//將網絡字節序的二進制格式的 IPv4 地址轉換為點分十進制的字符串格式。
char *inet_ntoa(struct in_addr in);//根據網絡號和主機號生成網絡字節序的完整 IP 地址。
struct in_addr inet_makeaddr(int net, int host);//提取 in_addr 結構體中的主機部分。
in_addr_t inet_lnaof(struct in_addr in);//提取 `in_addr` 結構體中的網絡部分。
in_addr_t inet_netof(struct in_addr in);
recvfrom
和 sendto
函數
recvfrom
和 sendto
是用于 UDP(無連接協議) 和 原始套接字(raw socket) 的函數,允許應用程序在網絡上傳輸數據。這些函數可以在不同的網絡地址和端口之間進行數據的發送和接收。
1. recvfrom
函數
recvfrom
用于接收來自指定地址的數據包。它不僅接收數據,還能夠獲取數據來源的信息(如發送者的 IP 地址和端口號),非常適用于 UDP 或 原始套接字 通信。
原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
參數:
sockfd
:套接字文件描述符。buf
:用于存儲接收到的數據的緩沖區。len
:緩沖區的大小。flags
:接收的標志位,通常設為 0。src_addr
:指向sockaddr
結構體的指針,接收者的地址信息(如 IP 地址和端口)。addrlen
:src_addr
結構體的大小。調用后,addrlen
會被設置為實際填充的地址長度。
2. sendto
函數
sendto
用于向指定的地址和端口發送數據。它通常用于 UDP 或 原始套接字,可以向特定的目標地址發送數據。
原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
參數:
sockfd
:套接字文件描述符。buf
:要發送的數據的緩沖區。len
:要發送的數據的字節數。flags
:發送的標志位,通常設為 0。dest_addr
:指向目標地址的sockaddr
結構體,指定數據包的目標地址和端口。addrlen
:dest_addr
結構體的大小。
返回值:
- 成功時,返回發送的字節數。
- 失敗時,返回 -1,
errno
會被設置為錯誤代碼。
UDP網絡編程
網絡服務端
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include "log.hpp"// 日志對象,用于記錄日志信息
Log lg;// 默認端口號為 8080
#define defaultport 8080
// 默認緩沖區大小為 1024 字節
int size = 1024;
// 默認 IP 地址為 "0.0.0.0",表示監聽所有可用的網絡接口
std::string defaultip = "0.0.0.0";// UdpServer 類定義:用于創建和管理 UDP 服務器
class UdpServer
{
public:// 構造函數,默認套接字文件描述符為 0,默認端口為 8080,默認 IP 為 "0.0.0.0"UdpServer(int sockfd = 0,uint16_t port = defaultport,std::string ip = defaultip):sockfd_(sockfd),port_(port),ip_(ip){}// 初始化服務器:創建套接字,綁定到指定的 IP 和端口void Init(){// 創建 UDP 套接字sockfd_ = socket(AF_INET,SOCK_DGRAM,0);if(sockfd_ < 0){// 如果創建套接字失敗,記錄日志并返回lg(fatal,"Socket error,errno: %d,error: %s",errno,strerror(errno));}lg(info,"Socket Success");// 配置本地地址(IPv4 地址)struct sockaddr_in local;bzero(&local,sizeof(local)); // 將地址結構清零local.sin_family = AF_INET; // 地址族為 IPv4local.sin_port = htons(port_); // 將端口號轉換為網絡字節序local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 將 IP 地址轉換為網絡字節序// 綁定套接字到指定的地址和端口int n = bind(sockfd_,(struct sockaddr*)&local,sizeof(local));if(n < 0){// 如果綁定失敗,記錄日志并返回lg(fatal,"Bind error,errno: %d ,errno: %s",errno,strerror(errno));}lg(info,"Bind Success");}// 服務器的主運行函數:接收客戶端消息,并將消息回復給客戶端void Run(){is_running_ = true;while(is_running_){char inbuffer[size]; // 用于接收數據的緩沖區struct sockaddr_in client; // 客戶端地址信息bzero(&client,sizeof(client)); // 將客戶端地址結構清零socklen_t clientSize = sizeof(client); // 客戶端地址的長度// 使用 recvfrom 接收客戶端發送的數據ssize_t n = recvfrom(sockfd_,inbuffer,sizeof(inbuffer),0,(struct sockaddr*)&client,&clientSize);if(n < 0){// 如果接收數據失敗,記錄日志并繼續等待其他數據lg(warning,"recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0; // 將接收到的數據字符串終結符設置為 NULLstd::string Info = inbuffer; // 將接收到的數據轉為字符串std::cout << Info << std::endl; // 輸出接收到的消息// 使用 sendto 發送數據回客戶端sendto(sockfd_,Info.c_str(),Info.size(),0,(struct sockaddr*)&client,sizeof(client)); }}// 析構函數:關閉套接字,釋放資源~UdpServer(){if(sockfd_ > 0) close(sockfd_);}private:int sockfd_; // 套接字文件描述符uint16_t port_; // 服務器端口std::string ip_; // 服務器 IP 地址bool is_running_ = false; // 服務器運行狀態
};
代碼結構及功能說明:
- 構造函數:初始化類的成員變量,包括套接字文件描述符、端口號和 IP 地址。默認值為端口 8080 和 IP 地址
0.0.0.0
,這意味著它將綁定到所有可用的網絡接口。 Init()
:初始化函數,用于創建 UDP 套接字并綁定到指定的 IP 地址和端口。如果創建套接字或綁定失敗,函數會記錄錯誤日志并停止執行。Run()
:主循環函數,服務器在此函數中運行。它不斷接收客戶端的消息,并將接收到的消息發送回客戶端。數據是通過recvfrom()
和sendto()
函數進行接收和發送的。~UdpServer()
:析構函數,用于關閉套接字,釋放相關資源。
網絡客戶端
Linux OS
#include <iostream>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#include "log.hpp"// 創建一個日志對象,用于記錄日志信息
Log lg;// 緩沖區大小設為 1024 字節
size_t size = 1024;// 使用說明函數,顯示如何使用客戶端程序
void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc ,char* argv[])
{// 參數檢查:需要兩個參數,服務器的 IP 地址和端口號if(argc != 3){Usage(argv[0]);return 1;}// 從命令行參數獲取 IP 地址和端口號std::string ip = argv[1]; std::uint16_t port = std::stoi(argv[2]); // 將端口號從字符串轉換為整數// 配置服務器的 sockaddr_in 結構體struct sockaddr_in server;bzero(&server,sizeof(server)); // 清空結構體server.sin_family = AF_INET; // 地址族為 IPv4server.sin_port = htons(port); // 端口號轉換為網絡字節序server.sin_addr.s_addr = inet_addr(ip.c_str()); // 將 IP 地址轉換為網絡字節序// 創建 UDP 套接字int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(sockfd < 0){// 如果套接字創建失敗,記錄日志并返回lg(fatal,"sock error: errno:%d,error:%s",errno,strerror(errno));return 1;}lg(info,"sock success"); // 創建套接字成功std::string message; // 存儲用戶輸入的消息while(true){message.clear(); // 清空消息內容std::cout << "Please Enter@: "; // 提示用戶輸入getline(std::cin, message); // 獲取用戶輸入// 配置臨時的客戶端地址結構struct sockaddr_in tmp;bzero(&tmp,sizeof(tmp)); // 清空結構體socklen_t tmplen = sizeof(tmp);// 發送數據到服務器sendto(sockfd, message.c_str(), size, 0, (struct sockaddr*)&server, sizeof(server));// 接收服務器的響應char buffer[size]; // 接收數據的緩沖區ssize_t s = recvfrom(sockfd, &buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen);if(s > 0){buffer[s] = '\0'; // 確保接收到的數據以 NULL 終結std::cout << buffer << std::endl; // 輸出服務器的響應}} // 關閉套接字close(sockfd);return 0;
}
代碼結構與功能說明:
Usage()
函數:如果程序沒有正確傳入 IP 地址和端口號參數,該函數會提示用戶如何使用該程序。它顯示了程序的運行方式。main()
函數:- 命令行參數處理:獲取用戶輸入的服務器 IP 地址和端口號。
server
配置:創建并配置sockaddr_in
結構體來存儲服務器的地址信息(IP 地址和端口)。- 套接字創建:使用
socket()
創建一個 UDP 套接字。如果創建失敗,則記錄錯誤并退出。 sendto()
和recvfrom()
:sendto()
用于將消息發送到服務器。recvfrom()
用于接收從服務器返回的數據。接收到的數據會被存儲在buffer
中,并輸出到終端。
- UDP 套接字:
- 客戶端不需要調用
bind()
來綁定端口,因為 UDP 是無連接的。發送數據時,UDP 套接字會自動選擇一個臨時端口。
- 客戶端不需要調用
- 消息發送與接收:
- 用戶輸入的消息通過
sendto()
發送到服務器。 - 程序等待并接收服務器的響應,通過
recvfrom()
來獲取數據,成功接收數據后輸出。
- 用戶輸入的消息通過
- 日志系統:
- 代碼使用
Log
類記錄重要的信息、錯誤、成功狀態等,便于調試和追蹤。
- 代碼使用
WIndows OS
//#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h> // 包含 Winsock2 庫,用于套接字編程
#include <Windows.h> // 包含 Windows API,提供操作系統相關的功能
#include <iostream> // 包含輸入輸出流庫,用于顯示信息
#include <string> // 包含字符串操作函數
#include <cstring> // 包含字符串處理函數#pragma warning(disable:4996) // 禁用有關不推薦使用的函數的編譯器警告
#pragma comment(lib, "ws2_32.lib") // 鏈接 Winsock 庫,確保使用 Winsock 2.2 APIint main() {std::cout << "hello client" << std::endl; // 輸出客戶端信息WSADATA wsaData; // 用于存儲 Winsock 初始化數據SOCKET udpSocket; // 用于存儲創建的 UDP 套接字sockaddr_in serverAddr; // 用于存儲服務器的地址信息int len = sizeof(serverAddr); // 地址結構的大小const char* serverIP = "119.3.219.187"; // 服務器的 IP 地址int serverPort = 8080; // 服務器的端口號// 初始化 Winsock 庫if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "Winsock initialization failed!" << std::endl; // 如果初始化失敗,輸出錯誤信息return 1;}// 創建 UDP 套接字udpSocket = socket(AF_INET, SOCK_DGRAM, 0); // 使用 IPv4 地址族和 UDP 協議if (udpSocket == INVALID_SOCKET) { // 如果創建套接字失敗std::cerr << "Socket creation failed!" << std::endl;WSACleanup(); // 清理 Winsock 庫資源return 1;}// 配置服務器的地址信息serverAddr.sin_family = AF_INET; // 地址族設置為 IPv4serverAddr.sin_port = htons(serverPort); // 將端口號轉換為網絡字節序serverAddr.sin_addr.s_addr = inet_addr(serverIP); // 將服務器 IP 地址轉換為網絡字節序// 發送數據std::string message; // 用于存儲用戶輸入的消息while (true){// 獲取用戶輸入的消息std::cout << "Please Enter#: ";getline(std::cin, message); // 從標準輸入讀取消息struct sockaddr_in tmp; // 臨時結構體,用于接收數據int tmplen = sizeof(tmp); // 地址結構的大小// 使用 sendto 函數發送 UDP 數據包到指定的服務器地址和端口sendto(udpSocket, message.c_str(), message.size(), 0, (struct sockaddr*)&serverAddr, len);// 接收來自服務器的響應char buffer[1024]; // 用于存儲接收到的數據int s = recvfrom(udpSocket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen); // 接收數據if (s > 0) // 如果接收到數據{buffer[s] = '\0'; // 確保接收到的字符串以 NULL 終止std::cout << buffer << std::endl; // 輸出接收到的消息}}std::cout << "Message sent to server!" << std::endl; // 輸出消息已發送的提示信息// 清理資源,關閉套接字并清理 Winsock 庫closesocket(udpSocket);WSACleanup();return 0;
}
代碼結構與功能說明:
- 初始化 Winsock 庫:
- 使用
WSAStartup()
初始化 Winsock 庫,初始化失敗時輸出錯誤信息并退出程序。
- 使用
- 創建 UDP 套接字:
- 使用
socket()
創建一個 UDP 套接字。若創建失敗,則輸出錯誤信息并退出。
- 使用
- 配置服務器地址:
- 設置服務器的地址族為
AF_INET
(IPv4 地址族),將端口號轉換為網絡字節序,并將服務器的 IP 地址轉換為網絡字節序。
- 設置服務器的地址族為
- 發送數據:
- 程序進入一個無限循環,等待用戶輸入消息并發送給服務器。用戶輸入的消息通過
sendto()
發送到指定的服務器地址和端口。
- 程序進入一個無限循環,等待用戶輸入消息并發送給服務器。用戶輸入的消息通過
- 接收數據:
- 使用
recvfrom()
接收服務器返回的響應。接收到的數據存儲在緩沖區buffer
中,然后輸出到終端。
- 使用
- 資源清理:
- 使用
closesocket()
關閉套接字,使用WSACleanup()
清理 Winsock 庫資源。
- 使用