網(wǎng)絡(luò)編程套接字(二)
簡單TCP服務(wù)器實現(xiàn)
我們將會使用到的頭文件放在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;
創(chuàng)建套接字
創(chuàng)建過程和UDP服務(wù)器幾乎完全一樣,除了使用的是TCP服務(wù)器使用的是流式服務(wù)(SOCK_STREAM),UDP使用的是數(shù)據(jù)包服務(wù)(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;
};
服務(wù)器綁定
綁定的過程和UDP服務(wù)器也是相同的,可以看著復(fù)習(xí)一下
// 2 服務(wù)器綁定
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
結(jié)構(gòu)體后,最好使用memset
對該結(jié)構(gòu)體進(jìn)行初始化,也可以使用bzero
函數(shù)進(jìn)行清空
void bzero(void *s, size_t n);
服務(wù)器監(jiān)聽
TCP服務(wù)器是面向連接的,,客戶端在正式向TCP服務(wù)器發(fā)送數(shù)據(jù)之前必須先于TCP服務(wù)器建立連接,然后才可以進(jìn)行通信
因此TCP服務(wù)器需要時刻注意是否有客戶端發(fā)來連接請求,需要將TCP創(chuàng)建的套接字設(shè)置成監(jiān)聽狀態(tài)
int listen(int socket, int backlog);
- socket : 需要設(shè)置為監(jiān)聽狀態(tài)的套接字對應(yīng)的文件描述符
- backlog: 全連接隊列的最大長度。如果有多個客戶端同時發(fā)來連接請求,此時未被服務(wù)器處理的連接會先被放入連接隊列,該參數(shù)代表這個連接隊列的最大長度,一般設(shè)置成5或者10即可
- 監(jiān)聽成功返回0,失敗返回-1同時設(shè)置錯誤碼
// 3 服務(wù)器監(jiān)聽
if (listen(listen_sock, 10) < 0) {
std::cerr << "listen error" << std::endl;
exit(4);
}
這里我們發(fā)現(xiàn)上文的sockfd
其實是一個被監(jiān)聽的文件描述符,為了變量命名更容易讓人理解,我們把sockfd
改為listen_sock
,并且在初始化TCP服務(wù)器中,只有套接字創(chuàng)建成功,綁定成功,監(jiān)聽成功,TCP服務(wù)器的初始化才算完成
vim 替換單詞
全文替換 ::#sockfd#listen_sock#g
使用 :#str1#str2#g
進(jìn)行全文替換,將str1全部替換成str2
局部替換: : 20, 30s#str1#str2#g
(將20到30行內(nèi)的str1替換成str2)
當(dāng)前行替換: : s#str1#str2#g
(將光標(biāo)所在行內(nèi)的str1 替換成 str2)
服務(wù)器接收連接
TCP服務(wù)器初始化后就可以開始運(yùn)行了,但是TCP服務(wù)器與客戶端在進(jìn)行網(wǎng)絡(luò)通信之前,服務(wù)器需要先獲取到客戶端的連接請求
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
-
socket : 特定的監(jiān)聽套接字,表示從該監(jiān)聽文件中獲取連接
-
address : 對端網(wǎng)絡(luò)相關(guān)的屬性信息
-
addrlen
: 傳入希望讀取到的address結(jié)構(gòu)體的長度,返回實際讀取到的address結(jié)構(gòu)體的長度,是一個輸入輸出參數(shù) -
獲取連接成功返回接收到的套接字的文件描述符,獲取連接失敗返回-1,同時設(shè)置錯誤碼
監(jiān)聽套接字和accept函數(shù)返回套接字的區(qū)別
- 監(jiān)聽套接字:用于獲取連接請求信息,accept函數(shù)不斷從監(jiān)聽文件中獲取新連接
- accept返回套接字:用于為這個連接提供服務(wù),進(jìn)行真正的業(yè)務(wù)數(shù)據(jù)傳輸
服務(wù)端獲取連接
-
accept函數(shù)獲取連接時可能會失敗,但是服務(wù)器不能因為獲取某一個連接失敗就退出,因此服務(wù)端獲取連接失敗后還需要繼續(xù)獲取連接
-
如果需要將獲取到的客戶端IP地址和端口號信息進(jìn)行輸出,需要調(diào)用
inet_ntoa
函數(shù)將整數(shù)IP轉(zhuǎn)換成字符串IP,調(diào)用ntohs
函數(shù)將網(wǎng)絡(luò)序列轉(zhuǎn)換成主機(jī)序列 -
inet_ntoa
函數(shù)會先將整數(shù)IP轉(zhuǎn)換成主機(jī)序列,然后再將其轉(zhuǎn)換成字符串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); // 進(jìn)行業(yè)務(wù)處理
}
}
服務(wù)器處理請求
現(xiàn)在服務(wù)器已經(jīng)可以和客戶端建立連接了,接下來就是到了通信階段。我們只需要通過對accept
函數(shù)打開的網(wǎng)絡(luò)文件進(jìn)行讀寫,就可以完成網(wǎng)絡(luò)數(shù)據(jù)的傳輸和接收了。為了能讓雙方都可以看到現(xiàn)象,這里就實現(xiàn)一個簡單的回聲TCP服務(wù)器
ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t read(int fildes, void *buf, size_t nbyte);
-
fd
:特定的文件描述符,表示從該文件描述符中讀取數(shù)據(jù) -
buffer : 數(shù)據(jù)的存儲位置,表示將讀取數(shù)據(jù)到該緩沖區(qū)中
-
count : 期望從該文件描述符中讀取的字節(jié)數(shù)
-
返回值大于零代表本次讀取到的字節(jié)數(shù),等于零表示對端對出,小于零說明讀取出現(xiàn)錯誤
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
: 特定的文件描述符,表示將把數(shù)據(jù)寫入該文件描述符對應(yīng)的文件 -
buffer : 需要寫入的數(shù)據(jù)
-
count :期望寫入的字節(jié)數(shù)
-
寫入成功返回實際寫入的字節(jié)數(shù),寫入失敗返回-1,同時錯誤碼被設(shè)置
還需要注意到,服務(wù)端讀取的數(shù)據(jù)是從服務(wù)套接字中獲取的,如果客戶端斷開連接服務(wù)結(jié)束那么需要關(guān)閉對應(yīng)文件。因為文件描述符本質(zhì)就是數(shù)組的下標(biāo),是有限的,如果一直占用會導(dǎo)致文件描述符泄漏
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) { // 發(fā)送響應(yīng)
std::cerr << "write response error" << std::endl;
} else {
std::cout << "send response success" << std::endl;
}
} else if (size == 0) { // 對端關(guān)閉連接
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客戶端實現(xiàn)
創(chuàng)建套接字
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() {
// 創(chuàng)建套接字
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;
};
客戶端發(fā)起連接
由于客戶端不需要用戶手動綁定也不需要監(jiān)聽,所以客戶端創(chuàng)建好套接字后就可以直接向服務(wù)端發(fā)起連接請求
int connect(int socket, const struct sockaddr *address, socklen_t address_len);
- socket : 特定的套接字,表示通過該套接字發(fā)起連接請求
-
address
: 對端的網(wǎng)絡(luò)相關(guān)信息 -
addrlen
: 傳入的addr
結(jié)構(gòu)體的長度 - 綁定成功返回0,連接失敗返回-1,同時錯誤碼被設(shè)置
void Start() {
// 發(fā)送連接請求
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();
}
客戶端發(fā)起請求
這里的代碼邏輯非常簡單,可以稍微看一看
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;
}
}
}
服務(wù)器簡單測試
首先使用telnet
進(jìn)行連接測試,可以看到服務(wù)器可以正常建立連接??刂?code>telnet給服務(wù)器發(fā)送信息服務(wù)器可以接收并能返回響應(yīng)
使用netstat
命令查看,可以看到一個名為tcp_server
的進(jìn)程正處于監(jiān)聽狀態(tài)
使用我們的客戶端連接,可以看到服務(wù)端可以打印客戶端的IP地址和端口號以及發(fā)送的數(shù)據(jù),客戶端也可以接收服務(wù)器發(fā)來的響應(yīng)。客戶端一旦退出,服務(wù)器也會立刻接收到并作出反應(yīng)。
服務(wù)器簡單測評
當(dāng)我們僅僅使用一個客戶端連接服務(wù)端時,客戶端可以正常享受服務(wù)器的服務(wù)。但是若再來一個客戶端時,雖然新來的客戶端也可以成功建立連接,但是我們的服務(wù)器正在為第一個客戶端提供服務(wù),無法立馬處理第二個客戶端的請求。只有等第一個客戶端推出后,才能對第二個客戶端發(fā)來的數(shù)據(jù)進(jìn)行打印
單執(zhí)行流服務(wù)器
這是因為我們的服務(wù)器是一個單執(zhí)行流的,這種服務(wù)一一次只能為一個客戶端提供服務(wù)。
單執(zhí)行流服務(wù)器為什么可以同時和多個客戶端建立連接
當(dāng)服務(wù)端在給第一個客戶端提供服務(wù)期間,第二個客戶端發(fā)送連接請求時是成功的,這是因為連接其實已經(jīng)建立,只是服務(wù)端還沒有調(diào)用accept
函數(shù)將連接獲取上來罷了
前文在介紹listen
接口的時候提到一個參數(shù)backlog
,實際上操作系統(tǒng)在底層會為我們維護(hù)一個連接隊列。服務(wù)端沒有accept的新連接會被放在這個連接隊列中,而這個隊列的最大長度是由backlog決定的。所以雖然服務(wù)端沒有使用accept
獲取第二個客戶端發(fā)來的請求,但實際上鏈接已經(jīng)建成了
那么如何解決服務(wù)器只能給一個客戶端提供服務(wù)這個問題呢?? 很簡單只要提供多進(jìn)程版的服務(wù)器或者多線程版的就可以了
多進(jìn)程版TCP服務(wù)器
當(dāng)服務(wù)端調(diào)用accept函數(shù)獲取到新連接,并未新連接打開網(wǎng)絡(luò)文件后,讓當(dāng)前執(zhí)行流調(diào)用fork()
函數(shù)創(chuàng)鍵子進(jìn)程,讓子進(jìn)程為父進(jìn)程獲取到的鏈接提供服務(wù),由于父子進(jìn)程是兩個不同的執(zhí)行流,父進(jìn)程創(chuàng)建子進(jìn)程后可以繼續(xù)從監(jiān)聽套接字中獲取新連接,不需要關(guān)心服務(wù)
子進(jìn)程會繼承父進(jìn)程的文件描述符表
文件描述符表是隸屬于一個進(jìn)程的,子進(jìn)程創(chuàng)建后會"復(fù)制"一份父進(jìn)程的文件描述符表,之后父子進(jìn)程之間會保持獨(dú)立性。對于套接字文件(網(wǎng)絡(luò)文件)也是一樣的,父進(jìn)程創(chuàng)建的子進(jìn)程也會繼承父進(jìn)程的套接字文件信息,此時子進(jìn)程也就能對特定的套接字文件進(jìn)行讀寫操作
等待子進(jìn)程問題
當(dāng)父進(jìn)程創(chuàng)建子進(jìn)程后,父進(jìn)程是必須等待子進(jìn)程退出的,以防止子進(jìn)程變成僵尸進(jìn)程造成內(nèi)存泄漏。如果服務(wù)端進(jìn)行阻塞式等待子進(jìn)程,那么服務(wù)端還是必須等待客戶端的服務(wù)完畢才能獲取下一個服務(wù)請求,顯然不合理。若采用非阻塞方式等待子進(jìn)程,那么服務(wù)端就必須將所有子進(jìn)程的PID保存下來,并每隔一段時間要對所有鏈接進(jìn)行檢測,顯然非常麻煩
總之,無論采用阻塞或非阻塞的方式等待子進(jìn)程,都不能很好的幫助我們將獲取鏈接和提供服務(wù)分離。
不等待子進(jìn)程退出的方式
1、 捕捉SIGCHLD信號,將其處理動作設(shè)置成忽略
2、讓父進(jìn)程創(chuàng)建子進(jìn)程,子進(jìn)程再創(chuàng)建孫子進(jìn)程,子進(jìn)程退出,孫子進(jìn)程就被操作系統(tǒng)領(lǐng)養(yǎng)并為客戶端進(jìn)行服務(wù)
捕捉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(); // 創(chuàng)建子進(jìn)程執(zhí)行服務(wù)
if (id == 0) {
Service(sock, client_ip, client_port);
exit(0);
}
}
}
孫子進(jìn)程提供服務(wù)
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) { // 子進(jìn)程
pid_t id = fork();
if(id == 0){ // 孫子進(jìn)程
Service(sock, client_ip, client_port);
exit(0);
}
exit(0);
}
waitpid(id, NULL, 0); // 直接將子進(jìn)程回收了
}
}
while :; do ps -axj | head -1 && ps -axj | grep tcp_server | grep -v grep; echo "############"; sleep 1; done # 監(jiān)視腳本
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-aZOZXrxO-1688734711200)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230707151802143.png)]
可以看到子進(jìn)程直接就推出了,孫子進(jìn)程正在為客戶端提供服務(wù)。當(dāng)客戶端推出后,孫子進(jìn)程直接就被操作系統(tǒng)回收了。它的PPID為1號進(jìn)程,表明這是一個孤兒進(jìn)程
多線程版TCP服務(wù)器
創(chuàng)建進(jìn)程的成本非常高,而創(chuàng)建線程的成本就會小很多,因為線程本質(zhì)就是再進(jìn)程地址空間中運(yùn)行的,創(chuàng)建出來的線程共享大部分資源。因此實現(xiàn)多執(zhí)行流的服務(wù)器最好采用多線程進(jìn)行實現(xiàn)
while :; do ps -aL | head -1 && ps -aL | grep tcp_server| grep -v grep; echo "#########################"; sleep 1; done
// 1、參數(shù)列表
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函數(shù)改為靜態(tài)函數(shù),使用struct Args* 指針將三個參數(shù)構(gòu)成結(jié)構(gòu)體傳進(jìn)去
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) { // 對端關(guān)閉連接
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;
}
}
}
該對線程服務(wù)器存在的問題
- 每當(dāng)新線程到來時,服務(wù)端的主線程才會為客戶端創(chuàng)建新線程,而服務(wù)結(jié)束又會將該線程銷毀。就像我們?nèi)ナ程贸燥?,我們?nèi)チ耸程冒⒁滩砰_始做飯。效率低下。
- 如果有大量的客戶端連接請求到來,計算機(jī)就要一一創(chuàng)建服務(wù)線程,線程越多CPU的壓力也就越大。因為CPU要在這些線程之間來回切換,線程間切換的成本就變得很高,此外線程變多,每個線程被調(diào)度到的時間就變長了,用戶體驗變差
解決方案
- 可以預(yù)先創(chuàng)建一批線程,當(dāng)有客戶端請求連接到來時就為這些線程提供服務(wù)。而不是客戶端來了才創(chuàng)建線程
- 當(dāng)某個線程對客戶端提供服務(wù)完成后,不讓該線程推出,讓該線程繼續(xù)給下一個客戶端提供服務(wù)。如果沒有可以讓線程先進(jìn)入休眠狀態(tài)
- 服務(wù)端創(chuàng)建的一批線程數(shù)量不能太多。此外,如果有海量客戶端接連到來,可以將這些新來的連接放在等待隊列中進(jìn)行排隊,等服務(wù)端這一批線程有空閑線程后再將連接拿上來處理
實際解決上述問題就是要讓我們再服務(wù)端引入線程池。線程池可以預(yù)先存儲線程并使線程循環(huán)往復(fù)的工作,并且線程池中還有一個任務(wù)隊列可以用于存儲任務(wù)。如果有任務(wù)就從任務(wù)隊列中Pop任務(wù),并調(diào)用任務(wù)對應(yīng)的Run函數(shù)對任務(wù)進(jìn)行處理,如果沒有任務(wù)就進(jìn)入休眠狀態(tài)
線程池版TCP服務(wù)器
線程池的實現(xiàn)在多線程那一章已經(jīng)講過了,這里直接套用了
#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();
}
現(xiàn)在想向服務(wù)器中引入線程池,因此在服務(wù)器類中新增一個線程池的指針成員
- 在實例化服務(wù)器對象時,先將線程池指針初始化為空
- 當(dāng)服務(wù)器初始化完畢,進(jìn)入正常運(yùn)行階段使用
GetInstance
接口獲取單例線程池。 - 主線程之后就用于獲取連接,然后將獲取到的客戶端
ip, port
以及打開的網(wǎng)絡(luò)文件sockfd
打包成一個任務(wù)交給線程池的任務(wù)隊列
線程池中的線程就通過不斷獲取任務(wù)隊列中的任務(wù),通過task
中包含的信息為客戶端提供服務(wù)
這實際也是一個生產(chǎn)消費(fèi)模型,其中監(jiān)聽進(jìn)程就是任務(wù)的生產(chǎn)者,線程池中的若干線程就是消費(fèi)者,交易場所就是線程池中的任務(wù)隊列
任務(wù)類的設(shè)計
任務(wù)類中必須包含服務(wù)器和客戶端進(jìn)行通信所需要的數(shù)據(jù)信息,包含網(wǎng)絡(luò)套接字,客戶端的IP,客戶端的端口號。表示該任務(wù)是為哪一個客戶端提供服務(wù)的,使用的是哪一個網(wǎng)絡(luò)文件
任務(wù)類中還必須帶有一個Run()
方法,線程池中的線程拿到數(shù)據(jù)后交給Run
方法對任務(wù)進(jìn)行處理(通信),這個方法實際就是上文實現(xiàn)的Service
函數(shù),將其放入任務(wù)類中充當(dāng)Run()
方法,但是這樣實際上并不利于軟件分層。我們可以給任務(wù)類新增一個仿函數(shù),當(dāng)任務(wù)執(zhí)行Run方法處理任務(wù)時就可以以回調(diào)的方式處理該任務(wù)
#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;
};
仿函數(shù)類 Handler類
使用Handler類可以讓我們的服務(wù)器處理不同的任務(wù),實際想要怎么處理這個任務(wù)得由Handler函數(shù)定。如果想讓服務(wù)器處理其它任務(wù),只需要修改Handler當(dāng)中的()重載函數(shù)即可,比如可以增加一個int 類型參數(shù)flag
,當(dāng)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) { // 對端關(guān)閉連接
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;
}
}
}
};
文章來源:http://www.zghlxwxcb.cn/news/detail-533543.html
現(xiàn)在無論有多少個客戶端發(fā)送請求,服務(wù)端只會有5個線程為其提供服務(wù),線程池中的線程數(shù)不會因為客戶端的增多而增多。這些線程也不會因為客戶端的退出而退出
參考文章:「2021dragon」的文章
原文鏈接:https://blog.csdn.net/chenlong_cxy/article/details/124650187文章來源地址http://www.zghlxwxcb.cn/news/detail-533543.html
到了這里,關(guān)于【計算機(jī)網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(二)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!