背景
最近想用gnet上层+http协议实现一个http服务器
原生的go net库有什么不好呢?
原生的net库已经很优秀了,充分利用了GMP模型,但是原生的模型是goroutine-pre-connect,一个链接一个携程,当瞬间百万的请求过来就完蛋了。所以面对这种链接多的场景选择用go net库。
先说说netty吧
写gnet的作者也说了,gnet is an event-driven networking framework that is fast and lightweight. It makes direct epoll and kqueue syscalls rather than using the standard Go net package and works in a similar manner as netty and libuv, which makes gnet achieve a much higher performance than Go net.应该是参考了netty,所以我在也是先看了一下netty
首先来说Netty 是 Reactor 模型的一个实现,这句话很重要。
流程:
- Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件。
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor 。
- subreactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理。
- 当有新事件发生时, subreactor 就会调用对应的handler处理。
- handler 通过read 读取数据,分发给后面的worker 线程处理。
- worker 线程池分配独立的worker 线程进行业务处理,并返回结果。
- handler 收到响应的结果后,再通过send 将结果返回给client。
- Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor
上面是标准的主从reactor,下面是netty怎么根据主从reactor来实现的。
我们一个一个来说
boss group 和 work group
首先是boss group 和 work group 其实就是对应着主reactor 和 子reactor ,一个是 bossGroup, 用于处理客户端的连接请求; 另一个是 workerGroup, 用于处理与各个客户端连接的 IO 操作
因为主reactor只有一个所以boss group就是1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
在你只是监听一个端口的情况下你设置多个其实也是浪费。
NioEventLoopGroup其实就是一个包含多个NioEventLoop的数组,一个 NioEventLoop 和一个特定的线程绑定,所以理解为每一个nioEventLoop就是一个线程,那这个线程干什么呢?就要说说什么是event loop 了。
什么是event loop呢,中文翻译为事件循环,怎么讲呢,你雇了一个看门大爷,让他一直在门口巡逻,有事情就通知你来处理,这就是event loop,我理解把这种不断监听事件、有事件就通知别人处理(自己不处理)的工作模式就叫做event loop,比如多路复用的select,他就需要不断监听accept、read、write事件的到来,来了再通知别人处理,这种工作模式就是event loop,简单点你就理解就是一个看门大爷。只会通知什么也不会干。从代码的实现上来看就是一个for循环。
所以这里看就是boss group请了一个大爷,work group请了多个大爷,boss group把接收到的链接分配给 work group中的多个大爷,work group中的多个大爷再不断的监听链接的读写事件,并交给后面做后续处理。
boosgroup 的第一步select、第二步processSelectKeys、第三步runAllTasks、注册新的channel
NioEventLoop 肩负着两种任务, 第一个是作为 IO 线程, 执行与 Channel 相关的 IO 操作, 包括 调用 select 等待就绪的 IO 事件、读写数据与数据的处理等; 而第二个任务是作为任务队列, 执行 taskQueue 中的任务,
第一步select不用说了就是在执行select
第二步processSelectKeys、就是返回了事件、bossGroup的main reactor返回的肯定是accept事件。然后做注册新的channel(下面说)
第三步runAllTasks就是执行 taskQueue 中的任务,比如你有一个需求需要统计每分钟的连接数,就可以把这个任务放到任务队列里。
注册新的channel,我们看到第二步processSelectKeys得到了accept事件,所以bossgroup(主reactor)需要将链接注册到workgroup(子reactor),Channel 是一个 Socket 的抽象, 它为用户提供了关于 Socket 状态(是否是连接还是断开) 以及对 Socket 的读写等操作. 每当 Netty 建立了一个连接后, 都会有一个对应的 Channel 实例.所以往深了说就是Channel 与对应的 workgroup下的某一个EventLoop 关联,如果有多个eventloop就需要通过负载均衡选择一个, Channel 中的所有 IO 操作都是在这个 EventLoop 中执行的; 当关联好 Channel 和 EventLoop 后, 会继续调用底层的 Java NIO SocketChannel 的 register 方法, 将底层的 Java NIO SocketChannel 注册到指定的 selector 中. 说白了就是将链接注册到select中,完成真正的注册。
workGroup的第一步select、第二步processSelectKeys、第三步runAllTasks
第一步select不用说了就是在执行select
第二步processSelectKeys,也还是返回不同的事件,比如有读事件,有写事件,交给谁来处理呢,交给你一个叫pipeline的地方(下面说)
第三步runAllTasks就是执行 taskQueue 中的任务
Pipeline
在实例化一个 Channel 时, 必然伴随着实例化一个 ChannelPipeline,也就是说每一个链接都有一个pipeline,链接过来了没有什么用,重要的是怎么处理,所以每一个链接都进入一个pipeline处理合情合理,我们可以像添加插件一样自由组合各种各样的 handler 来完成业务逻辑. 例如我们需要处理 HTTP 数据, 那么就可以在 pipeline 前添加一个 Http 的编解码的 Handler, 然后接着添加我们自己的业务逻辑的 handler, 这样网络上的数据流就向通过一个管道一样, 从不同的 handler 中流过并处理。
读gnet源码
我也不是java程序员,netty就分析到这里吧,大体的流程已经基本看明白了,剩下的一些基础实现就不深究。来读一读gent的源码吧
从他的官网看有两个模型
· Multiple Reactors
· Multiple Reactors With Groutine Pool
共同点都是主从reactor,区别在于一个在subreactor中把所有事情都干了,一个把业务处理部分放到了一个groutine pool里面。
所以来说我觉得netty和gent都是主从reactor的不同实现。一个爸爸的两个儿子
开始看源码了,
func serve(eventHandler EventHandler, listener *listener, options *Options, protoAddr string) error {
//确定eventloop的数量,一个sub reactor一个eventloop
numEventLoop := 1
if options.Multicore {
numEventLoop = runtime.NumCPU()
}
if options.NumEventLoop > 0 {
numEventLoop = options.NumEventLoop
}
//记得以前我自己写的一个tcp框架的最高类就叫engine,可以理解为这个是gent的引擎,心脏。
eng := new(engine)
eng.opts = options
eng.eventHandler = eventHandler //绑定给引擎绑定事件的处理类
eng.ln = listener
//负载均衡器,因为sub reactor有多个,当main reactor将链接注册到sub reactor需要通过负载均衡器选择其中的一个
switch options.LB {
case RoundRobin:
eng.lb = new(roundRobinLoadBalancer)
case LeastConnections:
eng.lb = new(leastConnectionsLoadBalancer)
case SourceAddrHash:
eng.lb = new(sourceAddrHashLoadBalancer)
}
eng.cond = sync.NewCond(&sync.Mutex{})
if eng.opts.Ticker {
eng.tickerCtx, eng.cancelTicker = context.WithCancel(context.Background())
}
e := Engine{eng}
switch eng.eventHandler.OnBoot(e) {
case None:
case Shutdown:
return nil
}
//启动引擎了、我们看看启动引擎做了什么。
if err := eng.start(numEventLoop); err != nil {
eng.closeEventLoops()
eng.opts.Logger.Errorf("gnet engine is stopping with error: %v", err)
return err
}
defer eng.stop(e)
allEngines.Store(protoAddr, eng)
return nil
}
//激活reactors,看来开始干活了。
func (eng *engine) activateReactors(numEventLoop int) error {
//根据eventloop的数量创建eventloop
for i := 0; i < numEventLoop; i++ {
//p就是poller,对多路复用的封装,我们这里用epoll来说
if p, err := netpoll.OpenPoller(); err == nil {
//eventloop是什么呢、上面说过,一个看门大爷
//type eventloop struct {
// ln *listener 大爷看的门
// idx int 大爷看的门的门牌号
// cache bytes.Buffer
// engine *engine 大爷的老板
// poller *netpoll.Poller 大爷本人
// buffer []byte 大爷的事件记事本
// connCount int32 链接的数量
// udpSockets map[int]*conn 所有udp链接
// connections map[int]*conn 所有tcp链接
// eventHandler EventHandler 真正处理事件的人
}
el := new(eventloop)
el.ln = eng.ln
el.engine = eng
el.poller = p
el.buffer = make([]byte, eng.opts.ReadBufferCap)
el.connections = make(map[int]*conn)
el.eventHandler = eng.eventHandler
//将这个eventloop注册到一个负载均衡器里,分析netty的时候说过,主reactor向子reactor注册时候需要从多个eventloop里找到一个
eng.lb.register(el)
} else {
return err
}
}
//启动sub reactors
eng.startSubReactors()
//启动main reactor
if p, err := netpoll.OpenPoller(); err == nil {
el := new(eventloop)
el.ln = eng.ln
el.idx = -1
el.engine = eng
el.poller = p
el.eventHandler = eng.eventHandler
if err = el.poller.AddRead(eng.ln.packPollAttachment(eng.accept)); err != nil {
return err
}
eng.mainLoop = el
// Start main reactor in background.
eng.wg.Add(1)
go func() {
el.activateMainReactor(eng.opts.LockOSThread)
eng.wg.Done()
}()
} else {
return err
}
// Start the ticker.
if eng.opts.Ticker {
go eng.mainLoop.ticker(eng.tickerCtx)
}
return nil
}
看一下启动sub reactors都做了什么
func (eng *engine) startSubReactors() {
//循环eng.lb(上面eng.lb.register(el)注册进去的)中的eventloop,每一个eventloop一个goroutine,并且goroutine跟操作系统线程绑定
eng.lb.iterate(func(i int, el *eventloop) bool {
eng.wg.Add(1)
go func() {
el.activateSubReactor(eng.opts.LockOSThread)
eng.wg.Done()
}()
return true
})
}
再看看el.activateSubReactor干了什么
func (el *eventloop) activateSubReactor(lockOSThread bool) {
//goroutine与系统线程进行绑定
if lockOSThread {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
}
//大爷真正干的事
//activateSubReactor里面核心的就是执行eventloop里面的poller的polling方法,,polling就是轮询阻塞当前goroutine,等待网络事件,点进polling方法看 其实就是一个for循环,
err := el.poller.Polling(func(fd int, ev uint32) error {
if c, ack := el.connections[fd]; ack {
//不断的监听read 和 write事件。
if ev&netpoll.OutEvents != 0 && !c.outboundBuffer.IsEmpty() {
if err := el.write(c); err != nil {
return err
}
}
if ev&netpoll.InEvents != 0 {
return el.read(c)
}
}
return nil
})
}
有了sub reactors 肯定要有main reactor,看源码main reactor也是只用了一个与系统线程绑定的goroutine。来看最核心的el.activateMainReactor(eng.opts.LockOSThread)
func (el *eventloop) activateMainReactor(lockOSThread bool) {
if lockOSThread {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
}
defer el.engine.signalShutdown()
//看没看到很熟悉的场景,主reactor只负责accept,我们在看看accept连接后是不是注册到sub reactor里面
err := el.poller.Polling(func(fd int, filter int16) error { return el.engine.accept(fd, filter) })
}
func (eng *engine) accept(fd int, _ netpoll.IOEvent) error {
//熟悉的味道,从众多sub reactor里面选择一个,然后将这个链接注册进去,可以理解为从众多的老大爷找一个。
el := eng.lb.next(remoteAddr)
c := newTCPConn(nfd, el, sa, el.ln.addr, remoteAddr)
//
//这个el.register其实就是调用了epoll_ctl方法,把链接交给老大爷让他去监听事件
err = el.poller.UrgentTrigger(el.register, c)
}
当有read事件时候怎么处理,就要看activateSubReactor里面的el.read©
func (el *eventloop) read(c *conn) error {
n, err := unix.Read(c.fd, el.buffer)
if err != nil || n == 0 {
if err == unix.EAGAIN {
return nil
}
if n == 0 {
err = unix.ECONNRESET
}
return el.closeConn(c, os.NewSyscallError("read", err))
}
c.buffer = el.buffer[:n]
//可以看到这里调用了事件处理器的OnTraffic方法
action := el.eventHandler.OnTraffic(c)
switch action {
case None:
case Close:
return el.closeConn(c, nil)
case Shutdown:
return gerrors.ErrEngineShutdown
}
_, _ = c.inboundBuffer.Write(c.buffer)
return nil
}
看到这里是不是感觉一下就通了。。。
参考
https://gnet.host/
https://segmentfault.com/a/1190000007403873/