本文介紹 UDP 服務端與客戶端 的創建流程,和相關的函數接口
核心流程
- 創建 socket → socket()
- 填寫服務器地址信息 → sockaddr_in 結構體
- 綁定地址和端口 → bind()
- 接收并響應客戶端數據 → recvfrom() / sendto()
socket()
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
參數 | 說明 |
---|---|
domain | 地址族(協議族),常見值: |
→ AF_INET :IPv4 | |
→ AF_INET6 :IPv6 | |
→ AF_UNIX :本地通信(進程間通信) | |
type | 套接字類型,決定通信方式: |
→ SOCK_STREAM :面向連接(TCP) | |
→ SOCK_DGRAM :無連接(UDP) | |
protocol | 一般寫 0,表示讓系統自動選擇適合給定 domain 和 type 的協議 |
使用示例
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//成功返回文件描述符
//失敗返回-1
if (sockfd < 0)
{std::cerr << "socket error" << std::endl;
}
sockaddr_in
他有4個成員,賦值前三個即可
struct sockaddr_in {sa_family_t sin_family; // 地址族,必須是 AF_INETuint16_t sin_port; // 端口號(網絡字節序)struct in_addr sin_addr; // IP 地址char sin_zero[8]; // 填充字節,保持與 sockaddr 一致
};
在賦值時需要注意,
- 端口號要轉換為網絡序列
- IP地址調用inet_addr
server.sin_port = htons(serverport); // 主機序列轉網絡序列server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 轉換4字節
INADDR_ANY
服務器端的服務需要固定的端口
而IP地址給INADDR_ANY,表示監聽任意IP地址,即從哪個網卡發來哪個請求都可以處理
其實和手動給0或者0.0.0.0 作用相似
bind()
服務器端和客戶端都需要將套接字和本地地址(IP+port)綁定,才能做到接收和轉發消息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數 | 含義 |
---|---|
sockfd | 用 socket() 創建的套接字文件描述符 |
addr | 本地地址結構體(sockaddr* 類型,實際通常傳 sockaddr_in* 轉換而來) |
addrlen | 結構體 addr 的大小(用 sizeof(sockaddr_in) ) |
使用示例
int n = bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
//成功返回0,失敗返回-1
if (n < 0)
{LOG(FATAL, "bind error, %s, %d\n", strerror(errno), errno);exit(BIND_ERROR);
}
客戶端的bind()
客戶端也是需要綁定的,不然如何發送消息呢。
但是,不需要我們手動調用bind(),
在第一次發送請求的時候,OS自動調用bind()
當你第一次調用: sendto()操作系統會自動調用 bind() 來:1.分配一個臨時的本地 IP(通常是默認網卡的 IP)2.分配一個 可用的隨機端口(稱為 ephemeral port)
recvfrom()
recvfrom() 是 UDP 套接字編程中用來接收數據報的核心函數,
它不僅接收數據,還能告訴你數據是從哪個客戶端發來的。
#include<sys/socket.h>
#include<sys/types.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
參數 | 含義 |
---|---|
sockfd | 套接字文件描述符(由 socket() 創建) |
buf | 指向緩沖區的指針,用來存放接收到的數據 |
len | 緩沖區大小 |
flags | 一般設置為 0 ,特殊需求可用 MSG_PEEK (窺視)、MSG_WAITALL 等 |
src_addr | 輸出參數,對方的地址結構體(可獲取對方 IP 和端口) |
addrlen | 輸入輸出參數,傳入結構體長度,返回時寫入實際地址大小 |
使用示例
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
//成功返回實際接收的字節數(就是多少個英文字符)
//失敗返回-1
//peer存客戶端的數據
sendto()
#include<sys/socket.h>
#include<sys/types.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
有區別的是len、det_addr
參數 | 含義 |
---|---|
sockfd | 套接字文件描述符(由 socket() 創建) |
buf | 要發送的數據緩沖區的指針 |
len | 要發送的數據字節數 |
flags | 通常為 0 ,特殊用途可以設置為 MSG_CONFIRM 等 |
dest_addr | 目標地址結構體(例如 sockaddr_in ,需強轉為 sockaddr* ) |
addrlen | dest_addr 的長度(如 sizeof(sockaddr_in) ) |
使用示例
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//客戶端要持續運行,所以給死循環
while (true)
{char buffer[1024];ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = {0};InetAddr addr(peer); //自己寫的類,為了獲取轉換后的網絡字節序和4字節序LOG(DEBUG, "get message from [%s:%d]:%s\n", addr.Ip(), addr.Port(), buffer);sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);}
}
小結
介紹了socket創建流程需要的接口,以及在這方面服務器端和客戶端的區別