muduo网络库的工具类

引言

本篇文章主要关于写网络库的一些工具类,比如地址解析,读写缓存之类的数据处理,这些都非常关键,因为我们需要解决tcp传输层的问题。

InetAddress

这里有一个我们需要学习的地方就是我们为了防止隐式构造,在我们需要传递参数的构造函数里面最好加上explicit,然后就是一些地址端口的转化函数。

cpp 复制代码
    class InetAddress {
    public:
        explicit InetAddress(uint16_t port = 0, std::string ip = "127.0.0.1");
        explicit InetAddress(const sockaddr_in &addr): m_addr(addr) 
        {}
        std::string toIp() const;
        std::string toIpPort() const;
        uint16_t toPort() const;

        const sockaddr_in* getSockAddr() const {return &m_addr;}
        void setSockAddr(const sockaddr_in &addr) {m_addr = addr;}
    private:
        sockaddr_in m_addr;
    };

对于构造函数,我们在里面封装了一个对于地址的组装,有tcp协议,端口号,ip地址。所以当我们要传入一个网络地址的时候就直接调用这个构造函数即可

cpp 复制代码
    InetAddress::InetAddress(uint16_t port, std::string ip)
    {
        bzero(&m_addr, sizeof(m_addr));
        m_addr.sin_family = AF_INET;
        m_addr.sin_port = htons(port);
        m_addr.sin_addr.s_addr = inet_addr(ip.c_str());
    }

这个是将二进制转化成点分十进制,

cpp 复制代码
    std::string InetAddress::toIp() const
    {
        char buf[128] = {0};
        ::inet_ntop(AF_INET, &m_addr.sin_addr.s_addr, buf, sizeof(buf));
        return buf;
    }
    
    std::string InetAddress::toIpPort() const
    {
        char buf[128] = {0};
        ::inet_ntop(AF_INET, &m_addr.sin_addr.s_addr, buf, sizeof(buf));
        size_t end = strlen(buf);
        uint16_t port = ntohs(m_addr.sin_port);
        sprintf(buf + end, ":%u", port);
        return buf;
    }

    uint16_t InetAddress::toPort() const
    {
        return ntohs(m_addr.sin_port);
    }

CurrentThread

每一个线程都会有一个id,我们会单独创建一个文件来执行这个函数,其目的就是因为我们得到线程的id是调用了linux内核的API,并且调用这个函数涉及到了系统间的切换,开销比较的大,所以我们只要执行了这个函数之后,就保存到当前线程的缓存里面。然后这个地方我们涉及到了c++20的一个语法,因为我们一开是创建了tid之后这个就不会是0了,所以极大概率我们调用这个函数的时候这个tid已经被创造了,所以我们把这个if放在极大不会执行的一边,这样子可以提高我们cpu的执行效率。最后我们存储的方式就是利用线程的唯一声明__thread

cpp 复制代码
        extern __thread int t_cachedTid; // 每个线程都有属于自己的tid

        void cacheTid();

        inline int tid() {
            if (t_cachedTid == 0) [[unlikely]] {
                cacheTid();
            }
            return t_cachedTid;
        }
cpp 复制代码
        __thread int t_cachedTid = 0;

        void cacheTid()
        {
            if (t_cachedTid == 0) {
                // 通过linux的系统调用获取当前线程的tid值
                t_cachedTid = static_cast<pid_t>(::syscall(SYS_gettid));
            }
        }

Buffer

我们需要一个buffer缓冲区,当数据通过tcp发过来的时候速率可能是很快的,我们读取的速率相对来说比较的慢,所以我们需要一个缓冲区。这个缓冲区分成三个部分,第一个部分是cheapPrepend,这一部分一共8个字节,目的就是接受tcp携带的一些信息数据;第二个部分是readableTypes,这个部分是可读的数据,也就是我们所有等待读取的数据都是在这个分段里面,第三个部分是writeableTypes(),也就是我们还可以写入(接受)的数据,因为这个缓冲区并不是无限大的,所以我们接受的数据也并不是无限大的,所以我们需要根据writeableTypes()来判断我们还能接受的数据并采取对应的操作。

所以我们这里就有readerIndex 和 writeIndex来划分出这三个界限,在实际过程之中,我们一次很有可能不可以读完所有的数据,所以当读完一次数据之后需要把数据重置,换句话说就是移动readerIndex的位置,因为我们每一次开始读取数据的位置都是根据readerIndex的位置

总的来说我们必须要理解一个道理就是readerIndex和writeIndex都是会移动的,reader部分的结束也就是write的开始,这中间夹着的就是我们需要读的数据。

cpp 复制代码
    class Buffer {
    public:
        static const size_t kCheapPrepend = 8;
        static const size_t kInitialSize = 1024;

        explicit Buffer(size_t initialSize = kInitialSize)
            : m_buffer(kCheapPrepend + kInitialSize)
            , m_readerIndex(kCheapPrepend)
            , m_writerIndex(kCheapPrepend)
        {}

        size_t readableBytes() const {
            return m_writerIndex - m_readerIndex;
        }

        size_t writerableBytes() const {
            return m_buffer.size() - m_writerIndex;
        }

        size_t prependableBytes() const {
            return m_readerIndex;
        }
 
        // 返回缓冲区种可读数据的起始地址
        const char* peek() const {
            return begin() + m_readerIndex;
        }

        void retrieve(size_t len) {
            if (len < readableBytes()) {
                m_readerIndex += len; // 应用只读取了可读缓冲区数据的一部分,就是len,还剩下m_readerIndex += len -> m_writerIndex
            } else {
                retrieveAll();
            }
        }

        void retrieveAll() {
            m_readerIndex = m_writerIndex = kCheapPrepend;
        }

        // 把onMessage函数上报的Buffer数据,转成string类型数据返回
        std::string retrieveAllAsString() {
            return retrieveAsString(readableBytes()); // 应用可读数据的长度
        }

        std::string retrieveAsString(size_t len) {
            std::string result(peek(), len);
            retrieve(len); // 上面一句把缓冲区中可读的数据已经读取出来,这里肯定要对缓冲区进行复位操作
            return result;
        }

        void ensureWriteableBytes(size_t len) {
            if (writerableBytes() < len) {
                makeSpace(len);
            }
        }

        // 把[data, data + len]内存上的数据,添加到writeable缓冲区中
        void append(const char* data, size_t len) {
            ensureWriteableBytes(len);
            std::copy(data, data + len, beginWrite());
            m_writerIndex += len;
        }

        char* beginWrite() {
            return begin() + m_writerIndex;
        }

        const char* beginWrite() const {
            return begin() + m_writerIndex;
        }

        // 从fd上读取数据
        ssize_t readFd(int fd, int* saveErrno);
        ssize_t writeFd(int fd, int* saveErrno);

    private:
        char* begin() {
            // 先是调用了迭代器的*号运算符重载函数,得到首元素
            // 再取地址
            return &*m_buffer.begin();
        }
        
        const char* begin() const {
            return &*m_buffer.begin();
        }

        /*
        kCheapPrepend  | reader | writer
        readIndex 并不是一定是8,如果已经读了一部分,那么就会有一部分是空闲的,而prependableBytes() - kChaepPrepend就是这一部分空闲的
        如果空闲的长度加上写部分的长度都不满足len,那么就要扩容
        */
        void makeSpace(size_t len) {
            if (writerableBytes() + prependableBytes() < len + kCheapPrepend) {
                m_buffer.resize(m_writerIndex + len);
            } else {
                size_t readable = readableBytes();
                std::copy(begin() + m_readerIndex, begin() + m_writerIndex, begin() + kCheapPrepend);
                m_readerIndex = kCheapPrepend;
                m_writerIndex = readable + m_readerIndex;
            }
        }

        std::vector<char> m_buffer;
        size_t m_readerIndex;  
        size_t m_writerIndex;
    };

我们在读取数据的时候会遇到一个很大的麻烦,就是我们虽然buffer有大小,但是我们却不知道tcp数据的大小,这样子就会导致我们为了读取完所有的数据会把buffer扩容的很大而浪费了很多的空间。这里Linux的内核给我们提供了一个API readv ,这个读取数据的特点是可以根据读到的数据自己分配内存空间,不浪费内存。所以我们先开一个数组,然后把现有的和额外的填充进iovec的结构体,最后根据我们读取的字节数量来判断我们额外的buffer用到了没有,如果用到了,就append在最后,如果没有用到,就把writeIndex移动读取的字节数量,因为这一些数据就全部被加入到了reader的区间里面。

当readfd了之后,自然writefd,这个操作就是在readabletypes()区间里面。

cpp 复制代码
/*
从fd上读取数据,Poller工作在LT模式上
buffer缓冲区是有大小的,但是从fd上读取数据的时候,却不知道tcp数据的大小
*/
ssize_t Litguidyo::Buffer::readFd(int fd, int *saveErrno)
{
    char extrabuf[65536] = {0}; // 栈上的内存空间,分配内存快且出作用域就自动回收   64k
    struct iovec vec[2];
    const size_t writeable = writerableBytes(); // 这是buffer底层缓冲区剩余的可写空间的大小
    vec[0].iov_base = begin() + m_writerIndex;
    vec[0].iov_len = writeable;
    
    vec[1].iov_base = extrabuf;
    vec[1].iov_len = sizeof(extrabuf);
    
    const int iovcnt = (writeable < sizeof(extrabuf)) ? 2 : 1;
    
    const ssize_t n = ::readv(fd, vec, iovcnt);
    if (n < 0) {
        *saveErrno = errno;
    } else if (n <= writeable) {
        m_writerIndex += n;
    } else { // extrabuf里面也写入了 数据
        m_writerIndex = m_buffer.size();
        append(extrabuf, n - writeable);
    }
    return n;
}
ssize_t Litguidyo::Buffer::writeFd(int fd, int* saveErrno)
{
    ssize_t n = ::write(fd, peek(), readableBytes());
    if (n <= 0) {
        *saveErrno = errno;
    }
    return n;
}

Socket

我们tcp通信很重要的一点就是对于socket的封装,里面有listen,bind,accept,还有对于fd的一些设置。

cpp 复制代码
    class Socket : noncopyable {
    public:
        explicit Socket(int sockfd) : m_sockfd(sockfd) 
        {}

        ~Socket();

        int fd() const {return m_sockfd;}
        void bindAddress(const InetAddress& localaddr);
        void listen();
        int accept(InetAddress* perraddr);

        void shutdownWrite();

        void setTcpNoDelay(bool on);
        void setReuseAddr(bool on);
        void setReusePort(bool on);
        void setKeepAlive(bool on);
    private:
        const int m_sockfd;
    };

一下就是一些基本的性质设置。

cpp 复制代码
    Socket::~Socket()
    {
        ::close(m_sockfd);
    }
    
    void Socket::bindAddress(const InetAddress& localaddr)
    {
        if (bind(m_sockfd, (sockaddr*)localaddr.getSockAddr(), sizeof(sockaddr_in)) != 0) {
            LOG_FATAL("bind sockfd: %d failed \n", m_sockfd);
        }
    }
    
    void Socket::listen()
    {
        if (::listen(m_sockfd, 1024) != 0) {
            LOG_FATAL("listen sockfd:%d fail \n", m_sockfd);
        }
    }
    
    void Socket::shutdownWrite()
    {
        if (::shutdown(m_sockfd, SHUT_WR) < 0) {
            LOG_ERROR("shutdownWrite error");
        }
    }
    
    void Socket::setTcpNoDelay(bool on)
    {
        int optval = on ? true : false;
        ::setsockopt(m_sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));
    }
    
    void Socket::setReuseAddr(bool on)
    {
        int optval = on ? true : false;
        ::setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    }
    
    void Socket::setReusePort(bool on)
    {
        int optval = on ? true : false;
        ::setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    }
    
    void Socket::setKeepAlive(bool on)
    {
        int optval = on ? true : false;
        ::setsockopt(m_sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
    }

我们一定要记得,一个好的服务器模型,一般都是non-blocking,所以我们要对fd进行设置,在接受客户端的时候我们这里就使用accept4,这样子就可以设置为非阻塞。

cpp 复制代码
    int Socket::accept(InetAddress* perraddr)
    {
        sockaddr_in addr;
        socklen_t len = sizeof(addr);
        bzero(&addr, sizeof(addr));
        // 模型是poller + non-blocking
        int connfd = ::accept4(m_sockfd, (sockaddr*)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
        if (connfd >= 0) {
            perraddr->setSockAddr(addr);
        }
        return connfd;
    }

总结

本篇文章到这里就结束了!!!希望可以帮助大家理解~~~~

相关推荐
码匠许师傅4 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试
C++ 老炮儿的技术栈4 小时前
Qt5.9.1 Windows 完整开发环境搭建流程
开发语言·c++·windows·qt·编辑器·代码化
xcLeigh4 小时前
Go入门:无类型常量与类型常量的区别
服务器·开发语言·golang
数据知道5 小时前
网络安全实战:子域名接管实战——从 CNAME 配置错误到完全控制
网络·安全·web安全·网络安全
键盘会跳舞5 小时前
C++:std::tuple 源码级深度拆解——变参模板、SFINAE与模板元编程核心技巧
开发语言·c++·sfinae·变参模板
bkspiderx5 小时前
从零开始:VS Code搭建C/C++开发环境全指南(Windows/macOS/Linux)
c语言·c++·windows·vs code·c/c++开发环境
东华万里5 小时前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区
luj_17685 小时前
元设计的诱惑与现实
c语言·开发语言·c++·经验分享·算法
lf13210275 小时前
用 JSON Schema 管装修节点记录:从照片台账到可校验工程数据
网络·数据库·人工智能·经验分享·物联网·json·智能家居
LayZhangStrive5 小时前
后端通识 - 远程服务调用RPC
网络·网络协议·rpc·sentinel·openfeign·远程服务调用