完整教程:【Linux】Socket编程UDP

一、socket编程相关接口函数(UDP)

需要包含头文件 <sys/socket.h>。

  • socket函数

功能:创建通信端点(套接字)。

原型:

int socket(int domain, int type, int protocol);

参数说明:

  • domain:指定通信域,常见值:AF_INET:IPv4 协议;AF_INET6:IPv6 协议;AF_UNIX:本地通信(UNIX域)。
  • type:定义数据传输方式:SOCK_STREAM:面向连接的可靠传输(如TCP);SOCK_DGRAM:无连接的报文传输(如UDP);SOCK_RAW:原始套接字(直接访问网络层)。
  • protocol:通常设为 0,表示自动选择默认协议(如 SOCK_STREAM 默认对应TCP)。

返回子:成功返回套接字描述符;失败返回 -1 并设置 errno。

  • bind函数

功能:将套接字(socket)绑定到特定的本地 IP 地址和端口号上。

原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:通过 socket 韩硕返回值获得的套接字描述符。
  • addr:指向 sockaddr 结构体的指针,包含要绑定的地址信息(IP 和 port)。
  • addrlen:addr 结构体的长度,必须真确设置为 sizeof(struct sockaddr_in) 或类似大小。

返回值:成功返回 0;失败返回 -1 并设置 errno。

  • recvfrom函数

功能:

  • recvfrom用于从无连接套接字(如UDP)接收数据。
  • 它能捕获数据内容及发送者的地址(如IP地址和端口号)。
  • 适用于需要响应特定发送者的场景,例如聊天服务器或传感器数据采集。

原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:套接字描述符。
  • buf:指向缓冲区的指针,用于存储接收到的数据。
  • len:缓冲区大小(字节数),指定能接受的最大数据长度。
  • flags:控制接收行为的标志位,常用值为0(无特殊行为)。
  • src_addr:指向 sockaddr 结构体的指针,用于存储发送者的地址信息。
  • addrlen:指向 socklen_t 变量的指针,指定 src_addr 缓冲区大小。

返回值: 成功返回接收到的字节数,失败返回 -1,并设置 errno;返回 0 表示连接已关闭。

注意:

  • 当调用recvfrom时,系统会阻塞(等待)直到数据到达或超时(取决于套接字设置)。
  • 接收到数据后:数据被复制到 buf 缓冲区,src_addr 填充个发送者地址,addrlen 更新为地址的实际长度。
  • 如果数据报长度超过 len,多余部分会被丢弃(取决于协议)。
  • 在 UDP 中,recvfrom 每次接收一个完整的数据报。
  • sendto函数

功能:向指定目标地址发送数据包,无需预先建立连接(无连接模式)。

原型:ssize_t sendto(
    int sockfd,                   // 套接字描述符
    const void *buf,              // 待发送数据的缓冲区
    size_t len,                   // 数据长度(字节数)
    int flags,                    // 发送标志(通常设为 0)
    const struct sockaddr *dest_addr, // 目标地址结构体
    socklen_t addrlen             // 目标地址结构体长度
);

参数说明:

  • sockfd:已创建的套接字描述符。
  • buf:指向待发送数据的指针。
  • len:发送数据的长度。
  • flags:控制发送行为的标志位,常用值:0:默认阻塞发送;MSG_DONTWATT:非阻塞发送;MSG_CONFIRM(Linux特有):确认链路有效。
  • dest_addr:指向目标地址的结构体指针。
  • addrlen:dest_addr 结构体的实际长度,例如:sizeof(struct sockaddr_in)

返回值: 成功返回实际发送的字节数,失败返回 -1 并设置 errno。

二、Echo Server

先随手创建一个不可拷贝的基类,之后实现的服务器类直接继承即可不可拷贝。

// nocopy.hpp
#pragma once
class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy&) = delete;
    nocopy& operator=(const nocopy&) = delete;
    ~nocopy(){}
};

接下来我们将套接字进行简单的封装(后面还有修改):

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
class InetAddr
{
public:
    InetAddr(struct sockaddr_in& addr):_addr(addr)
    {
        _ip = inet_ntoa(addr.sin_addr); // 最后补充里讲解这个函数
        _port = ntohs(addr.sin_port);
    }
    std::string IP() { return _ip; }
    int Port() { return _port; }
    std::string PrintInfo() {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port); // 127.0.0.1:1234
        return info;
    }
    ~InetAddr(){}
private:
    std::string _ip;
    int _port;
    struct sockaddr_in _addr;
};

最后我们使用上面的接口做一个简单的 UDP 服务器。

// Common.hpp
#pragma once
enum {
    Usage_err = 1,
    Socket_err,
    Bind_err
};
// UdpServer.hpp
#pragma once
#include 
#include 
#include 
#include 
#include 
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
const static uint16_t defaultport = 6666;
const static int defaultfd = -1;
const static int defaultsize = 1024;
using namespace LogModule;
class UdpServer : public nocopy
{
public:
    UdpServer(const uint16_t port = defaultport)
    :_port(port), _sockfd(defaultfd){}
    void Init() {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0) {
            LOG(LogLevel::ERROR) << "socket err, " << errno << ": " << strerror(errno);
            exit(Socket_err);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local)); // 置零
        local.sin_addr.s_addr = INADDR_ANY; // 0
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n != 0) {
            LOG(LogLevel::ERROR) << "bind err, " << errno << ": " << strerror(errno);
            exit(Bind_err);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start() {
        char buffer[defaultsize];
        while(true) {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, defaultsize - 1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0) {
                InetAddr addr(peer);
                buffer[n] = 0;
                std::cout << "[" << addr.PrintInfo() << "]# " << buffer << std::endl;
                sendto(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer(){}
private:
    uint16_t _port;
    int _sockfd;
};
  • Log.hpp 之前已经写了,这里不复制粘贴了。
  • 云服务器不允许直接绑定公有IP,我们也不推荐编写服务器的时候,bind明确的IP,推荐直接写成 INADDR_ANY

/* Address to accept any incoming messages. */

#define INADDR_ANY ((in_addr_t) 0x00000000)

在网络编程中,当一个进程需要绑定一个网络接口以进行通信时,可以使用 INADDR_ANY 作为 IP 地址参数。这样做意味着该接口可以接受任何来自 IP 地址的连接请求,无论是本地主机还是远程主机。例如,服务器有多个网卡(每个网卡上有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网卡/IP地址上获取的。

// UdpClient.cc
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Common.hpp"
void Usage(const std::string& process) {
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
        exit(Usage_err);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0) {
        std::cout << "socket error: " << strerror(errno) << std::endl;
        exit(Socket_err);
    }
    // client不需要进行bind吗?一定要!
    // 但是,不需要显示bind,client会在首次发送数据时自动bind
    // 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind 随机端口
    // 为什么?client 会非常多
    // 所以,client需要bind,但不需要显示bind,让本地OS自动随机bind,绑定随机端口
    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    while(true) {
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std:getline(std::cin, inbuffer);
        // 发送消息
        ssize_t n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n > 0) {
            // 接收信息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            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;
            }
            else break; // 出错或连接断开,退出
        }
        else break;
    }
    close(sockfd);
    return 0;
}
// UdpServer.cc
#include 
#include "UdpServer.hpp"
int main()
{
    std::unique_ptr us_ptr = std::make_unique();
    us_ptr->Init();
    us_ptr->Start();
    return 0;
}

三、DictServer

下面我们实现一个简单的英译汉的网络字典。

// Dict.txt

hello - 你好
goodbye - 再见
thank you - 谢谢
yes - 是
no - 不
apple - 苹果
water - 水
book - 书
house - 房子
family - 家庭
one - 一
two - 二
ten - 十
hundred - 百
thousand - 千
time - 时间
day - 天/日
week - 周/星期
month - 月
year - 年
teacher - 教师
doctor - 医生
student - 学生
engineer - 工程师
artist - 艺术家
computer - 电脑
internet - 互联网
software - 软件
data - 数据
innovation - 创新
happy - 快乐
sad - 悲伤
healthy - 健康
busy - 忙碌
tired - 疲惫
freedom - 自由
justice - 正义
knowledge - 知识
success - 成功
opportunity - 机会

// Dict.hpp
#pragma once
#include 
#include 
#include 
#include 
const std::string sep = " - ";
const std::string defaultpath = "./Dict.txt";
class Dict
{
public:
    Dict(const std::string path = defaultpath)
    :_path(path) {
        LoadDict();
    }
    std::string Translate(const std::string& key) {
        auto it = _dict.find(key);
        if(it == _dict.end()) return "Unknow";
        return it->second;
    }
    ~Dict(){}
private:
    void LoadDict() {
        std::ifstream in(_path);
        if(!in.is_open()) {
            std::cerr << "dict open error" << std::endl;
            return;
        }
        std::string line;
        while(std::getline(in, line)) {
            if(line.empty()) break;
            auto pos = line.find(sep);
            if(pos == std::string::npos) continue;
            std::string key = line.substr(0, pos);
            std::string value = line.substr(pos + sep.size());
            _dict[key] = value;
        }
        in.close();
    }
private:
    std::string _path;
    std::unordered_map _dict;
};
// DictServer.hpp
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
using func_t = std::function;
using namespace LogModule;
class DictServer : public nocopy
{
public:
    DictServer(func_t func, const uint16_t port = defaultport)
    :_port(port), _sockfd(defaultfd), _func(func){}
    void Init() {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0) {
            LOG(LogLevel::ERROR) << "socket err, " << errno << ": " << strerror(errno);
            exit(Socket_err);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local)); // 置零
        local.sin_addr.s_addr = INADDR_ANY; // 0
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n != 0) {
            LOG(LogLevel::ERROR) << "bind err, " << errno << ": " << strerror(errno);
            exit(Bind_err);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start() {
        char buffer[defaultsize];
        while(true) {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, defaultsize - 1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0) {
                InetAddr addr(peer);
                buffer[n] = 0;
                std::cout << "[" << addr.PrintInfo() << "]# " << buffer << std::endl;
                std::string resp;
                _func(buffer, &resp);
                sendto(_sockfd, resp.c_str(), resp.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~DictServer(){}
private:
    uint16_t _port;
    int _sockfd;
    func_t _func;
};
// DictServer.cc
#include 
#include "Dict.hpp"
#include "DictServer.hpp"
Dict dict;
void Func(const std::string& req, std::string* resp) {
    *resp = dict.Translate(req);
}
int main()
{
    std::unique_ptr ds_ptr = std::make_unique(Func);
    ds_ptr->Init();
    ds_ptr->Start();
    return 0;
}
// DictClient.cc
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Common.hpp"
void Usage(const std::string& process) {
    std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
        exit(Usage_err);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0) {
        std::cout << "socket error: " << strerror(errno) << std::endl;
        exit(Socket_err);
    }
    // client不需要进行bind吗?一定要!
    // 但是,不需要显示bind,client会在首次发送数据时自动bind
    // 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind 随机端口
    // 为什么?client 会非常多
    // 所以,client需要bind,但不需要显示bind,让本地OS自动随机bind,绑定随机端口
    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    while(true) {
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std:getline(std::cin, inbuffer);
        // 发送消息
        ssize_t n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if(n > 0) {
            // 接收信息
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            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;
            }
            else break; // 出错或连接断开,退出
        }
        else break;
    }
    close(sockfd);
    return 0;
}

DictServer 封装版

// Udp_Socket.hpp
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket
{
public:
    bool Socket() {
        _fd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_fd < 0) {
            perror("socket");
            return false;
        }
        return true;
    }
    void Close() {
        close(_fd);
    }
    bool Bind(const std::string& ip, uint16_t port) {
        sockaddr_in local;
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        ssize_t n = bind(_fd, (sockaddr*)&local, sizeof(local));
        if(n < 0) {
            perror("bind");
            return false;
        }
        return true;
    }
    bool Recvfrom(std::string* buf, std::string* ip = nullptr, uint16_t* port = nullptr) {
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char tmp[4096];
        ssize_t n = recvfrom(_fd, tmp, sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
        if(n < 0) {
            perror("recvfrom");
            return false;
        }
        buf->assign(tmp, n);
        if(ip) *ip = inet_ntoa(peer.sin_addr);
        if(port) *port = ntohs(peer.sin_port);
        return true;
    }
    bool Sendto(const std::string& buf, const std::string& ip, uint16_t port) {
        sockaddr_in addr;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        ssize_t n = sendto(_fd, buf.c_str(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));
        if(n < 0) {
            perror("sendto");
            return false;
        }
        return true;
    }
private:
    int _fd;
};
// Udp_Server.hpp
#pragma once
#include "Udp_socket.hpp"
#include 
using func_t = std::function;
class UdpServer
{
public:
    UdpServer() {
        assert(_sock.Socket());
    }
    bool Start(const std::string& ip, uint16_t port, func_t func) {
        if(_sock.Bind(ip, port) == false) {
            return false;
        }
        while(true) {
            std::string req;
            std::string resp;
            std::string des_ip;
            uint16_t des_port = 0;
            if(!_sock.Recvfrom(&req, &des_ip, &des_port)) continue;
            func(req, &resp);
            _sock.Sendto(resp, des_ip, des_port);
            std::cout << "req: " << req << ", resp: " << resp << std::endl;
        }
        _sock.Close();
        return true;
    }
private:
    UdpSocket _sock;
};
// Udp_Client.hpp
#include "Udp_socket.hpp"
class UdpClient
{
public:
    UdpClient(const std::string& ip, uint16_t port)
    :_ip(ip), _port(port) {
        assert(_sock.Socket());
    }
    bool Recvfrom(std::string* buf) {
        return _sock.Recvfrom(buf);
    }
    bool Sendto(const std::string& buf) {
        return _sock.Sendto(buf, _ip, _port);
    }
    ~UdpClient() { _sock.Close(); }
private:
    UdpSocket _sock;
    // 服务器的 ip 和 port
    std::string _ip;
    uint16_t _port;
};
// dict_server.cc
#include "Udp_Server.hpp"
#include "Dict.hpp"
Dict dict;
void Func(const std::string& req, std::string* resp) {
    *resp = dict.Translate(req);
}
int main(int argc, char* argv[])
{
    if(argc != 3) {
        std::cout << "Usage: " << argv[0] << " server_ip server_port!" << std::endl;
        return 1;
    }
    UdpServer us;
    us.Start(argv[1], atoi(argv[2]), Func);
    return 0;
}
// dict_client.cc
#include "Udp_Client.hpp"
int main(int argc, char* argv[])
{
    if(argc != 3) {
        std::cout << "Usage: " << argv[0] << " server_ip server_port!" << std::endl;
        return 1;
    }
    UdpClient client(argv[1], atoi(argv[2]));
    while(true) {
        std::string word;
        std::cout << "请输入英文单词: ";
        std::cin >> word;
        if(!std::cin) {
            std::cout << "Good Bye!" << std::endl;
            break;
        }
        client.Sendto(word);
        std::string ret;
        client.Recvfrom(&ret);
        std::cout << word << "意思是: " << ret << std::endl;
    }
    return 0;
}

四、简单聊天室

// Route.hpp
#pragma once
#include 
#include 
#include 
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogModule;
class Route
{
public:
    void MessageRoute(int sockfd, const std::string& message, InetAddr& peer) {
        if(!IsExist(peer)) AddUser(peer);
        std::string send_message = peer.PrintInfo() + "# " + message;
        std::cout << send_message << std::endl;
        for(auto& user : _online_user) {
            // if(user == peer) continue;
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr*)&(user.GetAddr()), sizeof(user.GetAddr()));
        }
        if(message == "QUIT") DeleteUser(peer);
    }
private:
    bool IsExist(InetAddr& peer) {
        for(auto& user : _online_user) {
            if(user == peer) return true;
        }
        return false;
    }
    void AddUser(InetAddr& peer) {
        LOG(LogLevel::INFO) << "聊天室新增一位用户: " << peer.PrintInfo();
        _online_user.push_back(peer);
    }
    void DeleteUser(InetAddr& peer) {
        for(auto it = _online_user.begin(); it != _online_user.end(); ++it) {
            if(*it == peer) {
                LOG(LogLevel::INFO) << "一位用户退出了聊天室: " << peer.PrintInfo();
                _online_user.erase(it);
                break;
            }
        }
    }
private:
    // 用户首次发出消息认为登入
    std::vector _online_user; // 在线用户
};
// UdpServer.hpp
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using func_t = std::function;
const static int defaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
    :_sockfd(defaultsockfd), _port(port), _func(func), _running(false){}
    void Init() {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0) {
            LOG(LogLevel::ERROR) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        ssize_t n = bind(_sockfd, (const sockaddr*)&local, sizeof(local));
        if(n < 0) {
            LOG(LogLevel::ERROR) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start() {
        _running = true;
        while(_running) {
            char buffer[4096];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);
            if(n > 0) {
                buffer[n] = 0;
                InetAddr client(peer);
                _func(_sockfd, buffer, client);
            }
        }
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _running;
    func_t _func;
};
// ServerMain.cc
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
#include 
using task_t = std::function;
int main(int argc, char* argv[])
{
    if(argc != 2) {
        std::cout << "Usage: " << argv[0] << " server_port" << std::endl;
        return 3;
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr route = std::make_unique();
    auto tp = ThreadPool::GetInstance();
    std::unique_ptr us = std::make_unique(port, [&](int sockfd, const std::string& message, InetAddr& peer){
        task_t t = std::bind(&Route::MessageRoute, route.get(), sockfd, message, peer);
        tp->Push(t);
    });
    us->Init();
    us->Start();
    return 0;
}
// ClientMain.hpp
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "InetAddr.hpp"
int sockfd = -1;
struct sockaddr_in peer;
void ClientQuit(int signo) {
    (void)signo;
    std::string message = "QUIT";
    sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&peer, sizeof(peer));
    exit(0);
}
void* Recver(void* arg) {
    char buffer[4096];
    while(true) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);
        if(n > 0) {
            buffer[n] = 0;
            std::cerr << buffer << std::endl; // 方便查看效果
        }
        else {
            perror("recvfrom");
            break;
        }
    }
    std::cout << "我退出了!" << std::endl;
    return nullptr;
}
int main(int argc, char* argv[])
{
    if(argc != 3) {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    signal(2, ClientQuit);
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0) {
        std::cerr << "socket error!" << std::endl;
        return 2;
    }
    std::cerr << "socket success, sockfd: " << sockfd << std::endl;
    bzero(&peer, sizeof(peer));
    peer.sin_addr.s_addr = inet_addr(ip.c_str());
    peer.sin_family = AF_INET;
    peer.sin_port = htons(port);
    pthread_t tid;
    pthread_create(&tid, nullptr, Recver, nullptr);
    std::string online = "我来啦!!!";
    sendto(sockfd, online.c_str(), online.size(), 0, (const sockaddr*)&peer, sizeof(peer));
    while(true) {
        std::cout << "Please Enter: ";
        std::string message;
        std::getline(std::cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&peer, sizeof(peer));
    }
    close(sockfd);
    return 0;
}

五、补充内容

地址转换函数

这里只介绍IPv4的socket网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位的IP地址。

但是我们通常用点分十进制的字符串表示 IP 地址,以下的函数可以在字符串表示 和 在 in_addr 表示之间转换。

字符串转 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 函数,是把这个返回结果放到了静态存储区,这个时候不需要我们进行手动释放。

那么问题来了,如果我们多次调用这个函数,会有怎样的效果呢?参考下面代码:

#include 
#include 
#include 
int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);
    printf("ptr1:%s, ptr2:%s\n", ptr1, ptr2);
    return 0;
}

因为 inet_ntoa 把结果放到了自己内部一个静态存储区,这样第二次调用时的结果会覆盖第一次的结果。

思考:

  • 如果有多个线程调用 inet_ntoa 函数,是否会出现异常情况呢?
  • 在 APUE 中,明确提出 inet_ntoa 不是线程安全函数。
  • 在多线程环境下,推荐使用 inet_ntop 函数,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
posted on 2026-02-06 17:58  ljbguanli  阅读(6)  评论(0)    收藏  举报