以下是基于 Linux 环境的 UDP 编程完整说明,代码全部在 Linux 服务器上实现(Windows 需调整网络 API,如替换unistd.h为windows.h、socket参数微调等)。
UDP 编程核心注意事项
在 UDP 开发中,需重点关注以下 4 个关键点:
1.Server 端 IP 绑定:INADDR_ANY
UDP 服务端需要绑定端口以监听请求,但 IP 应设置为INADDR_ANY(对应代码中htonl(INADDR_ANY)),表示绑定到服务器的所有网卡地址(如服务器同时有内网、公网 IP 时,均可接收请求),避免硬编码特定 IP 导致的访问限制。
2.Client 端无需显式 bind ()
UDP 是无连接协议,客户端不需要主动调用bind()绑定端口 ------ 系统会在调用sendto()时,自动为客户端分配一个临时端口,并完成隐式绑定。显式 bind 客户端端口反而会增加端口冲突风险(除非业务需要固定客户端端口)。
3.用回调函数解耦通信层与业务层
为了让 UDP 的 "网络通信逻辑" 和 "业务处理逻辑" 分离(解耦),可以通过包装器 + 回调函数 的设计:
封装udpServer类,负责网络套接字创建、绑定、消息收发等通用逻辑;
通过函数对象(如 C++ 的std::function)将 "业务处理函数" 作为回调传入udpServer,当服务端收到消息时,自动调用回调函数处理业务(如翻译、广播等)。
这种设计让网络层代码可复用,业务层逻辑可灵活替换。
4.网络字节序与主机字节序转换
不同设备的 "字节序"(数据在内存中的存储顺序)可能不同(如大端、小端),而网络传输统一使用大端字节序 。因此:
端口号:服务端绑定端口时,需用htons()(主机字节序转网络字节序,针对 16 位数据);接收客户端端口时,用ntohs()(网络字节序转主机字节序)。
IP 地址:服务端绑定 IP 时,INADDR_ANY需用htonl()(针对 32 位数据);接收客户端 IP 时,用inet_ntoa()(将网络字节序的 IP 转为字符串)。
其他重要注意点在代码中以注释展现
本章主要用udp实现了3个业务
- demo1:英语翻译功能(客户端发送英文,服务端返回中文释义)
- demo2:客户端控制服务端(客户端发送指令,服务端执行对应操作)
- demo3:网络聊天室(依赖
onlineUser.hpp文件实现在线用户的管理,如用户上线、下线、消息广播等)
udpServer.hpp
cpp
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdint>
#include <cstring>
#include <strings.h>
#include <stdlib.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>
namespace Server{
//const static uint16_t defaultport = 8888;
using namespace std;
const static std::string defaultip = "0.0.0.0";
enum {USAGE_ERR = 1,SOCKET_ERR = 2,BIND_ERR = 3};
typedef function<void(int,string,uint16_t,string)> func_t;
class udpServer
{
public:
udpServer(const func_t &cb, uint16_t &port,const string &ip = defaultip)
:_callback(cb),_port(port),_ip(ip),_sockfd(-1)
{}
void initServer()
{
_sockfd = socket(AF_INET,SOCK_DGRAM,0);//1.创建socket通信端点
if(_sockfd == -1)//创建失败退出
{
cout<<"socket error:"<<errno<<":"<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
//2.绑定port,ip
struct sockaddr_in local;//#include <arpa/inet.h>
bzero(&local,sizeof(local));//相当于memset(local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);//htons()将端口号转为网络字节序
//local.sin_addr.s_addr = inet_addr(_ip.c_str());//inet_addr(const char *n)将ip转化为网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址bind(可绑定本机所有网络接口,eg:外网ip,内网ip。。。),这是服务器的真实写法
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n == -1)//绑定失败退出
{
cout<<"bind error:"<<errno<<":"<<strerror(errno)<<endl;
exit(BIND_ERR);
}
//Udp Server Over!
}
void start()
{//服务器本质是死循环
for(;;)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
if(s > 0 )
{
buffer[s] = 0;
string clientip = inet_ntoa(peer.sin_addr);//将网络字节序转换为点分十进制的字符串
uint16_t clientport = ntohs(peer.sin_port);//
string message = buffer;
cout<<clientip<<"["<<clientport<<"]##"<<message<<endl;
_callback(_sockfd,clientip,clientport,message);
}
}
}
~udpServer(){}
private:
string _ip;//ip
uint16_t _port;//端口号
int _sockfd;
func_t _callback;
};
}
udpServer.cc
cpp
#include "udpServer.hpp"
#include <memory>
#include <fstream>
#include <unordered_map>
#include "onlineUser.hpp"
#include <pthread.h>
using namespace std;
using namespace Server;
//字符串剪切
static bool cutString(const string &line,string &key,string &val)
{
string sep = ":";
auto pos = line.find(sep);
if(pos == string::npos) return false;
key = line.substr(0,pos);
val = line.substr(pos+1);
return true;
}
//使用手册
static void Usage(string proc)
{
cout<<"Usage\n\t"<<proc<<"local port\n\n"<<endl;
exit(USAGE_ERR);
}
//功能(翻译)实现的数据创建
const string dictTxt = "./data.txt";
unordered_map<string,string> dict;
static void initDict()
{
ifstream in(dictTxt,std::ios::binary);
if(!in.is_open())
{
cerr<<"open file"<<dictTxt<<"error"<<endl;
exit(1);
}
string line;
string key,val;
while(getline(in,line))
{
if(cutString(line,key,val))
{
dict.insert(make_pair(key,val));
}
}
// 遍历每个键值对,auto自动推导类型为pair<string, string>
for (const auto& pair : dict)
{
cout << "键:" << pair.first << ",值:" << pair.second << endl;
}
cout<<"dict load success"<<endl;
}
//回调函数 demo1
void handerMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
//解决业务(翻译)
string response;
auto iter = dict.find(message);
if(iter == dict.end())
{
response = "Not Found";
}
else
{
response = iter->second;
}
//包装发送端的ip和port
struct sockaddr_in cli;
memset(&cli,0,sizeof(cli));
cli.sin_family = AF_INET;
cli.sin_addr.s_addr = inet_addr(clientip.c_str());
cli.sin_port = htons(clientport);
sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&cli,sizeof(cli));
}
//demo2()
void execCommand(int sockfd,string clientip,uint16_t clientport,string cmd)
{
//业务处理(接收指令并执行ls pwd.....
if(cmd.find("rm") != string::npos || cmd.find("rmdir") != string::npos ||
cmd.find("mv") != string::npos)//防止误删操作
{
cerr<<clientip<<"#正在做一个非法的操作--"<<cmd<<endl;
return;
}
string response;
FILE *fp= popen(cmd.c_str(),"r");
if(fp == nullptr) response = cmd + "exec failed";
char line[1024];
while(fgets(line,sizeof(line),fp))
{
response+=line;
}
pclose(fp);
//包装发送端的ip和port
struct sockaddr_in cli;
memset(&cli,0,sizeof(cli));
cli.sin_family = AF_INET;
cli.sin_addr.s_addr = inet_addr(clientip.c_str());
cli.sin_port = htons(clientport);
sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&cli,sizeof(cli));
}
//demo3
onlineUser on;
void routeMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
//业务处理(给所有在线用户转发消息)
if(message == "online") on.addUsers(clientip,clientport);
if(message == "offline") on.delUsers(clientip,clientport);
if(on.isOnline(clientip,clientport))
{
//消息路由
on.broadcastMessage(sockfd,clientip,clientport,message);
}
else//未在线提示在线登录s
{
//包装发送端的ip和port
struct sockaddr_in cli;
memset(&cli,0,sizeof(cli));
cli.sin_family = AF_INET;
cli.sin_addr.s_addr = inet_addr(clientip.c_str());
cli.sin_port = htons(clientport);
string response = "你还没有上线,请输入online上线";
sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&cli,sizeof(cli));
}
}
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
}
initDict();
//string ip = argv[1];
uint16_t port = atoi(argv[1]);//atoi()字符串转整数
std::unique_ptr<udpServer> usvr(new udpServer(routeMessage,port));
usvr->initServer();
usvr->start();
return 0;
}
udpClient.hpp
cpp
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdint>
#include <cstring>
#include <strings.h>
#include <stdlib.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace Client
{
using namespace std;
class udpClient
{
public:
udpClient(const string &serverip,const uint16_t &serverport):
_serverip(serverip),_serverport(serverport),_sockfd(-1),_quit(false)
{}
void initClient()
{
_sockfd = socket(AF_INET,SOCK_DGRAM,0);//1.创建socket通信端点
if(_sockfd == -1)//创建失败退出
{
cout<<"socket error:"<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
//client端需要bind吗??答:必须要,要建立ip接口,不过是有系统创建的!!!
}
static void *readMessage(void *args)
{
pthread_detach(pthread_self());//线程分离
int sockfd = *((int *)args);
while(true)
{
char buffer[1024];
struct sockaddr_in tem;
socklen_t len = sizeof(tem);
ssize_t s = recvfrom(sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&tem,&len);
if(s > 0) buffer[s] = 0;
cout<<buffer<<endl;
}
return nullptr;
}
void run()
{
//新线程负责读取信息
pthread_create(&_reader,nullptr,readMessage,(void *)&_sockfd);
//主线程负责发送消息
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
server.sin_port = htons(_serverport);
string message;
char buf[1024];
while(!_quit)
{
//cout<<"Plase Enter##"<<endl;
//cin>>message;//"不能带空格"
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf) - 1] = 0;
message = buf;
sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
~udpClient()
{}
private:
int _sockfd;
string _serverip;
uint16_t _serverport;
bool _quit;
pthread_t _reader;
};
}
udpClient.cc
cpp
#include "udpClient.hpp"
#include <memory>
using namespace Client;
static void Usage(string proc)
{
cout<<"Usage\n\t"<<proc<<"server_ip server_port\n\n"<<endl;
exit(2);
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
}
uint16_t serverport = atoi(argv[2]);
string serverip = argv[1];
std::unique_ptr<udpClient> ucli(new udpClient(serverip,serverport));
ucli->initClient();
ucli->run();
return 0;
}
onlineUser.hpp
cpp
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cstdint>
#include <cstring>
#include <strings.h>
#include <stdlib.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>
using namespace std;
class User
{
public:
User(const string ip,const uint16_t port):_ip(ip),_port(port)
{}
string ip() {return _ip;}
uint16_t port(){return _port;}
~User(){}
private:
string _ip;
uint16_t _port;
};
class onlineUser
{
public:
onlineUser(){}
~onlineUser(){}
void addUsers(const string &ip,const uint16_t &port)
{
string id = ip + "-" + to_string(port);
users.insert(make_pair(id,User(ip,port)));
}
void delUsers(const string &ip,uint16_t &port)
{
string id = ip + "-" + to_string(port);
users.erase(id);
}
bool isOnline(const string &ip,const uint16_t &port)
{
string id = ip + "-" + to_string(port);
return users.find(id) == users.end() ? false:true;
}
void broadcastMessage(int sockfd,const string &ip,const uint16_t &port,const string &message)
{
for (auto &user : users)
{
// 包装发送端的ip和port
struct sockaddr_in cli;
memset(&cli, 0, sizeof(cli));
cli.sin_family = AF_INET;
cli.sin_addr.s_addr = inet_addr(user.second.ip().c_str());
cli.sin_port = htons(user.second.port());
string response = "[" + ip + ":" + to_string(port) + "]##" + message;
sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&cli, sizeof(cli));
}
}
private:
unordered_map<string,User> users;
};
date.txt
banana:香蕉
apple:苹果
pear:鸭梨
peach:桃子
补充参考内容
地址转换函数
字符串转 in_addr 的函数:

in_addr 转字符串的函数:

其中 inet_pton和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的
in6_addr,因此函数接口是 void *addrptr。

关于 inet_ntoa
inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块
内存来保存 ip 的结果. 那么是否需要调用者手动释放呢?

man 手册上说, inet_ntoa 函数, 是把这个返回结果放到了静态存储区. 这个时候不需要
我们手动进行释放. 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为 inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆
盖掉上一次的结果
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在 APUE 中, 明确提出 inet_ntoa 不是线程安全的函数;
但是在 centos7 上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题
UDP/TCP 网络编程中的地址封装工具
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include<string.h>
class InetAddr
{
public:
//网络转主机
InetAddr(sockaddr_in &sock)
: _addr(sock)
{
//_ip=inet_ntoa(_sockaddrin.sin_addr);
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
//主机转网络
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
_addr.sin_port = htons(_port);
}
InetAddr(uint16_t port) : _port(port), _ip("0")
{
// 主机转网络
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_addr.sin_port = htons(_port);
}
~InetAddr()
{
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
bool operator==(const InetAddr &peer)
{
return _ip == peer._ip && _port == peer._port;
}
std::string StringAddr()
{
return _ip + ":" + std::to_string(_port);
}
socklen_t NetAddrLen()
{
return sizeof(_addr);
}
const struct sockaddr_in &NetAddr() { return _addr; }
private:
sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};