【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)

⭐️ 本篇博客开始给大家介绍网络编程中的套接字编程——基于UDP协议的套接字和基于TCP的套接字,这篇博客主要介绍基于UDP协议套接字,下一篇介绍基于TCP协议的套接字。在介绍套接字编程之前,我会先给大家介绍一些预备知识:源IP地址和目的IP地址、源端口号和目的端口号等,方便大家更好地理解网络套接字编写的整个流程。需要注意的是,我们是站在应用层进行编写套接字的,所以接下来会用到都是传输层的接口。话不多说,先看今天的主要内容~

目录

  • TCP相关的socket API
  • 基于TCP协议的套接字程序
    • 服务端
      • 整体框架
      • 服务端的初始化
        • 创建套接字
        • 绑定端口号
        • 将套接字设置为监听状态
      • 循环获取连接
    • 客户端
      • 整体框架
      • 客户端初始化
      • 客户端启动
        • 发起连接请求
        • 发起服务请求
    • 不同版本服务端服务代码
      • 多进程版本
        • 介绍
        • 测试
      • 多线程版本
        • 介绍
        • 测试
      • 线程池版本
        • 介绍
        • 测试
  • 浅谈TCP通信过程和socket API的关系
  • 总结


TCP相关的socket API

上一篇博客介绍了UDP的套接字编程,也介绍了几个相关的接口,如:socketbind两个,因为UDP是面向数据报的,所以只需要创建套接字并绑定端口号,等待数据到来即可,是比较简单的,而TCP是面向连接的,所以TCP创建好套接字,绑定好后,还需要进行监听,等待并获取连接,所以用的的API相比也会比UDP多几个,下面正式介绍:

  • listen

作用: 将套接字设置为监听状态,然后去监听socket的到来
函数原型:

#include  
int listen(int s, int backlog); 

参数:

  • s:要设置的套接字(称为监听套接字,通过socket创建)
  • backlog:连接队列的长度(不建议设置太长,后面的文章会详细介绍这个参数)

返回值: 成功返回0,失败返回-1

  • accept

作用: 接受请求,获取建立好的连接
函数原型:

#include 
#include 
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • s:监听套接字
  • addr:输出型参数,获取远端连接的相关信息
  • addrlen:输入输出型参数,获取addr的大小长度

返回值: 成功返回一个连接套接字,用来标识远端建立好连接的套接字,失败返回-1

  • connect

作用: 发起请求,请求与服务端建立连接(一般用于客户端向服务端发起请求)
函数原型:

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

参数:

  • sockfd:套接字,发起连接请求的套接字
  • addr:描述自身的相关信息,用来标识自身,需要自己填充,让对端知道是请求方的信息,以便进行响应
  • addrlen:描述addr的大小

返回值: 成功返回0,失败返回-1

答疑解惑: 不知道大家是否对accept会有疑惑,已经通过socket创建好了一个套接字,accept又返回了一个套接字,这两个套接字有什么区别吗?UDP只又一个套接字就可以进行通信了,而TCP还需要这么多个,这是为什么?

答案是肯定有的,socket创建的套接字是用来服务端本身进行绑定的。因为UDP是面向数据报,无连接的,所以创建好一个套接字之后直接等待数据到来即可,而TCP是面向连接,需要等待连接的到来,并获取连接,普通的一个套接字是不能够进行连接的监听,这时就需要用的listen来对创建好的套接字进行设置,将其设置为监听状态,这样这个套接字就可以不断监听连接状态,如果连接到来了,就需要通过accept获取连接,获取连接后返回一个值,也是套接字,这个套接字是用来描述每一个建立好的连接,方便维护连接和给对端进行响应,后期都是通过该套接字对客户端进行通信,也就是对客户端进行服务。
所以说,开始创建的套接字是与自身强相关的,用来描述自身,并且需要进行监听,所以我们也会称这个套接字叫做监听套接字,获取到的每一个连接都用一个套接字对其进行唯一性标识,方便维护与服务。
一个通俗的类比,监听套接字好比是一家饭馆拉客的,不断地去店外拉客进店,拉客进店后顾客需要享受服务,这时就是服务员对其进行各种服务,服务员就好比是accept返回的套接字,此时拉客的不需要关心服务员是如何服务顾客的,只需要继续去店外拉客进入店内就餐即可。

基于TCP协议的套接字程序

服务端

TCP服务端的编写分多个版本:多进程、多线程、线程池三个版本,有这么多个版本主要是因为TCP要去服务多个不同的连接,所以单进程目前来看是不现实的,因为主线程还需要去获取新的连接,当然后面博客还会介绍多路转接的内容,可以使用单进程来进行。但这里先不介绍单进程的版本,先介绍多进程和多线程去给请求连接提供服务,下面先介绍服务端核心内容,具体服务过程放在客户端的后面,方便测试。

整体框架

封装一个类,来描述tcp服务端,成员变量包含端口号和监听套接字两个即可,ip像udp服务端一样,绑定INADDR_ANY,构造函数根据传参初始化port,析构的时候关闭监听套接字即可

#define DEFAULT_PORT 8080 // 默认端口号为8080
#define BACK_LOG 5 // listen的第二个参数

class TcpServer
{
public:
  TcpServer(int port = DEFAULT_PORT)
    :_port(port)
     ,_listen_sock(-1)
  {}
  ~TcpServer()
  {
    if (_listen_sock >= 0) close(_listen_sock);
  }
private:
  int _port;
  int _listen_sock;
};

服务端的初始化

创建套接字

创建套接字这个过程相信大家都不陌生,UDP套接字那篇博客也介绍了,和UDP不同的是,TCP是面向连接的,所以第二个参数和TCP是不同的,填的是SOCK_STREAM,其它两个参数是一样的,协议家族填AF_INET,协议类别填0,具体代码如下:

bool TcpServerInit()
{
	// 创建套接字
	_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (_listen_sock < 0){
	  std::cerr << "socket creat fail" << std::endl;
	  return false;
	}
	std::cout << "socket creat succes, sock: " << _listen_sock << std::endl;
}

绑定端口号

绑定端口号,需要填充struct sockaddr_in这个结构体,里面有协议家族,端口号和IP,端口号根据用户传参进行填写,IP直接绑定INADDR_ANY,具体代码如下:

bool TcpServerInit()
{
  // 绑定
  struct sockaddr_in local;
  memset(&local, 0, sizeof(local));

  local.sin_family = AF_INET;
  local.sin_port = htons(_port);
  local.sin_addr.s_addr = INADDR_ANY;
  
  if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    std::cout << "bind fail" << std::endl;
    return false;
  }
  std::cout << "bind success" << std::endl;
}

将套接字设置为监听状态

这里就需要用的listen这个接口,让套接字处于监听状态,然后可以去监听连接的到来代码也很简单,具体如下:

bool TcpServerInit()
{
  // 将套接字设置为监听状态
  if (listen(_listen_sock, BACK_LOG) < 0){
    std::cout << "listen fail" << std::endl;
    return false;
  }
  std::cout << "listen success" << std::endl;
}

循环获取连接

监听套接字通过accept获取连接,一次获取连接失败不要直接将服务端关闭,而是重新去获取连接就好,因为获取一个连接失败而直接关闭服务端,带来的损失是很大的,所以只需要重新获取连接即可,返回的用于通信套接字记录下来,进行通信,然后可以用多种方式为各种连接连接提供服务,具体服务方式后面细说,先看获取连接的一部分代码:

void loop()
{
  struct sockaddr_in peer;// 获取远端端口号和ip信息
  socklen_t len = sizeof(peer);
  while (1){ 
    // 获取链接 
    // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len); 
    if (sock < 0){ 
      std::cout << "accept fail, continue accept" << std::endl; 
      continue; 
    }
    // 提供服务 service 后面介绍 
  }
}

客户端

整体框架

和服务端一样,封装一个类描述,类成员有服务端ip、服务端绑定的端口号以及自身套接字,代码如下:

class TcpClient
{
public:
  TcpClient(std::string ip, int port)
    :_server_ip(ip)
     ,_server_port(port)
     ,_sock(-1)
  {}
  ~TcpClient()
  {
    if (_sock >= 0) close(_sock);
  }
private:
  std::string _server_ip;
  int _server_port;
  int _sock;
};

客户端初始化

客户端的初始化只需要创建套接字即可,不需要绑定端口号,发起连接请求的时候,会自动给客户端分配一个端口号。创建套接字和服务端是一样的,代码如下:

bool TcpClientInit()
{
    // 创建套接字
    _sock = socket(AF_INET, SOCK_STREAM, 0);
    if (_sock < 0){
      std::cerr << "socket creat fail" << std::endl;
      return false;
    }
    std::cout << "socket creat succes, sock: " << _sock << std::endl;

    return true;
}

客户端启动

发起连接请求

使用connect函数,想服务端发起连接请求,注意,调用这个函数之前,需要先填充好服务端的信息,有协议家族、端口号和IP,请求连接失败直接退出进程,重新启动进程即可,连接成功之后就可以像服务端发起各自的服务请求(后面介绍),代码如下:

void TcpClientStart()
{
  // 连接服务器
  struct sockaddr_in peer;

  peer.sin_family = AF_INET;
  peer.sin_port = htons(_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 fail" << std::endl;
    exit(-1);
  }
  std::cout << "connect success" << std::endl;
  Request();// 下面介绍
}

发起服务请求

请求很简单,只需要让用户输入字符串请求,然后将请求通过write(send也可以,下篇博客介绍)发送过去,然后创建一个缓冲区,通过read(recv也可以)读取服务端的响应,这里需要着重介绍一下read的返回值
read的返回值:

  1. 大于0:实际读取的字节数
  2. 等于0:读到了文件末尾,说明对端关闭,用在服务端就是客户端关闭,用在客户端就是服务端关闭了,客户端可以直接退出
  3. 小于0:说明读取失败

代码如下:

void Request()
{
  std::string msg;
  while (1){
    std::cout << "Please Enter# ";
    getline(std::cin, msg);
    write(_sock, msg.c_str(), msg.size());
    char buf[256];
    ssize_t size = read(_sock, buf, sizeof(buf)-1);
    if (size <= 0){
      std::cerr << "read error" << std::endl;
      exit(-1);
    }
    buf[size] = 0;
    std::cout << buf << std::endl;
  }
}

不同版本服务端服务代码

多进程版本

介绍

思路: 为了给不同的连接提供服务,所以我们需要让父进程去不断获取连接,获取连接后,让父进程创建一个子进程去为这个获取到的连接提供服务,那么问题来了,**子进程去服务连接,父进程是否需要等待子进程?**按常理来说,是需要的,如果不等待的话,子进程退出,子进程的资源就没有人回收,就变成僵尸进程了,如果父进程等待子进程的话,父进程就需要阻塞在哪,无法去获取到新的连接,这也是不完全可行的,所以就有了一下两种解决方案:

  1. 通过注册SIGCHLD(子进程退出会想父进程发起该信号)信号,把它的处理信号的方式改成SIG_IGN(忽略),此时子进程退出就会自动清理资源不会产生僵尸进程,也不会通知父进程,这种方法比较推荐,也比较简单粗暴
  2. 通过创建子进程,子进程创建孙子进程,子进程直接退出,让1号进程领养孙子进程,这样父进程只需要等很短的时间就可以回收子进程的资源,这样父进程可以继续去获取连接,孙子进程给连接提供服务即可
    【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第1张图片
    注意: 方法二中,父进程创建好子进程之后,子进程可以将监听套接字关闭,此时该套接字对子进程来说是没有用的,当然也可以不用关闭,没有多大的浪费。但父进程关闭掉服务sock是有必要的,因为此时父进程不需要维护这些套接字了,孙子进程维护即可,如果不关闭,且有很多客户端向服务端发起请求,那么父进程这边就要维护很多不必要的套接字,让父进程的文件描述符不够用,造成文件描述符泄漏,所以父进程关闭服务套接字是必须的。
    方法一的代码编写:
void loop()
{
  // 对SIGCHLD信号进行注册,处理方式为忽略
  signal(SIGCHLD, SIG_IGN);
  struct sockaddr_in peer;// 获取远端端口号和ip信息
  socklen_t len = sizeof(peer);
  while (1){ 
    // 获取链接 
    // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len); 
    if (sock < 0){ 
      std::cout << "accept fail, continue accept" << std::endl; 
      continue; 
    } 
 
    // 创建子进程
    pid_t id = fork();
    if (id == 0){
      // 子进程
      close(_listen_sock); // 可以不关闭,但是建议关闭,以防后期子进程对监听套接字fd进行了一些操作,对父进程造成影响
      // 孙子进程
      int peerPort = ntohs(peer.sin_port);
      std::string peerIp = inet_ntoa(peer.sin_addr);
      std::cout << "get a new link, [" << peerIp << "]:[" << peerPort  << "]"<< std::endl;
      Service(peerIp, peerPort, sock);
    }
    // 父进程继续去获取连接
  }
}
void Service(std::string ip, int port, int sock)
{
   while (1){
     char buf[256];
     ssize_t size = read(sock, buf, sizeof(buf)-1);
     if (size > 0){
       // 正常读取size字节的数据
       buf[size] = 0;
       std::cout << "[" << ip << "]:[" << port  << "]# "<< buf << std::endl;
       std::string msg = "server get!-> ";
       msg += buf;
       write(sock, msg.c_str(), msg.size());
     }
     else if (size == 0){
       // 对端关闭
       std::cout << "[" << ip << "]:[" << port  << "]# close" << std::endl;
       break;
     }
     else{
       // 出错
       std::cerr << sock << "read error" << std::endl; 
       break;
     }
   }

   close(sock);
   std::cout << "service done" << std::endl;
   // 子进程退出
   exit(0);
}

方法二代码的编写:

void loop()
{
  struct sockaddr_in peer;// 获取远端端口号和ip信息
  socklen_t len = sizeof(peer);
  while (1){ 
    // 获取链接 
    // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len); 
    if (sock < 0){ 
      std::cout << "accept fail, continue accept" << std::endl; 
      continue; 
    } 
    // 创建子进程
    pid_t id = fork();
    if (id == 0){
      // 子进程
      // 父子进程的文件描述符内容一致
      // 子进程可以关闭监听套接字的文件描述符
      close(_listen_sock); // 可以不关闭,但是建议关闭,以防后期子进程对监听套接字fd进行了一些操作,对父进程造成影响
      if (fork() > 0){
        // 父进程
        // 直接退出,让孙子进程被OS(1号进程)领养,退出时资源被操作系统回收
        exit(0);
      }
        // 孙子进程
        int peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);
        std::cout << "get a new link, [" << peerIp << "]:[" << peerPort  << "]"<< std::endl;
        Service(peerIp, peerPort, sock);
    }

    // 关闭sock  如果不关闭,那么爷爷进程可用文件描述符会越来越少
    close(sock);
    // 爷爷进程等儿子进程
    waitpid(-1, nullptr, 0);// 以阻塞方式等待,但这里不会阻塞,因为儿子进程是立即退出的
  }
}
void Service(std::string ip, int port, int sock)
{
   while (1){
     char buf[256];
     ssize_t size = read(sock, buf, sizeof(buf)-1);
     if (size > 0){
       // 正常读取size字节的数据
       buf[size] = 0;
       std::cout << "[" << ip << "]:[" << port  << "]# "<< buf << std::endl;
       std::string msg = "server get!-> ";
       msg += buf;
       write(sock, msg.c_str(), msg.size());
     }
     else if (size == 0){
       // 对端关闭
       std::cout << "[" << ip << "]:[" << port  << "]# close" << std::endl;
       break;
     }
     else{
       // 出错
       std::cerr << sock << "read error" << std::endl; 
       break;
     }
   }

   close(sock);
   std::cout << "service done" << std::endl;
   // 孙子进程退出
   exit(0);
}

测试

这里就置测试第二种写法,下面是一段监控脚本,监控有多少进程在运行:

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

运行服务端和客户端的代码如下:

// server
#include "tcp_server.hpp"

// ./tcp_server port
int main(int argc, char* argv[])
{
  if (argc != 2){
    std::cout << "Usage: " << argv[0] << " port" << std::endl;
    return 1;
  }

  TcpServer* tsvr = new TcpServer(atoi(argv[1]));

  tsvr->TcpServerInit();
  tsvr->loop();

  delete tsvr;

  return 0;
}
// client
#include "tcp_server.hpp"

// ./tcp_server port
int main(int argc, char* argv[])
{
  if (argc != 2){
    std::cout << "Usage: " << argv[0] << " port" << std::endl;
    return 1;
  }

  TcpServer* tsvr = new TcpServer(atoi(argv[1]));

  tsvr->TcpServerInit();
  tsvr->loop();

  delete tsvr;

  return 0;
}
[wxj@VM-0-9-centos TCP1]$ cat tcp_client.cc
#include "tcp_client.hpp"
#include 

// ./tcp_client server_ip server_port
int main(int argc, char* argv[])
{
  if (argc != 3){
    std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
    return 1;
  }
  TcpClient* tclt = new TcpClient(argv[1], atoi(argv[2]));

  tclt->TcpClientInit();
  tclt->TcpClientStart();

  delete tclt;

  return 0;
}

先看动画: 先启动服务端,再启动客户端
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第2张图片

观察孙子进程的父进程: 可以发现有三个进程在跑,分别是爷爷进程和两个孙子进程,孙子进程被1号进程领养
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第3张图片

多线程版本

介绍

思路: 通过创建一个线程为客户端提供服务,创建好的线程之间进行线程分离,这样主线程就不需要等待其它线程了
方法: 让启动函数执行服务的代码,其中最后一个参数可以传一个类过去,这个类包含了,客户端端口号和套接字信息,如下:

struct Info
{
  int _port;
  std::string _ip;
  int _sock;

  Info(int port, std::string ip, int sock)
    :_port(port)
     ,_ip(ip)
     ,_sock(sock)
  {}
};

注意: 这里为了不让thread_run多一个this指针这个参数,所以用static修饰该函数,就没有this指针这个参数了,为了让创建出来的线程线程就可以调用该Service函数,这里将Service函数也用static修饰
核心代码如下:

static void* thread_run(void* arg)
{
  Info info = *(Info*)arg;
  delete (Info*)arg;
  // 线程分离
  pthread_detach(pthread_self());
  Service(info._ip, info._port, info._sock);
}
void loop()
{
  struct sockaddr_in peer;// 获取远端端口号和ip信息
  socklen_t len = sizeof(peer);
  while (1){ 
    // 获取链接 
    // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len); 
    if (sock < 0){ 
      std::cout << "accept fail, continue accept" << std::endl; 
      continue; 
    } 
    // 多线程版本 
    pthread_t tid; 
    int peerPort = ntohs(peer.sin_port); 
    std::string peerIp = inet_ntoa(peer.sin_addr);
    Info* info = new Info(peerPort, peerIp, sock); 
    pthread_create(&tid, nullptr, thread_run, (void*)info);
  }
}
static void Service(std::string ip, int port, int sock)
{
   while (1){
     char buf[256];
     ssize_t size = read(sock, buf, sizeof(buf)-1);
     if (size > 0){
       // 正常读取size字节的数据
       buf[size] = 0;
       std::cout << "[" << ip << "]:[" << port  << "]# "<< buf << std::endl;
       std::string msg = "server get!-> ";
       msg += buf;
       write(sock, msg.c_str(), msg.size());
     }
     else if (size == 0){
       // 对端关闭
       std::cout << "[" << ip << "]:[" << port  << "]# close" << std::endl;
       break;
     }
     else{
       // 出错
       std::cerr << sock << "read error" << std::endl; 
       break;
     }
   }

   close(sock);
   std::cout << "service done" << std::endl;
}

测试

为了方便测试,这里也写了一个监控脚本,监控线程数,如下:

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

动画效果:
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第4张图片

服务端启动,有一个主线程:
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第5张图片

两个客户端启动,多了两个线程:
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第6张图片

线程池版本

介绍

多线程版本效果看起来还不错,但是来一个连接就创建一个线程,断开一个连接就释放一个线程,这样频繁地创建和释放线程资源,对OS来说是一种负担,同时也带来资源的浪费,如果我们使用线程池,把每一个客户端连接封装成一个任务,让线程池去处理,这样就不需要频繁地创建和销毁消除,效率也能提升很多。
线程池在前面的博客中介绍过,代码如下:

#pragma once
#include 
#include 
#include 
#include 
#include "Task.hpp"

#define DEFAULT_MAX_PTHREAD 5

class ThreadPool
{
public:
  ThreadPool(int max_pthread = DEFAULT_MAX_PTHREAD)
    :_max_thread(max_pthread)
  {}
  static void* Runtine(void* arg)
  {
    pthread_detach(pthread_self());
    ThreadPool* this_p = (ThreadPool*)arg;

    while (1){
      this_p->LockQueue();
      while (this_p->IsEmpty()){
        this_p->ThreadWait();
      }
      Task* t;
      this_p->Get(t);
      this_p->UnlockQueue();
      // 解锁后处理任务
      t->Run();
      delete t;
    }
  }
  void ThreadPoolInit()
  {
    pthread_mutex_init(&_mutex, nullptr);
    pthread_cond_init(&_cond, nullptr);
    pthread_t t[_max_thread];
    for(int i = 0; i < _max_thread; ++i)
    {
      pthread_create(t + i, nullptr, Runtine, this);
    }
  }
  void Put(Task* data)
  {
    LockQueue();
    _q.push(data);
    UnlockQueue();
    WakeUpThread();
  }
  void Get(Task*& data)
  {
    data = _q.front();
    _q.pop();
  }
  ~ThreadPool()
  {
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
  }
public:
  void LockQueue()
  {
    pthread_mutex_lock(&_mutex);
  }
  void UnlockQueue()
  {
    pthread_mutex_unlock(&_mutex);
  }
  void ThreadWait()
  {
    pthread_cond_wait(&_cond, &_mutex);
  }
  void WakeUpThread()
  {
    pthread_cond_signal(&_cond);
    //pthread_cond_broadcast(&_cond);
  }
  bool IsEmpty()
  {
    return _q.empty();
  } 
private:
  std::queue<Task*>  _q;
  int             _max_thread;
  pthread_mutex_t _mutex;
  pthread_cond_t  _cond;
};

这里我们单独写一个头文件——Task.hpp,其中有任务类,任务类里面有三个成员变量,也就是端口号,IP和套接字,其中有一个成员方法——Run,里面封装了一个Service函数,也就是前面写的,把它放在Task.hpp这个头文件下,线程池里面的线程执行run函数即可,头文件内容如下:

#pragma once
#include 
#include 

static void Service(std::string ip, int port, int sock)
{
  while (1){
    char buf[256];
    ssize_t size = read(sock, buf, sizeof(buf)-1);
    if (size > 0){
      // 正常读取size字节的数据
      buf[size] = 0;
      std::cout << "[" << ip << "]:[" << port  << "]# "<< buf << std::endl;
      std::string msg = "server get!-> ";
      msg += buf;
      write(sock, msg.c_str(), msg.size());
    }
    else if (size == 0){
      // 对端关闭
      std::cout << "[" << ip << "]:[" << port  << "]# close" << std::endl;
      break;
    }
    else{
      // 出错
      std::cerr << sock << "read error" << std::endl; 
      break;
    }
  }

  close(sock);
  std::cout << "service done" << std::endl;
}

struct Task
{
  int _port;
  std::string _ip;
  int _sock;

  Task(int port, std::string ip, int sock)
    :_port(port)
    ,_ip(ip)
     ,_sock(sock)
  {}
  void Run()
  {
      Service(_ip, _port, _sock);
  }
};

服务器类的核心代码如下:

void loop()
{
  struct sockaddr_in peer;// 获取远端端口号和ip信息
  socklen_t len = sizeof(peer);
  _tp = new ThreadPool(THREAD_NUM); 
  _tp->ThreadPoolInit();
  while (1){
    // 获取链接
    // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字
    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
    if (sock < 0){
      std::cout << "accept fail, continue accept" << std::endl;
      continue;
    }
    int peerPort = ntohs(peer.sin_port);
    std::string peerIp = inet_ntoa(peer.sin_addr);
    std::cout << "get a new link, [" << peerIp << "]:[" << peerPort  << "]"<< std::endl;
    Task* task = new Task(peerPort, peerIp, sock);
    _tp->Put(task);

  }
}

注意几点变化:

  1. 服务器类增加一个线程池成员变量,初始化函数里面增加线程池创建(在堆上申请)
  2. 析构函数增加释放线程池资源一步
  3. loop函数中只需要封装任务,并把任务丢进线程池中即可

测试

这里用的监控脚本和多线程版本用的是一样的,先看动画演示:
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第7张图片

线程个数观察: 只有六个,且不变,主线程1个加上线程池的5个
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第8张图片
可以看到的是,不论服务端有多少个连接,都只有5个线程在为这些连接提供服务,这就很好地展示处理线程池带来的价值,不会频繁创建和销毁消除,不造成资源浪费,是一种不错的选择。

浅谈TCP通信过程和socket API的关系

这里介绍TCP的相关socket API和tcp三次握手和四次挥手对应关系,三次握手和四次挥手在后面介绍tcp协议的博客中我会详细介绍,这里了解个大概即可
下面是TCP建立连接三次握手的过程和断开连接四次挥手的过程:
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第9张图片

图中介绍了相关接口调用与实际通信对应的动作,详细动作后面的博客介绍。
几个问题:

服务器可不可以接受大量的连接?服务端是否需要维护这些连接,如何维护?

可以接受大量的连接,且需要维护,维护的方式就是先描述再组织,先将每一个通过一个结构体描述起来,然后通过某种数据结构将这些结构体组织起来,显然维护连接是有成本的,花费时间和空间

总结

今天博客内容就介绍到这里了,下一篇博客开始,我会将TCP/IP四层模型自顶向下讲解它的细节已经相关协议内容,喜欢的话,欢迎点赞。收藏和关注~
【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)_第10张图片

你可能感兴趣的