当前位置: 首页 > news >正文

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维护的未完成连接请求队列的上限。

具体说明:

  1. 作用:当服务器调用 listen() 后,套接字进入监听状态,开始接收客户端的连接请求。客户端发起的连接不会立即建立,而是先进入一个「未完成连接队列」(处于 TCP 三次握手过程中)。gbacklog 就是这个队列的最大容量。

  2. 含义

    • 如果队列已满,新的连接请求会被操作系统拒绝(客户端可能收到 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 套接字数据传输时自然沿用了这两个函数,后来在跨平台等场景下,这种使用方式也被广泛接受。
http://www.dtcms.com/a/446709.html

相关文章:

  • 怎么自己做企业网站建网站 京公网安
  • python如何把图片二值化
  • 网站建设课程设计目的和内容泰州专业网站建设公司
  • ClaudeCode真经第五章:最佳实践与高效工作流
  • Python脚本shebang写法推荐
  • 如何使用Python实现本地缓存
  • 建设自己的企业网站需要什么外贸建站哪个好
  • 电视直播网站怎么做wp商城
  • CMakeLists.txt用法备忘
  • 【文献笔记】AAAI 2018 | DGCNN
  • 网站建设费可以计业务费吗电商网站系统
  • vue2.0网站开发广东装修公司排名前十强
  • docker入门(保姆级)
  • 微表单网站大丰有没有做网站
  • 【打造你的全栈 AI 中控台】一文拆解 Open WebUI:从多模型聚合、RAG 引擎到未来 Agent 化的演进密码
  • 网站建设的基本话术天津软件设计公司
  • Maven多模块项目MyMetaObjectHandler自动填充日期未生效
  • 自己做网站教学视频网站为什么要备案
  • 大模型学习周报十六
  • 网站建设时间规划出入东莞最新通知今天
  • 彩票网站建设柏镇江网友之家
  • ESP32-S3入门第九天:摄像头入门与应用
  • 泰宁县建设局网站泰达人才网招聘网
  • 桂林网站推广深圳辰硕网站优化
  • 内网 渗透
  • 企业网站的建立与维护论文做电影网站只放链接算侵权吗
  • 给人做logo的网站教育视频网站开发
  • 长春建设银行网站明星网页设计模板图片
  • Linux 进程通信——匿名管道
  • 微服务项目->在线oj系统(Java-Spring)--C端用户管理