Tcp_Sever(线程池版本的 TCP 服务器)
- 前言
- [1. 功能介绍及展示](#1. 功能介绍及展示)
-
- [1.1 服务端连接](#1.1 服务端连接)
- [1.2 客户端连接(可多个用户同时在线连接服务端)](#1.2 客户端连接(可多个用户同时在线连接服务端))
- [1.3 功能服务](#1.3 功能服务)
-
- [1.3.1 defaultService(默认服务)](#1.3.1 defaultService(默认服务))
- [1.3.2 transform(大小写转换)](#1.3.2 transform(大小写转换))
- [1.3.3 ping(ping服务)](#1.3.3 ping(ping服务))
- [1.3.4 translate(翻译)](#1.3.4 translate(翻译))
- [1.4 服务器重连功能](#1.4 服务器重连功能)
- [2. 代码实现](#2. 代码实现)
-
- [2.1 总体代码设计](#2.1 总体代码设计)
-
- [2.1.1 .cc文件](#2.1.1 .cc文件)
- [2.1.2 .hpp文件](#2.1.2 .hpp文件)
- [2.1.3 其他文件](#2.1.3 其他文件)
- 2.2具体实现(代码都有注释)
-
- [2.2.1 Log.hpp](#2.2.1 Log.hpp)
- [2.2.2 nocopy.hpp](#2.2.2 nocopy.hpp)
- [2.2.3 LockGuard.hpp](#2.2.3 LockGuard.hpp)
- [2.2.4 Comm.hpp](#2.2.4 Comm.hpp)
- [2.2.5 Thread.hpp](#2.2.5 Thread.hpp)
- [2.2.6 ThreadPool.hpp](#2.2.6 ThreadPool.hpp)
- [2.2.7 InetAddr.hpp](#2.2.7 InetAddr.hpp)
- [2.2.8 Translate.hpp](#2.2.8 Translate.hpp)
- [2.2.9 Tcp_Server.hpp](#2.2.9 Tcp_Server.hpp)
- [2.2.10 Makefile](#2.2.10 Makefile)
- [2.2.11 Dict.txt](#2.2.11 Dict.txt)
- [2.2.12 Main.cc](#2.2.12 Main.cc)
- [2.2.13 Tcp_Client.cc](#2.2.13 Tcp_Client.cc)
前言
已经有半年没有更新博客了,在这期间,时而沉淀,时而因为就业感到迷茫,到现在,忽然看开了一点,不管未来咋样,至少不要让自己后悔,人生需要passion!干就完了!!!
源码地址 :tcp_server
注:该网络服务只能在有公网ip的机器或者云服务器之间进行
虚拟机上只能进行本地连接,不能连接其他虚拟机
1. 功能介绍及展示
1.1 服务端连接
cpp
./tcp_server 8888
执行结果:
1.2 客户端连接(可多个用户同时在线连接服务端)
连接服务器要知道服务器的ip地址
我们执行本地测试时,可以用ifconfig 指令查看本地ip地址
连接
cpp
./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
cpp
#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
cpp
#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
cpp
#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
cpp
#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
cpp
#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
cpp
#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
cpp
#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
cpp
#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
cpp
#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文件的代码注释看起·来比较乱,所以我分两部分放出来
无注释代码
cpp
.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
带注释代码
cpp
.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
词典单词数据,可按格式自行添加,数量不限
cpp
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
cpp
#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
cpp
#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; // 程序正常结束
}