一、核心功能说明
UDP Echo Server(UDP 回显服务器)是 UDP 网络编程的一个基础案例,核心逻辑是:服务器绑定指定端口后,持续接收客户端发送的 UDP 数据报,然后将接收到的数据原封不动地回传给客户端。
二、前置知识
2.1 创建UDP套接字
进行 UDP 通信前,需通过socket()系统调用创建套接字
c
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
domain:套接字域,使用AF_INET(粗体强调)表示 IPv4 网络通信。type:套接字类型,使用SOCK_DGRAM(粗体强调)表示面向数据报的 UDP 协议(无连接、不可靠、有最大数据长度限制)。protocol:指定具体协议,UDP 通信直接填0即可(由前两个参数确定 UDP 协议)。
返回值:
- 成功:返回套接字文件描述符(fd)。
- 失败:返回
-1,并设置错误码。
常用调用示例:
c
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
2.2 绑定端口(bind)
服务器必须将套接字与「IP + 端口」绑定,否则客户端无法定位到该服务进程。
c
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数说明:
sockfd:创建好的 UDP 套接字文件描述符。addr:指向struct sockaddr_in(IPv4)的指针,需提前填充 IP 和端口信息。addrlen:addr结构体的长度(如sizeof(struct sockaddr_in))。
返回值:
- 成功:返回
0 - 失败:返回
-1,并设置错误码。
2.3 接收数据(recvfrom)
UDP 是面向数据报的协议,不能使用普通文件的read()/write(),需用recvfrom()接收数据,同时能获取发送方的地址信息。
c
#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:UDP 套接字文件描述符;buf:输出型参数,存储接收的数据;len:缓冲区大小(建议填sizeof(buf)-1,预留字符串结束符位置);flags:读取方式,默认填0(阻塞读取);src_addr:输出型参数,存储发送方的 IP 和端口信息;addrlen:输入输出型参数,需先初始化为src_addr的长度。
返回值:
- 成功:返回实际接收的字节数;
- 失败:返回
-1,并设置错误码。
2.4 发送数据(sendto)
与recvfrom()对应,UDP 通过sendto()发送数据,需指定接收方的地址信息。
c
#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);
参数说明:
- 前 4 个参数与
recvfrom()一致。 dest_addr:接收方的 IP 和端口信息(可直接复用recvfrom()获取的src_addr)。addrlen:dest_addr结构体的长度。
返回值:
- 成功:返回实际发送的字节数。
- 失败:返回
-1,并设置错误码。
三、完整源代码
3.1 地址封装类:InetAddr.hpp
该文件封装了 IPv4 地址结构体sockaddr_in,简化了 IP 和端口的解析、获取操作,避免重复编写地址转换逻辑。
c++
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
private:
void GetAddress(std::string*ip, uint16_t*port)
{
*port=ntohs(_addr.sin_port); // 将网络序列转换成主机序列
*ip=inet_ntoa(_addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in&addr)
:_addr(addr)
{
GetAddress(&_ip, &_port);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
3.2 UDP 服务器核心类:UdpServer.hpp
该文件是 UDP 回显服务器的核心实现,包含套接字创建、端口绑定、数据接收与回显的完整逻辑。
c++
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"
enum
{
SOCKET_ERROR=1,
BIND_ERROR,
USAGE_ERROR
};
const static int defaultfd=-1;
class UdpServer
{
public:
UdpServer(uint16_t port)
:_sockfd(defaultfd)
,_port(port)
,_isrunning(false)
{}
void InitServer()
{
// 1.创建UDP套接字
_sockfd=socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd<0)
{
std::cout<<"socket create error!"<<std::endl;
exit(SOCKET_ERROR);
}
std::cout<<"socket create success, sockfd:"<<_sockfd<<std::endl;
// 2.填充sockaddr_in结构
struct sockaddr_in local;
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(字符串)转换成四字节ip,再把四字节ip转换成网络序列
local.sin_addr.s_addr=INADDR_ANY;
// 2.bind sockfd和网络信息(ip+port)
int n=bind(_sockfd, (struct sockaddr*)&local, sizeof(local)); // 成功返回0,失败返回-1
if(n<0)
{
std::cout<<"bind error!"<<std::endl;
exit(BIND_ERROR);
}
std::cout<<"socket bind success!"<<std::endl;
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len=sizeof(peer); // 此处必须初始化为peer结构体的长度,表明为sockaddr_in结构
// 1.先让server能接数据
// upd是面向数据报的
ssize_t n=recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n>0)
{
buffer[n]=0;
InetAddr addr(peer);
std::cout<<"get a message:"<<buffer<<", from ip:["<<addr.Ip()<<":"<<addr.Port()<<"]"<<std::endl;
// 2.再让server收到的数据,发给对方
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
}
~UdpServer()
{}
private:
int _sockfd;
// std::string _ip;
uint16_t _port; // 端口号
bool _isrunning;
};
3.3 服务器入口程序:Main.cc
该文件是 UDP 服务器的启动入口,负责处理命令行参数、创建服务器实例并启动核心服务逻辑。
c++
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// ./udpserver ip port
int main(int argc, char* argv[])
{
if(argc!=2)
{
std::cout<<"usage:./udpserver [ip] [port]"<<std::endl;
exit(USAGE_ERROR);
}
uint16_t port=std::stoi(argv[1]);
// C++14语法
std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port);
usvr->InitServer();
usvr->Start();
return 0;
}
3.4 UDP 客户端程序:UdpClient.cc
该文件实现了 UDP 客户端的核心功能,能够向指定服务器发送消息,并接收服务器的回显数据。
c++
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
enum
{
USAGE_ERROR=1
};
// ./udpclient serverip serverport
int main(int argc, char*argv[])
{
if(argc!=3)
{
std::cout<<"usage:./udpclient [serverip] [serverport]"<<std::endl;
exit(USAGE_ERROR);
}
std::string serverip=argv[1];
uint16_t serverport=std::stoi(argv[2]);
// 1.创建socket
int sockfd=socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd<0)
{
std::cout<<"socket create error!"<<std::endl;
}
// 2.client需不需要bind呢?client也有自己的ip和port,不需要显示的bind
// udp client首次发送数据的时候,OS会自己自动的给client进行bind
// 构建目标主机的socket信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(serverport);
server.sin_addr.s_addr=inet_addr(serverip.c_str());
// 直接进行通信
std::string message;
while(true)
{
std::cout<<"Please Enter# ";
std::getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
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);
if(n>0)
{
buffer[n]=0;
std::cout<<"server echo# "<<buffer<<std::endl;
}
}
return 0;
}
3.5 编译脚本:makefile
该文件是自动化编译脚本,支持一键编译服务器和客户端程序,同时提供清理编译产物的功能,简化编译操作。
makefile
.PHONY:all
all:udpserver udpclient
udpserver:Main.cc
g++ -o $@ $^ -std=c++14
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -f udpserver udpclient