【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

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

cpp 复制代码
// nocopy.hpp

#pragma once

class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy&) = delete;
    nocopy& operator=(const nocopy&) = delete;
    ~nocopy(){}
};

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

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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 服务器。

cpp 复制代码
// Common.hpp

#pragma once

enum {
    Usage_err = 1,
    Socket_err,
    Bind_err
};
cpp 复制代码
// UdpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#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地址上获取的。

cpp 复制代码
// UdpClient.cc

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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;
}
cpp 复制代码
// UdpServer.cc

#include <memory>
#include "UdpServer.hpp"

int main()
{
    std::unique_ptr<UdpServer> us_ptr = std::make_unique<UdpServer>();
    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 - 机会

cpp 复制代码
// Dict.hpp

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>

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<std::string, std::string> _dict;
};
cpp 复制代码
// DictServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <functional>
#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<void(const std::string& req, std::string* resp)>;

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;
};
cpp 复制代码
// DictServer.cc

#include <memory>
#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<DictServer> ds_ptr = std::make_unique<DictServer>(Func);
    ds_ptr->Init();
    ds_ptr->Start();
    return 0;
}
cpp 复制代码
// DictClient.cc

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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 封装版

cpp 复制代码
// Udp_Socket.hpp

#pragma once

#include <iostream>
#include <string>
#include <stdlib.h>
#include <cassert>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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;
};
cpp 复制代码
// Udp_Server.hpp

#pragma once

#include "Udp_socket.hpp"

#include <functional>

using func_t = std::function<void(const std::string& req, std::string* resp)>;

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;
};
cpp 复制代码
// 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;
};
cpp 复制代码
// 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;
}
cpp 复制代码
// 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;
}

四、简单聊天室

cpp 复制代码
// Route.hpp

#pragma once

#include <iostream>
#include <string>
#include <vector>
#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<InetAddr> _online_user; // 在线用户
};
cpp 复制代码
// UdpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

using func_t = std::function<void(int sockfd, const std::string& message, InetAddr& peer)>;
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;
};
cpp 复制代码
// ServerMain.cc

#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
#include <memory>

using task_t = std::function<void()>;

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> route = std::make_unique<Route>();

    auto tp = ThreadPool<task_t>::GetInstance();

    std::unique_ptr<UdpServer> us = std::make_unique<UdpServer>(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;
}
cpp 复制代码
// ClientMain.hpp

#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#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 函数,是把这个返回结果放到了静态存储区,这个时候不需要我们进行手动释放。

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

cpp 复制代码
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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 函数,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
相关推荐
嵌入式小能手4 小时前
飞凌嵌入式ElfBoard-Vim编辑器之Vim常用操作命令
linux·编辑器·vim
迷路爸爸1804 小时前
源码编译安装最新 tmux 教程(含 Debian/Ubuntu/CentOS/Arch/macOS 等系统)
linux·ubuntu·macos·centos·debian·tmux·archlinux
励志不掉头发的内向程序员4 小时前
【Linux系列】掌控 Linux 的脉搏:深入理解进程控制
linux·运维·服务器·开发语言·学习
東雪蓮☆4 小时前
K8s Ingress 详解与部署实战
linux·运维·kubernetes
望获linux4 小时前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统
真正的醒悟4 小时前
什么是网络割接
运维·服务器·网络
Bruce_Liuxiaowei5 小时前
Win7虚拟机加入域错误排查指南:解决无法启动服务问题
运维·网络·windows·安全·网络安全
聆风吟º5 小时前
无需 VNC / 公网 IP!用 Docker-Webtop+cpolar,在手机浏览器远程操控 Linux
linux·运维·docker
歪歪1005 小时前
使用 Wireshark 进行 HTTP、MQTT、WebSocket 抓包的详细教程
网络·websocket·测试工具·http·wireshark