引言
在前一篇文章中,我們詳細(xì)介紹了UDP協(xié)議和TCP協(xié)議的特點(diǎn)以及它們之間的異同點(diǎn)。本文將延續(xù)上文內(nèi)容,重點(diǎn)討論簡單的TCP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn)。通過本文的學(xué)習(xí),讀者將能夠深入了解TCP協(xié)議的實(shí)際應(yīng)用,并掌握如何編寫簡單的TCP網(wǎng)絡(luò)程序。讓我們一起深入探討TCP網(wǎng)絡(luò)程序的實(shí)現(xiàn)細(xì)節(jié),為網(wǎng)絡(luò)編程的學(xué)習(xí)之旅添上一份精彩的實(shí)踐經(jīng)驗(yàn)。
一、TCP協(xié)議
TCP(Transmission Control Protocol)是一種面向連接的通信協(xié)議,它要求在數(shù)據(jù)傳輸前先建立連接,以確保數(shù)據(jù)的可靠傳輸。TCP通過序號、確認(rèn)和重傳等機(jī)制來保證數(shù)據(jù)的完整性和可靠性,同時還實(shí)現(xiàn)了擁塞控制和流量控制,以適應(yīng)不同網(wǎng)絡(luò)環(huán)境下的數(shù)據(jù)傳輸需求。由于TCP的可靠性和穩(wěn)定性,它被廣泛應(yīng)用于網(wǎng)絡(luò)通信中,包括網(wǎng)頁瀏覽、文件傳輸、電子郵件等各種應(yīng)用場景,成為互聯(lián)網(wǎng)協(xié)議套件中的重要組成部分。詳介紹可以看上一篇文章:UDP協(xié)議介紹 | TCP協(xié)議介紹 | UDP 和 TCP 的異同
二、TCP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn)
接下來,我們打算運(yùn)用線程池技術(shù),模擬實(shí)現(xiàn)一個簡單的TCP網(wǎng)絡(luò)程序。通過充分利用線程池,我們能夠更有效地管理并發(fā)連接,從而提高程序的性能和穩(wěn)定性。這一實(shí)踐將有助于加深我們對網(wǎng)絡(luò)編程關(guān)鍵概念和技術(shù)的理解和掌握。在前文中已經(jīng)提到了線程池,這里就不再贅述其原理和作用。詳細(xì)可以點(diǎn)擊傳送門:?? 線程池
1. 預(yù)備代碼
?ThreadPool.hpp(線程池)
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
// 線程信息結(jié)構(gòu)體
struct ThreadInfo
{
pthread_t tid; // 線程ID
std::string name; // 線程名稱
};
static const int defalutnum = 10; // 默認(rèn)線程池大小為10
template <class T>
class ThreadPool
{
public:
void Lock() // 加鎖
{
pthread_mutex_lock(&mutex_);
}
void Unlock() // 解鎖
{
pthread_mutex_unlock(&mutex_);
}
void Wakeup() // 喚醒等待中的線程
{
pthread_cond_signal(&cond_);
}
void ThreadSleep() // 線程休眠
{
pthread_cond_wait(&cond_, &mutex_);
}
bool IsQueueEmpty() // 判斷任務(wù)隊(duì)列是否為空
{
return tasks_.empty();
}
std::string GetThreadName(pthread_t tid) // 獲取線程名稱
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
static void *HandlerTask(void *args) // 線程任務(wù)處理函數(shù)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty()) // 若任務(wù)隊(duì)列為空,則線程等待
{
tp->ThreadSleep();
}
T t = tp->Pop(); // 從任務(wù)隊(duì)列中取出任務(wù)
tp->Unlock();
t(); // 執(zhí)行任務(wù)
}
}
void Start() // 啟動線程池
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); // 創(chuàng)建線程
}
}
T Pop() // 從任務(wù)隊(duì)列中取出任務(wù)
{
T t = tasks_.front();
tasks_.pop();
return t;
}
void Push(const T &t) // 將任務(wù)推入任務(wù)隊(duì)列
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
static ThreadPool<T> *GetInstance() // 獲取線程池實(shí)例
{
if (nullptr == tp_) // 若線程池實(shí)例為空
{
pthread_mutex_lock(&lock_);
if (nullptr == tp_) // 雙重檢查鎖
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>(); // 創(chuàng)建線程池實(shí)例
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
ThreadPool(int num = defalutnum) : threads_(num) // 構(gòu)造函數(shù)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadPool() // 析構(gòu)函數(shù)
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
ThreadPool(const ThreadPool<T> &) = delete; // 禁用拷貝構(gòu)造函數(shù)
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 禁用賦值操作符,避免 a=b=c 的寫法
private:
std::vector<ThreadInfo> threads_; // 線程信息數(shù)組
std::queue<T> tasks_; // 任務(wù)隊(duì)列
pthread_mutex_t mutex_; // 互斥鎖
pthread_cond_t cond_; // 條件變量
static ThreadPool<T> *tp_; // 線程池實(shí)例指針
static pthread_mutex_t lock_; // 靜態(tài)互斥鎖
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr; // 初始化線程池實(shí)例指針為nullptr
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; // 初始化靜態(tài)互斥鎖
以上代碼實(shí)現(xiàn)了一個簡單的線程池模板類 ThreadPool
,其中包含了線程池的基本功能和操作。
-
首先定義了一個線程信息結(jié)構(gòu)體
ThreadInfo
,用來保存線程的ID和名稱。 -
然后定義了一個模板類
ThreadPool
,其中包含了線程池的各種操作和屬性:-
Lock()
和Unlock()
分別用于加鎖和解鎖。 -
Wakeup()
用于喚醒等待中的線程。 -
ThreadSleep()
用于使線程進(jìn)入休眠狀態(tài)。 -
IsQueueEmpty()
判斷任務(wù)隊(duì)列是否為空。 -
GetThreadName()
根據(jù)線程ID獲取線程名稱。
-
-
定義了靜態(tài)成員函數(shù)
HandlerTask
,作為線程的任務(wù)處理函數(shù)。在該函數(shù)中,線程會不斷地從任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行。 -
Start()
函數(shù)用于啟動線程池,創(chuàng)建指定數(shù)量的線程,并將線程的任務(wù)處理函數(shù)設(shè)置為HandlerTask
。 -
Pop()
函數(shù)用于從任務(wù)隊(duì)列中取出任務(wù)。 -
Push()
函數(shù)用于將任務(wù)推入任務(wù)隊(duì)列。 -
GetInstance()
函數(shù)用于獲取線程池的實(shí)例,采用了雙重檢查鎖(Double-Checked Locking)實(shí)現(xiàn)單例模式。 -
線程池的構(gòu)造函數(shù)和析構(gòu)函數(shù)分別用于初始化和銷毀互斥鎖和條件變量。
-
最后使用靜態(tài)成員變量初始化了線程池實(shí)例指針和靜態(tài)互斥鎖。
?makefile文件
.PHONY:all
all:tcpserverd tcpclient
tcpserverd:Main.cc
g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcpserverd tcpclient
這段代碼是一個簡單的 Makefile 文件,用于編譯生成兩個可執(zhí)行文件 tcpserverd
和 tcpclient
。
-
.PHONY: all
:聲明all
為一個偽目標(biāo),表示all
不是一個實(shí)際的文件名,而是一個指定的操作。 -
all: tcpserverd tcpclient
:定義了all
目標(biāo),它依賴于tcpserverd
和tcpclient
目標(biāo)。當(dāng)執(zhí)行make all
時,會先編譯tcpserverd
和tcpclient
。 -
tcpserverd: Main.cc
:定義了生成tcpserverd
可執(zhí)行文件的規(guī)則,依賴于Main.cc
源文件。使用g++
編譯器進(jìn)行編譯,指定輸出文件名為tcpserverd
,使用 C++11 標(biāo)準(zhǔn),并鏈接 pthread 庫。 -
tcpclient: TcpClient.cc
:定義了生成tcpclient
可執(zhí)行文件的規(guī)則,依賴于TcpClient.cc
源文件。同樣使用g++
編譯器進(jìn)行編譯,指定輸出文件名為tcpclient
,使用 C++11 標(biāo)準(zhǔn)。 -
.PHONY: clean
:聲明clean
為一個偽目標(biāo)。 -
clean: rm -f tcpserverd tcpclient
:定義了clean
目標(biāo),用于清理生成的可執(zhí)行文件。執(zhí)行make clean
時將刪除tcpserverd
和tcpclient
可執(zhí)行文件。
?打印日志文件
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默認(rèn)打印方式為屏幕輸出
path = "./log/"; // 默認(rèn)日志文件路徑為當(dāng)前目錄下的"log/"目錄
}
void Enable(int method)
{
printMethod = method; // 設(shè)置打印方式
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl; // 在屏幕上輸出日志信息
break;
case Onefile:
printOneFile(LogFile, logtxt); // 將日志信息寫入單個文件中
break;
case Classfile:
printClassFile(level, logtxt); // 根據(jù)日志級別將日志信息寫入不同的文件中
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname; // 拼接日志文件路徑
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打開或創(chuàng)建一個文件,以追加方式寫入
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size()); // 將日志信息寫入文件
close(fd); // 關(guān)閉文件
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // 生成日志文件名,例如"log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt); // 將日志信息寫入對應(yīng)級別的文件中
}
~Log()
{
}
// 重載()運(yùn)算符,用于輸出日志信息
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t); // 獲取當(dāng)前時間
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 格式化左側(cè)部分,包括日志級別和時間信息
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // 格式化右側(cè)部分,即用戶自定義的日志內(nèi)容
va_end(s);
// 格式:默認(rèn)部分+自定義部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // 拼接左右兩側(cè)的日志內(nèi)容
printLog(level, logtxt); // 打印日志信息
}
private:
int printMethod; // 打印方式
std::string path; // 日志文件路徑
};
這段代碼是一個簡單的日志記錄類 Log
,它提供了幾種不同的日志輸出方式和日志級別。
-
#pragma once
: 使用編譯器指令,確保頭文件只被編譯一次。 -
定義了一些常量:
-
SIZE
: 緩沖區(qū)大小為 1024。 - 日志級別常量:
Info
,Debug
,Warning
,Error
,Fatal
。 - 打印方式常量:
Screen
,Onefile
,Classfile
。 - 日志文件名常量:
LogFile
。
-
-
Log
類包含以下成員函數(shù)和變量:-
printMethod
: 記錄當(dāng)前的打印方式,默認(rèn)為屏幕輸出。 -
path
: 日志文件路徑,默認(rèn)為"./log/"。
-
-
構(gòu)造函數(shù)
Log()
初始化printMethod
和path
。 -
Enable(int method)
: 設(shè)置日志的打印方式。 -
levelToString(int level)
: 將日志級別轉(zhuǎn)換為對應(yīng)的字符串。 -
printLog(int level, const std::string &logtxt)
: 根據(jù)打印方式輸出日志信息。 -
printOneFile(const std::string &logname, const std::string &logtxt)
: 將日志信息寫入單個文件中。 -
printClassFile(int level, const std::string &logtxt)
: 根據(jù)日志級別將日志信息寫入不同的文件中。 -
析構(gòu)函數(shù)
~Log()
。 -
重載的函數(shù)調(diào)用運(yùn)算符
operator()
: 接受日志級別和格式化字符串,格式化輸出日志信息到不同的輸出位置。
?將當(dāng)前進(jìn)程轉(zhuǎn)變?yōu)槭刈o(hù)進(jìn)程
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null"; // 定義空設(shè)備文件路徑
// 將當(dāng)前進(jìn)程變?yōu)槭刈o(hù)進(jìn)程的函數(shù)
void Daemon(const std::string &cwd = "")
{
// 1. 忽略一些異常信號,以避免對守護(hù)進(jìn)程造成影響
signal(SIGCLD, SIG_IGN); // 忽略子進(jìn)程結(jié)束信號
signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信號
signal(SIGSTOP, SIG_IGN); // 忽略終止信號
// 2. 創(chuàng)建一個子進(jìn)程并使父進(jìn)程退出,確保守護(hù)進(jìn)程不是進(jìn)程組組長,創(chuàng)建一個新的會話
if (fork() > 0)
exit(0); // 父進(jìn)程退出
setsid(); // 創(chuàng)建新的會話,并成為該會話的首進(jìn)程
// 3. 更改當(dāng)前調(diào)用進(jìn)程的工作目錄,如果指定了工作目錄則切換到相應(yīng)目錄
if (!cwd.empty())
chdir(cwd.c_str()); // 切換工作目錄到指定路徑
// 4. 將標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯誤重定向至/dev/null,關(guān)閉不需要的文件描述符
int fd = open(nullfile.c_str(), O_RDWR); // 打開空設(shè)備文件
if (fd > 0)
{
dup2(fd, 0); // 標(biāo)準(zhǔn)輸入重定向至空設(shè)備
dup2(fd, 1); // 標(biāo)準(zhǔn)輸出重定向至空設(shè)備
dup2(fd, 2); // 標(biāo)準(zhǔn)錯誤重定向至空設(shè)備
close(fd); // 關(guān)閉打開的文件描述符
}
}
這段代碼實(shí)現(xiàn)了將當(dāng)前進(jìn)程轉(zhuǎn)變?yōu)槭刈o(hù)進(jìn)程的函數(shù) Daemon
。
- 忽略一些異常信號,避免對守護(hù)進(jìn)程產(chǎn)生影響。
- 創(chuàng)建一個子進(jìn)程并使父進(jìn)程退出,確保守護(hù)進(jìn)程不是進(jìn)程組組長,創(chuàng)建一個新的會話。
- 更改當(dāng)前調(diào)用進(jìn)程的工作目錄,如果指定了工作目錄,則切換到相應(yīng)目錄。
- 將標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤重定向至
/dev/null
,即空設(shè)備文件,關(guān)閉不需要的文件描述符,確保守護(hù)進(jìn)程不產(chǎn)生輸出和錯誤信息。
2. TCP 服務(wù)器端實(shí)現(xiàn)(TcpServer.hpp)
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 最大連接請求隊(duì)列長度
extern Log lg;
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer;
// 線程數(shù)據(jù)結(jié)構(gòu),用于傳遞給線程處理函數(shù)
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
{}
public:
int sockfd;
std::string clientip;
uint16_t clientport;
TcpServer *tsvr;
};
// TCP服務(wù)器類
class TcpServer
{
public:
// 構(gòu)造函數(shù),初始化端口和IP地址
TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip)
{
}
// 初始化服務(wù)器
void InitServer()
{
// 創(chuàng)建套接字
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", listensock_);
// 設(shè)置套接字選項(xiàng),允許地址重用
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
// 綁定本地地址
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d");
// 監(jiān)聽套接字,開始接受連接請求
if (listen(listensock_, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d");
}
// 啟動服務(wù)器
void Start()
{
// 將當(dāng)前進(jìn)程變?yōu)槭刈o(hù)進(jìn)程
Daemon();
// 啟動線程池
ThreadPool<Task>::GetInstance()->Start();
lg(Info, "tcpServer is running....");
// 循環(huán)接受客戶端連接并處理
while(true)
{
// 獲取新連接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
// 獲取客戶端IP和端口
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 打印客戶端連接信息
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 創(chuàng)建任務(wù)對象并加入線程池處理
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
// 析構(gòu)函數(shù)
~TcpServer() {}
private:
int listensock_; // 監(jiān)聽套接字
uint16_t port_; // 端口號
std::string ip_; // IP地址
};
這段代碼是一個簡單的TCP服務(wù)器的實(shí)現(xiàn),包括了創(chuàng)建套接字、綁定地址、監(jiān)聽連接、接受客戶端連接等基本操作。
3. TCP 客戶端實(shí)現(xiàn)(main函數(shù))
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
// 檢查命令行參數(shù)是否正確
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 設(shè)置服務(wù)器地址信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int cnt = 5; // 連接重試次數(shù)
int isreconnect = false; // 是否需要重連
int sockfd = 0;
// 創(chuàng)建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
do
{
// 嘗試連接服務(wù)器
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
isreconnect = true;
cnt--;
std::cerr << "connect error..., reconnect: " << cnt << std::endl;
sleep(2); // 等待一段時間后重連
}
else
{
break;
}
} while (cnt && isreconnect);
if (cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
// 與服務(wù)器建立連接后進(jìn)行通信
while (true)
{
std::string message;
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 向服務(wù)器發(fā)送消息
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
std::cerr << "write error..." << std::endl;
}
// 從服務(wù)器接收消息并顯示
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if (n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
}
// 關(guān)閉套接字
close(sockfd);
}
return 0;
}
溫馨提示
感謝您對博主文章的關(guān)注與支持!如果您喜歡這篇文章,可以點(diǎn)贊、評論和分享給您的同學(xué),這將對我提供巨大的鼓勵和支持。另外,我計(jì)劃在未來的更新中持續(xù)探討與本文相關(guān)的內(nèi)容。我會為您帶來更多關(guān)于Linux以及C++編程技術(shù)問題的深入解析、應(yīng)用案例和趣味玩法等。如果感興趣的話可以關(guān)注博主的更新,不要錯過任何精彩內(nèi)容!文章來源:http://www.zghlxwxcb.cn/news/detail-850501.html
再次感謝您的支持和關(guān)注。我們期待與您建立更緊密的互動,共同探索Linux、C++、算法和編程的奧秘。祝您生活愉快,排便順暢!文章來源地址http://www.zghlxwxcb.cn/news/detail-850501.html
到了這里,關(guān)于【探索Linux】P.29(網(wǎng)絡(luò)編程套接字 —— 簡單的TCP網(wǎng)絡(luò)程序模擬實(shí)現(xiàn))的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!