导语 | 现如今提起网络大家的第一反应就是epoll,而实际工程开发中绝大部分的情况都会优先考虑采用已有的一些开源网络框架来做功能的开发。网络框架不同的语言有不同的实现,例如java中大名鼎鼎的netty,再比如c++中的libevent、boost::asio、muduo等,golang中目前在开源社区比较有影响力的网络框架有gnet、evio、netpoll这几个。在之前研究完gnet后差不多一年多的时间了,近期机缘巧合又抽空研究了下netpoll的源码实现。在此总结一篇源码分析的文章,方便日后回顾。

netpoll是一款开源的golang编写的高性能网络框架(基于Multi-Reactor模型),旨在用于处理rpc场景,详细的介绍可参见下图介绍。

下面将为大家详细分析其内部的源码实现逻辑。

一、Reactor模型简介

我们在开始netpoll框架的源码分析前,方便大家阅读源码有一个更好的体验,先简单的回顾下网络编程中的Reacor模型吧。目前很多主流的网络框架都会采用经典的Reactor模型来进行框架内部的实现。而Reactor模型中用的最频繁的就属Multi-Reactor了,最基本的Multi-Reactor模型框架如下图所示。

Multi-Reactor模型中根据角色的不同,可以将Reactor分类两类:mainRactor、subReactor。一般mainReactor是一个,而subReactor会有多个。Multi-Reactor模型的原理如下:

mainReactor主要负责接收客户端的连接请求,建立新连接,接收完连接后mainReactor就会按照一定的负载均衡策略分发给其中一个subReactor进行管理。

subReactor会将新的客户端连接进行管理,负责后续该客户端的请求处理。

通常Reactor线程主要负责IO的操作(数据读写)、而业务逻辑的处理会由专门的工作线程来执行。

:此处所指的Reactor,以epoll为例可以简单理解成一个Reactor对应于一个epoll对象,由一个线程进行处理,Reactor线程又称为IO线程。

简单回顾完Multi-Reactor模型的原理后,下面我们进入正式的主题:netpoll网络框架的源码分析

二、 netpoll整体框架

(一)netpoll client和server端的交互过程

netpoll中对client和server都进行了封装,通过netpoll可以快速的创建一个server端程序。同时可以采用其提供的client方法可以和server进行交互。下面是client和server的一个完整的交互过程。

在netpoll中针对server端,它提供了以下几个方法和回调接口它们的功能分别如下:

Serve():启动服务端,监听等待客户端的请求。

OnPrepare():主要做一些初始化、准备的工作,创建连接前回调。

OnConnect():在连接创建后回调。

OnRequest():业务逻辑方法回调,实现业务逻辑异步处理。

(二)netpoll server端内部结构

下面这张图侧重于介绍netpoll中server端的核心逻辑,其实现原理和前面介绍的Multi-Reactor模型基本一致,其中Listener、loadbalance、pollmanager、EventLoop等都是netpoll中核心的概念,下面我们再对其做一一介绍。

Listener:主要用来初始化Listener,内部调用标准库的net.Listen(),然后再封装了一层。具体的实现则是调用socket()、bind()、listen()等系统调用。

EventLoop:框架对外提供的接口,对外暴露Serve()方法来创建server端程序。

Poll: 是抽象出的一套接口,屏蔽底层不同操作系统平台接口的差异,linux下采用epoll来实现、bsd平台下则采用kqueue来实现。

pollmanager:Poll的管理器,可以理解成一个Poll池,也就是一组epoll或者kqueue集合。

loadbalance:负责均衡封装,主要用来从pollmanager按照一定的策略(随机、轮询、最小连接等)选择出来一个Poll实例,一般在客户端初始化完成后,server会调用该接口拿到一个Poll实例,并将新建立的客户端加入到Poll管理。

三、netpoll Server端源码分析

(一) server使用示例

下面是netpoll的一个简单使用示例,从中我们可以看到它对外暴露的api的使用姿势。

从上可以看到采用netpoll只需要三步就可以实现,非常简洁。

创建Listener

创建EventLoop

Serve()

下面就让我们深入内部研究下,它的内部到底是如何实现的吧。

(二)server端启动入口

从上面的示例中可以看出,server端的启动主要是通过EventLoop的Serve方法来实现的,EventLoop实际是一个接口,其内部由eventloop实现。一个EventLoop表示一个server,EventLoop的源码定义如下所示。

下面是eventloop的结构定义及相应的方法实现逻辑

在eventloop中,Serve方法的实现如下,其中主要三步操作:

转换Listener

创建一个server对象

调用server的run()方法

由此来看最核心的就是run()方法的实现了,我们继续研究run()方法的实现。run()方法的实现如下:

通过上面的源码可以看到,run()方法主要完成了server端的FDOperator对象的创建,并对其赋值了OnRead、poll属性,run()方法执行完后,那server端就已经初始化完成了,这里比较重要的逻辑就出现了。

首先第一个就是mainReactor的分配,对应下面的这两行代码。

将server端fd注册到Poll中后,注册了可读事件,可读事件的回调方法则是OnRead(),在OnRead()中,可以看到主要完成的是调用listener对象的Accept()方法接收客户端的建立连接的请求。

但是我们知道两点:

问题1:server端mainReactor接收到的新连接需要分配给subReactors中的一个subReactor进行管理,这部分在哪里实现的呢?

这部分逻辑实际上就在OnRead()的这行方法中内部实现。在下面本章节第3部分我们重点分析该方法内部的逻辑。

问题2:server端需要初始化mainReactor、subReactors,在上述代码中并没有看到呀,这部分工作在哪里做的呢?

这部分在netpoll中是在pollmanager模块实现的,我们在下面本章节第4部分中给大家介绍。

(三)初始化新的client连接

下面是connection的init()方法源码实现,我们继续研究内部的实现逻辑。

从上面可以看到,init()方法中主要完成了给client连接对象connection分配FDOperator、分配读Buffer、写Buffer以及一些回调方法intpus()、inputAck()、outputs()、outputAck()。最后完成后调用register()方法将当前的client连接注册到poll中,此处的poll这也就是subReactor。

(四)全局pollmananger分析

下面我们介绍netpoll中的pollmanager模块,该模块维护了所有的Poll对象,也就是说不管是mainReactor还是subReactors都是维护在pollmanager中的。下面看看它的源码实现:

通过上面的源码可以看到,它里面通过一个[]Poll数组来维护所有的Poll对象,并且所有的初始化逻辑都在init()方法中完成。在上述方法中我们看到了几个陌生的方法:

openPoll(): 根据字面意思来理解,就是打开一个Poll,大概猜测下内部肯定就是调用了epoll_create()方法来实现。

poll.Wait(): 该方法是通过go协程异步调用的,猜想内部肯定是调用epoll的epoll_wait()方法。

具体关于Poll的实现咱们下一节介绍。

(五)Poll(epoll/kqueue)实现分析

首先我们介绍写Poll的定义,Poll是一个接口,内部封装了几个方法,这几个方法一看大家基本上就明白了,以epoll为例的话,这不就是对应于epoll的几个方法嘛。确实是,该接口就是用来屏蔽底层不同平台间网络系统调用的差异。netpoll中对不同平台的封装,分别在不同的文件中实现。epoll封装在poll_default_linux.go文件中,kqueue封装在poll_default_bsd.go文件中。

看完Poll接口的定义后,我们以epoll的实现为例进行源码的分析,kqueue的封装和epoll大同小异,读者对kqueue感兴趣的话可以自行阅读源码。

通过上面的实现,可以看到defaultPoll实现了Poll接口,并且内部通过调用原生的系统调用封装了epoll的3个底层接口epoll_create()、epoll_ctl()、epoll_wait()。打次基本上揭开了netpoll核心部分的源码分析。

(六)处理业务逻辑

在上文本章节第3部分中初始化connection时看到有一个inputs和inputAck的赋值,其中在inputAck中内部就实现了调用业务逻辑处理的回调方法OnRequest(),下面我们分析inputAck()的实现逻辑。

上面是读完输入的数据后,做的逻辑,读取的客户端数据会存放到inputBuffer中,然后接下来异步的调用OnRequest()方法执行业务逻辑,业务逻辑执行完后,可以调用connection的Write()和Flush()方法将要响应给客户端的数据写出去。其实内部实际上是写到缓冲区中,然后当客户端状态达到可写时进行写出。

下面简单再介绍下connection的Write()和Flush()方法的实现。

(七)小结

这一节中重点分析了netpoll server端的源码实现,主要从server的Serve入口开始分析,然后内部介绍了server接收客户端连接的实现逻辑、处理客户端请求的逻辑、以及pollmanager模块逻辑、Poll封装逻辑等内容。实际框架源码内容更多,本文是按照阅读代码的习惯,精简了核心代码进行了介绍。感兴趣的读者看完后可以直接打开项目进行阅读。

四、netpoll client端源码分析

前面在第三节重点分析了netpoll中server端的源码逻辑。netpoll中本身对client也做了一层封装,本节我们再快速的对client端的源码做一些分析。

client端抽象了一个Dialer接口,它继承了net.Dialer接口的api。下面是详细的定义。

从上面可以看到,netpoll对client支持主要包括:tcp协议和unix协议。而udp协议暂时还为支持。tcp协议的话,主要的建立连接就在DialTCP()方法中实现了,下面我们对该方法的实现进行分析。

在dialTCP()中主要调用了internetSocket()方法来创建一个连接,我们重点关注该方法内部的逻辑即可,下面进行分析。

上面是创建链接的核心实现逻辑,从中我们可以看到,最关键的也就是两步:

调用sysSocket()方法,该方法内部通过syscall.Socket()系统调用初始化一个socket描述。

调用netFD的dial()方法,该方法内部主要调用syscall.Connect()方法进行建立链接。

获得netFD后,最后再通过newTCPConnection()方法初始化connection信息,这部分逻辑也就和server端建立连接后调用init()初始化的过程是相同的。

总结

本文主要回顾了网络编程中经典的Multi-Reactor模型,并在此基础上分析了golang网络框架netpoll的server和client核心源码实现逻辑。再源码中主要关注了网络处理的核心逻辑实现。此外由于篇幅有限,本文并未对netpoll中采用的零拷贝、输出输出缓冲区等内容进行分析。读者感兴趣的话可以自行查看源码进行阅读。文章有理解不恰当地方还请大家指正。

参考资料:

1.https://github.com/cloudwego/netpoll.git

2.https://github.com/cloudwego/kitex.git

3.https://github.com/panjf2000/gnet.git

作者简介

文小飞

腾讯后台开发工程师

Cloud9项目组后台开发工程师,3年后台开发经验,熟悉推荐系统后台工作;对网络、存储、分布式共识算法(raft)等技术比较感兴趣。目前在PCG Cloud9项目组,主要负责后台核心模块研发工作。