Netty源码分析(一):基础知识

Netty源码分析主要分两部分:

基础知识

Netty中的Reactor主从模型

首先需要了解一下基本概念

一、同步、异步、阻塞、非阻塞(网上很多说比较乱,还是自己总结一下吧):

首先同步也异步是对与用户空间来说的,java上通常作用与线程;阻塞和非阻塞是对于kernel内核来说的,通常作用与socket。阻塞和非阻塞都属于同步。
同步(Synchronous communication): 需要阻塞等待消息执行返回结果,则为同步,例如线程池的submit()提交方式
异步(Asynchronous communication): 无需等待等待,通过callback回调接受消息则为异步。例如线程池的execute()提交方式。
BIO(blocking I/O): 阻塞IO,其实就是socket中设置为blocking状态

  1. 用户进程调用recvfrom向kernl发送system call
  2. 如果kernel层数据还没准备好,则处于wait_for_data阻塞等待状态
  3. kernel层数据准备好后,执行copy data from kernel to user操作,返回消息给用户;

NIO(non-blocking I/O): 非阻塞IO,其实就是将socket设置为non-blocking状态

  1. 用户进程调用recvfrom向kernl发送system call
  2. 如果kernel层数据还没准备好,会立刻返回消息/状态码
  3. 用户需要主动轮询发送消息,判断kernel层数据是否准备好
  4. kernel层数据准备好后,执行copy data from kernel to uer操作,返回消息给用户;

二、IO多路复用机制

多路复用机制如select/poll/epoll,都是IO的对路复用机制,其实就是同步非阻塞IO;
工作原理:
在一个线程中创建一个selector,然后将多个socket注册到selector中去,然后通过轮询socket,当某个socket有消息到达时,就会通知用户。
Select的缺点:

  1. 单线程监听文件描述符有限,一般默认是1024,数量越大性能越差
  2. 每次轮询需要将大量的fd从用户空间拷贝到内核空间,而且需要遍历整个列表
  3. 轮询的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符的IO操作,那么以后每次轮询都会把这个文件描述符通知给用户进程。

Poll的缺点:
虽然将select的文件描述符改成了链表形式,但是select的三大缺点依然存在。
Poll的缺点就是每次轮询都要将fd文件描述符集合从用户空间copy到内核空间,然后完全遍历,当fd数量多的时候,很消耗性能。
Epoll的多路复用机制:
epll_create:创建epoll对象,每个epoll对象都有一个eventpoll:红黑树+链表
epoll_ctl:将用户事件添加到红黑树中,事件有消息时,会通过callback添加到链表中
epoll_wait:只需要定时轮询检查链表中有没有epItem元素就行了,链表不为空就返回给用户,对于百万计注册用户来说,只需要监听活跃用户就行,因此遍历量很少,想读速度就非常快

Reactor模型:

以下源码摘自:https://blog.csdn.net/prestigeding/article/details/55100075
netty是基于Reactor模型设计的,它有多种结构模型,简单来说就是用来监听端口连接和处理io数据两个作用。Reactor是一个模型,他是基于IO多路复用的机制设计的。
lee大神提到的Reactor三个要素:

  1. Reactor:相当于一个路由,做一些统筹分配工作,负责监听和分配事件,将IO时间分派给对应的Handler。一般主从Reactor模型会将连接和数据处理分开
  2. Acceptor:处理客户端新连接,并分派请求到处理器链中
  3. Handler:将自身与事件绑定,执行非阻塞IO任务,完成channel读入,完成处理业务逻辑,负责将结果写入channel。

单Reactor单线程模型。

Reactor负责多路分离套接字,Accepter处理新连接,然后通过Handler处理数据。这些操作都在一个线程中完成,实现思路就是:

  1. 首先在Reactor中open一个ServerSocketChannel(对应netty中就是NioServerSocketChannel)和一个Selector,然后将socket设置成非阻塞并bind一个端口,通知注册到select中。
ServerSocketChannel  ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("127.0.0.1", 9080));
Selector  selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
  1. 然后select开启while循环select()阻塞遍历获取socket中的感兴趣的事件,如SelectionKey.OP_ACCEPT接受客户端链接请求,SelectionKey.OP_WRITE写事件,SelectionKey.OP_READ读事件等等。如果有,通过Set ops = selector.selectedKeys();获取接收到兴趣点的所有socket集合。
while(true) {
        selector.select(); //如果没有感兴趣的事件到达,阻塞等待
        Set<SelectionKey> ops = selector.selectedKeys();
    ........for循环处理消息.............
  1. 然后for循环中处理相关事件,处理一件移除一件,一面重复:
    (1)如果是客户端链接事件,接受并将socket设置为非阻塞并重新注册为read事件
    (2)如果是读事件,通过clientChannel.read读取流并重新注册为write事件
    (3)如果是写事件,通过clientChannel.write向客户端写数据并重新注册为read事件
for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
    SelectionKey key =  it.next();
    it.remove();
    try {
        if(key.isAcceptable()) { //客户端建立连接
            ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();//这里其实,可以直接使用ssl这个变量
            SocketChannel clientChannel = serverSc.accept();//接受链接请求
            clientChannel.configureBlocking(false);//设置为非阻塞
            clientChannel.register(selector, SelectionKey.OP_READ);//准备读取客户端数据
        } else if (key.isWritable()) { //向客户端发送请求
            SocketChannel clientChannel = (SocketChannel)key.channel();
            ByteBuffer buf = (ByteBuffer)key.attachment();
            buf.flip();
            clientChannel.write(buf);//服务端向客户端发送数据
            clientChannel.register(selector, SelectionKey.OP_READ);
        } else if(key.isReadable()) {  //处理客户端发送的数据
            ByteBuffer buf = ByteBuffer.allocate(1024);
            System.out.println(buf.capacity());
            clientChannel.read(buf);
            buf.put(b);
            clientChannel.register(selector, SelectionKey.OP_WRITE, buf);//注册写事件
        }
    } catch(Throwable e) {
        e.printStackTrace();
        System.out.println("客户端主动断开连接。。。。。。。");
        ssc.register(selector, SelectionKey.OP_ACCEPT);
    }
}

就好比餐厅门口一个接待员,接到客户后,自己又是服务员,去服务客户。因此缺点很明显,单线程无法处理大量数据,容易造成消息加压,处理延迟,也无法满足高并发。(就像门口客户排长队处理不过来)。

单Reactor多线程模型

这个模型是在单线程基础上,将handler处理数据的部分分离出来,然后通过线程池的方式处理,自身只负责链接请求的处理。

  1. 第一步跟单线程一样,open一个ServerSocketChannel以及一个Selector,并将socket设置成非阻塞并注册到Selector中去。区别就是同时创建一个线程池
在Reactor中创建一个线程池对象。
NioReactorThreadGroup  nioReactorThreadGroup = new NioReactorThreadGroup();
线程池对象构造的时候,创建多个线程并start,对应netty中就是在EventExecutor数组中new了很多的NioEventLoop。
public NioReactorThreadGroup(int threadCount) {
		this.nioThreadCount = threadCount;
		this.nioThreads = new NioReactorThread[threadCount];
		for(int i = 0; i < threadCount; i ++ ) {
			this.nioThreads[i] = new NioReactorThread();
			this.nioThreads[i].start(); //构造方法中启动线程,由于nioThreads不会对外暴露,故不会引起线程逃逸
		}
	}
然后通过dispatch方法,将链接发送过来的channel添加到线程中去
public void dispatch(SocketChannel socketChannel) {
    if(socketChannel != null ) {
		next().register(socketChannel);这里next就是为了获取一个线程
	}
}
  1. 开启while循环,selector通过阻塞遍历select()方法获取感兴趣事件。但是区别是在Reactor中只处理接受请求。将读写请求抛到多线程中去处理。
while (true) {
	try {
		selector.select();
		ops = selector.selectedKeys();
	} catch (Throwable e) {
		e.printStackTrace();
		break;
	}
	for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
		SelectionKey key = it.next();
		it.remove();
		try {
			if (key.isAcceptable()) { // 客户端建立连接
				ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();
				SocketChannel clientChannel = serverSc.accept();
				clientChannel.configureBlocking(false);
				nioReactorThreadGroup.dispatch(clientChannel); // 将建立链接的socket抛给线程池去处理
			}
		} catch (Throwable e) {
		}
	}
}
  1. 最后在子线程中开启while循环,不断处理Reactor抛出来的channel事件
while循环不断执行注册和读写事件
while(true) {
	Set<SelectionKey> ops = null;
	try {
		selector.select(1000);
		ops = selector.selectedKeys();
	} catch (IOException e) {
	}
	//处理相关事件
	for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
		SelectionKey key =  it.next();
		it.remove();
		try {
			if (key.isWritable()) {
				SocketChannel clientChannel = (SocketChannel)key.channel();
				ByteBuffer buf = (ByteBuffer)key.attachment();
				buf.flip();
				clientChannel.write(buf);
				clientChannel.register(selector, SelectionKey.OP_READ);
			} else if(key.isReadable()) {
				SocketChannel clientChannel = (SocketChannel)key.channel();
				ByteBuffer buf = ByteBuffer.allocate(1024);
				System.out.println(buf.capacity());
				clientChannel.read(buf);
				buf.put(b);
				clientChannel.register(selector, SelectionKey.OP_WRITE, buf);
			}
		} catch(Throwable e) {
		}
	}
	//注册事件
	if(!waitRegisterList.isEmpty()) {
		....这里简单来说就是从waitRegisterList中获取所有的客户端链接channel并注册到selector中去,同时移除避免重复注册.....
		sc.register(selector, SelectionKey.OP_READ);
		it.remove();
		.......
	}
}

就好比,餐厅门口,一个接待员,接到客户后,然后交给一帮服务员专门去服务。这个相比与第一种,Reactor能专门处理连接也事件响应分发工作,线程池提高业务处理效率。缺点就是,连接和事务响应工作还是在主线程,线程池消息处理完毕后,也是返回到主Reactor去send,如果连接请求太多,百万级别,一个接待员还是接待不过来的,如果再有安全认证,就像客户进门吃饭还得做个防控疫情量体温,那就废了。

多Reactor多线程模型(主从Reactor模型)

这个模型是在单Reactor多线程模型基础上进行改进的:将Reactor分为主main和从sub,相当与netty中的NioEventLoopGroup分为boss和worker一样。boss中只负责处理链接事件请求但是通过线程池处理的,并将链接成功的事件抛到worker中去执行,worker部分跟单Reactor多线程是一样的了。

  1. 在MainReactor中设置开启一个线程池,有线程池来处理客户端的链接请求事件。
// main Reactor 线程池,用于处理客户端的连接请求
private static ExecutorService mainReactor = Executors.newSingleThreadExecutor();
public void run() {
	ServerSocketChannel ssc = null;
	try {
		ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);
		ssc.bind(new InetSocketAddress(DEFAULT_PORT));
		dispatch(ssc);//连接工作交给线程池去处理
	} catch (IOException e) {
	}
}
private void dispatch(ServerSocketChannel ssc) {
	mainReactor.submit(new MainReactor(ssc));
}
  1. 在MainReactor中首先打开selector并将channel注册进去,然后创建subReactor,最后在run方法中开启while循环处理客户端的链接请求并将成功的链接抛到subReactor的线程池中处理
public MainReactor(SelectableChannel channel) {
	selector = Selector.open();
	channel.register(selector, SelectionKey.OP_ACCEPT);
	subReactorThreadGroup = new SubReactorThreadGroup(4);
}
public void run() {
	while (!Thread.interrupted()) {
		Set<SelectionKey> ops = null;
		selector.select(1000);
		ops = selector.selectedKeys();
		// 处理相关事件  
        for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {  
            SelectionKey key = it.next();  
            it.remove();  
            if (key.isAcceptable()) { // 客户端建立连接  
                ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();
                SocketChannel clientChannel = serverSc.accept();  
                clientChannel.configureBlocking(false);  
                subReactorThreadGroup.dispatch(clientChannel); // 转发该请求  
            }
        }
	}
}
  1. 之后在subReactor中线程池中执行处理客户端发来的读写消息,跟单Reactor多线程中的处理方式都是一样的了
    优点就是当大量客户发来请求链接同时还要做一些安全校验时,就可以通过主Reactor的线程池去完成,链接成功后又交给SubReactor的线程池去处理读写操作,这样效率和性能都将大大提高。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值