网络套接字编程(二)

网络套接字编程(二)

文章目录

  • 网络套接字编程(二)
    • 简易TCP网络程序
      • 服务端创建套接字
      • 服务端绑定IP地址和端口号
      • 服务端监听
      • 服务端运行
      • 服务端网络服务
      • 服务端启动
      • 客户端创建套接字
      • 客户端的绑定和监听问题
      • 客户端建立连接并通信
      • 客户端启动
      • 程序测试
      • 单执行流服务器的弊端
    • 多进程版TCP网络程序
      • 捕捉SIGCHLD信号
      • 孙子进程提供网络服务
    • 多线程版TCP网络服务
    • 线程池版TCP网络程序

在上一篇博客 网络套接字编程(一)-CSDN博客中利用套接字编程编写了简易的UDP网络程序,本文我们再编写一个简易的TCP网络程序。

简易TCP网络程序

和上一篇博客中编写的UDP网络程序一样,将TCP网络程序分为服务端和客户端,分别封装成类,然后定义类并调用类内函数进行TCP网络程序的初始化和启动,完成简易的网络通信。本文编写的TCP网络程序的功能同样是客户端发送数据给服务端,服务端将接收的数据回传给客户端。

服务端创建套接字

使用套接字编程进行网络通信第一步就是要创建套接字,因此在服务端类内的初始化函数内部进行套接字的创建:

enum
{
    SOCKET_ERROR=1
};
class TcpServer
{
    public:
    void InitServer() 
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            std::cerr << "create socket error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERROR);
        }
    }
    private:
    int _listensock;
};

socket函数的参数

TCP协议采用的是有连接的可靠的数据传输方式,因此socket的类型应该为SOCK_STREAM这种有连接的可靠的流式数据。

服务端绑定IP地址和端口号

套接字编程进行网络通信时确定唯一主机的方式就是利用IP地址和端口号,服务端必须要让众多客户端一定能找到,因此服务端要进行IP地址和端口号的绑定。(云服务同样不需要绑定指定IP地址)在服务端类内的初始化函数内部进行绑定:

enum
{
    SOCKET_ERROR=1,
    BIND_ERROR
};
static const uint16_t default_port = 8081;//端口号缺省值
class TcpServer
{
    public:
    TcpServer(uint16_t port = default_port):_port(port) {}
    void InitServer() 
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            std::cerr << "create socket error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERROR);
        }
        // 绑定IP地址和端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind socket error" << strerror(errno) << std::endl;
            exit(BIND_ERROR);
        }
    }   
    private:
    uint16_t _port;
    int _listensock;
};

云服务不需要绑定指定IP地址的原因

云服务不同于普通的主机,可能存在许多张网卡,因此云服务所使用的IP地址可能有许多个,数据接收的IP地址不确定,如果绑定云服务器某一指定IP地址,进程就可能接收不到其他IP地址收到的数据。INADDR_ANY对应的常量值是0.0.0.0,它表示绑定到所有可用的网络接口上,即可以通过任何可用的IP地址进行通信,云服务会把所有IP地址的得到的数据接收,然后根据端口号传输给指定进程。

服务端监听

TCP协议是需要建立连接的,由于建立连接的时机是不确定,因此要让服务端处于准备建立连接的状态,也就是然服务端进行监听,让服务端监听需要用到listen函数:

listen函数

//listen函数所在的头文件和函数声明

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

int listen(int sockfd, int backlog);
  • 该函数的功能是将指定的套接字设置为监听状态,以便接受客户端的连接请求。(用于TCP协议的服务端
  • sockfd参数: 套接字描述符,用于标识一个已经打开的套接字。
  • backlog参数: 指定连接请求队列的最大长度。该参数决定了在调用accept函数之前,能够排队等待处理的未完成连接请求的数量。
  • 返回值: 成功时,返回0表示操作成功。失败时,返回-1,并设置errno变量以指示具体的错误原因。

由于TCP协议需要在建立连接后进行网络通信,而监听需要在建立连接前进行,因此在服务端类内的初始化函数内部进行监听:

enum
{
    SOCKET_ERROR=1,
    BIND_ERROR,
    LISTEN_ERROR
};
static const uint16_t default_port = 8081;//端口号缺省值
static const int backlog = 32;
class TcpServer
{
    public:
    TcpServer(uint16_t port = default_port):_port(port) {}
    void InitServer() 
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            std::cerr << "create socket error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERROR);
        }
        // 绑定IP地址和端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind socket error" << strerror(errno) << std::endl;
            exit(BIND_ERROR);
        }
        // 监听
        if (listen(_listensock, backlog) < 0)
        {
            std::cerr << "listen socket error" << strerror(errno) << std::endl;
            exit(LISTEN_ERROR);
        }
    }   
    private:
    uint16_t _port;
    int _listensock;
};

服务端运行

服务端运行后,第一步要做的就是获取和客户端的连接,通过通过建立的连接进行网络通信,获取连接需要使用accept函数:

//accept函数所在的头文件和函数声明

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

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 该函数的功能是用于接受连接请求并建立连接。(用于TCP协议的服务端
  • sockfd参数: 套接字描述符,用于标识一个已经处于监听状态的套接字。
  • addr参数: 指向一个sockaddr结构体的指针,用于存储客户端的地址信息。
  • addrlen参数: 指向一个整数的指针,表示addr结构体的长度。
  • 返回值: 成功时,返回一个新的套接字的文件描述符,用于与客户端进行通信。失败时,返回-1,并设置errno变量以指示具体的错误原因。

在服务端类内的启动函数内部进行连接的获取:

class TcpServer
{
    public:
    void StartServer()//服务端运行函数 
    {
        while (true)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << "获取连接成功:" << sock << "from" << _listensock << "," << 
                clientip << "-" << clientport << std::endl;
            // 网络服务
            service(sock, clientip, clientport);
        }
    }
    private:
    uint16_t _port;
    int _listensock;
};

accept函数返回值

accept函数的返回值仍然是一个新的文件描述符,该文件描述符指向的文件不同于创建套接字时的文件描述符指向的文件,因为在TCP协议的套接字编程中,创建套接字时的返回的文件是专门用于监听的,而获取连接时返回的文件是用于和对应的建立连接的客户端通信的。

服务端网络服务

服务端运行起来后,首先要建立连接,然后要做的就是进行网络服务,也就是进行数据的接收、数据的处理、数据的发送的任务,因此在服务端类内的实现一个网络服务函数:

static const uint16_t default_port = 8081;
using func_t = std::function<std::string(const std::string &)>;
class TcpServer
{
    public:
    TcpServer(func_t func, uint16_t port = default_port):_func(func), _port(port) {}
    void service(int sock, std::string &clientip, uint16_t clientport)
    {
        std::string who = clientip + "-" + std::to_string(clientport);
        char buffer[128];
        while (true)
        {
            ssize_t n = read(sock, buffer, sizeof(buffer) - 1); // 数据接收
            if (n > 0)
            {
                buffer[n] = 0;
                std::string res = _func(buffer);
                std::cout << who << ">>>" << res << std::endl;
                write(sock, res.c_str(), res.size()); // 发送数据
            }
            else if (n == 0)
            {
                close(sock);
                std::cout << "client quit,me too" << std::endl;
                break;
            }
            else
            {
                close(sock);
                std::cout << "read error" << std::endl;
                break;
            }
        }
    }
    private:
    uint16_t _port;
    int _listensock;
    func_t _func;//数据处理方法
};

网络服务中数据处理的方法由定义类对象的外部来进行传入。

使用read和write的原因

由于TCP协议采用的是流式数据传输,因此可以使用read和write函数进行流式数据写入文件的缓冲区中,然后操作系统会让调用驱动提供的读取和写入函数将数据从网卡中读取和写入,然后网卡设备会将数据接收和发送。

服务端启动

调用服务端类内部的函数进行服务端的初始化,并运行服务端。为了给错误运行服务端纠错,引入了命令行参数,在运行服务端时做纠错提示:

enum
{
    SOCKET_ERROR=1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};

void Usage(const char *proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n" << std::endl; 
}

std::string echo(const std::string& message)//数据处理方法
{
    return message;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));
    tsvr->InitServer();
    tsvr->StartServer();
    return 0;
}

数据处理方法的实现

由于本文实现的简易TCP网络程序中服务端的功能是将从客户端接收的数据回传给客户端,因此数据处理方法的实现只是简单的将传入的数据返回即可。

客户端创建套接字

同样的,将客户端封装成类,在使用客户端时,只需要创建类对象,然后调用对应的函数即可使用客户端。在创建客户端类对象后的第一步就是初始化客户端,在初始化客户端时,首先就需要创建套接字:

enum
{
    SOCKET_ERROR=1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
    CONNECT_ERROR
};
class TcpClient
{
    public: 
    TcpClient(std::string serverip, uint16_t serverport):_serverip(serverip), _serverport(serverport) {}
    void InitClient()
    {
        //创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            std::cerr << "create socket error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERROR);
        }
    }
    private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

客户端的绑定和监听问题

同上一篇博客中所讲的一样,为了避免端口号的冲突,因此客户端无需自主绑定IP地址和端口号。

客户端是发起连接的一方,建立连接的时机由客户端决定,因此无需监听。

客户端建立连接并通信

由于使用TCP协议的网络通信前需要建立连接,因此客户端也需要建立连接,客户端需要使用connect函数:

//connect函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
  • 该函数的功能是发起连接请求并与服务器建立连接。(用于TCP协议的客户端
  • sockfd参数: 套接字描述符,用于标识一个已经打开的套接字。
  • addr参数: 指向一个sockaddr结构体的指针,包含了服务器的地址信息。
  • addrlen参数: 表示addr结构体的长度。
  • 返回值: 成功时,返回0,表示连接建立成功。失败时,返回-1,并设置errno变量以指示具体的错误原因。

客户端要想和服务端进行网络通信,首先要建立连接,然后进行网络通信,因此在客户端类内部的运行函数中实现建立连接和网络通信:

enum
{
    SOCKET_ERROR=1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
    CONNECT_ERROR
};
class TcpClient
{
    public: 
    TcpClient(std::string serverip, uint16_t serverport):_serverip(serverip), _serverport(serverport) {}
    void StartClient()
    {
        //建立连接
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_addr.s_addr = inet_addr(_serverip.c_str());
        peer.sin_port = htons(_serverport);
        if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) < 0)
        {
            std::cerr << "connect socket error" << strerror(errno) << std::endl;
            exit(CONNECT_ERROR);
        }
        std::string message;
        char buffer[128];
        while(true)
        {
            std::cout << "Please enter>";
            getline(std::cin, message);
            write(_sock, message.c_str(), message.size());
            int n = read(_sock, buffer, sizeof(buffer)-1);
            if (n > 0)
            {
                std::cout << "server echo#" << message << std::endl;
            }
            else if (n == 0)//服务端关闭
            {
                std::cout << "server quit" << std::endl;
                break;
            }
            else
            {
                std::cout << "write error" << std::endl;
                break;
            }
        }
    }
    private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

客户端启动

同样的,调用客户端类内部的函数进行客户端的初始化,并运行客户端。为了给错误运行客户端纠错,引入了命令行参数,在运行客户端时做纠错提示:

void Usage(const char *proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    std::unique_ptr<TcpClient> tpcr(new TcpClient(serverip, serverport));
    tpcr->InitClient();
    tpcr->StartClient();
    return 0;
}

程序测试

本地测试

在命令行输入指令启动服务端并加上要指定端口号,使用netstat -natp指令查看服务端的状态:

image-20231031181621496

服务端现处于监听状态,等待客户端的连接然后进行网络服务。

启动客户端时输入IP地址127.0.0.1和端口号8081:

image-20231031182507882

服务端获取到了连接,知晓了客户端的IP地址和端口号,客户端建立了连接可以开始发送消息。

使用客户端发送数据:

image-20231031182700386

服务端能够将接受的数据回传给客户端。

关闭客户端:

image-20231031182746236

关闭客户端后,read函数返回值为0,服务端会断开与该客户端的服务,但是服务端并没有停止运行。

网络测试

网络测试主要查看能否成功建立网络连接即可,其余现象和本地测试现象相同,在启动客户端时输入服务端所处的IP地址:

image-20231031183156269

单执行流服务器的弊端

使用两个客户端连接服务器后,两个客户端都显示了连接成功等待输入数据,但是服务端只获取了一个客户端的连接信息:

image-20231031183422798

两个客户端都向服务端发送数据,只有一个客户端能得到服务端的网络服务,得到回传的数据:

image-20231031183644649

关闭正在接收网络服务的客户端后,客户端才能获取另一个客户端的连接,并且给该客户端提供网络服务:

image-20231031183804756

单执行流的服务器

通过测试可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。这个服务器一次只能为一个客户端提供服务,是一个单执行流的服务端。

服务端需要获取连接,才能够给对应的客户端提供网络服务,因此正在接收网络服务的客户端关闭,其他客户端才能接收网络服务。

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

服务端处于监听状态后,客户端调用connect函数就可以与服务端建立连接,不受服务端是否获取连接的影响。

解决方法

单执行流的服务器一次只能给一个客户端提供服务,但是这样的服务端利用率是极低的,要解决这个问题就需要将服务器改为多执行流的,此时就要引入多进程或多线程。

多进程版TCP网络程序

多进程版与单执行流的服务端的区别在于当服务端获取连接后,将执行网络服务的任务交给子进程或孙子进程去处理,父进程依旧保持监听并获取连接,这样可以有多个客户端都可以接收网络服务。根据对回收子进程的策略不同分为捕捉SIGCHLD信号版的和孙子进程提供网络服务版的。

捕捉SIGCHLD信号

至于捕捉SIGCHLD信号的多进程版的代码改动相对简单,只需要在服务端运行函数中添加创建子进程,让子进程进行网络服务的部分:

class TcpServer
{
    public:
    void StartServer()
    {
        signal(SIGCHLD, SIG_IGN);//修改信号处理动作
        while (true)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << "获取连接成功:" << sock << "from" << _listensock << "," << clientip << "-" << clientport << std::endl;
            // 网络服务
            pid_t id = fork();//创建子进程
            if (id < 0) // 服务器过载
            {
                close(sock); // 无法承担任务,因此关闭文件
                continue;
            }
            else if (id == 0)
            {
                close(_listensock);//避免错误写入
                service(sock, clientip, clientport);
                exit(0);
            }
            close(sock);
        }
    }
    private:
    uint16_t _port;
    int _listensock;
    func_t _func; // 数据处理方法
};

子进程能够完成网络服务的原理

父进程的文件描述符表会被子进程所继承,也就是拷贝父进程的文件描述符表,因此父进程打开的文件,子进程也能够访问。子进程执行网络服务代码时,可以对执行网络通信的文件进行读写操作,即可完成网络服务的任务。

文件描述符表的关闭操作

执行网络服务的子进程关闭用于监听的文件描述符的原因是避免错误的读写操作。

父进程关闭获取连接所得的文件描述符的原因是父进程不进行网络服务不需要使用该文件描述符,而文件描述符是有限的,必须关闭,避免文件描述符泄露。

子进程回收策略

父进程调用signal函数将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

程序测试

使用两个客户端连接服务端:

image-20231031201721051

两个客户端现在可以同时接收网络服务了。

可以看到服务端为这个两个客户端都创建了子进程进行网络服务:

image-20231031201926442

客户端关闭后,系统自动回收了进入僵尸状态的子进程:

image-20231031202448615

孙子进程提供网络服务

孙子进程提供网络服务和捕捉SIGCHLD信号在代码层面上,改动如下:

class TcpServer
{
    public:
    void StartServer()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << "获取连接成功:" << sock << "from" << _listensock << "," << clientip << "-" << clientport << std::endl;
            pid_t id = fork();
            if (id < 0) 
            {
                close(sock); 
                continue;
            }
            else if (id == 0)
            {
                close(_listensock);
                if (fork() > 0) exit(0);//修改语句
                service(sock, clientip, clientport);
                exit(0);
            }
            close(sock);
            waitpid(id, nullptr, 0);//修改语句
            std::cout << id << "子进程等待成功" << std::endl;//修改语句
        }
    }
    private:
    uint16_t _port;
    int _listensock;
    func_t _func; 
};

孙子进程完成网络服务的原理

孙子进程完成网络服务的原理和子进程完成网络服务的原理相同,孙子进程会拷贝子进程的文件描述符表,孙子进程能够访问获取连接时得到的文件,因此孙子进程具有完成网络服务的能力。

子进程和孙子进程的回收机制

子进程在创建完孙子进程后就会退出,此时父进程立刻就能回收子进程,并接着完成接下来的监听和获取连接的任务。

孙子进程会在子进程死后变成孤儿进程,由系统回收。

程序测试

启动两个客户端连接服务端:

image-20231031204656315

可以看到客户端连接服务端成功后,服务端立刻回收了子进程,并且网络服务交给了孙子进程进行处理,孙子进程由于父进程终止,孙子进程的父进程ID变成为1,也就是被操作系统接管了。

将客户端都退出后,孙子进程由于执行结束也被操作系统回收了:

image-20231031204948288

多线程版TCP网络服务

相比单执行流服务端,多线程版TCP网络服务中服务端会在获取连接后,让其他线程完成网络服务任务,只需要在服务端运行函数中添加创建线程执行网路服务任务即可:

class TcpServer;
class ThreadData//线程执行所需数据
{
    public:
    ThreadData(int sock, const std::string& clientip, uint16_t clientport, TcpServer *ts):_sock(sock), _clientip(clientip), _clientport(clientport), _ts(ts) 
    {}
    int _sock;
    std::string _clientip;
    uint16_t _clientport;
    TcpServer *_ts;
};

class TcpServer
{
    public:
    void StartServer()
    {
        while (true)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << "获取连接成功:" << sock << "from" << _listensock << "," << clientip << "-" << clientport << std::endl;
            // 网络服务
            pthread_t tid;
            ThreadData *td = new ThreadData(sock, clientip, clientport, this);
            pthread_create(&tid, nullptr, threadRoutine, td);
        }
    }
    static void *threadRoutine(void *args)//线程执行函数
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        td->_ts->service(td->_sock, td->_clientip, td->_clientport);
        return nullptr;
    }
    private:
    uint16_t _port;
    int _listensock;
    func_t _func;
};

线程是否需要关闭监听套接字的文件描述符?

子进程会拷贝父进程的文件描述符表,因此子进程关闭任一文件描述符不影响父进程,但线程不同于进程,一个进程内部的多个线程共享同一份数据,包括文件描述符表,线程关闭监听套接字的文件描述符会影响主线程监听,因此线程无需关闭监听套接字的文件描述符。

线程回收机制

默认情况下,新创建的线程退出后,需要对其主动进行回收操作,否则无法释放资源,从而造成系统泄漏。但我们将线程分离,当线程退出时,自动释放线程资源。

程序测试

启动两个客户端连接服务端:

image-20231031212451966

可以看到服务端为两个客户端分别创建一个线程分别为它们执行网络服务。

关闭客户端,系统会自动回收终止的线程:

image-20231031213239324

线程池版TCP网络程序

引入线程池

引入线程池的作用是为了减少创建线程的开销,提高主机的效率。线程池的编写主播在另一边博客Linux]线程池-CSDN博客中有更详细的说明。

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

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

const int N = 5; // 线程池内线程数量

template <class T>
class ThreadPool
{
public:
    ThreadPool(int num = N) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }

    void UnLockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }

    void threadWait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }

    void threadWakeUP()
    {
        pthread_cond_signal(&_cond);
    }

    T getTask()
    {
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    bool isEmpty()
    {
        return _tasks.empty();
    }

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());

        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            tp->LockQueue();
            while (tp->isEmpty())
            {
                tp->threadWait();
            }
            T t = tp->getTask();
            tp->UnLockQueue();
            t.Run();
        }
    }

    void Start()
    {
        pthread_t tid;
        for (int i = 0; i < _num; i++)
        {
            pthread_create(&tid, nullptr, threadRoutine, this);
        }
    }

    void PushTask(T &task) // 添加任务
    {
        LockQueue();
        _tasks.push(task);
        threadWakeUP();
        UnLockQueue();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

private:
    int _num;                        // 线程数
    std::queue<T> _tasks;            // 任务队列

    pthread_mutex_t _mutex; // 保证互斥访问任务队列这一共享资源
    pthread_cond_t _cond;   // 根据任务队列中的任务数量控制线程的等待和运行
};

引入任务类

由于线程池是用模板类编写的,因此可以编写一个任务类传入线程池,任务类中需要有获取连接得到的文件描述符、客户端IP、客户端端口号用于完成网络服务,并且任务类需要实现Run函数供线程池调用,另外还实现了service函数实现网络服务的具体实现:

#include <iostream>
#include <unistd.h>


void service(int sock, std::string &clientip, uint16_t clientport)//网络服务
{
    std::string who = clientip + "-" + std::to_string(clientport);
    char buffer[128];
    while (true)
    {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1); // 数据接收
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << who << ">>>" << buffer << std::endl;
            write(sock, buffer, n); // 发送数据
        }
        else if (n == 0)
        {
            close(sock);
            std::cout << "client quit,me too" << std::endl;
            break;
        }
        else
        {
            close(sock);
            std::cout << "read error" << std::endl;
            break;
        }
    }
}

class Task
{
public:
    Task(int sock, std::string &clientip, uint16_t clientport)
        :_sock(sock), _clientip(clientip), _clientport(clientport)
    {
    }
    void Run()//提供给线程池调用
    {
        service(_sock , _clientip, _clientport);
    }

private:
    int _sock;
    std::string _clientip;
    uint16_t _clientport;
};

服务端实现

服务端类实现中需要将任务类引入并使用,具体代码如下:

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR
};
static const uint16_t default_port = 8081;
static const int backlog = 32;
class TcpServer
{
    public:
    TcpServer(uint16_t port = default_port) : _port(port) {}
    void InitServer()
    {
        // 创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            std::cerr << "create socket error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERROR);
        }
        // 绑定IP地址和端口号
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(_port);
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind socket error" << strerror(errno) << std::endl;
            exit(BIND_ERROR);
        }
        // 监听
        if (listen(_listensock, backlog) < 0)
        {
            std::cerr << "listen socket error" << strerror(errno) << std::endl;
            exit(LISTEN_ERROR);
        }
    }
    void StartServer()
    {
        ThreadPool<Task> tp;
        tp.Start();
        while (true)
        {
            // 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sock < 0)
            {
                std::cerr << "accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << "获取连接成功:" << sock << "from" << _listensock << "," << clientip << "-" << clientport << std::endl;
            // 网络服务
            Task t(sock, clientip, clientport);
            tp.PushTask(t);
        }
    }
    private:
    uint16_t _port;
    int _listensock;
};

程序测试

启动服务端,并且启动一个任务监视窗口输入指令while :; do ps -aL | head -1 && ps -aL | grep tcp_server; sleep 1; done不断监视线程:

image-20231101161056222

可以看出在启动服务端后,立刻就为线程池创建了5个线程等待任务。

启动客户端连接服务端进行网络通信:

image-20231101161329316

值得注意的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出。

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

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

相关文章

scrapy-redis分布式爬虫(分布式爬虫简述+分布式爬虫实战)

一、分布式爬虫简述 &#xff08;一&#xff09;分布式爬虫优势 1.充分利用多台机器的带宽速度 2.充分利用多台机器的ip地址 &#xff08;二&#xff09;Redis数据库 1.Redis是一个高性能的nosql数据库 2.Redis的所有操作都是原子性的 3.Redis的数据类型都是基于基本数据…

攻防世界-web-bug

1. 问题描述 没有额外的描述&#xff0c;仅仅是这样的一个登录界面 但是&#xff0c;我们注意到有注册&#xff08;Register&#xff09;和找回密码&#xff08;Findpwd&#xff09;这俩按钮 注册界面如下&#xff1a;需要输入用户名&#xff0c;密码&#xff0c;生日及地址 …

自动曝光算法(第一讲)

序言 失业在家无事&#xff0c;想到以后换方向不做自动曝光了&#xff0c;但是自动曝光的工作经验也不能浪费了&#xff0c;准备写一个自动曝光的教学&#xff0c;留给想做自动曝光的小伙伴参考。笔者当时开发自动曝光没有按摄影的avtvevbvsv公式弄&#xff0c;而是按正确的增…

K-means(K-均值)算法

K-means&#xff08;k-均值&#xff0c;也记为kmeans&#xff09;是聚类算法中的一种&#xff0c;由于其原理简单&#xff0c;可解释强&#xff0c;实现方便&#xff0c;收敛速度快&#xff0c;在数据挖掘、聚类分析、数据聚类、模式识别、金融风控、数据科学、智能营销和数据运…

UML类图关系

1.依赖 依赖关系由箭头表示&#xff0c;含义为A类在类中用到了B类&#xff0c;如B类作为A类的属性、参数、返回值等都属于依赖关系。 2.泛化&#xff08;继承&#xff09; 泛化用三角箭头和直线表示&#xff0c;extend。 3.实现 实现用三角箭头和虚线表示&#xff0c;在…

Mac 配置环境变量

Mac 配置环境变量 修改配置文件 vim ~/.bash_profile i进入编辑模式. Esc&#xff1a;wq 保存文件 esc:q 退出 如&#xff1a;jdk环境变量配置 JAVA_HOME/Library/Java/JavaVirtualMachines/jdk1.8.0_361.jdk/Contents/HomeCLASSPATH$JAVA_HOME/lib/tools.jar:$JAVA_HOME/…

11月的『备考学习计划』+高效的作息时间表 超好用~

每日作息时间表 每天有三个时间段学习效率高 上午10点左右 下午4点左右 晚上8点-10点左右 坚持住了&#xff0c;学习效果事半功倍 有同感的同学 可以举举手&#x1f91a;&#xff0c;点点赞&#x1f493; 每日作息时间表 6:30-7:00起床 6:30---7:00是起床的最佳时刻&am…

Ubuntu自建git服务器

Ubuntu 安装 gitlab-ce sudo apt-get update sudo apt-get install gitlab-ce 安装成功 sudo apt-get install gitlab-ce 正在读取软件包列表... 完成 正在分析软件包的依赖关系树 正在读取状态信息... 完成 下列【新】软件包将被安装&#xff1a;gitlab-ce 升…

解决方案 | 便民提效,电子签助力医疗保障服务模式创新

2023年2月&#xff0c;中共中央、国务院印发了《数字中国建设整体布局规划》&#xff0c;并发出通知&#xff0c;要求各地区各部门结合实际认真贯彻落实。《规划》指出&#xff0c;提升数字化服务水平&#xff0c;加快推进“一件事一次办”&#xff0c;推进线上线下融合&#x…

呼叫中心的重要考核指标

呼叫中心在运营过程中越来越精细化&#xff0c;在信息化管理的时代&#xff0c;呼叫中心系统是必不可少的&#xff0c;而呼叫中心的管理人员为了提升运营效率&#xff0c;通常会根据业务目标设置各种业务的考核指标&#xff0c;而我也根据OKCC在呼叫中心项目运营过程中的经验&a…

Window下SRS服务器的搭建

---2023.7.23 准备材料 srs下载&#xff1a;GitHub - ossrs/srs at 3.0release 目前srs release到5.0版本。 srs官方文档&#xff1a;Introduction | SRS (ossrs.net) Docker下载&#xff1a;Download Docker Desktop | Docker 进入docker官网选择window版本直接下载。由…

中颖单片机SH367309全套量产PCM,专用动力电池保护板开发资料

方案总体介绍 整套方案硬件部分共2块板子&#xff0c;包括MCU主板&#xff0c;采用SH79F6441-32作为主处理器。MCU主板包括2个版本。PCM动力电池保护板采用SH367309。 软件方案采用Keil51建立的工程&#xff0c;带蓝牙的版本&#xff0c;支持5~16S电池。 硬件方案--MCU主板 MC…

Android开发知识学习——TCP / IP 协议族

文章目录 学习资源来自&#xff1a;扔物线TCP / IP 协议族TCP连接TCP 连接的建立与关闭TCP 连接的建立为什么要三次握手&#xff1f; TCP 连接的关闭为什么要四次挥手&#xff1f; 为什么要⻓连接&#xff1f; 常见面试题课后题 学习资源来自&#xff1a;扔物线 TCP / IP 协议…

Simulink HDL--如何生成Verliog代码

Simulink生成HDL的方法可以快速设计出工程&#xff0c;并结合FPGA验证&#xff0c;相比于手写HDL代码虽然存在代码优化不足的问题。但是方法适合做工程的快速验证和基本框架搭建。本文将介绍Simulink HDL生成Verliog代码的基本操作 1、逻辑分析仪功能 Simulink生成HDL前需要通…

想翻译pdf文档,试了几个工具对比:有阿里(完全免费,快,好用,质量高,不用注册登录)道最好(有限免费) 百度(有限免费)和谷歌完全免费(网不好)

文档翻释作为基础设施&#xff0c;工作必备。 阿里 &#xff08;完全免费&#xff0c;快&#xff0c;好用&#xff0c;质量高&#xff0c;不用注册登录&#xff0c;无广告&#xff09;我给满分 https://translate.alibaba.com/#core-translation 先选好语言。 Google(完全免…

数据结构和算法——用C语言实现所有图状结构及相关算法

文章目录 前言图的基本概念图的存储方式邻接矩阵邻接表十字链表临界多重表 图的遍历最小生成树普里姆算法&#xff08;Prim&#xff09;克鲁斯卡尔算法&#xff08;Kruskal&#xff09; 最短路径BFS求最短路径迪杰斯特拉算法&#xff08;Dijkstra&#xff09;弗洛伊德算法&…

03-对象

对象 对象1.对象的创建字面量模式构造函数模式 2.对象的访问3.新增删除对象中的属性4.Object显示类型转换(强制类型转换)ECMAScript中可用的3种强制类型转换如下&#xff1a;Boolean(value)String(value)Number(value)Object类型到Boolean类型Object类型转String类型转换规则&a…

leetcode-字符串

1.反转字符串LeetCode344. 20230911 难度为0&#xff0c;此处就不放代码了 注意reverse和swap等一系列字符串函数什么时候该用&#xff0c;记一记库函数 swap可以有两种实现&#xff0c;涨知识了&#xff0c;除了temp存值还可以通过位运算&#xff1a;s[i] ^ s[j]; s[j] ^ s[i…

Unity Animator cpu性能测试

测试案例&#xff1a; 场景中共有4000个物体&#xff0c;挂在40个animtor 上&#xff0c;每个Animator控制100个物体的动画。 使用工具&#xff1a; Unity Profiler. Unity 版本&#xff1a; unity 2019.4.40f1 测试环境&#xff1a; 手机 测试过程&#xff1a; 没有挂…

Java 设计模式——命令模式

目录 1.概述2.结构3.案例实现3.1.命令接口3.2.具体命令3.3.接受者3.4.调用者3.5.测试 4.优缺点5.使用场景6.JDK 源码解析——Runnable 1.概述 &#xff08;1&#xff09;日常生活中&#xff0c;我们出去吃饭都会遇到下面的场景&#xff1a; &#xff08;2&#xff09;命令模…