【网络编程】万字详解||一个简单TCP服务器(TCP、线程池、守护进程)源码+介绍

00

TCP服务器

  • 锁:Lock.hpp
    • 代码
    • 介绍
  • 守护进程:daemonize.hpp
    • 代码
    • 说明
  • 日志文件:log.hpp
    • 代码
    • 说明
  • 任务处理 Task.hpp
    • 代码
    • 说明
  • 线程池 ThreadPool.hpp
    • 代码
    • 说明
  • 客户端 TCPClient.cc
    • 代码
    • 说明
  • 服务器 TCPServer.cc
    • 代码
    • 说明
  • 头文件包 util.hpp
    • 代码
  • Makefile

锁:Lock.hpp

代码

#pragma once#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(){pthread_mutex_init(&lock_, nullptr);}void lock(){pthread_mutex_lock(&lock_);}void unlock(){pthread_mutex_unlock(&lock_);}~Mutex(){pthread_mutex_destroy(&lock_);}private:pthread_mutex_t lock_;
};class LockGuard
{
public:LockGuard(Mutex *mutex) : mutex_(mutex){mutex_->lock();std::cout << "加锁成功..." << std::endl;}~LockGuard(){mutex_->unlock();std::cout << "解锁成功...." << std::endl;}private:Mutex *mutex_;
};

介绍

这段代码实现了一个简单的互斥锁(Mutex)和锁保护(LockGuard)机制,用于在多线程环境中保护共享资源的安全访问。

  • Mutex 类:

Mutex 类是一个封装了 pthread 互斥锁的简单类。它的构造函数用于初始化互斥锁,lock() 方法用于获取锁,阻塞其他尝试获取锁的线程,unlock() 方法用于释放锁,允许其他线程获取锁,析构函数用于销毁互斥锁。

  • LockGuard 类:

LockGuard 类是一个简单的锁保护类,它接受一个 Mutex 对象作为参数,并在构造函数中获取该锁,确保在进入作用域时自动获取锁,同时在析构函数中释放锁,确保在离开作用域时自动释放锁。这种方式称为“资源获取即初始化”(RAII)模式。

守护进程:daemonize.hpp

代码

#pragma once#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void daemonize()
{int fd = 0;// 1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);// 2. 更改进程的工作目录// chdir();// 3. 让自己不要成为进程组组长if (fork() > 0)exit(1);// 4. 设置自己是一个独立的会话setsid();// 5. 重定向0,1,2if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3{dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 6. 关闭掉不需要的fdif(fd > STDERR_FILENO) close(fd);}
}

说明

这段代码实现了将当前程序变成守护进程的功能。守护进程是在后台运行的一种特殊进程,它通常是由系统启动并在后台运行,没有控制终端与之关联,用于提供一些服务或执行某些任务。

  • 具体来说,这段代码中的 daemonize() 函数实现了以下步骤:
  1. 忽略 SIGPIPE 信号:

signal(SIGPIPE, SIG_IGN); 这句代码将 SIGPIPE 信号设置为忽略。SIGPIPE 通常在写入已经关闭的 socket 时产生,如果不忽略这个信号,会导致程序异常退出。

  1. 更改进程的工作目录:

chdir(); 这一行代码用于更改进程的工作目录。由于该行代码没有具体内容,可能是作者意图在此添加要切换的目录。

  1. 不要成为进程组组长:

fork() 返回的值大于 0,意味着在父进程中,此时父进程退出,而子进程继续执行。这样子进程就不会成为进程组组长。

  1. 设置独立会话:

setsid(); 这行代码会创建一个新的会话,并使当前进程成为该会话的领导进程。这样可以断开与控制终端的联系,让程序在后台运行。

  1. 重定向标准输入、输出和错误:

这部分代码将标准输入、标准输出和标准错误重定向到 /dev/null 设备文件,将输出丢弃。这是因为守护进程通常不需要与终端进行交互。

  1. 关闭不需要的文件描述符:

最后,将不需要的文件描述符关闭,避免造成资源浪费。
通过执行这些步骤,daemonize() 函数可以将当前程序转化为一个守护进程,使其在后台运行,并且不受终端的影响。

日志文件:log.hpp

代码

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};#define LOGFILE "serverTcp.log"// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; // ap -> char*va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap); // ap = NULL// 每次打开太麻烦umask(0);int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);assert(fd >= 0);FILE *out = (level == FATAL) ? stderr : stdout;dup2(fd, 1);dup2(fd, 2);fprintf(out, "%s | %u | %s | %s\n",log_level[level],(unsigned int)time(nullptr),name == nullptr ? "unknow" : name,logInfo);fflush(out); // 将C缓冲区中的数据刷新到OSfsync(fd);   // 将OS中的数据尽快刷盘 close(fd);// char *s = format;// while(s){//     case '%'://         if(*(s+1) == 'd')  int x = va_arg(ap, int);//     break;// }
}

说明

这段代码实现了一个日志记录函数 logMessage(),它可以在程序运行时记录不同级别的日志信息到日志文件。下面是对代码的逐行解释:

定义日志级别常量:使用整数值来表示不同的日志级别,即 DEBUG、NOTICE、WARNING 和 FATAL。

const char *log_level[]: 一个字符串数组,用于将日志级别对应的字符串名存储起来。

LOGFILE “serverTcp.log”: 定义一个用于存储日志的文件名。

void logMessage(int level, const char *format, …): 日志记录函数的定义。它接受一个日志级别参数 level 和格式化字符串 format,后面的 … 表示函数可以接受可变数量的参数。

assert(level >= DEBUG); assert(level <= FATAL);: 使用 assert 断言确保传入的日志级别在允许的范围内。

char *name = getenv(“USER”);: 获取当前用户的用户名,可以通过环境变量 “USER” 获取。

char logInfo[1024]; va_list ap; va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap); va_end(ap);: 使用可变参数列表 va_list 来处理 format 和后续的参数。vsnprintf 函数将格式化后的日志信息放入 logInfo 缓冲区。

umask(0); int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666); assert(fd >= 0);: 设置文件创建权限掩码为 0,确保日志文件的权限是 0666。然后使用 open 函数打开日志文件,O_WRONLY 表示以只写模式打开,O_CREAT 表示如果文件不存在则创建,O_APPEND 表示在文件末尾追加内容。

FILE *out = (level == FATAL) ? stderr : stdout;: 根据日志级别选择输出流,如果是 FATAL 级别则选择标准错误流 stderr,否则选择标准输出流 stdout。

dup2(fd, 1); dup2(fd, 2);: 使用 dup2 函数将文件描述符 fd 复制为标准输出和标准错误文件描述符,从而将输出定向到日志文件。

fprintf(out, “%s | %u | %s | %s\n”, …);: 使用 fprintf 将日志信息格式化输出到选定的输出流中。

fflush(out); fsync(fd);: 刷新输出流和将数据写入文件。

close(fd);: 关闭文件描述符。

这个日志记录函数允许你在不同级别下记录日志,并将其写入指定的日志文件中。根据传入的日志级别,你可以将不同级别的日志信息输出到标准输出或标准错误,或者将它们写入日志文件,以便在程序运行时进行记录和排查问题。

任务处理 Task.hpp

代码

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"class Task
{
public://等价于// typedef std::function<void (int, std::string, uint16_t)> callback_t;using callback_t = std::function<void (int, std::string, uint16_t)>;
private:int sock_; // 给用户提供IO服务的sockuint16_t port_;  // client portstd::string ip_; // client ipcallback_t func_;  // 回调方法
public:Task():sock_(-1), port_(-1){}Task(int sock, std::string ip, uint16_t port, callback_t func): sock_(sock), ip_(ip), port_(port), func_(func){}void operator () (){logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\pthread_self(), ip_.c_str(), port_);func_(sock_, ip_, port_);logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\pthread_self(), ip_.c_str(), port_);}~Task(){}
};

说明

这段代码实现了一个名为 Task 的类,用于封装一个需要在线程池中执行的任务。下面是对代码的逐行解释:

头文件包含:这段代码依赖于一些标准C++库、系统库以及之前提到的日志记录函数头文件 “log.hpp”。

class Task: 定义了一个名为 Task 的类,用于封装一个任务,以便在线程池中执行。

using callback_t = std::function<void (int, std::string, uint16_t)>;: 使用 std::function 定义了一个回调函数类型 callback_t,它接受三个参数:整数、字符串和一个无符号短整数。这个回调函数类型用于在任务执行时进行任务处理。

int sock_; uint16_t port_; std::string ip_; callback_t func_;: 私有成员变量,分别表示要处理的 socket、客户端端口、客户端IP 和回调函数。

构造函数:定义了两个构造函数,一个是默认构造函数,另一个接受四个参数:socket、客户端IP、客户端端口和回调函数。

void operator () (): 重载了调用运算符 (),使得 Task 类的实例可以像函数一样被调用。在调用时,会执行任务,包括输出开始日志、执行回调函数和输出结束日志。

析构函数:默认析构函数。

这个 Task 类的目的是将需要在线程池中执行的任务封装起来。每个任务可以包含一些需要执行的操作,当任务被线程池中的线程调度执行时,它会按照预定义的流程执行。在任务开始执行和结束执行时,会通过日志记录函数输出相应的日志信息,以便跟踪任务的执行情况。

线程池 ThreadPool.hpp

代码

#pragma once#include <iostream>
#include <cassert>
#include <queue>
#include <memory>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "Lock.hpp"using namespace std;int gThreadNum = 15;template <class T>
class ThreadPool
{
private:ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false){assert(threadNum_ > 0);pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;void operator=(const ThreadPool<T>&) = delete;public:static ThreadPool<T> *getInstance(){static Mutex mutex;if (nullptr == instance) //仅仅是过滤重复的判断{LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁if (nullptr == instance){instance = new ThreadPool<T>();}}return instance;}//类内成员, 成员函数,都有默认参数thisstatic void *threadRoutine(void *args){pthread_detach(pthread_self());ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);// prctl(PR_SET_NAME, "follower"); // 更改线程名称while (1){tp->lockQueue();while (!tp->haveTask()){tp->waitForTask();}//这个任务就被拿到了线程的上下文中T t = tp->pop();tp->unlockQueue();t(); // 让指定的先处理这个任务}}void start(){assert(!isStart_);for (int i = 0; i < threadNum_; i++){pthread_t temp;pthread_create(&temp, nullptr, threadRoutine, this);}isStart_ = true;}void push(const T &in){lockQueue();taskQueue_.push(in);choiceThreadForHandler();unlockQueue();}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}int threadNum(){return threadNum_;}private:void lockQueue() { pthread_mutex_lock(&mutex_); }void unlockQueue() { pthread_mutex_unlock(&mutex_); }bool haveTask() { return !taskQueue_.empty(); }void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }void choiceThreadForHandler() { pthread_cond_signal(&cond_); }T pop(){T temp = taskQueue_.front();taskQueue_.pop();return temp;}private:bool isStart_;int threadNum_;queue<T> taskQueue_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *instance;// const static int a = 100;
};template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;// #pragma once
// #include <iostream>
// #include <cassert>
// #include <queue>
// #include <memory>
// #include <cstdlib>
// #include <pthread.h>
// #include <unistd.h>
// #include <sys/prctl.h>
// #include "Lock.hpp"
// int gThreadNum=10;//线程数目
// template<class T>
// class TreadPool
// {
// private:
//     TreadPool(int threadNum=gThreadNum ):threadNum_(threadNum),isStart_(false)
//     {
//         assert(threadNum_>0);
//         pthread_mutexattr_init(&mutex_,nullptr);//互斥锁
//         pthread_cond_init(&cond_,nullptr);//条件变量//     }
//     ThreadPool(const ThreadPool<T> &) = delete;
//     void operator=(const ThreadPool<T>&) = delete;// public:
//     static ThreadPool<T>*getinstance() //单例
//     {
//         static Mutex mutex;
//         if(nullptr==instance)
//         {
//             LockGuard lockguard(&mutex); //进入代码块 加锁 退出 解锁
//             if(nullptr==instance)
//             {
//                 instance=new TreadPool<T>();
//             }
//         }
//         return instance;//     }
//     类内成员 成员函数 都有默认参数 this
//     static void *threadRoutine(void *args)
//     {
//         pthread_detach(pthread_self());
//         ThreadPool<T>*tp =static_cast<ThreadPool<T>*>(args);
//         while(1)
//         {
//             tp->lockQueue();
//             while(!tp->havaTask())
//             {
//                 tp->waitForTask();
//             }
//             运行到这里任务就进入线程
//             T t=tp->pop();
//             tp->unlockQueue();
//             t();//让指定的先处理这个任务
//         }
//     }
//     void start()
//     {
//         asser(!isStart_);
//         for(int i=0;i<threadNum_;i++)
//         {
//             pthread_t temp;
//             pthread_create(&temp,nullptr,threadRoutine,this);//         }
//         isStart_=true;
//     }//     void push(const T&in)
//     {
//         lockQueue();
//         taskQueue_.push(in);
//         choiceT
//     }//     ~TreadPool()
//     {
//          pthread_mutex_destroy(&mutex_);
//         pthread_cond_destroy(&cond_);
//     }// private:
//  void lockQueue() { pthread_mutex_lock(&mutex_); }
//     void unlockQueue() { pthread_mutex_unlock(&mutex_); }
//     bool haveTask() { return !taskQueue_.empty(); }
//     void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
//     void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
//     T pop()
//     {
//         T temp = taskQueue_.front();
//         taskQueue_.pop();
//         return temp;
//     }//     bool isStart_;
//     int threadNum_;
//     queue<T> taskQueue_;
//     pthread_mutex_t mutex_;
//     pthread_cond_t cond_;//     static ThreadPool<T> *instance;
//     const static int a = 100;
// };

说明

这段代码实现了一个线程池的模板类 ThreadPool,用于管理和调度多个线程执行任务。下面是对代码的逐行解释:

头文件包含:这段代码依赖于一些标准C++库、系统库以及之前提到的互斥锁头文件 “Lock.hpp”。

int gThreadNum = 15;:定义了全局变量 gThreadNum,表示线程池中的线程数量,默认为 15。

template class ThreadPool:定义了一个模板类 ThreadPool,用于管理多个线程执行任务。

私有成员变量:包括线程数量 threadNum_、线程是否已经启动 isStart_、任务队列 taskQueue_、互斥锁 mutex_ 和条件变量 cond_。

构造函数:私有化构造函数,确保通过 getInstance 方法获得线程池的实例。

static ThreadPool *getInstance(): 返回线程池的实例。采用单例模式,通过互斥锁保证线程安全。

static void *threadRoutine(void *args): 线程函数,用于循环获取任务并执行。当任务队列为空时,线程等待,有任务时则执行任务。

void start(): 启动线程池,创建多个线程执行任务。

void push(const T &in): 向任务队列中添加一个任务。

~ThreadPool(): 析构函数,销毁互斥锁和条件变量。

void lockQueue(), void unlockQueue(), bool haveTask(), void waitForTask(), void choiceThreadForHandler(), T pop(): 辅助方法用于管理任务队列、线程等待和唤醒。

静态成员:定义了静态成员变量 instance,用于存储线程池的实例。

该线程池实现了任务的添加、线程的创建和管理,保证了多线程任务的执行。代码中使用互斥锁和条件变量来实现线程的同步和任务的调度。这样,你可以将需要多线程处理的任务添加到线程池中,线程池会自动分配线程执行任务。

客户端 TCPClient.cc

代码

#include "util.hpp"
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!volatile bool quit = false;static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}std::string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);// 1. 创建socket SOCK_STREAMint sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽// 2.1 先填充需要连接的远端主机的基本信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverPort);inet_aton(serverIp.c_str(), &server.sin_addr);// 2.2 发起请求,connect 会自动帮我们进行bind!if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "connect: " << strerror(errno) << std::endl;exit(CONN_ERR);}std::cout << "info : connect success: " << sock << std::endl;std::string message;while (!quit){message.clear();std::cout << "请输入你的消息>>> ";std::getline(std::cin, message); // 结尾不会有\nif (strcasecmp(message.c_str(), "quit") == 0)quit = true;ssize_t s = write(sock, message.c_str(), message.size());if (s > 0){message.resize(1024);ssize_t s = read(sock, (char *)(message.c_str()), 1024);if (s > 0)message[s] = 0;std::cout << "Server Echo>>> " << message << std::endl;}else if (s <= 0){break;}}close(sock);return 0;
}

说明

这段代码实现了一个简单的 TCP 客户端程序,用于连接到指定的服务器并与其进行通信。下面是代码的逐行解释:

#include “util.hpp”:引入一个名为 “util.hpp” 的头文件,这可能是一些工具函数或者常量的声明。

volatile bool quit = false;:定义了一个 volatile 的布尔变量 quit,用于控制程序是否退出的标志。

static void Usage(std::string proc):定义了一个静态函数 Usage,用于打印程序的用法信息。

int main(int argc, char *argv[]):主函数开始。

if (argc != 3):检查命令行参数是否正确,需要传入服务器的IP地址和端口号。

获取服务器的IP地址和端口号,并做相应的类型转换。

int sock = socket(AF_INET, SOCK_STREAM, 0);:创建一个 AF_INET 类型的 SOCK_STREAM(TCP) 套接字。如果创建失败,程序会退出。

填充服务器的地址信息,并使用 connect 函数与服务器建立连接。如果连接失败,程序会退出。

std::string message;:声明一个字符串变量 message,用于存储用户输入的消息。

while (!quit):进入主循环,只要 quit 标志为 false,就一直运行。

读取用户输入的消息,判断是否是 “quit”,如果是则将 quit 标志设置为 true。

使用 write 函数将消息发送给服务器。

使用 read 函数从服务器接收返回的消息。

打印服务器返回的消息。

当用户输入 “quit” 或者连接发生异常时,跳出循环。

关闭套接字,程序结束。

该程序实现了一个简单的 TCP 客户端,用户可以通过命令行传入服务器的 IP 地址和端口号,然后与服务器进行交互,发送消息并接收服务器的回复。当用户输入 “quit” 或者发生异常时,程序会退出。

服务器 TCPServer.cc

代码

#include "util.hpp"
#include "Task.hpp"
#include "Threadpool.hpp"
#include "daemonize.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>class ServerTcp; // 申明一下ServerTcp// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串if (s > 0){// read successinbuffer[s] = '\0';if (strcasecmp(inbuffer, "quit") == 0){logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);// 可以进行大小写转化了for (int i = 0; i < s; i++){if (isalpha(inbuffer[i]) && islower(inbuffer[i]))inbuffer[i] = toupper(inbuffer[i]);}logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!logMessage(DEBUG, "server close %d done", sock);
}
//系统命令行
void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char command[BUFFER_SIZE];while (true){ssize_t s = read(sock, command, sizeof(command) - 1); //我们认为我们读到的都是字符串if (s > 0){command[s] = '\0';logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);// 考虑安全std::string safe = command;if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink"))){break;}// 我们是以r方式打开的文件,没有写入// 所以我们无法通过dup的方式得到对应的结果FILE *fp = popen(command, "r");if(fp == nullptr){logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));break;}char line[1024];while(fgets(line, sizeof(line)-1, fp) != nullptr){write(sock, line, strlen(line));}// dup2(fd, 1);// dup2(sock, fp->_fileno);// fflush(fp);pclose(fp);logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!logMessage(DEBUG, "server close %d done", sock);
}
class ThreadData
{
public:uint16_t clientPort_;std::string clinetIp_;int sock_;ServerTcp *this_;public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts){}
};
class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string &ip = ""): port_(port),ip_(ip),listenSock_(-1),tp_(nullptr){}~ServerTcp(){}public:void init(){// 1. 创建socketlistenSock_ = socket(PF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){logMessage(FATAL, "socket: %s", strerror(errno));exit(SOCKET_ERR);}logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);// 2. bind绑定// 2.1 填充服务器信息struct sockaddr_in local; // 用户栈memset(&local, 0, sizeof local);local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2 本地socket信息,写入sock_对应的内核区域if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind: %s", strerror(errno));exit(BIND_ERR);}logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);// 3. 监听socket,为何要监听呢?tcp是面向连接的!if (listen(listenSock_, 5 /*后面再说*/) < 0){logMessage(FATAL, "listen: %s", strerror(errno));exit(LISTEN_ERR);}logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);// 运行别人来连接你了// 4. 加载线程池tp_ = ThreadPool<Task>::getInstance();}// static void *threadRoutine(void *args)// {//     pthread_detach(pthread_self()); //设置线程分离//     ThreadData *td = static_cast<ThreadData *>(args);//     td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);//     delete td;//     return nullptr;// }void loop(){// signal(SIGCHLD, SIG_IGN); // only Linuxtp_->start();logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接, accept 的返回值是一个新的socket fd ??// 4.1 listenSock_: 监听 && 获取新的链接-> sock// 4.2 serviceSock: 给用户提供新的socket服务int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取链接失败logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);continue;}// 4.1 获取客户端基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",strerror(errno), peerIp.c_str(), peerPort, serviceSock);// 5 提供服务, echo -> 小写 -> 大写// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept// transService(serviceSock, peerIp, peerPort);// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的// pid_t id = fork();// assert(id != -1);// if(id == 0)// {//     close(listenSock_); //建议//     //子进程//     transService(serviceSock, peerIp, peerPort);//     exit(0); // 进入僵尸// }// // 父进程// close(serviceSock); //这一步是一定要做的!// 5.1 v1.1 版本 -- 多进程版本  -- 也是可以的// 爷爷进程// pid_t id = fork();// if(id == 0)// {//     // 爸爸进程//     close(listenSock_);//建议//     // 又进行了一次fork,让 爸爸进程//     if(fork() > 0) exit(0);//     // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收//     transService(serviceSock, peerIp, peerPort);//     exit(0);// }// // 父进程// close(serviceSock); //这一步是一定要做的!// // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式// assert(ret > 0);// (void)ret;// 5.2 v2 版本 -- 多线程// 这里不需要进行关闭文件描述符吗??不需要啦// 多线程是会共享文件描述符表的!// ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);// pthread_t tid;// pthread_create(&tid, nullptr, threadRoutine, (void*)td);// 5.3 v3 版本 --- 线程池版本// 5.3.1 构建任务// 5.3 v3.1// Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// tp_->push(t);// 5.3 v3.2// Task t(serviceSock, peerIp, peerPort, transService);// tp_->push(t);// 5.3 v3.3Task t(serviceSock, peerIp, peerPort, execCommand);tp_->push(t);// waitpid(); 默认是阻塞等待!WNOHANG// 方案1// logMessage(DEBUG, "server 提供 service start ...");// sleep(1);}}private:// sockint listenSock_;// portuint16_t port_;// ipstd::string ip_;// 引入线程池ThreadPool<Task> *tp_;
};static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"<< std::endl;
}// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3)ip = argv[2];daemonize(); // 我们的进程就会成为守护进程ServerTcp svr(port, ip);svr.init();svr.loop();return 0;
}

说明

这段代码实现了一个 TCP 服务器程序,用于接受客户端连接并提供服务。下面是代码的逐行解释:

#include:引入了多个头文件,包括 “util.hpp”、“Task.hpp”、“Threadpool.hpp”、“daemonize.hpp”,以及一些系统头文件。

class ServerTcp;:在代码的开头声明了一个 ServerTcp 类,这是为了在后面的代码中使用。

volatile bool quit = false;:定义了一个 volatile 的布尔变量 quit,用于控制程序是否退出的标志。

void transService(int sock, const std::string &clientIp, uint16_t clientPort):定义了一个函数 transService,用于提供大小写转换的服务。

void execCommand(int sock, const std::string &clientIp, uint16_t clientPort):定义了一个函数 execCommand,用于执行系统命令的服务。

class ThreadData:定义了一个 ThreadData 类,用于传递线程数据。

class ServerTcp:定义了一个 ServerTcp 类,用于实现 TCP 服务器功能。

void init():初始化服务器,包括创建 socket、绑定、监听等操作。

void loop():进入主循环,接受客户端连接并提供服务。

static void Usage(std::string proc):定义了一个静态函数 Usage,用于打印程序的用法信息。

int main(int argc, char *argv[]):主函数开始。

检查命令行参数是否正确,需要传入服务器的端口号和可选的 IP 地址。

获取端口号和可选的 IP 地址。

调用 daemonize() 函数,将进程转为守护进程。

创建一个 ServerTcp 实例,调用 init() 初始化服务器。

进入 loop() 主循环,等待客户端连接并提供服务。

这段代码实现了一个 TCP 服务器,可以通过命令行传入端口号和可选的 IP 地址来启动服务器。服务器会不断地接受客户端连接,根据客户端的请求提供不同的服务,包括大小写转换服务和执行系统命令服务。当客户端退出或发生异常时,相应的服务也会终止。

头文件包 util.hpp

代码

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"#define SOCKET_ERR 1
#define BIND_ERR   2
#define LISTEN_ERR 3
#define USAGE_ERR  4
#define CONN_ERR   5#define BUFFER_SIZE 1024

Makefile

.PHONY:all
all:TCPClient TCPServer
TCPClient:TCPClient.ccg++ -o $@ $^ -std=c++11 -lpthread
TCPServer:TCPServer.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f TCPClient TCPServer serverTcp.log

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode-每日一题【剑指 Offer 16. 数值的整数次方】

题目 实现 pow(x, n) &#xff0c;即计算 x 的 n 次幂函数&#xff08;即&#xff0c;xn&#xff09;。不得使用库函数&#xff0c;同时不需要考虑大数问题。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10输出&#xff1a;1024.00000 示例 2&#xff1a; 输入&#…

c语言--浮点数(float)与0值比较

我们定义一个float类型的数据a&#xff0c;赋值为123456789并输出&#xff1a; int main() {float a;a 123456789;printf("%f\n",a);return 0; }输出结果为&#xff1a; 由此可以看出&#xff0c;程序输出的结果并不是其被赋予的值。即说明了浮点数存在一定的误差。…

日常工具 之 一些 / 方便好用 / 免费 / 在线 / 工具整理

日常工具 之 一些 / 方便好用 / 免费 / 在线 / 工具整理 目录 日常工具 之 一些 / 方便好用 / 免费 / 在线 / 工具整理 1、在线Json &#xff0c;可以在线进行json 格式验证&#xff0c;解析转义等操作 2、Gif动图分解&#xff0c;在线把 gif 图分解成一张张单图 3、在线P…

记一次空间告警与pg_rman keep-data-days参数研究

一、 背景 收到一个磁盘空间告警&#xff0c;检查发现是本地备份保留比较多导致的&#xff0c;处理过程倒很简单&#xff0c;手动清理掉旧的备份&#xff08;已自动备到远端服务器&#xff09;&#xff0c;告警就恢复了。 但是检查备份脚本的时候&#xff0c;发现keep-data-day…

使用logback异步打印日志

文章目录 一、介绍二、运行环境三、演示项目1. 接口2. 日志配置文件3. 效果演示4. 异步输出验证 四、异步输出原理五、其他参数配置六、源码分析1. 同步输出2. 异步输出 七、总结 一、介绍 对于每一个开发人员来说&#xff0c;在业务代码中添加日志是至关重要的&#xff0c;尤…

[C初阶笔记]P1

什么是C语言 1、机器语言&#xff08;二进制&#xff09;>汇编语言&#xff08;助记符&#xff09;>高级语言&#xff08;C、C等&#xff09; 2、c语言擅长底层软件开发&#xff08;操作系统、驱动程序&#xff09;&#xff0c;并不意味着不能开发其他。 C语言更贴近操作…

怎么裁剪视频大小尺寸?简单的裁剪方法分享

怎么裁剪视频的画面大小尺寸呢&#xff1f;有时当我们下载下来一段视频&#xff0c;由于视频的画面大小比例不同&#xff0c;会有很多的黑边&#xff0c;我们不管是观看还是进行二次编辑都非常影响体验&#xff0c;而调整视频画面比例以适应观众的设备或平台&#xff0c;比如将…

Smart HTML Elements 16.1 Crack

Smart HTML Elements 是一个现代 Vanilla JS 和 ES6 库以及下一代前端框架。企业级 Web 组件包括辅助功能&#xff08;WAI-ARIA、第 508 节/WCAG 合规性&#xff09;、本地化、从右到左键盘导航和主题。与 Angular、ReactJS、Vue.js、Bootstrap、Meteor 和任何其他框架集成。 智…

mac安装redis 配置密码

brew install redis 两个重要目录文件 /opt/homebrew/opt/redis/bin/redis-server /opt/homebrew/etc/redis.conf 配置文件默认是非守护进程运行redis 默认是不用密码验证&#xff0c;配置为开启密码验证 重启redis brew services restart redis 客户端调用 redis-cli 密码 …

Jupyter Notebook 未授权访问远程命令执行漏洞

漏洞描述 Jupyter是一个开源的交互式计算环境&#xff0c;它支持多种编程语言&#xff0c;包括Python、R、Julia等。Jupyter的名称来源于三种编程语言的缩写&#xff1a;Ju(lia)、Py(thon)和R。 Jupyter的主要特点是它以笔记本&#xff08;Notebook&#xff09;的形式组织代码…

vue3 vite gzip

1、首先前端项目里安装 vite-plugin-compression 插件 yarn add vite-plugin-compression 2、在 vite.config.js 中 import vue from vitejs/plugin-vue import { defineConfig } from vite import compressPlugin from vite-plugin-compressionexport default defineConf…

百度、NVIDIA、Intel……各大厂商集结,共话文心与飞桨共享生态下的大模型训推部署创新实践计划...

由深度学习技术及应用国家工程研究中心主办、百度飞桨和文心大模型承办的WAVE SUMMIT 2023峰会重磅来袭&#xff01;本届峰会聚焦AI技术、产业生态、未来趋势等主要方向&#xff0c;产、学、研、用各界大咖将围绕深度学习及大模型技术的发展与未来&#xff0c;带来行业前瞻洞察…