Linux 基于 UDP 协议的简单服务器-客户端应用

news2024/12/23 10:33:27

目录

一、socket编程接口

1、socket 常见API

socket():创建套接字

bind():将用户设置的ip和port在内核中和我们的当前进程关联

listen()

accept()

2、sockaddr结构

3、inet系列函数

二、UDP网络程序—发送消息

1、服务器udp_server.hpp

initServer()初始化服务器

Start()启动服务器

2、udp_server.cc

3、udp_client.cc 

4、log.hpp日志系统

5、模拟运行过程:

三、UDP网络程序—发送命令

1、popen函数

2、udp_server.hpp

四、实现广播处理结果给每个客户端

五、多线程通信

1、udp_server.hpp

2、udp_server.cc

3、udp_client.cc

udpSend函数

udpRecv函数

main() 函数

4、thread.hpp线程管理

5、log.hpp日志系统

六、Windows客户端


一、socket编程接口

1、socket 常见API

在socket编程中,这些API函数是用于实现网络通信的基本构建模块,下面是每个函数的详细说明:

socket():创建套接字

#include <sys/socket.h>
#include <sys/types.h>
int socket(int domain, int type, int protocol);

这个函数用于创建一个新的套接字。

参数含义如下:

  • domain:地址家族,比如AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:套接字类型,对于TCP协议通常是SOCK_STREAM(面向连接的流式套接字),对于UDP协议则是SOCK_DGRAM(无连接的数据报套接字)。
  • protocol:使用的协议,通常设置为0,此时系统会选择domain和type指定的默认协议(对于TCP是IPPROTO_TCP,对于UDP是IPPROTO_UDP)。

返回值:

  • 函数返回值是一个套接字描述符(socket descriptor),它是系统用来引用这个套接字的句柄,后续的操作如绑定、监听、接受连接或发送数据都将使用这个描述符。

bind():将用户设置的ip和port在内核中和我们的当前进程关联

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

此函数用于将套接字与一个特定的本地地址(IP地址和端口号)关联起来,主要是服务器端在启动服务之前需要调用此函数。

参数含义如下:

  • socket:之前由socket()函数返回的套接字描述符。
  • address:指向sockaddr结构体的指针,该结构体包含了要绑定的IP地址和端口号信息,对于TCP/UDP套接字,通常是sockaddr_in或sockaddr_in6结构体。
  • address_len:地址结构体的长度,即sizeof(sockaddr_in)或sizeof(sockaddr_in6)。

返回值:如果绑定成功,函数返回0;否则返回非零错误码。

listen()

int listen(int socket, int backlog);

只有对于TCP服务端套接字才需要调用此函数,它使套接字进入监听状态,等待客户端的连接请求。参数含义如下:

成功监听后返回0,出错则返回非零错误码。

  • socket:要监听的服务器端套接字描述符。
  • backlog:指定同时可以排队等待处理的最大连接数。超过这个数量的连接请求会被拒绝。

accept()

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

也是只在TCP服务器端使用,用于接受一个客户端的连接请求。参数含义如下:

成功接受一个连接请求后,accept()函数返回一个新的套接字描述符,这个描述符用于与该客户端进行通信。同时,address参数所指向的结构体会填充上客户端的地址信息。

  • socket:已经监听的服务器端套接字描述符。
  • address:用于存储新连接客户端的地址信息的sockaddr结构体指针。
  • address_len:指向一个socklen_t变量的指针,用于记录地址结构体的实际大小,传入时应初始化为地址结构体的大小,返回时会更新为实际填充的大小。

connect()

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

该函数用于TCP客户端建立与服务器的连接。参数含义如下:

当成功连接到服务器时,connect()函数返回0;如果发生错误,则返回非零错误码。通过调用connect()函数,客户端与服务器建立起一条TCP连接,随后可以通过这个套接字进行双向数据传输。

  • sockfd:由socket()函数创建的客户端套接字描述符。
  • addr:指向包含服务器IP地址和端口号信息的sockaddr结构体指针。
  • addrlen:地址结构体的长度。

2、sockaddr结构

socket API作为一种跨网络协议的抽象接口层,旨在适应多种底层网络协议,如IPv4、IPv6及UNIX域套接字等。尽管各类协议的具体地址格式各异,socket API通过巧妙的设计保证了编程上的灵活性和一致性。

netinet/in.h头文件中,分别定义了IPv4和IPv6的地址结构。IPv4地址采用sockaddr_in结构体来封装,其中包含了16位表示地址家族的字段(通常是AF_INET)、16位的端口号以及32位的IPv4地址。而对于IPv6地址,有对应的sockaddr_in6结构体。

不论是IPv4还是IPv6,它们各自对应的标准地址类型常量分别为AF_INETAF_INET6。这样一来,只要持有某个sockaddr结构体的起始地址,通过查看地址家族字段,就能确切得知结构体内部其余字段的含义和布局。

        socket API巧妙地使用了struct sockaddr *这一通用类型来表示各种地址结构体,这意味着API函数可以接收指向不同类型(如sockaddr_insockaddr_in6或UNIX域套接字对应的结构体)的指针作为参数。这种设计带来的显著优点是增强了程序的通用性和可扩展性,使得同一套API函数能够轻松应对不同网络协议下的套接字地址处理,无需针对每种协议单独编写代码。在实际使用中,程序员可能需要将struct sockaddr *指针适当地强制转换为其真正指向的具体结构体类型,以便访问和操作相关的地址信息。

struct sockaddr_in 的结构定义大致如下:

struct sockaddr_in {
    sa_family_t sin_family;   // 地址族,对于 IPv4 地址,该字段应设置为 AF_INET(定义在 <sys/socket.h> 中)
    in_port_t   sin_port;     // 端口号,网络字节序(大端序),可以通过 htons() 函数将主机字节序转换为网络字节序
    struct in_addr sin_addr;  // 包含一个 IPv4 地址,实际上是 32 位无符号整数,通常通过 inet_addr() 或 inet_aton() 函数填充
    char         sin_zero[8]; // 未使用的填充字节,通常设为全零以保持与其他 `sockaddr` 结构一致,确保整个结构体大小至少为 16 字节
};

  • sin_family:这是一个用于标识地址家族的字段,对于 IPv4 地址而言,它的值应为 AF_INET

  • sin_port:用于存放网络服务的端口号,通常是一个16位无符号整数。由于网络协议规定端口号使用网络字节序(big-endian),所以经常需要用 htons() 函数将主机字节序转换为网络字节序后再赋值给 sin_port

  • sin_addr:这是一个嵌套的 in_addr 结构体,它本身包含一个32位的无符号整数 s_addr,用于表示IPv4地址。你可以通过 inet_addr() 函数将点分十进制的 IP 地址字符串转换成网络字节序的整数,然后赋值给 sin_addr.s_addr,或者直接赋值32位的整数值。

  • sin_zero:这是为了兼容早期的Berkeley sockets API设计的一个填充字段,确保结构体总长度与 struct sockaddr 保持一致,因为在很多函数调用中需要传递指向 struct sockaddr 的指针。现代编程实践中,通常会用 memset() 或 bzero() 函数将这部分清零。

在实际的网络编程中,比如创建一个TCP或UDP服务器或客户端时,struct sockaddr_in 会被用来封装服务器或客户端的IP地址和端口号信息,然后传递给诸如 bind()connect()sendto()recvfrom() 等系统调用函数。

3、inet系列函数

网络编程中涉及的inet系列函数是一组用于处理IP地址表示与转换的实用工具。这些函数在不同的编程语言和平台中可能有不同的实现,但它们的核心目的都是帮助程序员有效地操作IP地址,包括将字符串形式的IP地址转换为网络字节序的二进制表示,以及反之将二进制IP地址转换回人类可读的字符串形式。以下是几个常见的inet函数及其用途:

1、inet_addr:

in_addr_t inet_addr(const char *cp);

功能:将点分十进制表示的 IPv4 地址字符串(如 "192.168.0.1")转换为网络字节序的 32 位整数(in_addr_t 类型)。如果输入的字符串无法解析为有效 IPv4 地址,函数可能返回 INADDR_NONE(通常为 0xFFFFFFFF)。

2. inet_aton() (适用于C语言,IPv4)

功能: inet_aton()函数接受一个指向包含点分十进制IPv4地址的字符串(如 "192.168.0.1"),将其转换为网络字节序的32位整数,并存储在指定的struct in_addr结构体中。该函数常用于将用户输入或配置文件中的IP地址字符串解析为程序内部可以直接使用的二进制形式。

#include <arpa/inet.h>

char ip_str[] = "192.168.0.1";
struct in_addr ip_addr;

if (inet_aton(ip_str, &ip_addr) == 0) {
    // 处理错误或无效IP地址
} else {
    // ip_addr已成功填充为对应的网络字节序整数
}

3. inet_ntoa() (适用于C语言,IPv4)

功能: inet_ntoa()函数将一个struct in_addr结构体(其中包含网络字节序的IPv4地址)转换回点分十进制的字符串形式。这个函数主要用于将程序内部的二进制IP地址以人类可读的字符串输出。

#include <arpa/inet.h>
#include <stdio.h>

struct in_addr ip_addr = { .s_addr = 0xc0a80001 }; // 二进制表示的192.168.0.1

char* ip_str = inet_ntoa(ip_addr);

printf("IP address: %s\n", ip_str); // 输出 "IP address: 192.168.0.1"

4. inet_pton() (适用于IPv4和IPv6)

功能: inet_pton()函数("presentation to network"之意)是inet_aton()的泛化版本,支持IPv4和IPv6地址的转换。它接受一个地址族(如AF_INETAF_INET6)、一个指向包含IP地址字符串的指针,以及一个指向目标缓冲区的指针,用于存放转换后的网络字节序IP地址。对于IPv4,目标缓冲区通常是一个struct in_addr;对于IPv6,则是一个struct in6_addr

用法(C语言示例,IPv4):

#include <arpa/inet.h>

char ip_str[] = "192.168.0.1";
struct in_addr ip_addr;

int result = inet_pton(AF_INET, ip_str, &ip_addr);
if (result == 1) {
    // ip_addr已成功填充为对应的网络字节序整数
} else if (result == 0) {
    // 输入不是有效的IPv4地址字符串
} else {
    // 出现错误
}

5. inet_ntop() (适用于IPv4和IPv6)

功能: inet_ntop()函数("network to presentation"之意)是inet_ntoa()的泛化版本,同样支持IPv4和IPv6地址的转换。它接受一个地址族、一个指向网络字节序IP地址的指针,以及一个目标字符串缓冲区和其长度,将IP地址以人类可读的字符串形式写入缓冲区。

用法(C语言示例,IPv4):

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

struct in_addr ip_addr = { .s_addr = 0xc0a80001 }; // 二进制表示的192.168.0.1

char ip_str[INET_ADDRSTRLEN]; // 定义足够大的缓冲区存放IPv4地址字符串
int result = inet_ntop(AF_INET, &ip_addr, ip_str, sizeof(ip_str));
if (result != NULL) {
    printf("IP address: %s\n", ip_str); // 输出 "IP address: 192.168.0.1"
} else {
    // 出现错误
}

二、UDP网络程序—发送消息

//udp_server.hpp:
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1) {}

    bool initServer()
    {
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }

    void Start()
    {
        char buffer[SIZE];
        for(;;)
        {
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);

            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)
            {
                buffer[s] = 0;

                uint16_t cli_port = ntohs(peer.sin_port);
                std::string cli_ip = inet_ntoa(peer.sin_addr);
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

                sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            }
        }
    }

    ~UdpServer()
    {
        if(_sock >= 0) close(_sock);
    }

private:
    uint16_t _port;
    std::string _ip;
    int _sock;
};

#endif


//udp_server.cc:
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}


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

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

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

    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if(message == "quit") break;

        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sock);
    return 0;
}

1、服务器udp_server.hpp

udp_server.hpp是C++编写的UDP服务器头文件,它定义了一个名为UdpServer的类,用于创建和管理一个基于UDP协议的服务器。 

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip), _sock(-1)
    {}
    bool initServer()
    {
        // 开始初始化服务器,通过系统调用完成网络功能配置
        // 1. 创建UDP套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 使用IPv4协议族,创建UDP套接字
        if (_sock < 0)
        {
            // 如果创建套接字失败,记录错误日志并退出程序
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. 绑定本地地址和端口到套接字
        // 定义一个IPv4结构体存储本地地址信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清零结构体内容,防止遗留数据干扰

        // 设置协议族为IPv4
        local.sin_family = AF_INET;

        // 设置服务端监听的端口号,转换为主机字节序
        local.sin_port = htons(_port);

        // 设置服务端监听的IP地址
        // 如果_ip为空字符串,则绑定到任意可用IP(INADDR_ANY)
        // 否则,将点分十进制字符串形式的IP地址转换为网络字节序的整数值
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 执行bind操作,将套接字与本地地址和端口绑定
        if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            // 如果绑定失败,记录错误日志并退出程序
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 绑定成功,记录日志
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

        // 初始化服务器完成,返回成功标志
        return true;
    }
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

    // UdpServer 类的 Start 方法启动服务器进入持续监听模式,每当接收到客户端的 UDP 数据包时,
    // 服务器都会原样回复给客户端。
    void Start()
    {
        // 服务器将持续运行,不断循环等待和处理客户端的请求。
        char buffer[SIZE]; // 创建一个固定大小的缓冲区用于存储接收到的客户端数据,最大容量为 1024 字节。

        // 进入无限循环,使服务器成为常驻进程。
        for (;;) // 此处的空条件表示永久循环
        {
            // 初始化存储客户端信息的结构体,并清零,准备接收新客户端的数据。
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));

            // 为存储客户端地址信息分配足够的缓冲区空间。
            socklen_t len = sizeof(peer);

            // 开始从套接字接收数据,同时获取发送数据的客户端地址信息。
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);

            // 如果成功接收到数据(接收到的数据长度大于0)
            if (s > 0)
            {
                // 在接收缓冲区末尾添加终止符,将其视为字符串处理。
                buffer[s] = 0;

                // 输出接收到的客户端数据及其来源信息。
                // 解析出客户端的端口号(从网络字节序转换为主机字节序)。
                uint16_t cli_port = ntohs(peer.sin_port); 

                // 将客户端的4字节网络序列IP地址转换为便于显示的字符串形式。
                std::string cli_ip = inet_ntoa(peer.sin_addr);

                // 输出客户端的IP地址、端口号及发送的数据内容。
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

                // TODO: 在此处可以添加更多的数据处理逻辑。
            }

            // 不管是否接收到数据,都将之前接收到的数据原样发送回客户端。
            // 在此简化实现中,服务器简单地回显客户端发送的内容,
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

private:
    // 服务器监听的端口号(16位无符号整数)
    uint16_t _port;

    // 服务器绑定的 IP 地址(字符串形式)
    std::string _ip;

    // 存储已创建的套接字句柄
    int _sock; // 用于接收和发送数据的套接字描述符
};

#endif
  1. UdpServer类构造函数: 接收两个参数,一个是16位的整数型端口号uint16_t port,另一个是可选的字符串型IP地址std::string ip。默认情况下,若不提供IP地址,则服务器将监听所有网络接口。类中包含三个私有成员变量:服务器监听的端口号_port,可选的IP地址_ip,以及用于网络通信的套接字描述符_sock

  2. initServer()函数: 该函数负责初始化服务器,主要步骤如下:

    • 创建一个UDP套接字,使用AF_INET协议家族和SOCK_DGRAM套接字类型。
    • 若创建套接字失败,记录错误信息并退出程序。
    • 初始化一个sockaddr_in结构体local,设置其sin_familyAF_INETsin_port为服务器监听的端口号(转换为主机字节序),并将IP地址根据用户提供的字符串(或默认为INADDR_ANY)转换成网络字节序的整数值。
    • 调用bind函数将套接字与本地地址和端口关联。如果绑定失败,同样记录错误信息并退出程序。
    • 成功绑定后,记录一条日志信息表示服务器初始化完成。
  3. Start()函数: 该函数启动服务器的主循环,持续监听和处理来自客户端的请求。

    • 通过无限循环,不断地从套接字接收数据(使用recvfrom函数)。
    • 接收到数据后,将数据原样返回给客户端(使用sendto函数)。
    • 在这个例子中,服务器充当了一个简单的回显服务器,接收到客户端发送的消息后,原样返回给客户端。
  4. 析构函数: 当UdpServer对象生命周期结束时,析构函数会检查套接字是否已成功创建并处于打开状态,如果是,则关闭套接字,释放资源。

initServer()初始化服务器

initServer() 方法是 UdpServer 类中的一个成员函数,其主要目的是初始化一个 UDP 服务器,使其能够监听和接收特定 IP 地址和端口号上的数据报文。

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip), _sock(-1)
    {}
    bool initServer()
    {
        // 开始初始化服务器,通过系统调用完成网络功能配置
        // 1. 创建UDP套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 使用IPv4协议族,创建UDP套接字
        if (_sock < 0)
        {
            // 如果创建套接字失败,记录错误日志并退出程序
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. 绑定本地地址和端口到套接字
        // 定义一个IPv4结构体存储本地地址信息
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清零结构体内容,防止遗留数据干扰

        // 设置协议族为IPv4
        local.sin_family = AF_INET;

        // 设置服务端监听的端口号,转换为主机字节序
        local.sin_port = htons(_port);

        // 设置服务端监听的IP地址
        // 如果_ip为空字符串,则绑定到任意可用IP(INADDR_ANY)
        // 否则,将点分十进制字符串形式的IP地址转换为网络字节序的整数值
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 执行bind操作,将套接字与本地地址和端口绑定
        if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            // 如果绑定失败,记录错误日志并退出程序
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 绑定成功,记录日志
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

        // 初始化服务器完成,返回成功标志
        return true;
    }
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    // 服务器监听的端口号(16位无符号整数)
    uint16_t _port;

    // 服务器绑定的 IP 地址(字符串形式)
    std::string _ip;

    // 存储已创建的套接字句柄
    int _sock; // 用于接收和发送数据的套接字描述符
};

#endif

构造函数和析构函数是C++中类的重要组成部分,用于控制类对象的生命周期,即对象的创建和销毁过程。下面是关于UdpServer类构造函数和析构函数的详细讲解:

构造函数

UdpServer(uint16_t port, std::string ip = "")
    : _port(port), _ip(ip), _sock(-1)
{}
  1. 签名:构造函数接受两个参数:一个无符号16位整数uint16_t port(代表服务器监听的端口号)和一个默认为空字符串的std::string ip(代表服务器绑定的IP地址)。默认参数ip = ""意味着用户在创建对象时可以选择性地省略IP地址参数,此时服务器将绑定到任意可用IP(INADDR_ANY)。

  2. 初始化列表

    • _port(port):将传入的参数port值赋给_port,用于存储服务器监听的端口号。
    • _ip(ip):将传入的参数ip值赋给_ip,或者在用户未提供IP地址时使用默认的空字符串。此成员变量存储服务器绑定的IP地址。
    • _sock(-1):将-1赋给_sock,这是套接字描述符的初始值,表示尚未创建套接字。后续在initServer方法中创建并成功绑定套接字后,会将有效的套接字描述符赋给_sock

析构函数

~UdpServer()
{
    if (_sock >= 0)
        close(_sock);
}

析构函数检查_sock的值是否大于等于0(即套接字已成功创建并处于打开状态),如果是,则调用系统函数close(_sock)关闭套接字,释放与该套接字关联的系统资源。

bool initServer()

  1. 创建套接字

    _sock = socket(AF_INET, SOCK_DGRAM, 0);

    使用 socket() 系统调用来创建一个新的套接字。这里的参数含义如下:

    • AF_INET 表示使用IPv4地址簇,适用于大多数网络通信。
    • SOCK_DGRAM 表示使用无连接的UDP传输层协议,即数据报文协议。
    • 第三个参数通常设置为0,在UDP中不需要特别指定协议类型。

    如果创建套接字失败(即 _sock < 0),则记录致命错误日志,并通过 exit(2) 结束程序。

  2. 填充本地地址结构体

    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);

    创建一个 sockaddr_in 结构体实例 local,这是用于存储IPv4地址和端口号的结构。首先使用 bzero() 函数清零整个结构体,确保没有遗留数据。

    • local.sin_family 被设置为 AF_INET,表明这是一个IPv4地址结构。
    • local.sin_port 被设置为传入构造函数的 _port 变量值,但先进行了 htons() 函数转换。这是因为网络字节序通常是大端字节序,而端口号在内存中可能是小端字节序,所以需要用 htons() 函数将主机字节序转为网络字节序。
    • "192.168.110.132"是一个典型的点分十进制格式的IPv4地址,这种表示方式直观易读,便于我们理解和记忆。它由四个十进制数值组成,每个数值代表IP地址的一个八位字节(也称为一个“段”),彼此之间用点(.)分隔。具体来说:
    • 第一个数值,即"192",表示IP地址的第一个八位字节。这个字节的取值范围是0到255(对应二进制的00000000至11111111),恰好能容纳一个字节的全部可能值。

    • 后续数值,即"168"、"110"和"132",分别代表IP地址的第二、第三和第四个八位字节。它们同样遵循0到255的取值范围,每个字节独立承载部分地址信息。

    • 综上所述,一个IPv4地址通过四个这样的十进制数值串连起来,形成如"192.168.110.132"这样的点分十进制字符串。这种紧凑的四位结构,恰好能够用4个字节(每个字节8位,共32位)的二进制数据来完整表示,与IPv4地址的实际存储和传输格式相吻合。

      因此,从逻辑上看,一个IPv4地址无论采用点分十进制字符串形式还是其内在的4字节二进制形式,所蕴含的信息是完全一致的。在实际的网络通信中,应用程序通常需要在这两种表示形式之间进行转换,例如使用inet_addr()函数将点分十进制字符串转化为4字节的网络字节序整数,或者使用inet_ntoa()函数将4字节的网络字节序整数转换为点分十进制字符串,以适应不同场景的需求。

    • bzero()

      bzero() 是 C 语言标准库中的一个函数,主要用于将一段内存区域清零(即填充为全零)。不过要注意的是,这个函数在 C++11 标准后已被弃用,推荐使用 memset() 函数替代。

      函数原型如下:

      void bzero(void *s, size_t n);

      参数说明:

      • s:指向内存区域首地址的指针,你需要清零的内存块起始位置。
      • n:要清零的字节数。

      在上述代码片段中:

      bzero(&peer, sizeof(peer));

      这条语句的作用是将 peer 这个 sockaddr_in 结构体的所有成员变量清零。sizeof(peer) 计算出结构体的大小,然后调用 bzero() 函数将其所有字节都填充为零。这样做的目的是初始化结构体,确保之前的内容不会影响接下来的操作,尤其是涉及到网络通信时,往往需要确保结构体中存储的信息是已知和预期的初始状态。

      然而,在现代 C/C++ 编程中,建议使用 memset() 函数替换 bzero(),其功能相同,函数原型如下:

      void* memset(void* ptr, int value, size_t num);

      同样地,你可以使用 memset() 来达到同样的效果:

      memset(&peer, 0, sizeof(peer));

      htons() 

      htons() 是一个在 BSD Socket API 和许多其他网络编程接口中广泛使用的函数,主要用于将主机字节顺序(Host Byte Order)转换为网络字节顺序(Network Byte Order)。

      在网络通信中,不同的计算机体系结构可能会有不同的字节序(大端或小端)。为了确保不同机器之间的数据传输一致性,TCP/IP 协议规定了一系列的网络协议字段(如端口号、IP地址等)在传输过程中统一采用网络字节序(大端字节序,即高位字节在前)。

      函数原型如下:

      uint16_t htons(uint16_t hostshort);

      参数说明:

      hostshort:一个 16 位无符号整数,代表主机字节顺序下的数值。

      函数返回值:

      返回值为将输入的 hostshort 转换为网络字节顺序后的 16 位无符号整数。
       

      在上述代码片段中并没有直接使用 htons() 函数,但是有个类似的功能调用 ntohs() 的反向操作:

      uint16_t cli_port = ntohs(peer.sin_port);

      这里,peer.sin_port 是一个从网络字节顺序读取到的端口号,ntohs() 函数用于将其转换为主机字节顺序,使得本地程序能够正确理解和使用这个端口号。

      如果你需要将一个主机字节顺序的端口号转换为网络字节顺序再发送出去,这时候就会用到 htons() 函数,例如:

      uint16_t hostPort = 12345;
      uint16_t netPort = htons(hostPort);
      peer.sin_port = netPort;

      这样,peer.sin_port 就会被设置成网络字节顺序的端口号,可以在网络通信中正确传输。

  3. 处理IP地址

    local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

    这段代码处理服务器监听的IP地址:

    • 如果 _ip 字符串为空,则设置 local.sin_addr.s_addr 为 INADDR_ANY,这意味着服务器将监听所有可用的网络接口,可以接收来自任何接口的UDP数据报文。
    • 如果 _ip 不为空,则调用 inet_addr(_ip.c_str()) 来将点分十进制形式的IP地址字符串转换为网络字节序的32位整数(4字节),然后赋值给 local.sin_addr.s_addr
  4. 绑定套接字

    if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)

    使用 bind() 系统调用将之前填充好的 local 结构体与刚创建的套接字 _sock 关联起来,这样服务器就会在指定的IP地址和端口号上监听。若绑定失败,同样记录致命错误日志并结束程序。

  5. 初始化成功标志: 最后,方法会在成功执行上述操作后,记录一条普通级别日志,表示服务器初始化完成,并返回 true 表示初始化成功。

总之,initServer() 方法实现了创建并配置一个监听特定端口(可能针对特定IP地址)的UDP套接字,从而完成了UDP服务器的基本初始化过程。

Start()启动服务器

Start 函数的作用是启动一个永不退出的循环,循环中接收客户端发来的UDP数据包,简单处理后(此处仅为输出数据来源和内容),再将数据回送给客户端,从而实现了UDP回显服务器的功能。在实际应用中,可以根据需求在接收到数据后增加更多的业务逻辑处理代码。 

#define SIZE 1024

class UdpServer
{
public:
    // UdpServer 类的 Start 方法启动服务器进入持续监听模式,每当接收到客户端的 UDP 数据包时,
    // 服务器都会原样回复给客户端。
    void Start()
    {
        // 服务器将持续运行,不断循环等待和处理客户端的请求。
        char buffer[SIZE]; // 创建一个固定大小的缓冲区用于存储接收到的客户端数据,最大容量为 1024 字节。

        // 进入无限循环,使服务器成为常驻进程。
        for (;;) // 此处的空条件表示永久循环
        {
            // 初始化存储客户端信息的结构体,并清零,准备接收新客户端的数据。
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));

            // 为存储客户端地址信息分配足够的缓冲区空间。
            socklen_t len = sizeof(peer);

            // 开始从套接字接收数据,同时获取发送数据的客户端地址信息。
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);

            // 如果成功接收到数据(接收到的数据长度大于0)
            if (s > 0)
            {
                // 在接收缓冲区末尾添加终止符,将其视为字符串处理。
                buffer[s] = 0;

                // 输出接收到的客户端数据及其来源信息。
                // 解析出客户端的端口号(从网络字节序转换为主机字节序)。
                uint16_t cli_port = ntohs(peer.sin_port); 

                // 将客户端的4字节网络序列IP地址转换为便于显示的字符串形式。
                std::string cli_ip = inet_ntoa(peer.sin_addr);

                // 输出客户端的IP地址、端口号及发送的数据内容。
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

                // TODO: 在此处可以添加更多的数据处理逻辑。
            }

            // 不管是否接收到数据,都将之前接收到的数据原样发送回客户端。
            // 在此简化实现中,服务器简单地回显客户端发送的内容,
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

private:
    // UdpServer 类的私有成员变量:
    // 服务器监听的端口号(16位无符号整数)
    uint16_t _port;

    // 服务器绑定的 IP 地址(字符串形式)
    std::string _ip;

    // 存储已创建的套接字句柄
    int _sock; // 用于接收和发送数据的套接字描述符
};

UdpServer 类的 Start 函数是服务器的核心部分,负责启动并维持服务器的运行,处理客户端发来的UDP数据包。下面是详细的解释:

首先,定义了一个固定大小的缓冲区 char buffer[SIZE],用于存放接收到的客户端数据。这里的 SIZE 定义为1024字节。

接下来是一个无限循环,这意味着服务器会一直运行,除非进程被外部因素终止(如手动停止或者出现致命错误)。

在循环内部:

  1. 初始化一个 sockaddr_in 结构体 peer 用来存放客户端的信息,使用 bzero 函数清零该结构体的所有字节。

  2. 设置 socklen_t len 为 sizeof(peer),表示用来接收客户端地址信息的缓冲区长度。

  3. 使用 recvfrom 系统调用从套接字 _sock 接收数据,填入 buffer 缓冲区,并同时获取发送数据的客户端地址信息(存放在 peer 结构体中)。sizeof(buffer)-1 用来确保接收的数据不会超过缓冲区容量,并且为字符串留出空位放置结束符。

     ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
  4. 如果 recvfrom 成功接收到数据(ssize_t s > 0),则在缓冲区末尾添加字符串结束符 '\0',这样我们可以将其视为 C 风格的字符串。
     

    recvfrom()

     recvfrom() 是一个在 BSD Socket API 中提供的函数,主要用于 UDP 协议下的网络编程,也可以用于 RAW 或者 Datagram 套接字类型。它的作用是从指定的套接字接收数据,并返回发送数据的远程主机地址信息。

    函数原型如下:

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

    各参数含义:
    sockfd: 已经通过 socket() 函数创建并经过 bind() 绑定的套接字描述符。
    buf: 一个指向缓冲区的指针,用于接收数据。接收到的数据将被复制到这个缓冲区中。
    len: 缓冲区的大小,即最多能接收多少字节的数据。
    flags: 可选标志,通常设置为 0。在某些情况下,可以传递 MSG_PEEK 或 MSG_WAITALL 等标志,但这些在 UDP 中并不常用。

    src_addr: 类型为struct sockaddr *的指针,用于接收发送数据的远程主机的地址信息。通常需要预先分配一个适当大小的sockaddr结构体(如sockaddr_insockaddr_in6)并将其地址传递给此参数。函数完成后,该结构体将被填充为发送方的地址和端口信息。

    addrlen: 类型为socklen_t *的指针,用于传递和接收地址结构体的长度。在调用recvfrom()前,需要将指向地址结构体长度的指针(如sizeof(struct sockaddr_in))赋给addrlen。函数执行完毕后,addrlen指向的内存将被更新为实际接收到的地址结构体长度。
     



    函数返回值:

    如果成功接收到数据,返回接收到的数据字节数。
    如果没有数据可接收并且套接字是非阻塞的,返回值为 0。
    如果发生错误,返回 -1,并设置 errno 错误码。

  5. 解析客户端的端口号和IP地址:从 peer.sin_port 中提取端口号,通过 ntohs 函数将网络字节序转换为主机字节序;从 peer.sin_addr 中提取IP地址,利用 inet_ntoa 函数将网络字节序的IP地址转换为点分十进制格式的字符串。
     

    ntohs

    ntohs 是 Network To Host Short 的缩写,这是一个用于网络编程的函数,主要用于处理字节序转换问题。在不同的计算机体系结构中,整数(包括短整型)的字节存储顺序可能是不同的:

    大端字节序(Big-Endian):高位字节存储在内存的低地址处。
    小端字节序(Little-Endian):低位字节存储在内存的低地址处。

    网络传输数据时规定采用统一的标准字节序——网络字节序(Network Byte Order),它是大端字节序。

    当网络上接收到的数据是以网络字节序表示的数值时,如果本地机器采用的是与之不同的字节序,则需要进行转换才能正确解析这些数值。例如,在 TCP/IP 网络通信中,端口号和 IP 地址的某些部分在网络传输中都是以大端字节序发送的。

    ntohs 函数的作用就是将一个16位的无符号短整型数从网络字节序转换为主机字节序。具体来说:

    uint16_t cli_port = ntohs(peer.sin_port);


    这条语句会把 peer 结构体中的 sin_port 成员(网络字节序的16位端口号)转换成本地主机使用的字节序,这样就可以在本地程序中正确地理解和使用这个端口号了。对于其他类型的整数,还有类似的函数:
    ntohl:用于32位无符号长整型,Network To Host Long。
    这些函数在大多数网络编程库(如 POSIX 的 <arpa/inet.h> 或 Windows 下的 <winsock2.h>)中都有提供。
    htonl:Host To Network Long。
    htons:Host To Network Short。
    输出接收到的数据及其来源信息,格式为 [IP地址:端口号]# 数据内容

  6. 最后,使用 sendto 函数将接收到的数据原封不动地回送给客户端。参数分别为:套接字 _sock、缓冲区 buffer、数据长度 strlen(buffer)、附加选项(在此设为0)、以及客户端地址信息 (struct sockaddr*)&peer 和其长度 len

     sendto

    sendto() 是一个在 BSD Socket API 中的函数,用于在无连接的套接字(如 UDP 套接字)上发送数据。此函数可用于向指定的网络地址发送数据报文。

    函数原型一般如下:

    ssize_t sendto(int socket, const void *buffer, size_t length, int flags,
                  const struct sockaddr *dest_addr, socklen_t dest_len);

    各个参数的含义:
    socket: 这是一个已建立的套接字描述符,由 socket() 函数创建并由 bind() 或 connect() 函数准备用于发送数据。

    buffer: 这是一个指向缓冲区的指针,包含了要发送的数据。

    length: 表示缓冲区内待发送数据的字节数。

    flags: 通常情况下,对于 sendto() 函数,这个参数设置为 0。但在某些特殊情况下,可以使用特定的标志,如 MSG_OOB(发送带外数据)或 MSG_DONTROUTE(不使用路由表)等。

    dest_addr: 这是一个指向 sockaddr 结构体的指针,包含了目标主机的地址信息。对于 IPv4 地址,通常使用 sockaddr_in 结构体。

    dest_len: 指定了 dest_addr 参数所指向的地址结构体的大小。

    sendto() 函数会尝试发送指定长度的数据,并返回实际发送出去的字节数。如果返回值小于 length,可能是因为发生了错误或者网络缓冲区空间不足等情况。

2、udp_server.cc

udp_server.cc文件中的main函数是整个UDP服务器程序的入口点,负责创建和启动一个基于UDP协议的回显服务器。服务器监听指定的端口,接收到客户端的数据后立即将其回传给客户端,形成一个基本的回显服务功能。 

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}

udp_server.cc 文件包含了C++ UDP服务器的主程序,它主要完成了以下几项工作:

  1. 引入头文件:包含了之前定义的udp_server.hpp头文件,以及其他必要的系统库文件。

  2. 定义usage函数:这是一个辅助函数,用于输出程序的使用方法。在这个例子中,UDP服务器只需一个参数,即服务器要监听的端口号。

  3. 主函数main

    • 检查命令行参数的数量,确保用户输入了正确的端口号。如果参数数量不符,调用usage函数输出使用帮助并退出程序。
    • 根据用户输入的端口号创建一个UdpServer实例,并通过智能指针std::unique_ptr进行管理。
    • 调用UdpServer实例的initServer方法初始化服务器,主要包括创建UDP套接字、设置服务器监听的IP地址和端口号,并进行bind操作。
    • 初始化完成后,调用Start方法启动服务器主循环,该循环将持续监听客户端发来的UDP数据包,并原样回送给客户端。

3、udp_client.cc 

udp_client.cc 文件实现了一个简单的 UDP 客户端程序,它可以连接到指定的 UDP 服务器,并与服务器进行双向通信,实现类似回显的功能。当客户端发送消息给服务器时,服务器会原样返回该消息,客户端接收到服务器返回的消息后在控制台上展示。 

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

// 定义一个静态辅助函数,用于输出程序的正确使用方法
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// 示例命令行:./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    // 检查命令行参数的数量是否为3(程序名 + IP地址 + 端口号)
    if (argc != 3)
    {
        // 参数数量不对,输出使用方法并退出程序
        usage(argv[0]);
        exit(1);
    }

    // 创建一个UDP套接字,使用IPv4协议,SOCK_DGRAM表示数据报文(UDP)类型
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        // 创建套接字失败,输出错误信息并退出程序
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // 对于UDP客户端,通常不需要手动bind本地IP和端口
    // 因为当客户端首次发送数据时,操作系统会自动分配一个临时端口并绑定到本地IP

    // 定义用于存储服务器地址的结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 初始化结构体所有成员为0

    // 设置协议族为IPv4
    server.sin_family = AF_INET;

    // 将命令行参数中的端口号转换为网络字节顺序(主机字节序转网络字节序)
    server.sin_port = htons(atoi(argv[2]));

    // 将命令行参数中的IP地址字符串转换为网络字节顺序的IP地址
    server.sin_addr.s_addr = inet_addr(argv[1]);

    // 定义缓冲区用于接收服务器响应的数据
    char buffer[1024];

    // 进入无限循环,直到用户输入"quit"为止
    while (true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);

        // 如果用户输入"quit",则跳出循环
        if (message == "quit")
            break;

        // 将客户端消息发送至服务器
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        // 清零临时结构体,用于存储接收到的服务器响应信息
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        // 接收服务器的响应数据
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);

        // 如果成功接收到数据
        if (s > 0)
        {
            // 添加终止符,确保buffer是一个有效的C字符串
            buffer[s] = 0;

            // 输出接收到的服务器响应
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    // 关闭套接字,释放资源
    close(sock);

    // 主程序结束,返回0表示成功
    return 0;
}
  1. 首先,包含必要的头文件,如 <iostream><string><cstring><unistd.h><sys/socket.h><arpa/inet.h> 和 <netinet/in.h>,这些头文件包含了编写网络编程所需的基本函数和数据类型。

  2. 定义一个静态辅助函数 usage(),用于输出程序的正确用法。当命令行参数不足时,调用此函数打印帮助信息并退出程序。

  3. main() 函数是客户端程序的入口点,其接受两个命令行参数:服务器 IP 地址和端口号。如果参数数量不足,调用 usage() 输出用法并退出。

  4. 创建一个 UDP 套接字,使用 socket() 系统调用,参数 AF_INET 表示 IPv4,SOCK_DGRAM 表示使用 UDP 协议。如果创建套接字失败,输出错误信息并退出程序。

  5. 定义客户端要连接的服务器地址结构体 sockaddr_in server,并填充相关信息。设置家族类型为 AF_INET,将服务器端口号转换为主机字节序后赋值给 sin_port,使用 inet_addr() 函数将服务器 IP 地址从点分十进制字符串转换成网络字节序的整数值,存入 sin_addr.s_addr

  6. 在一个无限循环中执行以下操作:

         while(true)
        {
            std::cout << "请输入你的信息# ";
            std::getline(std::cin, message);
            if(message == "quit") break;
    
            sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);
    
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
            if(s > 0)
            {
                buffer[s] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
        }

    a. 提示用户输入消息。 b. 当用户输入 "quit" 时,跳出循环。 c. 使用 sendto() 函数将用户输入的消息发送到服务器。 d. 接收服务器返回的消息,使用 recvfrom() 函数,将接收到的数据存放在 buffer 中。 e. 如果成功接收到数据(即 recvfrom() 返回值大于 0),将 buffer 转换为字符串并在控制台输出。

  7. 循环结束后,关闭客户端套接字,确保资源释放。

为什么客户端没有绑定操作?

  1. 客户端需要绑定吗?

    • 理论上需要:如同服务端一样,客户端在发送和接收数据时也需要一个本地IP地址和端口。在UDP通信中,客户端与服务端之间的通信是基于四元组(源IP、源端口、目的IP、目的端口)进行的。因此,从技术角度来看,客户端确实需要绑定一个本地IP地址和端口才能进行网络通信。
  2. 为什么客户端通常不显式绑定?

    • 随机选择端口:当客户端发送数据时,如果没有预先绑定本地端口,操作系统会自动为其分配一个未被占用的临时端口。这种方式可以避免程序员手动指定端口带来的问题,如端口冲突(多个客户端同时使用同一端口)或端口资源浪费(固定端口可能长时间不释放)。
    • 简化程序逻辑:对于大多数客户端应用(如用户下载并使用的普通软件),程序员通常不需要关心客户端使用的具体端口,因为这并不影响用户正常使用。显式绑定端口只会增加程序复杂性和维护成本,而对用户体验几乎没有提升。
    • 适应性强:让操作系统自动为客户端分配端口,使得客户端在不同网络环境和设备上都能正常运行,无需担心端口被其他程序占用或特定端口被防火墙封锁等问题。
  3. 何时由操作系统自动绑定?

    • 首次发送数据时:在使用sendto()函数向服务端发送数据时,如果客户端尚未绑定本地端口,操作系统会在调用该函数的瞬间自动为客户端分配一个可用的本地端口,并隐式地执行bind()操作。之后,客户端便可以使用这个临时分配的端口继续进行通信。

4、log.hpp日志系统

#pragma once

#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

// #define LOGFILE "./threadpool.log"

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);

    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

5、模拟运行过程:

  1. 启动 UDP 服务器 (udp_server) 用户在终端运行以下命令启动 UDP 服务器:

    ./udp_server 8080

    这里假设用户指定了端口号为 8080,服务器将在本地主机上监听此端口,同时可以接收来自任意 IP 地址的消息。服务器初始化完成后,开始进入监听状态,等待客户端发送数据。

  2. 启动 UDP 客户端 (udp_client) 用户在另一个终端运行以下命令启动 UDP 客户端并连接到服务器:

    ./udp_client 127.0.0.1 8080

    客户端程序将连接到本地主机(IP 为 127.0.0.1)的 8080 端口。客户端进入交互模式,提示用户输入信息。

  3. 用户交互 用户在客户端终端输入一条消息,例如:"Hello, Server!",然后按回车键发送。

    客户端将消息发送到服务器,服务器接收到消息后打印出客户端的 IP 地址和端口号以及接收到的消息,如:

    [127.0.0.1:54321]# Hello, Server!

    然后服务器将接收到的消息原封不动地返回给客户端,客户端接收到消息后,在终端显示:

    server echo# Hello, Server!
  4. 持续交互 用户可以继续在客户端输入更多消息,服务器每次都会收到并原样返回。当用户在客户端输入 "quit" 并发送时,客户端退出循环,关闭套接字,程序结束。

整个过程中,服务器一直保持运行,等待并处理来自客户端的消息。而客户端在用户停止交互并退出程序后,服务器仍将继续监听该端口,等待其他客户端的连接和消息。

三、UDP网络程序—发送命令

1、popen函数

popen 是一个在 Unix-like 系统(包括 Linux)中广泛使用的 C 语言函数,用于实现进程间的通信(IPC),特别是通过管道(pipe)来执行外部程序并与其进行交互。以下是关于 popen 函数的详细讲解:

函数原型

FILE *popen(const char *command, const char *mode);

参数说明

  • const char *command:指向包含待执行命令字符串的指针。这个字符串通常是一个完整的 shell 命令,包括命令本身及其参数。例如,"ls -l" 或 "ping -c 5 example.com"

  • const char *mode:指定管道的打开模式,类似于 fopen 函数中的模式字符串。它可以是以下两种值之一:

    • "r":以只读模式打开管道,允许从子进程(即执行的外部命令)的标准输出读取数据。这意味着父进程可以通过 popen 返回的 FILE 流读取子进程产生的输出。

    • "w":以只写模式打开管道,允许向子进程的标准输入写入数据。这意味着父进程可以通过 popen 返回的 FILE 流向子进程发送数据,供其作为输入处理。

返回值

  • 成功执行时,popen 返回一个指向 FILE 类型的指针,该指针可用于对子进程进行读写操作,如同操作一个普通文件流。
  • 如果发生错误(如无法执行命令、内存不足、无法创建管道等),popen 返回 NULL。此时,可以调用 errno 获取具体错误代码,并使用 strerror(errno) 获取错误描述。

工作机制

  1. 创建管道popen 在内部创建一个匿名管道,该管道由两个文件描述符(一个用于读取,一个用于写入)组成。

  2. 生成子进程popen 使用 fork 系统调用创建一个子进程。子进程继承了管道的写端(对于模式 "r")或读端(对于模式 "w"),而父进程则保留了对应的另一端。

  3. 执行命令:在子进程中,popen 使用 exec 系列函数(如 execlp 或 execvp)替换当前进程映像,执行指定的 command。此时,子进程的标准输入或标准输出(取决于 mode)已经重定向到管道的一端。

  4. 返回流:在父进程中,popen 将管道的剩余端(与 mode 相匹配的读端或写端)包装成一个 FILE 流对象,并返回给调用者。这个流可以像操作普通文件一样使用,如通过 freadfgets 读取子进程的输出("r" 模式),或通过 fprintffwrite 向子进程发送数据("w" 模式)。

示例用法

#include <stdio.h>

int main() {
    FILE *pipe = popen("ls -l", "r"); // 以只读模式执行 "ls -l" 命令

    if (pipe == NULL) {
        perror("Failed to run command");
        return 1;
    }

    char buffer[BUFSIZ];
    while (fgets(buffer, BUFSIZ, pipe)) { // 逐行读取子进程的输出
        printf("%s", buffer);
    }

    int status = pclose(pipe); // 关闭管道并等待子进程结束
    if (status == -1) {
        perror("Failed to close pipe");
        return 1;
    } else if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
        fprintf(stderr, "Command exited with non-zero status: %d\n", WEXITSTATUS(status));
        return 1;
    }

    return 0;
}

注意事项

  • 资源管理:使用 popen 创建的子进程在 pclose 被调用之前会保持活动状态。因此,务必在完成与子进程的交互后调用 pclose,以便清理资源、关闭管道并等待子进程结束。否则,可能导致子进程成为僵尸进程。

  • 安全性:由于 popen 使用 shell 执行命令,因此需要注意命令注入的安全风险。如果 command 字符串包含不受信任的用户输入,应使用适当的转义或限制机制防止恶意命令被执行。

  • 同步与阻塞popen 执行的命令通常与父进程异步运行。在读模式下,如果子进程尚未生成输出,读取操作可能会阻塞。同样,在写模式下,如果管道缓冲区满,写入操作也可能阻塞。需要合理设计程序以处理这些同步问题。

  • 标准错误popen 默认不捕获子进程的标准错误输出。如果需要同时处理标准输出和标准错误,可能需要使用更复杂的 IPC 机制,如创建额外的管道或使用 dup2 重定向标准错误至标准输出。

综上所述,popen 函数提供了一种简单易用的机制,使 C 语言程序能够通过管道与外部命令进行交互。它通过创建子进程、重定向标准输入输出,并返回一个 FILE 流,使得程序可以像操作文件一样读写子进程的输入输出。在使用时需注意资源管理、安全性和同步问题。

2、udp_server.hpp

class UdpServer
{
public:
    // UdpServer 类的 Start 函数是服务器的核心处理循环,它将持续监听并处理来自客户端的 UDP 数据包。
    void Start()
    {
        // 定义一个足够大的缓冲区,用于存储接收到的客户端数据。
        char buffer[SIZE];

        // 进入一个无限循环,使服务器保持运行直到进程结束。
        for (;;)
        {
            // 初始化一个用于存储客户端信息的结构体,并清零。
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));

            // 定义缓冲区大小供 recvfrom 函数使用,用于接收客户端地址信息。
            socklen_t len = sizeof(peer);

            // 定义中间变量,用于存储命令执行结果(如果适用)。
            char result[256];
            char key[64];
            std::string cmd_echo;

            // 开始接收客户端数据。
            ssize_t bytes_received = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);

            // 如果接收到的数据长度大于0,则进行处理。
            if (bytes_received > 0)
            {
                // 将缓冲区结尾置零,使其成为一个完整的 C 字符串。
                buffer[bytes_received] = 0;

                // 输出接收到的数据信息。
                // (此处未实现)

                // 检查客户端发送的是否是危险命令(例如 "rm" 或 "rmdir")。
                if (strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
                {
                    // 发送错误消息给客户端,阻止执行危险命令。
                    std::string err_message = "坏人.... ";
                    std::cout << err_message << buffer << std::endl;
                    sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr*)&peer, len);

                    // 跳过本次循环,进入下一轮接收。
                    continue;
                }

                // 尝试执行缓冲区中的命令,并获取输出结果。
                FILE* fp = popen(buffer, "r");
                if (fp == nullptr)
                {
                    // 如果命令执行失败,记录错误信息并跳过本次循环。
                    logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                    continue;
                }

                // 读取命令执行的输出结果并累加到 cmd_echo 字符串中。
                while (fgets(result, sizeof(result), fp) != nullptr)
                {
                    cmd_echo += result;
                }

                // 关闭命令执行的管道。
                fclose(fp);
            }

            // 分析和处理数据(此处为 TODO,可根据实际需求补充相应逻辑)。

            // 将处理后的数据(或命令执行结果)发送回给客户端。
            sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);
        }
    }

    // 析构函数,在对象销毁时关闭套接字,释放资源。
    ~UdpServer()
    {
        // 如果套接字有效(即已创建),则关闭它。
        if (_sock >= 0)
        {
            close(_sock);
        }
    }

private:// 一个服务器,一般必须需要ip地址和port(16位的整数)
    // 类私有成员变量
    // 服务器监听的端口号
    uint16_t _port;
    // 服务器绑定的 IP 地址
    std::string _ip;
    // 存储套接字描述符
    int _sock;
};

#endif

UdpServer 类的 Start 函数是服务器的核心处理循环,负责接收客户端发送的数据,并根据数据内容做出响应。下面是 Start 函数的详细解释:

  1. 初始化缓冲区 char buffer[SIZE] 用于存储接收到的数据。

  2. 进入一个无限循环,持续监听客户端的 UDP 数据包。

  3. 初始化 struct sockaddr_in 类型的 peer 结构体,并用 bzero 函数清零,用于存储发送数据的客户端地址信息。

  4. 设置 socklen_t len 为 peer 结构体的大小,以便在 recvfrom 函数中接收客户端地址信息。

  5. 当接收到客户端数据时(ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);),检查接收的字节数 s 是否大于 0,若大于 0,则进行以下处理:

    • 将缓冲区末尾置为 \0,使其成为字符串便于处理。
    • 对接收到的命令字符串 buffer 进行安全检查:如果包含 "rm" 或 "rmdir" 字符串,则认为是非法指令,服务器回应一个错误信息,并跳过此次循环迭代。
    • 使用 popen 函数执行接收到的命令字符串(在此例中执行的是类似于 Shell 的命令,如 "ls -a -l"),并将命令的输出读取到 cmd_echo 字符串中。如果 popen 执行失败,输出错误信息并跳过此次循环迭代。
  6. 在分析和处理数据之后(这部分代码为 TODO,可根据实际需求实现),将处理结果(在这里是命令执行的输出 cmd_echo)通过 sendto 函数原样发送回给客户端。

四、实现广播处理结果给每个客户端

UdpServer类的Start函数负责启动并运行服务器的核心业务逻辑,即接收客户端发送的数据包,处理数据,并将处理结果广播给所有已连接的客户端。

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1) {}

    bool initServer()
    {
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }

    void Start()
    {
        char buffer[SIZE];
        for (;;)
        {
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;

            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;

                uint16_t cli_port = ntohs(peer.sin_port);
                std::string cli_ip = inet_ntoa(peer.sin_addr);
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
                logMessage(NORMAL, "key: %s", key);

                auto it = _users.find(key);
                if (it == _users.end())
                {
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }

            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer;
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }

    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    uint16_t _port;
    std::string _ip;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _users;
    std::queue<std::string> messageQueue;
};
  1. 定义接收缓冲区和相关变量

    • 定义一个大小为SIZE(1024字节)的字符数组buffer,用于接收客户端发送的数据。
    • 定义一个struct sockaddr_in类型的变量peer,用于存储发送数据的客户端地址信息。
    • 定义一个socklen_t类型的变量len,用于接收recvfrom函数返回的实际地址长度。
    • 定义一个字符数组result(256字节)和key(64字节),以及一个std::string类型的变量cmd_echo,用于后续数据处理。
  2. 无限循环接收数据

    • 使用一个无限循环(for(;;))确保服务器持续运行,不断接收客户端数据。
  3. 接收客户端数据

    • 调用recvfrom函数,从套接字_sock接收数据。接收的数据存放在buffer中,同时获取发送数据的客户端地址信息(存放在peer中),并更新len为实际地址长度。
    • recvfrom成功接收到数据(返回值s > 0),则对buffer末尾添加空字符,使之成为C风格的字符串,便于后续处理。
  4. 处理接收到的数据

    • 提取客户端地址信息:
      • 获取客户端端口号:使用ntohs函数将peer.sin_port从网络字节序转换为主机字节序,存储在cli_port中。
      • 获取客户端IP地址:使用inet_ntoa函数将peer.sin_addr转换为点分十进制字符串,存储在cli_ip中。
    • 构造客户端唯一标识(key):
      • 使用snprintf函数将cli_ipcli_port拼接成形如127.0.0.1-1234的字符串,并存储在key中。
    • 更新客户端列表:
      • 查找_users(一个std::unordered_map,键为客户端唯一标识,值为客户端地址信息)中是否存在key对应的客户端。
      • 如果不存在(it == _users.end()),则在日志中记录新用户信息,并将客户端地址信息(peer)插入到_users中,以key为键。
  5. 广播处理结果

    • 遍历_users中的每个客户端(使用范围基础迭代器auto &iter)。
      • 构造要发送的消息:将iter.first(客户端唯一标识)与#buffer拼接成形如127.0.0.1-1234# 你好的字符串,并存储在sendMessage中。
      • 记录日志,表明即将向哪个客户端推送消息。
      • 使用sendto函数,将sendMessage发送回客户端。发送目标地址由iter.second(客户端地址信息)提供,通过解引用并强制转换为struct sockaddr *类型传给sendto函数。同时传递实际地址长度sizeof(iter.second)

综上所述,UdpServer类的Start函数实现了以下功能:

  • 无限循环接收客户端通过UDP发送的数据。
  • 对接收到的数据进行简单处理,生成客户端唯一标识。
  • 更新客户端列表,记录新加入的客户端。
  • 将接收到的数据以客户端唯一标识#原始数据的形式广播给所有已连接的客户端。

五、多线程通信

//udp_server.hpp:
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1)
    {
    }
    bool initServer()
    {
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET->FP_INET
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

        return true;
    }
    void Start()
    {
        char buffer[SIZE];
        for (;;)
        {
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0;
             
                uint16_t cli_port = ntohs(peer.sin_port);
                std::string cli_ip = inet_ntoa(peer.sin_addr);
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
                logMessage(NORMAL, "key: %s", key);
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // exists
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
      
            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    uint16_t _port;
    std::string _ip;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _users;
    std::queue<std::string> messageQueue;
};

#endif


//udp_client.cc:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"

uint16_t serverport = 0;
std::string serverip;

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    std::string message;
    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());

    while (true)
    {
        std::cerr << "请输入你的信息# ";
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    serverport = atoi(argv[2]);
    serverip = argv[1];

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));

    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);
    return 0;
}

//udp_server.cc:
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    // std::string ip = argv[1];
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}

//thread.hpp:
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);

class ThreadData
{
public:
    void *args_;
    std::string name_;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void *args) : func_(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
        name_ = nameBuffer;

        tdata_.args_ = args;
        tdata_.name_ = name_;
    }
    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void*)&tdata_);
    }
    void join()
    {
        pthread_join(tid_, nullptr);
    }
    std::string name()
    {
        return name_;
    }
    ~Thread()
    {
    }

private:
    std::string name_;
    fun_t func_;
    ThreadData tdata_;
    pthread_t tid_;
};


//log.hpp:
#pragma once

#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);

    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

1、udp_server.hpp

这段代码实现了一个基础的UDP服务器,它监听指定端口(可选绑定特定IP地址),接收客户端发送的消息,并将接收到的消息广播给所有已知客户端。服务器使用std::unordered_map存储客户端信息,并通过日志模块记录关键操作。

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>

#define SIZE 1024

class UdpServer
{
public:
    // 构造函数初始化UdpServer实例,接受用户指定的端口号和可选的IP地址
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1)
    {
    }

    // 初始化服务器:创建套接字并将其绑定到指定的IP地址和端口
    bool initServer()
    {
        // 1. 创建一个基于IPv4的UDP套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET->FP_INET
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. 绑定套接字到指定的IP地址和端口
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);

        // 如果IP地址为空,则绑定到所有可用网络接口(INADDR_ANY)
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

        return true;
    }

    // 开始服务器循环,监听并处理客户端消息
    void Start()
    {
        char buffer[SIZE];

        // 无限循环,保持服务器运行直到进程终止
        for (;;)
        {
            // 接收客户端发送的消息
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;

            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 将接收到的数据视为字符串

                // 输出发送数据的客户端信息(IP地址和端口号)
                uint16_t cli_port = ntohs(peer.sin_port);
                std::string cli_ip = inet_ntoa(peer.sin_addr);

                // 根据客户端IP地址和端口号构建唯一标识符(key)
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
                logMessage(NORMAL, "key: %s", key);

                // 查找或添加客户端到用户映射表(_users)
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // 新增客户端到映射表
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
            // 向所有已知客户端广播接收到的消息
            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }

    // 服务器析构时关闭套接字
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    // 服务器端口
    uint16_t _port;

    // 服务器IP地址(可选)
    std::string _ip;

    // 服务器套接字描述符
    int _sock;

    // 用户映射表(键:客户端标识符;值:客户端套接字地址结构)
    std::unordered_map<std::string, struct sockaddr_in> _users;
};

#endif

这段代码定义了一个名为UdpServer的类,用于实现一个简单的UDP服务器。以下是各部分的详细解释:

  1. 包含头文件

    • log.hpp: 自定义日志模块头文件。
    • <iostream><string><cerrno><cstring><cstdlib><strings.h><sys/types.h><sys/socket.h><netinet/in.h><arpa/inet.h><unistd.h>:这些标准库头文件提供了网络编程所需的类型定义、错误码、字符串操作、系统调用(如socket、close等)以及网络地址转换函数(如inet_addr、inet_ntoa等)。
    • <unordered_map>:用于存储客户端信息的哈希映射表。
    • <queue>:虽然在这个示例中未使用,但保留了队列容器以备将来扩展。
  2. 类定义

    • UdpServer类包含以下成员变量:
      • _port: 服务器监听的端口号(uint16_t类型)。
      • _ip: 服务器绑定的IP地址(std::string类型,可选,默认为空,表示绑定到所有可用网络接口)。
      • _sock: 服务器套接字描述符(int类型,初始值为-1)。
      • _users: 一个std::unordered_map,键为客户端标识符(IP地址-端口号字符串),值为客户端的struct sockaddr_in套接字地址结构。
      • messageQueue: 一个std::queue,用于存储待处理的消息(在此示例中未使用)。
  3. 构造函数

    • UdpServer(uint16_t port, std::string ip = ""): 构造函数接受一个端口号(port)和一个可选的IP地址(ip)。当创建UdpServer实例时,会使用这些参数初始化对应的成员变量。
  4. 成员方法

    a. initServer()

    • 该方法负责初始化服务器,包括创建套接字和将其绑定到指定的IP地址和端口。
    • 创建套接字
      • 使用socket()系统调用创建一个基于IPv4的UDP套接字。如果创建失败(返回值小于0),记录错误信息并退出程序。
    • 绑定套接字
      • 初始化struct sockaddr_in结构体local,设置其sin_familyAF_INET(IPv4),sin_port为传入的端口号(通过htons()转换为网络字节序)。
      • 如果_ip为空,将local.sin_addr.s_addr设为INADDR_ANY,使服务器监听所有网络接口。否则,使用inet_addr()将IP地址字符串转换为网络字节序整数。
      • 调用bind()将套接字绑定到指定的IP地址和端口。如果绑定失败,记录错误信息并退出程序。
    • 返回
      • 成功初始化后,记录一条日志消息并返回true

    b. Start()

    • 该方法启动服务器循环,持续监听并处理来自客户端的消息。
    • 接收客户端消息
      • 在无限循环中,使用recvfrom()系统调用接收客户端发送的UDP数据包。同时,更新peer(客户端套接字地址结构)和len(地址结构长度)。
      • 如果接收到有效数据(s > 0),将缓冲区末尾的空字符置零,以便将接收到的数据视为字符串。
      • 获取客户端的IP地址(通过inet_ntoa()转换为点分十进制字符串)和端口号(通过ntohs()转换为主机字节序)。
      • 根据客户端IP地址和端口号构建唯一的标识符(key)。
      • 查找_users映射表中是否存在该标识符。若不存在,将客户端信息(keypeer)插入映射表,并记录一条日志消息。
    • 向所有客户端广播消息
      • 遍历_users映射表,对每个已知客户端:
        • 构建一条要发送的消息(包含客户端标识符、分隔符#和接收到的数据)。
        • 记录一条日志消息,表明将消息推送给该客户端。
        • 使用sendto()系统调用将消息发送给客户端,根据映射表中的struct sockaddr_in信息指定目标地址。

    c. 析构函数:当UdpServer实例被销毁时,如果套接字描述符 _sock 大于等于0(即已成功创建套接字),调用close()系统调用关闭套接字。

2、udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

// 打印程序使用说明的辅助函数
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    // 1. 参数检查:确保命令行提供了服务器监听的端口号
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    // 2. 创建UDP服务器实例,指定监听端口
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    // 3. 初始化服务器
    svr->initServer();

    // 4. 启动服务器主循环,开始接收并处理客户端消息
    svr->Start();

    return 0;
}

3、udp_client.cc

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

// 全局变量,用于存储服务器的IP地址和端口号
uint16_t serverport = 0;
std::string serverip;

// 打印程序使用说明的辅助函数
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// 定义发送消息的线程函数
static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    std::string message;
    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());

    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

// 定义接收回复的线程函数
static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }
}

int main(int argc, char *argv[])
{
    // 1. 参数检查:确保命令行提供了服务器的IP地址和端口号
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    // 2. 创建UDP套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // 3. 解析服务器IP地址和端口号
    serverport = atoi(argv[2]);
    serverip = argv[1];

    // 4. 创建并启动发送消息的线程
    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    sender->start();

    // 5. 创建并启动接收回复的线程
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));
    recver->start();

    // 6. 等待两个线程完成
    sender->join();
    recver->join();

    // 7. 关闭套接字,释放资源
    close(sock);

    return 0;
}

udp_client.cc 文件提供了使用 UDP 协议实现的一个简单客户端程序。以下是文件中各部分的详细讲解:

头文件包含与全局变量声明

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

uint16_t serverport = 0;
std::string serverip;

// ...(其他辅助函数定义)
  • 包含了必要的头文件,如 <iostream><string><sys/socket.h> 等,用于网络编程、输入输出和字符串处理。
  • 引入了 <memory> 和 thread.hpp 头文件,分别用于智能指针管理和多线程支持。
  • 定义了全局变量 serverport 和 serverip,用于存储服务器的端口号和 IP 地址。

usage() 函数:打印程序使用说明。

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

udpSend函数

udpSend是一个静态全局函数,它作为线程函数在客户端程序中被用来发送用户输入的消息到指定的服务器。

  1. 函数声明

    static void *udpSend(void *args)

    函数声明为static,意味着其作用域仅限于当前编译单元(即源文件)。返回类型为void *,这是POSIX线程(pthread)库中线程函数的标准返回类型,虽然在这个例子中实际返回值并未使用。参数类型为void *,表示它可以接受任何类型指针作为参数,这是因为线程库并不关心线程函数的具体参数类型,而是由程序员在创建线程时自行传递并正确转换。

  2. 参数解析

    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    函数参数args实际上指向一个ThreadData结构体实例,该结构体包含了线程所需的上下文信息。这里首先将args强制转换为ThreadData *指针类型,然后通过访问其成员args_(类型为void *)来获取实际的int类型套接字描述符sock。接着,同样通过访问name_成员获取线程名称(std::string类型)。

  3. 初始化服务器信息

    std::string message;
    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());

    初始化message变量用于存储用户输入的消息。接着,定义一个struct sockaddr_in类型的变量server,并使用memset清零。然后设置server的成员:

    • sin_family:设置为AF_INET,表示使用IPv4协议。
    • sin_port:将全局变量serverport转换为网络字节序(大端字节序),赋值给sin_port,表示服务器端口号。
    • sin_addr.s_addr:调用inet_addr将全局变量serverip(服务器IP地址字符串)转换为二进制整数形式,赋值给sin_addr.s_addr
  4. 消息发送循环

    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    进入无限循环,提示用户输入消息,并通过std::getline从标准输入(通常是键盘)读取一行文本,存入message变量。如果用户输入的是“quit”,则跳出循环。否则,调用sendto函数将message发送至服务器。

  5. 函数返回

    return nullptr;

    线程函数返回nullptr。虽然返回值在本例中未被使用,但遵循了POSIX线程库的要求。

udpRecv函数

udpRecv是一个静态全局函数,它作为线程函数在客户端程序中被用来接收服务器发送的回复消息。以下是该函数的详细讲解:

  1. 函数声明

    static void *udpRecv(void *args)

    函数声明为static,意味着其作用域仅限于当前编译单元(即源文件)。返回类型为void *,这是POSIX线程(pthread)库中线程函数的标准返回类型,虽然在这个例子中实际返回值并未使用。参数类型为void *,表示它可以接受任何类型指针作为参数,这是因为线程库并不关心线程函数的具体参数类型,而是由程序员在创建线程时自行传递并正确转换。

  2. 参数解析

    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    函数参数args实际上指向一个ThreadData结构体实例,该结构体包含了线程所需的上下文信息。这里首先将args强制转换为ThreadData *指针类型,然后通过访问其成员args_(类型为void *)来获取实际的int类型套接字描述符sock。接着,同样通过访问name_成员获取线程名称(std::string类型),但在本例中,线程名称似乎并未被使用。

  3. 初始化缓冲区

    char buffer[1024];

    定义一个大小为1024字节的字符数组buffer,用于存储从服务器接收的数据。

  4. 接收消息循环

    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }

    进入无限循环,每次循环开始时,使用memset清零buffer数组,确保其内容为空。然后定义一个临时的struct sockaddr_in变量temp,用于存储发送方(服务器)的地址信息。初始化socklen_t类型的变量lentemp结构体的大小,以便在recvfrom函数中接收实际填充的地址信息长度。

    调用recvfrom函数接收数据:

    • sock:套接字描述符,用于指定接收数据的套接字。
    • buffer:接收数据的缓冲区。
    • sizeof buffer:缓冲区大小。
    • 0:接收标志,通常设为0。
    • (struct sockaddr *)&temp:指向接收方地址结构体的指针。
    • &len:指向实际填充地址信息长度的指针。

    如果recvfrom成功接收到数据(返回值s > 0),则将buffer末尾的第s个位置置零(添加空字符\0),确保数据被视为C风格字符串。接着,将接收到的字符串打印到标准输出(通常是终端),并换行。

    循环继续,等待接收下一条消息。只有在程序意外终止或网络中断等异常情况下才会退出循环。

main() 函数

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    serverport = atoi(argv[2]);
    serverip = argv[1];

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));

    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);
    return 0;
}

该主函数是客户端程序的主要逻辑部分,负责创建UDP套接字、解析服务器地址和端口、启动发送和接收消息的线程,并在所有线程结束后关闭套接字和释放资源。以下是详细的步骤说明:

  1. 参数检查: 首先,检查命令行参数的数量(argc)是否等于3。若不满足,则调用usage函数打印程序使用帮助,并通过exit(1)退出程序。

  2. 创建UDP套接字: 使用socket函数创建一个UDP套接字,参数分别为AF_INET(表示使用IPv4协议族)和SOCK_DGRAM(表示创建一个数据报(UDP)类型的套接字)。如果创建失败(返回值小于0),则输出错误信息并退出程序。

  3. 解析服务器IP地址和端口号: 将命令行参数中的第二个参数(argv[1])赋值给全局变量serverip(服务器IP地址),第三个参数(argv[2])通过atoi函数转换为整数类型并赋值给全局变量serverport(服务器端口号)。

  4. 创建并启动发送消息的线程: 创建一个std::unique_ptr<Thread>对象,其中Thread类构造函数参数分别为线程ID(这里是1)、线程函数指针(指向udpSend函数)、以及线程函数参数(套接字描述符sock的地址)。调用start方法启动该线程,该线程负责从标准输入读取用户输入的消息,并通过套接字发送给服务器。

  5. 创建并启动接收回复的线程: 同样创建另一个std::unique_ptr<Thread>对象,但线程ID为2,线程函数指针指向udpRecv函数。启动该线程后,它将持续监听套接字,接收来自服务器的回复,并将接收到的消息打印到标准输出。

  6. 等待两个线程完成: 调用senderrecver线程对象的join方法,阻塞当前主线程,直至这两个线程都执行完毕。

  7. 关闭套接字,释放资源: 在所有线程都执行完后,调用close函数关闭之前创建的UDP套接字,释放系统资源。

  8. 返回0: 最后,主函数返回0,表示程序执行成功。

整个主函数的工作流程就是初始化客户端环境、创建并启动负责收发消息的线程,然后等待所有线程执行完毕后清理资源,实现了一个基本的UDP客户端程序功能。

4、thread.hpp线程管理

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);

// 定义线程数据结构,用于存储传递给线程回调函数的参数及线程名称
class ThreadData
{
public:
    void *args_; // 存储传递给线程回调函数的参数
    std::string name_; // 存储线程名称
};

// 定义线程类,封装了POSIX线程(pthread)的创建、启动、加入和销毁等功能
class Thread
{
public:
    // 构造函数,接收线程编号、回调函数指针和传递给回调函数的参数
    Thread(int num, fun_t callback, void *args)
        : func_(callback)
    {
        char nameBuffer[64]; // 临时缓冲区,用于构建线程名称字符串
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num); // 生成线程名称
        name_ = nameBuffer; // 将线程名称存储在类成员变量中

        tdata_.args_ = args; // 将参数存储在ThreadData实例中
        tdata_.name_ = name_; // 将线程名称存储在ThreadData实例中
    }

    // 启动线程
    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void*)&tdata_); // 使用pthread_create创建并启动线程
    }

    // 等待线程结束
    void join()
    {
        pthread_join(tid_, nullptr); // 使用pthread_join等待线程结束
    }

    // 获取线程名称
    std::string name()
    {
        return name_; // 直接返回类成员变量中的线程名称
    }

    // 析构函数,确保线程资源被正确释放
    ~Thread()
    {
    }

private:
    // 线程名称
    std::string name_;

    // 回调函数指针
    fun_t func_;

    // 存储线程参数和名称的ThreadData实例
    ThreadData tdata_;

    // 线程标识符(pthread_t类型)
    pthread_t tid_;
};

5、log.hpp日志系统

#pragma once

#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

// #define LOGFILE "./threadpool.log"

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);

    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

六、Windows客户端

在Windows环境下实现的一个简单的UDP客户端程序,主要用于与指定服务器(IP自定义,端口为8080)进行UDP通信。

#pragma warning(disable:4996)
#include <WinSock2.h>
#include <iostream>
#include <string>

using namespace std;
#pragma comment(lib,"ws2_32.lib") //固定用法

uint16_t serverport = 8080;
std::string serverip = "120.78.126.148";//你的服务器IP

int main()
{
	// windows 独有的
	WSADATA WSAData;
	WORD sockVersion = MAKEWORD(2, 2);
	if (WSAStartup(sockVersion, &WSAData) != 0)
		return 0;

	SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
	if (INVALID_SOCKET == clientSocket)
	{
		cout << "socket error!";
		return 0;
	}

	sockaddr_in dstAddr;
	dstAddr.sin_family = AF_INET;
	dstAddr.sin_port = htons(serverport);
	dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());

	char buffer[1024];

	while (true)
	{
		std::string message;
		std::cout << "请输入# ";
		std::getline(std::cin, message);
		sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));

		struct sockaddr_in temp;
		int len = sizeof(temp);
		int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
		if (s > 0)
		{
			buffer[s] = '\0';
			std::cout << "server echo# " << buffer << std::endl;
		}
	}

	// windows 独有
	closesocket(clientSocket);
	WSACleanup();

	return 0;
}
  1. 预处理器指令

    #pragma warning(disable:4996)

    这行代码用于禁用Visual Studio编译器针对inet_addr函数的安全警告,因为inet_addr函数在处理无效IP字符串时可能导致问题,推荐使用inet_pton替代。

  2. WinSock2库初始化

    WSADATA WSAData;
    WORD sockVersion = MAKEWORD(2, 2);
    if (WSAStartup(sockVersion, &WSAData) != 0)
        return 0;

    在Windows平台上,使用WinSock2库进行网络编程需要先调用WSAStartup函数进行初始化。sockVersion指定要使用的WinSock版本号(这里是2.2版),成功初始化后才能创建和使用套接字。

  3. 创建UDP套接字

    SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (INVALID_SOCKET == clientSocket)
    {
        cout << "socket error!";
        return 0;
    }

    使用socket函数创建一个UDP类型的套接字,AF_INET表示使用IPv4协议,SOCK_DGRAM表示数据报(UDP)类型。如果返回值为INVALID_SOCKET,则表明套接字创建失败。

  4. 设置服务器地址结构体

    sockaddr_in dstAddr;
    dstAddr.sin_family = AF_INET;
    dstAddr.sin_port = htons(serverport);
    dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());

    初始化sockaddr_in结构体,设置为服务器的IP地址和端口号。htons函数将主机字节序的端口号转换为网络字节序。inet_addr将服务器IP地址的字符串形式转换为网络字节格式。

  5. 通信循环

    
    	while (true)
    	{
    		std::string message;
    		std::cout << "请输入# ";
    		std::getline(std::cin, message);
    		sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));
    
    		struct sockaddr_in temp;
    		int len = sizeof(temp);
    		int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
    		if (s > 0)
    		{
    			buffer[s] = '\0';
    			std::cout << "server echo# " << buffer << std::endl;
    		}
    	}

    进入一个无限循环,不断接收用户输入并通过UDP发送给服务器,同时接收服务器的回应并显示。

  6. 发送数据

    std::string message;
    std::cout << "请输入# ";
    std::getline(std::cin, message);
    sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));

    从标准输入读取用户输入的消息,然后调用sendto函数将消息发送给服务器。sendto函数参数包括套接字描述符、待发送的数据(转化为C字符串)、数据长度、标志位(此处为0,表示默认选项)、服务器地址结构体指针和地址结构体大小。

  7. 接收数据

    struct sockaddr_in temp;
    int len = sizeof(temp);
    int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
    if (s > 0)
    {
        buffer[s] = '\0';
        std::cout << "server echo# " << buffer << std::endl;
    }

    使用recvfrom函数接收服务器发来的数据,数据存放在buffer中。若成功接收数据(接收字节数大于0),则在接收缓冲区末尾添加结束符'\0',将其视为字符串并打印出来。

  8. 资源清理

    closesocket(clientSocket);
    WSACleanup();

    在退出程序前,需要关闭套接字(调用closesocket函数),并调用WSACleanup函数释放WinSock库占用的资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1607997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Flutter】自动生成图片资源索引插件一:FlutterAssetRefGenerator

介绍 FlutterAssetRefGenerator 插件&#xff1a;windows上 点击生成图片索引按钮后&#xff0c;pubspec.yaml 会出现中文乱码&#xff0c;需要手动改乱码&#xff1b;mac上没问题。 优点&#xff1a;点击图标自动生成。 目录 介绍一、安装二、使用 一、安装 安装FlutterAsset…

ubunt18.04安装ROS2

本文无废话&#xff0c;实现了ubunt18.04 下ros2的安装&#xff0c;并且同时兼容ros和ros2 如果想完ros&#xff08;1&#xff09;的&#xff0c;请参考我的前一篇文章&#xff1a;ubunt18.04安装ROS避坑指南 参考&#xff1a; https://blog.csdn.net/cau_weiyuhu/article/deta…

2024年【A特种设备相关管理(锅炉压力容器压力管道)】考试内容及A特种设备相关管理(锅炉压力容器压力管道)操作证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;考试内容为正在备考A特种设备相关管理&#xff08;锅炉压力容器压力管道&#xff09;操作证的学员准备的理论考试专题&#xff0c;每个月更新的…

5.Godot节点和功能及Node节点属性分析

1. 节点和功能的关系 节点 Node &#xff0c;用于实现一种功能&#xff0c;例如&#xff0c;Sprite 节点&#xff0c;用于图片的显示一个节点的功能取决于它挂载了哪些子节点&#xff0c;它包含了哪些功能的子节点&#xff0c;就包含了对应子节点表示的功能节点是可选的&#…

Dota2 参议院

题目链接 Dota2 参议院 题目描述 注意点 senate[i] 为 ‘R’ 或 ‘D’假设每一位参议员都足够聪明&#xff0c;会为自己的政党做出最好的策略 解答思路 对于任意一位参议员&#xff0c;如果其有权利&#xff0c;当他后面没有另一方参议员&#xff0c;其会投票&#xff0c;…

小红书笔记写作方法和技巧分享,纯干货!

很多小伙伴感叹小红书笔记流量就是一个玄学&#xff0c;有时精心撰写的笔记却没有人看&#xff0c;自己随便写的笔记却轻轻松松上热门。实际上你还是欠点火候&#xff0c;小红书笔记写作是有一套方法和技巧的&#xff0c;总归是有套路的&#xff0c;如果你不知道&#xff0c;请…

C++ 数据结构 linux 【第一天】

1.命名空间 在C/C中&#xff0c;变量、函数和类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存在于全局作 用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c;以避免命名冲突或名字污染&#xff0c;namespace关键字…

【MIT6.824】lab3 Fault-tolerant Key/Value Service 实现笔记

引言 lab3A的实验要求如下&#xff1a; Your first task is to implement a solution that works when there are no dropped messages, and no failed servers. You’ll need to add RPC-sending code to the Clerk Put/Append/Get methods in client.go, and implement Pu…

✌粤嵌—2024/4/12—插入区间✌

代码实现&#xff1a; 解题思路&#xff1a;先将数组 newInterval 插入到数组 intervals 的末尾&#xff0c;再转换成合并区间 /*** Return an array of arrays of size *returnSize.* The sizes of the arrays are returned as *returnColumnSizes array.* Note: Both returne…

组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测

组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测 目录 组合预测 | Matlab实现ICEEMDAN-SMA-SVM基于改进完备集合经验模态分解-黏菌优化算法-支持向量机的时间序列预测预测效果基本介绍程序设计参考资料预测效果 基本…

两个查国内产业信息新闻数据的必备网站

产业经济信息网&#xff1a;产业经济信息网由报业协会主管主办&#xff0c;成立于1997年&#xff0c;是由报业行业报委员会发起&#xff0c;几十家权威行业媒体共同组建的、国内最大的行业信息发布网站之一。网站所拥有的“产经数据库”容纳了54家行业媒体的信息数据200多万条&…

Nextjs学习入门 - 创建第一个项目

1 通过npx创建一个nextjs项目 通过命令创建&#xff1a; npx create-next-applatest 得到如下项目结构图&#xff1a; my-app- src //源代码目录- app //引用目录- favicon.ico //网站图标- globals.css //全局css- layout.tsx //布局文件- page.tsx //页面 路径"…

C语言C/S架构PACS影像归档和通信系统源码 医院PACS系统源码

C语言C/&#xff33;架构PACS影像归档和通信系统源码 医院PACS系统源码 医院影像科PACS系统&#xff0c;意为影像归档和通信系统。它是应用在医院影像科室的系统&#xff0c;主要的任务是把日常产生的各种医学影像&#xff08;包括核磁、CT、超声、各种X光机、各种红外仪、显微…

YOLO-World——S(cvpr2024)

文章目录 Abstract成果 MethodPre-training Formulation: Region-Text PairsModel ArchitectureYOLO DetectorText EncoderText Contrastive HeadTraining with Online VocabularyInference with Offline Vocabulary Re-parameterizable Vision-Language PANText-guided CSPLay…

JavaSE 有这一篇就够(呕心狂敲41k字,只为博君一点赞!)

目录 一. 基础语法 1. 数据类型 2. 基本数据类型转换 3. 运算符 3. 循环语句 5. 定义方法 6. 数组 二. 面向对象 1. 类和对象 2. 构造方法 3. 方法的重载 4. this关键字 5. static关键字 6. 代码块 7. 访问权限修饰符 8. 面向对象的三大特征 封装 继承…

开关到模拟量全覆盖钡铼IOy系列模块集成热电阻、热电偶等传感器

钡铼IOy系列模块作为一种创新的工业自动化解决方案&#xff0c;以其灵活的自由拼接设计和丰富的接口类型&#xff0c;在工业级DI/DO/AI/AO集成方案中扮演着重要角色。其中&#xff0c;其在集成热电阻、热电偶等传感器方面的能力更是为工业控制系统带来了全新的可能性。 开关到…

BNB链融合

BNB Chain融合 BNB Chain目前有BNB智能链&#xff08;BSC&#xff09;&#xff0c;BNB信标链 BNB信标链&#xff1a;用作质押和投票的治理层&#xff0c;采用BEP-2代币标准BNB智能链(BSC)&#xff1a;用作EVM兼容层&#xff0c;提供DApp、DeFi服务、共识层、多链支持和其他Web3…

NVIDIA NCCL 源码学习(十四)- NVLink SHARP

背景 上节我们介绍了IB SHARP的工作原理&#xff0c;进一步的&#xff0c;英伟达在Hopper架构机器中引入了第三代NVSwitch&#xff0c;就像机间IB SHARP一样&#xff0c;机内可以通过NVSwitch执行NVLink SHARP&#xff0c;简称nvls&#xff0c;这节我们会介绍下NVLink SHARP如…

EasyExcel追加写入数据,分批查询多次写入场景下,注意使用方式【OOM警告】

使用.withTemplate(file) 将临时数据文件和真实数据文件合并的方式&#xff0c;在生产环境大批量数据下&#xff0c;完全不可取&#xff0c;有很高的内存溢出风险 伪代码 public static void writeAppend(String fileName) {String filePath "tempDir".concat(Fil…

linux_python源码安装及基础设置odoo安装

python源码安装及基础设置 1、资源下载2、源码安装3、 yum安装pip4、pip安装虚拟环境1、安装虚拟环境库2、配置环境变量3、创建自己的虚拟环境 5、安装升级pip的两种方式1、get-pip.py升级2、安装源码升级 6、odoo部署 1、资源下载 python3.13 python版本库 2、源码安装 yum…