本文介绍 UDP 服务端与客户端 的创建流程,和相关的函数接口
核心流程
- 创建 socket → socket()
- 填写服务器地址信息 → sockaddr_in 结构体
- 绑定地址和端口 → bind()
- 接收并响应客户端数据 → recvfrom() / sendto()

socket()
cpp
#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 的协议 |
使用示例
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//成功返回文件描述符
//失败返回-1
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
}
sockaddr_in
他有4个成员,赋值前三个即可
cpp
struct sockaddr_in {
sa_family_t sin_family; // 地址族,必须是 AF_INET
uint16_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充字节,保持与 sockaddr 一致
};
在赋值时需要注意,
- 端口号要转换为网络序列
- IP地址调用inet_addr
cpp
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)绑定,才能做到接收和转发消息
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 | 含义 |
---|---|
sockfd |
用 socket() 创建的套接字文件描述符 |
addr |
本地地址结构体(sockaddr* 类型,实际通常传 sockaddr_in* 转换而来) |
addrlen |
结构体 addr 的大小(用 sizeof(sockaddr_in) ) |
使用示例
cpp
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 套接字编程中用来接收数据报的核心函数,
它不仅接收数据 ,还能告诉你数据是从哪个客户端发来的。
cpp
#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 |
输入输出参数,传入结构体长度,返回时写入实际地址大小 |
使用示例
cpp
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()
cpp
#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) ) |
使用示例
cpp
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创建流程需要的接口,以及在这方面服务器端和客户端的区别