【TCP网络程序】线程池版本的 TCP 服务器

Makefile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

tcp_server.hpp

这段代码实现了一个简单的TCP服务器,具有以下特点:

  • 使用无限循环来处理客户端的请求,一旦有请求到达,就使用read函数接收数据,然后使用write函数将接收到的数据再发回去给客户端。
  • 当客户端关闭连接时,服务器也会关闭对应的连接。
  • 实现了三个不同的服务方法:service、change、dictOnline。service方法是一个简单的回显服务,change方法将客户端发送的数据中的小写字母转换为大写字母后再返回,dictOnline方法会根据客户端发送的数据返回对应的中文翻译,如果客户端发送的数据不在字典中,则返回“我不知道…”。
  • 使用了线程池ThreadPool来处理客户端请求,通过将任务Task加入到任务队列中,然后在线程池中进行处理。
  • 使用了C++11中的std::string和std::unordered_map等STL容器来方便地处理字符串和字典映射。

如何使用线程池的

在start()函数中,使用了线程池来处理多个客户端的连接请求。在一个无限循环中,服务器不断尝试从监听socket中accept新的连接,如果成功则创建一个Task任务(Task类的构造函数参数包括这个连接的socket描述符、客户端IP地址和端口号,以及处理该连接请求的回调函数)并将其提交给线程池进行处理。处理过程中会调用Task任务中保存的回调函数来处理客户端请求。如果accept失败,则记录错误信息并继续等待下一个连接请求。

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    
    
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    while (true)
    {
    
    
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
    
    
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
    
    
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        {
    
     //
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    
    
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    // read && write 可以直接被使用!
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
    
    
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        char *start = buffer;
        while(*start)
        {
    
    
            char c;
            if(islower(*start)) c = toupper(*start);
            else c = *start;
            message.push_back(c);
            start++;
        }

        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
    
    
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    {
    
     //
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}
static void dictOnline(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    
    
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
    
    
        {
    
    "apple", "苹果"},
        {
    
    "bite", "比特"},
        {
    
    "banana", "香蕉"},
        {
    
    "hard", "好难啊"}
    };
    // read && write 可以直接被使用!
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
    
    
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        auto iter = dict.find(buffer);
        if(iter == dict.end()) message = "我不知道...";
        else message = iter->second;
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
    
    
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    {
    
     //
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}

class TcpServer
{
    
    
private:
    const static int gbacklog = 20; // 后面再说
public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1), _port(port),
          _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {
    
    
    }
    void initServer()
    {
    
    
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
    
    
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
        // inet_aton(_ip.c_str(), &local.sin_addr);
        // local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
    
    
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
    
    
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    void start()
    {
    
    
        // signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        _threadpool_ptr->run();
        while (true)
        {
    
    
            // sleep(1);
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // fd(李四,王五等提供服务的服务员) vs _sock(张三 --- 获取新连接)
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
    
    
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
                       servicesock, client_ip.c_str(), client_port);
            // verison4 --- 线程池版本
            // Task t(servicesock, client_ip, client_port, service);
            // Task t(servicesock, client_ip, client_port, change);
            Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);

            // version 3 --- 多线程版本
            // ThreadData *td = new ThreadData();
            // td->_sock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // //在多线程这里用不用进程关闭特定的文件描述符呢??1 0
            // pthread_create(&tid, nullptr, threadRoutine, td);
            // close(servicesock);

            // pthread_join();
            // version2.1 -- 多进程版
            // pid_t id = fork();
            // if(id == 0)
            // {
    
    
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0/*子进程本身*/) exit(0); //子进程本身立即退出
            //     // 孙子进程, 孤儿进程,OS领养, OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); //不会阻塞!
            // close(servicesock);

            // 开始进行通信服务啦
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的! -- 为什么? 单进程
            // service(servicesock, client_ip, client_port);

            // version 2.0 -- 多进程版 --- 创建子进程
            // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?1 0
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
    
    
            //     // 子进程, 子进程会不会继承父进程打开的文件与文件fd呢?1, 0
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢?
            //     close(_listensock);
            //     service(servicesock, client_ip, client_port);
            //     exit(0); // 僵尸状态
            // }
            // close(servicesock); // 如果父进程关闭servicesock,会不会影响子进程??下节课
            // 父进程
            // waitpid(); //
        }
    }
    ~TcpServer() {
    
    }

private:
    uint16_t _port;
    std::string _ip;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

tcp_server.cc

这段代码是一个 TCP 服务器的实现。

#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    
    
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    
    
    if(argc != 2)
    {
    
    
    //usage(argv[0]);:如果参数个数不为 2,调用 usage 函数打印程序的使用方法。
        usage(argv[0]);
        exit(1);
    }
    //如果参数个数为 2,将第二个参数转换为整数类型并赋值给 port 变量。
    uint16_t port = atoi(argv[1]);
    //创建一个 TcpServer 对象,将监听的端口号作为参数传入构造函数。
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    //初始化 TcpServer 对象。
    svr->initServer();
    //启动 TcpServer 对象。
    svr->start();
    return 0;
}

tcp_client.cc (客户端)

这段代码是一个TCP服务器的主函数,主要做以下几件事情:

  • 通过命令行参数获取服务器监听的端口号,如果参数数量不为2则输出使用方法并退出程序。
  • 创建一个TcpServer对象并传入端口号,将该对象指针存储在一个std::unique_ptr智能指针中,确保在程序结束时能够自动释放内存。
  • 调用TcpServer对象的initServer()方法进行服务器初始化,该方法主要用于创建监听套接字和设置服务器地址等工作。
  • 调用TcpServer对象的start()方法启动服务器,该方法主要是通过一个事件循环处理客户端连接请求,接收客户端请求并响应。
  • 返回0表示程序正常结束。
#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    
    
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    
    
    if(argc != 2)
    {
    
    
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    
    
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
    
    
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

猜你喜欢

转载自blog.csdn.net/weixin_47952981/article/details/129986819
今日推荐