目录
[1. 未处理阻塞和非阻塞模式](#1. 未处理阻塞和非阻塞模式)
[2. 单线程处理限制](#2. 单线程处理限制)
[1. 资源自动回收](#1. 资源自动回收)
[2. 避免死锁风险](#2. 避免死锁风险)
[3. 适用于独立运行的任务线程](#3. 适用于独立运行的任务线程)
引言
我们已经讲过udp-server的3种基础业务模式了,我们现在来讲讲tcp-server的模式。我们还是跟之前一样,先讲echo-server,也就是基础的打印网络传输的数据,先见见tcp通信的基础结构,再在后面加上其他业务,只不过这回我们就直接加上多线程的版本。
代码部分
网络地址封装类-InetAddr.hpp
cpp#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // 封装网络地址类 class InetAddr { private: void ToHost(const struct sockaddr_in &addr) { _port = ntohs(addr.sin_port); //_ip = inet_ntoa(addr.sin_addr); char ip_buf[32]; ::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf)); _ip = ip_buf; } public: InetAddr(const struct sockaddr_in &addr) : _addr(addr) { ToHost(addr); // 将addr进行转换 } std::string AddrStr() { return _ip + ":" + std::to_string(_port); } InetAddr() { } bool operator==(const InetAddr &addr) { return (this->_ip == addr._ip && this->_port == addr._port); } std::string Ip() { return _ip; } uint16_t Port() { return _port; } struct sockaddr_in Addr() { return _addr; } ~InetAddr() { } private: std::string _ip; uint16_t _port; struct sockaddr_in _addr; };
这个网络地址封装类还是跟之前一样的,这里我们就不说明了。
日志类-Log.hpp
cpp#pragma once #include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <ctime> #include <stdarg.h> #include <fstream> #include <string.h> #include <pthread.h> namespace log_ns { enum { DEBUG = 1, INFO, WARNING, ERROR, FATAL }; std::string LevelToString(int level) { switch (level) { case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return "UNKNOW"; } } std::string GetCurrTime() { time_t now = time(nullptr); struct tm *curr_time = localtime(&now); char buffer[128]; snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d", curr_time->tm_year + 1900, curr_time->tm_mon + 1, curr_time->tm_mday, curr_time->tm_hour, curr_time->tm_min, curr_time->tm_sec); return buffer; } class logmessage { public: std::string _level; pid_t _id; std::string _filename; int _filenumber; std::string _curr_time; std::string _message_info; }; #define SCREEN_TYPE 1 #define FILE_TYPE 2 const std::string glogfile = "./log.txt"; pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; class Log { public: Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE) { } void Enable(int type) { _type = type; } void FlushLogToScreen(const logmessage &lg) { printf("[%s][%d][%s][%d][%s] %s", lg._level.c_str(), lg._id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str()); } void FlushLogToFile(const logmessage &lg) { std::ofstream out(_logfile, std::ios::app); if (!out.is_open()) return; char logtxt[2048]; snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s", lg._level.c_str(), lg._id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str()); out.write(logtxt, strlen(logtxt)); out.close(); } void FlushLog(const logmessage &lg) { pthread_mutex_lock(&glock); switch (_type) { case SCREEN_TYPE: FlushLogToScreen(lg); break; case FILE_TYPE: FlushLogToFile(lg); break; } pthread_mutex_unlock(&glock); } void logMessage(std::string filename, int filenumber, int level, const char *format, ...) { logmessage lg; lg._level = LevelToString(level); lg._id = getpid(); lg._filename = filename; lg._filenumber = filenumber; lg._curr_time = GetCurrTime(); va_list ap; va_start(ap, format); char log_info[1024]; vsnprintf(log_info, sizeof(log_info), format, ap); va_end(ap); lg._message_info = log_info; // 打印出日志 FlushLog(lg); } ~Log() { } private: int _type; std::string _logfile; }; Log lg; #define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0) #define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0) #define EnableFile() do {lg.Enable(FILE_TYPE);}while(0) }
日志类这里我们跟之前也是一样的。
线程封装类-Thread.hpp
cpp#pragma once #include <iostream> #include <unistd.h> #include <string> #include <functional> #include <pthread.h> namespace ThreadMudle { using func_t = std::function<void(const std::string &)>; class Thread { public: void Excute() { _isrunning = true; _func(_name); _isrunning = false; } public: Thread(const std::string &name, func_t func) : _name(name), _func(func) { } static void *ThreadRoutine(void *args) // 新线程都会执行该方法 { Thread *self = static_cast<Thread *>(args); // 获得了当前对象 self->Excute(); // 调用回调函数func的方法 return nullptr; } bool Start() // 启动线程 { //::强调此调用为系统调用 int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); if (n != 0) return false; // 创建失败返回false return true; } std::string Status() // 获取当前状态 { if (_isrunning) return "running"; else return "sleep"; } void Stop() // 中止线程 { if (_isrunning) { ::pthread_cancel(_tid); _isrunning = false; } } void Join() // 等待回收线程 { ::pthread_join(_tid, nullptr); } std::string Name() // 返回线程的名字 { return _name; } ~Thread() { } private: std::string _name; // 线程名 pthread_t _tid; // 线程id bool _isrunning; // 线程是否在运行 func_t _func; // 线程要执行的回调函数 }; }
线程封装类用的也是跟我们udp线程池版一样的。
线程池-ThreadPool.hpp
cpp#pragma once #include <iostream> #include <unistd.h> #include <string> #include <unistd.h> #include <vector> #include <functional> #include <queue> #include <pthread.h> #include "Thread.hpp" #include "Log.hpp" using namespace log_ns; using namespace ThreadMudle; // 开放封装好的线程的命名空间 static const int gdefaultnum = 5; // 线程池的个数 template <typename T> class ThreadPool { private: void LockQueue() { pthread_mutex_lock(&_mutex); } void UnlockQueue() { pthread_mutex_unlock(&_mutex); } void Wakeup() { pthread_cond_signal(&_cond); } void WakeupAll() { pthread_cond_broadcast(&_cond); } void Sleep() { pthread_cond_wait(&_cond, &_mutex); } bool IsEmpty() { return _task_queue.empty(); } // 处理任务 void HandlerTask(const std::string &name) // this { while (true) { // 取任务 LockQueue(); // 给任务队列上锁 while (IsEmpty() && _isrunning) // 如果这个线程还在运行任务且任务队列为空,就让线程去休息 { _sleep_thread_num++; LOG(INFO, "%s thread sleep begin!\n", name.c_str()); Sleep(); LOG(INFO, "%s thread wakeup!\n", name.c_str()); _sleep_thread_num--; } // 判定一种情况 if (IsEmpty() && !_isrunning) // 如果任务为空且线程不处于运行状态就可以让这个线程退出了 { UnlockQueue(); LOG(INFO, "%s thread quit\n", name.c_str()); break; } // 有任务 T t = _task_queue.front(); _task_queue.pop(); UnlockQueue(); // 处理任务 t(); // 处理任务,此处不用/不能再临界区中处理 // std::cout << name << ": " << t.result() << std::endl; // LOG(DEBUG, "hander task done, task is : %s\n", t.result().c_str()); } } void Init() // 创建线程 { func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1); for (int i = 0; i < _thread_num; i++) { std::string threadname = "thread-" + std::to_string(i + 1); _threads.emplace_back(threadname, func); LOG(DEBUG, "construct thread obj %s done, init sucess\n", threadname.c_str()); } } void Start() // 复用封装好的线程类里面的Start方法 { _isrunning = true; for (auto &thread : _threads) { LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str()); thread.Start(); } } ThreadPool(int thread_num = gdefaultnum) : _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0) { // 创建锁和条件变量 pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); } ThreadPool(const ThreadPool<T> &t) = delete; void operator=(const ThreadPool<T> &t) = delete; public: void Stop() // 停止执行任务 { LockQueue(); _isrunning = false; WakeupAll(); UnlockQueue(); LOG(INFO, "thread Pool Stop Success!\n"); } static ThreadPool<T> *GetInstance() { if (_tp == nullptr) { pthread_mutex_lock(&_sig_mutex); if (_tp == nullptr) { LOG(INFO, "creat threadpool\n"); _tp = new ThreadPool<T>(); _tp->Init(); _tp->Start(); } else { LOG(INFO, "get threadpool\n"); } pthread_mutex_unlock(&_sig_mutex); } return _tp; } void Equeue(const T &in) // 生产任务 { LockQueue(); if (_isrunning) { _task_queue.push(in); if (_sleep_thread_num > 0) { Wakeup(); // 唤醒之前Sleep的线程 } } UnlockQueue(); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } private: int _thread_num; // 线程个数 std::vector<Thread> _threads; // 用顺序表来存储线程 std::queue<T> _task_queue; // 用队列来存储任务数据 bool _isrunning; // 线程的运行状态 int _sleep_thread_num; // 没有执行任务的线程数量 pthread_mutex_t _mutex; // 锁 pthread_cond_t _cond; // 条件变量 // 单例模式 static ThreadPool<T> *_tp; static pthread_mutex_t _sig_mutex; }; template <typename T> ThreadPool<T> *ThreadPool<T>::_tp = nullptr; template <typename T> pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
线程池也是一样,以上这四个类我们都是复用udp线程池版本的代码,我这里就不多说明了,因为在udp聊天室的文章里我已经讲得很清楚了。
接下来我们来看看新的封装类。
锁的封装类-LockGuard.hpp
cpp#pragma once #include <pthread.h> class LockGuard { public: LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) { pthread_mutex_lock(_mutex); } ~LockGuard() { pthread_mutex_unlock(_mutex); } private: pthread_mutex_t *_mutex; };
锁的封装类比较简单,我们就只是对加锁和解锁操作进行了封装,且我们只需要构造函数和析构函数这两个函数就可以解决,这样做的好处是当我们要对某个功能进行锁的操作时,我们不需要在其生命周期解锁,因为当其生命周期结束时,它自己会调用析构函数进行解锁,大大帮助我们降低了因操作不当而造成死锁的局面。
Tcp服务端源文件-TcpServerMain.cc
cpp#include "TcpServer.hpp" #include <memory> // ./tcpserver 8888 int main(int argc, char *argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " local-port" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port); tsvr->InitServer(); tsvr->Loop(); return 0; }
在源文件这一块,我们的tcp与udp的编写逻辑别无二致,都是先获取命令行参数,且同样只需要端口号,不需要ip,验证通过之后,我们再创建服务端类的指针对象,然后调用创建服务接口和启动服务接口就可以了。
Tcp服务端头文件
cpp#pragma once #include <iostream> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring> #include <sys/wait.h> #include <pthread.h> #include "Log.hpp" #include "InetAddr.hpp" #include "ThreadPool.hpp" using namespace log_ns; enum { SOCKET_ERROR = 1, BIND_ERROR, LISTEN_ERROR }; const static int gport = 8888; const static int gsock = -1; const static int gbacklog = 8; using task_t = std::function<void()>; class TcpServer { public: TcpServer(uint16_t port = gport) : _port(port), _listensockfd(gsock), _isrunning(false) { } void InitServer() { // 1.创建socket _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_listensockfd < 0) { LOG(FATAL, "listensockfd create error\n"); exit(SOCKET_ERROR); } LOG(INFO, "listensockfd create success, fd: %d\n", _listensockfd); struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 2.bind _listensockfd 和 Socket addr if (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0) { LOG(FATAL, "bind error\n"); exit(BIND_ERROR); } LOG(INFO, "bind success\n"); // 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接 if (::listen(_listensockfd, gbacklog) < 0) { LOG(FATAL, "listen error\n"); exit(LISTEN_ERROR); } LOG(INFO, "listen success\n"); } class ThreadData { public: int _sockfd; TcpServer *_self; InetAddr _addr; public: ThreadData(int sockfd, TcpServer *self, const InetAddr &addr) : _sockfd(sockfd), _self(self), _addr(addr) { } }; void Loop() { _isrunning = true; while (_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 4. 获取连接 int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len); if (sockfd < 0) { LOG(WARNING, "accept error\n"); continue; } InetAddr addr(client); LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd); // version 0 --- 不稳定版本 // Service(sockfd, addr); // // version 1 ---多进程版本 // pid_t id = fork(); // if (id == 0) // { // // child // ::close(_listensockfd); // 建议 // if (fork() > 0) // exit(0); // Service(sockfd, addr); // exit(0); // } // // father // ::close(sockfd); // int n = waitpid(id, nullptr, 0); // if (n > 0) // { // LOG(INFO, "wait child success.\n"); // } // // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了 // pthread_t tid; // ThreadData *td = new ThreadData(sockfd, this, addr); // pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离 // version 3 --- 线程池版本 int sockfd,InetAddr addr task_t t = std::bind(&TcpServer::Service, this, sockfd, addr); ThreadPool<task_t>::GetInstance()->Equeue(t); } _isrunning = false; } static void *Execute(void *args) { pthread_detach(pthread_self()); ThreadData *td = static_cast<ThreadData *>(args); td->_self->Service(td->_sockfd, td->_addr); delete td; return nullptr; } void Service(int sockfd, InetAddr addr) { // 长服务 while (true) { char inbuffer[1024]; // 当作字符串 ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1); if (n > 0) { inbuffer[n] = 0; LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer); std::string echo_string = "[server echo]# "; echo_string += inbuffer; write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0) { LOG(INFO, "client %s quit\n", addr.AddrStr().c_str()); break; } else { LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str()); break; } } ::close(sockfd); } ~TcpServer() { } private: uint16_t _port; int _listensockfd; bool _isrunning; };
我们依旧需要打开我们的日志命名空间,并且我们还需要自定义错误码。三个错误码的意思依次代表socket套接字创建失败,绑定失败和监听失败。
我们再来看看我们的成员变量是怎么设计的,在成员变量这一点上,我们跟udp的设计思路基本上是差不多的,第一个是我们的端口号,第二个是我们的socket套接字,第三个是我们的服务运行状态。
我们需要给这三个值设置一个初始值,端口号我们一般都是设置成8080,然后我们的socket套接字默认设置为-1,方便我们后续的一个判断,gbacklog是监听队列的最大长度 (backlog 参数),用于指定操作系统为该套接字**(
_listensockfd
)**维护的未完成连接请求队列的上限。具体说明:
作用 :当服务器调用
listen()
后,套接字进入监听状态,开始接收客户端的连接请求。客户端发起的连接不会立即建立,而是先进入一个「未完成连接队列」(处于 TCP 三次握手过程中)。gbacklog
就是这个队列的最大容量。含义:
- 如果队列已满,新的连接请求会被操作系统拒绝(客户端可能收到
ECONNREFUSED
错误)。- 其值通常根据应用场景设置(如 5、10、100 等),具体上限可能受操作系统内核参数限制(例如 Linux 中默认可能为 128)。
task_t就是我们的服务包装器对象的类型了。
构造函数部分,我们需要做的就是给三个变量进行赋值,首先是我们的端口号,用我们自定义的全局端口号变量8080作为我们要初始化的数据,然后是我们的socket套接字和运行状态,这三个成员变量初始化完成,我们的构造函数的任务也就完成了。
cppvoid InitServer() { // 1.创建socket _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_listensockfd < 0) { LOG(FATAL, "listensockfd create error\n"); exit(SOCKET_ERROR); } LOG(INFO, "listensockfd create success, fd: %d\n", _listensockfd); struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 2.bind _listensockfd 和 Socket addr if (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0) { LOG(FATAL, "bind error\n"); exit(BIND_ERROR); } LOG(INFO, "bind success\n"); // 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接 if (::listen(_listensockfd, gbacklog) < 0) { LOG(FATAL, "listen error\n"); exit(LISTEN_ERROR); } LOG(INFO, "listen success\n"); }
接下来就是我们服务器的构建工作,我们服务器的构建要完成的任务是什么呢?我这里就不卖关子了。第一步,我们需要创建监听的socket套接字,第二步我们需要绑定监听的socket套接字,第三步就是开始监听了。
我们创建监听套接字的一个与udp不同的就是在第二个参数,我们的udp用的是基于数据报的传输,而我们的tcp用的是基于字节流的传输,所以我们的第二个参数是SOCK_STREAM。
然后我们绑定的socket和本地网络地址信息的方式和udp是一模一样的,这里我就不赘述了。
tcp相比于udp的连接过程来说,会多几步,在代码上的体现就是多了三步,第一步是监听,第二步是创建普通的socket套接字,第三步是accept连接。我们的listen监听就是在这里进行的。tcp为什么需要一个连接呢?这是因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接,而listen系统调用的核心功能是将一个套接字从 "主动连接" 状态转换为 "被动监听" 状态 ,使其能够接收客户端的连接请求。调用
listen
前,_listensockfd
是一个通过socket()
创建的 "未连接" 套接字(仅分配了资源,未指定角色)。调用listen
后,该套接字被标记为监听套接字(listening socket) ,专门用于接收客户端的connect()
请求(仅适用于 TCP 协议,UDP 无需监听)。这是我们封装的一个线程数据类,它的作用就是帮助我们更好的管理我们所关心的几个变量,比如sockfd,实体类对象以及网络地址。
cppvoid Loop() { _isrunning = true; while (_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 4. 获取连接 int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len); if (sockfd < 0) { LOG(WARNING, "accept error\n"); continue; } InetAddr addr(client); LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd); // version 0 --- 不稳定版本 // Service(sockfd, addr); // // version 1 ---多进程版本 // pid_t id = fork(); // if (id == 0) // { // // child // ::close(_listensockfd); // 建议 // if (fork() > 0) // exit(0); // Service(sockfd, addr); // exit(0); // } // // father // ::close(sockfd); // int n = waitpid(id, nullptr, 0); // if (n > 0) // { // LOG(INFO, "wait child success.\n"); // } // // version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了 // pthread_t tid; // ThreadData *td = new ThreadData(sockfd, this, addr); // pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离 // version 3 --- 线程池版本 int sockfd,InetAddr addr task_t t = std::bind(&TcpServer::Service, this, sockfd, addr); ThreadPool<task_t>::GetInstance()->Equeue(t); } _isrunning = false; }
接下来就是我们一直执行服务的内容了。我们先将服务状态设置为运行,然后就可以accept接收要进行通信的客户端了,并创建对应进行通信的socket套接字,接下来就是对数据的处理,但是我们发现我们有四个版本,第一个版本是不稳定的版本:
这段代码是一个简单的网络服务函数,用于处理客户端连接并进行消息回显。不稳定体现在下面这几个方面:
1. 未处理阻塞和非阻塞模式
- 阻塞模式 :
read
和write
在默认的阻塞模式下,如果客户端长时间不发送数据,read
调用会一直阻塞,导致整个服务线程被挂起,无法及时处理其他客户端连接(如果是单线程服务架构)。- 非阻塞模式 :如果套接字被设置为非阻塞模式,
read
可能会在没有数据可读时立即返回错误(如EAGAIN
或EWOULDBLOCK
),但当前代码没有对这种情况进行正确处理,可能导致服务逻辑混乱。2. 单线程处理限制
当前代码在一个无限循环中处理单个客户端连接,如果有大量客户端同时请求连接,单线程的处理方式会导致后面的连接请求长时间等待,无法及时响应,降低了服务的并发处理能力,从用户角度看服务表现不稳定。
接下来我们再来看看第二个版本:
cpp// version 1 ---多进程版本 pid_t id = fork(); if (id == 0) { // child ::close(_listensockfd); // 建议 if (fork() > 0) exit(0); Service(sockfd, addr); exit(0); } // father ::close(sockfd); int n = waitpid(id, nullptr, 0); if (n > 0) { LOG(INFO, "wait child success.\n"); }
第二个是多进程的版本,我们将数据的处理交给子进程去完成,我们的子进程得关闭监听套接字,因为子进程只需要处理数据,监听是父进程的任务,所以相对应的,我们的父进程得关闭通信的socket套接字,并且等待子进程的结束并回收它。
第三个是多线程的版本:
cpp// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了 pthread_t tid; ThreadData *td = new ThreadData(sockfd, this, addr); pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离
多线程的版本我们就用到了我们封装的数据类型对象了,将新线程交给线程函数去处理数据
我们用pthread_detach函数将线程进行分离操作,然后处理数据,至于为什么要进行线程分离呢?
在 POSIX 线程编程中,
pthread_detach(pthread_self());
语句的作用是将调用该函数的线程(通过pthread_self()
获取当前线程标识符 )设置为分离状态。它主要有以下几个方面的作用:1. 资源自动回收
- 常规线程回收 :默认情况下,线程创建后处于可结合(joinable)状态,当一个可结合状态的线程结束运行后,它的线程资源(比如线程栈等)并不会立即被系统释放,而是会一直保留,直到有其他线程调用
pthread_join
函数来获取该线程的退出状态,并完成资源回收。这就要求必须有对应的代码逻辑来处理线程回收,否则会造成资源泄漏。- 分离线程回收 :而被分离(detached)的线程,在线程执行结束后,其相关的系统资源会由系统自动释放,无需其他线程显式调用
pthread_join
来回收资源,简化了线程资源管理的流程。2. 避免死锁风险
- 可结合线程的潜在问题 :如果存在多个线程,并且有线程等待其他可结合线程结束(通过
pthread_join
),但由于某些异常情况(比如等待线程自身提前退出、逻辑错误导致没有执行pthread_join
等 ),使得可结合线程一直无法被正确回收,就可能导致死锁或者资源占用问题。- 分离线程的优势:将线程设置为分离状态后,不需要其他线程来等待它的结束,也就不存在因为等待回收而引发的死锁风险,增强了程序的稳定性和健壮性。
3. 适用于独立运行的任务线程
- 场景说明:有些线程执行的是一些独立的、不需要和主线程或者其他线程进行同步交互、也不需要返回特定结果的任务,例如后台的日志记录线程、定期的系统状态监控线程等。
- 设置分离的好处:对于这类线程,将其设置为分离状态是很合适的。它们在完成自己的任务后,资源能自动释放,不会干扰其他线程的执行,也不需要额外的同步和回收操作,提高了程序的运行效率和简洁性。
最后一个版本就是我们的线程池版本:
线程池就轻松了,直接将任务打包好交给我们的线程池就可以了。
Tcp客户端源文件-TcpClientMain.cc
cpp#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring> // ./tcpclient server-ip server-port int main(int argc, char *argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl; exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 1. 创建socket int sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { std::cerr << "create socket error" << std::endl; exit(1); } // 注意:不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,os会自动bind sockfd,用自己的IP和随机端口号 // 什么时候进行自动bind?connect 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); int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server)); if (n < 0) { std::cerr << "connect sockfd error" << std::endl; exit(2); } while (true) { std::string message; std::cout << "Enter # "; std::getline(std::cin, message); write(sockfd, message.c_str(), message.size()); char echo_buffer[1024]; n = read(sockfd, echo_buffer, sizeof(echo_buffer)); if (n > 0) { echo_buffer[n] = 0; std::cout << echo_buffer << std::endl; } else { break; } } ::close(sockfd); return 0; }
tcp客户端的逻辑跟udp的差不多,前面的命令参数的处理,以及socket套接字的创建,再到后面的IPV4的结构体对象的创建,这些流程跟udp是一模一样的,只不过需要对目标地址进行连接以及对传输数据的方式进行了改变,udp是send发送数据,recv接收数据,而tcp是write和read。主要原因如下做参考:
数据传输特性差异
- UDP :UDP 是无连接的协议,以数据报(datagram)为单位进行传输 ,数据报之间相互独立,发送的数据边界在接收端能够被保留。
send
系列函数(如sendto
、send
等 )设计上更契合 UDP 这种无连接、面向数据报的特性。例如sendto
函数,它允许在发送数据时明确指定目标地址,方便 UDP 向不同的目标发送独立的数据报。在接收端,recvfrom
函数可以获取到发送方的地址信息,以便于进行响应,这种机制和 UDP 数据报独立传输、不依赖连接的特点相匹配。- TCP :TCP 是面向连接的协议,提供字节流服务,数据被看作是无边界的字节序列。
write
和read
函数原本是系统 I/O 操作函数,用于对文件、套接字等描述符进行读写。由于 TCP 字节流没有明确的消息边界,使用write
写入数据就如同向文件中写入字节序列,read
读取数据也类似从文件中按顺序读取字节,更符合 TCP 字节流连续传输的特性,能很好地适应 TCP 这种将数据当作连续字节流处理的方式。数据可靠性保证机制不同
- UDP :UDP 本身不保证数据的可靠传输,没有内置的确认、重传等机制。
send
和recv
系列函数的设计相对简单直接,不会过多干预数据传输的可靠性处理。发送方调用send
函数将数据报发出后,不会等待对方的确认;接收方调用recv
函数接收数据报,若数据报丢失也不会触发自动重传等操作,应用程序需要自己处理丢包、重复等情况,这与 UDP 轻量级、低开销的设计理念相符。- TCP :TCP 有复杂的可靠性保证机制,如确认应答、超时重传、滑动窗口等。
write
函数只是将数据放入内核的发送缓冲区,由 TCP 协议栈按照自身机制来确保数据可靠发送到对端,read
函数从内核接收缓冲区读取数据,TCP 协议栈会负责处理数据的乱序、重复等问题,保证应用程序读取到的是有序、完整的数据,这与 TCP 为应用层提供可靠数据传输服务的目标是一致的。历史和设计习惯因素
- UDP :早期网络编程中,为了突出 UDP 无连接、简单快捷传输数据报的特点,专门设计了与之匹配的
send
和recv
相关函数,随着网络编程的发展和普及,这种函数接口使用习惯得以保留和延续。- TCP :因为 TCP 套接字本质上也是一种文件描述符(在 Unix 及类 Unix 系统中,一切皆文件的理念下),而
write
和read
是系统中通用的针对文件描述符的 I/O 操作函数,在处理 TCP 套接字数据传输时自然沿用了这两个函数,后来在跨平台等场景下,这种使用方式也被广泛接受。
Socket网络编程(1)——Echo Server
板鸭〈小号〉2025-10-07 23:49
相关推荐
明天会有多晴朗2 小时前
C语言入门教程(第1讲):最通俗的C语言常见概念详解与实战讲解爱上妖精的尾巴2 小时前
5-20 WPS JS宏 every与some数组的[与或]迭代(数组的逻辑判断)gopher95112 小时前
Go 语言的 panic 和 recover豆沙沙包?2 小时前
2025年--Lc165--H637.二叉树的层平均值(二叉树的层序遍历)--Java版24zhgjx-fuhao3 小时前
基于时间的ACL小蒜学长3 小时前
springboot二手儿童绘本交易系统设计与实现(代码+数据库+LW)李小白663 小时前
Python文件操作xqlily3 小时前
Go语言:高效简洁的现代编程语言数据知道4 小时前
Go语言:数据压缩与解压详解