Tcp_Sever(线程池版本的 TCP 服务器)

news2024/11/26 11:10:15

Tcp_Sever(线程池版本的 TCP 服务器)

  • 前言
  • 1. 功能介绍及展示
    • 1.1 服务端连接
    • 1.2 客户端连接(可多个用户同时在线连接服务端)
    • 1.3 功能服务
      • 1.3.1 defaultService(默认服务)
      • 1.3.2 transform(大小写转换)
      • 1.3.3 ping(ping服务)
      • 1.3.4 translate(翻译)
    • 1.4 服务器重连功能
  • 2. 代码实现
    • 2.1 总体代码设计
      • 2.1.1 .cc文件
      • 2.1.2 .hpp文件
      • 2.1.3 其他文件
    • 2.2具体实现(代码都有注释)
      • 2.2.1 Log.hpp
      • 2.2.2 nocopy.hpp
      • 2.2.3 LockGuard.hpp
      • 2.2.4 Comm.hpp
      • 2.2.5 Thread.hpp
      • 2.2.6 ThreadPool.hpp
      • 2.2.7 InetAddr.hpp
      • 2.2.8 Translate.hpp
      • 2.2.9 Tcp_Server.hpp
      • 2.2.10 Makefile
      • 2.2.11 Dict.txt
      • 2.2.12 Main.cc
      • 2.2.13 Tcp_Client.cc

前言


已经有半年没有更新博客了,在这期间,时而沉淀,时而因为就业感到迷茫,到现在,忽然看开了一点,不管未来咋样,至少不要让自己后悔,人生需要passion!干就完了!!!
在这里插入图片描述

源码地址:tcp_server

注:该网络服务只能在有公网ip的机器或者云服务器之间进行
虚拟机上只能进行本地连接,不能连接其他虚拟机

1. 功能介绍及展示

1.1 服务端连接

./tcp_server 8888

在这里插入图片描述

执行结果:
在这里插入图片描述

1.2 客户端连接(可多个用户同时在线连接服务端)

连接服务器要知道服务器的ip地址

我们执行本地测试时,可以用ifconfig指令查看本地ip地址
在这里插入图片描述

连接

./tcp_client+IP地址+服务器端口号
./tcp_client 192.168.42.200 8888

两个客户端同时连接
在这里插入图片描述
与此同时server端打印日志
在这里插入图片描述

1.3 功能服务

1.3.1 defaultService(默认服务)

默认服务就是给每个连接的客户端打印一份功能菜单
在这里插入图片描述

1.3.2 transform(大小写转换)

将小写字母转换为大写字母
在这里插入图片描述

1.3.3 ping(ping服务)

ping服务(心跳机制,用于检测服务是否正常),发送ping,服务器如果正常运行会回复一个Pong
在这里插入图片描述

1.3.4 translate(翻译)

输入英文单词,会返回对应的音标和中文解释
在这里插入图片描述

1.4 服务器重连功能

在连接过程中,如果服务端出现问题连接不上,可进行5次的重连,重连成功即可继续执行服务

在这里插入图片描述

2. 代码实现

2.1 总体代码设计

2.1.1 .cc文件

Main.cc:程序的初始化、配置以及主要逻辑流程。创建服务器或客户端实例,设置网络连接,处理用户输入
Tcp_Client.cc:实现TCP 客户端的功能。负责与服务器建立连接,发送和接收数据。包含连接管理、数据处理和错误处理的逻辑

2.1.2 .hpp文件

ThreadPool.hpp:定义线程池的接口和实现
LockGuard.hpp:实现一个锁的封装类,确保在作用域内自动加锁和解锁
InetAddr.hpp:处理网络地址相关的功能,IP 地址和端口的表示和转换
Comm.hpp:定义错误信息
Log.hpp:负责打印日志的功能。包含日志级别,日志时间
nocopy.hpp:防止类的复制构造和赋值操作,确保对象的唯一性
Tcp_Server.hpp:定义 TCP 服务器的接口和相关功能,实现如何接收客户端连接、处理请求和管理客户端会话
Thread.hpp:定义线程的接口和实现,管理线程的创建、执行和终止,与线程池配合使用
Translate.hpp:实现词典查找翻译功能

2.1.3 其他文件

Makefile:编译Tcp_Client.cc和Main.cc,同时便于管理生成的可执行程序
Dict.txt:存放词典数据

2.2具体实现(代码都有注释)

2.2.1 Log.hpp

#pragma once
#include <iostream>     // 引入输入输出流
#include <cstdarg>      // 引入变长参数处理
#include <ctime>        // 引入时间处理
#include <sys/types.h>  // 引入系统数据类型
#include <unistd.h>     // 引入Unix标准函数
#include <sys/stat.h>   // 引入文件状态处理
#include <fcntl.h>      // 引入文件控制定义
using namespace std;

// 定义日志级别
enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

// 定义日志输出样式
enum
{
    Screen = 10, // 输出到屏幕
    OneFile,     // 输出到一个文件
    ClassFile    // 输出到多个分类文件
};

// 常量定义
const int defaultstyle = Screen;            // 默认输出样式为屏幕
const std::string default_filename = "log."; // 默认日志文件名
const std::string logdir = "log";           // 日志目录

// 日志类定义
class Log
{
public:
    // 构造函数
    Log()
        : style(defaultstyle),                // 初始化日志样式
          filename(default_filename)         // 初始化文件名
    {
        mkdir(logdir.c_str(), 0775);        // 创建日志目录
    }

    // 启用指定的日志样式
    void Enable(int sty)
    {
        style = sty;
    }

    // 将日志级别转换为字符串
    std::string LevelToString(int level)
    {
        switch (level)
        {
        case Debug:
            return "Debug";
        case Info:
            return "Info";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "Unknown"; // 返回未知级别
        }
    }

    // 获取当前时间戳并格式化为字符串
    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr); // 获取当前时间
        struct tm *local_time = localtime(&currtime); // 转换为本地时间
        char time_buff[128];
        snprintf(time_buff, sizeof(time_buff), "%d-%d-%d %d:%d:%d",
                 local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
                 local_time->tm_hour, local_time->tm_min, local_time->tm_sec);

        return time_buff; // 返回格式化的时间字符串
    }

    // 写入日志到单个文件
    void WriteLogToOneFile(const string &logname, const string &message)
    {
        umask(0); // 重置文件创建掩码
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666); // 打开(或创建)日志文件
        if (fd < 0)
            return; // 打开失败则返回
        write(fd, message.c_str(), message.size()); // 写入日志消息
        close(fd); // 关闭文件
    }

    // 写入日志到分类文件
    void WriteLogToClassFile(const string &levelstr, const string &message)
    {
        string logname = logdir; // 获取日志目录
        logname += '/';
        logname += filename; // 添加文件名
        logname += levelstr; // 添加级别后缀

        WriteLogToOneFile(logname, message); // 调用写入单个文件的函数
    }

    // 统一写入日志
    void WriteLog(const string &levelstr, const std::string &message)
    {
        switch (style)
        {
        case Screen:
            cout << message << endl; // 输出到屏幕
            break;
        case OneFile:
            WriteLogToOneFile("all", message); // 写入到单个文件
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message); // 写入到分类文件
            break;
        }
    }

    // 记录日志信息
    void LogMessage(int level, const char *format, ...)
    {
        char rightbuffer[1024]; // 存储格式化后的消息
        va_list args; // 定义变长参数列表

        va_start(args, format); // 初始化变长参数列表
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 格式化消息
        va_end(args); // 结束变长参数处理

        char leftbuffer[1024]; // 存储日志头信息
        std::string currtime = TimeStampExLocalTime(); // 获取当前时间
        std::string levelstr = LevelToString(level);   // 获取日志级别字符串
        std::string idstr = std::to_string(getpid());  // 获取当前进程ID
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ", levelstr.c_str(), currtime.c_str(), idstr.c_str()); // 生成日志头

        std::string loginfo = leftbuffer; // 合并日志头和消息
        loginfo += rightbuffer;
        WriteLog(levelstr, loginfo); // 写入日志
    }

    // 析构函数
    ~Log() {}

private:
    int style; // 日志输出样式
    std::string filename; // 日志文件名
};

// 创建全局日志实例
Log lg;

// 配置类定义
class Conf
{
public:
    // 构造函数
    Conf()
    {
        lg.Enable(Screen); // 默认启用屏幕输出
    }
    ~Conf() {} // 析构函数
};

// 创建全局配置实例
Conf conf;

2.2.2 nocopy.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream> // 引入输入输出流库(虽然这里未使用)

// nocopy 类用于禁止对象的复制和赋值
class nocopy
{
public:
    // 默认构造函数
    nocopy()
    {}

    // 禁止复制构造函数
    nocopy(const nocopy &) = delete; // 删除复制构造函数,防止对象复制

    // 禁止赋值操作符重载
    const nocopy &operator=(const nocopy &) = delete; // 删除赋值操作符,防止对象赋值

    // 默认析构函数
    ~nocopy()
    {}
};

2.2.3 LockGuard.hpp

#pragma once // 确保头文件只被包含一次

#include <pthread.h> // 引入 pthread 库以使用 POSIX 线程相关功能

// Mutex 类用于封装 pthread_mutex_t 锁对象
class Mutex
{
public:
    // 构造函数,接受外部传入的锁对象
    Mutex(pthread_mutex_t *lock) : _lock(lock) // 初始化锁对象指针
    {}

    // 锁定函数
    void Lock()
    {
        pthread_mutex_lock(_lock); // 调用 pthread 库的锁定函数
    }

    // 解锁函数
    void Unlock()
    {
        pthread_mutex_unlock(_lock); // 调用 pthread 库的解锁函数
    }

    // 析构函数
    ~Mutex()
    {}

private:
    pthread_mutex_t *_lock; // 指向 pthread_mutex_t 类型的锁对象指针
};

// LockGuard 类用于自动管理锁的获取与释放
class LockGuard
{
public:
    // 构造函数,接受外部传入的锁对象并自动锁定
    LockGuard(pthread_mutex_t *lock) : _mutex(lock) // 初始化 LockGuard 对象
    {
        _mutex.Lock(); // 在构造时锁定
    }

    // 析构函数,自动解锁
    ~LockGuard()
    {
        _mutex.Unlock(); // 在析构时解锁
    }

private:
    Mutex _mutex; // 使用 Mutex 类实例来管理锁
};

2.2.4 Comm.hpp

#pragma once // 确保头文件只被包含一次

// 定义公共错误类型
enum
{
    Usage_Err = 1, // 用法错误
    Socket_Err,    // 套接字错误
    Bind_Err,      // 绑定错误
    Listen_Err     // 监听错误
};

// 宏定义:将地址指针转换为 sockaddr 结构指针
#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

2.2.5 Thread.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream>     // 引入输入输出流库
#include <string>       // 引入字符串库
#include <functional>   // 引入函数对象库
#include <pthread.h>    // 引入 pthread 库以支持多线程

// 定义一个函数类型,用于传递线程要执行的函数,参数为 T 类型的引用
template<class T>
using func_t = std::function<void(T&)>;

// 定义一个线程类模板
template<class T>
class Thread
{
public:
    // 构造函数,初始化线程名称、函数、数据
    Thread(const std::string &threadname, func_t<T> func, T &data)
        : _tid(0),               // 初始化线程ID为0
          _threadname(threadname), // 初始化线程名称
          _isrunning(false),      // 初始化线程运行状态为false
          _func(func),            // 初始化要执行的函数
          _data(data)             // 初始化要传递的数据
    {}

    // 线程的执行例程
    static void *ThreadRoutine(void *args) // 静态成员函数
    {
        // (void)args; // 防止编译器警告(若不使用 args)
        Thread *ts = static_cast<Thread *>(args); // 将 void 指针转换为 Thread 指针

        ts->_func(ts->_data); // 调用传入的函数,并传递数据

        return nullptr; // 返回空指针
    }

    // 启动线程
    bool Start()
    {
        // 创建线程,执行 ThreadRoutine,传递当前对象的指针
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if (n == 0) // 创建成功
        {
            _isrunning = true; // 更新运行状态
            return true; // 返回成功
        }
        else return false; // 返回失败
    }

    // 等待线程完成
    bool Join()
    {
        if (!_isrunning) return true; // 如果线程没有运行,直接返回成功
        int n = pthread_join(_tid, nullptr); // 等待线程结束
        if (n == 0) // 等待成功
        {
            _isrunning = false; // 更新运行状态
            return true; // 返回成功
        }
        return false; // 返回失败
    }

    // 获取线程名称
    std::string ThreadName()
    {
        return _threadname; // 返回线程名称
    }

    // 检查线程是否正在运行
    bool IsRunning()
    {
        return _isrunning; // 返回当前运行状态
    }

    // 析构函数
    ~Thread()
    {}

private:
    pthread_t _tid;       // 线程ID
    std::string _threadname; // 线程名称
    bool _isrunning;      // 线程运行状态
    func_t<T> _func;     // 要执行的函数
    T _data;             // 传递给函数的数据
};

2.2.6 ThreadPool.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream>      // 引入输入输出流库
#include <queue>        // 引入队列库
#include <vector>       // 引入向量库
#include <pthread.h>    // 引入 pthread 库以支持多线程
#include <functional>    // 引入函数对象库
#include "Log.hpp"      // 引入日志功能
#include "Thread.hpp"   // 引入线程类
#include "LockGuard.hpp" // 引入锁保护类

namespace TreadNs // 定义命名空间 TreadNs
{

    static const int defaultnum = 3; // 默认线程数量

    // 线程数据类,存储线程的名称
    class ThreadData
    {
    public:
        // 构造函数,初始化线程名称
        ThreadData(const std::string &name) : threadname(name)
        {
        }

        // 析构函数
        ~ThreadData()
        {
        }

    public:
        std::string threadname; // 线程名称
    };

    // 线程池类模板
    template <class T>
    class ThreadPool
    {
    private:
        // 构造函数,初始化线程池,创建指定数量的线程
        ThreadPool(int thread_num = defaultnum) : _thread_num(thread_num)
        {
            pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁
            pthread_cond_init(&_cond, nullptr);   // 初始化条件变量

            // 创建指定数量的线程
            for (int i = 0; i < _thread_num; i++)
            {
                std::string threadname = "thread-"; // 线程名称
                threadname += std::to_string(i + 1); // 生成线程名称

                ThreadData td(threadname); // 创建线程数据对象

                // 创建线程并绑定执行函数 ThreadRun
                _threads.emplace_back(threadname,
                                      std::bind(&ThreadPool<T>::ThreadRun, this,
                                                std::placeholders::_1),
                                      td);
                lg.LogMessage(Info, "%s is created...\n", threadname.c_str()); // 记录线程创建日志
            }
        }

        // 删除复制构造函数和赋值操作符,禁止复制
        ThreadPool(const ThreadPool<T> &tp) = delete;
        const ThreadPool<T> &operator=(const ThreadPool<T>) = delete;

    public:
        // 获取线程池单例
        static ThreadPool<T> *GetInstance()
        {
            if (instance == nullptr) // 如果实例不存在
            {
                LockGuard lockguard(&sig_lock); // 使用锁确保线程安全
                if (instance == nullptr) // 再次检查
                {
                    lg.LogMessage(Info, "创建单例成功...\n"); // 记录单例创建日志
                    instance = new ThreadPool<T>(); // 创建单例实例
                }
            }

            return instance; // 返回线程池实例
        }

        // 启动线程池
        bool Start()
        {
            for (auto &thread : _threads) // 遍历所有线程
            {
                thread.Start(); // 启动线程
                lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str()); // 记录线程运行日志
            }

            return true; // 返回成功
        }

        // 线程等待函数
        void ThreadWait(const ThreadData &td)
        {
            lg.LogMessage(Debug, "no task, %s is sleeping...\n", td.threadname.c_str()); // 记录线程睡眠日志
            pthread_cond_wait(&_cond, &_mutex); // 等待条件变量
        }

        // 唤醒线程
        void ThreadWakeup()
        {
            pthread_cond_signal(&_cond); // 发送信号唤醒等待的线程
        }

        // 检查线程池自身状态(功能待实现)
        void checkSelf()
        {
            // 1. _task_num > _task_num_high_water && _thread_num < _thread_num_high_water
            // 创建更多的线程,并更新_thread_num

            // 2. _task_num == _task_num_low_water && _thread_num >= _thread_num_high_water
            // 退出线程,并更新_thread_num
        }

        // 线程执行函数
        void ThreadRun(ThreadData &td)
        {
            while (true) // 无限循环,持续处理任务
            {
                T t; // 存储任务

                {
                    LockGuard lockguard(&_mutex); // 使用锁保护临界区
                    while (_q.empty()) // 如果任务队列为空
                    {
                        ThreadWait(td); // 线程等待
                        lg.LogMessage(Debug, "thread %s is wakeup\n", td.threadname.c_str()); // 记录线程唤醒日志
                    }
                    t = _q.front(); // 获取队列首部任务
                    _q.pop(); // 移除任务
                }

                // 处理任务
                t(); // 执行任务
                // lg.LogMessage(Debug, "%s handler task %s done, result is : %s\n",
                //               td.threadname, t.PrintTask().c_str(), t.PrintResult().c_str());
            }
        }

        // 推送任务到队列
        void Push(T &in)
        {
            LockGuard lockguard(&_mutex); // 使用锁保护临界区
            _q.push(in); // 将任务放入队列
            ThreadWakeup(); // 唤醒线程处理新任务
        }

        // 析构函数,清理资源
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mutex); // 销毁互斥锁
            pthread_cond_destroy(&_cond); // 销毁条件变量
        }

        // 等待所有线程完成(用于调试)
        void Wait()
        {
            for (auto &thread : _threads)
            {
                thread.Join(); // 等待线程结束
            }
        }

    private:
        std::queue<T> _q; // 任务队列
        std::vector<Thread<ThreadData>> _threads; // 线程池中的线程
        int _thread_num; // 线程数量
        pthread_mutex_t _mutex; // 互斥锁
        pthread_cond_t _cond; // 条件变量

        static ThreadPool<T> *instance; // 线程池单例实例
        static pthread_mutex_t sig_lock; // 锁,用于控制单例创建的线程安全
    };

    // 初始化静态成员
    template <class T>
    ThreadPool<T> *ThreadPool<T>::instance = nullptr;

    template <class T>
    pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER; // 初始化信号锁

} // 结束命名空间 TreadNs

2.2.7 InetAddr.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream>      // 引入输入输出流库
#include <string>        // 引入字符串库
#include <sys/types.h>   // 引入系统数据类型
#include <sys/socket.h>   // 引入套接字相关函数
#include <netinet/in.h>  // 引入互联网域套接字
#include <arpa/inet.h>   // 引入地址转换函数

// InetAddr 类用于封装和处理 IPv4 地址和端口
class InetAddr
{
public:
    // 构造函数,接受一个 sockaddr_in 结构体引用
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        // 将网络字节序的端口转换为主机字节序
        _port = ntohs(_addr.sin_port);
        
        // 使用 inet_ntop 将网络字节顺序的 IP 地址转换为字符串形式
        char ipbuffer[64]; // 存储 IP 地址字符串
        inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer)); // 将 IPv4 地址转换为可读格式
        _ip = ipbuffer; // 将 IP 地址存储为字符串
    }

    // 获取 IP 地址
    std::string Ip() { return _ip; }

    // 获取端口号
    uint16_t Port() { return _port; }

    // 打印调试信息,格式为 "IP:Port"
    std::string PrintDebug()
    {
        std::string info = _ip; // 获取 IP 地址
        info += ":"; // 添加分隔符
        info += std::to_string(_port); // 添加端口号
        return info; // 返回完整信息
    }

    // 获取 sockaddr_in 结构体的引用
    const struct sockaddr_in& GetAddr()
    {
        return _addr; // 返回地址结构体
    }

    // 重载等于运算符,用于比较两个 InetAddr 对象
    bool operator == (const InetAddr& addr)
    {
        // 检查 IP 和端口是否相等
        return this->_ip == addr._ip && this->_port == addr._port;
    }

    // 析构函数
    ~InetAddr() {}
    
private:
    std::string _ip;          // 存储 IP 地址字符串
    uint16_t _port;           // 存储端口号
    struct sockaddr_in _addr; // 存储 sockaddr_in 结构体
};

2.2.8 Translate.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream>          // 引入输入输出流库
#include <unordered_map>     // 引入无序映射库
#include <string>            // 引入字符串库
#include <vector>            // 引入向量库
#include <fstream>           // 引入文件流库
#include "Log.hpp"          // 引入日志功能

using namespace std; // 使用标准命名空间

const string unknown = "未知的"; // 未知词的默认返回值
const string mydict = "./resource/Dict.txt"; // 默认字典文件路径
const string sep = " "; // 字典文件中词与翻译之间的分隔符

// Translate 类用于实现翻译功能
class Translate
{
public:
    // 构造函数,接受字典路径,默认值为 mydict
    Translate(string dict_path = mydict) : _dict_path(dict_path)
    {
        LoadDict(); // 加载字典文件
        Parse(); // 解析字典内容
    }

    // 加载字典文件
    void LoadDict()
    {
        ifstream in(_dict_path); // 打开字典文件
        string line; // 存储每一行内容
        while (getline(in, line)) // 逐行读取文件
        {
            lines.push_back(line); // 将每行内容存入 lines 向量
        }
        in.close(); // 关闭文件
        lg.LogMessage(Debug, "Load dict success, path : %s\n", _dict_path.c_str()); // 记录加载成功日志
    }

    // 调试函数,输出字典内容
    void debug()
    {
        // 可选:输出所有行
        // for (auto &e : lines)
        // {
        //     cout << e << endl;
        // }
        // 输出字典中的每个词及其翻译
        for (auto &elem : dict)
        {
            cout << elem.first << " : " << elem.second << endl; // 输出格式为 "词 : 翻译"
        }
    }

    // 解析字典内容
    void Parse()
    {
        for (auto &line : lines) // 遍历加载的每一行
        {
            auto pos = line.find(sep); // 查找分隔符位置
            if (pos == string::npos) continue; // 如果未找到,跳过该行
            else
            {
                string word = line.substr(0, pos); // 获取词
                string chinese = line.substr(pos + sep.size()); // 获取翻译
                dict.insert(std::make_pair(word, chinese)); // 将词和翻译插入字典
            }
        }
        lg.LogMessage(Debug, "Parse dict success, path : %s\n", _dict_path.c_str()); // 记录解析成功日志
    }

    // 查找翻译
    string Excute(string word)
    {
        auto iter = dict.find(word); // 查找词在字典中的位置
        if (iter == dict.end()) return unknown; // 如果词未找到,返回默认值
        else return dict[word]; // 返回对应的翻译
    }

    // 析构函数
    ~Translate()
    {
        // 在这里可以添加资源清理代码(如果需要)
    }

private:
    string _dict_path; // 字典文件路径
    unordered_map<string, string> dict; // 存储字典映射(词 -> 翻译)
    vector<string> lines; // 存储字典文件的每一行
};

2.2.9 Tcp_Server.hpp

#pragma once // 确保头文件只被包含一次

#include <iostream>          // 引入输入输出流库
#include <string>            // 引入字符串库
#include <cerrno>            // 引入错误号库
#include <cstring>           // 引入字符串处理库
#include <sys/types.h>       // 引入系统数据类型
#include <sys/socket.h>      // 引入套接字相关函数
#include <stdlib.h>          // 引入标准库
#include <netinet/in.h>      // 引入互联网域套接字
#include <arpa/inet.h>       // 引入地址转换函数
#include <sys/wait.h>        // 引入进程管理相关函数
#include <pthread.h>         // 引入 pthread 库以支持多线程
#include <functional>        // 引入函数对象库
#include <unordered_map>      // 引入无序映射库

#include "ThreadPool.hpp"     // 引入线程池类
#include "InetAddr.hpp"       // 引入地址类
#include "Log.hpp"            // 引入日志功能
#include "nocopy.hpp"         // 引入禁止复制的类
#include "Comm.hpp"           // 引入通信相关类
// #include "Task.hpp"          // 可选的任务类

const static int default_backlog = 5; // 默认最大连接数
using task_t = function<void()>; // 定义任务类型
using callback_t = function<void(int, InetAddr &)>; // 定义回调函数类型

class TcpServer; // 前向声明 TcpServer 类

// TcpServer 类用于实现 TCP 服务器功能
class TcpServer : nocopy // 继承 nocopy,禁止复制
{
public:
    // 构造函数,初始化服务器端口和运行状态
    TcpServer(uint16_t port) : _port(port), _isrunning(false)
    {
    }

    // 初始化服务器
    void Init()
    {
        // 1. 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字
        if (_listensock < 0)
        {
            // 如果创建失败,记录日志并退出
            lg.LogMessage(Fatal, "创建套接字失败: %d, error string: %s\n", errno, strerror(errno));
            exit(Fatal);
        }
        lg.LogMessage(Debug, "创建套接字成功: sockfd: %d", _listensock);

        // 解决绑定失败的问题
        int opt = 1; // 设置套接字选项
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 允许重用地址和端口

        // 2. 填充本地网络信息并绑定
        struct sockaddr_in local; // 定义本地地址结构
        memset(&local, 0, sizeof(local)); // 清空结构体
        local.sin_family = AF_INET; // 使用 IPv4
        local.sin_port = htons(_port); // 设置端口
        local.sin_addr.s_addr = INADDR_ANY; // 允许接受所有连接

        // 2.1 绑定套接字
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) != 0)
        {
            // 如果绑定失败,记录日志并退出
            lg.LogMessage(Fatal, "bind套接字失败: %d, error string: %s\n", errno, strerror(errno));
            exit(Bind_Err);
        }
        lg.LogMessage(Debug, "bind套接字成功: sockfd: %d", _listensock);

        // 3. 设置套接字为监听状态
        if (listen(_listensock, default_backlog) != 0)
        {
            // 如果监听失败,记录日志并退出
            lg.LogMessage(Fatal, "监听套接字失败: %d, error string: %s\n", errno, strerror(errno));
            exit(Listen_Err);
        }
        lg.LogMessage(Debug, "监听套接字成功: sockfd: %d\n", _listensock);

        // 启动线程池
        TreadNs::ThreadPool<task_t>::GetInstance()->Start();

        // 注册默认服务
        funcs.insert(std::make_pair("defaultService", std::bind(&TcpServer::DefaultService, this, std::placeholders::_1, std::placeholders::_2)));
    }

    // 处理客户端请求
    void Service(int sockfd, InetAddr &addr)
    {
        char buff[1024]; // 缓冲区
        while (true)
        {
            ssize_t n = read(sockfd, buff, sizeof(buff) - 1); // 从套接字读取数据
            if (n > 0)
            {
                buff[n] = 0; // 添加字符串结束符
                cout << addr.PrintDebug() << "# " << buff << endl; // 输出客户端地址和消息
                string echo_string = "server#:";
                echo_string += buff; // 构造回显字符串
                write(sockfd, echo_string.c_str(), echo_string.size()); // 将回显字符串写回客户端
            }
            else if (n == 0) // 返回值为0,表示对端关闭了连接
            {
                lg.LogMessage(Info, "对端关闭了连接\n");
                break; // 结束循环
            }
            else
            {
                lg.LogMessage(Info, "读消息失败: %d, error string: %s\n", errno, strerror(errno));
                break; // 结束循环
            }
        }
    }

    // 启动服务器
    void Start()
    {
        _isrunning = true; // 设置运行状态为真
        signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号,以免产生僵尸进程
        while (_isrunning)
        {
            // 4. 获取连接
            struct sockaddr_in peer; // 定义客户端地址结构
            socklen_t len = sizeof(peer); // 地址结构体大小
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len); // 接受连接

            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "获取套接字失败: %d, error string: %s\n", errno, strerror(errno));
                continue; // 继续循环,等待下一个连接
            }
            lg.LogMessage(Debug, "获取套接字成功: sockfd: %d", sockfd);

            // 5. 提供服务
            InetAddr addr(peer); // 封装客户端地址
            task_t t = bind(&TcpServer::Routine, this, sockfd, addr); // 绑定任务
            TreadNs::ThreadPool<task_t>::GetInstance()->Push(t); // 将任务推送到线程池
        }
    }

    // 读取数据
    string Read(int sockfd)
    {
        char type[1024]; // 数据类型缓冲区
        ssize_t n = read(sockfd, type, sizeof(type) - 1); // 从套接字读取数据
        if (n > 0)
        {
            type[n] = 0; // 添加字符串结束符
        }
        else if (n == 0) // 返回值为0,表示对端关闭了连接
        {
            lg.LogMessage(Info, "对端关闭了连接\n");
        }
        else
        {
            lg.LogMessage(Info, "读消息失败: %d, error string: %s\n", errno, strerror(errno));
        }
        return type; // 返回读取到的数据
    }

    // 处理例程
    void Routine(int sockfd, InetAddr &addr)
    {
        funcs["defaultService"](sockfd, addr); // 调用默认服务
        string type = Read(sockfd); // 读取请求类型
        lg.LogMessage(Debug, "%s select %s \n", addr.PrintDebug(), type.c_str());

        // 根据请求类型调用相应的服务
        if (type == "ping")
        {
            funcs[type](sockfd, addr); // 处理 ping 请求
        }
        else if (type == "translate") // 翻译服务
        {
            funcs[type](sockfd, addr);
        }
        else if (type == "transform") // 转换服务
        {
            funcs[type](sockfd, addr);
        }
        else
        {
            // 处理其他类型请求
        }

        close(sockfd); // 关闭套接字
    }

    // 默认服务
    void DefaultService(int sockfd, InetAddr& addr)
    {
        (void)addr; // 防止未使用警告
        std::string service_list = " |";
        for (auto func : funcs)
        {
            service_list += func.first; // 拼接服务列表
            service_list += "|";
        }
        write(sockfd, service_list.c_str(), service_list.size()); // 将服务列表写回客户端
    }

    // 注册回调函数
    void RegisterFunc(const string &name, callback_t func)
    {
        funcs[name] = func; // 将函数注册到字典中
    }

    // 析构函数
    ~TcpServer()
    {
        // 在这里可以添加资源清理代码(如果需要)
    }

private:
    uint16_t _port; // 服务器端口
    int _listensock; // 监听套接字
    bool _isrunning; // 服务器运行状态

    // 存储注册的服务函数
    unordered_map<string, callback_t> funcs;
};

2.2.10 Makefile

因为Makefile文件的代码注释看起·来比较乱,所以我分两部分放出来
无注释代码

.PHONY:all
all:tcp_server tcp_client

tcp_server:Main.cc
	g++ -o $@ $^ -lpthread -std=c++14
	
tcp_client:Tcp_Client.cc
	g++ -o $@ $^ -lpthread -std=c++14
.PHONY:clean
clean:
	rm -f tcp_server tcp_client

带注释代码

.PHONY: all         # 声明 'all' 是一个伪目标,不会生成同名文件
all: tcp_server tcp_client  # 默认目标,构建 tcp_server 和 tcp_client

# 目标 tcp_server 的构建规则
tcp_server: Main.cc   # 指定依赖文件 Main.cc
	g++ -o $@ $^ -lpthread -std=c++14  # 使用 g++ 编译 Main.cc,生成可执行文件 tcp_server
	# $@ 表示目标名称(tcp_server),$^ 表示所有依赖文件(Main.cc)

# 目标 tcp_client 的构建规则
tcp_client: Tcp_Client.cc  # 指定依赖文件 Tcp_Client.cc
	g++ -o $@ $^ -lpthread -std=c++14  # 使用 g++ 编译 Tcp_Client.cc,生成可执行文件 tcp_client
	# $@ 表示目标名称(tcp_client),$^ 表示所有依赖文件(Tcp_Client.cc)

.PHONY: clean      # 声明 'clean' 是一个伪目标,用于清理构建文件
clean:             # 定义清理规则
	rm -f tcp_server tcp_client  # 删除生成的可执行文件 tcp_server 和 tcp_client

2.2.11 Dict.txt

词典单词数据,可按格式自行添加,数量不限

accident ['æksidənt]   n.事故,意外,偶然
careful  ['keəful]   a.仔细(小心)的
difficulty  ['difikəlti]   n.困难
flag  [flæg]   n.旗帜
horse  [:s]   n.马
lock  [lɔk]   n.&v.锁
nut  [nʌt]   n.竖果,螺帽
rain  [rein]   n.&v.雨,下雨
silk  [silk]   n.丝,丝绸
thirty  ['θə:ti]   a.&n.三十()
accidental  [.æksi'dentl]   a.意外的,偶然的
carrot  ['kærət]   n.胡萝卜
dinner  ['dinə]   n.正餐,晚餐
flat  [flæt]   a.平的,扁平的;n.套间
hospital  ['hɔspitl]   n. 医院
lonely  ['ləunli]   a.孤单的,孤寂的,偏僻的
Oceania  [.əuʃi'einiə]   n.大洋洲
rainy  ['reini]   a.多雨的
simple  ['simpl]   a.简单的,单纯的,朴素的
though  [ðəu]   ad.可是;conj.虽然,尽管

2.2.12 Main.cc

#include <iostream>            // 引入输入输出流库
#include <memory>              // 引入智能指针库
#include <algorithm>           // 引入算法库
#include "Log.hpp"            // 引入日志功能
#include "Tcp_Server.hpp"     // 引入 TCP 服务器类
#include "Translate.hpp"      // 引入翻译类

using namespace std; // 使用标准命名空间

// 使用说明函数
void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" // 输出程序使用说明
              << std::endl;
}

Translate trans; // 创建翻译类的实例

// 交互函数,通过套接字与客户端进行通信
void Interact(int sockfd, string &out, const string &in)
{
    char buff[1024]; // 缓冲区用于接收数据
    ssize_t n = read(sockfd, buff, sizeof(buff) - 1); // 从套接字读取数据
    if (n > 0)
    {
        buff[n] = 0; // 添加字符串结束符
        write(sockfd, in.c_str(), in.size()); // 将响应写回客户端
    }
    else if (n == 0) // 返回值为0,表示对端关闭了连接
    {
        lg.LogMessage(Info, "对端关闭了连接\n");
    }
    else
    {
        lg.LogMessage(Info, "读消息失败: %d, error string: %s\n", errno, strerror(errno));
    }
}

// 心跳机制函数,用于检测服务是否正常
void Ping(int sockfd, InetAddr addr)
{
    lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.PrintDebug().c_str(), "ping", sockfd);
    string message; // 用于存储响应消息
    Interact(sockfd, message, "Pong"); // 与客户端交互,发送 "Pong"
}

// 翻译服务函数
void Translate_S(int sockfd, InetAddr addr)
{
    lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.PrintDebug().c_str(), "Translate", sockfd);
    char wordbuff[128]; // 缓冲区用于接收单词
    int n = read(sockfd, wordbuff, sizeof(wordbuff) - 1); // 从套接字读取单词
    if (n > 0) wordbuff[n] = 0; // 添加字符串结束符
    std::string chinese = trans.Excute(wordbuff); // 调用翻译功能
    write(sockfd, chinese.c_str(), chinese.size()); // 将翻译结果写回客户端

    lg.LogMessage(Debug, "%s Translate , %s -> %s\n", addr.PrintDebug().c_str(), wordbuff, chinese.c_str()); // 记录翻译日志
}

// 转换服务函数,将消息转换为大写
void Transform(int sockfd, InetAddr addr)
{
    lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.PrintDebug().c_str(), "Transform", sockfd);
    char message[128]; // 缓冲区用于接收消息
    int n = read(sockfd, message, sizeof(message) - 1); // 从套接字读取消息
    if (n > 0) message[n] = 0; // 添加字符串结束符
    string messagebuf = message; // 转换为字符串
    transform(messagebuf.begin(), messagebuf.end(), messagebuf.begin(), [](unsigned char c)
    {
        return toupper(c); // 将字符转换为大写
    });
    write(sockfd, messagebuf.c_str(), messagebuf.size()); // 将转换后的消息写回客户端
}

int main(int argc, char *argv[])
{
    if (argc != 2) // 检查命令行参数数量
    {
        Usage(argv[0]); // 输出使用说明
        return Usage_Err; // 返回错误代码
    }
    uint16_t port = stoi(argv[1]); // 将端口号从字符串转换为整型

    unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port); // 创建 TCP 服务器实例

    // 注册服务函数
    tsvr->RegisterFunc("ping", Ping);
    tsvr->RegisterFunc("translate", Translate_S);
    tsvr->RegisterFunc("transform", Transform);

    tsvr->Init(); // 初始化服务器

    tsvr->Start(); // 启动服务器

    return 0; // 程序正常结束
}

2.2.13 Tcp_Client.cc

#include <iostream>            // 引入输入输出流库
#include <string>              // 引入字符串库
#include <cerrno>              // 引入错误号库
#include <cstring>             // 引入字符串处理库
#include <sys/types.h>        // 引入系统数据类型
#include <sys/socket.h>       // 引入套接字相关函数
#include <stdlib.h>           // 引入标准库
#include <netinet/in.h>       // 引入互联网域套接字
#include <arpa/inet.h>        // 引入地址转换函数
#include <unistd.h>           // 引入 UNIX 标准函数
#include <signal.h>           // 引入信号处理库

using namespace std; // 使用标准命名空间
#define Retry_count 5 // 定义最大重试次数

// 信号处理函数
void handler(int signo)
{
    std::cout << "signo: " << signo << std::endl; // 输出接收到的信号编号
    exit(0); // 退出程序
}

// 输出程序的使用说明
void Usage(const std::string &process)
{
    std::cout << "Usage: " << process << " + server_ip + server_port" << std::endl; // 指导用户如何使用程序
}

// 访问服务器的函数
bool visitServer(string &server_ip, uint16_t &server_port, int *cnt)
{
    // 1. 创建套接字
    string inbuffer; // 输入缓冲区用于接收用户输入
    char service_list[1024]; // 缓冲区用于存储服务列表
    ssize_t m = 0; // 读取字节数
    ssize_t n = 0; // 写入字节数
    int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字
    if (sock < 0)
    {
        cerr << "socket error" << endl; // 记录错误信息
        return false; // 返回失败
    }
    bool ret = true; // 初始化返回值为真
    
    // 2. 建立连接
    struct sockaddr_in server; // 定义服务器地址结构
    server.sin_family = AF_INET; // 使用 IPv4
    server.sin_port = htons(server_port); // 设置端口号
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); // 将 IP 字符串转换为网络字节序

    socklen_t len; // 用于存储地址长度
    int nn = connect(sock, (struct sockaddr *)&server, sizeof(server)); // 尝试连接服务器
    if (nn < 0)
    {
        cerr << "connect error" << endl; // 记录连接错误信息
        ret = false; // 设置返回值为假
        goto END; // 跳转到结束标签
    }
    
    *cnt = 0; // 初始化重试计数

    // 读取服务器提供的服务列表
    m = read(sock, service_list, sizeof(service_list) - 1); // 从套接字读取服务列表
    if (m > 0)
    {
        service_list[m] = 0; // 添加字符串结束符
        cout << "服务器提供的服务列表:" << service_list << endl; // 输出服务列表
    }

    // 选择服务
    cout << "请选择服务:"; // 提示用户选择服务
    getline(cin, inbuffer); // 读取用户输入的服务名称
    write(sock, inbuffer.c_str(), inbuffer.size()); // 将用户选择写入套接字

    // 进行通信
    cout << "请输入:"; // 提示用户输入消息
    getline(cin, inbuffer); // 读取用户输入
    if (inbuffer == "quit") // 检查用户是否输入 "quit"
        return true; // 返回成功,表示结束通信
        
    n = write(sock, inbuffer.c_str(), inbuffer.size()); // 将用户输入写入套接字
    if (n > 0)
    {
        char buff[1024]; // 缓冲区用于接收服务器响应
        ssize_t m = read(sock, buff, sizeof(buff) - 1); // 从套接字读取响应
        if (m > 0)
        {
            buff[m] = 0; // 添加字符串结束符
            cout << buff << endl; // 输出服务器响应
        }
        else if (m == 0) // 如果返回值为0,表示服务器关闭了连接
        {
            return true; // 返回成功
        }
        else
        {
            ret = false; // 设置返回值为假
            goto END; // 跳转到结束标签
        }
    }
    else
    {
        ret = false; // 设置返回值为假
        goto END; // 跳转到结束标签
    }

END:
    close(sock); // 关闭套接字
    return ret; // 返回结果
}

int main(int argc, char *argv[])
{
    if (argc != 3) // 检查命令行参数数量
    {
        Usage(argv[0]); // 输出使用说明
        return 1; // 返回错误代码
    }

    string server_ip = argv[1]; // 获取服务器 IP 地址
    uint16_t server_port = stoi(argv[2]); // 将端口号从字符串转换为整型
    signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
    int cnt = 1; // 初始化重试计数

    // 尝试访问服务器
    while (cnt <= Retry_count) // 在重试次数范围内循环
    {
        bool result = visitServer(server_ip, server_port, &cnt); // 访问服务器
        if (result) // 如果访问成功
        {
            break; // 退出循环
        }
        else
        {
            sleep(1); // 等待 1 秒
            cout << "正在尝试重连中..." << cnt << endl; // 输出重连信息
            cnt++; // 增加重试计数
        }
    }

    return 0; // 程序正常结束
}

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

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

相关文章

【STM32 ADC】

STM32 ADC功能简介 文章目录 前言一、ADC简介二、逐次逼近型ADC三、STM32的ADC内部框图四、STM32ADC输入通道五、规则组的四种转换模式六、规则组的触发源七、数据对齐八、转换时间九、校准部分十.ADC采样测量电压的程序设计十一、ADC的迟滞比较、滤波设计十二、四通道ADC采用问…

【github小问题】——push后报错error: src refspec master does not match any

温馨提示&#xff1a;这个问题可能有多种问题导致如未commit&#xff0c;本文在此讲述的是我遇到的这一种情况。 一、问题描述 从本地上传文件至github仓库时&#xff0c;add和commit都执行了且成功&#xff0c;但是执行git push -u origin master后出现了&#xff1a;error: …

深入理解JavaScript:两大编程思想和ES6类以及对象概念解析

文章目录 两大编程思想ES6中的类和对象 两大编程思想 面向过程 &#xff08;Procedural-Oriented Programming&#xff0c;POP&#xff09; 定义&#xff1a;面向过程的编程是一种基于过程调用的编程范式&#xff0c;它将程序看作是一系列函数或过程的集合。每个函数负责完成…

【K8S系列】Kubernetes pod节点Unknown 问题及解决方案详解【已解决】

在 Kubernetes 中&#xff0c;Pod 的状态为 Unknown 表示无法获取 Pod 的当前状态。这通常意味着 Kubernetes API 服务器无法与 Pod 所在的节点通信&#xff0c;或者 Kubelet 进程遇到问题。以下将详细介绍 Unknown 状态的原因、解决方案以及如何配置健康检查以提高系统的稳定性…

函数的实参和形参

什么是实参&#xff1f;什么是形参&#xff1f; 其实让我用语言来形容并不好描述&#xff0c;我们看例子&#xff1a; int add(int x , int y)//括号内就是形参 { int zxy; return z; } #include <stdio.h> int main() { int a8; int b9; int vadd(a,b);//括号内放置的参…

django-vue-admin测试环境搭建

django-vue-admin测试环境搭建 引言开发工具入门demo示例踩过的坑数据库字符集创建数据表前端路由 自定义app效果展示 引言 django-vue-admin框架&#xff0c;大幅度降低应用层代码难度,让每一个刚开始学习 django和vue的新手都能快速上手。这将会是你上手学习 djangovue的最佳…

PyQt 入门教程(3)基础知识 | 3.1、使用QtDesigner创建.ui文件

文章目录 一、使用QtDesigner创建.ui文件1、创建.ui文件2、生成.py文件3、使用新生成的.py文件4、编辑新生成的.py文件 一、使用QtDesigner创建.ui文件 1、创建.ui文件 打开PyCharm&#xff0c;使用自定义外部工具QtDesigner创建mydialog.ui文件&#xff0c;如下&#xff1a; …

pandas库——基础

1.概述 Pandas 是一个开源的第三方 Python 库&#xff0c;从 Numpy 和 Matplotlib 的基础上构建而来 Pandas 名字衍生自术语 "panel data"&#xff08;面板数据&#xff09;和 "Python data analysis"&#xff08;Python 数据分析&#xff09; Pandas 已…

Python酷库之旅-第三方库Pandas(166)

目录 一、用法精讲 761、pandas.Interval.closed_right属性 761-1、语法 761-2、参数 761-3、功能 761-4、返回值 761-5、说明 761-6、用法 761-6-1、数据准备 761-6-2、代码示例 761-6-3、结果输出 762、pandas.Interval.is_empty属性 762-1、语法 762-2、参数 …

【Mac 上将 MOV 格式转换为 MP4 格式的简易指南】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【C++】哈希实现unordered_map/set

关于哈希模拟实现unordered_map/set&#xff0c;与红黑树模拟实现map/set的大体思路相似。 【C】红黑树模拟实现map和set-CSDN博客 HashTable的迭代器 operator template<class K,class T,class KeyOfT> struct __HashTableIterator {typedef __HashTableIterator<…

电梯导航 - 点击标题跳转对应区域

需求 点击标题&#xff0c;使用a标签的锚点自动跳到对应区域滚动区域&#xff0c;右边自动切换对应的标题 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"wid…

大话红黑树之(3)进阶解析

红黑树高阶知识讲解 红黑树作为一种自平衡的二叉查找树&#xff08;BST&#xff09;&#xff0c;在大多数语言和库中有着广泛应用。它能够在常规操作&#xff08;查找、插入、删除等&#xff09;中保持 O(log n) 的时间复杂度。这篇文章从红黑树的高级特性、性能优化、旋转机制…

U9的插件开发之BE插件(1)

U9插件可分为&#xff1a;BE插件、BP插件、UI插件&#xff1b; BE(Business Entity) 简单就是指实体&#xff0c;U9的元数据。 我的案例是设置BE默认值&#xff0c;即在单据新增时&#xff0c;设置单据某一个字段的默认值&#xff0c;具体如下&#xff1a; 1.插件开发工具&a…

使用virtualenv导入ssl模块找不到指定的模块

最近在学习tensorflow&#xff0c;由于教程里面使用的是virtualenv&#xff0c;所以就按照教程开始安装了虚拟环境。但是在使用的时候&#xff0c;卡在了import ssl这一步&#xff0c;提示如下错误 >>> import ssl Traceback (most recent call last):File "<…

word删除空白页 | 亲测有效

想要删掉word里面的末尾空白页&#xff0c;但是按了delete之后也没有用 找了很久找到了以下亲测有效的方法 1. 通过鼠标右键在要删除的空白页面处显示段落标记 2. 在字号输入01&#xff0c;按ENTER&#xff08;回车键&#xff09; 3.成功删除了&#xff01;&#xff01; PS…

python excel如何转成json,并且如何解决excel转成json时中文汉字乱码的问题

1.解决excel转成json时中文汉字乱码的问题 真的好久没有打开这个博客也好久没有想起来记录一下问题了&#xff0c;今天将表格测试集转成json格式的时候遇到了汉字都变成了乱码的问题&#xff0c;虽然这不是个大问题&#xff0c;但是编码问题挺烦人的&#xff0c;乱码之后像下图…

018集——c# 实现CAD添加侧栏菜单(WPF控件)(CAD—C#二次开发入门)

本例实现的效果如下&#xff1a; 第一步&#xff1a;添加引用 using UserControl System.Windows.Controls.UserControl; using System.Windows.Forms.Integration;//PaletteSet integration 第二步 <UserControl x:Class"AcTools.UserControl1"xmlns"htt…

Pytorch学习--如何下载及使用Pytorch中自带数据集,如何把数据集和transforms联合在一起使用

一、标准数据集使用 pytorch官网–标准数据集 这里以CIFAR10数据集为例&#xff1a;CIFAR10 下载数据集 代码&#xff1a; import torchvision train_datatorchvision.datasets.CIFAR10(root"datasets",trainTrue,downloadTrue) test_datatorchvision.datasets.…

运维加薪之Ansible(DevOps Salary Increase with Ansible。‌)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…