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()); // 获取当前进程IDsnprintf(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; // 线程IDstd::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:// 构造函数,接受字典路径,默认值为 mydictTranslate(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; // 使用 IPv4local.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_clienttcp_server:Main.ccg++ -o $@ $^ -lpthread -std=c++14tcp_client:Tcp_Client.ccg++ -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.ccg++ -o $@ $^ -lpthread -std=c++14 # 使用 g++ 编译 Main.cc,生成可执行文件 tcp_server# $@ 表示目标名称(tcp_server),$^ 表示所有依赖文件(Main.cc)# 目标 tcp_client 的构建规则
tcp_client: Tcp_Client.cc # 指定依赖文件 Tcp_Client.ccg++ -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 [hɔ: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; // 使用 IPv4server.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; // 程序正常结束
}
相关文章:

Tcp_Sever(线程池版本的 TCP 服务器)
Tcp_Sever(线程池版本的 TCP 服务器) 前言1. 功能介绍及展示1.1 服务端连接1.2 客户端连接(可多个用户同时在线连接服务端)1.3 功能服务1.3.1 defaultService(默认服务)1.3.2 transform(大小写转…...

第十一章 Vue生命周期及生命周期的四个阶段
目录 一、引言 1.1. Vue生命周期的具体阶段 1.2. 每个阶段的具体作用和常用场景 1.3. 生命周期钩子函数 二、代码示例 三、运行效果 一、引言 Vue生命周期是指Vue组件实例从创建到销毁的整个过程。在这个过程中,组件经历了一系列的阶段,每个阶段…...

展厅展会客流显示屏的客流统计功能如何实现
随着科技的发展,展厅和展会的管理越来越智能化。客流显示屏作为一种高效的管理工具,能够实时显示参观人数,帮助主办方更好地了解客流情况,优化资源配置。本文将详细介绍展厅展会客流显示屏的客流统计功能如何实现,分为…...

golang正则表达式的使用及举例
正则表达式很强大,在一些场合如抓包,爬虫等方面很有用。在 Go语言中,正则表达式通过标准库 regexp 提供支持。使用正则表达式可以进行字符串匹配、替换和分割等操作。 以下是正则表达式的基本使用方法及示例: 1. 导入 regexp 包 …...

Flutter杂学: iOS 上启用自动填充和关联域
下面是详细的配置和代码,以确保在 iOS 上启用自动填充和关联域(Associated Domains)功能。 配置步骤 1. 在 Apple Developer 控制台中启用 Associated Domains 登录 Apple Developer。导航至您的 App ID 设置页面。找到您要配置的 App ID&…...

接口自动化-框架搭建(Python+request+pytest+allure)
使用代码如何开展接口自动化测试。 一 选择自动化测试用例 业务流程优先,单接口靠后,功能稳定优先,变更频繁不选。 二 搭建自动化测试环境 (1)安装python编译器3.7版本以上--自行安装 (2)安…...

[论文阅读]Constrained Decision Transformer for Offline Safe Reinforcement Learning
Constrained Decision Transformer for Offline Safe Reinforcement Learning Proceedings of the 40th International Conference on Machine Learning (ICML), July 23-29, 2023 https://arxiv.org/abs/2302.07351 泛读只需要了解其核心思想即可。 安全强化学习(Safe Rei…...

工具_Nginx
文章目录 location语法介绍跨域配置https配置http重定向到https配置反向代理配置负载均衡配置upstream配置负载均衡算法(1)rr轮询(默认)(2)wrr加权轮询(weight)(3&#x…...

web开发Model1
WEB开发模式–Model 1 Model1是指基于JSPJavaBean的开发模式,JSP负责web的相关部分,包括数据的展示,请求逻辑的控制等,JavaBean负责业务的逻辑部分,包括数据的存取,业务的实现。 这是我写的一个小项目&…...

ImportError: cannot import name ‘Sequential‘ from ‘keras.models‘
报错信息 ImportError: cannot import name Sequential from keras.models错误代码示例 import tensorflow as tf from keras.models import Sequential # 报错行model Sequential()错误分析 这个错误通常发生在 TensorFlow 和 Keras 的版本不兼容时。TensorFlow 2.x 版本…...

python实战(二)——房屋价格回归建模
一、任务背景 本章将使用一个经典的Kaggle数据集——House Prices - Advanced Regression Techniques进行回归建模的讲解。这是一个房价数据集,与我们熟知的波士顿房价数据集类似,但是特征数量要更多,数据也要更为复杂一些。下面,…...

UHF机械高频头的知识和待学习的疑问
电路图如上所示: 实物开盖清晰图如下: 待学习和弄懂的知识: 这是一个四腔的短路线谐振。分别是输入调谐,放大调谐,变频调谐和本振 第一个原理图输入为75欧(应该是面向有同轴线的天线了)如下图…...

深入理解 SQL 中的 WITH AS 语法
在日常数据库操作中,SQL 语句的复杂性往往会影响到查询的可读性和维护性。为了解决这个问题,Oracle 提供了 WITH AS 语法,这一功能可以极大地简化复杂查询,提升代码的清晰度。本文将详细介绍 WITH AS 的基本用法、优势以及一些实际…...

同三维T80005JEHA-4K60 4K60超高清HDMI/AV解码器
1路HDMI1路CVBS1路3.5音频输出,HDMI支持4K60,支持1路4K60解码,1路高清转码 产品简介: T80005JEHA-4K60是一款4K60超高清解码器,支持1路HDMI/CVBS解码输出,HDMI支持4K60,适用于各种音视频解决方…...

深信服秋季新品重磅发布:安全GPT4.0数据安全大模型与分布式存储EDS新版本520,助力数字化更简单、更安全
10月23日,深信服举办2024秋季新品发布会。发布会上,深信服正式推出了最新的创新成果:实现动静态数据分类分级和数据风险自动研判分析的安全GPT4.0、具备卓越可靠性和AI勒索防护能力的分布式存储EDS新版本520。通过这些新品和能力,…...

Flutter图片控件(七)
1、加载图片 import package:flutter/material.dart;void main() {runApp(const MaterialApp(home: MyHomePage(),)); }class MyHomePage extends StatelessWidget {const MyHomePage({super.key});overrideWidget build(BuildContext context) {return Scaffold(appBar: AppB…...

JavaEE初阶---文件IO总结
文章目录 1.文件初识2.java针对于文件的操作2.1文件系统的操作---file类2.2文件内容的操作---流对象的分类2.4字符流的操作》文本文件2.4.1异常的说明2.4.2第一种文件内容的读取方式2.4.3第二种读取方式2.4.4close的方法的介绍2.4.5close的使用优化操作2.4.6内容的写入 2.3字节…...

10.28Python_pandas_csv
三、读取CSV文件 CSV(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本); CSV 是一…...

数据处理与可视化:pandas 和 matplotlib 初体验(9/10)
数据处理与可视化:pandas 和 matplotlib 初体验(9/10) 介绍 在如今的数据驱动时代,掌握数据处理与可视化是每个开发者和数据科学家不可或缺的技能。Python 拥有强大的数据处理库 pandas 和数据可视化库 matplotlib,它…...

鸿蒙学习总结
鸿蒙(HarmonyOS),做为国产自主研发设计的第一个操作系统,从开放测试以来一直备受关注。其纯血鸿蒙版(HarmonyOS NEXT)也于进日发布。过去的一段时间里,我站在一个移动开发者的角度对HarmonyOS进…...

如何修改文件创建时间?六个超简单修改方法介绍
怎么修改文件创建时间?在信息安全与隐私保护的领域里,每一个细节都可能成为泄露敏感信息的突破口。文件的创建时间,这个看似微不足道的数据点,实则可能蕴含着重要的时间线索,对于不希望被外界窥探其内容或来源的个人及…...

【MySQL 保姆级教学】内置函数(9)
内置函数 1. 日期函数1.1 日期函数的种类1.2 示例1.3 日期的转换 2. 字符串函数2.1 种类2.2 示例 3. 数学函数3.1 种类3.2 向上取整和向下取整3.3 示例 4. 其他函数4.1 查询当前用户/数据库4.2 ifnull(val1,val2)4.3 md5()函数4.4 password()函数 1. 日期函数 1.1 日期函数的种…...

华为大咖说丨如何通过反馈机制来不断优化大模型应用?
本文分享自时习知 作者:袁泉(华为AI数据工程专家)全文约3015字,阅读约需8分钟 大模型应用正式投入使用后,存在一个较为普遍的情况:在利用“大模型提升业务运营效率”的过程中,业务部门和IT团队…...

上海亚商投顾:沪指缩量震荡 风电、传媒股集体走强
上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 市场全天缩量震荡,三大指数集体收涨,北证50则跌超7%,超80只北交所个股跌逾…...

三磺酸-Cy3.5-羧酸在水相环境中表现良好,能够提高成像的清晰度和准确性
一、基本信息 中文名称:三磺酸-Cy3.5-羧酸,水溶性Cy3.5 羧基 英文名称:trisulfo-Cy3.5-carboxylic acid,trisulfo-Cy3.5-COOH,trisulfo-Cyanine3.5-COOH 分子式:C41H44N2NaO11S3- 分子量:85…...

国标GB28181视频平台EasyGBS国标GB28181软件实现无需插件的视频监控对讲和网页直播
在当今社会,视频监控已经成为公共安全、企业管理、智能城市建设等领域不可或缺的一部分。然而,由于不同厂家和平台之间的兼容性问题,视频监控系统的联网和整合面临巨大挑战。为了解决这个问题,国家制定了《公共安全视频监控联网系…...

mac nwjs程序签名公证(其他mac程序也一样适用)
为什么需要公证 mac os14.5之后的系统,如果不对应用进行公证,安装,打开,权限使用上都会存在问题,而且有些问题你强制开启(sudo spctl --master-disable)使用后可能会有另外的问题, …...

网络应用技术 实验一:路由器实现不同网络间通信(华为ensp)
目录 一、实验简介 二、实验目的 三、实验需求 四、实验拓扑 五、实验任务及要求 1、任务 1:完成网络部署 2、任务 2:设计全网IP 地址 3、任务 3:实现全网主机互通 六、实验步骤 1、在ensp中部署网络 2、配置各主机 IP地址、子网掩…...

使用 Qt GRPC 构建高效的 Trojan-Go 客户端:详细指南
使用 Qt GRPC 构建高效的 Trojan-Go 客户端:详细指南 初识 Qt 和 gRPC 什么是 Qt?什么是 gRPC? 项目结构概述创建 proto 文件定义 API 下载 api.proto 文件解析 proto 文件 1. package 与 option 语句2. 消息类型定义 TrafficSpeedUserUserSt…...

【mysql进阶】5-事务和锁
mysql 事务基础 1 什么是事务 事务是把⼀组SQL语句打包成为⼀个整体,在这组SQL的执⾏过程中,要么全部成功,要么全部失败,这组SQL语句可以是⼀条也可以是多条。再来看⼀下转账的例⼦,如图: 在这个例⼦中&a…...