【计算机网络】网络编程套接字(二)

news2024/11/16 18:09:40

文章目录

  • 网络编程套接字(二)
      • 简单TCP服务器实现
        • 创建套接字
        • 服务器绑定
        • 服务器监听
        • 服务器接收连接
        • 服务器处理请求
      • 简单TCP客户端实现
        • 创建套接字
        • 客户端发起连接
        • 客户端发起请求
      • 服务器简单测试
      • 服务器简单测评
      • 多进程版TCP服务器
        • 捕捉SIGCHLD信号
        • 孙子进程提供服务
      • 多线程版TCP服务器
      • 线程池版TCP服务器

网络编程套接字(二)

简单TCP服务器实现

我们将会使用到的头文件放在comm.h文件中

#include <iostream>
#include <memory.h>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

创建套接字

创建过程和UDP服务器几乎完全一样,除了使用的是TCP服务器使用的是流式服务(SOCK_STREAM),UDP使用的是数据包服务(SOCK_DGRAM)

#include "comm.h"

class TcpServer {
public:
  TcpServer(){};
  void InitServer(){
    sock = socket(AF_INET, SOCK_STREAM, 0);     
    if (sock < 0) {
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
  }
  ~TcpServer(){
    if (sock >= 0) close(sock);
  }
private:
  void Socket();
  int sock;
};

服务器绑定

绑定的过程和UDP服务器也是相同的,可以看着复习一下

     // 2 服务器绑定
      sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr = INADDR_ANY;
      if (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        std::cerr << "bind error" << std::endl;
        exit(3);
      }
      std::cout << "server bind success" << std::endl;             

定义好struct sockaddr_in结构体后,最好使用memset对该结构体进行初始化,也可以使用bzero函数进行清空

void bzero(void *s, size_t n);

服务器监听

TCP服务器是面向连接的,,客户端在正式向TCP服务器发送数据之前必须先于TCP服务器建立连接,然后才可以进行通信

因此TCP服务器需要时刻注意是否有客户端发来连接请求,需要将TCP创建的套接字设置成监听状态

int listen(int socket, int backlog);
  • socket : 需要设置为监听状态的套接字对应的文件描述符
  • backlog: 全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接会先被放入连接队列,该参数代表这个连接队列的最大长度,一般设置成5或者10即可
  • 监听成功返回0,失败返回-1同时设置错误码
	  // 3 服务器监听
      if (listen(listen_sock, 10) < 0) {
        std::cerr << "listen error" << std::endl;
        exit(4);
      }

这里我们发现上文的sockfd其实是一个被监听的文件描述符,为了变量命名更容易让人理解,我们把sockfd改为listen_sock,并且在初始化TCP服务器中,只有套接字创建成功,绑定成功,监听成功,TCP服务器的初始化才算完成

vim 替换单词

全文替换 ::#sockfd#listen_sock#g 使用 :#str1#str2#g进行全文替换,将str1全部替换成str2

局部替换: : 20, 30s#str1#str2#g (将20到30行内的str1替换成str2)

当前行替换: : s#str1#str2#g (将光标所在行内的str1 替换成 str2)

服务器接收连接

TCP服务器初始化后就可以开始运行了,但是TCP服务器与客户端在进行网络通信之前,服务器需要先获取到客户端的连接请求

 int accept(int socket, struct sockaddr *restrict address,
              socklen_t *restrict address_len);
  • socket : 特定的监听套接字,表示从该监听文件中获取连接

  • address : 对端网络相关的属性信息

  • addrlen: 传入希望读取到的address结构体的长度,返回实际读取到的address结构体的长度,是一个输入输出参数

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

监听套接字和accept函数返回套接字的区别

  • 监听套接字:用于获取连接请求信息,accept函数不断从监听文件中获取新连接
  • accept返回套接字:用于为这个连接提供服务,进行真正的业务数据传输

服务端获取连接

  • accept函数获取连接时可能会失败,但是服务器不能因为获取某一个连接失败就退出,因此服务端获取连接失败后还需要继续获取连接

  • 如果需要将获取到的客户端IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将网络序列转换成主机序列

  • inet_ntoa函数会先将整数IP转换成主机序列,然后再将其转换成字符串IP

 void Start() {
    for (;;) {
      // 获取连接
      struct sockaddr_in peer;
      memset(&peer, 0, sizeof(peer));
      socklen_t len = sizeof(peer);
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
      if (sock < 0) {
        std::cerr << "accept error, continue" << std::endl;
        continue;
      }
      std::string client_ip = inet_ntoa(peer.sin_addr);  
      short client_port = ntohs(peer.sin_port);
      cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;
      Service(sock, client_ip, client_port);      // 进行业务处理
    }
  }

服务器处理请求

现在服务器已经可以和客户端建立连接了,接下来就是到了通信阶段。我们只需要通过对accept函数打开的网络文件进行读写,就可以完成网络数据的传输和接收了。为了能让双方都可以看到现象,这里就实现一个简单的回声TCP服务器

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t read(int fildes, void *buf, size_t nbyte);
  • fd:特定的文件描述符,表示从该文件描述符中读取数据

  • buffer : 数据的存储位置,表示将读取数据到该缓冲区中

  • count : 期望从该文件描述符中读取的字节数

  • 返回值大于零代表本次读取到的字节数,等于零表示对端对出,小于零说明读取出现错误

ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
ssize_t write(int fildes, const void *buf, size_t nbyte);
  • fd: 特定的文件描述符,表示将把数据写入该文件描述符对应的文件

  • buffer : 需要写入的数据

  • count :期望写入的字节数

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

还需要注意到,服务端读取的数据是从服务套接字中获取的,如果客户端断开连接服务结束那么需要关闭对应文件。因为文件描述符本质就是数组的下标,是有限的,如果一直占用会导致文件描述符泄漏

void Service(int sock, std::string client_ip, short client_port) {
    for (;;) {
#define BUFFER_SIZE 128
      char buffer[BUFFER_SIZE];
      ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);  				// 读取请求
      if (size > 0) {          // 读取成功
        buffer[size] = 0;
        std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;
        std::string response = "tcp server say # ";
        response += buffer;
        if (write(sock, response.c_str(), response.size()) < 0) {       // 发送响应
          std::cerr << "write response error" << std::endl;
        } else {
          std::cout << "send response success" << std::endl;
        }
      } else if (size == 0) { // 对端关闭连接
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      } else {                // 读取失败
        std::cerr << "read request error" << std::endl;
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      }
    }
  }

简单TCP客户端实现

创建套接字

class TcpClient{
public:  
  TcpClient(std::string& ip, short port)
    : sockfd(-1), server_ip(ip), server_port(port){}
  ~TcpClient(){
    if (sockfd > 0) close (sockfd);
  };

  int ClientInit() {
    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
      std::cerr << "socket error" << std::endl;
      exit(2);
    }
    std::cout << "socket success" << std::endl;

    return 0;
  }
private:
  int sockfd;
  std::string server_ip;
  short server_port;
};


客户端发起连接

由于客户端不需要用户手动绑定也不需要监听,所以客户端创建好套接字后就可以直接向服务端发起连接请求

 int connect(int socket, const struct sockaddr *address, socklen_t address_len);
  • socket : 特定的套接字,表示通过该套接字发起连接请求
  • address : 对端的网络相关信息
  • addrlen: 传入的addr结构体的长度
  • 绑定成功返回0,连接失败返回-1,同时错误码被设置
  void Start() {
    // 发送连接请求
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    if (connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) {
      std::cerr << "connect error" << std::endl;
      exit(3);
    }
    std::cout << "connect success" << std::endl;
    Request();
  }

客户端发起请求

这里的代码逻辑非常简单,可以稍微看一看

void Request() {
    for (; ;) {
      std::string msg;
      getline(cin, msg);
      if (msg == "quit") {
        break;
      }
      write(sockfd, msg.c_str(), msg.size());

#define BUFFER_SIZE 128
      char buffer[BUFFER_SIZE];
      ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);
      if (size > 0) {
        buffer[size] = 0;
        std::cout << buffer << std::endl;
      }
    }
  }

服务器简单测试

首先使用telnet进行连接测试,可以看到服务器可以正常建立连接。控制telnet给服务器发送信息服务器可以接收并能返回响应

使用netstat命令查看,可以看到一个名为tcp_server的进程正处于监听状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiPnBeEe-1688734778610)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707112949813.png)]

使用我们的客户端连接,可以看到服务端可以打印客户端的IP地址和端口号以及发送的数据,客户端也可以接收服务器发来的响应。客户端一旦退出,服务器也会立刻接收到并作出反应。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pm6hk0UN-1688734711199)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707113253782.png)]

服务器简单测评

当我们仅仅使用一个客户端连接服务端时,客户端可以正常享受服务器的服务。但是若再来一个客户端时,虽然新来的客户端也可以成功建立连接,但是我们的服务器正在为第一个客户端提供服务,无法立马处理第二个客户端的请求。只有等第一个客户端推出后,才能对第二个客户端发来的数据进行打印

单执行流服务器

这是因为我们的服务器是一个单执行流的,这种服务一一次只能为一个客户端提供服务。

单执行流服务器为什么可以同时和多个客户端建立连接

当服务端在给第一个客户端提供服务期间,第二个客户端发送连接请求时是成功的,这是因为连接其实已经建立,只是服务端还没有调用accept函数将连接获取上来罢了

前文在介绍listen接口的时候提到一个参数backlog,实际上操作系统在底层会为我们维护一个连接队列。服务端没有accept的新连接会被放在这个连接队列中,而这个队列的最大长度是由backlog决定的。所以虽然服务端没有使用accept获取第二个客户端发来的请求,但实际上链接已经建成了

那么如何解决服务器只能给一个客户端提供服务这个问题呢?? 很简单只要提供多进程版的服务器或者多线程版的就可以了

多进程版TCP服务器

当服务端调用accept函数获取到新连接,并未新连接打开网络文件后,让当前执行流调用fork()函数创键子进程,让子进程为父进程获取到的链接提供服务,由于父子进程是两个不同的执行流,父进程创建子进程后可以继续从监听套接字中获取新连接,不需要关心服务

子进程会继承父进程的文件描述符表

文件描述符表是隶属于一个进程的,子进程创建后会"复制"一份父进程的文件描述符表,之后父子进程之间会保持独立性。对于套接字文件(网络文件)也是一样的,父进程创建的子进程也会继承父进程的套接字文件信息,此时子进程也就能对特定的套接字文件进行读写操作

等待子进程问题

当父进程创建子进程后,父进程是必须等待子进程退出的,以防止子进程变成僵尸进程造成内存泄漏。如果服务端进行阻塞式等待子进程,那么服务端还是必须等待客户端的服务完毕才能获取下一个服务请求,显然不合理。若采用非阻塞方式等待子进程,那么服务端就必须将所有子进程的PID保存下来,并每隔一段时间要对所有链接进行检测,显然非常麻烦

总之,无论采用阻塞或非阻塞的方式等待子进程,都不能很好的帮助我们将获取链接和提供服务分离。

不等待子进程退出的方式

1、 捕捉SIGCHLD信号,将其处理动作设置成忽略

2、让父进程创建子进程,子进程再创建孙子进程,子进程退出,孙子进程就被操作系统领养并为客户端进行服务

捕捉SIGCHLD信号

  void Start() {    
    signal(SIGCHLD, SIG_IGN); 								// 忽略SIGCHLD信号    
    for (;;) {    
      // 获取连接    
      struct sockaddr_in peer;    
      memset(&peer, 0, sizeof(peer));    
      socklen_t len = sizeof(peer);    
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);    
      if (sock < 0) {    
        std::cerr << "accept error, continue" << std::endl;    
        continue;    
      }    
      std::string client_ip = inet_ntoa(peer.sin_addr);      
      short client_port = ntohs(peer.sin_port);    
      cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;                                                                                               
      pid_t id = fork();    								// 创建子进程执行服务
      if (id == 0) {    
        Service(sock, client_ip, client_port);    
        exit(0);    
      }    
    }    
  }    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMi1OtBx-1688734711199)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707123144765.png)]

孙子进程提供服务

  void Start() {
     // signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号
    for (;;) {
      // 获取连接
      struct sockaddr_in peer;
      memset(&peer, 0, sizeof(peer));
      socklen_t len = sizeof(peer);
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
      if (sock < 0) {
        std::cerr << "accept error, continue" << std::endl;
        continue;
      }
      std::string client_ip = inet_ntoa(peer.sin_addr);  
      short client_port = ntohs(peer.sin_port);
      cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;
        
        
        
      pid_t id = fork();
      if (id == 0) {    // 子进程
        pid_t id = fork();
        if(id == 0){    // 孙子进程
          Service(sock, client_ip, client_port);
          exit(0);
        }
        exit(0);
      }
      waitpid(id, NULL, 0);  // 直接将子进程回收了
    }
  }
while :; do ps -axj | head -1 && ps -axj | grep tcp_server | grep -v grep; echo "############"; sleep 1; done  # 监视脚本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZOZXrxO-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707151802143.png)]

可以看到子进程直接就推出了,孙子进程正在为客户端提供服务。当客户端推出后,孙子进程直接就被操作系统回收了。它的PPID为1号进程,表明这是一个孤儿进程

多线程版TCP服务器

创建进程的成本非常高,而创建线程的成本就会小很多,因为线程本质就是再进程地址空间中运行的,创建出来的线程共享大部分资源。因此实现多执行流的服务器最好采用多线程进行实现

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ljGpvcxw-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707155728792.png)]

// 1、参数列表
struct Args{
  Args(int _sock, std::string& _ip, short _port)
    : sock(_sock), ip(_ip), port(_port) {}

  int sock;
  std::string ip;
  short port;
};

void Start() {
    for (;;) {
      // 获取连接
      struct sockaddr_in peer;
      memset(&peer, 0, sizeof(peer));
      socklen_t len = sizeof(peer);
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
      if (sock < 0) {
        std::cerr << "accept error, continue" << std::endl;
        continue;
      }
      std::string client_ip = inet_ntoa(peer.sin_addr);  
      short client_port = ntohs(peer.sin_port);
      cout << "get a new link->"  << sock << "[" << client_ip << ":" << client_port << "]" << endl;
        
      // 2、编写多线程部分
      pthread_t tid; 
      struct Args *args = new struct Args(sock, client_ip, client_port);
      pthread_create(&tid, NULL, Service, (void*)args); 
      pthread_detach(tid);
    }
  }

  // 3、将Service函数改为静态函数,使用struct Args* 指针将三个参数构成结构体传进去
  static void* Service(void* arg) {
    struct Args* args = (struct Args*)arg;
    int sock = args->sock;
    std::string client_ip = args->ip;
    short client_port = args->port;
    delete args;
      
      
      
    for (;;) {
#define BUFFER_SIZE 128
      char buffer[BUFFER_SIZE];
      ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);
      if (size > 0) {          // 读取成功
        buffer[size] = 0;
        std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;
        std::string response = "tcp server say # ";
        response += buffer;
        if (write(sock, response.c_str(), response.size()) < 0) {
          std::cerr << "write response error" << std::endl;
        } else {
          std::cout << "send response success" << std::endl;
        }
      } else if (size == 0) { // 对端关闭连接
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      } else {                // 读取失败
        std::cerr << "read request error" << std::endl;
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      }
    }
  }

该对线程服务器存在的问题

  • 每当新线程到来时,服务端的主线程才会为客户端创建新线程,而服务结束又会将该线程销毁。就像我们去食堂吃饭,我们去了食堂阿姨才开始做饭。效率低下。
  • 如果有大量的客户端连接请求到来,计算机就要一一创建服务线程,线程越多CPU的压力也就越大。因为CPU要在这些线程之间来回切换,线程间切换的成本就变得很高,此外线程变多,每个线程被调度到的时间就变长了,用户体验变差

解决方案

  • 可以预先创建一批线程,当有客户端请求连接到来时就为这些线程提供服务。而不是客户端来了才创建线程
  • 当某个线程对客户端提供服务完成后,不让该线程推出,让该线程继续给下一个客户端提供服务。如果没有可以让线程先进入休眠状态
  • 服务端创建的一批线程数量不能太多。此外,如果有海量客户端接连到来,可以将这些新来的连接放在等待队列中进行排队,等服务端这一批线程有空闲线程后再将连接拿上来处理

实际解决上述问题就是要让我们再服务端引入线程池。线程池可以预先存储线程并使线程循环往复的工作,并且线程池中还有一个任务队列可以用于存储任务。如果有任务就从任务队列中Pop任务,并调用任务对应的Run函数对任务进行处理,如果没有任务就进入休眠状态

线程池版TCP服务器

线程池的实现在多线程那一章已经讲过了,这里直接套用了

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>

#define THREAD_NUM 5


template<typename T>
class Thread_Pool{
public:
  static Thread_Pool* GetInstance(size_t _thread_num = THREAD_NUM);
  static void* Routine(void* arg);
  ~Thread_Pool();

  void PushTask(const T& task);
  void PopTask(T* task);

  

private:
  Thread_Pool(size_t _thread_num);
    
private
  bool IsEmpty() { return task_que.empty(); }
  void QueueLock() { pthread_mutex_lock(&mtx); }
  void QueueUnLock() { pthread_mutex_unlock(&mtx); }
  void Wait() { pthread_cond_wait(&cond, &mtx); }
  void Wakeup() { pthread_cond_signal(&cond); }

private:

  size_t thread_num;
  std::queue<T> task_que;
  pthread_mutex_t mtx;
  pthread_cond_t cond;
  static Thread_Pool* instance;
};

template<typename T>
Thread_Pool<T>* Thread_Pool<T>::instance = nullptr;

template<typename T>
Thread_Pool<T>* Thread_Pool<T>::GetInstance(size_t _thread_num) {
  if (instance == nullptr) 
    instance = new Thread_Pool(_thread_num);
  return instance;
}

template<typename T>
Thread_Pool<T>::Thread_Pool(size_t _thread_num) 
  : thread_num(_thread_num){
    pthread_mutex_init(&mtx, NULL);
    pthread_cond_init(&cond, NULL);
    for (int i = 0; i < thread_num; i++) {
      pthread_t tid;
      pthread_create(&tid, NULL, Routine, (void*)this);
      pthread_detach(tid);
    }
  }

template<typename T>
Thread_Pool<T>::~Thread_Pool() {
  pthread_mutex_destroy(&mtx);
  pthread_cond_destroy(&cond);
}

template<typename T> 
void Thread_Pool<T>::PushTask(const T& task) {
  QueueLock();
  task_que.push(task);
  QueueUnLock();
  Wakeup();
}

template<typename T>
void* Thread_Pool<T>::Routine(void* arg) {
  Thread_Pool<T>* tp = (Thread_Pool<T>*)arg;
  tp->QueueLock();
  while (tp->IsEmpty()) {
    tp->Wait();
  }
  T* task = new T;
  tp->PopTask(task);
  tp->QueueUnLock();

  task->Run();
  // std::cout << task << std::endl; // for test
  delete task;
}

template<typename T>
void Thread_Pool<T>::PopTask(T* task) {
  *task = task_que.front();
  task_que.pop();
}

现在想向服务器中引入线程池,因此在服务器类中新增一个线程池的指针成员

  • 在实例化服务器对象时,先将线程池指针初始化为空
  • 当服务器初始化完毕,进入正常运行阶段使用GetInstance接口获取单例线程池。
  • 主线程之后就用于获取连接,然后将获取到的客户端ip, port以及打开的网络文件sockfd打包成一个任务交给线程池的任务队列

线程池中的线程就通过不断获取任务队列中的任务,通过task中包含的信息为客户端提供服务

这实际也是一个生产消费模型,其中监听进程就是任务的生产者,线程池中的若干线程就是消费者,交易场所就是线程池中的任务队列

任务类的设计

任务类中必须包含服务器和客户端进行通信所需要的数据信息,包含网络套接字,客户端的IP,客户端的端口号。表示该任务是为哪一个客户端提供服务的,使用的是哪一个网络文件

任务类中还必须带有一个Run()方法,线程池中的线程拿到数据后交给Run方法对任务进行处理(通信),这个方法实际就是上文实现的Service函数,将其放入任务类中充当Run()方法,但是这样实际上并不利于软件分层。我们可以给任务类新增一个仿函数,当任务执行Run方法处理任务时就可以以回调的方式处理该任务

#pragma once 
#include <iostream>
#include "handler.hpp"

class Task{
public:
  Task() {};
  ~Task() {};
  Task(int _sockfd, std::string& _client_ip, short _client_port)
    : sockfd(_sockfd), client_ip(_client_ip), client_port(_client_port){}
  void Run(){ handler(sockfd, client_ip, client_port); }
private:
  int sockfd;
  std::string client_ip;
  short client_port;
  Handler handler;
  
};

仿函数类 Handler类

使用Handler类可以让我们的服务器处理不同的任务,实际想要怎么处理这个任务得由Handler函数定。如果想让服务器处理其它任务,只需要修改Handler当中的()重载函数即可,比如可以增加一个int 类型参数flag,当flag ==1 , flag == 2 ……的时候的就可以提供不同的处理方法

#include "comm.h"

class Handler{
public:
  Handler(){};
  ~Handler(){};
  void operator()(int sock, std::string client_ip, int client_port) {
    for (;;) {
#define BUFFER_SIZE 128
      char buffer[BUFFER_SIZE];
      ssize_t size = read(sock, buffer, BUFFER_SIZE - 1);
      if (size > 0) {          // 读取成功
        buffer[size] = 0;
        std::cout << client_ip << ":" << client_port << " # " << buffer << std::endl;
        std::string response = "tcp server say # ";
        response += buffer;
        if (write(sock, response.c_str(), response.size()) < 0) {
          std::cerr << "write response error" << std::endl;
        } else {
          std::cout << "send response success" << std::endl;
        }
      } else if (size == 0) { // 对端关闭连接
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      } else {                // 读取失败
        std::cerr << "read request error" << std::endl;
        std::cout << client_ip << ":" << client_port << " quit..." << std::endl;
        close(sock);
        break;
      }
    }
  }
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wIk5pgoM-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707205621139.png)]

现在无论有多少个客户端发送请求,服务端只会有5个线程为其提供服务,线程池中的线程数不会因为客户端的增多而增多。这些线程也不会因为客户端的退出而退出
参考文章:「2021dragon」的文章
原文链接:https://blog.csdn.net/chenlong_cxy/article/details/124650187

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

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

相关文章

如何使用ai绘画生成器创造出惊人的作品

你们了解如何文字生图片吗&#xff0c;简单的来说就是用文字描述出图片的样子&#xff0c;然后通过ai技术生成出来。 这种技术现在应用的很广泛&#xff0c;在各行各业都有在使用&#xff0c;平常我无聊的时候&#xff0c;就靠它为我打磨时间了。 不过现在的ai绘画工具有很多…

51 # 二叉搜索树的实现

实现二叉搜索树 比如我们有数组&#xff1a; [10, 8, 19, 6, 15, 22, 20]需要把数组转为二叉搜索树&#xff0c;效果如下&#xff1a; // 节点 class Node {constructor(element, parent) {this.element element; // 存的数据this.parent parent; // 父节点this.left null…

订座排队小程序/H5/app【附源码】

订座排队小程序/H5/app 可看 应用效果 首页 答题活动规则 选择门店暂无门店 订座订座 订座成功 用户反馈订座 关于后端

抖音seo源码/源码开发部署环境配置及流程分享

抖音seo源码开发部署环境配置&#xff1a; 要配置抖音seo源码开发环境&#xff0c;您需要完成以下步骤&#xff1a; 安装 Python&#xff1a;请在官网下载 Python 并进行安装。 安装 Django&#xff1a;请在命令行中输入以下命令来安装 Django&#xff1a; pip install django…

双周连载 | 暗网分析报告Part 1:他们无法保护你

双周连载 | 暗网分析报告Part 1&#xff1a;他们无法保护你 暗网&#xff0c;一个人们讳莫如深的世界&#xff0c;一个充斥着悄无声息的威胁&#xff0c;似乎永久地悬在我们每个人的头顶。这是一个罪恶的不法之地&#xff0c;时刻散播着未知的可怕力量&#xff0c;这便是那些隐…

使用 Elastic 修剪传入日志体量

作者&#xff1a;Carly Richmond 记录日志还是不记录日志&#xff1f; 一直是软件工程师仍在努力解决的一个难题&#xff0c;这对站点可靠性工程&#xff08;SRE&#xff09;同事来说是不利的。 开发人员并不总能正确了解他们在应用程序中捕获的警告和错误的级别或上下文&#…

01-webpack的理解,解决了什么问题

一、背景 Webpack 最初的目标是实现前端项目的模块化&#xff0c;旨在更高效地管理和维护项目中的每一个资源 模块化 最早的时候&#xff0c;我们会通过文件划分的形式实现模块化&#xff0c;也就是将每个功能及其相关状态数据各自单独放到不同的JS 文件中 约定每个文件是一…

dubbo监控中心dubbo-admin老版本(dubbo-ops)使用

1、dubbo分组 在dubbo中&#xff0c;可以指定group&#xff0c;如下&#xff1a; <dubbo:registry protocol"zookeeper" address"${dubbo.registry.address}" client"curator" group"${dubbo.registry.group}" /> 使用proper…

差值结构的排斥能

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;AB训练集各由5张二值化的图片组成&#xff0c;让差值结构中有7个1, 行分布是0&#xff0c;1&#xff0c;2&#xff0c;2&#xff0c;2列分布是1&#xff0c;2&#xff0c;4.统计迭代次数并排序。 得…

今年面试真的很难...

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;今年面试实在是太卷了&#xff0c;不知道从何下手&#xff01; 不论是跳槽涨薪&#xff0c;还是学习提升&#xff01;先给自己定一个小目标&#xff0c;然后再朝着目标去努力就完事儿了&#xff01; 为了帮大家节约…

应用层:文件传输协议FTP

1.应用层&#xff1a;文件传输协议FTP 笔记来源&#xff1a; 湖科大教书匠&#xff1a;应用层概述 湖科大教书匠&#xff1a;文件传输协议FTP 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 将某台计算机中的文件通过网络传送到可能相距很远的另一…

猿人学web刷题1

1.第一题 js混淆源码乱码 - 猿人学 : url 时间戳加密 右键遇到反调试&#xff0c;参考前面的文章 过反调试 2.ast解混淆 首页1.js 拿到 function oo0O0, 在<script>标签里面, 无法调试&#xff0c;分析自己提取出来&#xff0c;或则hook替换 加密逻辑存在于window.a中&a…

Linux分布式应用 Zabbix监控软件实例:监控NGINX的性能

实例&#xff1a;监控NGINX的性能 1.安装&#xff08;确认是否有状态统计模块&#xff09; yum -y install nginx 2.更改NGINX配置文件添加状态统计站点 vim /etc/nginx/nginx.conf #主配置文件 vim /etc/nginx/conf.d/default.conf #子配置文件location /nginx_status {stub_…

Docker 常用指令集合,更换镜像(Ubantu)

1.更换镜像 先进入root用户 cat /etc/docker/daemon.json 查看有没有镜像创建目录,创建并编辑damon,json文件 mkdir -p /etc/docker vim /etc/docker/daemon.json# 填写内容 {"registry-mirrors": ["https://h5rurp1p.mirror.aliyuncs.com"] } 重新启…

在工作中保持情绪稳定:策略与实践

一、引言 近期发生的新闻热点再度引发公众对稳定情绪和心理健康的关注。在快节奏的现代生活中&#xff0c;我们常常面临各种压力和挑战&#xff0c;这可能会导致情绪波动&#xff0c;甚至影响我们的工作和生活质量。有时候我们遇到的最大的敌人&#xff0c;不是运气也不是能力…

Kubernetes的介绍(组件、Pod)和 安装使用

目录 Kubernetes是什么&#xff1f; 跟Kubernetes相似的软件&#xff1a; k8s里有哪些组件&#xff1f; 官方网站&#xff1a;Kubernetes 组件 | Kubernetes master上的Control Plane组件 什么是组件&#xff1f; Pod是什么呢&#xff1f; 1、kube-apiserver …

Oracle的CentOS安装

1.CentOS环境 阿里云轻量型服务器 2核-4g内存-80G系统盘 2.Oracle下载 Oracle下载 Oracle 数据库免费 CPU 限制 Oracle 数据库免费版自动将自身限制为两个内核进行处理。例如&#xff0c;在具有 2 个双核 CPU&#xff08;四个核&#xff09;的计算机上&#xff0c;如果大量…

【在线文件管理】响应式文件管理AngularJS

目录 1.功能展示截图 2.实现代码 2.1HTML页面代码 2.2后台代码 2.2.1项目结构 2.2.2项目代码 其他问题 1.功能展示截图 项目中需要用到文件管理来自由控制文件的上传、下载、删除等&#xff0c;就想到做一个简单的在线文件管理功能。 支持在线编辑&#xff1a; 2.实现代…

Java基础---SPI

目录 典型回答 从面向接口编程说起 接口位于调用方所在的包中 接口位于实现方所在的包中 注意 如何定义一个SPI SPI的实现原理 SPI的应用场景 典型回答 Java中区分 API 和 SPI&#xff0c;通俗的讲&#xff1a;API 和 SPI 都是相对的概念&#xff0c;他们的差别只在语义…

从零开始的Android逆向工程,开启对应用程序内部的探索之旅

Android逆向有那些发展方向 安全评估&#xff1a; 逆向工程可以帮助安全专家分析和评估Android应用程序的安全性。通过逆向应用程序&#xff0c;发现潜在的漏洞和安全隐患&#xff0c;并提供改进建议&#xff0c;以加强应用程序的安全性。 应用改进和优化&#xff1a; 逆向…