【计算机网络】TCP协议详解

news2024/12/25 9:35:24

欢迎来到 破晓的历程的 博客

⛺️不负时光,不负己✈️

文章目录

    • 1、引言
    • 2、udp和tcp协议的异同
    • 3、tcp服务器
      • 3.1、接口认识
      • 3.2、服务器设计
    • 4、tcp客户端
      • 4.1、客户端设计
      • 4.2、说明
    • 5、再研Tcp服务端
      • 5.1、多进程版
      • 5.2、多线程版
    • 5、守护进程化
      • 5.1、什么是守护进程
      • 5.2、如何实现守护进程

1、引言

在上一篇博客中,我们学习了Udp协议的相关内容,今天我们开始学习Tcp协议相关的本内容,并带着大家完成相关的代码的编写。

2、udp和tcp协议的异同

为了更好的学习Tcp协议的内容,我们就要对Tcp协议的特点有一个全面的了解。
以下是一个以表格形式呈现的UDP和TCP协议的异同点:

特性UDP(用户数据报协议)TCP(传输控制协议)
可靠性不保证数据的可靠性,不保证数据顺序或到达保证数据的可靠性,通过确认和重传机制确保数据正确无误地到达目的地
连接性无连接协议,发送数据前不需要建立连接面向连接的协议,在发送数据前需要建立连接(三次握手),数据传输完毕后需要断开连接(四次挥手)
头部开销头部较小(通常8字节),传输效率高头部较大(至少20字节,可能更多),包含更多的控制信息
数据传输方式数据以数据报的形式传输,每个数据报独立处理数据以字节流的形式传输,确保数据的连续性和完整性
拥塞控制不进行拥塞控制,网络拥塞时可能导致数据丢失通过滑动窗口和拥塞控制算法(如慢启动、拥塞避免、快重传、快恢复)来避免网络拥塞
实时性实时性较好,适用于对实时性要求较高的应用(如视频流、实时游戏)实时性较差,但可靠性高,适用于对可靠性要求较高的应用(如文件传输、电子邮件)
适用场景视频会议、流媒体、DNS查询、实时游戏等文件传输(FTP)、网页浏览(HTTP)、电子邮件(SMTP)等
流量控制不进行流量控制,发送方以恒定速率发送数据,不考虑接收方的接收能力通过滑动窗口机制进行流量控制,确保发送方的发送速率不超过接收方的接收能力
错误处理如果数据报在传输过程中出错,则丢弃该数据报,由上层协议负责错误处理通过确认和重传机制来处理错误,确保数据的正确传输

在现阶段,我们要关注的是:使用Tcp协议在通信的前提是客户端和服务器之间要建立链接。具体等到写代码时会详细的说明。

3、tcp服务器

3.1、接口认识

在Udp协议时,我们学习了几个网络方面常用的接口,今天我们需要再认识几个:

listen
listen函数是在socket编程中广泛使用的一个函数,特别是在TCP服务器端编程中。它的主要作用是将一个套接字(socket)设置为监听状态,以便能够接受来自客户端的连接请求。以下是listen函数的详细介绍:

一、函数原型

listen函数的原型定义在<sys/socket.h>头文件中,其原型如下:

#include <sys/socket.h>
int listen(int sockfd, int backlog);

二、参数说明

  • sockfd:这是一个已经创建好并绑定到特定IP地址和端口的套接字(socket)的文件描述符。它是socket函数的返回值,表示了一个端点(endpoint),用于网络通信。
  • backlog:这个参数指定了内核为相应套接字排队的最大连接个数,即请求队列的最大长度。它表示在某一时刻,服务器允许同时有最多backlog个客户端排队等待建立TCP三次握手。如果接收到更多的连接请求,这些请求可能会被忽略,客户端会收到ECONNREFUSED错误。

三、函数功能

listen函数的作用是将sockfd指定的套接字设置为监听状态,使其能够接受来自客户端的连接请求。当客户端发起连接请求时,内核会将请求放入请求队列中,然后等待服务器端的accept函数来接受这些连接。backlog参数限制了请求队列的最大长度,防止了服务器因接收过多连接请求而耗尽资源。

四、返回值

  • 成功时,listen函数返回0。
  • 失败时,返回-1,并设置errno以指示错误原因。常见的错误码包括EADDRINUSE(端口已被占用)、EINVAL(socket未绑定地址)、ENOTSOCK(不是一个socket文件描述符)等。

五、使用场景

在TCP服务器端编程中,listen函数通常紧随bind函数之后调用。bind函数用于将套接字绑定到特定的IP地址和端口上,而listen函数则将该套接字设置为监听状态,准备接受客户端的连接请求。之后,服务器可以调用accept函数来接受客户端的连接请求,并处理后续的数据传输。

六、注意事项

  • backlog的值应该根据服务器的处理能力和系统资源来合理设置。如果设置得太小,可能会导致新的连接请求被拒绝;如果设置得太大,可能会占用过多的系统资源。
  • listen函数不会阻塞等待连接请求的到来,它只是将套接字设置为监听状态。实际的连接请求接受是通过accept函数来完成的。

综上所述,listen函数是TCP服务器端编程中不可或缺的一部分,它使得服务器能够同时处理多个客户端的连接请求。

connect函数是网络编程中常用的一个函数,主要用于建立客户端与服务器之间的连接。以下是connect函数的详细介绍:

connect
一、函数原型

connect函数的原型定义在<sys/socket.h>头文件中,其原型如下:

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

二、参数说明

  • sockfd:这是一个由socket函数返回的套接字文件描述符,表示一个端点(endpoint),用于网络通信。
  • addr:这是一个指向sockaddr结构体的指针,该结构体包含了要连接的服务器的地址信息,包括IP地址和端口号。在实际使用中,通常会使用sockaddr_in结构体(对于IPv4)或sockaddr_in6结构体(对于IPv6)来提供这些信息,并在调用connect函数前将其地址强制转换为sockaddr *类型。
  • addrlen:这个参数指定了addr结构体的大小,通常使用sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6)来获取。

三、函数功能

connect函数用于客户端发起对服务器的连接请求。当客户端调用connect函数时,它会尝试与由addr参数指定的服务器地址和端口建立TCP连接。如果连接成功,connect函数返回0;如果连接失败,则返回-1,并设置errno以指示错误原因。

四、使用场景

connect函数主要在客户端编程中使用,用于与服务器建立连接。在客户端发起连接请求之前,通常需要先调用socket函数创建一个套接字,并调用bind函数(虽然对于客户端来说,bind函数是可选的,但在某些特定场景下可能需要使用)和listen函数(仅在服务器端使用)来准备套接字。然后,客户端就可以使用connect函数来尝试与服务器建立连接了。

五、注意事项

  • 在调用connect函数之前,需要确保服务器已经启动并监听在指定的地址和端口上。
  • connect函数在尝试建立连接时可能会阻塞,直到连接成功或发生错误。为了避免阻塞,可以使用非阻塞套接字或设置套接字选项来启用超时机制。
  • 在处理connect函数的返回值时,需要注意检查errno以确定连接失败的具体原因。

accpet

accept函数是网络编程中常用的一个函数,特别是在TCP服务器端编程中。它的主要作用是使服务器端接受客户端的连接请求,并在连接建立后返回一个用于后续通信的新的套接字文件描述符。以下是accept函数的详细介绍:

一、函数原型

accept函数的原型定义在<sys/socket.h>头文件中,其原型如下:

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

二、参数说明

  • sockfd:这是一个由socket函数返回的、已经绑定到特定IP地址和端口,并且处于监听状态的套接字文件描述符。
  • addr:这是一个指向sockaddr结构体的指针,用于存储接受到的客户端的地址信息(包括IP地址和端口号)。这个参数是可选的,如果不需要获取客户端的地址信息,可以将其设置为NULL。
  • addrlen:这是一个指向socklen_t类型的变量的指针,用于存储addr结构体的大小。在调用accept函数之前,应该将其初始化为addr结构体的大小(如sizeof(struct sockaddr_in)),函数返回时,它会被设置为实际返回的地址信息的长度。

三、函数功能

accept函数的作用是从sockfd指定的监听套接字的等待连接队列中抽取第一个连接请求,创建一个新的套接字,并将这个新套接字的文件描述符返回给调用者。这个新套接字用于与客户端进行后续的数据通信,而原始的监听套接字(sockfd)则继续保持在监听状态,等待接受其他客户端的连接请求。

四、返回值

  • 成功时,accept函数返回一个新的套接字文件描述符,该描述符用于与连接的客户端进行通信。
  • 失败时,返回-1,并设置errno以指示错误原因。常见的错误码包括EAGAIN(非阻塞模式下没有连接请求)、EBADF(无效的套接字文件描述符)、EINTR(操作被信号中断)等。

五、使用场景

accept函数主要在TCP服务器端编程中使用,用于接受客户端的连接请求。在服务器端调用listen函数将套接字设置为监听状态后,就可以通过循环调用accept函数来接受多个客户端的连接请求,并为每个连接请求创建一个新的套接字进行通信。

六、注意事项

  • 在调用accept函数之前,需要确保已经通过socket函数创建了套接字,并通过bind函数将其绑定到特定的IP地址和端口上,以及通过listen函数将其设置为监听状态。
  • 如果监听套接字被设置为非阻塞模式,并且没有等待的连接请求,accept函数会立即返回-1,并设置errno为EAGAIN或EWOULDBLOCK。为了避免这种情况下的忙等待,可以使用select函数或poll函数来检查套接字上是否有待处理的连接请求。
  • 当accept函数成功返回一个新的套接字文件描述符后,应该使用这个新的描述符与客户端进行通信,而不是原始的监听套接字描述符。
  • 在处理完与客户端的通信后,应该关闭这个新的套接字文件描述符以释放资源。但是,原始的监听套接字描述符应该保持打开状态,以便继续接受其他客户端的连接请求。

3.2、服务器设计

首先,服务器的完整代码如下:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

#include "log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
using namespace std;
namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    class TcpServer
    {
    public:
        TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                cerr << "socket error: " << errno << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success" << endl;

            // 2. bind绑定自己的网络信息
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                cerr << "bind error: " << errno << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            cout << "bind success" << endl;

            // 3. 设置socket 为监听状态
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                cerr << "bind error: " << errno << strerror(errno) << endl;
                exit(LISTEN_ERR);
            }
            cout << "bind success" << endl;
        }
        void start()
        {

            for (;;)
            {
                // 4. server 获取新链接
                // sock, 和client进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {

                    continue;
                }
                serverIO(sock);
                close(sock);
            }
        }
// 这个函数负责通信的过程。
        void serverIO(int sock)
        {
            char buffer[1024];
            while (1)
            {
                ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << buffer << endl;
                }
                else
                {
                    cout << "client end,me too" << endl;
                    close(sock);
                    break;
                }
            }
        }
        ~TcpServer() {}

    private:
        int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
        uint16_t _port;
    };

} // namespace server

代码说明:

  1. 包含必要的头文件:包含处理套接字和网络通信所需的头文件。
  2. 定义端口号和缓冲区大小:定义服务器监听的端口号和用于数据交换的缓冲区大小。
  3. 创建套接字:使用socket()函数创建一个新的套接字文件描述符。
  4. 绑定套接字:使用bind()函数将套接字绑定到服务器的地址和端口上。
  5. 监听连接:使用listen()函数让套接字进入监听状态,准备接受客户端的连接请求。
  6. 接受连接:使用accept()函数接受一个连接请求,并返回一个新的套接字文件描述符用于与客户端通信。
  7. 读取和发送数据:使用read()函数从客户端读取数据,使用send()函数向客户端发送数据。
  8. 关闭套接字:使用close()函数关闭套接字文件描述符,释放资源。

由于Tcp面向字节流这一特征,使得我们可以像


我们创建完套接字并绑定后,必须使得客户端处于listen状态,原因有:

  1. 准备接收连接listen函数的主要作用是将套接字(socket)从主动连接状态转变为被动监听状态。这意味着服务器端的套接字不再主动发起连接,而是等待客户端的连接请求。没有调用listen函数之前,套接字默认是处于主动连接状态,即用于客户端发起连接请求的。

  2. 设置监听队列listen函数允许你指定操作系统内核为相应套接字排队的最大连接数(backlog)。这个队列保存了那些已经与服务器建立了同步(即完成了TCP三次握手的前两步),但尚未被服务器accept函数处理的客户端连接。设置合理的backlog值对于服务器在高负载下的性能表现至关重要。

  3. 状态转换:从TCP/IP协议的角度来看,调用listen函数是TCP服务器状态转换的一部分。在TCP连接建立的过程中,服务器端套接字需要经历从CLOSEDLISTEN的转换,才能开始接受客户端的连接请求。

  4. 协议要求:TCP/IP协议规定,在服务器能够accept客户端的连接之前,必须先调用listen函数将套接字置于监听状态。这是一种协议级别的要求,确保了TCP连接的建立过程能够有序、可预测地进行。

  5. 错误检测listen函数的调用还可以帮助开发者在早期发现潜在的配置错误或资源限制问题。例如,如果尝试在一个非套接字文件描述符上调用listen,或者指定的backlog值过大导致系统资源不足,listen函数将返回错误。

调用listen函数是TCP服务器实现过程中不可或缺的一步,它确保了服务器能够正确地准备并接受来自客户端的连接请求。


在调用完listen函数后,我们就可以准备 接受来着客户端给我们发出的链接申请了。这个工作由accept完成。
#include <sys/types.h>  
#include <sys/socket.h>  
  
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

来看:
第一个参数为创建套接字的文件描述符。返回值也是一个文件描述符。这两个文件描述符是什么关系呢?

接下来,我给大家讲一个故事,大家听完这个故事就明白了。

一天,张三和李四来到了一个小镇,镇上有好多饭馆。他们决定挑一家店尝尝口味怎么样。他们来到一家饭馆门前,这时店小二对他们热情的说:“二位客观要吃饭吗?我们家是百年老店,味道绝对正宗”。然后他们两个就往店里走,店小二一边招呼着他们两个,一边说:“客人两位”。然后店小二又回到店门口继续拉客了,其他伙计负责点餐。

  • 我们调用accept的传入这个文件描述符就相当于拉客的这个店小二,负责对外等待连接。 收到一个连接后,将链接接手并传入后,继续对外等待连接
  • 返回的文件描述符就相当于店里其他的伙计,负责和链接进行具体的通信。

此时的代码是由问题的,因为这个服务端一次只能和一台客户端进行通信。这个问题我们一会儿想办法解决。

4、tcp客户端

4.1、客户端设计

完整的代码如下:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define NUM 1024

class TcpClient
{
public:
    TcpClient(const std::string &serverip, const uint16_t &serverport) 
    : _sock(-1), _serverip(serverip), _serverport(serverport)
    {}
    void initClient()
    {
        // 1. 创建socket
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(2);
        }
        // 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
        // 3. 要不要listen?不要!
        // 4. 要不要accept? 不要!
        // 5. 要什么呢??要发起链接!
    }
    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
        {
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            std::string msg;
            while(true)
            {
                std::cout << "Enter# ";
                std::getline(std::cin, msg);
                write(_sock, msg.c_str(), msg.size());

                char buffer[NUM];
                int n = read(_sock, buffer, sizeof(buffer)-1);
                if(n > 0)
                {
                    //目前我们把读到的数据当成字符串, 截止目前
                    buffer[n] = 0;
                    std::cout << "Server回显# " << buffer << std::endl;
                }
                else
                {
                    break;
                }
            }
        }
    }
    ~TcpClient()
    {
        if(_sock >= 0) close(_sock);
    }
private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

4.2、说明

  • 客户端依旧不需要显式的绑定端口号,因为端口号的数值对于客户端并无意义。只需要有一个端口号和客户端绑定即可。这个绑定的工作由第一次发送数据时由操作系统完成。
  • 然后我们就需要向服务器发起连接请求。这个工作由系统调用接口connect完成。
  • 然后我们就可以和操作文件一样来操作客户端。

5、再研Tcp服务端

在这里插入图片描述
如上图:上面的代码是串行执行的。所以当服务器在执行serverIO函数时,就不会执行accpet函数。
也就意味着:服务端在同一时间只能和一个客户端进行通信。这是不合理的。如何解决呢?

5.1、多进程版

       void start()
        {

            for (;;)
            {
                // 4. server 获取新链接
                // sock, 和client进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {

                    continue;
                }
                pid id=fork();
                if(id==0)//child 
                {
                    close(_listensock);
                    if(fork()>0)exit(0)
                    serverIO(sock);
                    close(sock);
                }
                // parent
                pid_t t=waitpid(id,nullptr,0);
                if(t>0)
                {
                    cout<<"waitpid success"<<endl;

                }
                
            }
        }

我们来分析一下如此设计的妙处:我们让子进程来执行通信任务,父进程阻塞式等待。但是这样做,整个代码还是串行执行的,和起初的代码没有区别。所以我们可以让子进程的子进程来执行通信任务,子进程返回,然后父进程马上waitpid成功开始执行accpet函数。此时的孙子进程就变成了一个孤儿进程,由操作系统领养,和主进程没有关系。


我们在学习操作系统信号部分时学到:子进程退出时会向父进程发送信号。如果我们将该信号自定义为忽略,父进程就不需要等待子进程退出了。所以代码我们可以这样设计:

void start()
        {
            signal(SIGCHLD,SIG_IGN);

            for (;;)
            {
                // 4. server 获取新链接
                // sock, 和client进行通信的fd
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {

                    continue;
                }
                pid id=fork();
                if(id==0)//child 
                {
                    close(_listensock);
                //    if( fork()>0)exit(0)
                    serverIO(sock);
                    close(sock);
                }
                // parent
                // pid_t t=waitpid(id,nullptr,0);
                // if(t>0)
                // {
                //     cout<<"waitpid success"<<endl;

                // }
                
            }
        }

5.2、多线程版

在这里插入图片描述
如图,这种方式比较简单,我们在学习线程时写过大量的这种代码,又因为改动的代码较多,所以这里我就不再对代码进行修改了。如上图大家自行对服务器代码进行修改。

5、守护进程化

如果我们在Linux服务器上跑着一个服务,突然我们的xshell异常终止了,这个服务也就被关闭了,这显然是不合理的。所以我们就需要将该服务守护进程化。


5.1、什么是守护进程

守护进程也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
在这里插入图片描述
如上图:就我现在用的Linux而言。我用的是从腾讯云租的服务器,我使用时用xsehll登上我的服务器即可。
登入服务器过后,服务器会在远端给我创建一个会话,会话中包括提供命令行解释的bash和若干个进程。这些进程可以有多个后台进程,但有且只能有一个前台进程。这个会话在关闭xshell后自动就丢失了。
在这里插入图片描述
如果在一个进程后面加上&符号,该进程后自动在后台运行。

如上图:一个进程组共同完成一个作业。同属于一个进程组的进程的PGID是相同的,它们的PID差一的。其中第一个进程为作业组长。
在这里插入图片描述
此外,我们也需要认识几个命令:

  • jobs:查询所有的系统中作业。
  • bg+作业号:把一个后台作业放到前台运行。
  • fg+作业号:使一个后台暂停的作业重新启动。

总结一下:我们如上讲的作业和进程组是受用户登录和注销影响的。如果我们想让进程不受用户登录和注销的影响(也就是实现守护进程化)。我们需要自称会话、自称进程组和终端设备。


5.2、如何实现守护进程

实现方案很多。
方案1
在系统中有相关的函数,我们可以通过相关的函数来实现守护进程。如下:
daemon函数是Linux系统中用于创建守护进程(Daemon Process)的一个函数。守护进程是一种在后台运行的特殊进程,它独立于控制终端,并且周期性地执行某种任务或等待处理某些发生的事件。下面是对daemon函数的详细介绍:

一、函数原型

#include <unistd.h>
int daemon(int nochdir, int noclose);

二、参数说明

  • nochdir:如果此参数为0,则daemon函数会将进程的当前工作目录更改为根目录(“/”)。这有助于守护进程与文件系统挂载点等环境隔离开来。
  • noclose:如果此参数为0,则daemon函数会将标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)重定向到/dev/null。这意味着守护进程不会接收任何输入,其输出和错误也不会显示在终端或任何文件中。

三、返回值

  • 成功时,daemon函数返回0。
  • 失败时,返回-1,并设置errno以指示错误原因。

四、实现原理

daemon函数的实现通常涉及以下几个步骤:

  1. 创建子进程:通过调用fork()函数创建一个新的子进程。父进程随后退出,留下子进程继续执行。这样做是为了让守护进程与父进程(通常是shell或启动脚本)的环境隔离开来。
  2. 创建新的会话:在子进程中,调用setsid()函数创建一个新的会话(session),并使该子进程成为会话的领头进程(session leader)。这会使得该进程完全与控制终端脱离。
  3. 改变工作目录:如果nochdir参数为0,则将当前工作目录更改为根目录(“/”)。
  4. 重定向标准I/O:如果noclose参数为0,则将标准输入、标准输出和标准错误输出重定向到/dev/null

五、主要用途

daemon函数主要用于需要长时间在后台运行的服务程序,如Web服务器、数据库服务器、作业调度器等。通过创建守护进程,这些服务可以在用户注销或终端关闭后继续运行,而不会受到终端会话结束的影响。

六、注意事项

  • 在编写守护进程时,需要注意信号处理、文件描述符管理、工作目录和文件创建掩码等问题,以确保守护进程的稳定性和安全性。
  • 不同版本的Unix和Linux系统可能在守护进程的编程规则上存在差异,因此在编写跨平台的守护进程时需要特别注意。

综上所述,daemon函数是Linux系统中用于创建守护进程的一个重要工具,它能够帮助程序在后台稳定运行,并处理各种系统任务。
但是,这个函数中规定了很多的内容。不如我们手写一个小组件使用起来方便。

方案2
在Linux文件中,有这样一个文件:
在这里插入图片描述
它的功能描述起来就是:将所有写进该文件的所有内容全部丢弃。但是在从文件中读取内容时不会阻塞也不会出错。
将标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)重定向到/dev/null。这意味着不会接收任何输入,其输出和错误也不会显示在终端或任何文件中。

setsid() 函数是 Unix 和 Unix-like 系统(如 Linux)中的一个系统调用,用于创建一个新的会话(session),并使调用进程成为该会话的领头进程(session leader)。同时,调用 setsid() 的进程会成为一个新的进程组的组长,并且脱离任何现有的控制终端(如果有的话)。

setsid() 函数的原型定义在 <unistd.h> 头文件中,其基本用法如下:

#include <unistd.h>

pid_t setsid(void);

如果调用成功,setsid() 返回调用进程的 PID(即新会话的领头进程的 PID)。如果调用失败,则返回 -1,并设置 errno 以指示错误原因。

以下是一些关于 setsid() 函数的要点:

  1. 新会话的创建:调用 setsid() 会创建一个新的会话,并且调用进程成为该会话的领头进程。这意味着该进程将不再属于之前的会话和进程组。

  2. 进程组的组长:新的会话领头进程也会成为其所在进程组的新组长。这是因为会话领头进程必须是一个进程组的组长。

  3. 脱离控制终端:如果调用 setsid() 的进程之前与控制终端相关联,那么调用成功后,该进程将不再与任何控制终端相关联。这是守护进程(daemon)的一个重要特性,因为守护进程通常需要在后台运行,并且不应该依赖于任何特定的终端。

  4. 使用场景setsid() 通常用于创建守护进程,但也可以用于其他需要独立会话和进程组的场景。

  5. 限制:调用 setsid() 的进程不能是进程组的组长。如果已经是组长,则调用会失败并返回 -1。

所以,我们需要做好调用setsid函数的准备:

  1. 要求不是进程组长。进程组长都是主进程,所以我们可以通过fork创建子进程,让子进程来调用setsid。
  2. 要求屏蔽信号,这里的信号主要是指因客户端异常的关闭而产生的信号,我们可以调用signal函数进行忽略。
  3. 由于守护进程和终端脱离。所以需要关闭默认打开的文件描述符。但是我们可以将文件描述符重定向到/dev/null中。
  4. 如果要改变工作目录,可以使用chdir进行改变。

所以,代码如下:

#pragma once
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<signal.h>
#define DEV "/dev/null"

void my_daemon()
{
    // 忽略信号
    signal(SIGPIPE,SIG_IGN);

    pid_t id=fork();
    if(id==0)
    {
    	//重定向工作
        int sock=open(DEV,O_RDWR);
        if(sock>=0)
        {
            dup2(sock,0);
            dup2(sock,1);
            dup2(sock,2);
        }
        else
        {
            close(0);
            close(2);
            close(1);
        }
        
        pid_t n=setsid();
        assert(n!=-1);
        //更改一下目录
        chdir("/");   
    }
}

本文内容到此结束,我们下一篇博客再见!

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

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

相关文章

Javascript面试基础6(下)

获取页面所有checkbox 怎样添加、移除、移动、复制、创建和查找节点 在JavaScript中&#xff0c;操作DOM&#xff08;文档对象模型&#xff09;是常见的任务&#xff0c;包括添加、移除、移动、复制、创建和查找节点。以下是一些基本的示例&#xff0c;说明如何执行这些操作&a…

【网络世界】HTTP协议

目录 &#x1f308;前言&#x1f308; &#x1f4c1; 概念 &#x1f4c1; URL &#x1f4c2; urlencode 和 urldecode &#x1f4c1; 协议格式 &#x1f4c1; 方法 &#x1f4c2; GET/get &#x1f4c2; POST/post &#x1f4c1; 常见的报头 &#x1f4c1; 状态码 &…

Web3 职场新手指南:从技能到素养,求职者如何脱颖而出?

随着 2024 年步入下半年&#xff0c;Web3 行业正在经历一系列技术革新。通过改进的跨链交互机制和兼容性&#xff0c;逐步消除市场碎片化的问题。技术的进步为开发者和用户都打开了新的前景。然而&#xff0c;复杂的技术和快速变化的市场环境也让许多新人望而却步。求职者如何找…

编译固件 -- 自用

编译环境 先安装编译环境 git clone <编译仓库路径> git checkout <编译主分支> 更新/下载 然后就是一样的更新下载 ./scripts/feeds update -a ./scripts/feeds install -a 然后直接编译 feeds/puppies/rom/scripts/make.sh 对应型号 make Vs 这里的对应型号可…

gitee的fork

通过fork操作&#xff0c;可以复制小组队长的库。通过复制出一模一样的库&#xff0c;先在自己的库修改&#xff0c;最后提交给队长&#xff0c;队长审核通过就可以把你做的那一份也添加入库 在这fork复制一份到你自己的仓库&#xff0c;一般和这个项目同名 现在你有了自己的库…

Footprint Analytics 助力 Core 区块链实现数据效率突破

Core 是一个基于比特币并兼容 EVM 的 Layer 1 区块链&#xff0c;正通过其创新解决方案引革新特币金融。作为首个引入非托管 BTC 质押协议及全球首个发行收益型 BTC ETP 产品的区块链&#xff0c;Core 站在了区块链技术的最前沿。通过利用超过 50% 的比特币挖矿哈希算力&#x…

24暑假算法刷题 | Day22 | LeetCode 77. 组合,216. 组合总和 III,17. 电话号码的字母组合

目录 77. 组合题目描述题解 216. 组合总和 III题目描述题解 17. 电话号码的字母组合题目描述题解 77. 组合 点此跳转题目链接 题目描述 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输…

python爬虫入门小案例

python爬虫 以下内容仅供学习交流,请勿用作其他用途,若涉及隐私和版权问题,请及时联系我删除 闲来无事,学了学爬虫小知识,适合入门,文笔拙劣,还望见谅 爬虫是什么: 爬取网页上的文字,图片,视频,音频 自动化操作浏览器,比如填写表单,打卡,提高工作效率爬虫的注意事项: 爬虫…

lua 游戏架构 之 游戏 AI (九)ai_mgr Ai管理

定义ai_mgr的类&#xff0c;用于管理游戏中实体的AI组件。 先定义 AI行为枚举和优先级&#xff1a; lua 游戏架构 之 游戏 AI &#xff08;八&#xff09;ai_tbl 行为和优先级-CSDN博客https://blog.csdn.net/heyuchang666/article/details/140712839?spm1001.2014.3001.55…

MySQL环境的配置文件json

突然了解到&#xff0c;使用json文件去进行环境的配置&#xff0c;这样修改参数的时候就只需要去改json文件中的内容&#xff0c;不需要去修改代码中的内容&#xff0c;其他人的MySQL和我的MySQL也不同&#xff0c;这时其他人只需要修改json文件中的内容&#xff0c;清晰明了&a…

基于微信小程序+SpringBoot+Vue的核酸检测服务系统(带1w+文档)

基于微信小程序SpringBootVue的核酸检测服务系统(带1w文档) 基于微信小程序SpringBootVue的核酸检测服务系统(带1w文档) 在目前的情况下&#xff0c;可以引进一款医院核酸检测服务系统这样的现代化管理工具&#xff0c;这个工具就是解决上述问题的最好的解决方案。它不仅可以实…

2024年开发者最爱用的Bug跟踪工具

国内外主流的10款BUG管理软件对比&#xff1a;PingCode、Worktile、禅道&#xff08;ZenTao&#xff09;、Bugzilla、Tapd、CODING、Teambition、Testin、Tower、乐道。 在软件开发的世界里&#xff0c;管理和跟踪Bug是一个让许多开发者头疼的问题。选择一个合适的Bug管理工具不…

C++题目_逃生路线总数(dfs)

题目描述 2021年夏天&#xff0c;LSH开开心心的骑着电动车出去玩&#xff0c;结果一不留神&#xff0c;他骑着电动车进入了一只恶犬的领地。恶犬发现它的领地被LSH侵犯了&#xff0c;立马去追LSH&#xff0c;准备咬他一大口。LSH慌忙逃窜&#xff0c;但是他的电动车电量即将耗…

电力电子中的电大、电小尺寸?

01 前言 大家好&#xff0c;这期我们聊一下电力电子中的电大尺寸和电小尺寸。对于大部分电力电子应用工程师来说&#xff0c;可能并不太清楚电尺寸的概念。因为要谈到电尺寸就要考虑电信号的传播速度&#xff0c;一般会在高频、超高频电路中有所涉及&#xff0c;而大部分硅基…

【优秀python系统毕设】基于Python flask的气象数据可视化系统设计与实现,有LSTM算法预测气温

第一章 绪论 1.1 研究背景 在当今信息爆炸的时代&#xff0c;气象数据作为重要的环境信息资源&#xff0c;扮演着关键的角色。然而&#xff0c;传统的气象数据呈现方式存在信息量庞大、难以理解的问题&#xff0c;限制了用户对气象信息的深入理解和利用。因此&#xff0c;基…

[算法题]非对称之美

题目链接: 非对称之美 题目要求求最长非回文子字符串的长度, 那么如果字符串本身不是回文串, 那么长度就是该字符串本身的长度: 如果字符串本身是一个回文串, 那么只需把该字符串去掉一个字母后, 该字符串就不是回文串了, 长度也就是原本的长度减 1, 即: 所以想要求最长非回文…

BCH码误码率ber性能仿真(MATLAB)

BCH码 不同于奇偶校验码只能检验数据传输是否出错&#xff0c;BCH码可以实现对数据的检验和纠错 BCH&#xff08;n&#xff0c;k&#xff09;中的n代表总码元&#xff0c;k代表有效码元&#xff0c;相应的n-k即代表纠错码元 本文着重比较分析BCH(255,207),BCH(255,131),BCH(255…

iOS 自定义 仿苹果地图 半屏滑动效果控件

前言 在前一篇文章AI编程探索- iOS 实现类似苹果地图 App 中的半屏拉起效果我们通过三方库实现了这个功能。可是我发现这个三方不能加阴影效果。也许是我不知道怎么加吧&#xff01;于是只有自己搞咯&#xff01; 拆解功能 这功能给人在感觉上&#xff0c;有点麻烦&#xff0…

奇怪的Excel单元格字体颜色格式

使用VBA代码修改单元格全部字符字体颜色是个很简单的任务&#xff0c;例如设置A1单元格字体颜色为红色。 Range("A1").Font.Color RGB(255, 0, 0)有时需要修改部分字符的颜色&#xff0c;如下图所示&#xff0c;将红色字符字体颜色修改为蓝色。代码将会稍许复杂&am…

【MySQL进阶之路 | 高级篇】MVCC三剑客:隐藏字段,Undo Log,ReadView

1. 再谈隔离级别 我们知道事务有四个隔离级别&#xff0c;可能存在三种并发问题&#xff1a; 在MySQL中&#xff0c;默认的隔离级别是可重复读&#xff0c;可以解决脏读和不可重复读的问题&#xff0c;如果仅从定义的角度来看&#xff0c;它并不能解决幻读问题。如果我们想要解…