【webserver】第4节 实现主线程的读写

本文介绍了一个使用线程池、通过epoll实现的Proactor版本的web服务器。代码开源,参考相关书籍和课程并做了整合与修改。详细阐述了主线程的读和写操作,包括recv、send、writev的介绍,流程说明、边角操作判断及具体实现,还提及了读写操作与子线程的交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码开源:

        https://github.com/PetterZhukov/webserver_HTTP

介绍:

        webserver_HTTP
        使用了线程池,通过epoll实现的Proactor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。

        访问的资源在 同级目录"resources"文件夹中

目录

 4.1 主线程的读

4.1.1 recv的介绍

4.1.2 流程介绍

4.1.3 各种边角操作

4.1.4 读操作的实现

4.2 主线程的写

4.2.1 send的介绍

4.2.2 分散写writev的介绍

4.2.3 流程介绍

4.2.4 各种边角操作的判断

4.2.5 具体实现


         因为关于子线程进行的操作(即process)以及最关键的模块http_conn我还没介绍,因此貌似会有一些缺漏未介绍。

        但是——让我很自豪的是,我通过各种修改,使得读写对子线程尽可能透明(当然仍然有作为接口的操作),排除了一些需要了解http_conn模块才能处理的部分。

        当然,比如报文需要分多份发送,因此有些变量必须是共享的,所以耦合性不能避免。

        值得一提的是,Read和Write之前是http_conn的成员方法,但是被我提出来了,我觉得这样更符合模块设计,也有更好的可读性。

 4.1 主线程的读

4.1.1 recv的介绍

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    参数:
        - sockfd: 要发送的fd
        - buf:要发送的数据存储的位置
        - len:要发送的长度
        - flags:通常为0
    返回值:
        - 成功:返回发送成功的字节数
            - 可能为0
        - 失败:返回-1
        - 结束:返回0

4.1.2 流程介绍

 PS:若发生EAGAIN错误会停止

PS2:假如之后进行分析说明本次连接还未结束,则会修改epoll内核的事件信息再次调用Write进行读写

 如图,使用recv进行循环读写,直到对方关闭连接/发生错误,因为使用的文件描述符是非阻塞的,所以发生的错误中若有EAGIN(使用写操作同理),说明当前没有数据可读(但是还没断开连接),也是正常情况。

4.1.3 各种边角操作

        因为是要与子线程的内存部分进行交互,且对子线程是透明的,因此我是采用手写的get和set方法对属性进行操作。

        读内存要修改和使用的值很简单,只有两个,一个是要修改的内存,一个是修改后内存的大小。因此开头加上获取内存和内存大小,然后进行读取后,结尾加上设置内存大小即可(内存因为是指针,直接进行对应的修改,不需要写回)。

4.1.4 读操作的实现

// 非阻塞的读,循环读取
bool epoll_class::Read(http_conn &conn)
{
    int m_sockfd=conn.get_sockfd();
    int m_read_index=conn.get_read_index();
    char *m_read_buf=conn.get_read_buf();

    // 缓冲已满
    if(m_read_index >= READ_BUFFER_SIZE){
        return false;
    }
    // 读取字节
    while(true)
    {
        int bytes_read=recv(m_sockfd,m_read_buf+m_read_index,READ_BUFFER_SIZE-m_read_index,0);
        if(bytes_read==-1){
            if(errno==EAGAIN || errno==EWOULDBLOCK)
                break;
            else
                return false;
        }
        else if(bytes_read==0){
            return false;
        }
        m_read_index+=bytes_read;
    }
    // 更新值
    conn.set_read_index(m_read_index);
    return true;
}

PS:假如去看我的master部分的代码(也就是更接近老师的版本的部分),因为Read定义在http_conn中, 因此其不需要开头和结尾的set,get操作,不过我觉得这样的分离是可以接受的。 

4.2 主线程的写

4.2.1 send的介绍

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    - sockfd:接收方对应的文件描述符
    - buf:存储要发送字符的缓冲区
    - len:发送的字符的长度
    - flags:一般为0

send和write其实很像,之所以要介绍send,是因为相比分散写,send和write更为常用(writev 是在发送多个分散内存的时候有用)。

4.2.2 分散写writev的介绍

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
 - 参数:
    - fd:接收方的fd
    - iov:存储发送数据的数组,每个元素包含起始指针和长度
    - iovcnt:iov数组的长度
    struct iovec {
               void  *iov_base;    /* Starting address */
               size_t iov_len;     /* Number of bytes to transfer */
           };
    struct iovec的结构,里面有起始位置,以及长度
- 返回值:
    - 成功:发送的字节数
    - 失败:-1 
    

        需要注意的是,在调用writev之后,它不会自动调整iov数组里面的值,因此假如需要多次发送(毕竟发送的数据一个报文不一定装得下)。这时候需要根据返回值来对其进行调整。不过好在writev是按顺序发的,即从第0,1,2,3......这样的顺序发送,因此遍历调整即可。 

4.2.3 流程介绍

        从刚刚的介绍可以得知,我们的目的是调用writev将几个零散的内存发送给接受方。

        通过后面的介绍可以得知,发送的主要是两块(具体是哪两块后面再介绍),且每一块的内容和长度都包含在iov数组中,其内容对Write函数是完全透明的。

        我先介绍一下http_conn类对Write的接口,它需要向Write传入以下几个参数

    - sockfd:接收方的文件描述符
    - bytes_to_send:要发送的总字节数
    - m_iv:iovec数组,包含了要发送的内容的信息
    - m_iv_count:m_iv数组的长度

 以下是发送流程

 由上一节的代码可以得知,返回值表示后续操作

  • true为不操作
  • 失败即关闭文件描述符,有两种情况,写失败,或者写成功且不保持连接

4.2.4 各种边角操作的判断

        我在http_conn中实现了clear操作,作用是将类中所有的中间属性全部清零(不包括文件描述符、地址等属性,其在init方法中操作),还有内存映射释放的操作。

        发送完成(要发送的字节数为0/已经全部发送),在返回之前需要调用clear进行清除操作。

        在返回之前,因为要重新设置epoll事件(假如是oneshot类型,则事件需要重新注册,因此都需要调用modfd函数。

        (至于其中的modfd函数的具体组成,详见下节)

        采用EPOLLONETSHOT事件的文件描述符上的注册事件只触发一次,要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态。

4.2.5 具体实现

// 非阻塞的写
bool epoll_class::Write(http_conn &conn)
{
    const int m_sockfd = conn.get_sockfd();
    const int bytes_to_send = conn.get_bytes_to_send();
    iovec *m_iv = conn.get_iv();
    const int m_iv_count = conn.get_iv_count();

    if(bytes_to_send==0){
        // 将要发送的字节为0
        modfd(epollfd,m_sockfd,EPOLLIN);
        conn.clear();
        return true;
    }
    while (true)
    {
        int ret = writev(m_sockfd, m_iv, m_iv_count);
        if (ret <= -1)
        {
            // 发送失败
            if (errno == EAGAIN)
            {// 重试
                modfd(epollfd, m_sockfd, EPOLLOUT);
                return true;
            }
            return false;
        }
        // 本次写成功
        // 维护还需发送字节数和已发送字节数

        //分散写第一部分是否写完
        if (ret >= m_iv[0].iov_len)
        { // 第一部分写完了
            m_iv[1].iov_base = (char *)m_iv[1].iov_base + (ret - m_iv[0].iov_len);
            m_iv[1].iov_len -= (ret - m_iv[0].iov_len);
            m_iv[0].iov_len = 0;
        }
        else
        { // 第一部分还没写完
            m_iv[0].iov_base = (char *)(m_iv[0].iov_base) + ret;
            m_iv[0].iov_len -= ret;
        }

        // 发送结束
        if (m_iv[1].iov_len<=0)
        { // 发送HTTP响应成功,释放内存
            modfd(epollfd, m_sockfd, EPOLLIN);
            // 是否keep-alive
            if (conn.is_keepalive())
            {
                conn.clear();
                // 继续接受信息
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值