【探索Linux】强大的命令行工具P.28:简单模拟UDP网络程序实现

阅读导航

  • 引言
  • 一、UDP协议
  • 二、UDP网络程序模拟实现
  • 1. 预备代码
  • ⭕makefile文件
  • ⭕打印日志文件
  • ⭕打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
  • 2. UDP 服务器端实现(UdpServer.hpp)
  • 3. UDP 客户端实现(main函数)
  • 温馨提示
  • 引言

    在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。本文将延续上文内容,重点讨论简单的UDP网络程序模拟实现。通过本文的学习,读者将能够深入了解UDP协议的实际应用,并掌握如何编写简单的UDP网络程序。让我们一起深入探讨UDP网络程序的实现细节,为网络编程的学习之旅添上一份精彩的实践经验。

    一、UDP协议

    UDP(User Datagram Protocol)是一种无连接的、轻量级的网络传输协议,它提供了快速、简单的数据传输服务。下面是一个简单的UDP程序实现示例,包括一个UDP服务器和一个UDP客户端。详介绍可以看上一篇文章:UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同

    二、UDP网络程序模拟实现

    1. 预备代码

    ⭕makefile文件

    .PHONY:all
    all:udpserver udpclient
    
    udpserver:Main.cc
    	g++ -o $@ $^ -std=c++11
    udpclient:UdpClient.cc
    	g++ -o $@ $^ -lpthread -std=c++11
    
    
    .PHONY:clean
    clean:
    	rm -f udpserver udpclient
    

    这段代码是一个简单的 Makefile 文件,用于编译 UDP 服务器(udpserver)和 UDP 客户端(udpclient)的程序。在这个 Makefile 中定义了两个规则:

    1. all:表示默认的目标,依赖于 udpserver 和 udpclient 目标,即执行 make 命令时会编译 udpserver 和 udpclient。
    2. clean:用于清理生成的可执行文件 udpserver 和 udpclient。

    在 Makefile 中使用了一些特殊的关键字和变量:

  • .PHONY:声明 all 和 clean 是伪目标,不是真正的文件名。
  • $@:表示目标文件名。
  • $^:表示所有依赖文件列表。
  • -std=c++11:指定 C++ 的编译标准为 C++11。
  • -lpthread:链接 pthread 库,用于多线程支持。
  • ⭕打印日志文件

    #pragma once
    
    #include <iostream>
    #include <time.h>
    #include <stdarg.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    #define SIZE 1024
    
    #define Info 0
    #define Debug 1
    #define Warning 2
    #define Error 3
    #define Fatal 4
    
    #define Screen 1
    #define Onefile 2
    #define Classfile 3
    
    #define LogFile "log.txt"
    
    class Log
    {
    public:
        Log()
        {
            printMethod = Screen; // 默认输出方式为屏幕打印
            path = "./log/"; // 默认日志文件存放路径
        }
    
        void Enable(int method)
        {
            printMethod = method; // 设置日志输出方式(屏幕、单个文件、分类文件)
        }
    
        std::string levelToString(int level)
        {
            switch (level)
            {
            case Info:
                return "Info";
            case Debug:
                return "Debug";
            case Warning:
                return "Warning";
            case Error:
                return "Error";
            case Fatal:
                return "Fatal";
            default:
                return "None";
            }
        }
    
        void printLog(int level, const std::string &logtxt)
        {
            switch (printMethod)
            {
            case Screen:
                std::cout << logtxt << std::endl; // 屏幕打印日志信息
                break;
            case Onefile:
                printOneFile(LogFile, logtxt); // 将日志信息追加写入单个文件
                break;
            case Classfile:
                printClassFile(level, logtxt); // 将日志信息追加写入分类文件
                break;
            default:
                break;
            }
        }
    
        void printOneFile(const std::string &logname, const std::string &logtxt)
        {
            std::string _logname = path + logname; // 构建日志文件的完整路径
            int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打开文件,如果文件不存在则创建
            if (fd < 0)
                return;
            write(fd, logtxt.c_str(), logtxt.size()); // 将日志信息写入文件
            close(fd);
        }
    
        void printClassFile(int level, const std::string &logtxt)
        {
            std::string filename = LogFile;
            filename += ".";
            filename += levelToString(level); // 构建分类文件名,例如"log.txt.Debug/Warning/Fatal"
            printOneFile(filename, logtxt); // 将日志信息追加写入分类文件
        }
    
        ~Log()
        {
        }
    
        void operator()(int level, const char *format, ...)
        {
            time_t t = time(nullptr);
            struct tm *ctime = localtime(&t);
            char leftbuffer[SIZE];
            snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                     ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                     ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
    
            va_list s;
            va_start(s, format);
            char rightbuffer[SIZE];
            vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
            va_end(s);
    
            // 格式:默认部分+自定义部分
            char logtxt[SIZE * 2];
            snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
    
            printLog(level, logtxt); // 打印日志信息
        }
    
    private:
        int printMethod; // 日志输出方式
        std::string path; // 日志文件存放路径
    };
    

    该代码实现了一个简单的日志记录类(Log),其中包括设置日志输出方式(屏幕、单个文件、分类文件)和打印日志信息的功能。

  • Log 类是一个用于记录日志的类。
  • Enable 函数用于设置日志输出方式,可以选择屏幕打印、单个文件或分类文件。
  • printLog 函数根据设置的日志输出方式,将日志信息打印到屏幕、追加写入单个文件或分类文件。
  • printOneFile 函数用于将日志信息追加写入单个文件。
  • printClassFile 函数用于将日志信息追加写入分类文件。
  • levelToString 函数将日志级别转换为对应的字符串表示。
  • operator() 函数是重载的函数调用运算符,用于打印日志信息。
  • path 是日志文件存放路径,默认为"./log/"。
  • printMethod 是日志输出方式,默认为屏幕打印。
  • SIZE 定义了缓冲区大小。
  • InfoDebugWarningErrorFatal 是日志级别的定义。
  • ScreenOnefileClassfile 是日志输出方式的定义。
  • LogFile 是单个文件名的定义。
  • ⭕打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符

    #include <iostream>
    #include <string>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    // 定义要打开的终端设备文件路径
    std::string terminal = "/dev/pts/6";
    
    // 打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符
    int OpenTerminal()
    {
        // 使用open函数以只写方式打开终端设备文件
        int fd = open(terminal.c_str(), O_WRONLY);
        if(fd < 0)
        {
            // 如果打开终端设备文件失败,则输出错误信息到标准错误输出
            std::cerr << "open terminal error" << std::endl;
            return 1; // 返回错误代码
        }
    
        // 将终端设备文件的文件描述符复制给标准错误输出的文件描述符
        // 这样标准错误输出就会重定向到指定的终端设备上
        dup2(fd, 2);
    
        // 如果需要在此处输出信息到标准错误输出,可以使用printf等函数
    
        // 关闭文件描述符
        // close(fd);
    
        return 0; // 返回成功代码
    }
    
    

    这段代码的作用是打开一个终端设备文件 “/dev/pts/6”,将其作为标准错误输出(stderr)的目标文件描述符,实现将错误信息输出到指定的终端设备上。

  • terminal 变量存储了要打开的终端设备文件路径 “/dev/pts/6”。
  • OpenTerminal 函数尝试打开指定的终端设备文件,并将其作为标准错误输出的目标文件描述符。
  • 首先使用 open 函数打开终端设备文件,以只写方式(O_WRONLY)。
  • 如果成功打开终端设备文件,则将其文件描述符复制给标准错误输出的文件描述符(2),即 dup2(fd, 2),这样标准错误输出就会重定向到该终端设备上。
  • 如果打开终端设备文件失败,则输出错误信息到标准错误输出,并返回错误代码 1。
  • 最后函数返回0表示成功。
  • 2. UDP 服务器端实现(UdpServer.hpp)

    #pragma once
    
    #include <iostream>
    #include <string>
    #include <strings.h>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <functional>
    #include <unordered_map>
    #include "Log.hpp"
    
    // 使用Log类记录日志信息
    Log lg;
    
    enum {
        SOCKET_ERR = 1,
        BIND_ERR
    };
    
    uint16_t defaultport = 8080;
    std::string defaultip = "0.0.0.0";
    const int size = 1024;
    
    class UdpServer {
    public:
        UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
            : sockfd_(0), port_(port), ip_(ip), isrunning_(false)
        {}
    
        void Init() {
            // 1. 创建UDP socket
            sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
            if (sockfd_ < 0) {
                lg(Fatal, "socket create error, sockfd: %d", sockfd_);
                exit(SOCKET_ERR);
            }
            lg(Info, "socket create success, sockfd: %d", sockfd_);
    
            // 2. 绑定socket
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port_); // 端口号需要转换为网络字节序
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将IP地址转换为网络字节序
    
            if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
                lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
                exit(BIND_ERR);
            }
            lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
        }
    
        void CheckUser(const struct sockaddr_in& client, const std::string clientip, uint16_t clientport) {
            // 检查用户是否已经存在在线用户列表中
            auto iter = online_user_.find(clientip);
            if (iter == online_user_.end()) {
                online_user_.insert({clientip, client});
                std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
            }
        }
    
        void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport) {
            // 广播消息给所有在线用户
            for (const auto& user : online_user_) {
                std::string message = "[";
                message += clientip;
                message += ":";
                message += std::to_string(clientport);
                message += "]# ";
                message += info;
                
                socklen_t len = sizeof(user.second);
                sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
            }
        }
    
        void Run() {
            isrunning_ = true;
            char inbuffer[size];
            while (isrunning_) {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
    
                // 接收客户端发送的消息
                ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
                if (n < 0) {
                    lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                    continue;
                }
    
                // 获取客户端的IP地址和端口号
                uint16_t clientport = ntohs(client.sin_port);
                std::string clientip = inet_ntoa(client.sin_addr);
    
                // 检查用户是否已经存在在线用户列表中
                CheckUser(client, clientip, clientport);
    
                std::string info = inbuffer;
    
                // 将接收到的消息广播给所有在线用户
                Broadcast(info, clientip, clientport);
            }
        }
    
        ~UdpServer() {
            if (sockfd_ > 0)
                close(sockfd_);
        }
    
    private:
        int sockfd_; // 网络文件描述符
        std::string ip_; // 服务器IP地址
        uint16_t port_; // 服务器端口号
        bool isrunning_; // 服务器运行状态
        std::unordered_map<std::string, struct sockaddr_in> online_user_; // 在线用户列表
    };
    
  • Log.hpp 是用于记录日志信息的头文件。
  • lg 是一个 Log 类的对象,用于输出日志信息。
  • enum 定义了两个错误类型:SOCKET_ERRBIND_ERR,分别表示 socket 创建错误和绑定错误。
  • defaultportdefaultip 分别设置默认的端口号和 IP 地址。
  • size 定义接收缓冲区的大小为 1024 字节。
  • UdpServer 类封装了一个 UDP 服务器。
  • 构造函数 UdpServer 接受端口号和 IP 地址作为参数,并初始化成员变量。
  • Init 函数用于初始化 UDP 服务器,其中:
  • 创建 UDP socket,并检查创建是否成功。
  • 绑定 socket 到指定的 IP 地址和端口号,并检查绑定是否成功。
  • CheckUser 函数用于检查用户是否已经存在在线用户列表中,如果不存在则将其添加到列表中。
  • Broadcast 函数用于向所有在线用户广播消息,其中:
  • 消息格式为 [发送者IP:发送者端口号]# 消息内容
  • 使用 sendto 函数发送消息给每个在线用户。
  • Run 函数是 UDP 服务器的主循环,其中:
  • 循环接收客户端发送的消息,并将其广播给所有在线用户。
  • 对每个客户端,获取其 IP 地址和端口号,并进行用户检查和消息广播。
  • ~UdpServer 析构函数关闭网络文件描述符。
  • sockfd_ 是网络文件描述符,用于创建和管理网络连接。
  • ip_ 是服务器的 IP 地址。
  • port_ 是服务器的端口号。
  • isrunning_ 表示服务器的运行状态,用于控制循环退出。
  • online_user_ 是一个无序映射,用于保存在线用户的 IP 地址和对应的 sockaddr_in 结构体。
  • 3. UDP 客户端实现(main函数)

    #include <iostream>
    #include <cstdlib>
    #include <unistd.h>
    #include <strings.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <pthread.h>
    #include "Terminal.hpp"
    
    using namespace std;
    
    // 函数声明:打印程序的使用方法
    void Usage(std::string proc);
    
    // 结构体:用于传递线程参数
    struct ThreadData
    {
        struct sockaddr_in server; // 服务器地址结构体
        int sockfd; // socket 文件描述符
        std::string serverip; // 服务器 IP 地址
    };
    
    // 线程函数:接收消息
    void *recv_message(void *args);
    
    // 线程函数:发送消息
    void *send_message(void *args);
    
    // 主函数
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]); // 打印使用方法
            exit(0);
        }
    
        // 解析命令行参数
        std::string serverip = argv[1]; // 服务器 IP 地址
        uint16_t serverport = std::stoi(argv[2]); // 服务器端口号
    
        // 初始化 ThreadData 结构体
        struct ThreadData td;
        bzero(&td.server, sizeof(td.server)); // 清零服务器地址结构体
        td.server.sin_family = AF_INET; // 设置地址族为 IPv4
        td.server.sin_port = htons(serverport); // 设置端口号(转换为网络字节序)
        td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 设置服务器 IP 地址(转换为网络字节序)
    
        // 创建 UDP socket
        td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (td.sockfd < 0)
        {
            cout << "socket error" << endl;
            return 1;
        }
    
        td.serverip = serverip; // 存储服务器 IP 地址
    
        pthread_t recvr, sender; // 定义接收消息和发送消息的线程
        pthread_create(&recvr, nullptr, recv_message, &td); // 创建接收消息线程
        pthread_create(&sender, nullptr, send_message, &td); // 创建发送消息线程
    
        // 等待接收消息和发送消息的线程退出
        pthread_join(recvr, nullptr);
        pthread_join(sender, nullptr);
    
        close(td.sockfd); // 关闭 socket
        return 0;
    }
    
    // 函数实现:打印程序的使用方法
    void Usage(std::string proc)
    {
        std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
    }
    
    // 线程函数实现:接收消息
    void *recv_message(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args); // 强制类型转换为 ThreadData 结构体指针
        char buffer[1024]; // 接收消息的缓冲区
        while (true)
        {
            memset(buffer, 0, sizeof(buffer)); // 清空缓冲区
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
    
            ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len); // 接收消息
            if (s > 0)
            {
                buffer[s] = 0;
                cerr << buffer << endl; // 输出接收到的消息
            }
        }
    }
    
    // 线程函数实现:发送消息
    void *send_message(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args); // 强制类型转换为 ThreadData 结构体指针
        string message; // 存储用户输入的消息
        socklen_t len = sizeof(td->server); // 服务器地址的长度
    
        // 发送欢迎消息
        std::string welcome = td->serverip + " comming...";
        sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr *)&(td->server), len);
    
        while (true)
        {
            cout << "Please Enter@ ";
            getline(cin, message); // 获取用户输入的消息
    
            sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len); // 发送消息给服务器
        }
    }
    
    

    温馨提示

    感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

    再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

    作者:Yawesh

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【探索Linux】强大的命令行工具P.28:简单模拟UDP网络程序实现

    发表评论