[Programme réseau TCP] Implémentation simple du serveur 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

Ce code est une implémentation de serveur TCP simple qui peut écouter un port spécifié, accepter les demandes de connexion des clients et fournir des services pour chaque connexion. Voici la principale logique d'implémentation de ce code :


Le constructeur de la classe TcpServer initialise le numéro de port et l'adresse IP du serveur. La fonction initServer() est utilisée pour initialiser le serveur, y compris la création de sockets, la liaison des ports et des adresses IP, et la surveillance des demandes de connexion.
La fonction start() est la logique principale du serveur, qui utilise la fonction accept() pour attendre la demande de connexion du client. Si une nouvelle demande de connexion arrive, un processus enfant est créé pour lui fournir des services. La fonction service() est appelée dans le processus enfant pour traiter la demande du client. Dans cette fonction, la fonction read() est utilisée pour lire les données envoyées par le client, et la fonction write() est utilisée pour les renvoyer à le client. Si le client ferme la connexion, le processus enfant se termine et devient un processus zombie, et le processus parent récupère les ressources du processus enfant via la fonction waitpid().
La fonction service() est un serveur d'écho, qui renverra intactes les données envoyées par le client au client.

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    
    
    //echo server
    //定义了一个大小为1024的字符数组,用于存储从客户端接收到的数据
    char buffer[1024];
    //用于不断地接收客户端发送过来的数据。
    while(true)
    {
    
    
        // read && write 可以直接被使用!
        //使用read()函数从sock中读取数据,read()函数的返回值s表示实际读取到的字节数
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        //如果s > 0,说明读取到了数据,将buffer数组中的数据转换为字符串并打印到控制台上,然后通过write()函数将数据回传给客户端。
        if(s > 0)
        {
    
    
            buffer[s] = 0; //将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        //如果s == 0,说明对端关闭了连接,此时打印一条日志信息,并跳出循环。
        else if(s == 0) //对端关闭连接
        {
    
    
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        //如果s < 0,说明读取数据时发生了错误,此时打印一条错误日志信息,并跳出循环
        else{
    
     // 
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
}
//这段代码实现了一个简单的回显服务器,它可以接收客户端发送过来的数据,并将其原封不动地回传给客户端。

class TcpServer
{
    
    
private:
    const static int gbacklog = 20;  //该变量用于指定在调用 listen() 函数时,内核所维护的未完成连接队列的最大长度。
    
public:
    TcpServer(uint16_t port, std::string ip=""):listensock(-1), _port(port), _ip(ip)
    {
    
    }

    void initServer()   //用于初始化服务器
    {
    
    
        // 1. 创建socket -- 进程和文件
        //调用 socket() 函数,指定地址族为 AF_INET(IPv4),传输方式为 SOCK_STREAM(面向连接的 TCP),协议为 0(自动选择协议)。
        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 -- 文件 + 网络
        //调用 bind() 函数,将套接字和指定的 IP 地址、端口号绑定起来。
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        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是面向连接的,当我们正式通信的时候,需要先建立连接
        //调用 listen() 函数,将套接字标记为被动套接字,开始监听连接请求。
        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信号,子进程退出的时候,会自动释放自己的僵尸状态
        while(true)
        {
    
    
            // sleep(1);
            // 4. 获取连接
            //sockaddr_in 是一个结构体类型,用于存储 IPv4 地址和端口号信息。
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // fd(李四,王五等提供服务的服务员) vs _sock(张三 --- 获取新连接)
            //调用 accept() 函数,等待客户端连接请求。如果连接请求成功,将返回一个新的套接字(servicesock),可以用于与客户端进行通信。
            int servicesock = accept(listensock, (struct sockaddr*)&src, &len);
            if(servicesock < 0)
            {
    
    
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功了
            //将 sockaddr_in 结构体变量 src 中的 sin_port 字段从网络字节序转换为本地字节序,以获取客户端连接请求中的端口号。
            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);
            // 开始进行通信服务啦
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的! -- 为什么? 单进程
            // service(servicesock, client_ip, client_port);

            // version 2.0 -- 多进程版 --- 创建子进程
            // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?1 0
            
            //创建一个子进程,用于处理新的连接请求。在子进程中,调用 service() 函数,用于提供服务。在服务完成后,调用 exit() 函数结束子进程。
            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;
};

tcp_server.cc

Ce code est une implémentation d'un serveur 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 (client)

#include <iostream>

int main()
{
    
    
    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);
}

Guess you like

Origin blog.csdn.net/weixin_47952981/article/details/129986346