Linux的Socket编程之UDP

目录

1、UDP编程的相关接口

[1.1 创建套接字](#1.1 创建套接字)

[1.2 绑定套接字](#1.2 绑定套接字)

[1.3 接收消息](#1.3 接收消息)

[1.4 发送消息](#1.4 发送消息)

[1.5 网络数据的转化](#1.5 网络数据的转化)

2、EchoServer

[2.1 大致思路](#2.1 大致思路)

[2.2 UdpServer.hpp](#2.2 UdpServer.hpp)

[2.3 UdpServer.cc](#2.3 UdpServer.cc)

[2.4 UdpClient.cc](#2.4 UdpClient.cc)

[2.5 示例及完整代码](#2.5 示例及完整代码)

3、DictServer

[3.1 大致思路](#3.1 大致思路)

[3.2 InetAddr.hpp](#3.2 InetAddr.hpp)

[3.3 Dict.hpp](#3.3 Dict.hpp)

[3.4 UdpServer.hpp](#3.4 UdpServer.hpp)

[3.5 UdpServer.cc](#3.5 UdpServer.cc)

[3.6 UdpClient.cc](#3.6 UdpClient.cc)

[3.7 示例及完整代码](#3.7 示例及完整代码)

4、ChatServer

[4.1 大致思路](#4.1 大致思路)

[4.2 Route.hpp](#4.2 Route.hpp)

[4.3 UdpServer.hpp](#4.3 UdpServer.hpp)

[4.4 UdpServer.cc](#4.4 UdpServer.cc)

[4.5 UdpClient.cc](#4.5 UdpClient.cc)

[4.6 示例及完整代码](#4.6 示例及完整代码)


1、UDP编程的相关接口

1.1 创建套接字

  • 创建套接字 本质上是在内核中创建一个用于网络通信 的抽象 "文件对象 ",并通过文件描述符 让用户进程操作它。
cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
// 创建套接字
int socket(int domain, int type, int protocol);

#include <unistd.h>
// 关闭套接字
int close(int fd);
  • socket(AF_INET****或 PF_INET,SOCK_DGRAM,0);
  • AF_INET表示:IPv4互联网域;AF_INET+SOCK_DGRAM表示: Udpprotocol通常是0AF_INET**=**PF_INET
  • socket()返回值success,返回一个socket的文件描述符 (可以读也可以写 ,且是全双工 (有独立的发送缓冲区和接收缓冲区,可以边读边写 ));error,返回**-1**。
  • close()返回值success,返回0error,返回**-1**。

1.2 绑定套接字

  • 绑定套接字IP + 端口 )的本质是给通信端点分配一个唯一的网络标识,让消息能在网络中 "准确投递"。
cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
        socklen_t addrlen);
  • sockfd套接字文件描述符

  • 网络通信addr一般传struct sockaddr_in*强转struct sockaddr*类似于多态

  • struct sockaddr_in****定义在 netinet/in.h中。

  • sin_familyAF_INET,表示IPv4互联网域。

  • sin_port无符号16位整型端口号0-1023专用端口号1024-65535可分配端口号

  • s_addr无符号32位整型IP地址,IP地址一般的表示形式是点分十进制,如:192.168.0.1,但是用字符串需要15个字节,其实4个字节(4个0-255)就行。

  • addrlenaddr大小

  • bind()返回值success,返回0error,返回**-1**。

1.3 接收消息

cpp 复制代码
#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套接字文件描述符
  • *buf输出型参数 ,输出信息(void*,可以是任意类型),lenbuf大小
  • flags0阻塞等待
  • *src_addr输出型参数 ,网络通信,一般传struct sockaddr_in*强转struct sockaddr*带有发送方IP端口号
  • *addrlen,既是输入型参数 ,表示src_addr****大小 ;也是输出型参数,表示实际使用的地址结构大小。
  • recvfrom()返回值success,返回接收到的字节数error,返回**-1**。
  • ssize_t表示有符号的整数类型

1.4 发送消息

cpp 复制代码
#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);
  • sockfd套接字文件描述符
  • *buf输出型参数 ,输入信息(void*,可以是任意类型),lenbuf大小。带了const是输入,。
  • flags0阻塞等待
  • *dest_addr输入型参数 ,网络通信,一般传struct sockaddr_in*强转struct sockaddr*带有接收方IP端口号
  • addrlen,表示src_addr****大小
  • sendto()返回值success,返回发送的字节数error,返回**-1**。
  • ssize_t表示有符号的整数类型

1.5 网络数据的转化

  • 在处理struct sockaddr_inIP端口号 时,注意对网络序列的转化
  • 注意:
  1. 网络中的数据 都是大端存储 (高位字节放低地址),例如:
    一个 32 位整数 0x12345678(4 字节),在大端主机中存储为 12 34 56 78(高位字节在前(低地址)),在小端主机中存储为 78 56 34 12(低位字节在前(低地址))。
cpp 复制代码
#include <arpa/inet.h>

// 端口号 主机 -> 网络(大端),因为是host -> net s表示short 16位
uint16_t htons(uint16_t netshort);

// IP 主机 -> 网络(大端),因为是process -> net
int inet_pton(int af, const char *src, void *dst);

  int af,指定地址族(Address Family)。为AF_INET:表示处理 IPv4 地址(32 位)
  const char *src,输入型参数,为点分十进制的IP地址的字符串。
  void *dst,输出型参数,4个字节的 IP 地址(大端)。
                传递&struct sockaddr_in.sin_addr。


// 端口号 网络(大端) -> 主机,因为是net -> host s表示short 16位
uint16_t ntohs(uint16_t netshort);

// IP 网络(大端) -> 主机,因为是net -> process
const char *inet_ntop(int af, const void *src,
                        char *dst, socklen_t size);

  int af,指定地址族(Address Family)。为AF_INET:表示处理 IPv4 地址(32 位)
  const void *src,输入型参数,指向二进制形式的 IP 地址(网络字节序,大端)。
                    传递&struct sockaddr_in.sin_addr。
  char *dst,输出型参数,存储转换后的点分十进制的字符串形式的 IP 地址。
  size为dst的大小。
  1. ntop(),pton(),是线程安全的。
  2. 上面的函数 ,主机 -> 网络(大端),保证struct sockaddr_in 里面的IP端口号大端存储;网络(大端) -> 主机,要取出来会自行判断大小端并进行转化
  3. 字节序(大端 / 小端)解决的是 "多字节数据" 在内存中如何排列的问题。
    char类型在 C 语言中占 1 个字节 (8 位),它本身没有 "高低位字节" 的概念 ------ 单个字节就是最小的存储单位,不存在 "排列顺序" 问题(多字节读取的问题)。

2、EchoServer

2.1 大致思路

  • 实现一个EchoServerUdpClientUdpServer****发什么UdpServer就给UdpClient****回什么
  • UdpClient.ccUdpServer.cc+UdpServer.hpp(因为UdpServer会复杂一点,分开写会清晰一点)。

2.2 UdpServer.hpp

  • 服务端套接字不建议绑定特定的IP ,因为可能一个服务器上有多个IP需要收到来自多个IP的消息 ,所以设置为INADDR_ANY(其实就是0),,可接收该机器上的任意IP
  • 服务器的端口号port 是一个进程标识 ,需要自己设置 ,因为服务器的端口号需要固定
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"

using namespace LogModule;

class UdpServer
{
public:
    UdpServer(uint16_t port)
    :_port(port)
    ,_running(false)
    {}
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        // 2. 给sockfd,绑定IP和端口号
        struct sockaddr_in server;
        memset(&server,0,sizeof(server)); // 初始化server
        server.sin_family = AF_INET;
        server.sin_port = htons(_port); // struct sockaddr_in里面必须是大端存储
        server.sin_addr.s_addr = INADDR_ANY; // INADDR,是0,无所谓大小端

        int n = bind(_sockfd,(struct sockaddr*)&server,sizeof(server));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd: "<< _sockfd;
    }
    
    void Start()
    {
        _running = true;

        while(_running) // 一直运行
        {
            // 1. 收消息
            char buf[128];
            struct sockaddr_in addr;
            memset(&addr,0,sizeof(addr));
            socklen_t len = sizeof(addr);
            ssize_t s = recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&addr,&len);
            if(s > 0)
            {
                buf[s] = 0;
                // 2. 发消息
                std::string echo_string = "server echo@ ";
                echo_string += buf;
                sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&addr,sizeof(addr));
            }
        }
    }

    ~UdpServer()
    {
        int n = close(_sockfd);
        if(n < 0)
            LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" error!";
        LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" success";
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _running;
};

2.3 UdpServer.cc

cpp 复制代码
#include "UdpServer.hpp"
#include <memory>

// 利用命令行参数
// ./udpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: "<< argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port);

    udp_server->Init();
    udp_server->Start();

    return 0;
}

2.4 UdpClient.cc

  • 客户端套接字IP端口号 ,OS会自动绑定 。OS知道IP,会采用随机端口号,避免端口冲突。如:开 2 个终端运行./udpclient,连同一个服务器,不会因为端口冲突报错:每个客户端的端口都是 OS 随机分配的,互不重复。
  • 客户端知道服务器IP端口号,因为客户端和服务端是同一家公司写的。
  • 通常机器上的IP 有(通过ip addr查看),本地环回 IP127.0.0.1,和另一个外部 IP。通过本地环回 IP,要求客户端和服务端必须在同一台机器上 (客户端用本地环回IP连接服务端,客户端自己的 IP 也是本地环回IP),实际上是本地通信 (不通过网络,在OS里转一圈交付给对方),一般用来进行网络代码的测试
cpp 复制代码
#include "Log.hpp"
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace LogModule;

// 利用命令行参数
// ./udpclient server_ip server_port
int main(int argc,char* argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    uint16_t server_port = std::stoi(argv[2]);
    std::string server_ip = argv[1];

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket error!";
        exit(1);
    }
    LOG(LogLevel::INFO) << "socket success : " << sockfd;

    // 2. 给sockfd,绑定IP和端口号,OS自动绑定

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET,server_ip.c_str(),&server.sin_addr);
    while (true)
    {
        // 1. 发消息
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input); // 以\n为结束符

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));

        // 2. 收消息
        char buf[128];
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        socklen_t len = sizeof(addr);
        int s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&addr,&len);
        if(s > 0)
        {
            buf[s] = 0;
            std::cout << buf << std::endl;
        }
    }

    return 0;
}

2.5 示例及完整代码

3、DictServer

3.1 大致思路

  • 实现一个DictServerUdpClientUdpServer****发英文UdpServer就给UdpClient****回对应的中文
  • 现在将网络数据的转化 ,封装成一个类(InetAddr);将翻译的函数 ,封装成一个类(Dict),服务端进行调用。分成多个模块,便于维护和扩展。

3.2 InetAddr.hpp

cpp 复制代码
#pragma once

#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in& addr)
    :_addr(addr)
    {
        // 网络 -> 主机
        char buf[32];
        inet_ntop(AF_INET,&_addr.sin_addr,buf,sizeof(buf)-1);
        _ip = buf;

        _port = ntohs(_addr.sin_port);
    }

    InetAddr(std::string& ip,uint16_t port)
    :_ip(ip)
    ,_port(port)
    {
        // 主机 -> 网络
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);

        _addr.sin_port = htons(_port);
    }

    std::string Ip() const
    {
        return _ip;
    }

    uint16_t Port() const
    {
        return _port;
    }
    
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

3.3 Dict.hpp

cpp 复制代码
#pragma once

#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

static const std::string default_dict = "./dictionary.txt";
static const std::string sep = ": ";

class Dict
{
public:
    Dict(const std::string& path = default_dict)
        : _path(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_path);
        if (!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open " << _path << " error!";
            return false;
        }

        std::string line;
        while (std::getline(in, line))
        {
            int pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }

            _dict.emplace(english, chinese);
        }

        in.close();
        return true;
    }

    std::string Translate(const std::string& word, const InetAddr& client)
    {
        auto it = _dict.find(word);
        if(it == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入翻译模块,[" << client.Ip() << " : " << client.Port() << "]# " << word << " -> None";
            return "None";
        }

        LOG(LogLevel::DEBUG) << "进入翻译模块,[" << client.Ip() << " : " << client.Port() << "]# " << word << " -> "<< it->second;
        return it->second;
    }

private:
    std::string _path; // 路径+文件名
    std::unordered_map<std::string, std::string> _dict;
};

3.4 UdpServer.hpp

cpp 复制代码
#pragma once

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

using namespace LogModule;

using task_t = std::function<std::string(const std::string& word,const InetAddr&)>;

class UdpServer
{
public:
    UdpServer(uint16_t port,task_t func)
    :_port(port)
    ,_running(false)
    ,_func(func)
    {}
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        // 2. 给sockfd,绑定IP和端口号
        struct sockaddr_in server;
        memset(&server,0,sizeof(server)); // 初始化server
        server.sin_family = AF_INET;
        server.sin_port = htons(_port); // struct sockaddr_in里面必须是大端存储
        server.sin_addr.s_addr = INADDR_ANY; // INADDR,是0,无所谓大小端

        int n = bind(_sockfd,(struct sockaddr*)&server,sizeof(server));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd: "<< _sockfd;
    }
    
    void Start()
    {
        _running = true;

        while(_running) // 一直运行
        {
            // 1. 收消息
            char buf[128];
            struct sockaddr_in addr;
            memset(&addr,0,sizeof(addr));
            socklen_t len = sizeof(addr);
            ssize_t s = recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&addr,&len);
            if(s > 0)
            {
                buf[s] = 0;
                // 2. 发消息
                InetAddr client(addr);
                std::string result = _func(buf,client); // 把客户端的消息传过去
                
                sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&addr,sizeof(addr));
            }
        }
    }

    ~UdpServer()
    {
        int n = close(_sockfd);
        if(n < 0)
            LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" error!";
        LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" success";
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _running;
    task_t _func;
};

3.5 UdpServer.cc

cpp 复制代码
#include <memory>
#include "UdpServer.hpp"
#include "Dict.hpp"

// 利用命令行参数
// ./udpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: "<< argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);
    Dict dict;
    dict.LoadDict();
    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port,[&dict](const std::string& buf,const InetAddr& client)->std::string{
        return dict.Translate(buf,client);
    });

    udp_server->Init();
    udp_server->Start();

    return 0;
}

3.6 UdpClient.cc

cpp 复制代码
#include "Log.hpp"
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace LogModule;

// 利用命令行参数
// ./udpclient server_ip server_port
int main(int argc,char* argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket error!";
        exit(1);
    }
    LOG(LogLevel::INFO) << "socket success : " << sockfd;

    // 2. 给sockfd,绑定IP和端口号,OS自动绑定

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET,server_ip.c_str(),&server.sin_addr);
    while (true)
    {
        // 1. 发消息
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input); // 以\n为结束符

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));

        // 2. 收消息
        char buf[128];
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        socklen_t len = sizeof(addr);
        int s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&addr,&len);
        if(s > 0)
        {
            buf[s] = 0;
            std::cout << buf << std::endl;
        }
    }

    return 0;
}

3.7 示例及完整代码

4、ChatServer

4.1 大致思路

  • 实现一个ChatServerUdpServer显示公共的聊天消息 ,并且一个UdpClientUdpServer****发送消息UdpServer会给所有的在线的 UdpClient****发送该消息
  • 我们实现简单一点,客户端使用两个终端会话,一个用来发消息;一个用来显示公共的消息(将标准错误重定向到该终端(先确认会话编号,再 ./udpclient server_ip server_port 2>/dev/pts/会话编号),将收到的公共消息打印到标准错误,就打印到了该终端上了)。
  • 引入线程池(基于前文Linux的线程池-CSDN博客):

4.2 Route.hpp

cpp 复制代码
#pragma once

#include <unordered_set>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "InetAddr.hpp"


using namespace LogModule;
using namespace MutexModule;

class Route
{
private:
    struct Hash
    {
        size_t operator()(const InetAddr& client) const
        {
            struct sockaddr_in addr = client.Addr();
            uint32_t ip = addr.sin_addr.s_addr;
            uint16_t port = addr.sin_port;

            size_t hash1 = std::hash<uint32_t>()(ip);
            size_t hash2 = std::hash<uint16_t>()(port);

            return hash1 ^ (hash2 << 1);
        }
    };

    void AddClient(const InetAddr& client)
    {
        if(_clients.find(client) == _clients.end())
        {
            LOG(LogLevel::INFO) << "新增一个在线用户:" << client.StringAddr();
            _clients.emplace(client);
        }
        else
        {
            LOG(LogLevel::WARNING) << "该用户:" << client.StringAddr() << " 已在线";
        }
    }

    void PopClient(const InetAddr& client)
    {
        if(_clients.find(client) == _clients.end())
        {
            LOG(LogLevel::WARNING) << "该用户:" << client.StringAddr() << " 不存在";
        }
        else
        {
            LOG(LogLevel::INFO) << "删除用户:" << client.StringAddr() << " 成功";
            _clients.erase(client);
        }
    }

public:
    void SendMessage(int sockfd,const std::string& message,const InetAddr& client)
    {
        LockGuard lockguard(_mutex); // 处理任务,可能会冲突,所以加锁

        if(_clients.find(client) == _clients.end()) // 首次发消息,认为是登录
            AddClient(client);

        if(message == "QUIT")
            PopClient(client);

        std::string send_message = client.StringAddr() + "# " + message; // 127.0.0.1:8080# 你好

        for(const auto& cli: _clients)
            sendto(sockfd,send_message.c_str(),send_message.size(),0,(const struct sockaddr*)&(cli.Addr()),sizeof(cli.Addr()));
    }

private:
    std::unordered_set<InetAddr,Hash> _clients;
    Mutex _mutex;
};

4.3 UdpServer.hpp

cpp 复制代码
#pragma once

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

using namespace LogModule;

using func_t = std::function<void(int,const std::string& word,const InetAddr&)>;

class UdpServer
{
public:
    UdpServer(uint16_t port,func_t func)
    :_port(port)
    ,_running(false)
    ,_func(func)
    {}
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd: " << _sockfd;

        // 2. 给sockfd,绑定IP和端口号
        struct sockaddr_in server;
        memset(&server,0,sizeof(server)); // 初始化server
        server.sin_family = AF_INET;
        server.sin_port = htons(_port); // struct sockaddr_in里面必须是大端存储
        server.sin_addr.s_addr = INADDR_ANY; // INADDR,是0,无所谓大小端

        int n = bind(_sockfd,(struct sockaddr*)&server,sizeof(server));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error!";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd: "<< _sockfd;
    }
    
    void Start()
    {
        _running = true;

        while(_running) // 一直运行
        {
            // 1. 收消息
            char buf[128];
            struct sockaddr_in addr;
            memset(&addr,0,sizeof(addr));
            socklen_t len = sizeof(addr);
            ssize_t s = recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&addr,&len);
            if(s > 0)
            {
                buf[s] = 0;
                // 2. 推送任务
                InetAddr client(addr);
                _func(_sockfd,buf,client);
            }
        }
    }

    ~UdpServer()
    {
        int n = close(_sockfd);
        if(n < 0)
            LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" error!";
        LOG(LogLevel::INFO) << "close socket "<< _sockfd <<" success";
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _running;
    func_t _func;
};

4.4 UdpServer.cc

cpp 复制代码
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "ThreadPool.hpp"
#include "Route.hpp"

using namespace ThreadPoolModule;

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

// 利用命令行参数
// ./udpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: "<< argv[0] << " port" << std::endl;
        return 1;
    }

    uint16_t port = std::stoi(argv[1]);

    Route route;
    auto thread_pool = ThreadPool<task_t>::GetInstance();

    std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port,[&route,&thread_pool](int sockfd,const std::string& message,const InetAddr& client){
        auto task = std::bind(&Route::SendMessage,&route,sockfd,message,client); // &route表示第一个参数this指针
        thread_pool->Enqueue(task);
    });

    udp_server->Init();
    udp_server->Start();

    return 0;
}

4.5 UdpClient.cc

cpp 复制代码
#include "Log.hpp"
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <thread>
#include <unistd.h>

using namespace LogModule;

std::string server_ip;
uint16_t server_port;
int sockfd;

bool running = true;

void Send()
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    while (running)
    {
        // 1. 发消息
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input); // 以\n为结束符

        sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        // 发送的QUIT,要让所有在线用户知道
        if (input == "QUIT")
        {
            running = false;
            break;
        }
    }
}

void Receive()
{
    while (running)
    {
        // 2. 收消息
        char buf[128];
        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        socklen_t len = sizeof(addr);
        int s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&addr, &len);
        if (s > 0)
        {
            buf[s] = 0;
            std::cerr << buf << std::endl;
        }
        else if (s == -1)
        {
            if (errno != EAGAIN && errno != EWOULDBLOCK)
            {
                LOG(LogLevel::ERROR) << "recvfrom error!";
            }
            // 超时后不退出循环,继续检查running(此时有机会退出)
        }
    }
}

// 利用命令行参数
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket error!";
        exit(1);
    }
    LOG(LogLevel::INFO) << "socket success : " << sockfd;

    // 2. 给sockfd,绑定IP和端口号,OS自动绑定

    // 关键修正:设置接收超时(例如 3 秒 0 微秒),避免recvfrom无限阻塞
    struct timeval timeout;
    timeout.tv_sec = 3;  // 秒
    timeout.tv_usec = 0; // 微秒
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

    std::thread t1(Send);
    std::thread t2(Receive);

    t1.join();
    t2.join();

    close(sockfd);
    return 0;
}

4.6 示例及完整代码

  • 确认客户端的会话编号:
  • 实例:
  • 完整代码:ChatServer
相关推荐
zimoyin3 小时前
Linux 程序使用 STDOUT 打印日志导致程序“假死”?一次线上 Bug 的深度排查与解决
linux·运维·bug
杜子不疼.3 小时前
【Linux】操作系统的认识
linux·运维·服务器
Dovis(誓平步青云)4 小时前
《Gdb 调试实战指南:不同风格于VS下的一种调试模式》
linux·运维·服务器
小-黯4 小时前
Ubuntu离线安装软件包
linux·运维·ubuntu
学不动CV了4 小时前
C语言(FreeRTOS)中堆内存管理分析Heap_1、Heap_2、Heap_4、Heap_5详细分析与解析(二)
linux·c语言·arm开发·stm32·单片机·51单片机
tt5555555555554 小时前
Linux驱动开发核心概念详解 - 从入门到精通
linux·运维·驱动开发
laolitou_10248 小时前
CentOS 7安装部署RabbitMQ
linux·centos·rabbitmq
aitav010 小时前
⚡ WSL2 搭建 s5p6818 Linux 嵌入式开发平台 (part 3):Wifi驱动移植、ssh移植、e2fsprogs移植
linux·wifi·ssh·嵌入式·e2fsprogs
南枝异客12 小时前
CentOS 7 网络连接问题
linux·运维·centos