目錄
一、相關(guān)函數(shù)?
1、listen()
2、accept()
3、connect()?
4、兩種IP地址轉(zhuǎn)換方式?
5、TCP和UDP數(shù)據(jù)發(fā)送和接收函數(shù)對比
5、log.hpp自定義記錄日志
二、udp_server.hpp單進(jìn)程版本
三、tcp_server.cc
四、Telnet客戶端(代替tcp_client.cc)
五、多進(jìn)程實(shí)現(xiàn)udp_server.hpp
1、多進(jìn)程版本一
2、tcp_client.cc
3、多進(jìn)程版本二
六、多線程版本
七、線程池版本
tcp_server.hpp
ThreadPool代碼
lockGuard.hpp
log.hpp
thread.hpp
threadPool.hpp
八、實(shí)現(xiàn)回顯、字符轉(zhuǎn)換、在線字典查詢服務(wù)
tcp_server.hpp
?三個(gè)服務(wù)函數(shù)
TcpServer類
tcp_server.cc
tcp_client.cc
九、TCP協(xié)議通訊流程
1、服務(wù)器初始化
2、建立連接的過程(三次握手)
3、數(shù)據(jù)傳輸?shù)倪^程
4、斷開連接的過程(四次揮手)
一、相關(guān)函數(shù)?
1、listen()
int listen(int socket, int backlog);
只有對于TCP服務(wù)端套接字才需要調(diào)用此函數(shù),它使套接字進(jìn)入監(jiān)聽狀態(tài),等待客戶端的連接請求。參數(shù)含義如下:
成功監(jiān)聽后返回0,出錯(cuò)則返回非零錯(cuò)誤碼。
-
socket
:要監(jiān)聽的服務(wù)器端套接字描述符。 -
backlog
:指定同時(shí)可以排隊(duì)等待處理的最大連接數(shù)。超過這個(gè)數(shù)量的連接請求會(huì)被拒絕。
2、accept()
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
也是只在TCP服務(wù)器端使用,用于接受一個(gè)客戶端的連接請求。參數(shù)含義如下:
成功接受一個(gè)連接請求后,accept()函數(shù)返回一個(gè)新的套接字描述符,這個(gè)描述符用于與該客戶端進(jìn)行通信。同時(shí),address參數(shù)所指向的結(jié)構(gòu)體會(huì)填充上客戶端的地址信息。
-
socket
:已經(jīng)監(jiān)聽的服務(wù)器端套接字描述符。 -
address
:用于存儲(chǔ)新連接客戶端的地址信息的sockaddr結(jié)構(gòu)體指針。 -
address_len
:指向一個(gè)socklen_t變量的指針,用于記錄地址結(jié)構(gòu)體的實(shí)際大小,傳入時(shí)應(yīng)初始化為地址結(jié)構(gòu)體的大小,返回時(shí)會(huì)更新為實(shí)際填充的大小。
3、connect()?
TCP的connect
函數(shù)是用于客戶端編程中的一個(gè)重要系統(tǒng)調(diào)用,它是TCP/IP協(xié)議棧的一部分,允許客戶端應(yīng)用程序建立與遠(yuǎn)程服務(wù)器的連接。在C語言或C++編程環(huán)境下,connect
函數(shù)的基本原型如下:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
-
sockfd:這是一個(gè)之前通過
socket()
函數(shù)創(chuàng)建并返回的套接字描述符,標(biāo)識(shí)著一個(gè)未連接的套接字。 -
serv_addr:這是一個(gè)指向
sockaddr
結(jié)構(gòu)體的指針,包含了遠(yuǎn)程服務(wù)器的地址信息,對于IPv4而言,通常會(huì)使用sockaddr_in
結(jié)構(gòu)體,其中包括服務(wù)器的IP地址和端口號(hào)。 -
addrlen:這是
serv_addr
指向的地址結(jié)構(gòu)體的長度。
當(dāng)調(diào)用connect
函數(shù)時(shí),TCP客戶端會(huì)執(zhí)行以下動(dòng)作:
-
發(fā)起連接請求:
connect
函數(shù)會(huì)觸發(fā)TCP三次握手的過程,即客戶端發(fā)送一個(gè)SYN(同步)分節(jié)給服務(wù)器,請求建立連接。 -
等待響應(yīng):客戶端會(huì)等待服務(wù)器回應(yīng)SYN+ACK分節(jié),然后發(fā)送ACK(確認(rèn))分節(jié)作為響應(yīng)。
-
連接建立:一旦三次握手成功完成,連接就建立了,此時(shí)套接字的狀態(tài)轉(zhuǎn)變?yōu)?code>ESTABLISHED,客戶端可以在該套接字上進(jìn)行讀寫操作。
-
錯(cuò)誤處理:如果在一定時(shí)間內(nèi)沒有收到服務(wù)器的響應(yīng),或者由于其他原因無法建立連接(比如網(wǎng)絡(luò)問題、服務(wù)器拒絕連接等),
connect
函數(shù)會(huì)返回錯(cuò)誤,errno會(huì)被設(shè)置為相應(yīng)的錯(cuò)誤代碼,如ETIMEDOUT
(超時(shí))、ECONNREFUSED
(連接被拒絕)等。
例如,假設(shè)已經(jīng)有了一個(gè)未連接的套接字sockfd
,并且有了服務(wù)器的地址信息serv_addr
,可以通過以下方式調(diào)用connect
函數(shù):
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT_NUMBER);
inet_pton(AF_INET, SERVER_IP_ADDRESS, &serv_addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect error");
// 錯(cuò)誤處理...
} else {
// 連接成功,可以開始進(jìn)行數(shù)據(jù)交換
}
在這里,PORT_NUMBER
是服務(wù)器監(jiān)聽的端口號(hào),SERVER_IP_ADDRESS
是服務(wù)器的IP地址,通過inet_pton
函數(shù)將IP地址字符串轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的形式存放在sin_addr
中。成功連接后,應(yīng)用程序就可以通過write
、read
或其他IO函數(shù)與服務(wù)器進(jìn)行雙向數(shù)據(jù)傳輸。
4、兩種IP地址轉(zhuǎn)換方式?
在這段代碼中,TcpServer
?類用于創(chuàng)建一個(gè) TCP 服務(wù)器,初始化時(shí)會(huì)綁定到特定的 IP 地址和端口上。
第一種方式:
// 默認(rèn)構(gòu)造函數(shù),若不傳入ip則默認(rèn)綁定所有網(wǎng)絡(luò)接口,“0.0.0.0”等于空字符“”
TcpServer(uint16_t port, std::string ip = "0.0.0.0")
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
-
inet_pton
?是一個(gè)從點(diǎn)分十進(jìn)制格式的字符串轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序二進(jìn)制格式的函數(shù),這里它將?_ip
?字符串轉(zhuǎn)換為?sockaddr_in
?結(jié)構(gòu)體中的?sin_addr
?成員。 - 當(dāng)?
_ip
?為空字符串或默認(rèn)值 "0.0.0.0" 時(shí),服務(wù)器將會(huì)監(jiān)聽所有可用網(wǎng)絡(luò)接口。
第二種方式:
TcpServer(uint16_t port, std::string ip = "") // 若不傳入ip,則默認(rèn)為空字符串
inet_aton(_ip.c_str(), &local.sin_addr);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
-
inet_aton
?和?inet_pton
?功能類似,也是將點(diǎn)分十進(jìn)制的 IP 地址字符串轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序的二進(jìn)制表示形式。 - 如果?
_ip
?為空字符串,那么下面的條件語句會(huì)執(zhí)行:local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
- 當(dāng)?
_ip
?為空時(shí),sin_addr.s_addr
?被賦值為?INADDR_ANY
,這同樣表示服務(wù)器應(yīng)監(jiān)聽所有可用網(wǎng)絡(luò)接口。
- 當(dāng)?
結(jié)論:兩種方式都可以實(shí)現(xiàn)當(dāng)未傳入 IP 地址參數(shù)時(shí),服務(wù)器監(jiān)聽所有網(wǎng)絡(luò)接口的目的。不過,在現(xiàn)代 C++ 編程實(shí)踐中,推薦使用?inet_pton
?函數(shù),因?yàn)樗С?IPv6 地址,并且在一些平臺(tái)上兼容性更好。
5、TCP和UDP數(shù)據(jù)發(fā)送和接收函數(shù)對比
TCP
- 數(shù)據(jù)發(fā)送:
-
write()
?或?send()
?函數(shù)用于在已建立連接的TCP套接字上發(fā)送數(shù)據(jù)。send()
?可以帶有額外的標(biāo)志參數(shù),但對于大多數(shù)情況,write()
?即可滿足需求。
-
- 數(shù)據(jù)接收:
-
read()
?或?recv()
?函數(shù)用于在TCP套接字上接收數(shù)據(jù)。recv()
?同樣可以攜帶標(biāo)志參數(shù),但通常情況下,read()
?已足夠用于接收TCP數(shù)據(jù)流。
-
UDP
- 數(shù)據(jù)發(fā)送:
- 因?yàn)閁DP是無連接的協(xié)議,所以在發(fā)送數(shù)據(jù)時(shí)需要指定目的地址,因此使用?
sendto()
?函數(shù),它需要包含目標(biāo)IP地址和端口號(hào)的sockaddr
結(jié)構(gòu)體作為參數(shù)。
- 因?yàn)閁DP是無連接的協(xié)議,所以在發(fā)送數(shù)據(jù)時(shí)需要指定目的地址,因此使用?
- 數(shù)據(jù)接收:
- 對應(yīng)地,在接收UDP數(shù)據(jù)時(shí),不僅要接收數(shù)據(jù),還需要得到發(fā)送數(shù)據(jù)的源地址和端口號(hào),因此使用?
recvfrom()
?函數(shù),它不僅能返回接收到的數(shù)據(jù),還能填充提供給它的sockaddr
結(jié)構(gòu)體。
- 對應(yīng)地,在接收UDP數(shù)據(jù)時(shí),不僅要接收數(shù)據(jù),還需要得到發(fā)送數(shù)據(jù)的源地址和端口號(hào),因此使用?
5、log.hpp自定義記錄日志
#pragma once
#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"};
#define LOGFILE "./threadpool.log"
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if (level == DEBUG)
return;
#endif
char stdBuffer[1024];
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);
char logBuffer[1024];
va_list args;
va_start(args, format);
vsnprintf(logBuffer, sizeof(logBuffer), format, args);
va_end(args);
FILE *fp = fopen("LOGFILE", "a+");
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
二、udp_server.hpp單進(jìn)程版本
TcpServer
類實(shí)現(xiàn)了創(chuàng)建TCP服務(wù)器、監(jiān)聽客戶端連接、處理客戶端連接服務(wù)的基本功能。通過調(diào)用initServer()
方法初始化服務(wù)器,然后調(diào)用start()
方法開始監(jiān)聽和處理客戶端連接。當(dāng)有新客戶端連接時(shí),創(chuàng)建子進(jìn)程(或線程)處理與該客戶端的通信。
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>
// 定義靜態(tài)函數(shù),用于處理客戶端連接的服務(wù)邏輯
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buffer[1024];
while (true)
{
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << clientip << ":" << clientport << "#" << buffer << std::endl;
}
else if (s == 0)
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
// 定義TCP服務(wù)器類
class TcpServer
{
private:
const static int gbacklog = 20; // 服務(wù)器監(jiān)聽隊(duì)列長度(最大掛起連接數(shù))
public:
// 構(gòu)造函數(shù),接收服務(wù)器監(jiān)聽端口和可選的綁定IP地址
TcpServer(uint16_t port, std::string ip = "")
: listensock(-1), _port(port), _ip(ip)
{}
// 初始化服務(wù)器:創(chuàng)建套接字、綁定端口、監(jiān)聽連接
void initServer()
{
listensock = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建TCP套接字
if (listensock < 0)
{
logMessage(FATAL, "%d%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success listensock: %d", listensock);
struct sockaddr_in local; // 用于存儲(chǔ)服務(wù)器地址信息的結(jié)構(gòu)體
memset(&local, 0, sizeof local);
local.sin_family = AF_INET; // 設(shè)置協(xié)議族為IPv4
local.sin_port = htons(_port); // 設(shè)置服務(wù)器監(jiān)聽端口,轉(zhuǎn)換為主機(jī)字節(jié)序
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 設(shè)置服務(wù)器綁定IP地址
if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定套接字到指定地址和端口
{
logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
exit(3);
}
if (listen(listensock, gbacklog) < 0) // 開始監(jiān)聽客戶端連接,設(shè)置監(jiān)聽隊(duì)列長度
{
logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
// 啟動(dòng)服務(wù)器:設(shè)置信號(hào)處理、循環(huán)接受客戶端連接并創(chuàng)建子進(jìn)程處理
void start()
{
// 設(shè)置信號(hào)處理,忽略SIGCHLD信號(hào)以自動(dòng)回收子進(jìn)程資源
signal(SIGCHLD, SIG_IGN);
while (true)
{
struct sockaddr_in src; // 用于存儲(chǔ)客戶端地址信息的結(jié)構(gòu)體
socklen_t len = sizeof src;
int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客戶端連接請求
if (servicesock < 0)
{
logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
continue;
}
uint16_t client_port = ntohs(src.sin_port); // 獲取客戶端端口
std::string client_ip = inet_ntoa(src.sin_addr); // 獲取客戶端IP地址
logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
servicesock, client_ip.c_str(), client_port);
service(servicesock, client_ip, client_port); // 子進(jìn)程處理客戶端連接
close(servicesock); // 主進(jìn)程中關(guān)閉已接受的客戶端連接套接字
}
}
// 析構(gòu)函數(shù)
~TcpServer()
{
}
private:
uint16_t _port; // 服務(wù)器監(jiān)聽端口
std::string _ip; // 服務(wù)器綁定IP地址(可選)
int listensock; // 服務(wù)器監(jiān)聽套接字
};
這段代碼定義了一個(gè)名為TcpServer
的類,用于實(shí)現(xiàn)一個(gè)基礎(chǔ)的TCP服務(wù)器。該服務(wù)器具有以下功能:
-
構(gòu)造函數(shù):
-
TcpServer(uint16_t port, std::string ip = "")
:初始化服務(wù)器對象,接收一個(gè)監(jiān)聽端口號(hào)port
和一個(gè)可選的服務(wù)器綁定IP地址ip
。默認(rèn)情況下,如果不提供IP地址,服務(wù)器將在所有可用網(wǎng)絡(luò)接口上監(jiān)聽。
-
-
initServer():
- 創(chuàng)建TCP套接字。
- 使用
struct sockaddr_in
結(jié)構(gòu)體存儲(chǔ)服務(wù)器地址信息。 - 綁定服務(wù)器套接字到指定的IP地址和端口號(hào)。
- 開始監(jiān)聽客戶端的連接請求,并設(shè)置監(jiān)聽隊(duì)列的最大長度為
gbacklog
(默認(rèn)20)。
-
service()靜態(tài)函數(shù):
- 用于處理與單個(gè)客戶端的連接服務(wù)邏輯。
- 通過
read()
函數(shù)讀取客戶端發(fā)送過來的數(shù)據(jù),然后回顯到控制臺(tái)。 - 若讀取到的數(shù)據(jù)長度為0,表示客戶端關(guān)閉連接,服務(wù)器也結(jié)束對該客戶端的服務(wù)。
- 若讀取過程中發(fā)生錯(cuò)誤,則記錄錯(cuò)誤并結(jié)束服務(wù)。
- 使用
write()
函數(shù)將讀取到的數(shù)據(jù)回傳給客戶端。
-
start():
- 設(shè)置信號(hào)處理,忽略
SIGCHLD
信號(hào),這樣操作系統(tǒng)會(huì)在子進(jìn)程結(jié)束后自動(dòng)回收資源。 - 服務(wù)器進(jìn)入無限循環(huán),不斷地通過
accept()
函數(shù)接受新的客戶端連接請求。 - 當(dāng)接收到新的連接請求時(shí),獲取客戶端的IP地址和端口號(hào),并調(diào)用
service()
函數(shù)處理客戶端連接。 - 處理完客戶端連接后,關(guān)閉已接受的客戶端連接套接字。
- 設(shè)置信號(hào)處理,忽略
-
析構(gòu)函數(shù):
- 類似于其他類的析構(gòu)函數(shù),
~TcpServer()
在此處沒有特別的操作,但在實(shí)際開發(fā)中可能需要關(guān)閉監(jiān)聽套接字或執(zhí)行其他清理工作。
- 類似于其他類的析構(gòu)函數(shù),
三、tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
// 定義展示程序用法的幫助函數(shù)
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
// 檢查命令行參數(shù)數(shù)量是否為2(即程序名 + 監(jiān)聽端口號(hào))
if (argc != 2)
{
usage(argv[0]); // 輸出程序使用說明
exit(1); // 參數(shù)錯(cuò)誤,退出程序
}
// 從命令行參數(shù)中獲取監(jiān)聽端口號(hào)并轉(zhuǎn)換為整型數(shù)值
uint16_t port = atoi(argv[1]);
// 使用智能指針創(chuàng)建并管理TCP服務(wù)器實(shí)例
std::unique_ptr<TcpServer> svr(new TcpServer(port));
// 初始化服務(wù)器,包括創(chuàng)建套接字、綁定端口和開始監(jiān)聽客戶端連接
svr->initServer();
// 啟動(dòng)服務(wù)器,開始循環(huán)接受客戶端連接并創(chuàng)建子進(jìn)程處理
svr->start();
// 當(dāng)`svr`的作用域結(jié)束時(shí),智能指針會(huì)自動(dòng)釋放TCP服務(wù)器實(shí)例
// 此時(shí)由于TCP服務(wù)器已經(jīng)進(jìn)入了無限循環(huán)的`start()`方法,程序不會(huì)立即結(jié)束
// 而是在接收到終止信號(hào)(如Ctrl+C)或系統(tǒng)關(guān)閉時(shí),TCP服務(wù)器才會(huì)停止運(yùn)行
// 返回0,表示程序正常退出
return 0;
}
四、Telnet客戶端(代替tcp_client.cc)
在Linux CentOS環(huán)境下,telnet
?是一個(gè)命令行工具,用于通過Telnet協(xié)議與遠(yuǎn)程主機(jī)上的服務(wù)進(jìn)行交互。Telnet最初設(shè)計(jì)用于遠(yuǎn)程登錄和命令行交互,但在現(xiàn)代環(huán)境中,由于其不提供加密保護(hù),通常被更安全的SSH(Secure Shell)協(xié)議所取代。盡管如此,Telnet在某些特定場景下(如測試、調(diào)試網(wǎng)絡(luò)服務(wù))因其簡單易用仍被臨時(shí)使用。
1. 安裝Telnet客戶端
在CentOS系統(tǒng)中,telnet
客戶端可能未預(yù)裝。若要使用telnet
,首先需要確保已經(jīng)安裝了該客戶端??梢酝ㄟ^以下命令安裝:
sudo yum install telnet
2. 使用telnet
命令
基本語法如下:
telnet [options] host [port]
-
options
:可選的命令行選項(xiàng),如?-l username
?用于指定登錄用戶名。 -
host
:遠(yuǎn)程主機(jī)的IP地址或域名,如?192.168.0.100
?或?example.com
。 -
port
:可選的端口號(hào),用于指定遠(yuǎn)程主機(jī)上服務(wù)監(jiān)聽的端口。如果不指定,默認(rèn)為Telnet服務(wù)的標(biāo)準(zhǔn)端口?23
。
3. 示例:連接到遠(yuǎn)程主機(jī)
要連接到IP地址為192.168.0.100
、端口為23
的遠(yuǎn)程主機(jī),執(zhí)行:
telnet 192.168.0.100
或者連接到特定端口(如?8080
)上的服務(wù):
telnet 192.168.0.100 8080
4. 交互過程
-
成功連接后,將看到類似于以下的響應(yīng):
Trying 192.168.0.100... Connected to 192.168.0.100. Escape character is '^]'.
-
如果遠(yuǎn)程主機(jī)要求身份驗(yàn)證,可能需要輸入用戶名和密碼。這些憑據(jù)將以明文形式在網(wǎng)絡(luò)中傳輸。
-
輸入用戶名和密碼后(如果有),將進(jìn)入遠(yuǎn)程主機(jī)的命令行環(huán)境,可以像在本地終端一樣輸入命令并觀察響應(yīng)。
-
若要斷開連接,可以輸入命令?
logout
?或?quit
,然后按回車。或者直接使用快捷鍵?Ctrl+]
(即按住Ctrl
鍵同時(shí)按下右方括號(hào)]
),接著輸入?q
?并回車,快速退出Telnet客戶端。
5. 安全注意事項(xiàng)
由于Telnet不提供任何加密保護(hù),其明文傳輸特性使得用戶名、密碼以及整個(gè)會(huì)話內(nèi)容都容易被嗅探。在實(shí)際環(huán)境中,強(qiáng)烈建議使用更安全的替代方案,如SSH(Secure Shell),它提供了加密的遠(yuǎn)程登錄功能,能有效保護(hù)敏感信息的安全。如果必須使用Telnet,請確保僅在受信任的網(wǎng)絡(luò)環(huán)境中進(jìn)行,并且了解潛在的安全風(fēng)險(xiǎn)。
6. 其他實(shí)用操作
除了基本的遠(yuǎn)程登錄,telnet
還可以用于簡單的網(wǎng)絡(luò)診斷,如測試某個(gè)端口是否開放。例如,要檢查遠(yuǎn)程主機(jī)example.com
的80
端口是否開放,可以執(zhí)行:
telnet example.com 80
如果端口開放且服務(wù)響應(yīng),將看到類似以下的輸出(以HTTP服務(wù)為例):
Trying 93.184.216.34...
Connected to example.com.
Escape character is '^]'.
此時(shí),可以嘗試輸入HTTP請求(如GET / HTTP/1.1
,然后回車兩次),觀察服務(wù)是否返回響應(yīng)。如果端口未開放或無服務(wù)響應(yīng),將看到類似“Connection refused”或“Timeout”的錯(cuò)誤消息。這種簡易的測試方法可以幫助初步判斷遠(yuǎn)程主機(jī)的網(wǎng)絡(luò)服務(wù)狀態(tài)。然而,對于專業(yè)的網(wǎng)絡(luò)診斷,建議使用更專業(yè)的工具,如nc
(Netcat)或nmap
。
五、多進(jìn)程實(shí)現(xiàn)udp_server.hpp
1、多進(jìn)程版本一
這個(gè)TcpServer
類利用了fork()
函數(shù)實(shí)現(xiàn)了多進(jìn)程方式處理并發(fā)客戶端連接,相比單進(jìn)程版本增加了并發(fā)能力和資源隔離性,但也引入了額外的系統(tǒng)調(diào)用開銷。
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>
// 定義靜態(tài)函數(shù),用于處理客戶端連接的服務(wù)邏輯
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buffer[1024];
while (true)
{
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << clientip << ":" << clientport << "#" << buffer << std::endl;
}
else if (s == 0)
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
// 定義TCP服務(wù)器類
class TcpServer
{
private:
const static int gbacklog = 20; // 服務(wù)器監(jiān)聽隊(duì)列長度(最大掛起連接數(shù))
public:
// 構(gòu)造函數(shù),接收服務(wù)器監(jiān)聽端口和可選的綁定IP地址
TcpServer(uint16_t port, std::string ip = "")
: listensock(-1), _port(port), _ip(ip)
{}
// 初始化服務(wù)器:創(chuàng)建套接字、綁定端口、監(jiān)聽連接
void initServer()
{
listensock = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建TCP套接字
if (listensock < 0)
{
logMessage(FATAL, "%d%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success listensock: %d", listensock);
struct sockaddr_in local; // 用于存儲(chǔ)服務(wù)器地址信息的結(jié)構(gòu)體
memset(&local, 0, sizeof local);
local.sin_family = AF_INET; // 設(shè)置協(xié)議族為IPv4
local.sin_port = htons(_port); // 設(shè)置服務(wù)器監(jiān)聽端口,轉(zhuǎn)換為主機(jī)字節(jié)序
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 設(shè)置服務(wù)器綁定IP地址
if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定套接字到指定地址和端口
{
logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
exit(3);
}
if (listen(listensock, gbacklog) < 0) // 開始監(jiān)聽客戶端連接,設(shè)置監(jiān)聽隊(duì)列長度
{
logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
// 啟動(dòng)服務(wù)器:設(shè)置信號(hào)處理、循環(huán)接受客戶端連接并創(chuàng)建子進(jìn)程處理
void start()
{
// 設(shè)置信號(hào)處理,忽略SIGCHLD信號(hào)以自動(dòng)回收子進(jìn)程資源
signal(SIGCHLD, SIG_IGN);
while (true)
{
struct sockaddr_in src; // 用于存儲(chǔ)客戶端地址信息的結(jié)構(gòu)體
socklen_t len = sizeof src;
int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客戶端連接請求
if (servicesock < 0)
{
logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
continue;
}
uint16_t client_port = ntohs(src.sin_port); // 獲取客戶端端口
std::string client_ip = inet_ntoa(src.sin_addr); // 獲取客戶端IP地址
logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
servicesock, client_ip.c_str(), client_port);
// 多進(jìn)程處理客戶端連接
pid_t id = fork(); // 創(chuàng)建子進(jìn)程
assert(id != -1); // 斷言子進(jìn)程創(chuàng)建成功
if (id == 0) // 子進(jìn)程
{
close(listensock); // 子進(jìn)程中關(guān)閉監(jiān)聽套接字
service(servicesock, client_ip, client_port); // 子進(jìn)程處理客戶端連接
exit(0); // 子進(jìn)程處理完客戶端連接后退出
}
close(servicesock); // 主進(jìn)程中關(guān)閉已接受的客戶端連接套接字
}
}
// 析構(gòu)函數(shù)
~TcpServer()
{
}
private:
uint16_t _port; // 服務(wù)器監(jiān)聽端口
std::string _ip; // 服務(wù)器綁定IP地址(可選)
int listensock; // 服務(wù)器監(jiān)聽套接字
};
這個(gè)TcpServer
類相較于單進(jìn)程版本,主要區(qū)別在于如何處理每個(gè)客戶端連接。在這個(gè)多進(jìn)程版本中,服務(wù)器在接收到客戶端連接請求后,通過fork()
系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程來處理每個(gè)客戶端連接。以下是不同之處的詳細(xì)說明:
-
啟動(dòng)服務(wù)器(start()方法)的變化:
-
在
start()
方法內(nèi),當(dāng)服務(wù)器通過accept()
函數(shù)成功接受一個(gè)客戶端連接后,調(diào)用fork()
創(chuàng)建一個(gè)子進(jìn)程。 -
子進(jìn)程中:
- 關(guān)閉監(jiān)聽套接字(
listensock
),因?yàn)樗鼉H用于監(jiān)聽新的連接請求,無需在處理現(xiàn)有客戶端連接的子進(jìn)程中保持打開。 - 調(diào)用
service()
函數(shù)處理客戶端連接。 - 在
service()
函數(shù)結(jié)束后,子進(jìn)程調(diào)用exit(0)
退出,釋放資源。
- 關(guān)閉監(jiān)聽套接字(
-
主進(jìn)程中:同樣關(guān)閉已接受的客戶端連接套接字,但在主進(jìn)程中這樣做是為了讓主進(jìn)程能夠繼續(xù)監(jiān)聽新的客戶端連接,而不是去處理已連接的客戶端通信。
-
-
多進(jìn)程處理客戶端連接的優(yōu)勢:
- 并發(fā)處理:父進(jìn)程可以繼續(xù)接受新的客戶端連接請求,而子進(jìn)程獨(dú)立處理已連接的客戶端,從而實(shí)現(xiàn)并發(fā)處理多個(gè)客戶端連接。
- 資源隔離:每個(gè)客戶端連接都在各自的子進(jìn)程中處理,使得各個(gè)連接間的資源相互獨(dú)立,避免了共享資源的競爭問題。
-
注意點(diǎn):在實(shí)際部署中,頻繁創(chuàng)建和銷毀子進(jìn)程可能會(huì)帶來一定的開銷,尤其是當(dāng)客戶端連接數(shù)量很大時(shí)。在某些情況下,可能選擇多線程而非多進(jìn)程的方式來處理并發(fā)連接,這取決于具體應(yīng)用場景和性能要求。
-
在上述提供的TCP服務(wù)器類中,
TcpServer
的start
方法中,在主進(jìn)程每次接受到客戶端連接請求并創(chuàng)建子進(jìn)程后,都會(huì)關(guān)閉已接受的客戶端連接套接字servicesock
。這是因?yàn)橹鬟M(jìn)程并不需要處理與已連接客戶端的實(shí)際通信,這部分任務(wù)交由子進(jìn)程完成。主進(jìn)程關(guān)閉
servicesock
的原因:-
資源釋放:每個(gè)文件描述符都是系統(tǒng)資源的一部分。在主進(jìn)程關(guān)閉已接受的客戶端連接套接字后,可以釋放系統(tǒng)資源,以便主進(jìn)程可以繼續(xù)接受新的客戶端連接,而不會(huì)因?yàn)槲募枋龇谋M而導(dǎo)致無法創(chuàng)建新的連接。
-
避免資源競爭:當(dāng)主進(jìn)程不關(guān)閉已連接的客戶端套接字時(shí),子進(jìn)程和主進(jìn)程之間會(huì)產(chǎn)生資源競爭,因?yàn)橥惶捉幼衷诟缸舆M(jìn)程中同時(shí)存在,會(huì)導(dǎo)致難以預(yù)料的行為。
-
使用telnet客戶端運(yùn)行示例:
2、tcp_client.cc
這段代碼是一個(gè)簡單的TCP客戶端程序,首先從命令行參數(shù)獲取服務(wù)器的IP地址和端口號(hào),然后創(chuàng)建一個(gè)TCP套接字并與服務(wù)器建立連接。接著,程序進(jìn)入一個(gè)無限循環(huán),循環(huán)中接收用戶輸入并通過套接字發(fā)送給服務(wù)器,并從服務(wù)器接收回顯的數(shù)據(jù)。如果在任何環(huán)節(jié)出現(xiàn)錯(cuò)誤(如創(chuàng)建套接字失敗、連接服務(wù)器失敗等),程序?qū)⒋蛴″e(cuò)誤信息并退出。
#include <iostream>
#include <string>
#include <unistd.h> // 提供Unix標(biāo)準(zhǔn)函數(shù),如close、read、write等
#include <sys/socket.h> // 提供創(chuàng)建、操作套接字的函數(shù)原型
#include <arpa/inet.h> // 提供IPv4地址轉(zhuǎn)換函數(shù),如inet_addr、htonl等
#include <netinet/in.h> // 提供Internet地址家族(AF_INET)相關(guān)的結(jié)構(gòu)和常量
#include <sys/types.h> // 提供通用的數(shù)據(jù)類型定義
// 使用示例:./tcp_client 目標(biāo)IP 目標(biāo)端口
void usage(std::string proc)
{
std::cout << "Usage: " << proc << " serverIp serverPort" << std::endl;
}
int main(int argc, char *argv[])
{
// 檢查命令行參數(shù)個(gè)數(shù)是否正確(IP地址+端口號(hào))
if (argc != 3)
{
usage(argv[0]);
exit(1); // 參數(shù)錯(cuò)誤,退出程序
}
// 從命令行參數(shù)中提取目標(biāo)服務(wù)器的IP地址和端口號(hào)
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]); // 將端口號(hào)字符串轉(zhuǎn)換為整數(shù)
// 創(chuàng)建一個(gè)基于IPv4的TCP套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 檢查套接字創(chuàng)建是否成功
if (sock < 0)
{
std::cerr << "socket create error" << std::endl;
exit(2); // 套接字創(chuàng)建失敗,退出程序
}
// 初始化服務(wù)器地址結(jié)構(gòu)體
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // 清零內(nèi)存區(qū)域
server.sin_family = AF_INET; // 設(shè)置為IPv4協(xié)議
server.sin_port = htons(serverport); // 將端口號(hào)轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 將IP地址字符串轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序
// 嘗試連接服務(wù)器
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3); // 連接服務(wù)器失敗,退出程序
}
// 進(jìn)入通信循環(huán),等待用戶輸入并向服務(wù)器發(fā)送數(shù)據(jù),接收并顯示服務(wù)器響應(yīng)
while (true)
{
std::string line;
std::cout << "請輸入# ";
std::getline(std::cin, line); // 從標(biāo)準(zhǔn)輸入讀取一行文本
send(sock, line.c_str(), line.size(), 0); // 發(fā)送消息至服務(wù)器
// 準(zhǔn)備接收服務(wù)器響應(yīng)的緩沖區(qū)
char buffer[1024];
// 接收服務(wù)器數(shù)據(jù)
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
// 如果接收到數(shù)據(jù)
if (s > 0)
{
buffer[s]=0; // 在有效數(shù)據(jù)后面添加結(jié)束符,方便當(dāng)作字符串處理
std::cout<<"server 回顯# "<<buffer<<std::endl; // 輸出服務(wù)器響應(yīng)
}
}
// 主程序結(jié)束,返回0表示正常退出
return 0;
}
3、多進(jìn)程版本二
class TcpServer
{
private:
const static int gbacklog = 20; // 服務(wù)器監(jiān)聽隊(duì)列長度(最大掛起連接數(shù))
public:
// 啟動(dòng)服務(wù)器:設(shè)置信號(hào)處理、循環(huán)接受客戶端連接并創(chuàng)建子進(jìn)程處理
void start()
{
while (true)
{
struct sockaddr_in src; // 用于存儲(chǔ)客戶端地址信息的結(jié)構(gòu)體
socklen_t len = sizeof src;
int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客戶端連接請求
if (servicesock < 0)
{
logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
continue;
}
uint16_t client_port = ntohs(src.sin_port); // 獲取客戶端端口
std::string client_ip = inet_ntoa(src.sin_addr); // 獲取客戶端IP地址
logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
servicesock, client_ip.c_str(), client_port);
// 多進(jìn)程v2
pid_t id = fork();
if (id == 0)
{
// 子進(jìn)程
close(listensock);
if (fork() > 0 )// 子進(jìn)程本身
exit(0); // 子進(jìn)程本身立即退出
// 孫子進(jìn)程,孤兒進(jìn)程,OS領(lǐng)養(yǎng),OS在孤兒進(jìn)程退出的時(shí)候,由OS自動(dòng)回收孤兒進(jìn)程!
service(servicesock, client_ip, client_port);
}
// 父進(jìn)程
waitpid(id, nullptr, 0); // 不會(huì)阻塞!
close(servicesock);
}
}
private:
uint16_t _port; // 服務(wù)器監(jiān)聽端口
std::string _ip; // 服務(wù)器綁定IP地址(可選)
int listensock; // 服務(wù)器監(jiān)聽套接字
};
六、多線程版本
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include "log.hpp" // 自定義的日志記錄模塊
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <cassert>
// 靜態(tài)函數(shù),用于處理客戶端連接的服務(wù)邏輯
// 參數(shù):sock - 與客戶端建立連接的套接字
// clientip - 客戶端IP地址字符串
// clientport - 客戶端端口號(hào)
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buffer[1024]; // 緩沖區(qū),用于讀取和發(fā)送數(shù)據(jù)
while (true)
{
ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // 從客戶端接收數(shù)據(jù)
if (s > 0)
{
buffer[s] = 0; // 添加字符串結(jié)束符
std::cout << clientip << ":" << clientport << "#" << buffer << std::endl; // 輸出接收到的數(shù)據(jù)和客戶端信息
write(sock, buffer, strlen(buffer)); // 將接收到的數(shù)據(jù)原樣發(fā)送回客戶端
}
else if (s == 0)
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport); // 如果讀取到EOF,認(rèn)為客戶端已關(guān)閉連接
break;
}
else
{
logMessage(ERROR, "read socket error,%d:%s", errno, strerror(errno)); // 若讀取發(fā)生錯(cuò)誤,記錄錯(cuò)誤信息并斷開連接
break;
}
}
}
// 定義ThreadData類,用于傳遞給線程處理函數(shù)的參數(shù)
class ThreadData
{
public:
int _sock; // 客戶端連接套接字
std::string _ip; // 客戶端IP地址
uint16_t _port; // 客戶端端口號(hào)
};
// TCP服務(wù)器類
class TcpServer
{
private:
const static int gbacklog = 20; // 服務(wù)器監(jiān)聽隊(duì)列大小,表示能同時(shí)待處理的連接請求個(gè)數(shù)
// 線程處理函數(shù),負(fù)責(zé)處理客戶端連接
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 確保線程結(jié)束后能夠被內(nèi)核回收資源
ThreadData *td = static_cast<ThreadData *>(args);
service(td->_sock, td->_ip, td->_port); // 調(diào)用service函數(shù)處理客戶端連接
delete td; // 刪除ThreadData對象
return nullptr;
}
public:
// 構(gòu)造函數(shù),接收服務(wù)器監(jiān)聽端口和可選的綁定IP地址
TcpServer(uint16_t port, std::string ip = "")
: listensock(-1), _port(port), _ip(ip)
{}
// 初始化服務(wù)器:創(chuàng)建套接字、綁定端口、監(jiān)聽連接
void initServer()
{
listensock = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建TCP套接字
if (listensock < 0)
{
logMessage(FATAL, "%d%s", errno, strerror(errno)); // 記錄并輸出錯(cuò)誤信息
exit(2); // 出錯(cuò)則退出程序
}
logMessage(NORMAL, "create socket success listensock: %d", listensock);
struct sockaddr_in local; // 本地服務(wù)器地址結(jié)構(gòu)體
memset(&local, 0, sizeof local); // 清零結(jié)構(gòu)體內(nèi)容
local.sin_family = AF_INET; // 設(shè)置為IPv4協(xié)議
local.sin_port = htons(_port); // 設(shè)置服務(wù)器監(jiān)聽端口(主機(jī)字節(jié)序)
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 設(shè)置服務(wù)器綁定IP地址(若為空則監(jiān)聽所有地址)
if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0) // 綁定套接字到指定地址和端口
{
logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
exit(3);
}
if (listen(listensock, gbacklog) < 0) // 開始監(jiān)聽客戶端連接,設(shè)置監(jiān)聽隊(duì)列長度
{
logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success"); // 初始化服務(wù)器成功
}
// 啟動(dòng)服務(wù)器:設(shè)置信號(hào)處理、循環(huán)接受客戶端連接并創(chuàng)建子線程處理
void start()
{
// 設(shè)置信號(hào)處理,忽略SIGCHLD信號(hào)以自動(dòng)回收子進(jìn)程(這里是子線程)資源
signal(SIGCHLD, SIG_IGN);
while (true)
{
struct sockaddr_in src; // 客戶端地址結(jié)構(gòu)體
socklen_t len = sizeof src; // 結(jié)構(gòu)體大小
int servicesock = accept(listensock, (struct sockaddr *)&src, &len); // 接受客戶端連接請求
if (servicesock < 0)
{
logMessage(ERROR, "accept error,%d:%s", errno, strerror(errno));
continue;
}
uint16_t client_port = ntohs(src.sin_port); // 獲取客戶端端口(網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)為主機(jī)字節(jié)序)
std::string client_ip = inet_ntoa(src.sin_addr); // 獲取客戶端IP地址
logMessage(NORMAL, "link success servicesock: %d | %s : %d |\n", \
servicesock, client_ip.c_str(), client_port);
// 多線程處理客戶端連接
ThreadData *td = new ThreadData();
td->_sock = servicesock;
td->_ip = client_ip;
td->_port = client_port;
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, td); // 創(chuàng)建新線程處理客戶端連接
// 注意:不應(yīng)該在這里關(guān)閉servicesock,否則新創(chuàng)建的線程將無法繼續(xù)通過該套接字與客戶端通信
}
}
// 析構(gòu)函數(shù)
~TcpServer()
{
// 可在此處關(guān)閉監(jiān)聽套接字(如果需要的話),但通常在程序退出前由操作系統(tǒng)自動(dòng)關(guān)閉所有打開的文件描述符
}
private:
uint16_t _port; // 服務(wù)器監(jiān)聽端口
std::string _ip; // 服務(wù)器綁定IP地址(可選)
int listensock; // 服務(wù)器監(jiān)聽套接字
};
七、線程池版本
?tcp_server.hpp
代碼實(shí)現(xiàn)了一個(gè)可以并發(fā)處理多個(gè)客戶端連接的TCP服務(wù)器,通過線程池調(diào)度不同的客戶端連接任務(wù),每個(gè)任務(wù)都在獨(dú)立的線程中執(zhí)行service
函數(shù)以處理客戶端的數(shù)據(jù)傳輸。服務(wù)器啟動(dòng)后,將在指定的端口上監(jiān)聽客戶端連接,并在接收到連接請求時(shí)創(chuàng)建新的線程處理連接,從而實(shí)現(xiàn)高效、并發(fā)的通信服務(wù)。?
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"
// 服務(wù)處理函數(shù),負(fù)責(zé)處理客戶端連接并回顯數(shù)據(jù)
static void service(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
char buffer[1024];
while (true)
{
// 讀取客戶端發(fā)送的數(shù)據(jù)
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
// 結(jié)束字符串
buffer[s] = 0;
// 打印客戶端信息及接收到的數(shù)據(jù)
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if (s == 0) // 對端關(guān)閉連接
{
logMessage(NORMAL, "%s:%d shutdown, closing connection...", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "Read socket error, %d:%s", errno, strerror(errno));
break;
}
// 將接收到的數(shù)據(jù)回寫給客戶端
write(sock, buffer, strlen(buffer));
}
// 關(guān)閉已斷開的客戶端連接
close(sock);
}
// TCP服務(wù)器類
class TcpServer
{
private:
static const int gbacklog = 20; // 用于listen的連接請求隊(duì)列長度
public:
// 構(gòu)造函數(shù),初始化服務(wù)器監(jiān)聽端口與IP地址
TcpServer(uint16_t port, std::string ip = "0.0.0.0")
: _listensock(-1), _port(port), _ip(ip),
_threadpool_ptr(ThreadPool<Task>::getThreadPool())
{
}
// 初始化服務(wù)器:創(chuàng)建socket,綁定地址,并開始監(jiān)聽
void initServer()
{
// 1. 創(chuàng)建socket
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "Create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
// 2. 綁定socket到指定IP地址和端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "Bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 開始監(jiān)聽連接請求
if (listen(_listensock, gbacklog) < 0)
{
logMessage(FATAL, "Listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "Init server success");
}
// 啟動(dòng)服務(wù)器主循環(huán),接受新連接并將它們分配給線程池
void start()
{
// 忽略子進(jìn)程結(jié)束時(shí)產(chǎn)生的SIGCHLD信號(hào)(避免產(chǎn)生僵尸進(jìn)程)
// signal(SIGCHLD, SIG_IGN);
// 啟動(dòng)線程池
_threadpool_ptr->run();
// 主循環(huán)等待并處理新連接
while (true)
{
// 4. 接受新的客戶端連接請求
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "Accept error, %d:%s", errno, strerror(errno));
continue;
}
// 獲取客戶端信息
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "Link success, servicesock: %d | %s : %d |\n", servicesock, client_ip.c_str(), client_port);
// 創(chuàng)建任務(wù)對象并將客戶端連接放入線程池執(zhí)行服務(wù)
Task t(servicesock, client_ip, client_port, service);
_threadpool_ptr->pushTask(t);
}
}
// 析構(gòu)函數(shù)
~TcpServer() {}
private:
uint16_t _port; // 監(jiān)聽端口號(hào)
std::string _ip; // 監(jiān)聽IP地址
int _listensock; // 服務(wù)器監(jiān)聽套接字
std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 線程池指針
};
-
服務(wù)處理函數(shù) service():
-
service
函數(shù)接收四個(gè)參數(shù):客戶端套接字描述符(int sock
)、客戶端IP地址(const std::string &clientip
)、客戶端端口號(hào)(const uint16_t &clientport
)和線程名稱(const std::string &thread_name
)。 - 函數(shù)在一個(gè)無限循環(huán)中讀取客戶端發(fā)送的數(shù)據(jù),并將接收到的數(shù)據(jù)回顯給客戶端。
- 當(dāng)讀取到0字節(jié)(
s == 0
)時(shí),表示客戶端已關(guān)閉連接,服務(wù)器也關(guān)閉連接并退出循環(huán)。 - 若讀取過程中發(fā)生錯(cuò)誤,記錄錯(cuò)誤信息并退出循環(huán)。
- 通過
write
函數(shù)將接收到的數(shù)據(jù)回寫給客戶端,確保數(shù)據(jù)雙向傳輸。
-
-
TCP服務(wù)器類 TcpServer:
- 類內(nèi)定義了服務(wù)器監(jiān)聽套接字描述符
_listensock
、監(jiān)聽的端口號(hào)_port
、監(jiān)聽的IP地址_ip
和一個(gè)線程池_threadpool_ptr
。 - 構(gòu)造函數(shù)初始化服務(wù)器對象,接收端口號(hào)和可選的IP地址。
-
initServer
方法負(fù)責(zé)初始化服務(wù)器,包括創(chuàng)建套接字、綁定地址和開始監(jiān)聽連接請求。 -
start
方法啟動(dòng)服務(wù)器主循環(huán),首先啟動(dòng)線程池,然后不斷地等待并處理新的客戶端連接請求。當(dāng)有新的連接請求時(shí),通過accept
函數(shù)接收連接,并創(chuàng)建一個(gè)Task
對象,將客戶端連接信息和service
函數(shù)封裝進(jìn)去,然后將任務(wù)推送到線程池中執(zhí)行。 - 析構(gòu)函數(shù)確保在服務(wù)器實(shí)例銷毀時(shí)關(guān)閉監(jiān)聽套接字。
- 類內(nèi)定義了服務(wù)器監(jiān)聽套接字描述符
運(yùn)行結(jié)果:
ThreadPool代碼
lockGuard.hpp
#pragma once
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t *mtx) : pmtx_(mtx){};
void lock() { pthread_mutex_lock(pmtx_); }
void unlock() { pthread_mutex_unlock(pmtx_); }
~Mutex() {}
private:
pthread_mutex_t *pmtx_;
};
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx) : mtx_(mtx)
{
mtx_.lock();
}
~lockGuard()
{
mtx_.unlock();
}
private:
Mutex mtx_;
};
log.hpp
#pragma once
#include <cstring>
#include <iostream>
#include <cstdio>
#include <ctime>
#include <cstdarg>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"};
// #define LOGFILE "./threadpool.log"
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if (level == DEBUG)
return;
#endif
char stdBuffer[1024];
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%d]", gLevelMap[level], timestamp);
char logBuffer[1024];
va_list args;
va_start(args, format);
vsnprintf(logBuffer, sizeof(logBuffer), format, args);
va_end(args);
// FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
typedef void *(*fun_t)(void *);
class ThreadData
{
public:
void *args_;
std::string name_;
};
class Thread
{
public:
Thread(int num, fun_t callback, void *args)
: func_(callback)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
name_ = nameBuffer;
tdata_.args_ = args;
tdata_.name_ = name_;
}
void start()
{
pthread_create(&tid_, nullptr, func_, (void *)&tdata_);
}
void join()
{
pthread_join(tid_, nullptr);
}
std::string name()
{
return name_;
}
~Thread()
{
}
private:
std::string name_;
fun_t func_;
ThreadData tdata_;
pthread_t tid_;
};
threadPool.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"
// 定義默認(rèn)線程數(shù)量
const int g_thread_num = 5;
// 類模板ThreadPool,代表一個(gè)線程池,可以處理不同類型的任務(wù)(T)
template <class T>
class ThreadPool
{
public:
// 獲取線程池內(nèi)部使用的互斥鎖
pthread_mutex_t *getMutex()
{
return &lock;
}
// 判斷任務(wù)隊(duì)列是否為空
bool isEmpty()
{
return task_queue_.empty();
}
// 線程等待條件變量
void waitCond()
{
pthread_cond_wait(&cond, &lock);
}
// 從任務(wù)隊(duì)列中取出并移除一個(gè)任務(wù)
T getTask()
{
T t = task_queue_.front();
task_queue_.pop();
return t;
}
private:
// ThreadPool構(gòu)造函數(shù),初始化線程池,創(chuàng)建指定數(shù)量的工作線程
ThreadPool(int thread_num = g_thread_num) : num_(thread_num)
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
for (int i = 1; i <= num_; i++)
{
threads_.push_back(new Thread(i, &ThreadPool::routine, this));
}
}
// 刪除拷貝構(gòu)造函數(shù)和賦值操作符,避免線程池實(shí)例的拷貝
ThreadPool(const ThreadPool<T> &other) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;
public:
// 獲取線程池的單例實(shí)例
static ThreadPool<T> *getThreadPool(int num = g_thread_num)
{
// 使用雙重檢查鎖定模式確保線程安全地初始化單例
if (nullptr == thread_ptr)
{
// 加鎖
lockGuard lockguard(&mutex);
// 如果在加鎖后仍然沒有初始化,則創(chuàng)建一個(gè)新的線程池實(shí)例
if (nullptr == thread_ptr)
{
thread_ptr = new ThreadPool<T>(num);
}
// 不需要顯式解鎖,因?yàn)閘ockGuard會(huì)在作用域結(jié)束時(shí)自動(dòng)解鎖
}
return thread_ptr;
}
// 啟動(dòng)線程池中的所有工作線程
void run()
{
for (auto &iter : threads_)
{
iter->start();
// 記錄線程啟動(dòng)成功的日志消息
logMessage(NORMAL, "%s %s", iter->name().c_str(), "啟動(dòng)成功");
}
}
// 靜態(tài)方法,作為工作線程的執(zhí)行入口
static void *routine(void *args)
{
// 解封裝傳入的參數(shù)
ThreadData *td = (ThreadData *)args;
ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;
// 工作線程循環(huán)執(zhí)行,直到收到終止信號(hào)
while (true)
{
T task;
// 上鎖,同步訪問任務(wù)隊(duì)列
{
lockGuard lockguard(tp->getMutex());
// 等待非空任務(wù)到來
while (tp->isEmpty())
tp->waitCond();
// 從任務(wù)隊(duì)列中取出一個(gè)任務(wù)
task = tp->getTask();
}
// 執(zhí)行任務(wù)
task(td->name_);
// 這里假設(shè)任務(wù)完成后會(huì)自動(dòng)重置循環(huán)條件,否則需要顯式判斷是否退出循環(huán)
}
}
// 將新任務(wù)推送到線程池的任務(wù)隊(duì)列中
void pushTask(const T &task)
{
// 加鎖,同步訪問任務(wù)隊(duì)列
lockGuard lockguard(&lock);
// 將任務(wù)放入隊(duì)列,并通知條件變量,有一個(gè)新的任務(wù)可被處理
task_queue_.push(task);
pthread_cond_signal(&cond);
}
// 線程池析構(gòu)函數(shù),清理所有線程資源
~ThreadPool()
{
// 確保所有工作線程完成其任務(wù)后再銷毀
for (auto &iter : threads_)
{
iter->join();
delete iter;
}
// 銷毀互斥鎖和條件變量
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
private:
// 存儲(chǔ)工作線程實(shí)例的容器
std::vector<Thread *> threads_;
// 工作線程的數(shù)量
int num_;
// 任務(wù)隊(duì)列,用于存放待執(zhí)行的任務(wù)
std::queue<T> task_queue_;
// 單例實(shí)例指針
static ThreadPool<T> *thread_ptr;
// 用于保護(hù)線程池單例初始化的全局互斥鎖
static pthread_mutex_t mutex;
// 用于控制線程同步的互斥鎖
pthread_mutex_t lock;
// 條件變量,用于實(shí)現(xiàn)線程間的通信,如通知工作線程有新任務(wù)到來
pthread_cond_t cond;
};
// 初始化靜態(tài)成員變量
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;
八、實(shí)現(xiàn)回顯、字符轉(zhuǎn)換、在線字典查詢服務(wù)
tcp_server.hpp
?三個(gè)服務(wù)函數(shù)
// tcp_server.hpp
#pragma once
// 引入必要的頭文件,包括C++標(biāo)準(zhǔn)庫和POSIX網(wǎng)絡(luò)編程相關(guān)的頭文件
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
// 引入自定義的日志模塊和線程池模塊
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"
// 定義三個(gè)靜態(tài)服務(wù)函數(shù),分別實(shí)現(xiàn)不同的客戶端請求處理邏輯
// service函數(shù):實(shí)現(xiàn)回顯服務(wù),從客戶端接收數(shù)據(jù)并在控制臺(tái)打印,同時(shí)將接收到的數(shù)據(jù)原樣返回給客戶端
static void service(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
char buffer[1024];
while (true)
{
// 從客戶端讀取數(shù)據(jù)
ssize_t bytesReceived = read(sock, buffer, sizeof(buffer) - 1);
if (bytesReceived > 0)
{
// 結(jié)束字符串,便于打印
buffer[bytesReceived] = 0;
// 打印客戶端信息和接收到的消息
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
// 將接收到的消息原樣寫回給客戶端
write(sock, buffer, bytesReceived);
}
else if (bytesReceived == 0) // 對端關(guān)閉連接
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else // 讀取數(shù)據(jù)出錯(cuò)
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
}
// 關(guān)閉與客戶端的連接
close(sock);
}
// change函數(shù):實(shí)現(xiàn)字符轉(zhuǎn)換服務(wù),將客戶端發(fā)送的小寫字母轉(zhuǎn)換為大寫后返回
static void change(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
// 轉(zhuǎn)換輸入字符串中小寫字母為大寫
std::string convertedMessage;
for (char *c = buffer; *c; ++c)
convertedMessage.push_back(islower(*c) ? toupper(*c) : *c);
// 將轉(zhuǎn)換后的消息寫回給客戶端
write(sock, convertedMessage.c_str(), convertedMessage.size());
}
else if (s == 0)
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
// 關(guān)閉與客戶端的連接
close(sock);
}
// dictOnline函數(shù):實(shí)現(xiàn)在線字典查詢服務(wù),根據(jù)客戶端發(fā)送的單詞查詢預(yù)定義字典并返回結(jié)果
static void dictOnline(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
char buffer[1024];
static std::unordered_map<std::string, std::string> dictionary = {
{"producer", "生產(chǎn)者"},
{"consumer", "消費(fèi)者"},
{"udp", "用戶數(shù)據(jù)報(bào)協(xié)議"},
{"tcp", "傳輸控制協(xié)議"},
{"http", "超文本傳輸協(xié)議"}
};
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
// 查找字典中是否存在該單詞及其對應(yīng)含義
std::string response;
auto it = dictionary.find(buffer);
if (it == dictionary.end())
response = "我不知道...";
else
response = it->second;
// 將查詢結(jié)果寫回給客戶端
write(sock, response.c_str(), response.size());
}
else if (s == 0)
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
// 關(guān)閉與客戶端的連接
close(sock);
}
-
service函數(shù):
-
service
函數(shù)負(fù)責(zé)實(shí)現(xiàn)最基礎(chǔ)的回顯服務(wù),即服務(wù)器接收到客戶端發(fā)送的數(shù)據(jù)后,原樣返回給客戶端。 - 首先,函數(shù)通過
read
系統(tǒng)調(diào)用從給定的套接字sock
中讀取客戶端發(fā)送的數(shù)據(jù),存儲(chǔ)在緩沖區(qū)buffer
中。 - 當(dāng)讀取到有效數(shù)據(jù)時(shí)(
read
返回值大于0),將在控制臺(tái)上打印客戶端的IP地址、端口號(hào)以及發(fā)送的消息,并將接收到的消息原樣通過write
系統(tǒng)調(diào)用返回給客戶端。 - 若
read
返回值為0,表示客戶端已經(jīng)關(guān)閉連接,服務(wù)端也會(huì)相應(yīng)地關(guān)閉連接。 - 若出現(xiàn)讀取錯(cuò)誤(
read
返回負(fù)數(shù)),函數(shù)將記錄錯(cuò)誤日志,并關(guān)閉連接。
-
-
change函數(shù):
-
change
函數(shù)實(shí)現(xiàn)了一個(gè)簡單的字符轉(zhuǎn)換服務(wù),將客戶端發(fā)送的所有小寫字母轉(zhuǎn)換成大寫字母后再發(fā)送回去。 - 讀取客戶端數(shù)據(jù)的過程與
service
函數(shù)相同,但在讀取之后,函數(shù)遍歷接收到的字符,利用islower
和toupper
函數(shù)將小寫字母轉(zhuǎn)換為大寫字母,然后構(gòu)建一個(gè)新的字符串convertedMessage
。 - 最后,將轉(zhuǎn)換后的大寫字符串發(fā)送回給客戶端。
-
-
dictOnline函數(shù):
-
dictOnline
函數(shù)實(shí)現(xiàn)了在線字典查詢服務(wù),允許客戶端發(fā)送一個(gè)單詞請求,服務(wù)器在其內(nèi)部維護(hù)的一個(gè)預(yù)定義字典(這里是通過std::unordered_map
實(shí)現(xiàn))中查找該單詞的含義。 - 類似地,先通過
read
讀取客戶端發(fā)送的單詞。 - 查找字典中是否存在該單詞,若存在,則將對應(yīng)的含義發(fā)送回給客戶端;若不存在,則返回一個(gè)默認(rèn)提示信息。
- 注意這里字典是靜態(tài)局部變量,因此在整個(gè)函數(shù)生命周期內(nèi)只初始化一次,提高了效率。
-
TcpServer類
class TcpServer
{
private:
// 設(shè)置服務(wù)器可掛起的最大連接數(shù)
const static int gbacklog = 20;
public:
// 構(gòu)造函數(shù),初始化服務(wù)器監(jiān)聽端口和IP地址,默認(rèn)監(jiān)聽所有網(wǎng)絡(luò)接口(0.0.0.0)
TcpServer(uint16_t port, std::string ip = "0.0.0.0")
: _listensock(-1), _port(port),
_ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
{
}
// 初始化服務(wù)器,包括創(chuàng)建socket、綁定端口/IP、設(shè)置監(jiān)聽
void initServer()
{
// 1. 創(chuàng)建socket,AF_INET代表IPv4,SOCK_STREAM代表TCP協(xié)議
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "Failed to create socket, error: %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "Created socket successfully, listensock: %d", _listensock);
// 2. 綁定socket到指定IP地址和端口
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "Failed to bind socket, error: %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 設(shè)置監(jiān)聽,允許最多gbacklog個(gè)連接排隊(duì)
if (listen(_listensock, gbacklog) < 0)
{
logMessage(FATAL, "Failed to listen on socket, error: %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "Initialized server successfully");
}
// 啟動(dòng)服務(wù)器并開始接受客戶端連接
void start()
{
// 啟動(dòng)線程池
_threadpool_ptr->run();
while (true)
{
// 4. 等待并接受來自客戶端的連接請求
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "Failed to accept connection, error: %d:%s", errno, strerror(errno));
continue;
}
// 獲取已連接客戶端的IP地址和端口
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "Accepted connection, servicesock: %d | %s : %d |\n", servicesock, client_ip.c_str(), client_port);
// 根據(jù)需求選擇不同的服務(wù)函數(shù),并將其封裝為Task對象,推送到線程池中處理
Task t(servicesock, client_ip, client_port, dictOnline);
_threadpool_ptr->pushTask(t);
}
}
// 析構(gòu)函數(shù),確保資源正確釋放
~TcpServer() {}
private:
// 服務(wù)器監(jiān)聽的端口號(hào)
uint16_t _port;
// 服務(wù)器監(jiān)聽的IP地址
std::string _ip;
// 服務(wù)器監(jiān)聽用的套接字描述符
int _listensock;
// 線程池實(shí)例,用于并發(fā)處理客戶端連接請求
std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};
tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(port));
svr->initServer();
svr->start();
return 0;
}
tcp_client.cc
tcp_client.cc
?是一個(gè)簡單的TCP客戶端程序,它通過命令行參數(shù)獲取服務(wù)器的IP地址和端口號(hào),然后嘗試與服務(wù)器建立連接,并進(jìn)行交互。
// tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定義一個(gè)幫助函數(shù),用于輸出程序的使用說明
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
// 主函數(shù),接收命令行參數(shù):服務(wù)器IP地址和端口號(hào)
int main(int argc, char *argv[])
{
// 檢查命令行參數(shù)數(shù)量是否正確(應(yīng)為3個(gè),包括程序名本身)
if (argc != 3)
{
// 如果參數(shù)數(shù)量不正確,則輸出使用說明并退出程序
usage(argv[0]);
exit(1);
}
// 獲取命令行參數(shù)中的服務(wù)器IP地址和端口號(hào)
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 客戶端狀態(tài)標(biāo)志,標(biāo)識(shí)當(dāng)前客戶端是否已連接至服務(wù)器
bool alive = false;
// 客戶端套接字描述符
int sock = 0;
// 用于暫存用戶輸入的行數(shù)據(jù)
std::string line;
// 主循環(huán),持續(xù)監(jiān)聽用戶輸入并與其進(jìn)行交互
while (true)
{
// 如果當(dāng)前沒有與服務(wù)器建立連接,則嘗試創(chuàng)建并建立連接
if (!alive)
{
// 創(chuàng)建一個(gè)AF_INET協(xié)議族下的SOCK_STREAM類型套接字(即TCP套接字)
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 客戶端無需bind到本地地址,操作系統(tǒng)會(huì)自動(dòng)為其分配一個(gè)可用的源端口
// 準(zhǔn)備服務(wù)器的地址結(jié)構(gòu)體
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
// 設(shè)置地址族為IPv4,端口號(hào)轉(zhuǎn)換為主機(jī)字節(jié)序
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
// 將服務(wù)器IP地址轉(zhuǎn)換為二進(jìn)制格式
server.sin_addr.s_addr = inet_addr(serverip.c_str());
// 嘗試連接到服務(wù)器
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
// 輸出連接成功的提示
std::cout << "connect success" << std::endl;
// 設(shè)置alive標(biāo)志為真,表明已連接至服務(wù)器
alive = true;
}
// 提示用戶輸入,并讀取一行
std::cout << "請輸入# ";
std::getline(std::cin, line);
// 如果用戶輸入"quit",則跳出循環(huán),結(jié)束客戶端程序
if (line == "quit")
break;
// 將用戶輸入的數(shù)據(jù)發(fā)送給服務(wù)器
ssize_t s = send(sock, line.c_str(), line.size(), 0);
if (s > 0)
{
// 接收服務(wù)器的回應(yīng)數(shù)據(jù)
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
// 如果接收到數(shù)據(jù)長度大于0,則輸出服務(wù)器的回顯內(nèi)容
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回顯# " << buffer << std::endl;
}
// 若接收的數(shù)據(jù)長度為0,通常意味著服務(wù)器關(guān)閉了連接,此時(shí)客戶端也需要關(guān)閉連接并重置alive標(biāo)志
else if (s == 0)
{
alive = false;
close(sock);
}
}
// 發(fā)送數(shù)據(jù)失敗時(shí),同樣關(guān)閉連接并重置alive標(biāo)志
else
{
alive = false;
close(sock);
}
}
// 關(guān)閉套接字并退出程序
return 0;
}
-
main函數(shù)入口:
- 檢查命令行參數(shù)的數(shù)量是否為3,如果不是則輸出幫助信息并退出程序。
- 解析服務(wù)器的IP地址和端口號(hào)。
- 進(jìn)入一個(gè)無限循環(huán),持續(xù)嘗試或保持與服務(wù)器的連接。
-
建立TCP連接:
- 如果當(dāng)前沒有活躍的連接(
alive
為false),則創(chuàng)建一個(gè)TCP套接字(socket
),使用AF_INET
表示IPv4協(xié)議,SOCK_STREAM
表示使用TCP協(xié)議。 - 不需要客戶端顯示地
bind
本地端口,操作系統(tǒng)會(huì)自動(dòng)分配一個(gè)可用端口。 - 填充
sockaddr_in
結(jié)構(gòu)體,設(shè)置服務(wù)器的IP地址和端口號(hào)。 - 使用
connect
函數(shù)嘗試連接服務(wù)器,如果連接成功,則設(shè)置alive
為true,并輸出“connect success”。
- 如果當(dāng)前沒有活躍的連接(
-
用戶交互:
- 在循環(huán)中,提示用戶輸入消息,并通過
getline
讀取一行文本。 - 如果用戶輸入的是"quit",跳出循環(huán),結(jié)束程序。
- 發(fā)送用戶輸入的消息到服務(wù)器,使用
send
函數(shù)。 - 接收服務(wù)器的回復(fù),使用
recv
函數(shù),并將接收到的數(shù)據(jù)打印出來作為服務(wù)器的回顯。
- 在循環(huán)中,提示用戶輸入消息,并通過
-
錯(cuò)誤處理與重連機(jī)制:若
send
或recv
過程中發(fā)生錯(cuò)誤,將alive
設(shè)置為false,關(guān)閉套接字(close(sock)
),進(jìn)入下一輪循環(huán)重新嘗試連接服務(wù)器。
通過這個(gè)TCP客戶端程序,用戶可以向指定服務(wù)器發(fā)送消息并接收服務(wù)器的回復(fù),直到用戶選擇退出程序。在每次交互中,客戶端都會(huì)檢查網(wǎng)絡(luò)連接的狀態(tài),確保在連接斷開時(shí)能夠嘗試重新連接。
九、TCP協(xié)議通訊流程
1、服務(wù)器初始化
- 調(diào)用socket, 創(chuàng)建文件描述符;
- 調(diào)用bind, 將當(dāng)前的文件描述符和ip/port綁定在一起; 如果這個(gè)端口已經(jīng)被其他進(jìn)程占用了, 就會(huì)bind失敗;
- 調(diào)用listen, 聲明當(dāng)前這個(gè)文件描述符作為一個(gè)服務(wù)器的文件描述符, 為后面的accept做好準(zhǔn)備;
- 調(diào)用accecpt, 并阻塞, 等待客戶端連接過來;
2、建立連接的過程(三次握手)
-
創(chuàng)建套接字: 客戶端同樣通過
socket()
系統(tǒng)調(diào)用創(chuàng)建一個(gè)新的套接字,生成用于與服務(wù)器通信的文件描述符。 -
發(fā)起連接請求: 調(diào)用
connect()
函數(shù),向服務(wù)器的IP地址和端口發(fā)起連接請求。此操作會(huì)引發(fā)TCP的“三次握手”過程: -
第一次握手: 客戶端發(fā)送一個(gè)帶有SYN(同步序列編號(hào))標(biāo)志的TCP報(bào)文段,該報(bào)文段包含一個(gè)隨機(jī)生成的初始序列號(hào)(ISN)。此時(shí),客戶端進(jìn)入SYN_SENT狀態(tài),等待服務(wù)器的確認(rèn)。
-
第二次握手: 服務(wù)器接收到客戶端的SYN報(bào)文段后,回應(yīng)一個(gè)SYN-ACK(同步確認(rèn))報(bào)文段。該報(bào)文段不僅確認(rèn)了客戶端的SYN(設(shè)置ACK標(biāo)志),還包含了服務(wù)器自己的初始序列號(hào)。服務(wù)器進(jìn)入SYN_RCVD狀態(tài)。文章來源:http://www.zghlxwxcb.cn/news/detail-859476.html
-
第三次握手: 客戶端收到服務(wù)器的SYN-ACK報(bào)文段后,發(fā)送一個(gè)ACK(確認(rèn))報(bào)文段,確認(rèn)服務(wù)器的SYN(設(shè)置ACK標(biāo)志并使用服務(wù)器的ISN+1作為確認(rèn)序列號(hào))。至此,客戶端和服務(wù)器雙方均確認(rèn)了對方的初始序列號(hào),連接建立成功,客戶端進(jìn)入ESTABLISHED狀態(tài),服務(wù)器也從SYN_RCVD狀態(tài)切換到ESTABLISHED狀態(tài)。文章來源地址http://www.zghlxwxcb.cn/news/detail-859476.html
connect()
函數(shù)在客戶端一側(cè)會(huì)阻塞,直到三次握手完成或發(fā)生錯(cuò)誤。而在服務(wù)器一側(cè),通常通過
accept()
函數(shù)阻塞等待客戶端的連接請求,并在接收到有效連接請求后返回一個(gè)新的已連接套接字供后續(xù)通信使用。
3、數(shù)據(jù)傳輸?shù)倪^程
- 在數(shù)據(jù)傳輸過程中,TCP(Transmission Control Protocol)協(xié)議作為互聯(lián)網(wǎng)層與應(yīng)用層之間的關(guān)鍵橋梁,為網(wǎng)絡(luò)通信提供了可靠、有序且面向連接的全雙工服務(wù)。全雙工模式意味著,在同一條TCP連接上,通信雙方能夠在同一時(shí)刻獨(dú)立地進(jìn)行數(shù)據(jù)的發(fā)送與接收,猶如兩條并行的高速公路,使得信息能夠在雙向通道中同時(shí)流動(dòng),顯著提升了通信效率。
- 當(dāng)服務(wù)器通過
accept()
系統(tǒng)調(diào)用成功接納一個(gè)客戶端的連接請求后,一個(gè)新的TCP連接便正式建立起來。此時(shí),服務(wù)器進(jìn)入待命狀態(tài),立即調(diào)用read()
函數(shù)嘗試從該連接的socket中讀取數(shù)據(jù)。這個(gè)過程就如同守候在一個(gè)信息管道入口,若此時(shí)客戶端尚未發(fā)送任何數(shù)據(jù),服務(wù)器端的read()
函數(shù)會(huì)暫時(shí)陷入阻塞狀態(tài),耐心等待數(shù)據(jù)流的到來。 - 與此同時(shí),客戶端在連接建立后,開始執(zhí)行其業(yè)務(wù)邏輯,調(diào)用
write()
函數(shù)向服務(wù)器發(fā)送請求。這些請求數(shù)據(jù)沿著已建立的TCP連接,如同信使般穿越網(wǎng)絡(luò),準(zhǔn)確無誤地送達(dá)服務(wù)器端。服務(wù)器的read()
函數(shù)感知到數(shù)據(jù)到達(dá),立即解除阻塞狀態(tài),從socket中取出客戶端的請求進(jìn)行處理。 - 在服務(wù)器專心處理客戶端請求的同時(shí),客戶端并不閑著,它調(diào)用
read()
函數(shù)進(jìn)入阻塞狀態(tài),靜候服務(wù)器對請求的響應(yīng)。這種同步阻塞模式確保了客戶端能夠及時(shí)接收到服務(wù)器端的反饋,保持通信的連貫性。 - 服務(wù)器完成請求處理后,通過
write()
函數(shù)將處理結(jié)果打包成數(shù)據(jù)包,沿原路返回給客戶端。如同投遞員將信件放入郵筒,這些結(jié)果數(shù)據(jù)被安全、高效地傳遞至客戶端的socket。發(fā)送完畢后,服務(wù)器再次調(diào)用read()
函數(shù),重新進(jìn)入阻塞等待狀態(tài),準(zhǔn)備接收客戶端可能發(fā)出的下一條請求。
4、斷開連接的過程(四次揮手)
- 當(dāng)客戶端完成所有業(yè)務(wù)交互,不再有新的請求需要發(fā)送時(shí),它會(huì)選擇主動(dòng)發(fā)起連接的關(guān)閉流程,即所謂的“斷開連接”??蛻舳送ㄟ^調(diào)用
close()
函數(shù),向服務(wù)器發(fā)送一個(gè)特殊的TCP控制報(bào)文——FIN(Finish,結(jié)束)標(biāo)志位被置為1的報(bào)文段。這標(biāo)志著客戶端已經(jīng)沒有數(shù)據(jù)要發(fā)送,期望結(jié)束該連接。這是斷開連接過程中的第一次“揮手”。 - 服務(wù)器端在接收到客戶端發(fā)送的FIN報(bào)文后,立即做出響應(yīng)。它會(huì)向客戶端發(fā)送一個(gè)ACK(Acknowledgment,確認(rèn))報(bào)文段,確認(rèn)序號(hào)為收到的FIN報(bào)文的序號(hào)加1,表明已正確接收并理解了客戶端的斷開意圖。與此同時(shí),服務(wù)器端的
read()
函數(shù)會(huì)返回值0,這是一個(gè)重要信號(hào),提示應(yīng)用程序客戶端已關(guān)閉寫入,即不會(huì)再有新的數(shù)據(jù)到來。這是斷開連接過程中的第二次“揮手”。 - 服務(wù)器端應(yīng)用程序在得知客戶端關(guān)閉連接后,通常會(huì)進(jìn)行必要的清理工作,如釋放資源、更新狀態(tài)等,然后調(diào)用自身的
close()
函數(shù),向客戶端發(fā)送一個(gè)FIN報(bào)文段,明確告知其服務(wù)器也已完成數(shù)據(jù)發(fā)送,希望關(guān)閉連接。這是斷開連接過程中的第三次“揮手”。 - 最后,客戶端收到服務(wù)器發(fā)送的FIN報(bào)文后,同樣以一個(gè)ACK報(bào)文段作為回應(yīng),確認(rèn)序號(hào)為收到的FIN報(bào)文的序號(hào)加1,表示已知悉服務(wù)器關(guān)閉連接的信息。至此,雙方均確認(rèn)對方已無數(shù)據(jù)待發(fā)送,且都同意關(guān)閉連接,斷開連接的四次“揮手”過程宣告完成。這就是網(wǎng)絡(luò)通信中著名的“四次揮手”(Four-way Handshake)機(jī)制,它確保了TCP連接能夠有序、可靠地關(guān)閉,避免了數(shù)據(jù)丟失或混亂,保障了網(wǎng)絡(luò)環(huán)境的穩(wěn)定性和效率。
到了這里,關(guān)于Linux 基于 TCP 協(xié)議的簡單服務(wù)器-客戶端應(yīng)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!