linux网络:TCP套接字基础

我们直接从一个简单的测试入手

一、准备工作

我们先把代码放出来,然后对着代码进行分析

1、用户类

cpp 复制代码
class InetAddr
{
public:
     InetAddr(struct sockaddr_in&addr):_addr(addr)
     {
        //网络转主机
        _port=ntohs(_addr.sin_port);//从网络中拿到网络序列
        char ipbuffer[64];
        inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(_addr));
        _ip=ipbuffer;
     }
     InetAddr(const std::string &ip,uint16_t port):_ip(ip),_port(port)
     {
         //主机转网络
         memset(&_addr,0,sizeof(_addr));
         _addr.sin_family=AF_INET;
         inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
         _addr.sin_port=htons(_port);
     }
     InetAddr(uint16_t port):_port(port),_ip("0")
     {
         //主机转网络
         memset(&_addr,0,sizeof(_addr));
         _addr.sin_family=AF_INET;
         _addr.sin_addr.s_addr=INADDR_ANY;
         _addr.sin_port=htons(_port);
     }
     //返回_addr
     struct sockaddr_in& Netaddr()
     {
        return _addr;
     }
     //返回_addr的地址
     const struct sockaddr *NetAddrPtr()
     {
         return CONV(_addr);
     }
     //返回长度
     socklen_t NetAddrLen()
     {
          return sizeof(_addr);
     }
     bool operator==(InetAddr&peer)
     {
        return _ip==peer._ip&&_port==peer._port;
     }
     std::string Ip()
     {
         return _ip;
     }
     uint16_t Port()
     {
         return _port;
     }
     std::string StringAddr()
     {
        return _ip+":"+std::to_string(_port);
     }
     ~InetAddr(){}
private:
     struct sockaddr_in _addr;
     std::string _ip;
     uint16_t _port;
};

我们先定一个标准,但凡在操作中遇到网络序列转本地序列,或者本地序列转网络序列的,我们尽量用固定的函数:

ntohs:

作用是将16位的网络字节序转化为主机字节序

inet_ntop:

作用是将网络二进制格式的IP地址转化为人类可读的字符串IP

需要注意的是dst是一个输出缓冲区,用来存储点分字符串,而size是_addr的长度

htons:

作用是将主机字节序的16位端口号转化为网络字节序

inet_pton:

作用是将字符串IP转化为二进制格式的IP地址

需要注意src是我们要转化的点分字符串ip,而dst是输出,存放二进制ip的,IPv4需要传入&struct in_addr。

对这个类进行分析:

作用:把原生 C 语言的 struct sockaddr_in 做面向对象封装,统一管理 IP、端口、网络地址结构体,屏蔽字节序转换、IP 字符串 / 二进制转换的重复代码。

第一个构造函数的使用场景:需要查看是谁进入了监听范围,此时需要人类能够读懂的ip和port。

第二个构造函数的使用场景:上传人类可读的二进制ip和16位的端口号,转化为可用于connect和bind的网络地址。

第三个构造函数的使用场景:服务器监听时,需要bind本机所有网卡,只需要传入端口号即可。

Netaddr的作用:返回封装好的原生 sockaddr_in 结构体(网络字节序)

NetAddrPtr的作用:将_addr的地址返回,需要注意的是,这里使用了一个宏,CONV(addr)等同于(struct sockaddr*)&_addr。

StringAddr的作用:返回人类能够看懂的字符串ip和port。

2、日志打印

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略 : 子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << gsep;
        }
        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志的策略 : 子类
    const std::string defaultpath = "./log";
    const std::string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_path))
            {
                return;
            }
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);

            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
            std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }
        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式

    // 1. 形成日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }
    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;
        localtime_r(&curr, &curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }

    // 1. 形成日志 && 2. 根据不同的策略,完成刷新
    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 表示的是未来的一条日志
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTimeStamp()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                // 日志的左边部分,合并起来
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _src_name << "] "
                   << "[" << _line_number << "] "
                   << "- ";
                _loginfo = ss.str();
            }
            // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // a = b = c =d;
                // 日志的右半部分,可变的
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:
            std::string _curr_time;
            LogLevel _level;
            pid_t _pid;
            std::string _src_name;
            int _line_number;
            std::string _loginfo; // 合并之后,一条完整的信息
            Logger &_logger;
        };

        // 这里故意写成返回临时对象
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

    // 全局日志对象
    Logger logger;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__)
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

3、锁

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex &_mutex;
    };
}

4、Common.hpp

cpp 复制代码
#pragma once
#include<iostream>
enum ExitCode
{
    OK=0,
    USAGE_ERR,
    BIND_ERR,
    SOCK_ERR,
    LISTEN_ERR
};


//防止拷贝
class NoCopy
{
public:
     NoCopy(){}
     ~NoCopy(){}
     NoCopy(const NoCopy&)=delete;
     const NoCopy&operator=(const NoCopy&)=delete;
};

#define CONV(addr) ((struct sockaddr*)&addr)

在这里面我们统一了退出码,方便查找错误,分析程序崩溃的原因

禁止拷贝类,是为了让派生类防止拷贝,因为fd属于独占资源,如果允许拷贝对象,那么两个对象持有同一个fd,析构时会先后两次close(fd)

二、服务端测试版本

在开始之前呢,我想先给大家讲一个故事:

有一个好再来鱼庄,鱼庄安排了张三在街上招人,碰到人就说:要不要来我们这里来吃鱼啊,假如说招到了,就安排李四等服务员来接客,而张三再回门口继续吆喝,假如说没有招到,那就找寻下一个目标,其实在这过程当中,好再来鱼庄就是服务端,而马路就是操作系统,张三就是_listensockfd,只负责监听,而accept的返回值,就是为我们提供服务的sockfd。

函数介绍:

accept的作用就是获取顾客,从监听那里获取连接,其实就是服务员从张三那里获得来吃饭的客人。

初始化接口:

cpp 复制代码
void Init()
      {
          //创建套接字
          _listensockfd=socket(AF_INET,SOCK_STREAM,0);//SOCK_STREAM意味着面向字节流
          if(_listensockfd<0)
          {
             LOG(LogLevel::FATAL)<<"socket error";
             exit(SOCK_ERR);
          }
          LOG(LogLevel::INFO)<<"socket success";
          //bind固定端口
          InetAddr local(_port);
          int n=bind(_listensockfd,(const sockaddr*)local.NetAddrPtr(),local.NetAddrLen());
          //bind失败:
          if(n<0)
          {
             LOG(LogLevel::FATAL)<<"bind error";
             exit(BIND_ERR);
          }
          LOG(LogLevel::INFO)<<"bind success:"<<_listensockfd;
          //_listen
          n=listen(_listensockfd,backlog);//backlog代表底层设置的权限队列长度
          if(n<0)
          {
             LOG(LogLevel::FATAL)<<"listen error";
             exit(LISTEN_ERR);
          }
           LOG(LogLevel::INFO)<<"listen success"<<_listensockfd;
      }

首先第一步就是创建套接字,其实就是雇佣张三,第二步就是绑定端口,绑定就是将文件描述符_listensockfd与本地IP+port进行绑定,类似于给张三分配特定的职务,开始监听,其实就相当于拉拢客人。

运行接口:

cpp 复制代码
void Run()
      {
            _isrunning=true;
            while(_isrunning)
            {
                  struct sockaddr_in peer;
                  socklen_t len=sizeof(peer);
                 //获取连接,由_listensockfd来拉拢客人
                 int sockfd=accept(_listensockfd,CONV(peer),&len);
                 if(sockfd<0)
                 {
                     LOG(LogLevel::WARNING)<<"accept error";
                     continue;
                 }
                 InetAddr addr(peer);
                 //网络转主机,打印消息
                 LOG(LogLevel::INFO)<<"accept success,peer addr:"<<addr.StringAddr();
                 Server(sockfd,addr);
            }
            _isrunning=false;
      }

获取拉拢过来的客人的的信息peer,然后获取连接,其实就是服务员从张三那里获得来吃饭的客人,然后获得主机消息,让用户知道是谁来了!!

需要注意的是这里的accept被调用成功之后,操作系统会自动填充好peer中的客户端ip地址和端口,peer其实就相当于一个容器,用来存放客户端的地址的。

服务接口:

cpp 复制代码
 void Server(int sockfd, InetAddr &peer)
      {
            char buffer[1024];
            // TCP的读取工作类似于读文件一样!!
            // 先读取数据
            while (true)
            {
                  ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
                  if (n > 0)
                  {
                        buffer[n] = 0;
                        LOG(LogLevel::INFO) << peer.StringAddr() << "say#" << buffer;
                        // 再写回数据
                        std::string echo_server = "server say#";
                        echo_server += buffer;
                        write(sockfd, echo_server.c_str(), echo_server.size());
                  }
                  else if (n == 0)
                  {
                        LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";
                        close(sockfd);
                        break;
                  }
                  else
                  {
                        LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";
                        close(sockfd);
                        break;
                  }
            }
      }

read:

服务端在运行时首先会读取数据,用read读取,从sockfd(与客户端通信的通道)客户端读取数据,再把数据写回给客户端。

读取数据有三种情况:

n>0:读取成功

n<0:读取失败

n==0:对端把连接关了,相当于服务器读到了文件的结尾 --- 与pipe类似。

我们用telnet来进行测试,telnet单纯充当一个TCP的收发工具,是一个通用的TCP客户端工具。

可以看到测试成功,当我关闭客户端时:

三、单进程正规版本

缺点就是一次只能处理一个请求:

cpp 复制代码
#include<iostream>
#include"Common.hpp"
#include"InetAddr.hpp"
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<memory>
#include<unistd.h>
//tcpclient server_ip server_port
void Usage(const char*process)
{
     std::cout<<"Usage:"<<process<<"ip"<<"port"<<std::endl;
}

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
       Usage(argv[0]);
    }
    //获取服务端的ip和端口号
    std::string serverip=argv[1];
    uint16_t serverport=std::stoi(argv[2]);
    //1、创建套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cerr<<"socket error"<<std::endl;
        exit(SOCK_ERR);
    }
    //套接字创建成功之后就需要发起连接请求了,用connect
    InetAddr peer(serverip,serverport);
    int n=connect(sockfd,CONV(peer),sizeof(peer));
    if(n<0)
    {
        std::cerr<<"connect error"<<std::endl;
        exit(CONNECT_ERR);
    }
    while(true)
    {
        //开始发送消息
        std::string line;
        std::cout<<"Please Enter#";
        std::getline(std::cin,line);
        write(sockfd,line.c_str(),line.size());
        //接受服务端发来的消息,用read去读
        char buffer[1024];
        ssize_t size=read(sockfd,buffer,sizeof(buffer)-1);
        if(size>0)
        {
             buffer[size]=0;
             std::cout<<"server#"<<buffer<<std::endl;
        }
    }
    close(sockfd);
    //2、客户端不需要显示的绑定,会自动选择本地当前的ip和端口号为我们创建客户端,随机方式选择端口号
    return 0;
}

客户端代码

四、多进程正规版本

多进程正规版本就是一次可以处理多个请求

cpp 复制代码
void Run()
      {
            _isrunning = true;
            while (_isrunning)
            {
                  struct sockaddr_in peer;
                  socklen_t len = sizeof(peer);
                  // 获取连接,由_listensockfd来拉拢客人
                  int sockfd = accept(_listensockfd, CONV(peer), &len);
                  if (sockfd < 0)
                  {
                        LOG(LogLevel::WARNING) << "accept error";
                        continue;
                  }
                  InetAddr addr(peer);
                  // 网络转主机,打印消息
                  LOG(LogLevel::INFO) << "accept success,peer addr:" << addr.StringAddr();
                  //多进程版本
                  pid_t id=fork();
                  if(id<0)
                  {
                        LOG(LogLevel::FATAL)<<"fork error";
                        exit(FORK_ERR);
                  }
                  else if(id==0)
                  {
                        //子进程不能看到父进程的listensockfd
                        close(_listensockfd);
                        Server(sockfd,addr);
                        exit(OK);
                  }
                  else
                  {
                        //父进程运行
                        close(sockfd);
                        //等待子进程,要不然就僵尸了
                        pid_t rid=waitpid(id,nullptr,0);
                  }
            }
            _isrunning = false;
      }

我们先有一个雏形,再抛出问题,这里我们创建了多进程,我们不想让子进程看到父进程的_listensockfd,所以我们在子进程中关闭了这个文件描述符,我们不想让父进程看到子进程的sockfd,所以我们在父进程中关闭了这个文件描述符,然后子进程去执行server的任务,而父进程去执行accept获取连接的任务,但是父进程要等待子进程完成之后进行回收啊,那这里不就是阻塞了嘛?阻塞跟单进程有什么区别?最简单的方法就是用signal,让父进程忽略信号,直接并行执行。

直接在初始化时设置忽略信号。

但是这样太简单了,我们竟然要写多进程,那么我们就得把多进程优雅的设计给搞进来!!

来看看改变之后的代码:

我们就加了,为何会让代码有了质的提升呢?我们来好好分析一下!!

我们的子进程在这里其实又创建了一个子进程,为了形象点,我们在这里叫做孙子进程,当fork()>0时,其实就是子进程执行的代码,直接退出了,此时我们的父进程执行完代码后压根就不需要去阻塞等待了,直接回收,而Server的任务交给了我们的孙子进程,但是孙子进程的父进程直接退出了,自己就变成了孤儿进程,孤儿进程是被系统所回收的,所以成功解决了难题!!

客户端和服务端需要一直维持那个链接,当其他人的进程加进来的时候,我们可以写脚本发现,只要是加进来的程序,一定会一直被受理。

可以看到孤儿进程越来越多了。

ctrl+c退出之后,孤儿进程就变少了。

五、多线程版本

我们先回答两个问题!

1、如果进程打开了一个文件,得到了一个fd,这个fd线程能看到吗?

答案:能够看到,一个进程的文件描述符是被所有线程共享的

2、线程敢不敢关闭自己不需要的fd?

答案:肯定不敢

我们来分析一下一些注意事项,因为ThreadData是一个内部类,所以我们可以在Run中直接调用,而我们又把Routine定义在了类中,我们需要把它设置静态的,因为有this指针,不能不传参数,但是呢定义为了静态的之后,就无法调用内部成员函数了,所以我们需要在ThreadData内部定义一个TcpServer的对象,然后在Routine内部创建的进程就可以去调用这个TcpServer对象,然后调用Server执行任务了!!

值得注意的是这里的Server我们不退,那么服务就一直进行,我们称这种服务为长服务,长服务适合多线程和多进程,短服务就是连接完,应答完,立马关掉连接;

六、线程池版本

cpp 复制代码
using namespace LogModule;
using namespace ThreadPoolModule;
using task_t = std::function<void()>;
// 服务器往往是禁止拷贝的,所以我们继承一下NoCopy,想要完成整个服务器的拷贝,就得完成NoCopy的拷贝,所以我们就能禁止服务器拷贝了
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
      TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
      {
      }
      void Init()
      {
            //signal(SIGCHLD,SIG_IGN);//忽略SIG_IGN信号,意味着父进程再也不用等子进程了,非常简单
            // 创建套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM意味着面向字节流
            if (_listensockfd < 0)
            {
                  LOG(LogLevel::FATAL) << "socket error";
                  exit(SOCK_ERR);
            }
            LOG(LogLevel::INFO) << "socket success:"<<_listensockfd;
            // bind固定端口
            InetAddr local(_port);
            int n = bind(_listensockfd, (const sockaddr *)local.NetAddrPtr(), local.NetAddrLen());
            // bind失败:
            if (n < 0)
            {
                  LOG(LogLevel::FATAL) << "bind error";
                  exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success:" << _listensockfd;
            //_listen
            n = listen(_listensockfd, backlog); // backlog代表底层设置的权限队列长度
            if (n < 0)
            {
                  LOG(LogLevel::FATAL) << "listen error";
                  exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success:" << _listensockfd;
      }

      void Server(int sockfd, InetAddr &peer)
      {
            char buffer[1024];
            // TCP的读取工作类似于读文件一样!!
            // 先读取数据
            while (true)
            {
                  ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
                  if (n > 0)
                  {
                        buffer[n] = 0;
                        LOG(LogLevel::INFO) << peer.StringAddr() << " say#" << buffer;
                        // 再写回数据
                        std::string echo_server = "server say#";
                        echo_server += buffer;
                        write(sockfd, echo_server.c_str(), echo_server.size());
                  }
                  else if (n == 0)
                  {
                        LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";
                        close(sockfd);
                        break;
                  }
                  else
                  {
                        LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";
                        close(sockfd);
                        break;
                  }
            }
      }

      static void*Routine(void*args)
      {
           pthread_detach(pthread_self());
           ThreadData*td=static_cast<ThreadData*>(args);
           td->tsvr->Server(td->sf,td->addr);
           delete td;
           return nullptr;
      }
      struct ThreadData
      {
          ThreadData(int sockfd,InetAddr&ar,TcpServer*s):sf(sockfd),addr(ar),tsvr(s)
          {}
          int sf;
          InetAddr addr;
          TcpServer *tsvr;
      };
      void Run()
      {
            _isrunning = true;
            while (_isrunning)
            {
                  struct sockaddr_in peer;
                  socklen_t len = sizeof(peer);
                  // 获取连接,由_listensockfd来拉拢客人
                  int sockfd = accept(_listensockfd, CONV(peer), &len);
                  if (sockfd < 0)
                  {
                        LOG(LogLevel::WARNING) << "accept error";
                        continue;
                  }
                  InetAddr addr(peer);
                  // 网络转主机,打印消息
                  LOG(LogLevel::INFO) << "accept success,peer addr:" << addr.StringAddr();
                  
                //线程池版本,线程池一般比较适合处理短服务
                //将新链接和客户端构建一个新的任务,push到线程池中,让线程池来处理
                ThreadPool<task_t>::GetInstance()->Enqueue([this,sockfd,&addr](){
                  this->Server(sockfd,addr);
                });
            }
            _isrunning = false;
      }
      ~TcpServer()
      {
      }

private:
      uint16_t _port;
      int _listensockfd; // 监听套接字
      bool _isrunning;
};

直接定义了一个任务,是一个function容器,然后将lambda表达式这个处理方法保存到这个容器里,然后当我们执行任务时,直接进行回调,完成这个任务。

大家也可以尝试做一下我们之前的业务版本,比如说翻译啊!

相关推荐
志栋智能1 小时前
安全超自动化:构建全方位安全可观测性的基础
网络·安全·自动化
时空无限2 小时前
ubuntu dpkg -l 输出第一列解释
linux·运维·ubuntu
mqiqe2 小时前
线程调度与 Schedulers:Project Reactor 并发模型的核心引擎
java·开发语言·网络
略略略咯咯3 小时前
网络协议-study
网络·网络协议
DLYSB_3 小时前
从监控告警到现场处置:用 HTTP API、Modbus TCP 构建声光语音告警系统
网络协议·tcp/ip·http·报警灯
爱学习的程序媛4 小时前
通信协议完整梳理
网络·网络协议·信息与通信·通信协议
叮咚侠4 小时前
Ubuntu 24.04 安装zabbix-agent 5.0 服务
linux·ubuntu·zabbix·zabbix-agent
故乡dee云4 小时前
华为云国际服务器连不上怎么办?安全组、端口、IP 和网络排查方法
服务器·网络·华为云
佳児素花痴╮5 小时前
服务器模型、网络调试工具、网络协议头分析
服务器·网络·网络协议