网络套接字编程(二)
网络套接字编程(二)
文章目录
- 网络套接字编程(二)
- 简易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
指令查看服务端的状态:
服务端现处于监听状态,等待客户端的连接然后进行网络服务。
启动客户端时输入IP地址127.0.0.1和端口号8081:
服务端获取到了连接,知晓了客户端的IP地址和端口号,客户端建立了连接可以开始发送消息。
使用客户端发送数据:
服务端能够将接受的数据回传给客户端。
关闭客户端:
关闭客户端后,read函数返回值为0,服务端会断开与该客户端的服务,但是服务端并没有停止运行。
网络测试
网络测试主要查看能否成功建立网络连接即可,其余现象和本地测试现象相同,在启动客户端时输入服务端所处的IP地址:
单执行流服务器的弊端
使用两个客户端连接服务器后,两个客户端都显示了连接成功等待输入数据,但是服务端只获取了一个客户端的连接信息:
两个客户端都向服务端发送数据,只有一个客户端能得到服务端的网络服务,得到回传的数据:
关闭正在接收网络服务的客户端后,客户端才能获取另一个客户端的连接,并且给该客户端提供网络服务:
单执行流的服务器
通过测试可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。这个服务器一次只能为一个客户端提供服务,是一个单执行流的服务端。
服务端需要获取连接,才能够给对应的客户端提供网络服务,因此正在接收网络服务的客户端关闭,其他客户端才能接收网络服务。
客户端为什么会显示连接成功?
服务端处于监听状态后,客户端调用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出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
程序测试
使用两个客户端连接服务端:
两个客户端现在可以同时接收网络服务了。
可以看到服务端为这个两个客户端都创建了子进程进行网络服务:
客户端关闭后,系统自动回收了进入僵尸状态的子进程:
孙子进程提供网络服务
孙子进程提供网络服务和捕捉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;
};
孙子进程完成网络服务的原理
孙子进程完成网络服务的原理和子进程完成网络服务的原理相同,孙子进程会拷贝子进程的文件描述符表,孙子进程能够访问获取连接时得到的文件,因此孙子进程具有完成网络服务的能力。
子进程和孙子进程的回收机制
子进程在创建完孙子进程后就会退出,此时父进程立刻就能回收子进程,并接着完成接下来的监听和获取连接的任务。
孙子进程会在子进程死后变成孤儿进程,由系统回收。
程序测试
启动两个客户端连接服务端:
可以看到客户端连接服务端成功后,服务端立刻回收了子进程,并且网络服务交给了孙子进程进行处理,孙子进程由于父进程终止,孙子进程的父进程ID变成为1,也就是被操作系统接管了。
将客户端都退出后,孙子进程由于执行结束也被操作系统回收了:
多线程版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);delete td;return nullptr;}private:uint16_t _port;int _listensock;func_t _func;
};
线程是否需要关闭监听套接字的文件描述符?
子进程会拷贝父进程的文件描述符表,因此子进程关闭任一文件描述符不影响父进程,但线程不同于进程,一个进程内部的多个线程共享同一份数据,包括文件描述符表,线程关闭监听套接字的文件描述符会影响主线程监听,因此线程无需关闭监听套接字的文件描述符。
线程回收机制
默认情况下,新创建的线程退出后,需要对其主动进行回收操作,否则无法释放资源,从而造成系统泄漏。但我们将线程分离,当线程退出时,自动释放线程资源。
程序测试
启动两个客户端连接服务端:
可以看到服务端为两个客户端分别创建一个线程分别为它们执行网络服务。
关闭客户端,系统会自动回收终止的线程:
线程池版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
不断监视线程:
可以看出在启动服务端后,立刻就为线程池创建了5个线程等待任务。
启动客户端连接服务端进行网络通信:
值得注意的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出。
相关文章:

网络套接字编程(二)
网络套接字编程(二) 文章目录 网络套接字编程(二)简易TCP网络程序服务端创建套接字服务端绑定IP地址和端口号服务端监听服务端运行服务端网络服务服务端启动客户端创建套接字客户端的绑定和监听问题客户端建立连接并通信客户端启动程序测试单执行流服务器的弊端 多进程版TCP网络…...

LLaMA-Adapter源码解析
LLaMA-Adapter源码解析 伪代码 def transformer_block_with_llama_adapter(x, gating_factor, soft_prompt):residual xy zero_init_attention(soft_prompt, x) # llama-adapter: prepend prefixx self_attention(x)x x gating_factor * y # llama-adapter: apply zero_init…...

JavaScript设计模式之发布-订阅模式
发布者和订阅者完全解耦(通过消息队列进行通信) 适用场景:功能模块间进行通信,如Vue的事件总线。 ES6实现方式: class eventManager {constructor() {this.eventList {};}on(eventName, callback) {if (this.eventL…...

mysql---索引
概要 索引:排序的列表,列表当中存储的是索引的值和包含这个值的数据所在的行的物理地址 作用:加快查找速度 注:索引要在创建表时尽量创建完全,后期添加影响变动大。 索引也需要占用磁盘空间,innodb表数据…...

微信小程序——简易复制文本
在微信小程序中,可以使用wx.setClipboardData()方法来实现复制文本内容的功能。以下是一个示例代码: // 点击按钮触发复制事件 copyText: function() {var that this;wx.setClipboardData({data: 要复制的文本内容,success: function(res) {wx.showToa…...

【51单片机】矩阵键盘与定时器(学习笔记)
一、矩阵键盘 1、矩阵键盘概述 在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式 采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态 2、扫描的概念 数码管扫描(输出扫描):…...

vue 中使用async await
在程序中使用同步的方式来加载异步的数据的方式: async function() {let promise new Promise((resolve, reject) > {resolve(res);}).then(re > {return re; });await promise; }...

C语言学习之内存区域的划分
内存区域的划分:32位OS可以访问的虚拟内存空间为0~4G;一、内核空间:3~4G;二、用户空间0~3G;栈区:局部变量在栈区分配、由OS负责分配和回收堆区:由程序员手动分配(malloc函数)和回收(free函数);静…...

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

数据结构 - 顺序表ArrayList
目录 实现一个通用的顺序表 总结 包装类 装箱 / 装包 和 拆箱 / 拆包 ArrayList 与 顺序表 ArrayList基础功能演示 add 和 addAll ,添加元素功能 ArrayList的扩容机制 来看一下,下面的代码是否存在缺陷 模拟实现 ArrayList add 功能 add ind…...

【Echarts】玫瑰饼图数据交互
在学习echarts玫瑰饼图的过程中,了解到三种数据交互的方法,如果对您也有帮助,不胜欣喜。 一、官网教程 https://echarts.apache.org/examples/zh/editor.html?cpie-roseType-simple (该教程数据在代码中) import *…...

k8s、pod
Pod k8s中的port【端口:30000-32767】 port :为Service 在 cluster IP 上暴露的端口 targetPort:对应容器映射在 pod 端口上 nodePort:可以通过k8s 集群外部使用 node IP node port 访问Service containerPort:容…...

一天掌握python爬虫【基础篇】 涵盖 requests、beautifulsoup、selenium
大家好,我是python222小锋老师。前段时间卷了一套 Python3零基础7天入门实战 以及1小时掌握Python操作Mysql数据库之pymysql模块技术 近日锋哥又卷了一波课程,python爬虫【基础篇】 涵盖 requests、beautifulsoup、selenium,文字版视频版。1…...

睿趣科技:想知道开抖音小店的成本
随着互联网的发展,越来越多的人开始尝试通过开设网店来创业。抖音作为目前最受欢迎的短视频平台之一,也提供了开店的功能。那么,开一家抖音小店需要多少成本呢? 首先,我们需要了解的是,抖音小店的开店费用是…...

python项目部署代码汇总:目标检测类、人体姿态类
一、AI健身计数 1、图片视频检测 (cpu运行): 注:左上角为fps,左下角为次数统计。 1.哑铃弯举:12,14,16 详细环境安装教程:pyqt5AI健身CPU实时检测mediapipe 可视化界面…...

力扣每日一题92:反转链表||
题目描述: 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left < right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 示例 1: 输入:head [1,2,3,4,5], left 2, right 4 输…...

Vue+OpenLayers从入门到实战进阶案例汇总目录,兼容OpenLayers7和OpenLayers8
本篇作为《VueOpenLayers入门教程》和《VueOpenLayers实战进阶案例》所有文章的二合一汇总目录,方便查找。 本专栏源码是由OpenLayers结合Vue框架编写。 本专栏从Vue搭建脚手架到如何引入OpenLayers依赖的每一步详细新手教程,再到通过各种入门案例和综合…...

C#中使用LINQtoSQL管理SQL数据库之添加、修改和删除
目录 一、添加数据 二、修改数据 三、删除数据 四、添加、修改和删除的源码 五、生成效果 1.VS和SSMS原始记录 2.删除ID2和5的记录 3.添加记录ID2、5和8 4.修改ID3和ID4的记录 用LINQtoSQL管理SQL Server数据库时,主要有添加、修改和删除3种操作。 项目中创…...

飞致云及其旗下1Panel项目进入2023年第三季度最具成长性开源初创榜单
2023年10月26日,知名风险投资机构Runa Capital发布2023年第三季度ROSS指数(Runa Open Source Startup Index)。ROSS指数按季度汇总并公布在代码托管平台GitHub上年化增长率(AGR)排名前二十位的开源初创公司和开源项目。…...

Maven实战-私服搭建详细教程
Maven实战-私服搭建详细教程 1、为什么需要私服 首先我们为什么需要搭建Maven私服,一切技术来源于解决需求,因为我们在实际开发中,当我们研发出来一个 公共组件,为了能让别的业务开发组用上,则搭建一个远程仓库很有…...

uniapp-自定义表格,右边操作栏固定
uniapp-自定义表格,右边操作栏固定 在网上找了一些,没找到特别合适的,收集了一下其他人的思路,基本都是让左边可以滚动,右边定位,自己也尝试写了一下,有点样式上的小bug,还在尝试修…...

基于Electron27+React18+ArcoDesign客户端后台管理EXE
基于electron27.xreact18搭建电脑端exe后台管理系统模板 electron-react-admin 基于electron27整合vite.jsreact18搭建桌面端后台管理程序解决方案。 前几天有分享electron27react18创建跨平台应用实践,大家感兴趣可以去看看。 https://blog.csdn.net/yanxinyun1990…...

QT5交叉编译保姆级教程(arm64、mips64)
什么是交叉编译? 简单说,就是在当前系统平台上,开发编译运行于其它平台的程序。 比如本文硬件环境是x86平台,但是编译出来的程序是在arm64架构、mips64等架构上运行 本文使用的操作系统:统信UOS家庭版22.0 一、安装…...

python计算图片的RGB值,可以作为颜色的判断条件
python计算图片的RGB值,可以作为颜色的判断条件 import colorsys import PIL.Image as Imagedef get_dominant_color(image):max_score 0.0001dominant_color Nonefor count,(r,g,b) in image.getcolors(image.size[0]*image.size[1]):# 转为HSV标准saturation c…...

oracle 日期
日期加减 Oracle中日期进行加减可以使用多种方式,以下介绍三种 一种是针对天的操作,适用于对日,时,分,秒的操作, 一种是对月的操作,适用于月,年的操作, 一种是使用INTER…...

JVM堆内存解析
一、JVM堆内存介绍 Java大多数对象都是存放在堆中,堆内存是完全自动化管理,根据垃圾回收机制不同,Java堆有不同的结构,下面是我们一台生产环境服务器JVM堆内存空间分配情况,JVM只设置了-Xms2048M -Xmx2048M。 1、JVM堆…...

C#Onnx模型信息查看工具
效果 Netron效果 项目 代码 using Microsoft.ML.OnnxRuntime; using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms;namespace Onnx_Demo {public partial class frmMain : Form{public frmMain(){InitializeComponent();}string…...

RK3588 ubuntu系统安装opencv
废话不多说直接上步骤: 先切换至root用户 sudo su 输入密码先更新一下本地软件 apt update apt upgrade 安装相关环境 apt install build-essential cmake git pkg-config libgtk-3-dev \ libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-…...

常用的vue UI组件库
背景:Vue.js 是一个渐进式 javascript 框架,用于构建 UIS(用户界面)和 SPA(单页应用程序)。UI 组件库的出现提高了我们的开发效率,增强了应用的整体外观、感觉、交互性和可访问性,下…...

防范欺诈GPT
去年,ChatGPT的发布让全世界都感到惊讶和震惊。 突然间出现了一个平台,它比之前的任何其他技术都更深入地了解互联网。人工智能可以被训练成像阿姆一样说唱,以世界著名诗人的风格写作,并精确地翻译内容,以至于它似乎能…...