大白话之 从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/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
XGBoost(eXtreme Gradient Boosting)是一种非常流行的机器学习算法,它是一种梯度提升树模型。它的设计目标是提高其前身GBDT(Gradient Boosting Decision Tree)算法的性能和鲁棒性。 XGBoost使用的是一种特殊的决策树模型,称为CART(Classification and Regression Trees)。与传统的决策树不同,CART决策树在每个节点上进行分裂时,会使用一种称为泰勒展开的方法,来近似地找到最优分裂点。通过这种方法,XGBoost能够更精确地构建决策树模型,并提高预测的准确性。 XGBoost还通过引入正则化技术,如L1和L2正则化,来避免模型过拟合。正则化可以限制模型的复杂性,提高模型的泛化能力,并使得模型对噪音数据不敏感。 在训练过程中,XGBoost使用梯度提升算法,该算法通过迭代地训练多个决策树,并使用梯度下降法来优化模型的损失函数。在每一轮迭代中,XGBoost会根据之前模型的预测结果和真实标签之间的误差,调整每个样本的权重,并生成一个新的决策树。通过这种迭代优化的方式,XGBoost能够逐步提升模型的准确性。 此外,XGBoost还具备优化性能的功能。它使用一种称为并行化的技术,通过同时在多个处理器上训练多个决策树,来加快训练速度。另外,XGBoost还支持特征重要性评估,可以通过计算每个特征对模型的贡献度来帮助我们理解数据的特征重要性。 总之,XGBoost是一种非常强大的机器学习算法,它通过使用特殊的决策树模型、正则化技术、梯度提升算法和优化性能等方法,提高了模型的预测准确性和鲁棒性。它在很多数据竞赛和实际应用中都取得了出色的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值