018-Linux-Socket编程-UDP

Socket编程-UDP

1. 相关接口和命令

1.1 创建套接字

c 复制代码
#include <sys/socket.h>

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

功能:创建套接字,在Linux中一切皆文件,这里可以理解为,打开"网卡"这个文件,发送信息,就是向网卡里写,接收消息,就是从网卡里读。

参数详解:

  1. domain(协议族/地址族) 指定通信的协议族,常见的有:
    • AF_INET:IPv4协议。
    • AF_INET6:IPv6协议。
    • AF_UNIX:本地通信(Unix域套接字)。
    • AF_PACKET:底层数据链路层通信(如原始套接字)。
  2. type(套接字类型) 指定通信语义,常见的有:
    • SOCK_STREAM:面向连接的可靠通信(如TCP)。
    • SOCK_DGRAM:无连接的不可靠通信(如UDP)。
    • SOCK_RAW:原始套接字,允许直接处理IP层数据。
    • SOCK_SEQPACKET:提供可靠、有序的包传输(如SCTP)。
  3. protocol(具体协议) 通常指定为0,表示自动选择默认协议。例如:
    • domainAF_INETtypeSOCK_STREAM时,协议默认为TCP。
    • typeSOCK_DGRAM时,协议默认为UDP。

返回值:

  • 成功 :返回一个套接字文件描述符(非负整数),用于后续操作(如bind()connect()等)。
  • 失败 :返回-1,并设置全局变量errno表示错误原因(如EINVAL参数无效、EAFNOSUPPORT地址族不支持等)。

1.2 绑定端口号

c 复制代码
#include <sys/socket.h>

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

功能:用于将套接字与一个特定的网络地址(IP地址和端口号)进行绑定。对于服务器程序来说,这是必经步骤,用于指定监听的地址和端口。

参数详解:

  1. sockfdsocket() 系统调用返回的套接字文件描述符。
  2. addr 指向一个结构体的指针,该结构体包含了要绑定的地址信息。由于 bind() 需要支持多种协议族,这里使用通用的 struct sockaddr 结构体指针,实际使用时需要强制转换为具体协议族的结构体(如 struct sockaddr_in 用于 IPv4)。
  3. addrlen addr 所指向结构体的字节长度。

1.3 sockaddr_in结构体

c 复制代码
#include <netinet/in.h>

struct sockaddr_in {
    sa_family_t     sin_family;     /* 地址族:AF_INET 表示 IPv4 */
    in_port_t       sin_port;       /* 端口号(网络字节序) */
    struct in_addr  sin_addr;       /* IPv4 地址 */
    unsigned char   sin_zero[8];    /* 填充字段(通常设为0) */
};

字段详解:

  1. sin_family(地址族) 固定为 AF_INET,表示使用 IPv4 协议。这是与 struct sockaddr_in6(IPv6)的主要区别。
  2. sin_port(端口号) 使用 in_port_t 类型(通常是 uint16_t),存储网络字节序(大端序)的端口号。 关键转换 :必须使用 htons() 函数将主机字节序转换为网络字节序。
  3. sin_addr(IPv4 地址) 是一个 struct in_addr 结构体,通常包含一个 s_addr 字段(uint32_t 类型),存储网络字节序的 IP 地址。 特殊值
    • INADDR_ANY(即 0.0.0.0):表示绑定到所有可用接口。
    • INADDR_LOOPBACK(即 127.0.0.1):表示本地回环地址。
    • INADDR_BROADCAST(即 255.255.255.255):表示广播地址。
  4. sin_zero(填充字段) 用于填充结构体,使其长度与 struct sockaddr(通用地址结构)保持一致。通常用 memset() 清零。

1.4 点分十进制字符串转换32位无符号整数

c 复制代码
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

功能:用于将点分十进制格式的IPv4地址字符串(如 "192.168.1.1")转换为网络字节序的32位无符号整数(in_addr_t 类型)。

参数:

  • cp :指向以空字符结尾的IPv4地址字符串的指针(例如 "10.0.0.1")。

返回值:

  • 成功 :返回以网络字节序表示的32位无符号整数(in_addr_t 类型)。
  • 失败
    • 如果 cp 是无效地址(如格式错误),返回 INADDR_NONE(通常为 255.255.255.255)。
    • 如果 cpNULL,行为未定义。

1.5 32位ip地址转换点分十进制字符串

inet_ntoa

c 复制代码
#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);

功能:将一个 网络二进制格式的 IP 地址 (即 struct in_addr)转换为 点分十进制字符串(如 "192.168.1.1")。

参数:

  • struct in_addr in:一个 in_addr 结构体,其中包含一个 uint32_t 类型的 s_addr 成员,以 网络字节序(大端序) 存储 IP 地址的二进制值。这个结构体是sockaddr_in结构体中的一个成员。

返回值:

  • 成功时 :返回一个指向静态缓冲区的 char * 指针,该缓冲区包含转换后的点分十进制字符串(如 "127.0.0.1")。
  • 失败时 :函数本身没有返回错误值,但如果输入的 in.s_addr 无效,返回的字符串可能不正确。

【注意】这里返回的char*类型,这是在它的内部申请了一块内存,每次返回之前,他都会将最新的转换结果放在这里,然后返回指针,所以无论传进去的参数是如何的,多次调用传回的指针都是一样的,指向同一块内存,此时后面调用的结果会覆盖掉前面调用的结果,那么这意味着inet_ntoa并不是线程安全的,在多线程访问时最好加锁并且将返回的结果拷贝到自己的缓冲区中保存。使用下面的inet_ntop也可以避免上述问题。

inet_ntop

c 复制代码
#include <arpa/inet.h>

const char *inet_ntop(int af, const void * src,
                      char *dst, socklen_t size);

功能:用于将网络地址的二进制表示形式转换为人类可读的字符串格式(如点分十进制或十六进制格式)。它是 inet_ntoa() 的现代化替代品,支持 IPv4 和 IPv6,并且是线程安全的。

参数:

  1. af(地址族) 指定地址类型:
    • AF_INET:IPv4 地址。
    • AF_INET6:IPv6 地址。
  2. src(源地址指针) 指向二进制形式网络地址的指针:
    • 对于 IPv4,指向 struct in_addruint32_t
    • 对于 IPv6,指向 struct in6_addr
  3. dst(目标缓冲区) 指向用于存储结果字符串的缓冲区的指针。调用者必须确保缓冲区足够大。
  4. size(缓冲区大小) dst 缓冲区的字节数。所需最小大小:
    • IPv4:INET_ADDRSTRLEN(通常为 16)。
    • IPv6:INET6_ADDRSTRLEN(通常为 46)。

返回值:

  • 成功 :返回指向 dst 的指针(即字符串的起始地址)。
  • 失败 :返回 NULL,并设置 errno。常见错误包括:
    • EAFNOSUPPORTaf 不是 AF_INETAF_INET6
    • ENOSPCsize 太小,无法容纳结果字符串。

1.6 从套接字接收数据

c 复制代码
#include <sys/socket.h>

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

功能:用于从套接字接收数据,并可选地获取发送方的地址信息。它特别适用于无连接的通信(如UDP),但也适用于有连接的通信(如TCP)。

参数详解:

  1. sockfd 套接字文件描述符,由 socket()accept() 返回。
  2. buf 指向接收缓冲区的指针,用于存储接收到的数据。
  3. len 缓冲区的最大容量(字节数)。
  4. flags 控制接收行为的标志,可通过按位或组合多个标志:
    • MSG_OOB:接收带外数据(紧急数据)。
    • MSG_PEEK:查看数据但不从接收队列中移除。
    • MSG_WAITALL:等待所有数据到达(阻塞直到填满缓冲区或出错)。
    • MSG_NOSIGNAL :当连接关闭时不发送 SIGPIPE 信号(某些系统)。
    • MSG_DONTWAIT:非阻塞模式,如果无数据则立即返回错误。
    • MSG_TRUNC:如果数据报大于缓冲区,截断并返回实际长度(UDP)。
    • MSG_CTRUNC:截断辅助数据。
  5. src_addr 指向 struct sockaddr 结构的指针,用于存储发送方的地址(如IP和端口)。如果不需要,可设为 NULL
  6. addrlen 指向 socklen_t 的指针,指定 src_addr 结构的大小,并在返回时更新为实际存储的地址长度。如果 src_addrNULL,则 addrlen 也应为 NULL

返回值:

  • 成功 :返回接收到的字节数。如果连接正常关闭(如TCP FIN),返回 0
  • 失败 :返回 -1,并设置 errno(如 EAGAIN 表示无数据、ECONNRESET 表示连接重置)。

1.7 向指定地址发送数据

c 复制代码
#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);

功能:用于向指定地址发送数据。它特别适用于无连接的通信(如UDP),但也适用于有连接的通信(如TCP)。

参数:

  1. sockfd 套接字文件描述符,由 socket()accept() 返回。
  2. buf 指向发送缓冲区的指针,包含要发送的数据。
  3. len 要发送的数据长度(字节数)。
  4. flags 控制发送行为的标志,可通过按位或组合多个标志:
    • MSG_OOB:发送带外数据(紧急数据)。
    • MSG_DONTWAIT:非阻塞模式,如果发送缓冲区满则立即返回错误。
    • MSG_NOSIGNAL :当连接关闭时不发送 SIGPIPE 信号。
    • MSG_MORE:表示还有更多数据要发送(延迟发送,优化性能)。
    • MSG_EOR:表示数据结束(某些协议)。
  5. dest_addr 指向目标地址结构(如 struct sockaddr_in)的指针,指定数据接收方的地址和端口。对于已连接的TCP套接字,此参数通常设为 NULL
  6. addrlen dest_addr 结构体的字节长度。如果 dest_addrNULL,则 addrlen 应为0。

返回值:

  • 成功:返回实际发送的字节数(可能小于请求的字节数,尤其在非阻塞模式下)。
  • 失败 :返回 -1,并设置 errno(如 EAGAIN 表示缓冲区满、ECONNRESET 表示连接重置)。

2. V1版本-echo server

接下来实现一个简单的回显服务器和客户端代码。

2.1 前置头文件

这里使用到的日志是前面文章中实现的日志,这里直接cv过来用。

cpp 复制代码
// LockGuard.hpp
// 用于给日志输出时加锁的前置头文件

#pragma once

#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }

    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t *_mutex;
};
cpp 复制代码
// Log.hpp
// 日志头文件

#pragma once

#include <iostream>
#include <fstream>
#include <ctime>
#include <cstring>
#include <cstdarg>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"

namespace log_ns
{

    enum
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string LevelToString(int level)
    {
        switch (level)
        {
        case DEBUG:
            return "DEBUG";
        case INFO:
            return "INFO";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    std::string GetTime()
    {
        time_t now = time(nullptr);
        struct tm *curr_time = localtime(&now);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                 curr_time->tm_year + 1900,
                 curr_time->tm_mon + 1,
                 curr_time->tm_mday,
                 curr_time->tm_hour,
                 curr_time->tm_min,
                 curr_time->tm_sec);
        return buffer;
    }

    class logmessage
    {
    public:
        std::string _level;
        pid_t _pid;
        std::string _filename;
        int _line;
        std::string _time;
        std::string _message;
    };

#define SCREEN_TYPE 1
#define FILE_TYPE 2

    const std::string glogfile = "./log.txt";
    pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;

    class Log
    {
    public:
        Log(const std::string &logfile = glogfile) : _type(SCREEN_TYPE), _logfile(logfile)
        {
        }

        void Enable(int type)
        {
            _type = type;
        }

        void FlushLogToScreen(const logmessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s",
                   lg._level.c_str(),
                   lg._pid,
                   lg._filename.c_str(),
                   lg._line,
                   lg._time.c_str(),
                   lg._message.c_str());
        }

        void FlushLogToFile(const logmessage &lg)
        {
            std::ofstream out(_logfile, std::ios::app);
            if (!out.is_open())
                return;

            char logtxt[2048];
            snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                     lg._level.c_str(),
                     lg._pid,
                     lg._filename.c_str(),
                     lg._line,
                     lg._time.c_str(),
                     lg._message.c_str());
            out.write(logtxt, strlen(logtxt));
            out.close();
        }

        void FlushLog(const logmessage &lg)
        {
            LockGuard LockGuard(&glock); // 加锁,为了防止多个线程同时打印日志造成输出信息错乱。
            switch (_type)
            {
            case SCREEN_TYPE:
                FlushLogToScreen(lg);
                break;
            case FILE_TYPE:
                FlushLogToFile(lg);
                break;
            }
        }

        void logMessage(std::string filename, int line, int level, const char *format, ...)
        {
            logmessage lg;

            lg._level = LevelToString(level);
            lg._pid = getpid();
            lg._filename = filename;
            lg._line = line;
            lg._time = GetTime();

            va_list ap;
            va_start(ap, format);
            char log_msg[1024];
            vsnprintf(log_msg, sizeof(log_msg), format, ap);
            va_end(ap);
            lg._message = log_msg;

            // print log
            FlushLog(lg);
        }

        ~Log()
        {
        }

    private:
        int _type;
        std::string _logfile;
    };

    Log lg;

#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)

#define LOG(Level, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)

}

2.2 nocopy类

对于服务端的对象,我们不希望它可以进行拷贝操作,此时我们只需要让它继承一个nocopy类,nocopy类中,将拷贝构造、赋值重载设置成delete即可。

cpp 复制代码
// nocopy.hpp

#pragma once

// 不允许派生类对象进行拷贝操作
class nocopy
{
public:
    nocopy() {}
    ~nocopy() {}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

2.3 UdpServer类

在这个类中我们希望初始化的时候,使用用户提供的ip和端口号来进行socket和bind,在运行过程中,只需要不断的读入信息,然后加上一个前缀,再返回即可。

cpp 复制代码
#pragma once

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

using namespace log_ns;

static const int gsockfd = -1;
static const int glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
public:
    UdpServer(const std::string &localip, int localport = glocalport)
        : _sockfd(gsockfd), _localport(localport), _localip(localip), _isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd);

        // 2.绑定端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport); // 注意主机序列转网络序列
        local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 需要4字节ip,需要网络序列ip

        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }

    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0)
            {
                inbuffer[n] = '\0';
                std::string echo_string = "[udp_server echo] # ";
                echo_string += inbuffer;

                ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd) ::close(_sockfd);
    }

private:
    int _sockfd;
    int _localport;
    std::string _localip;
    bool _isrunning;
};

2.4 ServerMain

这里只需要创建Udpserver的对象,然后使其运行起来即可。

cpp 复制代码
// ServerMain.cc

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

using namespace log_ns;

int main()
{
    uint16_t port = 8899;
    std::string ip = "127.0.0.1";
    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
    usvr->InitServer();
    usvr->Start();

    return 0;
}

2.5 ClientMain

由于这里比较简单,就不像Server一样封装成类了,运行前需要用户提供服务端的ip和端口号,创建socket,然后让用户输入信息,发送信息到服务端,再接收服务器端的信息,回显即可。

cpp 复制代码
// ClientMain.cc

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

// 客户端必须要提前就知道服务器的IP和端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    
    // 发送消息之前,要先知道服务器的地址和端口
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    // client的端口号,一般不自己设定,而是让client OS随机分配
    // client需要bind自己的IP和端口,但是不用显式bind,在首次向server发送数据的时候,OS会自动bind它的IP和端口
    while (true)
    {
        std::string line;
        std::cout << "Please Enter # ";
        std::getline(std::cin, line);

        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if (n > 0)
        {
            char buffer[1024];
            int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr); // 当前对于server的IP和端口只有一个,不需要更改,所以这里不在乎发送方信息,置空就行了
            if (m > 0)
            {
                buffer[m] = '\0';
                std::cout << buffer << std::endl;
            }
            else break;
        }
        else break;
    }

    ::close(sockfd);

    return 0;
}

2.6 运行效果

2.7 关于绑定IP地址的问题

对于上面的代码,server端绑定127.0.0.1(回环地址),这个地址只是用于做本地测试,这里可以改成绑定0.0.0.0,这个的意思是绑定服务器的所有ip地址(假设端口为8888),一台服务器可能会有多个ip地址,绑定0.0.0.0,无论是哪个ip地址,只要是发送给8888端口的信息,这里都能拿到。

这里只需要对Server的代码稍做修改即可:

cpp 复制代码
// UdpServer.hpp

#pragma once

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

using namespace log_ns;

static const int gsockfd = -1;
static const int glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

class UdpServer : public nocopy
{
public:
    UdpServer(int localport = glocalport)
        : _sockfd(gsockfd), _localport(localport), _isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd);

        // 2.绑定端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport); // 注意主机序列转网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 进行任意IP地址绑定,可以收到发到当前机器所有IP的信息

        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }

    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0)
            {
                inbuffer[n] = '\0';
                std::cout << "[udp_client say] # " << inbuffer << std::endl;
                std::string echo_string = "[udp_server echo] # ";
                echo_string += inbuffer;

                ::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd) ::close(_sockfd);
    }

private:
    int _sockfd;
    int _localport;
    bool _isrunning;
};
cpp 复制代码
// ServerMain.cc

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

using namespace log_ns;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);
    EnableScreen();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->InitServer();
    usvr->Start();

    return 0;
}

3. V2版本-DictServer

在上面能进行通信的基础上,为server端添加一些简单功能,这里希望对server端发送一个英文单词,server端可以翻译成中文发送回来。

3.1 InetAddr类

对于上面获取发送方ip和端口号的方式不太优雅,这里我们可以把它封装起来。

cpp 复制代码
// InetAddr.hpp

#pragma once

#include <iostream>
#include <string>
#include <arpa/inet.h>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }
public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }

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

    uint16_t Post()
    {
        return _port;
    }

    ~InetAddr()
    {}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

3.2 Dict类

这里我们希望我们的业务逻辑和UdpServer类的逻辑解耦,所以这里单独封装一个Dict类,然后对UdpServer类进行微调。

在Dict类里,我们希望以文件的方式将我们预设的词典导入到内存中,然后提供一个接口来进行翻译。

cpp 复制代码
// Dict.hpp

#pragma once

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

using namespace log_ns;

const static std::string sep = ": ";

class Dict
{
private:
    void LoadDict(const std::string& path)
    {
        std::ifstream in(path);
        if (!in.is_open())
        {
            LOG(FATAL, "open %s failed!\n", path.c_str());
            exit(1);
        }

        std::string line;
        while (std::getline(in, line))
        {
            LOG(DEBUG, "load info: %s , success\n", line.c_str());
            if (line.empty()) continue;
            auto pos = line.find(sep);
            if (pos == std::string::npos) continue;

            std::string key = line.substr(0, pos);
            if (key.empty()) continue;
            std::string value = line.substr(pos + sep.size());
            if (value.empty()) continue;

            _dict[key] = value;
        }
        LOG(INFO, "load %s done\n", path.c_str());

        in.close();
    }
public:
    Dict(const std::string &dict_path): _dict_path(dict_path)
    {
        LoadDict(_dict_path);
    }

    std::string Translate(std::string word)
    {
        if (word.empty() || !_dict.count(word)) return "None";
        return _dict[word];
    }

    ~Dict()
    {}
private:
    std::unordered_map<std::string, std::string> _dict;
    std::string _dict_path;
};
cpp 复制代码
// UdpServer.hpp

#pragma once

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

using namespace log_ns;

static const int gsockfd = -1;
static const int glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

using func_t = std::function<std::string(const std::string)>;

class UdpServer : public nocopy
{
public:
    UdpServer(func_t func, uint16_t localport = glocalport)
        : _func(func), _sockfd(gsockfd), _localport(localport), _isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd);

        // 2.绑定端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport); // 注意主机序列转网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 进行任意IP地址绑定,可以收到发到当前机器所有IP的信息

        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n"); 
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }

    void Start()
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                inbuffer[n] = '\0';
                std::cout << "[" << addr.Ip() << ":" << addr.Post() << "] # " << inbuffer << std::endl;
               
                std::string result = _func(inbuffer);

                ::sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd) ::close(_sockfd);
    }

private:
    int _sockfd;
    int _localport;
    bool _isrunning;

    func_t _func;
};
cpp 复制代码
// ServerMain.cc

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

using namespace log_ns;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

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

    Dict dict("./dict.txt");
    func_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(translate, port);
    usvr->InitServer();
    usvr->Start();

    return 0;
}
复制代码
// dict.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

从此开始,对于UdpServer类,如果是想这样的收到消息、处理消息、返回消息给发送方,那么UdpServe就可以只负责收发信息,收到信息,然后交给我们传入的func函数,然后经过我们外部的业务逻辑进行处理,将处理结果返回给UdpServer,UdpServer就将我们处理的信息原封不动的发送回Client。

3.3 运行结果

4. V3版本-简单聊天室

在上面的代码中,都是单进程单线程的处理,这里我们引入之前写过的线程池。

4.1 程序逻辑

对于这个聊天室,UdpServer模块负责收信息,收取完信息后,对于发送信息的用户,还需要维护一个在线用户列表,对于每位用户,使用IP+Port的方式进行唯一标识,每当收到一个用户的消息,如果这个用户不在在线用户列表中,将他添加进去,然后将消息分配给线程池中的线程,让线程一一的去把这条消息发送给其他用户。

对于用户端,我们希望有两个线程,一个用于循环收消息,一个用于循环发消息。

下面的代码以V1版本的代码为基础做修改。

4.2 引入线程池

对之前的线程池稍作修改,加上日志,方便调试,并将线程池改成单例模式,使用static接口来获取线程池。

cpp 复制代码
// Thread.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "Log.hpp"

using namespace log_ns;

namespace ThreadMoudle
{
    using func_t = std::function<void(const std::string&)>;

    class Thread
    {
    public:
        void Excute()
        {
            LOG(DEBUG, "%s, is running\n", _name.c_str());
            _isrunning = true;
            _func(_name);
            _isrunning = false;
        }
    public:
        Thread(const std::string &name, func_t func):_name(name),_func(func)
        {
            LOG(DEBUG, "create %s done\n", name.c_str());
        }

        static void *ThreadRoutine(void *args)
        {
            Thread *self = static_cast<Thread*>(args);
            self->Excute();
            return nullptr;
        }

        bool Start()
        {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if (n != 0) return false;
            return true;
        }

        std::string Status()
        {
            if (_isrunning) return "running";
            return "sleep";
        }

        void Stop()
        {
            if (_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
                LOG(DEBUG, "%s, stop\n", _name.c_str());
            }
        }

        void Join()
        {
            if (!_isrunning)
            {
                ::pthread_join(_tid, nullptr);
                LOG(DEBUG, "%s, joined\n", _name.c_str());
            }
        }

        std::string Name()
        {
            return _name;
        }

        ~Thread()
        {

        }

    private:
        std::string _name;
        pthread_t _tid;
        bool _isrunning;
        func_t _func;
    };
}
cpp 复制代码
// ThreadPool.hpp

#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <functional>
#include <queue>
#include "Thread.hpp"
#include "LockGuard.hpp"

using namespace ThreadMoudle;

static const int gdefaultnum = 5;

template<typename T>
class ThreadPool
{
private:
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }

    void WakeupAll()
    {
        pthread_cond_broadcast(&_cond);
    }

    bool IsEmpty()
    {
        return _task_queue.empty();
    }

    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    void HandlerTask(const std::string &name)
    {
        while (true)
        {
            // 获取任务
            LockQueue();

            // 任务列表为空,且主线程没有调用Stop
            while (IsEmpty() && _isrunning) 
            {
                _sleep_thread_num++;
                LOG(DEBUG, "%s, sleep begin\n", name.c_str());
                Sleep();
                LOG(DEBUG, "%s, sleep end\n", name.c_str());
                _sleep_thread_num--;
            }
            // 任务列表为空,且主线程已调用Stop,线程解锁并退出
            if (IsEmpty() && !_isrunning)
            {
                LOG(DEBUG, "%s, quit\n", name.c_str());
                UnlockQueue();
                break;
            }
            // 任务列表不为空,无论主线程是否调用Stop,都要把剩余任务执行完
            LOG(DEBUG, "%s execute the task\n", name.c_str());
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();

            // 执行任务
            t();
        }
    }

    ThreadPool(int thread_num = gdefaultnum):_thread_num(thread_num), _isrunning(false), _sleep_thread_num(0)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ThreadPool(const ThreadPool<T> &) = delete;
    void operator= (const ThreadPool<T> &) = delete;

    void Init()
    {
        func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        for (int i = 0; i < _thread_num; i++)
        {
            std::string threadname = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(threadname, func);
        }
    }

    void Start()
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            thread.Start();
        }
    }
public:
    void Stop()
    {
        LockQueue();
        _isrunning = false;
        WakeupAll();
        UnlockQueue();
    }

    static ThreadPool<T> *GetInstance()
    {
        if (_tp == nullptr)
        {
            LockGuard lockguard(&_sig_mutex);
            if (_tp == nullptr)
            {
                _tp = new ThreadPool();
                _tp->Init();
                _tp->Start();
            }
        }
        
        return _tp;
    }

    void Equeue(const T &in)
    {
        LockQueue();
        if (_isrunning) // 只有处于运行状态,才可以添加任务。
        {
            _task_queue.push(in);
            if (_sleep_thread_num) Wakeup();
        }
        UnlockQueue();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
private:
    int _thread_num;
    std::vector<Thread> _threads;
    std::queue<T> _task_queue;
    bool _isrunning;

    int _sleep_thread_num;

    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    // 单例模式
    static ThreadPool<T> *_tp;
    static pthread_mutex_t _sig_mutex;
};

template<typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;

template<typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;

4.3 InetAddr类

这里还是一样,封装一个类以方便获取IP和Port,这里还需要重载==,方便后续的代码比较两个IP+Port是否相同。

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <arpa/inet.h>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        _ip = inet_ntoa(addr.sin_addr);
    }
public:
    InetAddr(const struct sockaddr_in &addr):_addr(addr)
    {
        ToHost(addr);
    }

    bool operator==(const InetAddr& addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }

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

    uint16_t Post()
    {
        return _port;
    }

    struct sockaddr_in Addr()
    {
        return _addr;
    }

    ~InetAddr()
    {}
private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

4.4 UdpServer类

和之前一样,UdpServer负责接收信息,不同的是,不再由它发送信息,而是将信息传给我们传入的回调函数来处理。

cpp 复制代码
#pragma once

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

using namespace log_ns;

static const int gsockfd = -1;
static const int glocalport = 8888;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR
};

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

class UdpServer : public nocopy
{
public:
    UdpServer(service_t func, int localport = glocalport)
        : _func(func), _sockfd(gsockfd), _localport(localport), _isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建socket文件
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket error\n");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd);

        // 2.绑定端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_localport); // 注意主机序列转网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 进行任意IP地址绑定,可以收到发到当前机器所有IP的信息

        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error\n"); 
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "socket bind success\n");
    }

    void Start()
    {
        _isrunning = true;
        char message[1024];
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = ::recvfrom(_sockfd, message, sizeof(message) - 1, 0, (sockaddr*)&peer, &len);
            if (n > 0)
            {
                InetAddr addr(peer);
                message[n] = '\0';
                LOG(DEBUG, "[%s:%d] # %s\n", addr.Ip().c_str(), addr.Post(), message);
                _func(_sockfd, message, addr);
                LOG(DEBUG, "erturn udpserver");
            }
            else
            {
                LOG(ERROR, "recvfrom, error\n");
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd > gsockfd) ::close(_sockfd);
    }

private:
    int _sockfd;
    int _localport;
    bool _isrunning;
    service_t _func;
};

4.5 Route类

这个类里封装了需要传给UdpServer的任务,这里需要拿到sockfd、收到的信息、信息的发送方(用户IP+Port)三个数据,然后分下面几步处理:

  • 对于这个类,类中首先需要维护一个在线用户列表
  • 先判断用户是否存在列表中,如果不存在则添加,存在则什么都不做
  • 判断用户是否发送退出信息,如果发送的是退出信息,将用户从列表中移除
  • 将消息进行处理,加上发送者的信息
  • 将发送任务交给线程池中的线程,然后由线程进行发送,主执行流则继续回到UdpServer接收信息
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <pthread.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include "LockGuard.hpp"

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

class Route
{
private:
    void CheckOnlineUser(InetAddr& who)
    {
        LockGuard lockguard(&_mutex);
        for(auto& user : _online_user)
        {
            if (user == who) return;
        }
        _online_user.push_back(who);
        LOG(DEBUG, "add user %s:%d\n", who.Ip().c_str(), who.Post());
    }

    void Offline(InetAddr& who)
    {
        LockGuard lockguard(&_mutex);
        auto iter = _online_user.begin();
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == who)
            {
                _online_user.erase(iter);
                LOG(DEBUG, "user %s:%d quit\n", who.Ip().c_str(), who.Post());
                break;
            }
        }
    }

    void ForwardHelper(int sockfd, const std::string message)
    {
        LockGuard lockguard(&_mutex);
        for (auto& user : _online_user)
        {
            struct sockaddr_in peer = user.Addr();
            LOG(DEBUG, "sendto [%s:%d]\n", user.Ip().c_str(), user.Post());
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
        }
    }
public:
    Route()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }

    void Forward(int sockfd, const std::string message, InetAddr who)
    {
        // 1.该用户是否在在线列表中,不在则添加
        CheckOnlineUser(who);

        // 2.如果信息为"QUIT",则意味着退出
        if (message == "QUIT")
        {
            Offline(who);
        }

        // 3.构造信息
        std::string new_message = "[" + who.Ip() + ":" + std::to_string(who.Post()) + "] # " + message;

        // 4.who一定在用户列表,把who的信息发送给所有其他人
        task_t t = std::bind(&Route::ForwardHelper, this, sockfd, new_message);
        ThreadPool<task_t>::GetInstance()->Equeue(t);
    }

    ~Route()
    {
        pthread_mutex_destroy(&_mutex);
    }
private:
    std::vector<InetAddr> _online_user;
    pthread_mutex_t _mutex;
};

4.6 ServerMain

主函数的内容也需要稍作修改,先定义Route对象,再将Forward接口进行绑定,再使用这个函数对象来new一个UdpServer。

cpp 复制代码
// ServerMain.cc

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

using namespace log_ns;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " server-port" << std::endl;
        exit(0);
    }

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

    Route messageRoute;

    service_t message_route = std::bind(&Route::Forward, &messageRoute, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(message_route, port);
    usvr->InitServer();
    usvr->Start();

    return 0;
}

4.7 ClientMain

对于客户端,创建两个线程,一个用于专门发消息,一个用于专门收消息。

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

using namespace ThreadMoudle;

int InitClient()
{
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    return sockfd;
}

void RecvMessage(int sockfd, const std::string &name)
{
    while (true)
    {
        char buffer[1024];
        int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr); // 当前对于server的IP和端口只有一个,不需要更改,所以这里不在乎发送方信息,置空就行了
        if (n)
        {
            buffer[n] = '\0';
            std::cout << buffer << std::endl;
        }
        else break;
}
}

void SendMessage(int sockfd, std::string serverip, int serverport, const std::string &name)
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    std::string cli_profix = name + "# ";
    while (true)
    {
        std::string line;
        std::cout << cli_profix;
        std::getline(std::cin, line);

        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
        if (n <= 0) 
        {
            break;
        }
    }
}

// 客户端必须要提前就知道服务器的IP和端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }

    int sockfd = InitClient();
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    
    Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));
    Thread sender("sender-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));

    recver.Start();
    sender.Start();

    recver.Join();
    sender.Join();

    ::close(sockfd);

    return 0;
}

4.8 运行效果

这里使用了五个终端,其中中间为服务端,旁边四个为客户端,模拟多用户接入同一个聊天室。

由于客户端的收消息和发消息在同一个终端下,会导致消息错乱,这里提供一个解决思路:可以通过重定向解决,将输入的相关消息重定向到一个终端,输出的消息重定向到一个终端,就可以实现一个窗口收,一个窗口发,由于这里还需要将代码的输出输出消息部分做修改,这里就不再演示。

相关推荐
十五年专注C++开发1 小时前
tiny-process-library:一个用 C++ 编写的轻量级、跨平台(支持 Windows、Linux、macOS)的进程管理库
linux·c++·windows·进程管理
学不完的1 小时前
Nginx
linux·运维·nginx·运维开发
汇智信科1 小时前
汇智信科网络考试系统:以技术赋能,重构在线测评新范式
linux·数据库·mysql·oracle·sqlserver·java技术
码农编程录1 小时前
【notes14】debugfs
linux
数据与人1 小时前
Linux中Too many open files错误的解决
linux·服务器·前端
Joren的学习记录1 小时前
【Linux运维大神系列】k8s项目部署实战
linux·运维·kubernetes
杰克崔1 小时前
android的lmkd的实现及代码分析
android·linux·运维·服务器·车载系统
Codefengfeng1 小时前
webshell流量分析-Practice1
linux·web安全
BullSmall1 小时前
从2026年春晚 详细分析未来IT行业的发展
linux·运维·服务器·数据库