Tinynet框架简述

2023/02/15 tinynet 共 2107 字,约 7 分钟

网络模式概述

在网络编程早期的时候使用 Process Per Connection,即一个进程一个连接;后面又使用更轻量的线程去替代进程,即 thread pre connection,为了解决线程不断的创建销毁的问题,使用了线程池尽可能的节约资源。但依然存在很多问题,比如一个连接如果没有可读数据,read系统调用就会阻塞,即使是使用了non-blocking,也需要一直轮询检测是否有可读数据。

I/O 多路复用这时候就派上了用场,epollpollselect等系统调用会阻塞直到有感兴趣的事件发生,就可以用一个线程监视所有连接。

Rector模式(one loop per thread),主循环负责监听是否有事件发生,一旦有感兴趣的事件发生,执行对应的回调函数。Reactor模式在执行回调函数的时候不能阻塞过久,否则客户端容易失去响应,一般采用非阻塞IO

为了解决同步中出现的阻塞问题,Proactor应运而生,是一种完全的异步框架。

由于项目中使用的是多Reactor模式,就不过多阐述Proactor,但目前协程 + Proactor应该会成为一种趋势

Reactor模型实现

EventLoopClass

Reactor模式由EventLoop类实现,框架如图,主要包括以下几个类

  • Channel,负责注册和响应一个文件描述符fd的感兴趣的事件,不负责关闭该fd,生命期由拥有者例如EventLoop控制。
  • poller,一个EventLoop使用一个poller,通过调用底层的epoll相关函数监听关心的事件,生命期和EventLoop相同
  • timers, 对定时任务的包装,包括超时时间,超时回调函数等,生命期由TimerQueue控制
  • TimerQueue,定时器需要快速选择出超时任务,因此需要选择有序数据结构,又希望可以快速删除节点任务,因此这里选用了底层基于红黑树的set

主要的类EventLoop是主体是一个循环(one loop per thead),在监测到事件发生的时候执行事件,如图;

EventLoop

一开始程序阻塞在Epoll类的poll成员函数,即等待关心的事件发生,一旦事件发生,执行activeChannel中的任务,即感兴趣的事件的回调函数,最后执行用户的任务。

可以想到的是,如果一直没有事件发生,那么用户任务就一直无法执行。为了解决这个问题,需要把程序从poll阻塞唤醒:关心一个自定义的fd的可读事件,当向EventLoop中添加任务的时候,写入该fd数据,这样就不会阻塞太久导致用户任务迟迟无法执行。

Tcp网络库

TcpServer

网络库部分TcpServer组成大致如图,包括以下类

  • InetAddress,包装sockaddr_in,封装与之相关的系统调用
  • Socket,包装sockfd,在析构函数中关闭fd,即利用RAII管理资源,并且封装与之相关的系统调用
  • Buffer,输入输出缓冲区,在非阻塞编程中接收和发送信息必不可少。底层利用2个指针在一段内存上移动控制可读可写区域。
  • TcpConnection,负责管理一个连接,一个TcpConnection活动在给定的loop中,通过设置各种读写回调函数来实现对消息的接收和发送,消息存放在inputBufferoutputBuffer中,在接收到消息的后,把inputBuffer传给回调函数处理,在发送的时候,把发送的数据传递给outputBuffer
  • Acceptor,用于acceptTCP连接,并通过回调通知使用者。
  • EventLoopThreadPool,多Reactor模型

最后TcpServer是供用户直接使用的,生命期由用户控制。通过acceptor接收新连接的时候调用回调函数产生TcpConnection。每一个连接对应一个TcpConnection实例。消息的发送和接收都通过这个实例完成。

如果只是以sockfd做为连接的标识,容易出现串话的可能性,即前一个关闭了一个连接A的sockfd,后面又马上创建了连接B,使用了和A相同的sockfd,如果在其他程序处理的时候完成了这种偷梁换柱,比如接收了A的消息,正在处理消息,准备发送消息的时候,还以为现在的B是之前的A,就把消息发给了B。

把连接直接抽象成一整个类隐含一个好处,避免串话的可能性,因为当连接结束后,这个TcpConneciton就会被析构,那么下一个连接过来的时候就会产生新的连接,之前的消息什么就都没有了,就不会出现串话的可能性

下图为一个Tcpserver产生连接发送数据到最后销毁连接的大致流程:

server

首先是设置线程数目,然后启动EventLoop线程池,调用Acceptor的成员函数listen进行监听,当客户端连接的时候,调用Acceptor的回调函数,在TcpServer中会产生TcpConnection实例,这个实例就代表这一条连接;之后通过EventLoop进行数据的发送和接收。在读取到0字节的时候关闭连接,Tcpserver在回调函数中移除该连接的实例,实例会最终被析构

TcpClientTcpServer基本一致,除了connector在连接建立之后就会析构,而Acceptor生命期和Tcpserver一样。

Search

    Table of Contents