Socket网络编程(1)——Echo Server
目录
引言
代码部分
网络地址封装类-InetAddr.hpp
日志类-Log.hpp
线程封装类-Thread.hpp
线程池-ThreadPool.hpp
锁的封装类-LockGuard.hpp
Tcp服务端源文件-TcpServerMain.cc
Tcp服务端头文件
具体说明:
1. 未处理阻塞和非阻塞模式
2. 单线程处理限制
1. 资源自动回收
2. 避免死锁风险
3. 适用于独立运行的任务线程
Tcp客户端源文件-TcpClientMain.cc
数据传输特性差异
数据可靠性保证机制不同
历史和设计习惯因素
引言
我们已经讲过udp-server的3种基础业务模式了,我们现在来讲讲tcp-server的模式。我们还是跟之前一样,先讲echo-server,也就是基础的打印网络传输的数据,先见见tcp通信的基础结构,再在后面加上其他业务,只不过这回我们就直接加上多线程的版本。
代码部分
网络地址封装类-InetAddr.hpp
#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
#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 2const 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
#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; // 创建失败返回falsereturn true;}std::string Status() // 获取当前状态{if (_isrunning)return "running";elsereturn "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; // 线程idbool _isrunning; // 线程是否在运行func_t _func; // 线程要执行的回调函数}; }
线程封装类用的也是跟我们udp线程池版一样的。
线程池-ThreadPool.hpp
#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
#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
#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服务端头文件
#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 addrif (::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 addrtask_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套接字和运行状态,这三个成员变量初始化完成,我们的构造函数的任务也就完成了。
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 addrif (::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,实体类对象以及网络地址。
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 addrtask_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. 单线程处理限制
当前代码在一个无限循环中处理单个客户端连接,如果有大量客户端同时请求连接,单线程的处理方式会导致后面的连接请求长时间等待,无法及时响应,降低了服务的并发处理能力,从用户角度看服务表现不稳定。
接下来我们再来看看第二个版本:
// 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套接字,并且等待子进程的结束并回收它。
第三个是多线程的版本:
// 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
#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. 创建socketint 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?connectstruct 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 套接字数据传输时自然沿用了这两个函数,后来在跨平台等场景下,这种使用方式也被广泛接受。