35 | 答疑:编写高性能网络编程框架时,都需要注意哪些问题?

你好,我是盛延敏,这里是网络编程实战的第35讲,欢迎回来。

这一篇文章是实战篇的答疑部分,也是本系列的最后一篇文章。非常感谢你的积极评论与留言,让每一篇文章的留言区都成为学习互动的好地方。在今天的内容里,我将针对评论区的问题做一次集中回答,希望能帮助你解决前面碰到的一些问题。

有关这部分内容,我将采用Q&A的形式来展开。

为什么在发送数据时,会先尝试通过socket直接发送,再由框架接管呢?

这个问题具体描述是下面这样的。

当应用程序需要发送数据时,比如下面这段,在完成数据读取和回应的编码之后,会调用tcp_connection_send_buffer方法发送数据。

//数据读到buffer之后的callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
    printf("get message from tcp connection %s\n", tcpConnection->name);
    printf("%s", input->data);

    struct buffer *output = buffer_new();
    int size = buffer_readable_size(input);
    for (int i = 0; i < size; i++) {
        buffer_append_char(output, rot13_char(buffer_read_char(input)));
    }
    tcp_connection_send_buffer(tcpConnection, output);
    return 0;
}

而tcp_connection_send_buffer方法则会调用tcp_connection_send_data来发送数据:

int tcp_connection_send_buffer(struct tcp_connection *tcpConnection, struct buffer *buffer) {
    int size = buffer_readable_size(buffer);
    int result = tcp_connection_send_data(tcpConnection, buffer->data + buffer->readIndex, size);
    buffer->readIndex += size;
    return result;
}

在tcp_connection_send_data中,如果发现当前 channel 没有注册 WRITE 事件,并且当前 tcp_connection 对应的发送缓冲无数据需要发送,就直接调用 write 函数将数据发送出去。

//应用层调用入口
int tcp_connection_send_data(struct tcp_connection *tcpConnection, void *data, int size) {
    size_t nwrited = 0;
    size_t nleft = size;
    int fault = 0;

    struct channel *channel = tcpConnection->channel;
    struct buffer *output_buffer = tcpConnection->output_buffer;

    //先往套接字尝试发送数据
    if (!channel_write_event_is_enabled(channel) && buffer_readable_size(output_buffer) == 0) {
        nwrited = write(channel->fd, data, size);
        if (nwrited >= 0) {
            nleft = nleft - nwrited;
        } else {
            nwrited = 0;
            if (errno != EWOULDBLOCK) {
                if (errno == EPIPE || errno == ECONNRESET) {
                    fault = 1;
                }
            }
        }
    }

    if (!fault && nleft > 0) {
        //拷贝到Buffer中,Buffer的数据由框架接管
        buffer_append(output_buffer, data + nwrited, nleft);
        if (!channel_write_event_is_enabled(channel)) {
            channel_write_event_enable(channel);
        }
    }

    return nwrited;
}

这里有同学不是很理解,为啥不能做成无论有没有 WRITE 事件都统一往发送缓冲区写,再把WRITE 事件注册到event_loop中呢?

这个问题问得非常好。我觉得有必要展开讲讲。

如果用一句话来总结的话,这是为了发送效率。

我们来分析一下,应用层读取数据,进行编码,之后的这个buffer对象是应用层创建的,数据也在应用层这个buffer对象上。你可以理解,tcp_connection_send_data里面的data数据其实是应用层缓冲的,而不是我们tcp_connection这个对象里面的buffer。

如果我们跳过直接往套接字发送这一段,而是把数据交给我们的tcp_connection对应的output_buffer,这里有一个数据拷贝的过程,它发生在buffer_append里面。

int buffer_append(struct buffer *buffer, void *data, int size) {
    if (data != NULL) {
        make_room(buffer, size);
        //拷贝数据到可写空间中
        memcpy(buffer->data + buffer->writeIndex, data, size);
        buffer->writeIndex += size;
    }
}

但是,如果增加了一段判断来直接往套接字发送,其实就跳过了这段拷贝,直接把数据发往到了套接字发生缓冲区。

//先往套接字尝试发送数据
if (!channel_write_event_is_enabled(channel) && buffer_readable_size(output_buffer) == 0) {
        nwrited = write(channel->fd, data, size)
        ...

在绝大部分场景下,这种处理方式已经满足数据发送的需要了,不再需要把数据拷贝到tcp_connection对象中的output_buffer中。

如果不满足直接往套接字发送的条件,比如已经注册了回调事件,或者output_buffer里面有数据需要发送,那么就把数据拷贝到output_buffer中,让event_loop的回调不断地驱动handle_write将数据从output_buffer发往套接字缓冲区中。

//发送缓冲区可以往外写
//把channel对应的output_buffer不断往外发送
int handle_write(void *data) {
    struct tcp_connection *tcpConnection = (struct tcp_connection *) data;
    struct event_loop *eventLoop = tcpConnection->eventLoop;
    assertInSameThread(eventLoop);

    struct buffer *output_buffer = tcpConnection->output_buffer;
    struct channel *channel = tcpConnection->channel;

    ssize_t nwrited = write(channel->fd, output_buffer->data + output_buffer->readIndex,buffer_readable_size(output_buffer));
    if (nwrited > 0) {
        //已读nwrited字节
        output_buffer->readIndex += nwrited;
        //如果数据完全发送出去,就不需要继续了
        if (buffer_readable_size(output_buffer) == 0) {
            channel_write_event_disable(channel);
        }
        //回调writeCompletedCallBack
        if (tcpConnection->writeCompletedCallBack != NULL) {
            tcpConnection->writeCompletedCallBack(tcpConnection);
        }
    } else {
        yolanda_msgx("handle_write for tcp connection %s", tcpConnection->name);
    }

}

你可以这样想象,在一个非常高效的处理条件下,你需要发送什么,都直接发送给了套接字缓冲区;而当网络条件变差,处理效率变慢,或者待发送的数据极大,一次发送不可能完成的时候,这部分数据被框架缓冲到tcp_connection的发送缓冲区对象output_buffer中,由事件分发机制来负责把这部分数据发送给套接字缓冲区。

关于回调函数的设计

在epoll-server-multithreads.c里面定义了很多回调函数,比如onMessage, onConnectionCompleted等,这些回调函数被用于创建一个TCPServer,但是在tcp_connection对照中,又实现了handle_read handle_write 等事件的回调,似乎有两层回调,为什么要这样封装两层回调呢?

这里如果说回调函数,确实有两个不同层次的回调函数。

第一个层次是框架定义的,对连接的生命周期管理的回调。包括连接建立完成后的回调、报文读取并接收到output缓冲区之后的回调、报文发送到套接字缓冲区之后的回调,以及连接关闭时的回调。分别是connectionCompletedCallBack、messageCallBack、writeCompletedCallBack,以及connectionClosedCallBack。

struct tcp_connection {
    struct event_loop *eventLoop;
    struct channel *channel;
    char *name;
    struct buffer *input_buffer;   //接收缓冲区
    struct buffer *output_buffer;  //发送缓冲区

    connection_completed_call_back connectionCompletedCallBack;
    message_call_back messageCallBack;
    write_completed_call_back writeCompletedCallBack;
    connection_closed_call_back connectionClosedCallBack;

    void * data; //for callback use: http_server
    void * request; // for callback use
    void * response; // for callback use
};

为什么要定义这四个回调函数呢?

因为框架需要提供给应用程序和框架的编程接口,我把它总结为编程连接点,或者叫做program-hook-point。就像是设计了一个抽象类,这个抽象类代表了框架给你提供的一个编程入口,你可以继承这个抽象类,完成一些方法的填充,这些方法和框架类一起工作,就可以表现出一定符合逻辑的行为。

比如我们定义一个抽象类People,这个类的其他属性,包括它的创建和管理都可以交给框架来完成,但是你需要完成两个函数,一个是on_sad,这个人悲伤的时候干什么;另一个是on_happy,这个人高兴的时候干什么。

abstract class People{
  void on_sad();
  
  void on_happy();
}

这样,我们可以试着把tcp_connection改成这样:

abstract class TCP_connection{
  void on_connection_completed();
  
  void on_message();
  
  void on_write_completed();
  
  void on_connectin_closed();
}

这个层次的回调,更像是一层框架和应用程序约定的接口,接口实现由应用程序来完成,框架负责在合适的时候调用这些预定义好的接口,回调的意思体现在“框架会调用预定好的接口实现”。

比如,当连接建立成功,一个新的connection创建出来,connectionCompletedCallBack函数会被回调:

struct tcp_connection *
tcp_connection_new(int connected_fd, struct event_loop *eventLoop,
connection_completed_call_back connectionCompletedCallBack,
connection_closed_call_back connectionClosedCallBack,
message_call_back messageCallBack, 
write_completed_call_back writeCompletedCallBack) {
    ...
    // add event read for the new connection
    struct channel *channel1 = channel_new(connected_fd, EVENT_READ, handle_read, handle_write, tcpConnection);
    tcpConnection->channel = channel1;

    //connectionCompletedCallBack callback
    if (tcpConnection->connectionCompletedCallBack != NULL) {
        tcpConnection->connectionCompletedCallBack(tcpConnection);
    }

   ...
}

第二个层次的回调,是基于epoll、poll事件分发机制的回调。通过注册一定的读、写事件,在实际事件发生时,由事件分发机制保证对应的事件回调函数被及时调用,完成基于事件机制的网络I/O处理。

在每个连接建立之后,创建一个对应的channel对象,并为这个channel对象赋予了读、写回调函数:

// add event read for the new connection
struct channel *channel1 = channel_new(connected_fd, EVENT_READ, handle_read, handle_write, tcpConnection);

handle_read函数,对应用程序屏蔽了套接字的读操作,把数据缓冲到tcp_connection的input_buffer中,而且,它还起到了编程连接点和框架的耦合器的作用,这里分别调用了messageCallBack和connectionClosedCallBack函数,完成了应用程序编写部分代码在框架的“代入”。

int handle_read(void *data) {
    struct tcp_connection *tcpConnection = (struct tcp_connection *) data;
    struct buffer *input_buffer = tcpConnection->input_buffer;
    struct channel *channel = tcpConnection->channel;

    if (buffer_socket_read(input_buffer, channel->fd) > 0) {
        //应用程序真正读取Buffer里的数据
        if (tcpConnection->messageCallBack != NULL) {
            tcpConnection->messageCallBack(input_buffer, tcpConnection);
        }
    } else {
        handle_connection_closed(tcpConnection);
    }
}

handle_write函数则负责把tcp_connection对象里的output_buffer源源不断地送往套接字发送缓冲区。

//发送缓冲区可以往外写
//把channel对应的output_buffer不断往外发送
int handle_write(void *data) {
    struct tcp_connection *tcpConnection = (struct tcp_connection *) data;
    struct event_loop *eventLoop = tcpConnection->eventLoop;
    assertInSameThread(eventLoop);

    struct buffer *output_buffer = tcpConnection->output_buffer;
    struct channel *channel = tcpConnection->channel;

    ssize_t nwrited = write(channel->fd, output_buffer->data + output_buffer->readIndex,buffer_readable_size(output_buffer));
    if (nwrited > 0) {
        //已读nwrited字节
        output_buffer->readIndex += nwrited;
        //如果数据完全发送出去,就不需要继续了
        if (buffer_readable_size(output_buffer) == 0) {
            channel_write_event_disable(channel);
        }
        //回调writeCompletedCallBack
        if (tcpConnection->writeCompletedCallBack != NULL) {
            tcpConnection->writeCompletedCallBack(tcpConnection);
        }
    } else {
        yolanda_msgx("handle_write for tcp connection %s", tcpConnection->name);
    }

}

tcp_connection对象设计的想法是什么,和channel有什么联系和区别?

tcp_connection对象似乎和channel对象有着非常紧密的联系,为什么要单独设计一个tcp_connection呢?

我也提到了,开始的时候我并不打算设计一个tcp_connection对象的,后来我才发现非常有必要存在一个tcp_connection对象。

第一,我需要在暴露给应用程序的onMessage,onConnectionCompleted等回调函数里,传递一个有用的数据结构,这个数据结构必须有一定的现实语义,可以携带一定的信息,比如套接字、缓冲区等,而channel对象过于单薄,和连接的语义相去甚远。

第二,这个channel对象是抽象的,比如acceptor,比如socketpair等,它们都是一个channel,只要能引起事件的发生和传递,都是一个channel,基于这一点,我也觉得最好把chanel作为一个内部实现的细节,不要通过回调函数暴露给应用程序。

第三,在后面实现HTTP的过程中,我发现需要在上下文中保存http_request和http_response数据,而这个部分数据放在channel中是非常不合适的,所以才有了最后的tcp_connection对象。

struct tcp_connection {
    struct event_loop *eventLoop;
    struct channel *channel;
    char *name;
    struct buffer *input_buffer;   //接收缓冲区
    struct buffer *output_buffer;  //发送缓冲区

    connection_completed_call_back connectionCompletedCallBack;
    message_call_back messageCallBack;
    write_completed_call_back writeCompletedCallBack;
    connection_closed_call_back connectionClosedCallBack;

    void * data; //for callback use: http_server
    void * request; // for callback use
    void * response; // for callback use
};

简单总结下来就是,每个tcp_connection对象一定包含了一个channel对象,而channel对象未必是一个tcp_connection对象。

主线程等待子线程完成的同步锁问题

有人在加锁这里有个疑问,如果加锁的目的是让主线程等待子线程初始化event_loop,那不加锁不是也可以达到这个目的吗?主线程while循环里面不断判断子线程的event_loop是否不为null不就可以了?为什么一定要加一把锁呢?

//由主线程调用,初始化一个子线程,并且让子线程开始运行event_loop
struct event_loop *event_loop_thread_start(struct event_loop_thread *eventLoopThread) {
    pthread_create(&eventLoopThread->thread_tid, NULL, &event_loop_thread_run, eventLoopThread);

    assert(pthread_mutex_lock(&eventLoopThread->mutex) == 0);

    while (eventLoopThread->eventLoop == NULL) {
        assert(pthread_cond_wait(&eventLoopThread->cond, &eventLoopThread->mutex) == 0);
    }
    assert(pthread_mutex_unlock(&eventLoopThread->mutex) == 0);

    yolanda_msgx("event loop thread started, %s", eventLoopThread->thread_name);
    return eventLoopThread->eventLoop;
}

要回答这个问题,就要解释多线程下共享变量竞争的问题。我们知道,一个共享变量在多个线程下同时作用,如果没有锁的控制,就会引起变量的不同步。这里的共享变量就是每个eventLoopThread的eventLoop对象。

这里如果我们不加锁,一直循环判断每个eventLoopThread的状态,会对CPU增加很大的消耗,如果使用锁-信号量的方式来加以解决,就变得很优雅,而且不会对CPU造成过多的影响。

关于channel_map的设计,特别是内存方面的设计。

我们来详细介绍一下channel_map。

channel_map实际上是一个指针数组,这个数组里面的每个元素都是一个指针,指向了创建出的channel对象。我们用数据下标和套接字进行了映射,这样虽然有些元素是浪费了,比如stdin,stdout,stderr代表的套接字0、1和2,但是总体效率是非常高的。

你在这里可以看到图中描绘了channel_map的设计。

而且,我们的channel_map还不会太占用内存,在最开始的时候,整个channel_map的指针数组大小为0,当这个channel_map投入使用时,会根据实际使用的套接字的增长,按照32、64、128这样的速度成倍增长,这样既保证了实际的需求,也不会一下子占用太多的内存。

此外,当指针数组增长时,我们不会销毁原来的部分,而是使用realloc()把旧的内容搬过去,再使用memset() 用来给新申请的内存初始化为0值,这样既高效也节省内存。

总结

以上就是实战篇中一些同学的疑问。

在这篇文章之后,我们的专栏就告一段落了,我希望这个专栏可以帮你梳理清楚高性能网络编程的方方面面,如果你能从中有所领悟,或者帮助你在面试中拿到好的结果,我会深感欣慰。

如果你觉得今天的答疑内容对你有所帮助,欢迎把它转发给你的朋友或者同事,一起交流一下。

精选留言

  • 酸葡萄

    2019-11-30 19:47:26

    为什么在发送数据时,会先尝试通过 socket 直接发送,再由框架接管呢?
    老师你好,这个问题中,发送缓冲区有数据说明发送效率低(数据多,网络差等原因导致),没有注册WRITE事件是什么意思呢?(感觉这时一个基础问题[小尴尬])
    作者回复

    有两种发送数据的方式,第一种是通过注册WRITE事件,等待reactor来驱动我们把数据发送出去;第二种是不需要reactor驱动,直接往套接字上发送。这里的解释是说,在大部分情况下,为了效率,直接往套接字上发送,当一次解决不了时,再通过reactor来驱动数据发送。

    2019-12-01 15:45:15

  • Geek_63bb29

    2020-07-30 23:02:25

    谢谢盛老师,链接是关于实战代码的流程图 https://app.yinxiang.com/fx/7e601cad-6501-4fe7-8e4e-f0fbd9d02c4b
    作者回复

    ������

    2020-08-16 21:40:31

  • 范闲

    2020-03-27 17:24:36


    这是我改造的c++版本,目前还在调试中
    https://gitee.com/willam-dialogue/net_reactor


    调试过程中遇到了几个问题:
    1.在telnet以后, 客户端第一次发送消息,可以正常收到消息. 客户端第二次发送消息,会导致server coredump. 目前初步定位到问题是发生在TcpConnection.h中的sendData函数,更具体的原因没有找到

    2.如果将回调函数注册为如下方式:
    TcpConnection 公有云继承了enabled_shared_from_this
    typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
    typedef std::function<void (const TcpConnectionPtr &)> ConnCompleteCallBack;
    typedef std::function<void (const TcpConnectionPtr &)> ConnCloseCallBack;
    typedef std::function<void (const TcpConnectionPtr &)> WriteCompleteCallBack;
    typedef std::function<void (Buffer*, const TcpConnectionPtr &)> MessageCallBack;

    在TcpConnection调用ConnCompleteCallBack就没有问题.

    但是在channel中绑定了TcpConnection.h 中的handleRead和handleWrite的回调,在调试过程中会报weak_ptr的相关错误.实际定位发现在handleRead里面调用了MessageCallBack的回调,MessageCallBack的入参是shared_from_this(),weak_ptr的错误由这个产生的,目前还在看是否因为使用方法的原因引起的.

    希望同学们能够一起帮忙看看 这些问题, 我还没找到好的方法.邮箱hy572801400@163.com
    作者回复

    给你顶一下,大家一起帮忙看(PS:我最近有点忙,闲下来也来帮你一起看)

    2020-03-29 20:24:23

  • 王小白白白

    2020-08-01 17:54:39

    首先非常感谢老师的课程,系统的学习了网络编程相关知识,受益匪浅。
    这里是我改写的c++ epoll服务器版本,https://github.com/wangxiaobai-dd/BowServer
    主要改动有:
    1,使用c++语法,智能指针,variant,std::mutex,std::thread等,代码结构有些改变
    2,消除一些内存泄漏,(buffer相关待做)
    3,加入一个事件队列channel_queue,这样event_dispatch可以改为非阻塞,取消唤醒机制
    4, 有新连接时选择任务数最少的连接
    5,epoll del ,update 接口有所修改
    会持续完善优化项目,一起学习进步~ 有帮助的话点个星星嘻嘻
    作者回复

    赞,已点。

    2020-08-16 21:45:44

  • CofCai

    2022-01-22 14:44:27

    我最开始是直接一头代码的细节里面去,没先从宏观上有个把握,然后读的很痛苦。于是自己就借助一些工具,比如思维导图画一下函数调用关系、各种结构体对象的关系,总算有一点头绪了。贴上我的学习笔记(笔记是边读源码边写的,有的理解后来觉得不对,但可能没来得及修改,希望各位伙伴带着思考):
    各种结构体对象关系:https://www.processon.com/view/link/5ead14555653bb6efc7cbe59
    作者回复

    强大,请问我可以引用么?

    2022-01-23 15:53:57

  • 日就月将

    2021-10-20 16:26:34

    老师 您写的代码好像没有加内存释放处理
    作者回复

    有可能遗漏的,能帮忙在githut提MR么?

    2021-10-25 09:52:55

  • 范闲

    2020-03-27 17:15:43

    https://gitee.com/willam-dialogue/net_reactor

    这是目前我改造的cpp版本,正在调试中。
    调试过程中遇到一些问题
    1.telnet连接以后,第一次发送消息正常。但是第二次发送消息就会coredump, 初步定位到问题出在TcpConnection.h 中的sendData函数中,具体原因还在排查
    2.如果将回调函数改成
    typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
    typedef std::function<void (const TcpConnectionPtr &)> ConnCompleteCallBack;
    typedef std::function<void (const TcpConnectionPtr &)> ConnCloseCallBack;
    typedef std::function<void (const TcpConnectionPtr &)> WriteCompleteCallBack;
    typedef std::function<void (Buffer*, const TcpConnectionPtr &)> MessageCallBack;
    作者回复

    C++的模板太强大了,不过也很复杂,加油~

    2020-03-29 20:23:30

  • yusuf

    2019-11-26 11:31:58

    // add event read for the new connection
    struct channel *channel1 = channel_new(connected_fd, EVENT_READ, handle_read, handle_write, tcpConnection);

    请问这里第4个参数设置了handle_write函数,为什么第2个参数没有设置EVENT_WRITE呢?
    原本以为这个地方是漏掉了EVENT_WRITE,可添加上EVENT_WRITE后,发现tcp服务器收到数据后会一直打印,而http服务器响应一次请求后会崩溃。这又是为什么呢?
    作者回复

    这里是向reactor注册了数据可读的事件,注意这个时候缓冲区是没有写入的需求的,如果注册了可写事件,相当于这个事件是肯定会发生的(因为套接字写缓冲区都是空的,可以往里写),所以这个时候你会看到一直会打印。

    也就是说,只有在真正有数据需要发送的时候,才需要注册EVENT_WRITE,让reator驱动把需要发送的数据发送完。

    2019-12-01 15:32:48

  • 功夫熊猫

    2022-07-30 22:06:26

    谢谢老师,目前已经照着老师的思路重新拿c语言实现了一遍。然后现在准备利用go的channel,interface和协程重构一遍,有些语言上的支持感觉会方便很多。我自己现在准备往老师的思路里添加时间轮或时间堆来增加这个框架的功能。
  • 范龙dragon

    2020-07-15 00:57:07

    哪位大神回答下,框架中哪里有释放tcp_connection和channel资源的地方,从代码中看到这两个对象都是malloc出来的,但没找到在哪里free的,求指教!
    作者回复

    我的锅,应该在tcp_connection_shutdown函数里面释放channel和tcp_connection两个对象。抽空改下。

    2020-07-16 21:59:21

  • 范闲

    2020-03-27 17:12:22

    https://gitee.com/willam-dialogue/net_reactor
    作者回复

    可以的, C++版本看起来更清爽,本专栏用C语言也是为了减轻大家对语言的负担。

    2020-03-29 20:22:56

  • songber

    2020-03-26 19:03:37

    老师,好像您封装的这个框架跟netty神似
    作者回复

    嗯,应该说基于事件分发机制的框架,大致的思想都是相同的,不信你可以再去看看ACE, libevent或者其他的库。

    2020-03-29 20:05:10

  • 范闲

    2020-03-22 18:45:36

    老师你好,最近我在做这部分的C++代码的改造,但是再改造过程中有几个疑问点需要像您请教下。
    1.关于channel的设计,channel 对象里面定义了一个void *data用来转换成EventLoop指针,在channel_write_event_enable 和 channel_write_event_disable用来改变事件的状态,而且内部的实现都是调用了EventLoop的event_loop_update_channel_event这个函数。为什么不直接将loop 指针透传呢?
    2.关于TcpConnection的设计里面的一个成员是EventLoop指针,在tcp_connection_new中会调用channel_new, channel_new的赋值成了tcpConnection, 即data = ttcpConnection。但是在调用channel_write_event_enable 转换成的是EventLoop指针,所以在TcpConnection中EventLoop放第一个是为了这个调用?
    3.在Reactor中,做事件分发的是Acceptor,但是在我们这个代码里实际做事件分发的应该是TcpServer啊~handle_connection_established中分发到了不同的线程上
    作者回复

    1.应该直接透传了event_loop指针的,不是太明白你的问题;
    2.保留event_loop对象到tcp_connection里面,是为了后面将这个connection对应的事件和event_loop绑定,就是你说的那部分理解;
    3.handle_connection_established就是acceptor线程所要干的事情,也就是你说的从线程池event_loop数组里面获取一个去处理。

    2020-03-29 20:19:12

  • Ray_h

    2019-11-11 19:35:00

    非常感谢老师的付出!前面基础篇和提升篇的课程可以很快消化。实践篇里面的内容我则需要花比较多的时间去梳理对象之间的关系,然后才能弄清楚运行时各个对象之间的联系。正如前面有同学说老师虽然是用C语言写的代码,但是处处是面向对象的思想。
    我认为tcp-server与http-server是基类和子类关系;channel和acceptor也是基类与子类的关系。当然里面还存在大量的包含关系。tcp-connection类继承自channel,但是tcp-connection与eventloop的关系我就不是很确定。还想请老师或者其他同学能够指点迷津,最终很想将老师的c代码改写成c++风格,希望能跟各位同学相互讨论。email: leihao22@126.com
    作者回复

    非常支持c++改造,可以贴出代码地址大家一起review。

    2019-11-17 19:42:57

  • Running man

    2022-09-02 01:42:44

    event_loop_channel_buffer_nolock中申请channelElement并添加到eventLoop链表中,但代码中并没有看到释放该对象堆栈资源,这里是不是一处内存泄露?应该需要在event_loop_handle_pending_channel中去处理释放这部分内存吧?
  • 有手也不行

    2021-09-14 22:34:45

    为什么在发送数据时,会先尝试通过 socket 直接发送,再由框架接管呢?
    老师关于这个问题我还有一点问题就是,假如此时这个channel没有注册write事件,那么我们直接通过write发送内容,但是此时的数据量很大,只发送了一部分,那么框架将接管剩余没发送的数据的,将之注册到channel中,那么我们在接收端收到的数据不就成了两部分了,接收端如何连接这两部分(或者说如何辨认这两部数据是属于一个完整信息的呢)
    作者回复

    第一个问题,文章中已经解释了,为了性能。

    第二个问题,代码可以说明一切。(把没有发送的数据拷贝到缓冲区中)
    f (!fault && nleft > 0) { //拷贝到Buffer中,Buffer的数据由框架接管 buffer_append(output_buffer, data + nwrited, nleft); if (!channel_write_event_is_enabled(channel)) { channel_write_event_enable(channel); } }

    2021-10-07 11:11:42

  • 小家伙54

    2021-03-27 17:15:14

    老师,您最后的服务器代码是那个lib文件夹里的内容吗?我刚开始学这方面的内容,不知道怎么运行,您能说一下吗?
    作者回复

    看一下README。整个工程使用CMake编译。

    2021-03-28 20:21:08

  • Steiner

    2021-02-17 20:53:23

    如果用C++编写的话,这个channel_map可以用map<int, channel>对象来表示 fd 与 channel的映射吗
    作者回复

    可以用std:map的。

    2021-02-28 20:55:23

  • Geek_de83f6

    2021-01-21 02:48:43

    代码看了两周终于看完了,没感觉架构哪里好,把挺简单的事情写的这么复杂。。。
    作者回复

    额,仁者见仁,智者见智吧。个人觉得架构的好处是,可以快速进行业务逻辑的编写。

    2021-01-23 22:30:58

  • 刘忽悠

    2020-07-12 04:50:32

    直接往socket发送是不是就是线程池里面业务线程处理完成之后,在线程里面直接write,这样做的话有一部分I/O业务相当于在业务线程里面发送,没有进行分离;交给框架去做的话,那么I/O就全部交给负责I/O的线程去处理,清晰一些;不过在业务线程里面直接发,效率高一些,也能理解,所以先去尝试直接发送,不成功再交给框架去处理,不知道理解对不对
    作者回复

    基本正确吧。

    2020-07-16 21:52:52