大白话之 从netty的到gent

6 篇文章 0 订阅
1 篇文章 0 订阅

背景

最近想用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 模型的一个实现,这句话很重要。
在这里插入图片描述
流程:

  1. Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件。
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor 。
  3. subreactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理。
  4. 当有新事件发生时, subreactor 就会调用对应的handler处理。
  5. handler 通过read 读取数据,分发给后面的worker 线程处理。
  6. worker 线程池分配独立的worker 线程进行业务处理,并返回结果。
  7. handler 收到响应的结果后,再通过send 将结果返回给client。
  8. 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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值