【Linux】35.封装 UdpSocket(2)

news/2025/2/23 10:15:55

文章目录

  • 2. 实现一个简易的聊天室应用
    • 2.1 log.hpp - 日志记录系统
    • 2.2 Terminal.hpp - 终端重定向管理器
    • 2.3 UdpClient.cc - 多线程聊天客户端
    • 2.4 UdpServer.hpp - 广播式聊天服务器
    • 2.5 main.cc - 服务器启动程序


2. 实现一个简易的聊天室应用

2.1 log.hpp - 日志记录系统

log.hpp

#pragma once  // 防止头文件被重复包含

// 包含必要的头文件
#include <iostream>     // 标准输入输出
#include <time.h>      // 时间相关函数
#include <stdarg.h>    // 可变参数函数支持
#include <sys/types.h> // 系统类型定义
#include <sys/stat.h>  // 文件状态
#include <fcntl.h>     // 文件控制
#include <unistd.h>    // UNIX标准函数
#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
        printOneFile(filename, logtxt);
    }

    // 重载函数调用运算符,实现格式化日志输出
    void operator()(int level, const char *format, ...)
    {
        // 1. 获取时间信息
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        
        // 2. 格式化时间和级别信息
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", 
                 levelToString(level).c_str(),
                 ctime->tm_year + 1900,    // 年(从1900年开始)
                 ctime->tm_mon + 1,        // 月(0-11,需要+1)
                 ctime->tm_mday,           // 日
                 ctime->tm_hour,           // 时
                 ctime->tm_min,            // 分
                 ctime->tm_sec);           // 秒

        // 3. 处理可变参数
        va_list s;                         // 定义可变参数列表
        va_start(s, format);               // 初始化可变参数列表
        char rightbuffer[SIZE];            // 存储格式化后的可变参数
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);                         // 清理可变参数列表

        // 4. 组合完整日志消息
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // 5. 输出日志
        printLog(level, logtxt);
    }

private:
    int printMethod;        // 日志输出方式
    std::string path;       // 日志文件路径
};

/* 可变参数示例
int sum(int n, ...)
{
    va_list s;           // 定义可变参数列表
    va_start(s, n);      // 初始化,n是最后一个固定参数

    int sum = 0;
    while(n)
    {
        sum += va_arg(s, int);  // 依次获取int类型的参数
        n--;
    }

    va_end(s);          // 清理可变参数列表
    return sum;
}
*/

2.2 Terminal.hpp - 终端重定向管理器

Terminal.hpp

// 包含必要的头文件
#include <iostream>      // 标准输入输出流
#include <string>        // 字符串类
#include <unistd.h>     // UNIX标准函数,提供dup2等系统调用
#include <sys/types.h>  // 基本系统数据类型
#include <sys/stat.h>   // 文件状态信息
#include <fcntl.h>      // 文件控制选项

// 指定终端设备路径
// /dev/pts/N 是伪终端(pseudo-terminal)的设备文件
// 数字N表示特定的终端编号
std::string terminal = "/dev/pts/6";

// 打开并重定向到指定终端的函数
int OpenTerminal()
{
    // 以只写模式打开终端设备
    // O_WRONLY: 以只写方式打开
    int fd = open(terminal.c_str(), O_WRONLY);
    
    // 错误处理:如果打开失败
    if(fd < 0)
    {
        std::cerr << "open terminal error" << std::endl;
        return 1;
    }

    // 重定向标准错误输出(stderr)到新打开的终端
    // dup2(oldfd, newfd):将newfd重定向到oldfd
    // 2代表标准错误输出(stderr)
    dup2(fd, 2);

    /* 测试代码(已注释)
    // printf("hello world\n");
    // close(fd);
    */

    return 0;
}

这段代码的主要功能是:

  1. 打开一个指定的伪终端设备(/dev/pts/6)
  2. 将标准错误输出重定向到这个终端
  3. 这样所有的错误信息都会显示在指定的终端上

使用场景:

  • 调试输出重定向
  • 日志输出到特定终端
  • 多终端输出管理

注意事项:

  1. 需要确保目标终端(/dev/pts/6)存在且有写权限
  2. 终端编号(6)可能需要根据实际情况调整
  3. 使用完后应该关闭文件描述符(当前未实现)

2.3 UdpClient.cc - 多线程聊天客户端

UdpClient.cc

// 必要的头文件
#include <iostream>      // 标准输入输出
#include <cstdlib>      // 标准库函数
#include <unistd.h>     // UNIX标准函数
#include <strings.h>    // bzero等字符串操作
#include <string.h>     // 字符串操作
#include <sys/types.h>  // 基本系统数据类型
#include <sys/socket.h> // 套接字接口
#include <netinet/in.h> // Internet地址族
#include <arpa/inet.h>  // IP地址转换函数
#include <pthread.h>    // POSIX线程
#include "Terminal.hpp" // 终端操作

using namespace std;

// 打印使用说明
void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// 线程间共享数据结构
struct ThreadData
{
    struct sockaddr_in server; // 服务器地址信息
    int sockfd;               // 套接字描述符
    std::string serverip;     // 服务器IP地址
};

// 接收消息的线程函数
void *recv_message(void *args)
{
    // OpenTerminal(); // 可选的终端重定向
    ThreadData *td = static_cast<ThreadData *>(args); // 类型转换
    char buffer[1024]; // 接收缓冲区

    while (true)
    {
        memset(buffer, 0, sizeof(buffer)); // 清空缓冲区
        struct sockaddr_in temp;           // 临时存储发送方地址
        socklen_t len = sizeof(temp);

        // 接收数据
        // sockfd: 套接字描述符
        // buffer: 接收缓冲区
        // 1023: 接收大小(留1字节给\0)
        // flags: 0
        // temp: 发送方地址
        // len: 地址结构长度
        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);
    string message;
    socklen_t len = sizeof(td->server);

    // 发送欢迎消息
    std::string welcome = td->serverip;
    welcome += " comming...";
    sendto(td->sockfd, message.c_str(), message.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);
    }
}

// 主函数
// 使用方式:./udpclient serverip serverport
int main(int argc, char *argv[])
{
    // 检查命令行参数
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    
    // 获取服务器IP和端口
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 初始化线程数据结构
    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套接字
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

    td.serverip = serverip;

    // 创建接收和发送线程
    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);
    return 0;
}

工作流程:

初始化
  ↓
创建套接字
  ↓
创建线程 ─┬─→ 接收线程: 循环接收服务器消息
          └─→ 发送线程: 循环发送用户输入
  ↓
等待线程结束
  ↓
清理资源

代码功能总结:

  1. 创建UDP套接字与服务器通信
  2. 使用多线程分别处理消息的发送和接收
  3. 发送线程负责获取用户输入并发送
  4. 接收线程负责显示来自服务器的消息
  5. 实现了简单的聊天室客户端功能

主要特点:

  1. 使用UDP协议进行通信
  2. 多线程设计,支持同时收发消息
  3. 支持命令行参数配置服务器地址

2.4 UdpServer.hpp - 广播式聊天服务器

UdpServer.hpp

#pragma once  // 防止头文件重复包含

// 必要的头文件包含
#include <iostream>      // 标准输入输出
#include <string>        // 字符串类
#include <strings.h>     // bzero等字符串操作
#include <cstring>       // C风格字符串操作
#include <sys/types.h>   // 基本系统数据类型
#include <sys/socket.h>  // 套接字接口
#include <netinet/in.h>  // Internet地址族
#include <arpa/inet.h>   // IP地址转换函数
#include <functional>    // std::function
#include <unordered_map> // 哈希表
#include "Log.hpp"       // 日志系统

// 定义回调函数类型:处理消息并返回响应
// 参数:消息内容、客户端IP、客户端端口
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;

// 全局日志对象
Log lg;

// 错误码枚举
enum {
    SOCKET_ERR = 1,  // 套接字创建错误
    BIND_ERR         // 绑定错误
};

// 默认配置
uint16_t defaultport = 8080;              // 默认端口号
std::string defaultip = "0.0.0.0";        // 默认IP(监听所有网卡)
const int size = 1024;                    // 缓冲区大小

// UDP服务器类
class UdpServer {
public:
    // 构造函数:初始化服务器参数
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        :sockfd_(0), port_(port), ip_(ip), isrunning_(false)
    {}

    // 初始化服务器
    void Init()
    {
        // 创建UDP套接字
        // AF_INET: IPv4协议族
        // SOCK_DGRAM: UDP数据报套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            lg(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
        }
        lg(Info, "socket create success, sockfd: %d", sockfd_);

        // 绑定套接字到指定地址和端口
        struct sockaddr_in local;
        bzero(&local, sizeof(local));           // 清零地址结构
        local.sin_family = AF_INET;             // 使用IPv4
        local.sin_port = htons(port_);          // 端口转换为网络字节序
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); // IP转换为网络字节序
        // local.sin_addr.s_addr = htonl(INADDR_ANY);   // 替代方案:监听所有网卡

        // 绑定套接字
        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_)
        {
            // 构造广播消息格式:[IP:PORT]# message
            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;
            }
            
            // 获取客户端信息
            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_; // 在线用户表
};

服务器工作流程:

  1. 初始化:创建套接字并绑定到指定地址和端口
  2. 主循环:
    • 接收客户端消息
    • 检查是否是新用户
    • 广播消息给所有在线用户
  3. 关闭:清理资源

特点:

  1. 支持多客户端
  2. 消息广播功能
  3. 在线用户管理
  4. 集成日志系统

2.5 main.cc - 服务器启动程序

main.cc

#include "UdpServer.hpp"  // UDP服务器类
#include <memory>        // 智能指针
#include <cstdio>       // 标准输入输出
#include <vector>       // vector容器

// "120.78.126.148" 点分十进制字符串风格的IP地址

// 打印使用说明
void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

// 主函数
// 使用方式:./udpserver port
int main(int argc, char *argv[])
{
    // 检查命令行参数
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    // 获取端口号
    uint16_t port = std::stoi(argv[1]);

    // 创建UDP服务器实例(使用智能指针管理)
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    // 初始化服务器
    svr->Init();
    // 运行服务器
    svr->Run();

    return 0;
}

代码说明:

  1. 这是一个UDP服务器的主程序
  2. 支持命令行参数指定端口号
  3. 使用智能指针管理服务器实例
  4. 原本包含远程命令执行功能,但已被注释掉以提高安全性

被注释掉的功能:

  1. Handler: 消息处理函数
  2. SafeCheck: 命令安全检查
  3. ExcuteCommand: 命令执行功能

安全考虑:

  1. 移除了远程命令执行功能
  2. 去掉了可能造成系统危险的操作
  3. 将服务器功能限制为纯消息转发

使用方式:

./udpserver 8080 // 在8080端口启动服务器


http://www.niftyadmin.cn/n/5863305.html

相关文章

【QT】信号与槽 窗口坐标

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Qt 目录 一&#xff1a;&#x1f525; Qt 中的窗口坐标 二&#xff1a;&#x1f525; 信号和槽 &#x1f98b; 信号和槽概述&#x1f98b; 信号的本质&#x1f98b; 槽的本质 三&#xff1a;&#…

实验题目:SQL 数据更新、视图

一、实验环境 实验使用普通PC机一台&#xff0c;MySQL数据库版本8.0.36&#xff0c;使用Navicat Premium 16提供图形化界面。 二、实验内容 1、insert 语句使用方法 2、update 语句使用方法 3、delete 语句使用方法 4、掌握 truncate table 语句使用方法 5、掌握视图的创…

分布式锁实现(数据库+Redis+Zookeeper)

1. 数据库分布式锁 实现原理 基于唯一索引&#xff1a; 创建一张锁表&#xff0c;通过唯一索引&#xff08;如锁名称&#xff09;保证互斥性。 加锁&#xff1a;插入一条记录&#xff0c;成功则获取锁&#xff0c;失败则重试。 解锁&#xff1a;删除对应记录。 乐观锁&…

Vue3 + Spring WebMVC 验证码案例中的跨域问题与解决方法

最近在基于vue3 SpringWebMVC前后端分离的开发环境中实现一个验证码的案例&#xff0c;在开发过程中遇到了一些复杂的跨域问题&#xff0c;现已解决&#xff0c;故将解决方法分享&#xff0c;希望能帮到有需要的人。 出现的问题&#xff1a; 对于验证码的实现&#xff0c;我选…

deepseek自动化代码生成

使用流程 效果第一步&#xff1a;注册生成各种大模型的API第二步&#xff1a;注册成功后生成API第三步&#xff1a;下载vscode在vscode中下载agent&#xff0c;这里推荐使用cline 第四步&#xff1a;安装完成后&#xff0c;设置模型信息第一步选择API provider&#xff1a; Ope…

Python基于Django的广州、北京、杭州二手房房价可视化分析系统(附源码)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

4. grafana(7.5.17)功能菜单简介

点击可以返回home页面 搜索Dashboard 新建按钮&#xff1a;用户创建Dashboard、文件夹。以及导入外部&#xff08;社区&#xff09;Dashboard 用于查看活管理Dashboard&#xff0c;包括home、Manage、playlists、snapshots功能 explore&#xff08;探索&#xff09;&#x…

linux串口通讯

在当今的科技世界中,串口通讯虽然不像一些新兴的高速通信技术那般夺目,但它依然在众多领域有着不可替代的地位,尤其是在嵌入式系统开发、工业自动化控制等场景。而 Linux 系统,凭借其开源、稳定且强大的特性,为串口通讯提供了出色的支持。 一、串口通讯基础 串口通讯,简…