计算机网络—TCP协议详解:特性、应用(1)

 

                                        🎬慕斯主页修仙—别有洞天

                                       ♈️今日夜电波:マリンブルーの庭園—ずっと真夜中でいいのに。

                                                           0:34━━━━━━️💟──────── 3:34
                                                                🔄   ◀️   ⏸   ▶️    ☰  

                                 💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


 

目录

socket在TCP中主要接口的介绍

listen

accept

connect

setsockopt

telnet工具

Telnet命令行交互

什么是TCP协议?

通过程序来理解TCP

单进程版服务器

客户端

多进程版服务器

多线程版服务器

线程池版服务器

一些杂谈

守护进程化

前置知识

什么是守护进程

setsid

写出一个符合上述服务端的Daemon

系统中提供的Daemon


 

socket在TCP中主要接口的介绍

listen

listen函数是在网络编程中常用的一个函数,特别是在使用套接字(sockets)进行服务器端的编程时。这个函数的主要作用是使套接字进入监听状态,等待客户端的连接请求。

函数原型通常如下(在C语言中):

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

参数说明:

  • sockfd:这是你要监听的套接字的文件描述符。这个套接字通常是之前通过socket函数创建的,并且已经绑定到了一个特定的地址和端口上(通过bind函数)。
  • backlog:这是等待连接队列的最大长度。换句话说,这是可以等待处理的未完成连接请求的最大数量。当队列满时,新的连接请求可能会被拒绝。这个值通常设置为5或更大,但具体的值取决于你的应用需求和系统限制。

返回值:

  • 如果函数成功,则返回0。
  • 如果函数失败,则返回-1,并设置全局变量errno以指示错误。

在使用listen函数之前,你通常需要按照以下步骤设置套接字:

  1. 调用socket函数创建一个新的套接字。
  2. 调用bind函数将套接字绑定到一个特定的地址和端口。
  3. 调用listen函数使套接字进入监听状态。

        然后,你可以使用accept函数来接受客户端的连接请求。当accept函数被调用时,它会阻塞(除非设置了非阻塞模式),直到有一个客户端连接请求到达。一旦有请求到达,accept函数就会返回一个新的套接字描述符,这个描述符可以用于与客户端进行通信。

        需要注意的是listen函数只是使套接字进入监听状态,它并不会接受任何连接请求。实际的连接请求是由accept函数处理的。

accept

   accept函数是Linux网络编程中用于服务器端的一个关键函数,它的主要作用是接受客户端的连接请求,并返回一个新的套接字用于和客户端通信。(这个套接字可以理解成什么呢?我们可以理解为文件系统中的fd,也就是文件标识符,我们后续就是根据这个来像文件一样使用write、read进行通信的!)

函数的原型通常如下:

#include <sys/types.h>  
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  • sockfd:这是用于监听客户端连接请求的套接字文件描述符。这个套接字应该是之前通过socket函数创建的,并且通过bindlisten函数进行了绑定和监听设置。
  • addr:这是一个指向struct sockaddr的指针,用于保存客户端的套接字地址结构。这个结构包含了客户端的IP地址和端口号等信息。如果调用者对此信息不感兴趣,可以将此参数设为NULL。
  • addrlen:这是一个指向socklen_t变量的指针,用于传入addr结构的大小,并在函数返回时填充实际接收到的地址结构的大小。

返回值:

  • 如果成功,accept函数会返回一个新的套接字文件描述符,这个描述符可以用来与客户端进行通信。
  • 如果失败,则返回-1,并设置全局变量errno以指示错误。

        调用accept函数是一个阻塞的过程。如果没有客户端连接请求到来,accept函数会一直阻塞,直到有客户端发起连接请求并成功完成三次握手。一旦有客户端连接请求成功,accept函数会返回一个新的套接字描述符,这个套接字描述符专用于与刚刚连接的客户端进行通信。同时,客户端的地址信息会被填充到addr所指向的结构中。

        在实际编程中,accept函数通常与bindlisten等函数结合使用,以完成一个完整的服务器端程序。在服务器开始监听客户端连接之前,需要先调用bind函数将套接字绑定到一个特定的地址和端口,然后调用listen函数使套接字进入监听状态。当有客户端连接请求到来时,再调用accept函数接受连接,并返回新的套接字用于通信。

        需要注意的是,如果套接字被设置为非阻塞模式,并且没有未完成的连接请求队列,accept函数会立即返回并设置errnoEAGAIN表示资源暂时不可用。为了处理这种情况,可以使用selectpoll函数来检查套接字上是否有传入的连接请求。

connect

    connect函数是编程中用于建立连接或关联的常用函数之一。在网络编程中,connect函数主要用于客户端与服务器之间的连接建立。它的基本作用是在两个组件(在这种情况下,通常是客户端和服务器)之间建立通信通道,以便在不同组件之间传递数据或实现功能。

函数的原型通常如下:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:这是由socket函数创建的套接字文件描述符,代表客户端的套接字。
  • addr:这是一个指向struct sockaddr的指针,包含了要连接的服务器的地址信息,包括IP地址和端口号。
  • addrlen:这是一个整数,表示addr参数指向的地址结构的大小,通常通过sizeof(struct sockaddr_in)或类似表达式获得。

返回值:

  • 如果连接成功,connect函数返回0。
  • 如果连接失败,connect函数返回-1,并设置全局变量errno以指示错误原因。

        在客户端编程中,connect函数通常用于主动发起与服务器的连接请求。一旦连接建立成功,客户端和服务器就可以通过各自的套接字进行数据传输和通信。

        需要注意的是connect函数是阻塞的,意味着它会等待直到连接建立成功或发生错误。如果服务器不可达或拒绝连接,connect函数将返回错误。

        与connect函数相对应的是服务器端的accept函数,它用于接受客户端的连接请求并返回一个新的套接字描述符用于通信。

setsockopt

    setsockopt函数是一个系统调用,主要用于在套接字层面上设置一些参数,以调整套接字的行为。它允许程序员配置套接字的各种属性。

函数的基本原型如下:

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数说明:

  • sockfd:标识一个套接字的描述字,即指向一个打开的套接口。
  • level:选项定义的层次,支持以下值:
    • SOL_SOCKET:基本套接口
    • IPPROTO_IP:IPv4套接口
    • IPPROTO_IPV6:IPv6套接口
    • IPPROTO_TCP:TCP套接口
  • optname:需设置的选项名称。
  • optval:指向存放选项待设置的新值的缓冲区。这个参数是一个指向变量的指针,变量的类型可以是整形、套接口结构或其他结构类型,如linger{}timeval{}
  • optlenoptval缓冲区长度,表示optval的大小。

返回值:

  • 如果设置选项成功,setsockopt返回0。
  • 如果设置选项失败,setsockopt返回-1,并且会设置errno来表示具体的错误原因。

        在编程中,使用setsockopt函数可以针对特定的套接字进行各种配置,例如设置套接字的发送和接收缓冲区大小、启用或禁用广播等。通过合理地设置这些选项,可以优化网络性能,提高应用程序的可靠性和效率。

        需要注意的是setsockopt函数的具体用法可能因操作系统和编程环境的不同而有所差异。因此,在使用该函数时,建议查阅相关的文档和资料,以确保正确理解和使用该函数。

telnet工具

        在Linux系统中,telnet是一个用于远程登录到另一台计算机或设备的命令行工具。它使用TCP协议,并允许用户通过命令行界面与远程系统进行交互。以下是关于Linux下telnet使用的详细解释:

安装telnet

        首先,你需要确保你的Linux系统已经安装了telnet客户端。你可以使用包管理器来安装它。例如,在基于Debian的系统(如Ubuntu)上,你可以使用以下命令安装:

sudo apt-get install telnet

在基于Red Hat的系统(如CentOS或Fedora)上,你可以使用:

sudo yum install telnet

或者,如果你使用的是dnf

sudo dnf install telnet

使用telnet连接到远程主机:

一旦安装完成,你可以使用以下命令格式连接到远程主机:

telnet 远程主机IP地址 端口号

通常,telnet使用TCP端口23,因此如果你没有指定端口号,它将尝试连接到该端口。例如:

telnet 192.168.1.100

这将尝试通过TCP端口23连接到IP地址为192.168.1.100的远程主机。

        在连接成功后,通常会提示你使用^[来进入,进入后可以向对应的TCP程序发送字节流。想要退出的话可以使用quit或者^]。

Telnet命令行交互

        一旦连接建立成功,你就可以在telnet命令行界面中输入命令并与远程系统进行交互。这通常涉及到登录到远程系统(如果需要的话),然后执行各种命令。通常使用

Telnet选项和命令:

   telnet命令本身也支持一些选项和命令,用于控制telnet会话的行为。你可以通过查看telnet的手册页来获取这些选项的完整列表:

man telnet

一些常用的telnet选项包括:

  • -l--login:自动登录到远程系统。
  • -e--escape:设置转义字符,默认为“^]”。
  • -K--keep-alive:使用TCP keepalive。

注意事项:

  1. 安全性:telnet在传输数据时并不加密,因此它被认为是不安全的,特别是在公共网络上。SSH(安全外壳协议)是一个更安全的替代方案,它提供了加密的数据传输。
  2. 防火墙和网络配置:确保远程主机的防火墙和网络配置允许telnet连接。在某些情况下,你可能需要在远程主机上配置telnet服务以接受连接。
  3. 终端类型:telnet会话可能需要指定终端类型(例如vt100)。这可以通过telnet的-t选项或远程系统的配置来完成。

什么是TCP协议?

        TCP协议,全称传输控制协议(Transmission Control Protocol),是一种面向连接的、可靠的、基于字节流的传输层通信协议。该协议由IETF的RFC 793定义,主要用于在主机间建立一个虚拟连接,以实现高可靠性的数据包交换。

TCP协议的工作原理主要可以分为以下三个步骤:

  1. 建立连接:通信双方首先要建立TCP连接。客户端发送一个连接请求(SYN包)到服务器,并等待服务器的确认(ACK包)。服务器收到客户端的连接请求后,发送确认和自己的连接请求(SYN/ACK包)给客户端。客户端再发送确认(ACK包),至此连接建立完成。
  2. 数据传输:一旦连接建立,通信双方可以开始传输数据。发送方将数据划分成小块(称为报文段),并添加头部和校验等信息,然后通过TCP协议将这些报文段发送给接收方。接收方收到报文段后,校验数据的完整性,并把它们重新组装成完整的数据流。
  3. 可靠传输:TCP通过各种机制来保证数据的可靠传输。它使用序列号对每个报文段进行标记,并确保接收方按照正确的顺序进行数据重组。如果发送方发现某个报文段丢失或未收到确认,它会重新发送该报文段。另外,TCP也采用滑动窗口的机制,允许发送方连续发送多个报文段,而不需要等待确认。

        TCP协议在网络通信中扮演着重要角色,尤其在需要高可靠性的数据传输场景中。然而,也需要注意到TCP协议在某些情况下可能存在性能问题,例如在网络拥塞或高延迟环境中,TCP的流量控制机制可能导致传输效率降低。因此,在选择使用TCP协议时,需要根据具体的应用场景和需求进行权衡。

        重点总结如下:传输层协议、有连接、可靠传、输面向字节流

 

通过程序来理解TCP

单进程版服务器

        这只是一个简单的单进程TCP,按照TCP的规则,首先获取对应IPV4协议和TCP协议的socket,然后创建对应的sockaddr_in用于后续的bind操作,OS在bind了对应的端口和地址后,与UDP不同的是,TCP需要通过listen使套接字进入监听状态,再通过accept来接受连接请求,需要注意accept会返回链接的sockfd,后续我们就可以根据这个sockfd进行相应的write和read的操作了(就像文件一样)。

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
using namespace std;

const int defaultfd = -1;
const int defaultport = 8888;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer
{
public:
    TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip)
        : _listensock(defaultfd), _ip(ip), _port(port)
    {
    }

    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg.LogMessage(Info, "create socket success, _listensock: %d \n", _listensock);

        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)

        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg.LogMessage(Info, "bind socket success, _listensock: %d \n", _listensock);

        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensock,backlog)<0)
        {
            lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg.LogMessage(Info, "listen socket success, _listensock: %d \n", _listensock);

    }

    void Start()
    {
        lg.LogMessage(Info, "tcpServer is running.... \n");
        for(;;)
        {
             struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd=accept(_listensock,(struct sockaddr *)&client,&len);
             if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            uint16_t clientport=ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            // version 1 -- 单进程版
            Service(sockfd, clientip, clientport);
            close(sockfd);
        }
    }

    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
        // 测试代码
        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 = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            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;
            }
        }
    }
    ~TcpServer() {}


private:
    int _listensock;
    uint16_t _port;
    string _ip;
};

客户端

        客户端同UDP一样不需要我们显示的bind,OS会自动帮我们bind,随机端口。那么是在什么时候进行绑定的呢?在客户端发起connect的时候,进行自动随机bind。客户端中的connect就是为了让服务端的accept响应建立联系的函数。后续我们就可以根据经过socket函数获得的sockfd进行相应的write和read的操作了(就像文件一样,这个socketfd注意是自己(客户端)使用socket函数获得的)。需要注意:这里我们做了重连的处理,也就是do while循环以及其中的内容。

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

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        int cnt = 5;
        int isreconnect = false;
        int sockfd = 0;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }
        do
        {
            // tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
            // 客户端发起connect的时候,进行自动随机bind
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
                sleep(2);
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        // while (true)
        // {
            std::string message;
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);

            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                std::cerr << "write error..." << std::endl;
                // break;
            }

            char inbuffer[4096];
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << inbuffer << std::endl;
            }
            else{
                // break;
            }
        // }
        close(sockfd);
    }

    return 0;
}

多进程版服务器

        只是改动了一些代码,加入了子进程的创建以及进程等待的少量代码。需要特别注意代码中的注释部分,该部分保证了是多进程而不是还为单进程。当然也可以不做上述的步骤,更改SIGCHLD信号:signal(SIGCHLD, SIG_IGN);让父进程不等待即可。

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
using namespace std;

const int defaultfd = -1;
const int defaultport = 8888;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer
{
public:
    TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip)
        : _listensock(defaultfd), _ip(ip), _port(port)
    {
    }

    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg.LogMessage(Info, "create socket success, _listensock: %d \n", _listensock);

        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)

        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg.LogMessage(Info, "bind socket success, _listensock: %d \n", _listensock);

        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensock,backlog)<0)
        {
            lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg.LogMessage(Info, "listen socket success, _listensock: %d \n", _listensock);

    }

    void Start()
    {
        lg.LogMessage(Info, "tcpServer is running.... \n");
        for(;;)
        {
             struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd=accept(_listensock,(struct sockaddr *)&client,&len);
             if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            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)
            {
                //child
                close(_listensock);
                if(fork() > 0) exit(0); //这一步做出后,子进程会马上退出被父进程wait到,父进程可以继续进行循环
                Service(sockfd, clientip, clientport); //孙子进程, system 领养
                close(sockfd);
                exit(0);
            }
            close(sockfd);
            // father
            pid_t rid = waitpid(id, nullptr, 0);
            (void)rid;

        }
    }

    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
        // 测试代码
        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 = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            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;
            }
        }
    }
    ~TcpServer() {}


private:
    int _listensock;
    uint16_t _port;
    string _ip;
};

多线程版服务器

        要实现多线程版,我们要特别用到pthread_detach这个函数。他会将指定的线程标记为可分离的。当一个线程被标记为可分离时,该线程的资源(如堆栈和线程描述符)会在该线程退出时自动被系统回收,而不需要等待其他线程使用pthread_join函数来释放它们。接着对于线程传入的函数Routine我们要特别注意将this指针也传入,为什么呢?因为线程需要用static函数共享资源的缘故。因此需要额外构造一个ThreadData类来储存isockfd、clientip、clientport、this指针。

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
using namespace std;

const int defaultfd = -1;
const int defaultport = 8888;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip)
        : _listensock(defaultfd), _ip(ip), _port(port)
    {
    }

    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg.LogMessage(Info, "create socket success, _listensock: %d \n", _listensock);

        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)

        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg.LogMessage(Info, "bind socket success, _listensock: %d \n", _listensock);

        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensock,backlog)<0)
        {
            lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg.LogMessage(Info, "listen socket success, _listensock: %d \n", _listensock);

    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//???
        delete td;
        return nullptr;
    }

    void Start()
    {
        lg.LogMessage(Info, "tcpServer is running.... \n");
        for(;;)
        {
             struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd=accept(_listensock,(struct sockaddr *)&client,&len);
             if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            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, clientip, clientport, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);

        }
    }

    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
        // 测试代码
        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 = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            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;
            }
        }
    }
    ~TcpServer() {}


private:
    int _listensock;
    uint16_t _port;
    string _ip;
};

线程池版服务器

        需要额外传入Task的任务,这个自己决定,对于线程池则是之前实现的单例模式的线程池,该线程池默认创建10个线程,通过Push这个接口传入任务,后续线程池会自动处理任务。

ThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) // ???
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

TcpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace std;

const int defaultfd = -1;
const int defaultport = 8888;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer( const uint16_t &port = defaultport,const string &ip = defaultip)
        : _listensock(defaultfd), _ip(ip), _port(port)
    {
    }

    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            lg.LogMessage(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg.LogMessage(Info, "create socket success, _listensock: %d \n", _listensock);

        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg.LogMessage(Info, "bind socket success, _listensock: %d \n", _listensock);

        // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensock,backlog)<0)
        {
            lg.LogMessage(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg.LogMessage(Info, "listen socket success, _listensock: %d \n", _listensock);

    }

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//???
        delete td;
        return nullptr;
    }

    void Start()
    {
        lg.LogMessage(Info, "tcpServer is running.... \n");
        ThreadPool<Task>::GetInstance()->Start();
        for(;;)
        {
             struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd=accept(_listensock,(struct sockaddr *)&client,&len);
             if (sockfd < 0)
            {
                lg.LogMessage(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            uint16_t clientport=ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            // version 4 --- 线程池版本
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);

        }
    }

    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
        // 测试代码
        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 = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            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;
            }
        }
    }
    ~TcpServer() {}


private:
    int _listensock;
    uint16_t _port;
    string _ip;
};

一些杂谈

127.0.0.1本地回环

TCP协议是面相字节流的:可以用read、write。读写网络就像读、写文件一般。

为什么TCP、UDP中对于端口以及IP等等需要设置为网络字节序,而写入读出的信息却不用?端口号和IP地址需要使用网络字节序是为了确保在不同计算机系统之间能够被正确识别,而实际传输的数据则依赖于应用层的处理,不强制要求转换为网络字节序。这是由网络协议的设计和网络通信的需求所决定的。

signal(SIGPIPE, SIG_IGN);常常用于socket在信息被write或者read是socketfd被提前关闭(这个就同进程中的管道一样,可能会导致进程被杀死)。SIG_IGN就是为了防止进程被直接杀死。

守护进程化

前置知识

我们通常会通过ps -axj命令查看所有的进程,如下先认识一下几个概念:

session的概念:session(会话)是用户与操作系统之间建立的一个交互环境。当用户通过命令行工具或图形界面登录到Linux系统后,系统会为该用户创建一个独立的会话。这个会话包含了用户当前的工作环境和正在运行的程序,为用户提供了一个独立且隔离的工作空间。可以理解为我们通过xshell打开的一个新会话,我们每打开一个新的会话就会多一个型的session,那么session id则表示在同一个会话里面的进程!

PGID的概念:终端进程组ID通常指的是与某个特定终端(或称为控制终端、tty)相关联的前台进程组的ID。在终端中,前台进程组是当前用户正在与之交互的进程组,而后台进程组则是在后台运行的进程组。当用户在一个终端中启动多个进程时,这些进程可能属于不同的进程组,但只有一个进程组会成为前台进程组。

TTY的概念:TTY是Teletype的缩写,它指的是由虚拟控制台、串口以及伪终端设备组成的终端设备。这些设备提供了用户与计算机进行交互的接口。通过TTY设备,用户可以输入命令并查看计算机的输出。

守护进程实际的特点:

  • 守护进程基本上都是以超级用户启动( UID 为 0 )
  • 没有控制终端( TTY 为 ?)
  • 终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)

组长的概念:进程组组长的进程ID(PID)与进程组ID(PGID)是相同的。这个特性使得系统可以方便地识别和管理进程组组长。进程组组长负责管理组内的其他进程,包括创建新的进程组、将其他进程加入到自己的进程组中,以及对组内的进程进行协调和控制。而进程组内其他成员的PGID则为组长的PID。作为组长如果退出了,那么他的组员是会出错误的!

守护进程实际上就是脱离了session,变成自成进程组、自成会话的进程!他的本质实际上就是孤儿进程的原理,利用fork()创建一个子进程(为什么呢?如果要调用setsid则不能作为组长!),父进程退出,子进程调用setsid()创建一个新的会话并成为会话领导,子进程则会被OS所领养。

介绍一个文件夹:/dev/null

/dev/null 是 Unix 和类 Unix 系统(如 Linux)中的一个特殊设备文件,它被称为“空设备”或“空文件”。当你向 /dev/null 写入数据时,数据会被丢弃,就像被送入了一个黑洞。而从 /dev/null 读取数据则什么也得不到(立即返回 EOF,即文件结束标记)。

/dev/null 常被用于以下几个目的:

  1. 丢弃输出:当你运行一个命令或程序,并且不想看到其输出时,你可以将其重定向到 /dev/null。例如,command > /dev/null 会将标准输出重定向到 /dev/null,从而丢弃所有输出。如果你还想丢弃错误输出,可以使用 command > /dev/null 2>&1
  2. 提供一个空的文件输入:当某个命令或程序需要一个文件输入,但你希望提供一个空的内容时,可以使用 /dev/null。例如,command < /dev/null 会将标准输入设置为来自 /dev/null,这样命令就不会从任何地方读取到数据。

/dev/null 在 shell 脚本编程和系统管理中非常有用,因为它提供了一个简单的方法来丢弃不需要的输出或提供一个空的输入源。

简单来说,/dev/null 是一个数据“黑洞”,你可以向它写入任何你想要丢弃的数据,或者从它读取(但什么也得不到)。

什么是守护进程

        守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lpd等(这里的结尾字母d就是Daemon的意思)。

        在创建守护进程时,一个典型的步骤是父进程先创建子进程,然后父进程退出,子进程继续执行。这样子进程就成为了一个守护进程。守护进程会在后台运行,不受前台用户的影响,即使终端被关闭,守护进程也会继续运行。

        守护进程的特点主要包括:

  1. 在后台运行。
  2. 独立于控制终端(tty)。也就是说,在终端控制的用户无法直接控制它。
  3. 周期性地执行任务或者等待处理某些事件。
  4. 一般不会随着用户的退出而结束,而是会一直运行,直到系统关闭或者接收到特定的终止信号。

setsid

    setsid 是在 Unix 和 Linux 环境中用于处理进程和会话的一个重要工具。它既可以作为系统调用在程序中使用,也可以作为命令行工具来执行。下面是对 setsid 的详细解释:

作为系统调用

        当 setsid 作为系统调用时,它允许一个进程创建一个新的会话,并成为该会话的领导。这在创建守护进程时尤其有用,因为守护进程需要在后台运行,且不受前台用户或终端会话的控制。

  • 调用条件:只有当前进程不是会话领导时,才能成功调用 setsid。如果已经是会话领导,则调用会失败,并返回错误。
  • 效果:成功调用 setsid 后,当前进程会成为新会话的会话领导和进程组领导,且新会话没有控制终端。此外,新会话的 ID 和进程组 ID 将与调用进程的 PID 相同。
  • 用途:常用于创建守护进程。守护进程是在系统启动时开始运行,并在系统关闭时终止的进程。它们通常在后台运行,执行一些系统级任务,如监听网络请求、管理硬件资源等。

作为命令行工具

setsid 也可以作为命令行工具来使用,用于在后台启动一个进程,并将其与当前终端会话分离。

  • 语法setsid [选项] 命令 [参数]
    • 选项:用于控制 setsid 的行为,如 -w 表示等待命令完成后再返回,-c 用于指定要在后台运行的命令等。
    • 命令:要在后台运行的程序。
    • 参数:传递给命令的参数。
  • 用途:常用于在终端关闭或会话结束后,仍需要继续运行的进程。例如,你可能使用 setsid 来启动一个长时间运行的任务,如备份操作或网络服务器,以确保即使你关闭了终端,这些任务也会继续执行。

示例

        在编程中,你可能会看到类似以下的代码片段,用于创建一个守护进程:

pid_t pid = fork(); // 创建一个子进程
if (pid < 0) {
    // 处理 fork 失败的情况
} else if (pid > 0) {
    // 父进程退出,子进程继续执行
    exit(0);
} else {
    // 子进程中调用 setsid,创建新的会话并成为会话领导
    if (setsid() < 0) {
        // 处理 setsid 调用失败的情况
    }
    // 继续执行守护进程的初始化操作和任务...
}

        在命令行中,你可能会使用类似以下的命令来在后台启动一个程序:

setsid myprogram arg1 arg2 &

        这条命令会在后台启动 myprogram,并将其与当前终端会话分离。即使你关闭了终端,myprogram 也会继续运行。

写出一个符合上述服务端的Daemon

#pragma once 
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile="/dev/null";

void daemon(const std::string &cwd="")
{
    // 1. 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 将自己变成独立的会话
    if(fork()>0) exit(0);

    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());

     // 4. 标准输入,标准输出,标准错误重定向至/dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }

}

系统中提供的Daemon

        在 C 语言中,你可以使用 daemon 函数来将当前进程转换为守护进程。daemon 函数定义在 <unistd.h> 头文件中,其原型如下:

#include <unistd.h>

int daemon(int nochdir, int noclose);

daemon 函数接受两个整数参数:

  • nochdir:如果此参数非零,则 daemon 不会将当前工作目录更改为根目录(/)。
  • noclose:如果此参数非零,则 daemon 不会关闭标准输入、标准输出和标准错误输出。

daemon 函数的返回值是一个整数:

  • 如果成功,返回 0。
  • 如果失败,返回 -1,并设置 errno 以指示错误。

下面是一个简单的示例,展示了如何使用 daemon 函数:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main() {
    pid_t pid;
    
    // 创建守护进程
    if (daemon(0, 0) == -1) {
        perror("daemon failed");
        exit(EXIT_FAILURE);
    }
    
    // 由于守护进程已经关闭了标准输入、输出和错误输出,
    // 如果需要记录日志,可以重新打开或重定向到某个文件。
    int fd = open("/tmp/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }
    
    // 将标准输出和标准错误输出重定向到日志文件
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    
    // 守护进程的主循环
    while (1) {
        sleep(10); // 休眠10秒
        printf("Daemon is still alive...\n"); // 写入日志
        fflush(stdout); // 刷新输出缓冲区
    }
    
    close(fd); // 关闭文件描述符(实际上这个代码不会被执行到,因为有一个无限循环)
    return 0;
}

        在上面的示例中,我们首先调用 daemon(0, 0) 来创建守护进程。然后,我们打开一个日志文件,并将标准输出和标准错误输出重定向到该文件。最后,守护进程进入一个无限循环,每隔10秒向日志文件写入一条消息。请注意,由于守护进程在后台运行并且已经脱离了控制终端,因此它们不会响应终端信号(如 SIGINT)。如果你需要优雅地关闭守护进程,你应该实现一种机制来接收和处理适当的信号(如 SIGTERM)。

 


                       感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java中常见的锁策略

目录 乐观锁 vs 悲观锁 悲观锁: 乐观锁&#xff1a; 重量级锁 vs 轻量级锁 ⾃旋锁&#xff08;Spin Lock&#xff09; 公平锁 vs 非公平锁 可重⼊锁 vs 不可重入锁 读写锁 乐观锁 vs 悲观锁 悲观锁: 总是假设最坏的情况&#xff0c;每次去拿数据的时候都认为别…

3d整体模型怎么拆图---模大狮模型网

3D整体模型怎么拆图 一、模型准备 在进行3D整体模型的拆图之前&#xff0c;首先需要确保模型的构造清晰明了&#xff0c;各个部件之间的连接关系明确&#xff0c;并且模型的层级结构已经建立完成。 二、确定拆图方式 拆解方向&#xff1a;根据模型的结构和设计需求&#xff…

教你用Fiddler捕获HTTPS请求

安装Fiddler 这里不特别说明了&#xff0c;网上搜索一大把&#xff0c;根据安装引导一步步安装即可。&#xff08;这里采用的是fiddler v4.6&#xff09; 配置Fiddler 1、打开fiddler配置Tools –>Telerik Fiddler Options。 2、打开HTTPS配置项&#xff0c;勾选“Captur…

Leetcode-2580-统计将重叠区间合并成组的方案数-c++

题目详见https://leetcode.cn/problems/count-ways-to-group-overlapping-ranges/ 题目要求将最后的ranges分为两个组。也就是说当你的ranges已经满足题目要求的时候&#xff0c;这两个组怎么分是随意的&#xff0c;这里也就引出了 2 k 2^k 2k 的由来&#xff0c;其实就是每组…

Leetcode-2810-故障键盘-c++

题目详见https://leetcode.cn/problems/faulty-keyboard/ 题解 这道题的关键是如何合理地使用STL&#xff0c;毕竟是一道简单题。 之前常用到的Vector容器是单向开口的连续内存空间 deque则是一种双向开口的连续线性空间&#xff0c;又称双端动态数组。所谓的双向开口&#x…

Windows Server 2022 使用ApacheDS用户远程桌面登录服务器

Windows Server 2022 使用ApacheDS用户远程桌面登录服务器 1、接上篇 Windows Server 2022 使用ApacheDS用户认证 使用Administrator用户远程登录192.168.1.100windows server&#xff0c;打开pGina软件 2、输入刚刚在ApacheDS中的新添加的用户测试一下&#xff0c;会自动添加…

200个有趣的HTML前端游戏项目合集(持续更新中)

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

[实时流基础 flink] 窗口

在批处理统计中&#xff0c;我们可以等待一批数据都到齐后&#xff0c;统一处理。但是在实时处理统计中&#xff0c;我们是来一条就得处理一条&#xff0c;那么我们怎么统计最近一段时间内的数据呢&#xff1f;引入“窗口”。 文章目录 6.1 窗口的概念6.2 窗口的分类**1&#x…

让接口自动化测试更简单

HTTP 接口测试很简单&#xff0c;不管工具、框架、还是平台&#xff0c;只要很的好的几个点就是好工具。 测试数据问题&#xff1a;比如删除接口&#xff0c;重复执行还能保持结果一致&#xff0c;必定要做数据初始化。接口依赖问题&#xff1a;B 接口依赖 A 的返回值&#xf…

新model开发记录

模型使用 -- 用blender导出为 fbx &#xff0c;修改渲染方式&#xff08;点击模型->Materials->Extract Materials(将材质从fbx中 单独提取出来了)->Materials 选择 Shader -> SimpleURPToonLitExample 点开脸的材质&#xff0c;勾选第一条&#xff09; 解决角色…

【C++的奇迹之旅(二)】C++关键字命名空间使用的三种方式C++输入输出命名空间std的使用惯例

文章目录 &#x1f4dd;前言&#x1f320; C关键字(C98)&#x1f309; 命名空间&#x1f320;命名空间定义&#x1f309;命名空间使用 &#x1f320;命名空间的使用有三种方式&#xff1a;&#x1f309;加命名空间名称及作用域限定符&#x1f320;使用using将命名空间中某个成员…

MATLAB绘制堆叠填充图--巧用句柄

MATLAB绘制堆叠填充图–巧用句柄 目录 MATLAB绘制堆叠填充图--巧用句柄1. 主要原理讲解1.1 主要函数1.2 句柄原理 2. 绘图示例2.1 准备数据2.2 绘制堆叠填充图-使用句柄控制图形属性2.3 设置填充颜色和样式2.4 添加标题和标签2.5 绘图效果 3. 结语 堆叠填充图是一种常见的数据可…

StringBuffer与StringBuilder

1.区别 (1). String : 不可变字符序列. (2). StringBuffer : 可变字符序列.线程安全&#xff0c;但效率低. (3). StringBuilder : 可变字符序列.线程不安全&#xff0c;但效率高. 既然StringBuffer与StringBuilder都是可变字符序列&#xff0c;但二者咋区分开呢&#xff1f…

Dimitra:基于区块链、AI 等前沿技术重塑传统农业

根据 2023 年联合国粮食及农业组织&#xff08;FAO&#xff09;、国际农业发展基金&#xff08;IFAD&#xff09;等组织联合发布的《世界粮食安全和营养状况》报告显示&#xff0c;目前全球约有 7.35 亿饥饿人口&#xff0c;远高于 2019 年的 6.13 亿&#xff0c;这意味着农业仍…

碳素光线疗法与宠物健康

碳素光线与宠物健康 生息在地球上的所有动物、在自然太阳光奇妙的作用下、生长发育。太阳光的能量使它们不断进化、繁衍种族。现在、生物能够生存、全仰仗于太阳的光线。太阳光线中、包含有动物健康所需要的极为重要的波长。因此、和户外饲养的动物相比、在室内喂养的观赏动物、…

【机器学习300问】59、计算图是如何帮助人们理解反向传播的?

在学习神经网络的时候&#xff0c;势必会学到误差反向传播&#xff0c;它对于神经网络的意义极其重大&#xff0c;它是训练多层前馈神经网络的核心算法&#xff0c;也是机器学习和深度学习领域中最为重要的算法之一。要正确理解误差反向传播&#xff0c;不妨借助一个工具——计…

初始C语言最后一章《编译、链接与预处理详解》

前言 感谢老铁们的陪伴和支持&#xff0c;初始C语言专栏在本章内容也是要结束了&#xff0c;这创作一路下来也是很不容易&#xff0c;如果大家对 Java 后端开发感兴趣&#xff0c;欢迎各位老铁来我的Java专栏&#xff01;当然了&#xff0c;我也会更新几章C语言实现简单的数据结…

从C到C++:深入理解基础语法差别

C基础语法讲解 前言1.输入输出2.命名空间2.1命名空间的理解&#xff1a;2.2命名空间的使用方式 3.缺省参数3.1概念&#xff1a;3.2分类&#xff1a;半缺省函数注意事项&#xff1a; 3.3使用案例&#xff1a;顺序表的初始化 4.函数重载4.1参数重载类型类型&#xff1a; 5.引用5.…

C++刷题篇——05静态扫描

一、题目 二、解题思路 注意&#xff1a;注意理解题目&#xff0c;缓存的前提是先扫描一次 1、使用两个map&#xff0c;两个map的key相同&#xff0c;map1&#xff1a;key为文件标识&#xff0c;value为文件出现的次数&#xff1b;map2&#xff1a;key为文件标识&#xff0c;va…

性能测试必备docker环境准备

在当今快速发展的软件开发领域&#xff0c;docker作为一种开源的容器化技术&#xff0c;已经成为提高应用部署效率、实现快速、一致的环境配置的重要工具。而性能测试&#xff0c;则是确保软件应用在各种负载和压力条件下表现良好的关键步骤。二者的结合&#xff0c;为软件开发…