UDP 是无连接、不可靠、基于数据报 的传输协议,相比 TCP,它不需要建立连接、速度更快。
一、函数补充
sendto
sendto 是 Linux 下 C 语言 用于UDP 套接字 发送数据的核心系统调用,专门用于无连接的 UDP 通信(必须指定目标地址)。

也就是:
#include <sys/socket.h>
ssize_t sendto(
int sockfd, // 套接字文件描述符
const void *buf, // 要发送的数据缓冲区
size_t len, // 数据长度(字节)
int flags, // 标志位,一般填 0
const struct sockaddr *dest_addr, // 目标地址(IP+端口)
socklen_t addrlen // 目标地址结构体长度
);
参数:
| 参数 | 说明 |
|---|---|
sockfd |
UDP 创建的 socket 文件描述符 |
buf |
要发送的数据指针(char 数组、字符串等) |
len |
发送数据的字节数 |
flags |
控制选项,UDP 正常发送直接填 0 |
dest_addr |
目标地址结构体(必须指定:对方 IP + 端口) |
addrlen |
地址结构体的大小,一般用 sizeof() |
返回值:
成功:返回实际发送的字节数,失败:返回 -1,并设置 errno 错误码
**注意事项:**UDP 必须填写目标地址,Linux 用 sockaddr_in 存储 IP 和端口
sendto 核心特点
- 无连接:每次发送都必须指定目标地址
- 数据报模式:一次 sendto = 一个独立 UDP 包
- 不保证到达 :函数返回成功只代表数据进入发送缓冲区,不代表对方收到
- 不粘包 :一次 sendto 对应对方一次
recvfrom - 客户端 / 服务端都能用:UDP 没有严格区分
recvfrom
recvfrom 是 Linux 下用于从 UDP 套接字(SOCK_DGRAM)接收数据报 的核心系统调用,它不仅能读取数据,还能同时获取数据发送方的 IP 地址和端口号,这是 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);
参数:
sockfd- 已创建并绑定(
bind)的 UDP 套接字文件描述符。
- 已创建并绑定(
buf- 指向接收数据缓冲区的指针,用于存放接收到的内容。
len- 缓冲区
buf的最大容量(字节),决定单次最多接收的数据大小。
- 缓冲区
flags- 控制接收行为的标志位,常用值:
0:默认阻塞模式,无数据时进程会挂起等待。MSG_DONTWAIT:非阻塞模式 ,无数据立即返回-1,errno设为EAGAIN。MSG_PEEK:窥探数据,读取后不清除内核缓冲区,下次仍可读到。MSG_WAITALL:等待直到接收满len字节(UDP 很少用)。
- 控制接收行为的标志位,常用值:
src_addr- 输出参数 :指向
struct sockaddr结构体指针,接收完成后自动填充发送方的 IP 和端口。 - 实际使用时,通常传入
struct sockaddr_in(IPv4)或struct sockaddr_in6(IPv6),再强制类型转换。 - 不需要源地址可设为
NULL,但老机器上可能会出现bug
- 输出参数 :指向
addrlen- 输入输出参数 :
- 传入:
src_addr结构体的初始大小。 - 返回:内核写入的实际地址长度。
- 传入:
- 必须初始化为有效地址,不能为
NULL
- 输入输出参数 :
返回值
- 成功 :返回实际接收到的字节数 (
ssize_t有符号整型)。 - 失败 :返回
-1,全局变量errno存储错误码。 - 注意(UDP 特有) :UDP 无连接,永远不会返回 0(返回 0 属异常 / 网络错误)。
关键注意事项
- UDP 数据报边界
- 每次
recvfrom严格接收一个完整数据报。 - 数据报 >
len:截断 ,多余字节丢弃,errno设为MSG_TRUNC。
- 每次
- 阻塞 vs 非阻塞
- 默认阻塞:无数据时进程休眠等待。
- 非阻塞(
MSG_DONTWAIT):无数据立即返回-1/EAGAIN。
- 地址结构体
- IPv4 用
struct sockaddr_in,IPv6 用struct sockaddr_in6。 - 必须初始化
addr_len,否则地址获取失败。
- IPv4 用
关于sockaddr结构体的介绍请见上一篇。
二、代码实现
在正式敲代码前,我们先考虑一下聊天室的大概逻辑:
首先,有多个用户进行聊天,那么就需要将所有用户管理起来;其次,每个用户都会有自己的IP加Port地址,故每个用户的地址也需要管理起来。以及等等......
地址管理封装:InetAddr
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
// 网络地址封装
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address)
: _address(address), _len(sizeof(address))
{
_ip = inet_ntoa(address.sin_addr);
_port = ntohs(address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _ip(ip), _port(port)
{
// 使用sockaddr_in结构体前先清空,别忘了
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // 主机向网络的时候记得转化端口号
_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式
_len = sizeof(_address);
}
InetAddr() {}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
~InetAddr()
{
}
bool operator==(const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
private:
struct sockaddr_in _address;
std::string _ip;
uint16_t _port;
socklen_t _len;
};
用户信息管理:UserManager
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"
class UserManager
{
public:
UserManager() {}
void AddUser(const InetAddr &addr)
{
if (SearchUser(addr))
return;
_users.push_back(addr);
}
void DelUser(const InetAddr &addr)
{
for (auto its = _users.begin(); its < _users.end(); its++)
{
if (*its == addr)
{
_users.erase(its);
break; //迭代器这里记得break
}
}
}
bool SearchUser(const InetAddr &addr)
{
// e是容器中的元素本身
for (auto &e : _users)
{
if (e == addr)
return true;
}
return false;
}
void ModUser(const InetAddr &addr)
{
DelUser(addr);
AddUser(addr);
}
std::vector<InetAddr> &Users()
{
return _users;
}
~UserManager()
{
}
private:
std::vector<InetAddr> _users;
};
路由器,消息转发:Route
// 路由器,消息转发
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Mutex.hpp"
#include "UserManager.hpp"
#include "InetAddr.hpp"
class Route
{
public:
Route() : _uma(std::make_unique<UserManager>())
{
}
void CheckUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->AddUser(addr);
}
void OfflineUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->DelUser(addr);
}
void Broadcast(int sockfd, std::string message)
{
LockGuard lockguard(_lock);
auto &users = _uma->Users();
for (auto &user : users)
{
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)user.GetNetAddress(), user.Len());
}
}
~Route()
{
}
private:
std::unique_ptr<UserManager> _uma;
Mutex _lock;
};
服务端:UdpServer
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <string>
#include <strings.h>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
// 回调函数
using handler_addr_t = std::function<void(const InetAddr &)>; // 管理地址
using handler_msg_t = std::function<void(int sokcfd, std::string msg)>; // 管理信息
enum
{
SUCCESS,
SOCKET_ERROR,
USAGE_ERROR,
BIND_ERR
};
class UdpServer
{
public:
// UdpServer(std::string &ip, uint16_t port)
UdpServer(uint16_t port)
: _socketfd(default_fd), _port(port)
{
}
~UdpServer()
{
close(_socketfd);
}
void Init()
{
// socket函数创建网络通信的接口 / 通道,返回值是文件描述符
// AF_INET:指定使用 IPv4 协议进行网络通信
// SOCK_DGRAM:指定为 UDP 数据报套接字,无连接、不可靠传输
// 0:让系统自动匹配 UDP 协议,无需手动指定
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERROR);
}
LOG(LogLevel::INFO) << "socket success, fd:" << _socketfd;
// 第二步,填充网络信息
// 注意了,最好了解一下三种sockaddr结构体的组成
// struct sockaddr_in local;
// // 使用sockaddr_in结构体前先清空
// bzero(&local, sizeof(local));
// local.sin_family = AF_INET;
// local.sin_port = htons(_port); // 主机向网络的时候记得转化端口号
// // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // ip将点字符串方式转化为四字节模式
// local.sin_addr.s_addr = INADDR_ANY; // 绑定该机器上的任意IP地址!!
//将上面的地址处理交给封装好的对象处理
InetAddr local(_port);
// 第三步,bin socket信息
int n = bind(_socketfd, (struct sockaddr *)local.GetNetAddress(), local.Len());
if (n < 0)
{
perror("bind failed reason: ");
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success" << ", port: " << _port;
}
void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
{
_handler_addr = handler_addr;
_handler_msg = handler_msg;
}
void Start()
{
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0)
{
// 服务端
inbuffer[n] = 0;
// 1. 检测新用户
InetAddr clientaddress(peer);
std::string tips = clientaddress.ToString();
std::string message = tips + inbuffer;
LOG(LogLevel::DEBUG) << message;
_handler_addr(clientaddress);
// 2. 转发消息
_handler_msg(_socketfd, message);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _socketfd;
// std::string _ip; // ip有两种形式(点分式和四字节ip),这里选点分式
uint16_t _port; // uint16_t = 无符号 16 位整数,专门用来存端口号等网络数据。
handler_addr_t _handler_addr;
handler_msg_t _handler_msg;
};
客户端函数入口:ChatClient
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
using namespace NS_THREAD_MODULE;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t";
std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
std::cout << "Please Set Your Nick Name# ";
std::getline(std::cin, nickname);
std::string online_message = nickname + " online!";
ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
void RecvMessage()
{
while (true)
{
// recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl; // 2
}
}
}
void SendMessage()
{
InetAddr serveraddr(server_port, server_ip);
Online(serveraddr);
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter# "; // 1
std::getline(std::cin, message);
message = nickname + "# " + message;
// 2. clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
}
// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
Thread recver(RecvMessage);
Thread sender(SendMessage);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
return 0;
}
服务端函数入口:ChatServer
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp" // 任务
#include "UdpServer.hpp" // 获取事件
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
// 线程池模块
auto thread_pool = ThreadPool<task_t>::Instance();
// 路由模块
Route r;
// 网络模块
UdpServer usvr(server_port);
usvr.Init();
usvr.RegisterService(
[&r](const InetAddr &addr)
{
r.CheckUser(addr);
},
[&r, thread_pool](int sockfd, std::string msg)
{
auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
thread_pool->Enqueue([&r, &sockfd, &msg](){
r.Broadcast(sockfd, msg);
});
});
usvr.Start();
return 0;
}
除了上面给出的函数文件外,还额外包含了自个创建的线程、线程池、日志、锁的封装文件。这些基本都能直接使用库中的函数,日志则可以找其它大佬的开源,懒的话只当练习可以直接去掉。
结果预览:
