【Linux网络】 网络套接字(三)socket编程_TCP网络程序

news2024/12/26 10:41:50

目录

  • TCP网络程序
    • 服务端
      • 创建套接字并绑定
      • 服务端监听
      • 服务端获取连接
      • 服务器处理请求
    • 客户端
      • 客户端创建套接字
      • 客户端连接服务器
      • 客户端发起请求
      • 测试
  • 服务器存在的问题
    • 多进程版的TCP网络程序
    • 多线程版的TCP网络程序
    • 线程池版的TCP网络程序
  • TCP网络程序总结图

TCP网络程序

服务端

创建套接字并绑定

TCP创建套接字并绑定的过程和UDP创建套接字并绑定的过程一样,这里对一些参数进行说明。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
class TcpServer
{
public:
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }
    }
private:
    int _listensock;
    in_port_t _port;
};

TCP服务器在调用socket函数创建套接字时,参数设置如下:

  • 协议家族选择AF_INET,因为我们要进行的是网络通信。
  • 创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器,SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
  • 协议类型默认设置为0即可

服务端监听

UDP服务器的初始化只需要上述的创建套接字并绑定,因为UDP是面向数据报服务的,负责把消息传递即可。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前需要进行连接,才能进行通信。

因此TCP服务端需要时刻监听客户端是否发来连接请求,那么我们上面创建的套接字需要用来监听。

listen 函数

设置套接字为监听状态的函数叫做listen,该函数的函数原型如下:

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符。
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。

返回值说明:

  • 监听成功返回0,监听失败返回-1,同时错误码会被设置

服务器监听

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#define BACKLOG 5
class TcpServer
{
public:
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }

        if(listen(_listensock,BACKLOG) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(-3);
        }
    }
private:
    int _listensock;//监听套接字
    in_port_t _port;
};

TCP服务端的初始化与UDP服务端初始化相比,TCP需要监听服务。

服务端获取连接

上面的初始化工作已经完成,现在开始连接工作。
在TCP服务端与客户端进行通信之前,服务器需要先获取到客户端的连接请求。

accept函数

获取连接的函数叫做accept,该函数的函数原型如下:

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

参数说明:

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

注意这里的返回值是套接字的文件描述符

监听套接字和返回的套接字的区别:

  • 监听套接字: 用来获取客户端发来的连接请求,accept 函数会不断从监听套接字当中获取新连接。
  • 返回的套接字: 用于为本次accept 获取到的连接提供服务。

监听套接字只是用来获取客户端发来的连接请求,而accept 函数返回的套接字才是为这些连接提供服务的。

服务端获取连接

服务端在获取连接时需要注意:

  • accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。
  • 如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列。
  • inet_ntoa函数在底层实际做了两个工作,一是将网络序列转换成主机序列,二是将主机序列的整数IP转换成字符串风格的点分十进制的IP。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#define BACKLOG 5
class TcpServer
{
public:
    TcpServer(in_port_t port):_port(port)
    {}
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }

        if(listen(_listensock,BACKLOG) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(-3);
        }
    }
    void ServerStart()
    {
        while(true)
        {
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                exit(-4);
            }
            std::string client_ip = inet_ntoa(peer.sin_addr);
            in_port_t client_port = ntohs(peer.sin_port);
            std::cout << "get a new link->" << 
            "sock:" << sock << " client_ip:" << client_ip << " client_port:" << client_port << std::endl;
        }
    }
private:
    int _listensock;//监听套接字
    in_port_t _port;
};

服务端接收连接测试

现在我们可以做一下简单的测试,看看当前服务器能否成功接收请求连接。在运行服务端时需要传入一个端口号作为服务端的端口号,然后我们用该端口号构造一个服务端对象,对服务端进行初始化后启动服务端即可。

#include "tcp_server.hpp"

using namespace std;

int main()
{
    TcpServer* tsvr = new TcpServer(8082);
    tsvr->ServerInit();
    tsvr->ServerStart();
    return 0;
}

在这里插入图片描述
服务端运行后,通过netstat命令可以查看到一个程序名为tcp_server的服务程序,它绑定的端口就是8082,而由于服务器绑定的是INADDR_ANY,因此该服务器的本地IP地址是0.0.0.0,这就意味着该TCP服务器可以读取本地任何一张网卡里面的数据。此外,最重要的是当前该服务器所处的状态是LISTEN状态,表明当前服务器可以接收外部的请求连接。
在这里插入图片描述
虽然现在还没有编写客户端相关的代码,但是我们可以使用telnet命令远程登录到该服务器,因为telnet底层实际采用的就是TCP协议。

使用telnet命令连接当前TCP服务器后可以看到,此时服务器接收到了一个连接,为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的,其分别对应标准输入流、标准输出流和标准错误流,而3号文件描述符在初始化服务器时分配给了监听套接字,因此当第一个客户端发起连接请求时,为该客户端提供服务的套接字对应的文件描述符就是4。
在这里插入图片描述
在这里插入图片描述

服务器处理请求

此时服务端已经能够获取到连接的请求了,下面对获取到的连接进行处理。

为了能够让通信双方都能看到对应的现象,验证通信是正常的,我们设计一个服务端将客户端发来的数据接收到以后,再重新发回给客户端。

read函数

TCP服务器读取数据的函数叫做read,该函数的函数原型如下:

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

write函数

TCP服务器写入数据的函数叫做write,该函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。

服务端处理请求

服务端读取数据是从服务套接字中读取的,服务端写入数据是写入服务套接字的。也就是说,为客户端提供服务的套接字,即可以读取数据也可以写入数据,这就是TCP全双工通信的体现。

在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#define BACKLOG 5
class TcpServer
{
public:
    TcpServer(in_port_t port):_port(port)
    {}
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }

        if(listen(_listensock,BACKLOG) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(-3);
        }
    }
    void ServerStart()
    {
        while(true)
        {
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                exit(-4);
            }
            std::string client_ip = inet_ntoa(peer.sin_addr);
            in_port_t client_port = ntohs(peer.sin_port);
            std::cout << "get a new link->" << 
            "sock:" << sock << " client_ip:" << client_ip << " client_port:" << client_port << std::endl;

            //处理请求
            Service(sock,client_ip,client_port);
        }
    }
    void Service(int sock,std::string client_ip,in_port_t client_port)
    {
        char buffer[1024];
        while(true)
        {
            ssize_t n = read(sock,buffer,sizeof(buffer)-1);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout << "read success" << std::endl;

                std::string msg = "server get a message: ";
                msg += buffer;
                write(sock,msg.c_str(),msg.size());
            }
            else if(n == 0)
            {
                std::cout << "client close" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        close(sock);
        std::cout <<"server done" << std::endl;
    }
private:
    int _listensock;//监听套接字
    in_port_t _port;
};

客户端

客户端创建套接字

客户端不需要我们自己绑定,交给操作系统绑定。客户端一般不需要监听。

  • 服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知,不能随意改变。而客户端虽然也需要IP地址和端口号,但是客户端并不需要我们进行绑定操作,客户端连接服务端时系统会自动指定一个端口号给客户端。
  • 服务端需要进行监听是因为服务端需要通过监听来获取新连接,但是不会有人主动连接客户端,因此客户端是不需要进行监听操作的。

此外,客户端必须要知道它要连接的服务端的IP地址和端口号,因此客户端除了要有自己的套接字之外,还需要知道服务端的IP地址和端口号,这样客户端才能够通过套接字向指定服务器进行通信。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
class TcpClient
{
public:
    TcpClient(std::string ip,in_port_t port)
        :_server_ip(ip),_server_port(port)
        {}
    void ClientInit()
    {
        _sock = socket(AF_INET,SOCK_STREAM,0);
        if(_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(-1);
        }
    }
    ~TcpClient()
    {
        close(_sock);
    }
private:
    int _sock;//客户端套接字
    std::string _server_ip;//服务端IP地址
    in_port_t _server_port;//服务端端口号
};

客户端连接服务器

由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以向服务端发起连接请求。

connect函数

发起连接请求的函数叫做connect,该函数的函数原型如下:

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

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

客户端连接服务器

此外,调用connect函数向服务端发起连接请求时,需要传入服务端对应的网络信息。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
class TcpClient
{
public:
    TcpClient(std::string ip,in_port_t port)
        :_server_ip(ip),_server_port(port)
        {}
    void ClientInit()
    {
        _sock = socket(AF_INET,SOCK_STREAM,0);
        if(_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(-1);
        }
    }
    void ClientStart()
    {
        struct sockaddr_in peer;
        bzero(&peer,sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = ntohs(_server_port);
        peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());

        if(connect(_sock,(struct sockaddr*)&peer,sizeof(peer)) < 0)
        {
            std::cerr << "connect error" << std::endl;
            exit(-2);
        }
        else
        {
            std::cout <<"connect success" << std::endl;
            Request();//发起请求
        }
    }
    ~TcpClient()
    {
        close(_sock);
    }
private:
    int _sock;//客户端套接字
    std::string _server_ip;//服务端IP地址
    in_port_t _server_port;//服务端端口号
};

客户端发起请求

当客户端连接到服务器后,客户端就可以发起请求了。我们这里每次发送一条消息给客户端,客户端处理后再发回来。
以下是发起请求的代码,Request()函数里先向服务端发一条消息,然后再接收服务端的消息。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
class TcpClient
{
public:
    TcpClient(std::string ip,in_port_t port)
        :_server_ip(ip),_server_port(port)
        {}
    void ClientInit()
    {
        _sock = socket(AF_INET,SOCK_STREAM,0);
        if(_sock < 0)
        {
            std::cerr << "socket create error" << std::endl;
            exit(-1);
        }
    }
    void ClientStart()
    {
        struct sockaddr_in peer;
        bzero(&peer,sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = ntohs(_server_port);
        peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());

        if(connect(_sock,(struct sockaddr*)&peer,sizeof(peer)) < 0)
        {
            std::cerr << "connect error" << std::endl;
            exit(-2);
        }
        else
        {
            std::cout <<"connect success" << std::endl;
            Request();//发起请求
        }
    }
    void Request()
    {
        char buffer[1024];
        while(true)
        {
            std::string msg;
            std::cout << "Please Enter" << std::endl;
            std::getline(std::cin,msg);
            write(_sock,msg.c_str(),msg.size());

            ssize_t n = read(_sock,buffer,sizeof(buffer)-1);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout << "server echo# " << buffer << std::endl;
            }
            else if(n == 0)
            {
                std::cout << "server close" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }
    ~TcpClient()
    {
        close(_sock);
    }
private:
    int _sock;//客户端套接字
    std::string _server_ip;//服务端IP地址
    in_port_t _server_port;//服务端端口号
};

在运行客户端程序时我们就需要携带上服务端对应的IP地址和端口号,然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象,对客户端进行初始后启动客户端即可。

#include "tcp_client.hpp"

int main()
{
    TcpClient * tclt = new TcpClient("127.0.0.1",8082);
    tclt->ClientInit();
    tclt->ClientStart();
    return 0;
}

测试

在这里插入图片描述
启动服务端后,用 netstat命令进行查看,此时就能看到名为tcp_server的服务进程,此时正处于监听状态。
在这里插入图片描述

此时就可以启动客户端连接服务端,进行服务了。
在这里插入图片描述

在这里插入图片描述
客户端给服务端发消息,服务端回显对应的消息。

当关闭该客户端后,服务端对该客户端的服务也就结束了。
在这里插入图片描述

服务器存在的问题

该TCP服务器存在一个问题:一次只能为一个客户端服务,如果有多个客户端连接,那么只能等一个客户端退出后,其他客户端才能被服务器服务。
在这里插入图片描述

如图所示,客户端1能够成功连接,正常通信。
当客户端2尝试连接的时候,会发现服务器没有显示客户端2的相关信息,客户端2向服务器发消息也没回应。
在这里插入图片描述
当我们关闭客户端1后,服务端就会显示客户端2的消息,并且接收和回应客户端2发来的消息。
在这里插入图片描述

单执行流的服务器

通过实验现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器,这个服务器一次只能为一个客户端提供服务。

当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端。

客户端2为什么会显示连接成功?

当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了。

实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。

如何解决?

单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的,此时就要引入多进程或多线程

多进程版的TCP网络程序

当服务端调用accept函数获取连接成功后,不是由当前执行流为其提供服务,而是创建一个子进程,让子进程为父进程获取到的连接提供服务。
父进程不关系获取到的连接服务是否提供完毕,交给子进程后,自己又去监听是否有其他客户端想获取连接。

等待子进程问题

当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏。因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待。

阻塞式等待与非阻塞式等待:

  • 如果服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完当前客户端,才能继续获取下一个连接请求,此时服务端仍然是以一种串行的方式为客户端提供服务。
  • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出。

总之,服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不尽人意。此时我们可以考虑让服务端不等待子进程退出。

不等待子进程退出的方式

  • 捕捉SIGCHLD信号,将其处理动作设置为忽略

多进程版本代码编写

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#define BACKLOG 5
class TcpServer
{
public:
    TcpServer(in_port_t port):_port(port)
    {}
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }

        if(listen(_listensock,BACKLOG) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(-3);
        }
    }
    void ServerStart()
    {
        while(true)
        {
            signal(SIGCHLD,SIG_IGN);
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                exit(-4);
            }
            std::string client_ip = inet_ntoa(peer.sin_addr);
            in_port_t client_port = ntohs(peer.sin_port);
            std::cout << "get a new link->" << 
            "sock:" << sock << " client_ip:" << client_ip << " client_port:" << client_port << std::endl;

            //处理请求
            pid_t id = fork();
            if(id < 0)
            {
                close(sock);
            }
            else if(id == 0)//子进程创建成功
            {
                //关闭不需要的套接字
                close(_listensock);//子进程是不需要监听套接字的,关闭
                Service(sock,client_ip,client_port);
                exit(0);
            }
            close(sock);//父进程,父进程不要服务套接字,只需监听套接字。
        }
    }
    void Service(int sock,std::string client_ip,in_port_t client_port)
    {
        char buffer[1024];
        while(true)
        {
            ssize_t n = read(sock,buffer,sizeof(buffer)-1);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout << "read success" << std::endl;

                std::string msg = "server get a message: ";
                msg += buffer;
                write(sock,msg.c_str(),msg.size());
            }
            else if(n == 0)
            {
                std::cout << "client close" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        close(sock);
        std::cout <<"server done" << std::endl;
    }
private:
    int _listensock;//监听套接字
    in_port_t _port;
};

代码测试

重新编译程序运行服务端后,可以通过以下监控脚本对服务进程进行监控。

while :; do ps axj | head -1 && ps axj | grep tcp_server | grep -v grep;echo "######################";sleep 1;done

此时可以看到,一开始没有客户端连接该服务器,此时服务进程只有一个,该服务进程就是不断获取新连接的进程,而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的。

在这里插入图片描述
此时我们运行一个客户端,让该客户端连接服务器,此时服务进程就会调用fork函数创建出一个子进程,由该子进程为这个客户端提供服务。
在这里插入图片描述
如果再有一个客户端连接服务器,此时服务进程会再创建出一个子进程,让该子进程为这个客户端提供服务。

最重要的是,由于这两个客户端分别由两个不同的执行流提供服务,因此这两个客户端可以同时享受到服务,它们发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应。
在这里插入图片描述

当客户端一个个退出后,在服务端对应为之提供服务的子进程也会相继退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接。
在这里插入图片描述

多线程版的TCP网络程序

创建进程的成本是很高的,创建进程时需要创建该进程对应的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等数据结构。而创建线程的成本比创建进程的成本会小得多,因为线程本质是在进程地址空间内运行,创建出来的线程会共享该进程的大部分资源,因此在实现多执行流的服务器时最好采用多线程进行实现。

当服务进程调用accept函数获取到一个新连接后,就可以直接创建一个线程,让该线程为对应客户端提供服务。

当然,主线程(服务进程)创建出新线程后,也是需要等待新线程退出的,否则也会造成类似于僵尸进程这样的问题。但对于线程来说,如果不想让主线程等待新线程退出,可以让创建出来的新线程调用pthread_detach函数进行线程分离,当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程(服务进程)就可以继续调用accept函数获取新连接,而让新线程去服务对应的客户端。

各个线程共享同一张文件描述符表

文件描述符表维护的是进程与文件之间的对应关系,因此一个进程对应一张文件描述符表。而主线程创建出来的新线程依旧属于这个进程,因此创建线程时并不会为该线程创建独立的文件描述符表,所有的线程看到的都是同一张文件描述符表。
在这里插入图片描述
因此当服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是能够直接访问这个文件描述符的。

需要注意的是,虽然新线程能够直接访问主线程accept上来的文件描述符,但此时新线程并不知道它所服务的客户端对应的是哪一个文件描述符,因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符的值,也就是告诉每个新线程在服务客户端时,应该对哪一个套接字进行操作。

参数结构体

实际新线程在为客户端提供服务时就是调用Service函数,而调用Service函数时是需要传入三个参数的,分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数,而实际在调用pthread_create函数创建新线程时,只能传入一个类型为void*的参数。

这时我们可以设计一个参数结构体Param,此时这三个参数就可以放到Param结构体当中,当主线程创建新线程时就可以定义一个Param对象,将客户端对应的套接字、IP地址和端口号设计进这个Param对象当中,然后将Param对象的地址作为新线程执行例程的参数进行传入。

此时新线程在执行例程当中再将这个void类型的参数强转为Param类型,然后就能够拿到客户端对应的套接字,IP地址和端口号,进而调用Service函数为对应客户端提供服务。

class Param
{
public:
    Param(int sock,std::string ip,in_port_t port)
        :_sock(sock),_ip(ip),_port(port)
        {}
private:
    int _sock;
    std::string _ip;
    in_port_t _port; 
};

文件描述符关闭的问题

由于此时所有线程看到的都是同一张文件描述符表,因此当某个线程要对这张文件描述符表做某种操作时,不仅要考虑当前线程,还要考虑其他线程。

  • 对于主线程accept上来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。
  • 对于监听套接字,虽然创建出来的新线程不必关心监听套接字,但新线程不能将监听套接字对应的文件描述符关闭,否则主线程就无法从监听套接字当中获取新连接了。

Service函数定义为静态成员函数

由于调用pthread_create函数创建线程时,新线程的执行例程是一个参数为void*,返回值为void*的函数。如果我们要将这个执行例程定义到类内,就需要将其定义为静态成员函数,否则这个执行例程的第一个参数是隐藏的this指针。

代码测试

此时我们再重新编译服务端代码,由于代码当中用到了多线程,因此编译时需要携带上-pthread选项。此外,由于我们现在要监测的是一个个的线程,因此在监控时使用的不再是ps -axj命令,而是ps -aL命令。

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

运行服务端,通过监控可以看到,此时只有一个服务线程,该服务线程就是主线程,它现在在等待客户端的连接到来。

在这里插入图片描述

当一个客户端连接到服务端后,此时主线程就会为该客户端构建一个参数结构体,然后创建一个新线程,将该参数结构体的地址作为参数传递给这个新线程,此时该新线程就能够从这个参数结构体当中提取出对应的参数,然后调用Service函数为该客户端提供服务,因此在监控当中显示了两个线程。
在这里插入图片描述
当第二个客户端发来连接请求时,主线程会进行相同的操作,最终再创建出一个新线程为该客户端提供服务,此时服务端当中就有了三个线程。

由于为这两个客户端提供服务的也是两个不同的执行流,因此这两个客户端可以同时享受服务端提供的服务,它们发送给服务端的消息也都能够在服务端进行打印,并且这两个客户端也都能够收到服务端的回显数据。
在这里插入图片描述
此时无论有多少个客户端发来连接请求,在服务端都会创建出相应数量的新线程为对应客户端提供服务,而当客户端一个个退出后,为其提供服务的新线程也就会相继退出,最终就只剩下最初的主线程仍在等待新连接的到来。

在这里插入图片描述

线程池版的TCP网络程序

当前多线程版的服务器存在的问题:

  • 每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样做不仅麻烦,而且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程。
  • 如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也迟迟得不到应答。

解决思路

针对这两个问题,对应的解决思路如下:

  • 可以在服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程。
  • 当某个线程为客户端提供完服务后,不要让该线程退出,而是让该线程继续为下一个客户端提供服务,如果当前没有客户端连接请求,则可以让该线程先进入休眠状态,当有客户端连接到来时再将该线程唤醒。
  • 服务端创建的这一批线程的数量不能太多,此时CPU的压力也就不会太大。此外,如果有客户端连接到来,但此时这一批线程都在给其他客户端提供服务,这时服务端不应该再创建线程,而应该让这个新来的连接请求在全连接队列进行排队,等服务端这一批线程中有空闲线程后,再将该连接请求获取上来并为其提供服务。

引入线程池

实际要解决这里的问题我们就需要在服务端引入线程池,因为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价,此外,线程池还能够保证内核充分利用,防止过分调度。

其中在线程池里面有一个任务队列,当有新的任务到来的时候,就可以将任务Push到线程池当中,在线程池当中我们默认创建了5个线程,这些线程不断检测任务队列当中是否有任务,如果有任务就拿出任务,然后调用该任务对应的Run函数对该任务进行处理,如果线程池当中没有任务那么当前线程就会进入休眠状态。

#define NUM 5

//线程池
template<class T>
class ThreadPool
{
private:
	bool IsEmpty()
	{
		return _task_queue.size() == 0;
	}
	void LockQueue()
	{
		pthread_mutex_lock(&_mutex);
	}
	void UnLockQueue()
	{
	    pthread_mutex_unlock(&_mutex);
	}
	void Wait()
	{
	    pthread_cond_wait(&_cond, &_mutex);
	}
	void WakeUp()
	{
	    pthread_cond_signal(&_cond);
	}
public:
	ThreadPool(int num = NUM)
		: _thread_num(num)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	~ThreadPool()
	{
	    pthread_mutex_destroy(&_mutex);
	    pthread_cond_destroy(&_cond);
	}
	//线程池中线程的执行例程
	static void* Routine(void* arg)
	{
	    pthread_detach(pthread_self());
	    ThreadPool* self = (ThreadPool*)arg;
	    //不断从任务队列获取任务进行处理
		while (true){
			self->LockQueue();
			while (self->IsEmpty()){
				self->Wait();
			}
			T task;
			self->Pop(task);
			self->UnLockQueue();
			
			task.Run(); //处理任务
		}
	}
	void ThreadPoolInit()
	{
		pthread_t tid;
		for (int i = 0; i < _thread_num; i++){
			pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针
		}
	}
	//往任务队列塞任务(主线程调用)
	void Push(const T& task)
	{
	    LockQueue();
	    _task_queue.push(task);
	    UnLockQueue();
	    WakeUp();
	}
	//从任务队列获取任务(线程池中的线程调用)
	void Pop(T& task)
	{
	    task = _task_queue.front();
	    _task_queue.pop();
	}
	
private:
	std::queue<T> _task_queue; //任务队列
	int _thread_num; //线程池中线程的数量
	pthread_mutex_t _mutex;
	pthread_cond_t _cond;
};

服务类新增线程池成员

现在服务端引入了线程池,因此在服务类当中需要新增一个指向线程池的指针成员:

  • 当实例化服务器对象时,先将这个线程池指针先初始化为空。
  • 当服务器初始化完毕后,再实际构造这个线程池对象,在构造线程池对象时可以指定线程池当中线程的个数,也可以不指定,此时默认线程的个数为5。
  • 在启动服务器之前对线程池进行初始化,此时就会将线程池当中的若干线程创建出来,而这些线程创建出来后就会不断检测任务队列,从任务队列当中拿出任务进行处理。

现在当服务进程调用accept函数获取到一个连接请求后,就会根据该客户端的套接字、IP地址以及端口号构建出一个任务,然后调用线程池提供的Push接口将该任务塞入任务队列。

这实际也是一个生产者消费者模型,其中服务进程就作为了任务的生产者,而后端线程池当中的若干线程就不断从任务队列当中获取任务进行处理,它们承担的就是消费者的角色,其中生产者和消费者的交易场所就是线程池当中的任务队列。

#include "ThreadPool.hpp"
#include "Task.hpp"
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#define BACKLOG 5
class TcpServer
{
public:
    TcpServer(in_port_t port):_port(port)
    {}
    void ServerInit()
    {
        // 创建监听套接字
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(-1);
        }
        //绑定,绑定之前需要填充绑定函数参数里的结构体字段
        struct sockaddr_in local;
        bzero(&local,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)
        {
            std::cerr << "bind socket error" << std::endl;
            exit(-2);
        }

        if(listen(_listensock,BACKLOG) < 0)
        {
            std::cerr << "listen error" << std::endl;
            exit(-3);
        }
        _tp = new ThreadPool<Task>(); //构造线程池对象,Task是要执行的任务类
    }
    void ServerStart()
    {
        _tp->ThreadPoolInit();//初始化线程池
        while(true)
        {
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                exit(-4);
            }
            std::string client_ip = inet_ntoa(peer.sin_addr);
            in_port_t client_port = ntohs(peer.sin_port);
            std::cout << "get a new link->" << 
            "sock:" << sock << " client_ip:" << client_ip << " client_port:" << client_port << std::endl;

            Task task(sock,client_ip,client_port);//构造任务
            _tp->Push(task);//将任务push进队列
        }
    }
    void Service(int sock,std::string client_ip,in_port_t client_port)
    {
        char buffer[1024];
        while(true)
        {
            ssize_t n = read(sock,buffer,sizeof(buffer)-1);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout << "read success" << std::endl;

                std::string msg = "server get a message: ";
                msg += buffer;
                write(sock,msg.c_str(),msg.size());
            }
            else if(n == 0)
            {
                std::cout << "client close" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        close(sock);
        std::cout <<"server done" << std::endl;
    }
private:
    int _listensock;//监听套接字
    in_port_t _port;
    ThreadPool<Task>* _tp;
};

设计任务类

现在我们要做的就是设计一个任务类,该任务类当中需要包含客户端对应的套接字、IP地址、端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个。

此外,任务类当中需要包含一个Run方法,当线程池中的线程拿到任务后就会直接调用这个Run方法对该任务进行处理,而实际处理这个任务的方法就是服务类当中的Service函数,服务端就是通过调用Service函数为客户端提供服务的。

我们可以直接拿出服务类当中的Service函数,将其放到任务类当中作为任务类当中的Run方法,但这实际不利于软件分层。我们可以给任务类新增一个仿函数成员,当执行任务类当中的Run方法处理任务时就可以以回调的方式处理该任务。

class Task
{
public:
    Task()
    {
    }
    Task(int sock, std::string client_ip, int client_port)
        : _sock(sock), _client_ip(client_ip), _client_port(client_port)
    {
    }
    void Run()
    {
        _handler(_sock,_client_ip,_client_port);
    }
private:
    int _sock;
    std::string _client_ip;
    u_int16_t _client_port;
    Handler _handler;//处理方法
};

注意: 当任务队列当中有任务时,线程池当中的线程会先定义出一个Task对象,然后将这个Task对象作为输出型参数调用任务队列的Pop函数,从任务队列当中获取任务,因此Task类除了提供带参的构造函数以外,还需要提供一个无参的构造函数,方便我们可以定义无参对象。

设计Handler类

此时需要再设计一个Handler类,在Handler类当中对()操作符进行重载,将()操作符的执行动作重载为执行Service函数的代码。

class Handler
{
public:
    void operator()(int sock, std::string ip, u_int16_t port)
    {
        char buffer[1024];
        while (true)
        {
            ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = '\0';
                std::cout << "read success" << std::endl;

                std::string msg = "server get a message: ";
                msg += buffer;
                write(sock, msg.c_str(), msg.size());
            }
            else if (n == 0)
            {
                std::cout << "client close" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
        close(sock);
        std::cout << "server done" << std::endl;
    }
};

实际我们可以让服务器处理不同的任务,当前服务器只是在进行字符串的回显处理,而实际要怎么处理这个任务完全是由任务类当中的handler成员来决定的。

如果想要让服务器处理其他任务,只需要修改Handler类当中对()的重载函数就行了,而服务器的初始化、启动服务器以及线程池的代码都是不需要更改的,这就叫做把通信功能和业务逻辑在软件上做解耦。

代码测试

运行服务端后,就算没有客户端发来连接请求,此时在服务端就已经有了6个线程,其中有一个是接收新连接的服务线程,而其余的5个是线程池当中为客户端提供服务的线程。
在这里插入图片描述
此时当客户端连接服务器后,服务端的主线程就会获取该客户端的连接请求,并将其封装为一个任务对象后塞入任务队列,此时线程池中的5个线程就会有一个线程从任务队列当中获取到该任务,并执行该任务的处理函数为客户端提供服务。

在这里插入图片描述
当第二个客户端发起连接请求时,服务端也会将其封装为一个任务类塞到任务队列,然后线程池当中的线程再从任务队列当中获取到该任务进行处理,此时也是不同的执行流为这两个客户端提供的服务,因此这两个客户端也是能够同时享受服务的。
在这里插入图片描述之前不同的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出。
在这里插入图片描述
我们设置的线程池的最大线程容量为5,当连接的客户端大于5个后,从第六个客户端开始,就要在连接队列中进行等待。当有一个线程退出后,后面的一个线程就可以正常通信了。

TCP网络程序总结图

在这里插入图片描述

本文章参考自hguisu和2021dragon

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

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

相关文章

一站式财务管家工具:Zoho Books审批功能详细介绍

Zoho Books作为一款功能强大的财务管理软件&#xff0c;提供了多种实用的功能&#xff0c;其中审批流程是非常重要的一个。那么&#xff0c;Zoho Books的审批功能是如何实现的呢&#xff1f;本文将为您详细介绍。 1. 什么是审批功能 审批是企业内部重要业务流程中的前置环节&a…

华为数通HCIP-OSPF基础

路由协议 作用&#xff1a;用于路由设备学习非直连路由&#xff1b; 动态路由协议&#xff1a;使路由设备自动学习到非直连路由&#xff1b; 分类&#xff1a; 按照算法分类&#xff1a; 1、距离矢量路由协议&#xff1b;&#xff08;RIP、BGP&#xff09; 只交互路由信息…

基于FPGA实现OSD功能

简介 基于FPGA平台实现简单的OSD的功能,对于FPGA实现OSD只能实行简单的画框和文字叠加,如果实现复杂的车道线画框,则没法实现(起码我个人感觉,这个功能没有思路执行)。 FPGA实现OSD功能需要7系列平台,以及VDMA、OSD等Xilinx公司的IP使用(本功能工程采用Vivado2017.4平台…

windows关闭某个进程

一、使用命令 &#xff08;1&#xff09;winR键打开命令提示符&#xff0c;输入cmd &#xff08;2&#xff09;输入netstat -ano &#xff08;3&#xff09;输入taskkill /f /pid 进程ID。例如&#xff1a;taskkill /f /pid 19216 如果成功终止的话&#xff0c;会出现成功&…

Vue+Nodejs 使用WebSocket创建一个简易聊天室

文章目录 一、页面效果二、架构流程三、技术细节1.客户端2. 服务端 一、页面效果 二、架构流程 使用vue编写前端页面&#xff0c;nodejs处理服务端消息&#xff0c;WebSocket进行实时通信 三、技术细节 1.客户端 <template><div><form onsubmit"return…

Ubuntu录屏软件Kazam

1. 安装 1.1. 桌面右键“打开终端” 1.2. 安装kazam这款软件。 sudo apt-get install kazam 2. 使用 2.1. 安装后打开&#xff0c;我们看看这款软件界面还是很友好很简洁的。 2.2. 除了录像我们还可以截图&#xff0c;也可以选择全屏、窗口、区域的方式录制。 2.3. 如果要录…

Hybird app 热更新工作原理

大家对于原生应用和混合应用已经非常熟悉了&#xff0c;这里就不再进行详细的介绍&#xff0c;用通俗易懂的话解释下他们的一些特点。 1、原生应用 在 Android、iOS 等移动平台上利用提供的开发语言、开发类库、开发工具进行 App 软件开发。比如 Android 是用 Java、Eclipse、…

改进的北方苍鹰算法优化VMD参数,最小包络熵、样本熵、信息熵、排列熵(适应度函数可自行选择,一键修改)包含MATLAB源代码...

今天给大家带来一期由改进的北方苍鹰算法(SCNGO)优化VMD的两个参数。 同样以西储大学数据集为例&#xff0c;选用105.mat中的X105_BA_time.mat数据中1000个数据点。没有数据的看这篇文章。西储大学轴承诊断数据处理&#xff0c;matlab免费代码获取 选取四种适应度函数进行优化&…

【开发问题】flink-cdc不用数据库之间的,不同类型的转化

不同的数据库之期间数据类型转化 问题来源与原因解决过程&#xff0c;思路错误&#xff0c;导致各种错误错误思路是什么 正确解决方式&#xff0c;找官网对应的链接器&#xff0c;数据转化 问题来源与原因 我一开始是flink-cdc&#xff0c;oracle2Mysql&#xff0c;sql 我一开…

Hygon海光电脑:window无法对计算机进行,windows无法对计算机进行启动到下一个安装阶段怎么办...

海光CPU电脑安装第2个系统&#xff0c;Windows10LTSC&#xff0c;U盘引导顺利&#xff0c;安装顺利&#xff0c;在最后一步时出错&#xff1a;。 出错提示 Windows10安装过程中提示&#xff1a; windows无法对计算机进行启动到下一个安装阶段的准备。要安装Windows&#xff0…

时空复杂度详解

&#x1f493;博主个人主页:不是笨小孩&#x1f440; ⏩专栏分类:数据结构与算法&#x1f440; &#x1f69a;代码仓库:笨小孩的代码库&#x1f440; ⏩社区&#xff1a;不是笨小孩&#x1f440; &#x1f339;欢迎大家三连关注&#xff0c;一起学习&#xff0c;一起进步&#…

【CAS6.6源码解析】调试Rest API接口

CAS的web层默认是基于webflow实现的&#xff0c;ui和后端是耦合在一起的&#xff0c;做前后端分离调用和调试的时候不太方便。但是好在CAS已经添加了支持Rest API的support模块&#xff0c;添加相应模块即可。 文章目录 添加依赖并重新build效果 添加依赖并重新build 具体添加…

第54步 深度学习图像识别:MLP-Mixer建模(Pytorch)

基于WIN10的64位系统演示 一、写在前面 &#xff08;1&#xff09;MLP-Mixer MLP-Mixer&#xff08;Multilayer Perceptron Mixer&#xff09;是Google在2021年提出的一种新型的视觉模型结构。它的主要特点是完全使用多层感知机&#xff08;MLP&#xff09;来处理图像&#…

seaborn笔记 pairplot PairGrid

1 数据集 鸢尾花数据集 # Visual Python: Data Analysis > File vp_df pd.read_csv(https://raw.githubusercontent.com/visualpython/visualpython/main/visualpython/data/sample_csv/iris.csv) vp_df 1.1 基本pairplot import seaborn as snsg sns.pairplot(vp_df) …

前端随笔:HTML/CSS/JavaScript和Vue

前端随笔 1&#xff1a;HTML、JavaScript和Vue 最近因为工作需要&#xff0c;需要接触一些前端的东西。之前虽然大体上了解过HTML、CSS和JavaScript&#xff0c;也知道HTML定义了内容、CSS定义了样式、JavaScript定义了行为&#xff0c;但是却没有详细的学习过前端三件套的细节…

2023 年第二届钉钉杯大学生大数据挑战赛初赛 初赛 A:智能手机用户监测数据分析 问题二分类与回归问题Python代码分析

2023 年第二届钉钉杯大学生大数据挑战赛初赛 初赛 A&#xff1a;智能手机用户监测数据分析 问题二分类与回归问题Python代码分析 相关链接 【2023 年第二届钉钉杯大学生大数据挑战赛初赛】 初赛 A&#xff1a;智能手机用户监测数据分析 问题一Python代码分析 【2023 年第二届…

RocketMQ 5.0 无状态实时性消费详解

作者&#xff1a;绍舒 背景 RocketMQ 5.0 版本引入了 Proxy 模块、无状态 pop 消费机制和 gRPC 协议等创新功能&#xff0c;同时还推出了一种全新的客户端类型&#xff1a;SimpleConsumer。 SimpleConsumer 客户端采用了无状态的 pop 机制&#xff0c;彻底解决了在客户端发布…

SpringBoot原理分析 | Redis集成

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Springboot集成Redis 依赖导入 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis<…

九、数据结构——顺序队列中的循环队列

目录 一、循环队列的定义 二、循环队列的实现 三、循环队列的基本操作 ①初始化 ②判空 ③判满 ④入队 ⑤出队 ⑥获取长度 ⑦打印 四、循环队列的应用 五、全部代码 数据结构中的循环队列 在数据结构中&#xff0c;队列&#xff08;Queue&#xff09;是一种常见的线性数据结…

防火墙NAT地址转换的四种应用实验与防火墙的双机热备实验

一、NAT实验 一、源地址转换 1、首先搭建NAT实验环境的拓扑&#xff1a; 这里需要配置各个设备的ip、掩码、网关&#xff1b;省略 2、登录防火墙设备并且为防火墙设备的0/0/0接口配置与虚拟网卡一个网段的ip&#xff0c;并且开启该接口的全部服务 [USG6000V1]int gi 0/0/0…