【Linux】TCP网络套接字编程+守护进程

news2024/11/23 16:50:39

文章目录

  • 日志类(完成TCP/UDP套接字常见连接过程中的日志打印)
  • 单进程版本的服务器客户端通信
  • 多进程版本和多线程版本
  • 守护进程化的多线程服务器


日志类(完成TCP/UDP套接字常见连接过程中的日志打印)

为了让我们的代码更规范化,所以搞出了日志等级分类,常见的日志输出等级有 Info Debug Warning Error Fatal 等,再配合上程序运行的时间,输出的内容等,公司中就是使用日志分类的方式来记录程序的输出,方便程序员找bug。 实际上在系统目录/var/log/messages文件中也记录了Linux系统自己的日志输出,可以看到我的Linux系统中之前在使用时产生了很多的error和warning,我们的代码也可以搞出来这样的输出日志信息到文件或者显示器的功能。

#pragma once
#include <iostream>
#include <string>
#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#define SIZE 1024
#define Screen 1    // 向屏幕打印
#define oneFile 2   // 向一个文件中打印
#define classFile 3 // 分类打印
#define LogFileName "log.txt"
enum
{
    Info = 0, // 信息
    Debug,    // 调试
    Warning,
    Error,
    Fatal // 严重错误
};

class Log
{
private:
    int _printMethod;

public:
    Log()
    {
        _printMethod = Screen;
    }
    ~Log()
    {
    }

    // 设置打印方式
    void Enable(int method)
    {
        _printMethod = method;
    }

    // 将日志等级转化为string
    std::string LevelToSting(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // 向一个文件中打印
    void PrintfOneFile(const std::string &filename, const std::string &logtxt) // log.txt
    {
        int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    // 分类打印
    void PrintfClassFile(int level, const std::string &logtxt) // log.txt.Info/Debug/Error等等
    {
        std::string filename = LogFileName;
        filename += '.';
        filename += LevelToSting(level);
        PrintfOneFile(filename, logtxt);
    }

    void printlog(int level, std::string logtxt)
    {
        switch (_printMethod)
        {
        case Screen:
        {
            std::cout << logtxt << std::endl;
            break;
        }
        case oneFile:
        {
            PrintfOneFile(LogFileName, logtxt);
            break;
        }
        case classFile:
        {
            PrintfClassFile(level, logtxt);
            break;
        }
        default:
            break;
        }
    }

    // 将日志信息写入到screen \ file
    void LogMessage(int level, const char *format, ...)
    {
        char LeftBuffer[SIZE];
        time_t t = time(NULL);
        struct tm *ctime = localtime(&t);
        snprintf(LeftBuffer, sizeof(LeftBuffer), "[%s]:[%d-%d-%d %d:%d:%d]", LevelToSting(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        char RightBuffer[SIZE];
        va_list list;
        va_start(list, format);                                    // 将list指向可变参数的第一个参数
        vsnprintf(RightBuffer, sizeof(RightBuffer), format, list); // 这个函数按照调用者传过来的format格式执行list的可变参数部分
        va_end(list); //将list置NUll

        char logtxt[2 * SIZE];
        snprintf(logtxt, sizeof(logtxt), "%s %s", LeftBuffer, RightBuffer);
        // 现在将Log打印到stdout
        // printf("%s", logtxt);
        printlog(level, logtxt);
    }
};
  1. 上面的localtime()是Linux中将时间戳转化本地时间的API,函数会返回一个结构struct tm *这个结构里面的成员就是年月日-时分秒,这个API的参数是本机的时间戳使用time(NULL)在这里插入图片描述
  2. snprintf是按照格式将指定内容和长度写入到指定缓冲区
  3. va_list 是 C 语言中用于处理可变参数列表的数据类型。在使用可变参数函数(如 printf、vprintf、fprintf、vfprintf 等)时,需要使用 va_list 类型的变量来访问这些参数。
    通常,你会在函数中声明一个 va_list 类型的变量,然后使用一系列宏来访问可变参数列表中的参数。在使用完之后,需要调用相应的宏来清理 va_list 变量。
    在这里插入图片描述4. vsnprintf是一个 C 标准库函数,用于格式化字符串并将结果输出到字符数组中。它类似于 snprintf,但是接受一个 va_list 类型的参数,允许处理可变参数列表。通过 vsnprintf,你可以将格式化后的字符串输出到指定的字符数组中,而不需要提前知道可变参数的数量。在这里插入图片描述

单进程版本的服务器客户端通信

TCP套接字的创建和UDP一样,先使用socket创建套接字,在结构中设置IP和port,其次就是将IP 和 端口的bind

  1. 不同点是bind之后需要将套接字设置为监听状态,因为TCP协议是面向连接的 在这里插入图片描述
    监听函数success的返回0,错误则返回-1,错误码被设置
  2. 在UDPbind完成套接字之后,就是recvfrom接受客户端发过来的数据,其次就是sendto
    将消息处理后发回客户端。但是在TCP将套接字设置为监听状态之后,需要accept接收客户端连接请求,并且返回一个新的sockfd文件描述符这个新的套接字用于与客户端进行通信,而原始的监听套接字仍然可以继续接受其他客户端的连接请求。,那么我们使用socketAPI创建套接字的时候,这个API返回的sockfd和我们使用accept返回的sockfd有什么区别呢?
    在这里插入图片描述
    使用socketAPI创建的套接字属于监听套接字,也就是说listenAPI需要使用它,它不能进行网络通信,使用accept接收的套接字这才是我们进行网络通信的套接字,如果是多线程或者多进程版本的服务器,我们就会使用监听套接字来进行另一个客户端的accept
  3. 在TCP套接字编程中使用read 和 write 进行读写数据
//TcpSever.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

const uint16_t default_port = 8080;
const std::string default_ip = "0.0.0.0";
Log lg;

class TcpSever
{
private:
    int _listen_sockfd;
    uint16_t _port;
    std::string _ip;
public:
    TcpSever(uint16_t port = default_port, std::string ip = default_ip) : _port(port), _ip(ip)
    {
    }

    ~TcpSever()
    {
    }

    void Init()
    {
        // 创建tcp套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "socket success: %d", _listen_sockfd);

        // 设置端口的IP
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 绑定套接字
        if (bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(-1);
        }

        lg.LogMessage(Info, "bind socket success, listensock_: %d", _listen_sockfd);

        // 将套接字设置为监听状态
        if (listen(_listen_sockfd, 10) < 0)
        {
            lg.LogMessage(Fatal, "listen Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "listen success");
    }

    void Service(int sockfd, uint16_t clientport, const std::string &clientip)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say# " << buffer << std::endl;
                std::string echo_string = "server echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            // 如果客户端提前退出,服务端会读取到0
            else if (n == 0)
            {
                lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    void Run()
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        while (true)
        {
            // 接收客户端连接  返回通信套接字!!!
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);

            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }

            // 接收数据
            // 拿到客户端的 IP地址 和 端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            // version 1 单进程版本  只能有一个用户进程进行读写
            Service(sockfd, clientport, clientip);
            close(sockfd);
        }
    }
};

我们在Main.cc中创建一个服务器对象,然后进行初始化 和 运行服务器端
使用命令行参数告诉服务器端的port

//Main.cc
#include "TcpSever.hpp"
#include<iostream>
#include<memory>

void Useage(const std::string& argv)
{
    std::cout << argv << " -> Should Enter port 1024+" << std::endl;
}

// ./tcpsever 8080
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Useage(argv[0]);
        return -1;
    }
    uint port = atoi(argv[1]);
    std::unique_ptr<TcpSever> tcp_sever(new TcpSever(port));
    tcp_sever->Init();
    tcp_sever->Run();
    return 0;
}

接下来就是编写客户端代码了:在TCP套接字编程中,connect 函数用于向服务器发起连接请求。当客户端创建一个套接字后,需要调用 connect 函数来连接到服务器的指定地址和端口。

这里是引用
同样客户端也是需要bind的,但是不需要用户显式bind:在TCP套接字编程中,客户端不需要显式调用 bind 函数来绑定地址的原因主要有两点:

  1. 动态选择本地端口: 在客户端调用 connect 函数时,系统会自动为客户端选择一个合适的本地端口,并将其绑定到客户端的套接字上。这样可以确保客户端套接字与服务器端建立连接时不会与其他套接字冲突。
  2. 客户端套接字的行为: 客户端通常不需要在网络上提供服务,而是主动连接到服务器端,因此不需要像服务器端那样在特定地址上监听连接请求。客户端的套接字行为是发起连接,而不是等待连接,因此不需要显式绑定地址。
//TcpClient.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
Log lg;
using namespace std;

void Useage(const std::string &argv)
{
    std::cout << argv << " -> Should Enter port 1024+" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Useage(argv[0]);
        return -1;
    }
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
        exit(-1);
    }
    lg.LogMessage(Info, "socket success: %d", sockfd);
    // 建立连接
    struct sockaddr_in sever;
    socklen_t len = sizeof(sever);
    memset(&sever, 0, sizeof(sever));
    uint port = atoi(argv[2]);
    std::string ip = argv[1];
    sever.sin_family = AF_INET;
    sever.sin_port = htons(port);
    inet_aton(ip.c_str(), &(sever.sin_addr));
    if (connect(sockfd, (sockaddr *)&sever, len) < 0)
    {
        lg.LogMessage(Fatal, "connect Error: %s", strerror(errno));
        exit(-1);
    }
    std::string message;
    while(true)
    {
        cout << "client please Enter@ " << endl;
        getline(cin, message);
        write(sockfd, message.c_str(), message.size());
        char inbuffer[4096];
        int n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            cout <<  inbuffer << endl;
        }
    }
    close(sockfd);
    return 0;
}

客户端开始死循环运行时,第一件事就是向服务器发起连接请求,这个连接的工作也不难做,因为客户端知道目的ip和目的port,所以直接填充server结构体中的各个字段,然后直接发起连接请求即可。连接成功后就可以开始通信,同样的客户端也是使用read和write等接口来进行数据包的发送和接收。如果服务器读到0,则说明客户端已经不写了,那么如果客户端继续向服务器发消息,就相当于写端向已经关闭的读端继续写入,此时OS会终止掉客户端进程。
由于UDP和TCP分别是无连接和面向连接的,所以两者有些许不同,TCP的服务器如果挂掉,客户端继续写,则客户端进程会被操作系统终止掉,而UDP的服务器如果挂掉,客户端是可以继续写的,只不过客户端发送的数据包会被简单的丢弃掉罢了

问题提出: 现在出现了一个新的问题,用户1连接成功并开始通信时,用户2可以连接服务器,因为服务器一直处于监听状态,但用户2发送的消息却并不会被服务器回显,而只有当第一个用户进程被终止掉之后,用户2进程才会立马回显刚刚所发送的一堆消息,接下来用户2才可以正常和服务器通信,这是为什么呢?其实主要是因为我们的代码逻辑是串行执行的,一旦服务器启动时,因为是单进程,所以连接一个客户端之后,服务器就会陷入service的死循环,无法继续循环执行accept以接收来自客户端的连接请求。而当连接的客户端终止掉之后,service会读到0,此时才会break跳出死循环,重新执行accept建立新的连接。
所以如果想要让服务器同时建立多个连接,可以通过多进程或多线程以及线程池的方式来实现。


多进程版本和多线程版本

多进程的实现方案也很简单,让父进程去执行客户端连接的代码,也就是执行accept的功能,让fork出来的子进程执行客户端进行通信的服务代码,也就是执行service,创建子进程后,子进程应该将自己不使用的文件描述符关闭,防止子进程对父进程打开的文件进行误操作,尤其是像listenSockfd这样的文件描述符,如果子进程对listenSockfd进行写入什么的,很有可能会导致服务器崩溃,此外,关闭不用的文件描述符也可以给子进程腾出来一部分文件描述符表的下标位置,防止文件描述符泄露。

创建出来的子进程是需要等待的,pid_t result = waitpid(pid, &status, WNOHANG); // 使用 WNOHANG 选项进行非阻塞等待在代码中使用非阻塞式等待是一个非常不好用的做法,这会让服务器的工作主线偏离,因为如果要使用非阻塞式等待,则势必得通过轮询的方式来检测子进程的状态,那服务器就需要一直询问子进程是否退出,但我服务器的核心工作主线是接收客户端的请求并建立连接进行网络通信的啊,一旦非阻塞等待,服务器的性能就一定会下降,因为需要一直做不必要的工作:比如询问子进程状态,况且waitpid还是系统调用,每次循环还要陷入内核,所以非阻塞式等待是一个非常不好的方案,不要用他。

第一种解决方案就是让子进程fork出孙子进程,子进程立马退出终止,让孙子进程去提供service服务,孙子进程退出时会被1号进程init进程接管,回收孙子进程(孤儿进程)的资源。父进程此时就可以阻塞式等待子进程退出,这个阻塞其实可以忽略不计,因为一旦创建出子进程,子进程就会立马退出,父进程也会立马回收掉子进程的资源,从而父进程可以继续向后accept其他客户端的连接请求,而让孙子进程提供service服务,当孙子进程退出后,1号进程会回收他的资源。

第二种解决方案就比较简单轻松,可以直接捕捉SIGCHLD信号显示设置为SIG_IGN,这样会直接忽略掉,父进程就不需要等待子进程,当子进程退出时,linux系统会自动帮我们回收子进程资源,父进程就省心了,不用管子进程退不退出的事了,把这件事丢给linux系统来干,我父进程专心accept其他的客户端连接请求就OK。

SIGCHLD 信号是在子进程改变状态时(如终止或停止)由内核发送给父进程的信号。父进程通常会安装一个信号处理函数来处理 SIGCHLD 信号。
通常情况下,SIGCHLD 信号的处理方式有以下几种:

  1. 忽略信号: 父进程可以选择忽略 SIGCHLD 信号。这通常意味着父进程对子进程的状态变化不感兴趣,因此子进程终止后会被操作系统回收。
  2. 捕获并处理信号: 父进程可以安装一个信号处理函数来处理 SIGCHLD 信号。在信号处理函数中,父进程可以调用 wait 或 waitpid 函数来等待子进程的终止,并处理子进程的退出状态。
  3. 使用信号的默认处理方式: 如果父进程没有对 SIGCHLD 信号进行特殊处理,那么默认情况下,操作系统会将子进程的状态变化通知给父进程,父进程可以通过调用 wait 或 waitpid 函数来获取子进程的退出状态。
    SIGCHLD 信号的处理方式通常取决于父进程的需求以及对子进程状态变化的关注程度。在使用非阻塞式等待子进程时,SIGCHLD 信号通常用于提醒父进程子进程的状态变化,以便父进程可以及时处理。
//多进程
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

const uint16_t default_port = 8080;
const std::string default_ip = "0.0.0.0";
Log lg;
class TcpSever; //声明一下
class ThreadData
{
public:
    uint16_t _port;
    std::string _ip;
    int _sockfd;
    TcpSever* _tsvr;
    ThreadData(int sockfd, uint16_t port, const std::string &ip, TcpSever* ts) : _port(port), _ip(ip), _sockfd(sockfd), _tsvr(ts)
    {
    }
};
class TcpSever
{
private:
    int _listen_sockfd;
    uint16_t _port;
    std::string _ip;
public:
    TcpSever(uint16_t port = default_port, std::string ip = default_ip) : _port(port), _ip(ip)
    {
    }

    ~TcpSever()
    {
    }

    void Init()
    {
        // 创建tcp套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "socket success: %d", _listen_sockfd);

        // 设置端口的IP
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 绑定套接字
        if (bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(-1);
        }

        lg.LogMessage(Info, "bind socket success, listensock_: %d", _listen_sockfd);

        // 将套接字设置为监听状态
        if (listen(_listen_sockfd, 10) < 0)
        {
            lg.LogMessage(Fatal, "listen Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "listen success");
    }

    void Service(int sockfd, uint16_t clientport, const std::string &clientip)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say# " << buffer << std::endl;
                std::string echo_string = "server echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            // 如果客户端提前退出,服务端会读取到0
            else if (n == 0)
            {
                lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    
    void Run()
    {
        signal(SIGCHLD, SIG_IGN); // 忽略子进程的退出状态,让OS自动回收子进程僵尸
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        while (true)
        {
            // 接收客户端连接  返回通信套接字!!!
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);

            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }

            // 接收数据
            // 拿到客户端的 IP地址 和 端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip))
            // version 2 多进程版
             pid_t id = fork();
             if (id == 0)
             {
                 close(_listen_sockfd);  //子进程不需要这个fd,父进程会指向fd文件
                 Service(sockfd, clientport, clientip);
                 close(sockfd);
                 exit(-1);
              }
            close(sockfd);  //父进程需要关闭,文件描述符是引用计数的,如果父进程不关闭的话,用户不断增多,系统中fd会不断增多
        }
    }
};

多进程并不是一个好的实现方案,因为创建一个进程的代价远比线程大得多,频繁的创建和回收进程会给系统带来很大的压力,所以多进程是一个比较重量化方案,而反观多线程是一种轻量化的方案,所以使用多线程能让服务器的性能消耗更小一些。
实现的方式也比较简单,我们知道threadRoutine要传给pthread_create的话,必须为静态方法,如果是静态方法就无法调用service,所以我们搞一个结构体td包含TcpServer类的指针this,以及accept返回的用于通信的套接字文件描述符sockfd,将td地址传递给threadRoutine函数,线程函数内部进行回调service,service如果调用结束不要忘记将sockfd关闭,避免文件描述符资源泄露。在线程这里只有阻塞式等待join和不等待两种情况,没有非阻塞式等待,所以主线程创建线程之后如果不想阻塞式join从线程的退出,则可以创建线程之后立马将从线程设置为detach状态即线程分离,线程函数执行完毕之后退出时,由操作系统负责回收从线程资源,主线程也就撒手不管了。

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include "Log.hpp"
#include <arpa/inet.h> //struct sockaddr_in 结构在这个头文件里面
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

const uint16_t default_port = 8080;
const std::string default_ip = "0.0.0.0";
Log lg;
class TcpSever; //声明一下
class ThreadData
{
public:
    uint16_t _port;
    std::string _ip;
    int _sockfd;
    TcpSever* _tsvr;
    ThreadData(int sockfd, uint16_t port, const std::string &ip, TcpSever* ts) : _port(port), _ip(ip), _sockfd(sockfd), _tsvr(ts)
    {
    }
};
class TcpSever
{
private:
    int _listen_sockfd;
    uint16_t _port;
    std::string _ip;
public:
    TcpSever(uint16_t port = default_port, std::string ip = default_ip) : _port(port), _ip(ip)
    {
    }

    ~TcpSever()
    {
    }

    void Init()
    {
        // 创建tcp套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            lg.LogMessage(Fatal, "socket Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "socket success: %d", _listen_sockfd);

        // 设置端口的IP
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 绑定套接字
        if (bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(-1);
        }

        lg.LogMessage(Info, "bind socket success, listensock_: %d", _listen_sockfd);

        // 将套接字设置为监听状态
        if (listen(_listen_sockfd, 10) < 0)
        {
            lg.LogMessage(Fatal, "listen Error: %s", strerror(errno));
            exit(-1);
        }
        lg.LogMessage(Info, "listen success");
    }

    void Service(int sockfd, uint16_t clientport, const std::string &clientip)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say# " << buffer << std::endl;
                std::string echo_string = "server echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            // 如果客户端提前退出,服务端会读取到0
            else if (n == 0)
            {
                lg.LogMessage(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg.LogMessage(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    static void *Handler(void *args)
    {
        ThreadData *td = static_cast<ThreadData*>(args);
        // 为例实现客户端-服务器端 并发运行 将线程设置为分离状态 线程终止的时候会自动释放资源
        pthread_detach(pthread_self());
        // 线程之间的大多数资源是共享的,所以不能不关闭文件描述符
        td->_tsvr->Service(td->_sockfd, td->_port, td->_ip);
        delete td;
        return nullptr;
    }
    void Run()
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        while (true)
        {
            // 接收客户端连接  返回通信套接字!!!
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);

            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }

            // 接收数据
            // 拿到客户端的 IP地址 和 端口
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            // version 3 多线程版
            ThreadData *td = new ThreadData(sockfd, clientport, clientip, this);
            pthread_t tid;
            pthread_create(&tid, 0, Handler, td);
        }
    }
};

守护进程化的多线程服务器

上面的多线程服务器已经很完美了,但美中不足的是只要我的xshell或者vscode关闭了,该服务器就会被终止掉,我们还需要重新启动服务器,我们希望的是只要服务器启动之后,就不再受用户登录和注销的影响,这样的服务器进程我们把他叫做守护进程。
当xshell打开时,Linux会为我们创建一个会话,在一个会话当中有且只能有一个前台任务,可以有0个或多个后台任务,Linux创建的会话中,刚开始都是以bash作为前台任务,bash就是命令行解释器,用于客户输入指令和Linux kernel进行交互,当我们的程序运行起来时,bash进程会自动被切换为后台进程,所以你可以简单的试一下,当在命令行中启动进程后,执行pwd,ls,touch等bash指令一定是无效的,因为此时bash被切到后台运行了,等到进程终止退出后,Linux会重新将bash从后台切换为前台进程,此时用户就又可以通过bash指令重新和Linux kernel交互了。

自成一个会话的进程就被叫做守护进程,也叫做精灵进程。
要想让会话关闭以后进程还在运行,就需要让这个进程自成一个会话,也就是成为守护进程。
在这里插入图片描述
系统调用setsid的作用就是将调用该函数的进程变成守护进程,也就是创建一个新的会话,这个会话中只有当前进程。

注意:调用系统调用setsid的进程在调用之前不能是进程组的组长,否则无法创建新的会话,也就无法成为守护进程。

守护进程也具有文件描述符,但是它们通常不会使用标准输入、标准输出和标准错误这三个标准文件描述符(通常分别对应0、1和2),因为守护进程通常不与终端相关联,而是在后台独立运行。在守护进程启动时,通常会将这些标准文件描述符重定向到/dev/null(或者其他适当的文件描述符),以确保它们不会产生输出或输入。
这样做的目的是为了防止在后台运行时出现意外的输出,同时使得守护进程可以独立于终端运行,不会受到终端的影响。
/dev/null 是一个特殊的设备文件,系统中被用作无效设备。它实际上是一个位于文件系统中的文件,但是任何写入到 /dev/null 的数据都会被丢弃,任何从 /dev/null 读取的操作都会立即返回文件结束符。这意味着它可以用来丢弃不需要的输出或者提供一个空的输入源。
在一些情况下,程序可能会输出一些信息,但你并不想在这些信息中处理。这时你可以将输出重定向到 /dev/null,这样输出就会被丢弃而不会在终端上显示。同样地,如果程序需要一些输入但你并不想提供,你也可以将输入重定向自 /dev/null,这样程序会读取到空数据而不会被阻塞。总之,/dev/null 是一个用于丢弃数据或提供空数据的特殊设备文件,常用于重定向输入或输出到无效设备的场景中 。

在这里插入图片描述


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

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

相关文章

鸿蒙实战开发-如何使用声明式UI编程框架的基础组件

介绍 在本教程中&#xff0c;我们将通过一个简单的样例&#xff0c;学习如何使用声明式UI编程框架的基础组件。本篇Codelab将会使用Image组件、Slider组件、Text组件共同实现一个可调节的风车动画&#xff0c;实现效果如图所示 相关概念 Text组件&#xff1a;文本组件&#x…

9.Python类与对象

1 面向对象 类和对象都是面向对象中的重要概念。面向对象是一种编程思想&#xff0c; 即按照真实世界的思维方式构建软件系统。 例如&#xff0c;在真实世界的校园里有学生和老师&#xff0c;学生有学号、姓名、所 在班级等属性&#xff08;数据&#xff09;&#xff0c;还有…

MAC的Safari浏览器没有声音解决办法

有一段时间没打开电脑&#xff0c;也不知道是系统自动更新或是什么缘故&#xff0c;所有浏览器都无法正常发声。 现象如下&#xff1a; 首先&#xff0c;Safari浏览器无法自动播放声音&#xff0c;下载的360浏览器现象一致&#xff0c;但是播放其他音乐播放软件和视频软件都正…

Java 面试宝典:请说下你对 Netty 中Reactor 模式的理解

大家好&#xff0c;我是大明哥&#xff0c;一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技术网站&#xff1a;https://skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经 回答 Reactor 模式是一种高效处理并发网络事件的设计模式&…

新手使用GIT上传本地项目到Github(个人笔记)

亲测下面的文章很有用处。 1. 初次使用git上传代码到github远程仓库 - 知乎 (zhihu.com) 2. 使用Git时出现refusing to merge unrelated histories的解决办法 - 知乎

五、Yocto集成QT5(基于Raspberrypi 4B)

Yocto集成QT5 本篇文章为基于raspberrypi 4B单板的yocto实战系列的第五篇文章&#xff1a; 一、yocto 编译raspberrypi 4B并启动 二、yocto 集成ros2(基于raspberrypi 4B) 三、Yocto创建自定义的layer和image 四、Yocto创建静态IP和VLAN 本章节实操代码请查看github仓库&…

yolov5 v7.0打包exe文件,使用C++调用

cd到yolo5文件夹下 pyinstaller -p 当前路径 -i logo图标 detect.py问题汇总 运行detect.exe找不到default.yaml 这个是yolov8里的文件 1 复制权重文件到exe所在目录。 2 根据报错提示的配置文件路径&#xff0c;把default.yaml复制放到相应的路径下。&#xff08;缺少相应…

Linux的开发工具(二):编译器gcc/g++与Linux项目自动化构建工具-Makefile

Linux的编译器-gcc/g 基本概念&#xff1a;gcc是专门用来编译c语言的&#xff0c;g可以编译c或c语言 问题一&#xff1a;gcc有时候为什么不能编译带有for循环的c语言源文件&#xff1f; 答&#xff1a;gcc版本过低会不支持for循环等c99标准下的内容 解决方式&#xff1a;gcc…

Spring定义Bean对象笔记

前言&#xff1a;面向对象语言最基本的元素就是对象&#xff0c;在Spring中把对象都封装为一个个的Bean&#xff0c;即通过Bean容器来管理对象&#xff1b;那么接下来我们看下在Spring中如何创建所需要的Bean。 一、环境准备 员工类 package com.xlb.bean;public class Empl…

在 C#和ASP.NET Core中创建 gRPC 客户端和服务器

关于gRPC和Google protobuf gRPC 是一种可以跨语言运行的现代高性能远程过程调用 (RPC) 框架。gRPC 实际上已经成为 RPC 框架的行业标准&#xff0c;Google 内外的组织都在使用它来从微服务到计算的“最后一英里”&#xff08;移动、网络和物联网&#xff09;的强大用例。 gRP…

数据结构——二叉树——堆

前言&#xff1a; 在前面我们已经学习了数据结构的基础操作&#xff1a;顺序表和链表及其相关内容&#xff0c;今天我们来学一点有些难度的知识——数据结构中的二叉树&#xff0c;今天我们先来学习二叉树中堆的知识&#xff0c;这部分内容还是非常有意思的&#xff0c;下面我们…

重读 Java 设计模式: 深入探讨原型模式,灵活复制对象

引言 在软件开发中&#xff0c;经常会遇到需要创建对象的情况。有时候&#xff0c;我们希望创建一个新的对象&#xff0c;但又不想通过传统的构造方法来创建&#xff0c;而是希望通过复制一个现有对象的方式来创建新的对象。这时&#xff0c;原型模式就能派上用场了。原型模式…

【51单片机入门记录】A2普中开发板实验课例程

一、开发板原理图 二、例程 &#xff08;1&#xff09;呼吸灯 #include <STC89C5xRC.H> #include<intrins.h>unsigned int PWM_wanttime0; //想要多长时间改变的占空比一次 unsigned int Breath_wanttime0; //想要多长时间改变呼气或者吸气的值时间 unsigned …

Android Studio 新建aar包

有几个注意事项 第一是注意是选择Android library这个包注意moudle name不能包含apk这三个字符

动手学机器学习双线性模型+习题

在数学中&#xff0c;双线性的含义为&#xff0c;二元函数固定任意一个自变量时&#xff0c;函数关于另一个自变量线性 矩阵分解 设想有N个用户和M部电影&#xff0c;构建一个用户画像库&#xff0c;包含每个用户更偏好哪些类型的特征&#xff0c;以及偏好的程度。假设特征的个…

0.5米多光谱卫星影像在农业中进行地物非粮化、非农化监测

一、引言 随着科技的发展&#xff0c;卫星遥感技术已经成为了农业领域中重要的数据来源。其中&#xff0c;多光谱卫星影像以其独特的优势&#xff0c;在农业应用中发挥着越来越重要的作用。本文将重点探讨0.5米加2米多光谱卫星影像在农业中的应用。 二、多光谱卫星影像概述 多…

ESP8266 WiFi物联网智能插座—上位机软件实现

1、软件架构 上位机主要作为下位机数据上传服务端以及节点调试的控制端&#xff0c;可以等效认为是专属版本调试工具。针对智能插座协议&#xff0c;对于下位机进行可视化监测和管理。 软件技术架构如下&#xff0c;主要为针对 Windows 的PC 端应用程序&#xff0c;采用WPF以及…

Mock.js的基本使用

mock顾名思义&#xff0c;就是模拟的意思&#xff0c;它模拟什么呢&#xff1f;假设我们在开发的过程中&#xff0c;我们需要使用到接口&#xff0c;但是后端接口并没有完善&#xff0c;那么我们就可以使用到mock.js&#xff0c;它可以随机生成数据&#xff0c;拦截AJAX请求&am…

壁纸小程序Vue3(分类页面和用户页面基础布局)

1.配置tabBar pages.json "tabBar": {"color": "#9799a5","selectedColor": "#28B389","list": [{"text": "推荐","pagePath": "pages/index/index","iconPath&quo…

网络安全-内网渗透2

一、MIC 将我们上次未描述完的MIC在这里详细解释一下 咱们所抓的第二个包会给返回一个服务端的challenge 之后服务器回包的第三个包会回复一个client challenge 所以咱们客户端和服务端现在分别有两个challenge&#xff0c;相当于客户端和服务端互相交换了一下challenge 因此…