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

news2024/11/17 5:51:38

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. 忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2. 更改进程的工作目录
    // chdir();
    // 3. 让自己不要成为进程组组长
    if (fork() > 0)
        exit(1);
    // 4. 设置自己是一个独立的会话
    setsid();
    // 5. 重定向0,1,2
    if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 6. 关闭掉不需要的fd
        if(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 3

const 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缓冲区中的数据刷新到OS
    fsync(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服务的sock
    uint16_t port_;  // client port
    std::string ip_; // client ip
    callback_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;
    }
    //类内成员, 成员函数,都有默认参数this
    static 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_STREAM
    int 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); // 结尾不会有\n
        if (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 success
            inbuffer[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. 创建socket
        listenSock_ = 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 Linux
        tp_->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.3
            Task t(serviceSock, peerIp, peerPort, execCommand);
            tp_->push(t);

            // waitpid(); 默认是阻塞等待!WNOHANG
            // 方案1

            // logMessage(DEBUG, "server 提供 service start ...");
            // sleep(1);
        }
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::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.cc
	g++ -o $@ $^ -std=c++11 -lpthread
TCPServer:TCPServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f TCPClient TCPServer serverTcp.log

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【工程实践】使用Roformer-sim(SimBERTv2 )做数据增强

前言 此文仅记录以Roformer-sim为基础模型做数据增强的过程&#xff0c;Roformer-sim模型细节请移步&#xff1a;SimBERTv2来了&#xff01;融合检索和生成的RoFormer-Sim模型 - 科学空间|Scientific Spaces https://github.com/ZhuiyiTechnology/roformer-sim 1.功能介绍 可以…

[LeetCode - Python]344.反转字符串(Easy);345. 反转字符串中的元音字母(Easy);977. 有序数组的平方(Easy)

1.题目 344.反转字符串(Easy) 1.代码 class Solution:def reverseString(self, s: List[str]) -> None:"""Do not return anything, modify s in-place instead."""# 双指针left,right 0, len(s)-1while left < right:temp s[left]s[…

利用PCL实现点云配准

一、介绍 This document demonstrates using the Iterative Closest Point algorithm in your code which can determine if one PointCloud is just a rigid transformation of another by minimizing the distances between the points of two pointclouds and rigidly tran…

Ishikawa

Ishikawa 石川、鱼骨头、因果 其实我压根不知道 Ishikawa 这个日文就是石川&#xff0c;^_^&#xff0c;视乎也没啥影响

fastadmin动态获取单选框选中值修改页面

需求场景&#xff1a; 在编辑页面中&#xff0c;要求要根据某一单选框&#xff08;字段名称popup&#xff09;的选中值&#xff0c;来动态显示或者隐藏某个div&#xff08;idupload_img&#xff09;。 edit: function () {var popVal $("input[typeradio][namerow[popup]…

CNN(四):ResNet与DenseNet结合--DPN

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 前面实现了ResNet和DenseNet的算法&#xff0c;了解了它们有各自的特点&#xff1a; ResNet&#xff1a;通过建立前面层与后面层之间的“短路…

U8g2 驱动oled自定义中文字库

原文&#xff1a;Arduino驱动LED128X64 - U8g2 参考&#xff1a; Arduino通过u8g2库驱动OLED适合 u8g2 的中文字体&#xff0c;采用文泉驿点阵宋体作为源本&#xff0c;提供 12x12、13x13、14x14、15x15 和 16x16 点阵字库。 本文所需工具下载 我们在项目中大概率会遇到LED显示…

淘宝商品详情(API接口)各大请求示例参考参数

主图&#xff0c;标题&#xff0c;价格&#xff0c;销量&#xff0c;库存&#xff0c;sku&#xff0c;详情信息&#xff0c;促销价&#xff0c;优惠券信息&#xff0c;券后价等 请求示例&#xff1a; <?php// 请求示例 url 默认请求参数已经URL编码处理 // 本示例代码未加…

一文走进时序数据库性能测试工具 TSBS

一、背景 在物联网、车联网等时序数据场景中&#xff0c;数据的高速写入能力至关重要&#xff0c;会对产品方案的可用性、可靠性和扩展性产生影响。 以物联网为例&#xff0c;当面临千万甚至上亿设备、平均每个设备采集几十个到几百个指标时&#xff0c;每秒生成的数据将达到…

使用JavaScript开发网页地图导航

使用JavaScript开发网页地图导航 导航是生活中的一个常见需求&#xff0c;而在互联网时代&#xff0c;网页地图导航成为了人们获取信息和帮助的重要工具。在网页中开发一个地图导航功能&#xff0c;能够提供用户位置定位、路线规划、交通情况等有用的信息&#xff0c;提供便利…

企业权限管理(五)-订单分页

订单分页查询 PageHelper介绍 PageHelper是国内非常优秀的一款开源的mybatis分页插件&#xff0c;它支持基本主流与常用的数据库&#xff0c;例如mysql、oracle、mariaDB、DB2、SQLite、Hsqldb等。 PageHelper使用 集成 引入分页插件有下面2种方式&#xff0c;推荐使用 Maven …

【Redis】Spring/SpringBoot 操作 Redis Java客户端

目录 操作 Redis Java客户端SpringBoot 操作Redis 步骤 操作 Redis Java客户端 1.Jedis 2.Lettuce(主流) <-Spring Data Redis SpringBoot 操作Redis 步骤 1.添加Redis 驱动依赖 2.设置Redis 连接信息 spring.redis.database0 spring.redis.port6379 spring.redis.host…

售后服务行业呼叫中心系统解决方案

随着社会经济的不断发展和消费者对售后服务需求的不断提高&#xff0c;在售后服务行业中&#xff0c;越来越多的企业使用呼叫中心系统来帮助企业提高售后服务质量和效率&#xff0c;提高客户满意度&#xff0c;增强企业竞争力。 一、呼叫中心系统的定义和功能 呼叫中心系统是指…

[mongo]性能机制,分析工具

性能机制 应用端 应用端-选择节点 对于复制集读操作&#xff0c;选择哪个节点是由readPreference决定的 primary/primaryPreferredsecondary/secondaryPreferrednearest 如果不希望一个远距离节点被选择 将它设置为隐藏节点通过标签&#xff08;Tag&#xff09;控制可选的节点…

分享Java技术下AutojsPro7云控代码

引言 有图有真相&#xff0c;那短视频就更是真相了。下面是三大语言的短视频。 Java源码版云控示例&#xff1a; Java源码版云控示例在线视频 核心技术&#xff1a;各个编程语言的WebSocket技术。 Java&#xff1a;Nettey、Net&#xff1a;Fleck、Python&#xff1a;Tornad…

Android Framework底层原理之WMS的启动流程

一 概述 今天&#xff0c;我们介绍 WindowManagerService&#xff08;后续简称 WMS&#xff09;的启动流程&#xff0c;WMS 是 Android 系统中&#xff0c;负责窗口显示的的服务。在 Android 中它也起着承上启下的作用。 如下图&#xff0c;就是《深入理解 Android》书籍中的…

模拟实现消息队列项目(完结) -- 基于MQ的生产者消费者模型

目录 前言 1. 生产者 2. 消费者 3. 启动消息队列服务器 4. 运行效果 结语 前言 在上一章节,我们完成了消息队列的客户端部分,至此我们整个消息队列项目就构建完成了,那我们做的这个消息队列到底有什么效果,以及如何去使用我们自己的消息队列呢?那么本文,就将我们的MQ进行实战操…

GSEA富集分析结果详解

1. GSEA富集分析原理图 2. GSEA富集分析过程 1. 计算富集分数&#xff08;ES&#xff09; 富集分数&#xff1a;S 反应基因集&#xff08;比如某个通路内的基因集&#xff09;成员 s 在排序基因集 L&#xff08;比如根据 logFC 排序的差异基因集&#xff0c;默认降序&#xf…

“为爱起航,一村一书院”在阳朔落地

2023年8月1-5 日&#xff0c;“关爱祖国下一代&#xff0c;助力乡村振兴” 之为爱起航项目在阳朔举行。 本次活动由千里思乡村振兴促进会联合中国文化交流大使组委会携同大湾区19位师生加入到首批“为爱起航&#xff0c;一村一书院”项目中&#xff0c;同时&#xff0c;本项目得…

分页查询从接口到实现,统一对日期类型进行格式化处理

编写Service实现类编写Mapper的sql&#xff0c;但复杂的sql语句需要写到mapper对应的xml文件中日期类型格式化处理 /*** 扩展springmvc框架的消息转换器* param converters*/Overrideprotected void extendMessageConverters(List<HttpMessageConverter<?>> conve…