Linux-Socket编程UDP

以下是基于 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;
};
相关推荐
运维行者_12 小时前
跨境企业 OPM:多币种订单与物流同步管理,依靠网络自动化与 snmp 软件
大数据·运维·网络·数据库·postgresql·跨境企业
oMcLin12 小时前
如何在Ubuntu 20.04上通过配置ZFS存储池,提升高性能存储系统的可靠性与扩展性
linux·运维·ubuntu
独自破碎E12 小时前
使用Linux的top命令进行性能监控的步骤?
linux
Ha_To12 小时前
2026.1.6 Windows磁盘相关
linux·运维·服务器
牛奶咖啡1312 小时前
shell脚本编程(一)
linux·shell·shell脚本·shell脚本解析·grep命令语法·grep选项详解·正则表达式解析
liulilittle12 小时前
XDP VNP虚拟以太网关(章节:三)
网络·c++·网络协议·信息与通信·通信·xdp
天码-行空12 小时前
【大数据环境安装指南】HBase集群环境搭建教程
大数据·linux·运维·hbase
天空之外13612 小时前
生成一个带 IP 的自签证书、制作Http证书
linux·运维·服务器
无限大.13 小时前
为什么游戏需要“加载时间“?——从硬盘读取到内存渲染
网络·人工智能·游戏
-To be number.wan13 小时前
两道经典IP子网题解析|掌握CIDR与广播地址的奥秘
网络·网络协议·tcp/ip·计算机网络