【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服务器实现,可以监听指定的端口,接受客户端的连接请求,并为每个连接提供服务。下面是这段代码的主要实现逻辑:


TcpServer类的构造函数初始化了服务器的端口号和IP地址,initServer()函数用于初始化服务器,包括创建socket、绑定端口和IP地址以及监听连接请求等操作。
start()函数是服务器的主要逻辑,其中使用了accept()函数等待客户端的连接请求,如果有新的连接请求到来,就创建一个子进程为其提供服务。子进程中调用service()函数来处理客户端请求,该函数中使用read()函数读取客户端发送过来的数据,并使用write()函数将其回传给客户端。如果客户端关闭了连接,子进程会退出并成为一个僵尸进程,父进程通过waitpid()函数来回收子进程资源。
service()函数是一个回显服务器,它会将客户端发送过来的数据原封不动地回传给客户端。

#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

这段代码是一个 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 (客户端)

#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);
}

猜你喜欢

转载自blog.csdn.net/weixin_47952981/article/details/129986346