计算机网络socket编程(5)_TCP网络编程实现echo_server
个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创计算机网络socket编程(5)_TCP网络编程实现echo_server
收录于专栏【计算机网络】
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
功能介绍
InetAddr.hpp
LockGuard.hpp
Log.hpp
Thread.hpp
ThreadPool.hpp
TcpServer.hpp
TcpServerMain.cc
TcpClientMain.cc
效果展示
功能介绍
和上回 UDP 网络编程一样, 实现简单的 echo_server, 不过, 这里我们 TCP 网络编程使用了 多线程, 不过大体都差不多~
还有就是网络编程代码真的是又多又杂, 有的时候我自己都烦, 没办法网络部分就是这样的, 我最近会尽快更完这个 socket 编程, 提早进入概念部分, 一直编程感觉少了什么~ 还得跟概念结合起来看, 感兴趣的宝子们不要忘记了点赞关注哦! 我现在在网络部分真的待不了一点, 希望我能尽快挣脱网络, 更新数据库 MySQL 的东西吧!
InetAddr.hpp
这个类封装了 sockaddr_in 结构体,用于简化对 IP 地址和端口的处理。其核心功能是将网络字节序的 sockaddr_in 地址转换为易于操作的主机字节序的 IP 地址字符串和端口号,并提供相关的成员函数来获取这些信息。
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);// _ip = inet_ntoa(addr.sin_addr);char ip_buf[32];// inet_p to n// p: process// n: net// inet_pton(int af, const char *src, void *dst);// inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}InetAddr(){}bool operator == (const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};
私有成员变量
_ip:存储 IP 地址的字符串(如 "192.168.0.1")。
_port:存储端口号。
_addr:存储一个 sockaddr_in 结构体,用于保存 IP 地址和端口。
私有成员函数 ToHost
ToHost 函数的作用是将一个 sockaddr_in 地址结构转换为 InetAddr 类的成员 _ip 和 _port。
ntohs(addr.sin_port):将网络字节顺序的端口号(从 sockaddr_in 中获取)转换为主机字节顺序。网络字节顺序是大端模式,而主机字节顺序通常取决于平台。
inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf)):将 sockaddr_in 中的 IP 地址(以二进制形式存储)转换为点分十进制字符串表示(如 "192.168.0.1")。
这里 inet_ntop 和 ntohs 用于处理网络字节序和主机字节序的转换,确保 IP 地址和端口在不同环境下的正确性。
构造函数 InetAddr(const struct sockaddr_in &addr)
该构造函数接受一个 sockaddr_in 类型的参数 addr,并调用 ToHost 方法将其转换为 InetAddr 类内部的 _ip 和 _port。
_addr(addr):将 sockaddr_in 结构体存储在 _addr 中。
默认构造函数 InetAddr()
默认构造函数没有做任何事情。它用于创建一个空的 InetAddr 对象
运算符重载 ==
重载了 == 运算符,用于比较两个 InetAddr 对象是否相等。它通过比较 ip 和 port 字段来判断是否相同。
成员函数 Ip
返回当前 InetAddr 对象的 IP 地址。
成员函数 Port
返回当前 InetAddr 对象的端口号。
成员函数 Addr
返回存储的 sockaddr_in 结构体。sockaddr_in 包含了完整的 IP 地址和端口信息。
成员函数 AddrStr
返回一个格式化的字符串,表示 IP 地址和端口,格式为 "ip:port"(例如 "192.168.0.1:8080")。
LockGuard.hpp
#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};
构造函数 LockGuard(pthread_mutex_t *mutex)
构造函数接收一个 pthread_mutex_t* 类型的指针作为参数,并在构造时通过 pthread_mutex_lock 来锁定该互斥量。
mutex 参数是指向一个 pthread_mutex_t 类型的指针,这个互斥量将用来保护临界区。
_mutex(mutex) 是初始化成员变量 _mutex 的成员初始化列表,它将构造函数的参数 mutex 的值赋给 _mutex 成员变量。
pthread_mutex_lock(_mutex) 调用会尝试锁定互斥量 _mutex。如果互斥量已经被其他线程锁定,当前线程会被阻塞,直到该互斥量变为可用。
析构函数 ~LockGuard()
析构函数负责在 LockGuard 对象生命周期结束时自动解锁互斥量。
当 LockGuard 对象的作用域结束时,析构函数会自动被调用。
pthread_mutex_unlock(_mutex) 会释放锁,即解锁互斥量。这样可以确保即使在发生异常或提前返回的情况下,互斥量也能被正确解锁,从而避免死锁。
成员变量 _mutex
_mutex 是一个指向 pthread_mutex_t 类型的指针,它保存了传递给构造函数的互斥量地址。这个指针将用于在构造和析构中对互斥量进行锁定和解锁操作。
关键特点:
自动锁定:当 LockGuard 对象被创建时,构造函数自动锁定互斥量。
自动解锁:当 LockGuard 对象超出作用域时,析构函数自动解锁互斥量。
简化代码:使用 LockGuard 类可以避免手动调用 pthread_mutex_lock 和 pthread_mutex_unlock,并且保证解锁操作一定会发生。
Log.hpp
#pragma once#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"namespace log_ns
{enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};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 GetCurrTime(){time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1
#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage &lg){// 加过滤逻辑 --- TODOLockGuard lockguard(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info = log_info;// 打印出来日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level, Format, ...) \do \{ \lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \} while (0)
#define EnableScreen() \do \{ \lg.Enable(SCREEN_TYPE); \} while (0)
#define EnableFILE() \do \{ \lg.Enable(FILE_TYPE); \} while (0)
};
日志系统, 我们的老演员了, 这里就不再多介绍了~~
Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>namespace ThreadMoudle
{// 线程要执行的方法,后面我们随时调整// typedef void (*func_t)(ThreadData *td); // 函数指针类型// typedef std::function<void()> func_t;using func_t = std::function<void(const std::string&)>;class Thread{public:void Excute(){_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func):_name(name), _func(func){}static void *ThreadRoutine(void *args) // 新线程都会执行该方法!{Thread *self = static_cast<Thread*>(args); // 获得了当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if(n != 0) return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if(_isrunning){::pthread_cancel(_tid);_isrunning = false;}}void Join(){::pthread_join(_tid, nullptr);}std::string Name(){return _name;}~Thread(){}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程要执行的回调函数};
} // namespace ThreadModle
线程函数类型 func_t
using func_t = std::function<void(const std::string&)>;
这里使用了 std::function 来定义线程要执行的函数类型。func_t 是一个函数对象类型,它表示接受一个 std::string 类型参数并返回 void 的函数。使用 std::function 的好处是,它可以适配普通函数、lambda 表达式以及成员函数等,使得线程任务的定义更加灵活。
Thread 类
Thread 类是该代码的核心,封装了 POSIX 线程的管理操作,包括线程的创建、执行、停止、等待和状态查询。
成员变量
_name: 线程的名称,用于标识线程。
_tid: 线程标识符 (pthread_t 类型),用于标识线程。
_isrunning: 布尔变量,表示线程是否正在运行。
_func: 线程执行的任务(即回调函数),使用 func_t 类型存储。
构造函数
Thread 类的构造函数接收线程名称 (name) 和线程任务 (func) 作为参数,并初始化相关成员变量。_isrunning 被初始化为 false,表示线程在创建时默认处于非运行状态。
线程执行方法 Excute
Excute 方法是线程执行的主体部分,它首先将 _isrunning 设置为 true,然后执行通过构造函数传入的任务函数 _func。执行完后,将 _isrunning 设置为 false,表示线程已经结束执行。
线程例程 ThreadRoutine
ThreadRoutine 是一个静态方法,它会作为线程的入口函数。当创建线程时,系统会调用这个函数。
在 ThreadRoutine 中,首先通过 static_cast 将传入的 void* 类型的参数转换为 Thread* 类型,这样我们就可以访问到线程的成员变量和方法。
然后调用 self->Excute(),即执行线程实际的工作。
启动线程 Start
Start 方法通过 pthread_create 创建一个新的线程。pthread_create 会接受线程标识符 _tid、线程属性(这里是 nullptr,即默认属性)、线程入口函数(这里是 ThreadRoutine)以及线程传递的参数(这里是 this,即当前对象)。
如果线程创建成功,返回 true;否则返回 false。
查询线程状态 Status
Status 方法返回当前线程的状态。如果线程正在执行(_isrunning == true),返回 "running",否则返回 "sleep"。
停止线程 Stop
Stop 方法用于停止正在运行的线程。它调用 pthread_cancel 来请求终止指定线程 _tid。然后将 _isrunning 设置为 false,表示线程已停止。
等待线程完成 Join
Join 方法用于等待线程执行完毕。它通过 pthread_join 阻塞当前线程,直到指定线程 _tid 执行完毕。
获取线程名称 Name
Name 方法返回线程的名称。
ThreadPool.hpp
这段代码实现了一个 线程池 模式,提供了多线程处理任务的能力,能够管理多个线程执行任务,并且通过线程池单例模式(Singleton)来确保只会创建一个线程池实例。
#pragma once#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <functional>
#include "Thread.hpp"
#include "Log.hpp"
#include "LockGuard.hpp"using namespace ThreadMoudle;
using namespace log_ns;static const int gdefaultnum = 10;void test()
{while (true){std::cout << "hello world" << std::endl;sleep(1);}
}template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond, &_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string &name) // this{while (true){// 取任务LockQueue();while (IsEmpty() && _isrunning){_sleep_thread_num++;LOG(INFO, "%s thread sleep begin!\n", name.c_str());Sleep();LOG(INFO, "%s thread wakeup!\n", name.c_str());_sleep_thread_num--;}// 判定一种情况if (IsEmpty() && !_isrunning){UnlockQueue();LOG(INFO, "%s thread quit\n", name.c_str());break;}// 有任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();// 处理任务t(); // 处理任务,此处不用/不能在临界区中处理// std::cout << name << ": " << t.result() << std::endl;// LOG(DEBUG, "hander task done, task is : %s\n", t.result().c_str());}}void Init(){func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);for (int i = 0; i < _thread_num; i++){std::string threadname = "thread-" + std::to_string(i + 1);_threads.emplace_back(threadname, func);LOG(DEBUG, "construct thread %s done, init success\n", threadname.c_str());}}void Start(){_isrunning = true;for (auto &thread : _threads){LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str());thread.Start();}}ThreadPool(int thread_num = gdefaultnum): _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;void operator=(const ThreadPool<T> &) = delete;public:void Stop(){LockQueue();_isrunning = false;WakeupAll();UnlockQueue();LOG(INFO, "Thread Pool Stop Success!\n");}// 如果是多线程获取单例呢?static ThreadPool<T> *GetInstance(){if (_tp == nullptr){LockGuard lockguard(&_sig_mutex);if (_tp == nullptr){LOG(INFO, "create threadpool\n");// thread-1 thread-2 thread-3...._tp = new ThreadPool<T>();_tp->Init();_tp->Start();}else{LOG(INFO, "get threadpool\n");}}return _tp;}void Equeue(const T &in){LockQueue();if (_isrunning){_task_queue.push(in);if (_sleep_thread_num > 0)Wakeup();}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:int _thread_num;std::vector<Thread> _threads;std::queue<T> _task_queue;bool _isrunning;int _sleep_thread_num;pthread_mutex_t _mutex;pthread_cond_t _cond;// 单例模式// volatile static ThreadPool<T> *_tp;static ThreadPool<T> *_tp;static pthread_mutex_t _sig_mutex;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
类定义:ThreadPool<T>
ThreadPool 类是一个模板类,能够处理类型为 T 的任务。ThreadPool 负责管理一组线程,这些线程会从任务队列中取出任务并执行。
成员变量:
线程池大小:_thread_num 表示线程池中线程的数量。
线程队列:_threads 用来存储所有创建的线程。
任务队列:_task_queue 是一个 std::queue,存储待处理的任务。
运行状态:_isrunning 标志线程池是否正在运行。
空闲线程数:_sleep_thread_num 表示当前空闲的线程数量。
同步机制:
_mutex:用于保护任务队列的互斥锁。
_cond:用于线程同步的条件变量,确保线程池在没有任务时可以进入等待状态。
单例模式:
_tp:一个静态指针,用于存储线程池的唯一实例。
_sig_mutex:一个静态互斥锁,用于控制对 _tp 的访问,确保线程池的单例实现是线程安全的。
成员函数:
(1) 同步操作:
LockQueue() 和 UnlockQueue():这些是保护任务队列的互斥锁方法,确保在访问任务队列时线程是同步的,避免并发冲突。
Wakeup() 和 WakeupAll():分别是唤醒一个或所有线程的函数。当有任务加入时,如果某些线程在等待任务,这些函数可以用来通知线程继续工作。
Sleep():如果任务队列为空,线程会调用这个函数进入等待状态,直到有新的任务被加入到队列中。
IsEmpty():检查任务队列是否为空。
(2) 任务处理:
HandlerTask():这是线程执行的主要任务。每个线程会不断地从任务队列中获取任务并执行,直到线程池被停止。
线程首先会尝试从任务队列中取出任务。
如果队列为空且线程池仍然在运行,线程将会休眠,直到有新任务到来。
如果线程池已停止并且队列为空,线程将退出。
如果队列非空,线程会执行任务。
(3) 初始化和启动:
Init():为线程池中的每个线程创建一个 Thread 对象,并绑定任务处理函数 HandlerTask(),然后将线程添加到 _threads 向量中。
Start():启动所有线程。
(4) 停止:
Stop():停止线程池,首先设置 _isrunning = false,然后唤醒所有处于等待状态的线程。
(5) 单例实现:
GetInstance():这是一个线程安全的单例实现,使用双重检查锁定(Double-Checked Locking)来确保线程池实例 _tp 只会被创建一次。_sig_mutex 用于同步对 _tp 的访问。
(6) 任务队列:
Equeue():将任务 in 添加到任务队列中。任务加入后,如果有空闲线程,某些线程会被唤醒来处理这些任务。
(7) 析构函数:
pthread_mutex_destroy(&_mutex) 和 pthread_cond_destroy(&_cond) 用于销毁互斥锁和条件变量,释放相关资源。
线程池的工作流程:
初始化和启动线程池:
通过 ThreadPool<T>::GetInstance() 获取线程池的唯一实例(如果还没有创建)。
调用 ThreadPool::Start() 启动线程池中的所有线程,每个线程执行 HandlerTask() 函数。
任务处理:
任务通过 ThreadPool::Equeue() 被加入到任务队列 _task_queue 中。
线程在 HandlerTask() 中从队列中取任务并执行。
线程阻塞与唤醒:
如果队列为空,线程会调用 Sleep() 进入等待状态,直到有新任务被添加。
当任务被加入队列时,如果有空闲线程,它们会被唤醒执行任务。
停止线程池:
调用 ThreadPool::Stop() 停止线程池,设置 _isrunning = false,并唤醒所有等待中的线程。每个线程在执行完当前任务后退出。
单例模式的线程安全性分析:
ThreadPool<T>::GetInstance() 中使用了双重检查锁定(Double-Checked Locking)来实现线程安全的单例模式:
第一次检查 _tp == nullptr 是为了减少锁的竞争。
第二次检查 _tp == nullptr 是为了确保在锁定后 _tp 仍然没有被创建(避免其他线程已经创建了 ThreadPool 实例)。
LockGuard 用于确保在操作 _tp 时,访问是线程安全的。
TcpServer.hpp
这个 TcpServer 类是一个简单的 TCP 服务器实现,它能够接受客户端的连接,并处理客户端发送的消息。它使用线程池来处理每个客户端的连接,避免了多进程和单线程模型的缺点。
#pragma once
#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"using namespace log_ns;enum
{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERR
};const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;using task_t = std::function<void()>;class TcpServer
{
public:TcpServer(uint16_t port = gport): _port(port),_listensockfd(gsock),_isrunning(false){}void InitServer(){// 1. 创建socket_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(FATAL, "socket create error\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd: %d\n", _listensockfd); // 3struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;// 2. bind sockfd 和 Socket addrif (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(INFO, "bind success\n");// 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接if (::listen(_listensockfd, gblcklog) < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERR);}LOG(INFO, "listen success\n");}class ThreadData{public:int _sockfd;TcpServer *_self;InetAddr _addr;public:ThreadData(int sockfd, TcpServer *self, const InetAddr &addr):_sockfd(sockfd), _self(self), _addr(addr){}};void Loop(){// signal(SIGCHLD, SIG_IGN);_isrunning = true;while (_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// 4. 获取新连接int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");continue;}InetAddr addr(client);LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);// version 0 --- 不靠谱版本// Service(sockfd, addr);// version 1 --- 多进程版本// pid_t id = fork();// if (id == 0)// {// // child// ::close(_listensockfd); // 建议!// if(fork() > 0) exit(0);// Service(sockfd, addr);// exit(0);// }// // father// ::close(sockfd);// int n = waitpid(id, nullptr, 0);// if (n > 0)// {// LOG(INFO, "wait child success.\n");// }// version 2 ---- 多线程版本 --- 不能关闭fd了,也不需要了// pthread_t tid;// ThreadData *td = new ThreadData(sockfd, this, addr);// pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离// version 3 ---- 线程池版本 int sockfd, InetAddr addrtask_t t = std::bind(&TcpServer::Service, this, sockfd, addr);ThreadPool<task_t>::GetInstance()->Equeue(t);}_isrunning = false;}static void *Execute(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_self->Service(td->_sockfd, td->_addr);delete td;return nullptr;}void Service(int sockfd, InetAddr addr){// 长服务while (true){char inbuffer[1024]; // 当做字符串ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){inbuffer[n] = 0;LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);std::string echo_string = "[server echo] #";echo_string += inbuffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}}::close(sockfd);}~TcpServer() {}private:uint16_t _port;int _listensockfd;bool _isrunning;
};
1. 类结构和成员变量
成员变量:
_port:服务器监听的端口号,默认为 8888。
_listensockfd:服务器的监听套接字描述符。
_isrunning:标志服务器是否在运行,控制服务器的生命周期。
枚举常量:
SOCKET_ERROR:表示创建套接字失败时的错误码。
BIND_ERROR:表示绑定地址失败时的错误码。
LISTEN_ERR:表示监听失败时的错误码。
常量:
gport:默认的监听端口号。
gsock:默认的套接字标识符,表示未初始化的套接字。
gblcklog:用于 listen() 调用的 backlog 参数,指定操作系统允许的最大连接数。
task_t:使用 std::function<void()> 定义的任务类型,代表线程池中将要执行的任务。这里用来封装客户端连接的处理工作。
2. TcpServer 类的主要函数
TcpServer::InitServer()
该函数用于初始化服务器,完成以下工作:
创建套接字:
使用 ::socket() 创建一个 TCP 套接字。
如果创建失败,输出错误日志并退出。
绑定地址:
使用 ::bind() 将套接字与指定的地址和端口绑定。
如果绑定失败,输出错误日志并退出。
监听连接:
使用 ::listen() 将套接字设为监听状态,等待客户端连接。
如果监听失败,输出错误日志并退出。
TcpServer::Loop()
该函数是服务器的主循环,它负责接受客户端的连接并将连接交给线程池处理:
在循环中,调用 ::accept() 接受来自客户端的连接请求。
如果连接成功,创建一个 InetAddr 对象以保存客户端的 IP 地址和端口信息。
将处理任务(客户端连接的处理)封装为一个 task_t,然后将任务提交给线程池。
TcpServer::Service()
这是处理客户端请求的核心函数:
该函数会从客户端套接字中读取数据(通过 ::read())。
如果读取成功,则将客户端发送的消息打印出来,并以 "server echo" 开头将数据返回给客户端。
如果读取到的数据为空(客户端关闭连接),则输出日志并关闭套接字。
如果发生读取错误,则输出错误日志并关闭套接字。
TcpServer::Execute()
这是一个静态函数,用于在线程池中执行处理任务。该函数被线程池中的线程调用,处理客户端请求:
它首先将线程标记为分离状态,以便线程结束后自动释放资源。
然后它调用 Service() 来处理客户端请求。
处理完成后,销毁 ThreadData 对象。
3. 线程池的使用
服务器使用线程池来处理每个客户端的连接。每当有新连接时,创建一个 task_t(封装了 TcpServer::Service() 方法),然后将任务提交给线程池:
这样线程池中的线程会并行处理这些任务,避免了每次都创建新线程的开销。
4. ThreadData 类
ThreadData 类用于封装每个客户端连接的相关信息:
sockfd:客户端的套接字。
self:指向当前 TcpServer 对象的指针。
addr:客户端的地址信息。
该类主要用于在线程中传递参数,它的生命周期由线程池中的线程控制。
5. 错误处理
在整个过程中,如果发生错误(如创建套接字失败、绑定失败、监听失败等),会通过 LOG() 打印详细错误信息,并通过 exit() 终止程序。可以在实际应用中根据需求改进错误处理逻辑(如重新尝试,或者返回错误状态)。
6. 程序退出
服务器的退出主要由 Loop() 中的 _isrunning 控制。当前 _isrunning 为 false 时,Loop() 会退出。程序会继续执行后续的清理工作,并最终终止。
TcpServerMain.cc
#include "TcpServer.hpp"#include <memory>// ./tcpserver 8888
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);tsvr->InitServer();tsvr->Loop();return 0;
}
TcpClientMain.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// ./tcpclient server-ip server-port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}// 注意:不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,OS会自动bind sockfd,用自己的IP和随机端口号// 什么时候进行自动bind?If the connection or binding succeedsstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect socket error" << std::endl;exit(2);}while(true){std::string message;std::cout << "Enter #";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());char echo_buffer[1024];n = read(sockfd, echo_buffer, sizeof(echo_buffer));if(n > 0){echo_buffer[n] = 0;std::cout << echo_buffer << std::endl;}else{break;}}::close(sockfd);return 0;
}
效果展示
虽然做的有点粗糙, 但是完成的还不错!
下一章还是 TCP socket 编程, 实现命令处理的功能, 处理从客户端接收到的命令,检查这些命令的安全性,并执行这些命令。好了, 篇幅已经很长了, 我们下期在见~
相关文章:
计算机网络socket编程(5)_TCP网络编程实现echo_server
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 计算机网络socket编程(5)_TCP网络编程实现echo_server 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交…...
w055基于web的服装生产管理的设计与实现
🙊作者简介:多年一线开发工作经验,原创团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取,记得注明来意哦~🌹赠送计算机毕业设计600个选题excel文…...
hadoop集群搭建
一、环境准备⼯作: 确保已经按照以下文档安装完毕:虚拟机和远程工具的安装(hadoop集群安装01)-CSDN博客 Linux设置以及软件的安装(hadoop集群安装02)-CSDN博客 虚拟机的克隆和免密(hadoop集群03)-CSDN博…...
JVM中TLAB(线程本地分配缓存区)是什么
JVM中TLAB(线程本地分配缓存区)是什么 简单来说 TLAB,线程本地分配缓存区,是在 Java 堆内存中的一块线程专属的内存区域,每个线程在创建对象时,首先会尝试在自己的 TLAB 区域内分配内存,这样多…...
如何使用 MMPreTrain 框架
如何使用 MMPreTrain 框架进行预训练模型的微调和推理 MMPreTrain 是一个基于 PyTorch 的开源框架,专注于图像分类和其他视觉任务的预训练模型。它提供了丰富的预训练模型和便捷的接口,使得研究人员和开发者可以轻松地进行模型微调和推理。本文将详细介…...
Python的tkinter如何把日志弄进文本框(Text)
当我们用python的Tkinter包给程序设计界面时,在有些时候,我们是希望程序的日志显示在界面上的,因为用户也需要知道程序目前运行到哪一步了,以及程序当前的运行状态是否良好。python的通过print函数打印出来的日志通常显示在后台&a…...
bash笔记
0 $0 是脚本的名称,$# 是传入的参数数量,$1 是第一个参数,$BOOK_ID 是变量BOOK_ID的内容 1 -echo用于在命令窗口输出信息 -$():是命令替换的语法。$(...) 会执行括号内的命令,并将其输出捕获为一个字符串ÿ…...
嵌入式工程师面试题 - 2024/11/19
欢迎找我进行职业规划,超值的自我投资 -> 嵌入式软件工程师一对一指导 转载请注明来源 1.以下叙述中正确的是() A 程序必须包含所有三种基本结构才能成为一种算法 B 我们所写的每条C语句,经过编译最终都将转换成二进制的机器…...
springboot配置https,并使用wss
学习链接 springboot如何将http转https SpringBoot配置HTTPS及开发调试 可借鉴的参考: springboot如何配置ssl支持httpsSpringBoot配置HTTPS及开发调试的操作方法springboot实现的https单向认证和双向认证(java生成证书)SpringBoot配置Https访问的详细步骤Sprin…...
【C语言】连接陷阱探秘(3):形参、实参与返回值
目录 一、形参的 “迷障” 1.1. 定义与功能 1.2. 类型不匹配 1.3. 数量不一致 1.4. 顺序不一致 1.5. 数组形参退化 二、实参的 “暗礁” 2.1. 定义与功能 2.2. 求值顺序 “谜题” 2.3. 悬空指针 “深渊” 三、返回值的 “陷阱” 3.1. 定义与功能 3.2. 陷阱与缺陷 …...
学习笔记030——若依框架中定时任务的使用
定时任务是软件开发中经常使用一个功能。 Java定时任务广泛应用于各种需要定时执行或周期性执行任务的场景,如: 数据备份:定期备份数据库中的数据,确保数据的安全性和可靠性。数据同步:如果有多个数据源需要同步数据…...
基于SpringBoot的数据结构系统设计与实现(源码+定制+开发)
博主介绍: ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台…...
利用 Python 和 Selenium 高效启动和管理 Chrome 浏览器
在自动化测试和网页抓取的过程中,Selenium 是最常用的工具之一。其强大的功能可以与浏览器无缝集成,实现复杂的操作。然而,为了提高效率和扩展性,尤其在处理大量任务时,我们可以通过定制化的方法启动 Chrome 浏览器并与…...
外卖系统开发实战:从架构设计到代码实现
开发一套外卖系统,需要在架构设计、技术选型以及核心功能开发等方面下功夫。这篇文章将通过代码实例,展示如何构建一个基础的外卖系统,从需求梳理到核心模块的实现,帮助你快速掌握开发要点。 一、系统架构设计 一个完整的外卖系…...
类和对象(上)--类、类的实例化(对象)、this指针
1.类 1.1定位: 和namespace一样,类也有类域。同样起到既保护又限制的功能。别人不能随意访问类里的东西,得通过特定的方式来访问(访问方法和命名空间域一样,三种方法)。 1.2作用 在C语言中,…...
使用ENSP实现浮动静态路由
一、项目拓扑 二、项目实现 1.路由器AR1配置 进入系统试图 sys将路由器命名为R1 sysname R1关闭信息中心 undo info-center enable 进入g0/0/0接口 int g0/0/0将g0/0/0接口IP地址配置为1.1.1.1/24 ip address 1.1.1.1 24进入g0/0/1接口 int g0/0/1将g0/0/1接口IP地址配置为2.…...
JavaEE 【知识改变命运】02 多线程(1)
文章目录 线程是什么?1.1概念1.1.1 线程是什么?1.1.2 为什么要有线程1.1.3 进程和线程的区别1.1.4 思考:执行一个任务,是不是创建的线程或者越多是不是越好?(比如吃包子比赛)1.1.5 ) Java 的线程…...
pytorch官方FasterRCNN代码详解
本博文转自捋一捋pytorch官方FasterRCNN代码 - 知乎 (zhihu.com),增加了其中代码的更详细的解读,以帮助自己理解该代码。 代码理解的参考Faster-RCNN全面解读(手把手带你分析代码实现)---前向传播部分_手把手faster rcnn-CSDN博客 1. 代码结构 作为 to…...
【线程】Java多线程编程
【线程】Java多线程编程 一、前言一个最简单的多线程编程示例可以使用的工具 二、创建线程的方式三、Thread类中重要的属性和方法3.1 构造方法3.2 常见属性 一、前言 当有多个线程的时候,这些线程的执行顺序是不确定的。这一点,是我们之前提到的操作系统…...
4.2 MySQL存储过程
存储过程是MySQL中用于封装一组 SQL 语句的数据库对象,便于简化重复任务、增强性能和逻辑复用。本文将从多个角度详细解析存储过程的功能、语法和应用场景。 1. 存储过程概述 1.1 什么是存储过程 存储过程(Stored Procedure)是一段在数据库…...
archlinux安装waydroid
目录 参考资料 注意 第一步切换wayland 第二步安装binder核心模组 注意 开始安装 AUR安裝Waydroid 启动waydroid 设置网络(正常的可以不看) 注册谷歌设备 安装Arm转译器 重启即可 其他 参考资料 https://ivonblog.com/posts/archlinux-way…...
C语言数学函数库<math.h>的常用函数讲解
math函数 一. 基础数学函数1. fabs(double x) — 绝对值2. fmod(double x, double y) — 余数3. pow(double x, double y) — 幂运算4. sqrt(double x) — 平方根5. cbrt(double x) — 立方根6. hypot(double x, double y) — 计算斜边 二. 对数和指数函数1. exp(double x) — …...
Swift 实现判断链表是否存在环:快慢指针法
文章目录 前言摘要描述题解答案题解代码题解代码分析示例测试及结果时间复杂度空间复杂度总结关于我们 前言 本题由于没有合适答案为以往遗留问题,最近有时间将以往遗留问题一一完善。 LeetCode - #141 环形链表 不积跬步,无以至千里;不积小流…...
借助算力云跑模型
算力平台:FunHPC | 算力简单易用 AI乐趣丛生 该文章只讲述了最基本的使用步骤(因为我也不熟练)。 【注】:进入平台,注册登录账号后,才能租用。学生认证+实名认证会有免费的算力资源࿰…...
Python 版本的 2024详细代码
2048游戏的Python实现 概述: 2048是一款流行的单人益智游戏,玩家通过滑动数字瓷砖来合并相同的数字,目标是合成2048这个数字。本文将介绍如何使用Python和Pygame库实现2048游戏的基本功能,包括游戏逻辑、界面绘制和用户交互。 主…...
Homebrew切换成国内源
将 Homebrew 切换成国内的源可以显著提高软件包的下载速度。以下是详细步骤,帮助你将 Homebrew 的主仓库和核心仓库切换到国内镜像源。这里以中国科学技术大学(USTC)的镜像源为例,其他镜像源的切换方法类似。 切换 Homebrew 主仓…...
《Shader 入门精要》学习笔记 茵蒂克丝
渲染流水线 《UnityShader入门精要》学习1_cpu设置渲染状态是什么-CSDN博客 UnityShader 基础 《UnityShader入门精要》学习2_unityshader入门精要2-CSDN博客 学习Shader需要的数学基础 《UnityShader入门精要》学习3_透视投影到-1到1空间-CSDN博客 实现一个简单的顶点/…...
小苯吃糖果
一、题目 二、代码——解析 1、3种情况,第一种:他们都相等;第二种:前大后;第三种:后大前 2、转换成列表,使用列表方法进行升序 x,y,z map(int,input().split()) a [x,y,z] a.sort() if a[0…...
Docker+Nginx | Docker(Nginx) + Docker(fastapi)反向代理
在DockerHub搜 nginx,第一个就是官方镜像库,这里使用1.27.2版本演示 1.下载镜像 docker pull nginx:1.27.2 2.测试运行 docker run --name nginx -p 9090:80 -d nginx:1.27.2 这里绑定了宿主机的9090端口,只要访问宿主机的9090端口&#…...
开发工具 - VSCode 快捷键
以下是一些常用的 VS Code 快捷键(Windows、macOS 和 Linux 均适用,略有不同): 常用快捷键 功能Windows/LinuxmacOS打开命令面板Ctrl Shift P 或 F1Cmd Shift P打开文件Ctrl OCmd O保存文件Ctrl SCmd S全部保存Ctrl K,…...
session理解
一、术语session session:中文经常翻译为 ‘会话’,其本来的含义是指有始有终的一系列动作/消息,比如:打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称为一个session。在阅读技术书籍时我们可能会看到这样的话“在…...
linux服务器中tomcat应用使用root用户启动之后再使用普通用户启动后无法启动问题解决
参考一 参考二 修改用户名用户组为普通用户 在Linux中,您可以使用chown命令修改文件夹的所有者(用户名),使用chgrp命令修改文件夹的所属组(用户组)。还可以使用chown命令同时修改所有者和所属组。 例如&…...
非线性控制器设计原理
非线性控制器设计原理 非线性控制器设计旨在解决非线性系统的控制问题,克服传统线性控制器在处理非线性现象(如饱和、死区、耦合、时变性等)时的不足。其核心在于利用非线性数学工具和设计方法,使控制系统在非线性条件下具备良好…...
【CSS3】Flex弹性布局
文章目录 前言一、基本概念1.容器和项目:2.主轴和交叉轴: 二、容器属性1.flex-direction:决定主轴的方向,即x轴还是y轴2.flex-wrap:定义项目的换行方式3.flex-flow:flex-direction属性和flex-wrap属性的简写…...
Ubuntu24.04LTS设置root用户可远程登录
Ubuntu24.04LTS设置root用户可远程登录 文章目录 Ubuntu24.04LTS设置root用户可远程登录1. 设置root密码2. 设置root用户可远程登录1. 查看ssh服务是否安装2. 安装ssh服务3. 再次查看ssh服务是否安装4. 配置ssh文件5. 重启ssh服务6. root远程登录 1. 设置root密码 Ubuntu安装后…...
【计算机网络】数据链路层
跨网络传输的本质:由许多局域网(子网)转发的结果 要彻底理解跨网络转发,首先要理解 -> 局域网中报文的转发原理! 一、以太网帧格式 1.报头的含义 源地址/目的地址:源MAC地址和目的MAC地址 帧协议类型&…...
SciPy库spatial.transform模块Rotation类的from_rotvec 函数介绍
SciPy 库的 spatial.transform 模块 Rotation 类 是一个工具类,用于在多种旋转表示形式(例如旋转矩阵、四元数、旋转向量、欧拉角等)之间进行转换,以及执行旋转操作。 示例代码 1. 构造旋转对象 from scipy.spatial.transform …...
递归-迭代
24. 两两交换链表中的节点 Leetcode 24 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。 递归解法 // 注意:cpp …...
Arcpy 多线程批量重采样脚本
Arcpy 多线程批量重采样脚本 import arcpy import os import multiprocessingdef resample_tifs(input_folder, output_folder, cell_size0.05, resampling_type"BILINEAR"):"""将指定文件夹下的所有 TIFF 文件重采样到指定分辨率,并输出…...
使用bcc/memleak定位C/C++应用的内存泄露问题
C/C应用的内存泄露 在笔者之前的一篇文章中,提到了通过每隔一段时间抓取应用的/proc/XXX/maps文件对比得到进程的内存增长区域,然后调用gdb调试工具的dump binary memory命令将增长的内存(即对应着泄露的内存数据)导出到文件&…...
前端和后端
前端和后端 前端、后端的编程语言/服务器前端定义前端技术栈后端定义后端技术栈 web服务器数据库浏览器URL 前端、后端的编程语言/服务器 前端定义 前端指的是用户在使用软件时所看到的那部分,是与用户直接进行交互的部分。主要负责展示信息或数据,并将…...
【含文档】基于SpringBoot+Vue的茶园茶农文化交流平台(含源码+数据库+lw)
1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…...
vue3项目部署在阿里云轻量应用服务器上
文章目录 概要整体部署流程技术细节小结 概要 vue3前端项目部署在阿里云轻量服务器 整体部署流程 首先有一个Vue3前端项目和阿里云应用服务器 确保环境准备 如果是新的服务器,在服务器内运行以下命令更新软件包 sudo apt update && sudo apt upgrade -y …...
Sklearn 内置数据集简介
sklearn内置了一些机器学习的数据集,其中包括iris(鸢尾花)数据集、乳腺癌数据集、波士顿房价数据集、糖尿病数据集、手写数字数据集、体能训练数据集和酒质量数据集。 datasetsloadersiris(鸢尾花)datasets.load_iris…...
【AI战略思考12】调整战略目标,聚焦数据分析,学习德川家康,剩者为王
【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 战略目标调整 长期战略目标和理想不变 参与打造 AI 完全体,即通用人工智能(AGI)和超级人工智能(ASI)长期指 10 年以上 调整短期战略目标 原目标&…...
Go语言switch语句
在Go语言中,switch,是一个高度灵活,其功能强大的控制结构,相比较Java中的switch,更受到语言重视。 目录 1.基础用法2.多值匹配3.不指定表达式的 switch4.使用 fallthrough 强制进入下一个分支5.使用类型断言的 switch…...
【汽车制动】汽车制动相关控制系统
目录 1.ABS (Anti-lock Brake System,防抱死制动系统) 2.EBD(Electronic Brake-force Distribution,电子制动力分配系统) 3.TCS(Traction Control System,牵引力控制系统) 4.VDC(…...
动态规划算法--01背包问题详细讲解步骤
举个例子 要确定哪些物品被放入背包以达到最大价值,可以在计算 dp 数组的同时记录选择的物品。具体来说,可以使用一个额外的数组来记录每个状态的选择情况。以下是一个详细的步骤和代码实现: n 3 W 5 weights [2, 1, 3] values [6, 3…...
操作系统(系统调用)
一. DPL目标描述符特权级,CPL当前描述符特权级 DPL和CPL用于区分内核态和用户态。内核态的DPL为0,用户态的DPL为3。注意每个段寄存器都有DPL的数据段。当从用户态切换到内核态时需要把段寄存器的DPL也设置为0。 CPL从当前段CS寄存器的低三位读出&#x…...
“iOS profile文件与私钥证书文件不匹配”总结打ipa包出现的问题
目录 文件和证书未加载或特殊字符问题 证书过期或Profile文件错误 确认开发者证书和私钥是否匹配 创建证书选择错误问题 申请苹果 AppId时勾选服务不全问题 总结 在上线ios平台的时候,在Hbuilder中打包遇见了问题,生成ipa文件时候,一…...