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

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; // 程序正常结束
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rust与Javascript的使用对比

一、常量 RustJavascriptletconst 二、变量 RustJavascriptlet mutlet / var 三、常用打印 RustJavascriptprintln!(“换行”);console.log(‘hello’);print!(“不换行”);console.info(‘信息’);-console.error(‘错误’);-console.warn(‘警告’); 四、定义字符串 R…

开放式耳机哪个品牌音质好?高评分爆款开放式耳机推荐!

一直活跃在蓝牙耳机圈子里的我&#xff0c;对各种类型的耳机多少都有自己的看法&#xff0c;完全可以说是个耳机狂热者。近几年&#xff0c;开放式蓝牙耳机愈发火爆。开放式耳机不是任何品牌都能轻松做好的产品&#xff0c;特别是音质&#xff0c;它涵盖了核心单元技术等诸多方…

负载均衡服务器攻击怎么解决最有效?

负载均衡服务器攻击怎么解决最有效&#xff1f;常见的有效解决方法包括&#xff1a;使用SYNCookie机制、限制ICMP包速率、基于源IP的连接速率限制、检测并丢弃异常IP包、配置访问控制列表&#xff08;ACL&#xff09;、设置虚拟服务器/服务器连接数量限制、设置HTTP并发请求限制…

ABAQUS应用11——支座弹簧

文章目录 0、背景1、ABAQUS中几类弹簧的简介2、SPRING1的性质初探 0、背景 1、ABAQUS中几类弹簧的简介 先说参考来源&#xff0c;ABAQUS2016的帮助文档里第4卷&#xff0c;32.1.1节&#xff0c;有三种弹簧&#xff08;SPRING1 、SPRING2 以及SPRINGA&#xff09;。 三种弹簧里…

C for Graphic:视差渲染(一)

记录一下最近优化场景的做法&#xff1a;视差渲染 原理&#xff1a;通过视口坐标的变化&#xff0c;观察不同采样画面的功能&#xff0c;画面的载体为低模平面 我早期工作&#xff0c;在小作坊全栈的时候&#xff0c;做过一段时间web开发&#xff0c;做了一个古董藏…

【传知代码】机器学习在情绪预测中的应用(论文复现)

在科技迅猛发展的今天&#xff0c;我们不仅在追求更强大的计算能力和更高的精度&#xff0c;还希望我们的机器能够理解和回应我们复杂的情感世界。设想一下&#xff0c;当你面对挫折时&#xff0c;设备不仅能识别你的情绪&#xff0c;还能以一种富有同情心和洞察力的方式作出反…

开放式耳机哪个牌子好?开放式蓝牙耳机排行榜分享

​耳机已经成为我们日常生活中的必需品&#xff0c;但长时间佩戴传统入耳式耳机可能会导致耳朵不适&#xff0c;甚至影响健康。为了应对这一挑战&#xff0c;开放式耳机应运而生。这类耳机不侵入耳道&#xff0c;有效减轻了耳朵的压力&#xff0c;同时减少了感染风险&#xff0…

fmql之Linux中I2C总线框架

正点原子第44章 I2C zynq I2C pcf8563芯片 我们用的是ds3231. Linux I2C总线框架 I2C总线驱动 这部分内容是半导体厂商编写的。 I2C总线设备 zynq I2C适配器驱动 I2C设备驱动编写 使用设备树 代码编写 设备树修改 设备驱动编写 因为用的是ds3231&#xff0c;所以先找…

使用 PyTorch 构建 LSTM 股票价格预测模型

目录 引言准备工作1. 训练模型&#xff08;train.py&#xff09;2. 模型定义&#xff08;model.py&#xff09;3. 测试模型和可视化&#xff08;test.py&#xff09;使用说明模型调整结论 引言 在金融领域&#xff0c;股票价格预测是一个重要且具有挑战性的任务。随着深度学习…

1024软件推荐-rubick

开源的插件化桌面端效率工具箱。插件是基于 npm 进行安装和卸载&#xff0c;非常轻便。插件数据支持 webdav 多端同步&#xff0c;非常安全。支持内网部署&#xff0c;可二次定制化开发&#xff0c;非常灵活。 前言 rubick 之前的插件管理&#xff0c;依托于云服务器存储&…

滴水逆向三期笔记与作业——02C语言——13 指针(3)(4)

滴水逆向三期笔记与作业——02C语言——13 指针3、4 一、模拟实现CE的数据搜索功能 OneNote迁移 一、模拟实现CE的数据搜索功能 //其中有0xAA&#xff0c;超过有符号char范围&#xff0c;在vscode中会报错&#xff0c;所以使用unsigned char unsigned char data[100] {0x00,0…

一起搭WPF架构之完结总结篇

一起搭WPF架构之完结总结篇 前言设计总结设计介绍页面一页面二页面三 结束 前言 整体基于WPF架构&#xff0c;根据自己的需求简单设计与实现了衣橱的数据统计、增加与读取数据、并展示数据的小软件。我知道自己在设计方面还有很多不足&#xff0c;暂时先做到这里了&#xff0c…

gbase8s权限管理

一 权限分类 分片级权限&#xff08;分片表&#xff09; 表引用 类型级权限 例程级权限 语言级权限 序列级权限 等... 其中常用的为 数据库级权限&#xff0c;表级权限&#xff0c;序列级权限以及例程级权限 二 权限控制 当创建一个用户时&#xff0c;该用户没有任何权…

为了数清还有几天到周末,我用python绘制了日历

日历的秘密 昨天&#xff0c;在看小侄子写作业的时候&#xff0c;发现了一个秘密&#xff1a;他在“演算纸”&#xff08;计算数学题用的草纸&#xff09;上画了非常多的日历。对此我感到了非常的困惑&#xff0c;“这是做什么的&#xff1f;” 后来&#xff0c;经过了我不懈…

机器学习面试笔试知识点-线性回归、逻辑回归(Logistics Regression)和支持向量机(SVM)

机器学习面试笔试知识点-线性回归、逻辑回归Logistics Regression和支持向量机SVM 一、线性回归1.线性回归的假设函数2.线性回归的损失函数(Loss Function)两者区别3.简述岭回归与Lasso回归以及使用场景4.什么场景下用L1、L2正则化5.什么是ElasticNet回归6.ElasticNet回归的使…

【设计模式】MyBatis 与经典设计模式:从ORM到设计的智慧

作者&#xff1a;后端小肥肠 &#x1f347; 我写过的文章中的相关代码放到了gitee&#xff0c;地址&#xff1a;xfc-fdw-cloud: 公共解决方案 &#x1f34a; 有疑问可私信或评论区联系我。 &#x1f951; 创作不易未经允许严禁转载。 姊妹篇&#xff1a; 【设计模式】揭秘Spri…

计算机网络:数据链路层 —— 以太网(Ethernet)

文章目录 局域网局域网的主要特征 以太网以太网的发展100BASE-T 以太网物理层标准 吉比特以太网载波延伸物理层标准 10吉比特以太网汇聚层交换机物理层标准 40/100吉比特以太网传输媒体 局域网 局域网&#xff08;Local Area Network, LAN&#xff09;是一种计算机网络&#x…

GitLab-删除仓库分支(删除远程分支)

进入对应仓库选择对应的分支进行删除操作。

为什么学习使用数控加工中心吗?

现代制造业现代制造业对高精度、高效率的加工需求日益增长&#xff0c;数控加工中心作为核心设备&#xff0c;其操作和维护技能成为企业招聘的重要考量。企业需要能够熟练操作数控加工中心&#xff0c;并具备解决复杂加工问题的能力的人才。 学校通过系学习和实践&#xff0c;学…

不用编程,快速实现多台西门子PLC跟三菱PLC之间数据通讯

PLC通讯智能网关IGT-DSER模块支持汇川、西门子、三菱、欧姆龙、罗克韦尔AB、GE等各种品牌的PLC之间通讯&#xff0c;同时也支持PLC与Modbus协议的变频器、智能仪表等设备通讯。网关有多个网口、串口&#xff0c;也可选择WIFI无线通讯。PLC内无需编程开发&#xff0c;在智能网关…