Linux|【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; };

客户端初始化
【Linux|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)】客户端的初始化只需要创建套接字即可,不需要绑定端口号,发起连接请求的时候,会自动给客户端分配一个端口号。创建套接字和服务端是一样的,代码如下:
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|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
    文章图片

    注意: 方法二中,父进程创建好子进程之后,子进程可以将监听套接字关闭,此时该套接字对子进程来说是没有用的,当然也可以不用关闭,没有多大的浪费。但父进程关闭掉服务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|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

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

多线程版本
介绍 思路: 通过创建一个线程为客户端提供服务,创建好的线程之间进行线程分离,这样主线程就不需要等待其它线程了
方法: 让启动函数执行服务的代码,其中最后一个参数可以传一个类过去,这个类包含了,客户端端口号和套接字信息,如下:
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|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

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

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

线程池版本
介绍 多线程版本效果看起来还不错,但是来一个连接就创建一个线程,断开一个连接就释放一个线程,这样频繁地创建和释放线程资源,对OS来说是一种负担,同时也带来资源的浪费,如果我们使用线程池,把每一个客户端连接封装成一个任务,让线程池去处理,这样就不需要频繁地创建和销毁消除,效率也能提升很多。
线程池在前面的博客中介绍过,代码如下:
#pragma once #include #include #include #include #include "Task.hpp"#define DEFAULT_MAX_PTHREAD 5class 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 = https://www.it610.com/article/_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_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|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

线程个数观察: 只有六个,且不变,主线程1个加上线程池的5个
Linux|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

可以看到的是,不论服务端有多少个连接,都只有5个线程在为这些连接提供服务,这就很好地展示处理线程池带来的价值,不会频繁创建和销毁消除,不造成资源浪费,是一种不错的选择。
浅谈TCP通信过程和socket API的关系 这里介绍TCP的相关socket API和tcp三次握手和四次挥手对应关系,三次握手和四次挥手在后面介绍tcp协议的博客中我会详细介绍,这里了解个大概即可
下面是TCP建立连接三次握手的过程和断开连接四次挥手的过程:
Linux|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

图中介绍了相关接口调用与实际通信对应的动作,详细动作后面的博客介绍。
几个问题:
服务器可不可以接受大量的连接?服务端是否需要维护这些连接,如何维护?
可以接受大量的连接,且需要维护,维护的方式就是先描述再组织,先将每一个通过一个结构体描述起来,然后通过某种数据结构将这些结构体组织起来,显然维护连接是有成本的,花费时间和空间
总结 今天博客内容就介绍到这里了,下一篇博客开始,我会将TCP/IP四层模型自顶向下讲解它的细节已经相关协议内容,喜欢的话,欢迎点赞。收藏和关注~
Linux|【Linux篇】第十九篇——网络套接字编程(二)(TCP套接字的编写+多进程版本+多线程版本+线程池版本)
文章图片

    推荐阅读