Netty 学习笔记
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
Netty基于java的NIO核心:缓存区、通道、选择器。这里推荐一本书:java NIO,出版社是O'REILLY。
一、缓存区。
缓冲区的工作与通道紧密联系,通道是I/O传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,你想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在你所提供的缓冲区中。这种在协同对象之间进行的缓冲区数据传递是高效数据处理的关键。
缓冲区的属性:容量(capacity能够容纳的数据元素的最大数量)、上界(limit现存元素的计数)、标记(mark一个备忘位置)、位置(Posistion下一个要被读或写的元素的索引),还有缓冲区操作:翻转、压缩等。
这里只简介那个位置指针就好了,一切从它开始。
初始状态:一个空的字节缓冲区,position指向0
接着我们写入一些数据,例如:
byteBuffer.put(110);
byteBuffer.put(120);
现在的状态是:posistion指向2了,如果再写入数据,那么就会在posistion(这里是2)的位置继续向上存储了。如果你把指针设置为1,那么数据就会从1位置开始写入,原先的数据会被覆盖
如果你需要把数据读出来,那么你需要把指针重置0,然后调用get()方法获取第一个字节,这时候posistion会自动+1,指向1,如此类推直到把全部数据读完。
示例:
1 public static void main(String[] args) { 2 ByteBuffer buffer = ByteBuffer.allocate(10); 3 buffer.put((byte) 110);//posistion->1 4 buffer.put((byte) 120);//posistion->2 5 6 //获取posistion为2的数据,即缓存区第三个元素。 7 System.out.println("byteBuffer get:"+buffer.get()); 8 //翻转,使posistion指向0 9 buffer.flip(); 10 11 //获取posistion为0的数据,即缓存区第一个个元素。此时posistion指向1 12 System.out.println("byteBuffer get:"+buffer.get()); 13 //获取posistion为1的数据,即缓存区第二个个元素。此时posistion指向2 14 System.out.println("byteBuffer get:"+buffer.get()); 15 16 //或者可以直接指定下标 17 System.out.println("byteBuffer get(0):"+buffer.get(0)); 18 System.out.println("byteBuffer get(1):"+buffer.get(1)); 19 20 }
输出结果:
byteBuffer get:0
byteBuffer get:110
byteBuffer get:120
byteBuffer get(0):110
byteBuffer get(1):120
二、通道。
Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的I/O服务。缓冲区则是通道内部用来发送和接受数据的端点。
我们在网络编程中主要用到四个通道:文件通道FileChannel、套接字通道ServerSocketChannel/SocketChannel/DatagramChannel。其中FileChannel和DatagramChannel不在讨论范围。
要想使用ServerSocketChannel和SocketChannel,首先要打开通道,然后绑定端口,连接成功后就可以对Buffer进行读和写的操作了(记住,ServerSocket/Socket是通过inputStream和outputStream对数据进行操作的,而通道的操作对象是缓冲区):
1 public static void main(String[] args) { 2 int serverPort = 8001; 3 ServerSocketChannel serverChannel; 4 try { 5 //打开通道 6 serverChannel = ServerSocketChannel.open(); 7 //ServerSocketChannel本身不提供绑定功能,只能调用它所包装的ServerSocket的方法 8 serverChannel.socket().bind(new InetSocketAddress(serverPort)); 9 } catch (IOException e) { 10 } 11 12 try { 13 SocketChannel clientChannel = SocketChannel.open(); 14 clientChannel.connect(new InetSocketAddress("192.168.21.94",serverPort)); 15 } catch (IOException e) { 16 } 17 18 }
他们继承了WritableByteChannel和ReadableByteChannel接口,从而有了读和写的可能:
1 public interface WritableByteChannel{ 2 public int write(ByteBuffer src) throws IOException; 3 } 4 5 public interface ReadableByteChannel{ 6 public int read(ByteBuffer dst) throws IOException; 7 }
下面展示一个简单的示例:
1 public class ServerChannelTest { 2 3 public static void main(String[] args) { 4 int serverPort = 8001; 5 ServerSocketChannel serverChannel; 6 try { 7 //打开通道 8 serverChannel = ServerSocketChannel.open(); 9 //ServerSocketChannel本身不提供绑定功能,只能调用它所包装的ServerSocket的方法 10 serverChannel.socket().bind(new InetSocketAddress(serverPort)); 11 System.out.println("wait from client……"); 12 SocketChannel accept = serverChannel.accept(); 13 ByteBuffer buffer = ByteBuffer.allocate(10); 14 accept.read(buffer); 15 buffer.flip(); 16 System.out.println("rec from client:"+buffer.get()); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 } 21 22 } 23 24 25 26 public class ClientChannelTest { 27 28 public static void main(String[] args) { 29 int serverPort = 8001; 30 try { 31 SocketChannel clientChannel = SocketChannel.open(); 32 clientChannel.connect(new InetSocketAddress("127.0.0.1",serverPort)); 33 ByteBuffer buffer = ByteBuffer.allocate(10); 34 buffer.put((byte) 1); 35 buffer.flip(); 36 clientChannel.write(buffer); 37 } catch (IOException e) { 38 e.printStackTrace(); 39 } 40 41 } 42 43 }
运行结果:
wait from client……
rec from client:1
最后,通道提供了一种被称为Scatter/Gather的重要新功能。Scatter/Gather是一个简单却强大的概念,它是指在多个缓冲区上实现一个简单的I/O操作。下文是java NIO中对这个功能的描述:
对于一个write操作而言,数据是从几个缓冲区按顺序抽取(称为gather)并沿着通道发送的。缓冲区本身并不需要具备这种gather的能力(通常它们也没有此能力)。该gather过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。
对于一个read操作而言,从通道读取的数据会按顺序被散布(称为scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。
大多数现代操作系统都支持本地矢量I/O。当你在一个通道上请求一个Scatter/Gather操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。
三、选择器(NIO的核心)
选择器提供了选择执行已经就绪的任务的能力,这使得多元I/O成为可能。就绪选择和多元执行使得单线程能够有效率地同时管理多个I/O通道。在c/c++的工具箱中,许多年前就已经有了select()和poll()这两个系统调用可以用了。但对于java来说,就绪选择功能知道JDK1.4才成为可行的方案(所以以前写网络,都是一个连接一个线程)。
为了更好地说明就绪选择,我举个列子:某工厂有5条传送通道,5个工人每人看管一条通道,当有货物从通道上运下来时就把货物装箱。这种方式不易于扩展,而且也十分浪费。对于每新增一条通道都需要一个新的工人,以及其他相关经费,如表格、椅子、纸张(内存、cpu周期、上下文切换)等等。并且当事情变慢下来时,这些资源(以及相关花费)大多数时候都是闲置的。
现在想象一下另一个不同的场景,所有通道都连接到一个窗口,每个通道在这个窗口上都对应着一个槽(缓冲区)可以放置运输过来的物品,每个槽又有一个指示器(选择键 selectionkey),当运输的物品进入时会亮起。同时想象一下这个窗口只有一个工人在看管,他有一个喜欢看《一个工人的自我修养》的嗜好。工人每看一段都会瞄一下窗口的指示灯(调用select方法),来查看一个通道是否有物品在(就绪选择),如果有就工作。工人在通道闲置的时候可以做其他事情,但需要注意的时候又可以进行及时的处理(要尽可能及时的话,“工作”这里不能有延迟或阻塞)。就绪选择的真正价值在于潜在的大量的通道可以同时进行就绪状态的检查。
传统的监控多个socket的java解决方案是为每个socket创建一个线程并使线程可以在read()调用阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了socket监控器,并将java虚拟机的线程调度机制当作了通知机制。这两者本来都不是为了这种目的而设计的。程序员和java虚拟机都为管理所有这些线程的复杂性和性能损耗付出了代价,这在线程数量的增长失控时表现得更为突出。
真正的就绪选择必须由操作系统来做!操作系统的一项最重要的功能就是处理I/O请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使得java代码能够以可移植的方式,请求底层的操作系统提供就绪选择的服务。
下面介绍就绪选择的几个主要成员:选择器(selector)、可选择通道(ServerSocketChannel/SocketChannel)、selectionKey(选择键)。它们之间的关系:选择键封装了可选择通道与选择器之间的注册关系。通道通过调用register方法将自身注册到一个选择器中,并返回一个表示这种注册关系的的标记(selectionkey)。选择键包含了两个比特集,指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
示例:
1 public static void main(String[] args) { 2 try { 3 ServerSocketChannel serverChannel = ServerSocketChannel.open(); 4 serverChannel.socket().bind(new InetSocketAddress(8001)); 5 //设置非阻塞模式 6 serverChannel.configureBlocking(false); 7 Selector selector = Selector.open(); 8 //将通道注册到一个选择器中,这个通道关心的操作是SelectionKey.OP_ACCEPT,即接收连接请求 9 SelectionKey selectionKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT); 10 //一个selectionkey包含着注册通道和选择器,还有通道关心的操作、已准备好的操作 11 SelectableChannel channel = selectionKey.channel(); 12 Selector selector2 = selectionKey.selector(); 13 int interestOps = selectionKey.interestOps(); 14 int readyOps = selectionKey.readyOps(); 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } 18 }
为了进一步了解这种关系,我贴出这几个类的相关接口:
ServerSocketChannel和SocketChannel都继承了SelectableChannel
选择器基础:一个选择器包含三个集合:一个是键集,包含所有注册到该选择器的通道(通过keys()获取);第二个是就绪集合,包含键集中所有已经准备好要执行的通道(通过selectedkeys()获取);第三个已取消的集合,取消某个通道,选择器不会立刻执行销毁操作,而是先放到这个已取消集合中,等待下一次进行选择操作(select())的时候再清除。
下面展示一个示例,一个客户端创建多个通道来与服务器端通信,服务器端监听后回写信息(遇到不明白的先把疑惑放进心里,后面会详细说明):
1 public class DemoServer { 2 3 /** 4 * @Return void 5 * 6 */ 7 public static void main(String[] args) { 8 9 try { 10 //服务器监听配置 11 Selector selector = Selector.open(); 12 ServerSocketChannel serverChannel = ServerSocketChannel.open(); 13 serverChannel.configureBlocking(false); 14 serverChannel.socket().bind(new InetSocketAddress(8001)); 15 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 16 17 while(true) { 18 //进行就绪查询,超时时间500毫秒 19 if(selector.select(500) > 0) { 20 //获取已经准备好操作的通道 21 Set<SelectionKey> selectedKeys = selector.selectedKeys(); 22 Iterator<SelectionKey> iterator = selectedKeys.iterator(); 23 while(iterator.hasNext()) { 24 SelectionKey next = null; 25 try { 26 next = iterator.next(); 27 iterator.remove();//需要手动删除 28 SelectableChannel channel = next.channel(); 29 if(channel instanceof ServerSocketChannel) { 30 //接收远程客户端连接,并设置为非阻塞模式,他们关心的操作是读和写 31 SocketChannel accept = ((ServerSocketChannel) channel).accept(); 32 accept.configureBlocking(false); 33 accept.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); 34 }else { 35 SocketChannel socketChannel =(SocketChannel)channel; 36 if((next.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { 37 //如果准备好的操作是读操作 38 ByteBuffer byteBuffer = ByteBuffer.allocate(10); 39 // int total = 0,size = 0; 40 // //尝试最多从该通道中读取 r 个字节,其中 r 是调用此方法时缓冲区中剩余的字节数,即 byteBuffer.remaining()。 41 // while((size = socketChannel.read(byteBuffer)) != -1) { 42 // total += size; 43 // } 44 socketChannel.read(byteBuffer); 45 byteBuffer.flip(); 46 System.out.println("rev from socketChannel:"+byteBuffer.get()); 47 byteBuffer.flip(); 48 //将接收到的信息返回给客户端 49 socketChannel.write(byteBuffer); 50 }else if((next.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE){ 51 //如果准备好的操作是写操作 52 System.out.println("write ready"); 53 }else { 54 System.out.println("ERROR Channel:"+channel.getClass().getSimpleName()+",readyOps:"+next.readyOps()); 55 } 56 } 57 } catch (Throwable t) { 58 next.cancel(); 59 System.err.println(t.getMessage()); 60 } 61 62 } 63 } 64 // if(selector.keys().size() == 1) { 65 // System.out.println("channel all cancel"); 66 // break; 67 // } 68 //dosomething 69 } 70 } catch (IOException e) { 71 e.printStackTrace(); 72 } 73 74 } 75 /** 76 rev from socketChannel:1 77 write ready 78 write ready 79 rev from socketChannel:2 80 write ready 81 write ready 82 rev from socketChannel:3 83 write ready 84 write ready 85 write ready 86 rev from socketChannel:4 87 write ready 88 write ready 89 write ready 90 write ready 91 rev from socketChannel:5 92 write ready 93 write ready 94 rev from socketChannel:6 95 write ready 96 write ready 97 write ready 98 write ready 99 rev from socketChannel:7 100 write ready 101 write ready 102 write ready 103 write ready 104 write ready 105 write ready 106 write ready 107 write ready 108 rev from socketChannel:8 109 write ready 110 write ready 111 write ready 112 write ready 113 write ready 114 write ready 115 write ready 116 write ready 117 write ready 118 rev from socketChannel:9 119 write ready 120 write ready 121 write ready 122 write ready 123 write ready 124 …… 125 …… 126 远程主机强迫关闭了一个现有的连接。 127 远程主机强迫关闭了一个现有的连接。 128 远程主机强迫关闭了一个现有的连接。 129 远程主机强迫关闭了一个现有的连接。 130 远程主机强迫关闭了一个现有的连接。 131 远程主机强迫关闭了一个现有的连接。 132 远程主机强迫关闭了一个现有的连接。 133 远程主机强迫关闭了一个现有的连接。 134 远程主机强迫关闭了一个现有的连接。 135 136 */ 137 }
1 public class DemoClient { 2 3 /** 4 * @Return void 5 * 6 */ 7 public static void main(String[] args) { 8 Selector selector; 9 try { 10 selector = Selector.open(); 11 //建立10个客户端通道 12 for(int i=0;i<10;i++) { 13 registerAndconnect(selector,(byte) i); 14 } 15 while(true) { 16 if(selector.select(500) > 0) { 17 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 18 while(iterator.hasNext()) { 19 try { 20 SelectionKey next = iterator.next(); 21 iterator.remove(); 22 SocketChannel channel = (SocketChannel)next.channel(); 23 ByteBuffer byteBuffer = ByteBuffer.allocate(10); 24 channel.read(byteBuffer); 25 byteBuffer.flip(); 26 System.out.println("rev from serverChannel:"+byteBuffer.get()); 27 next.cancel(); 28 }catch (Throwable t) { 29 t.printStackTrace(); 30 } 31 } 32 } 33 if(selector.keys().size() == 0) { 34 System.out.println("channel all cancel"); 35 break; 36 } 37 } 38 } catch (IOException e) { 39 e.printStackTrace(); 40 } 41 } 42 43 private static void registerAndconnect(Selector selector,byte num) { 44 try { 45 SocketChannel socketChannel = SocketChannel.open(); 46 socketChannel.connect(new InetSocketAddress("127.0.0.1",8001)); 47 socketChannel.configureBlocking(false); 48 socketChannel.register(selector, SelectionKey.OP_READ); 49 ByteBuffer byteBuffer =ByteBuffer.allocate(10); 50 byteBuffer.put(num).flip(); 51 socketChannel.write(byteBuffer); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } 55 } 56 /** 57 * 58 rev from serverChannel:5 59 rev from serverChannel:6 60 rev from serverChannel:7 61 rev from serverChannel:2 62 rev from serverChannel:4 63 rev from serverChannel:3 64 rev from serverChannel:1 65 rev from serverChannel:9 66 rev from serverChannel:8 67 channel all cancel 68 69 */ 70 }
四、Netty的链式处理
Netty使用一个boss线程和数个worker线程来处理所有请求,boss线程负责socket的连接请求,然后把连接通道分配给某个worker线程,worker线程负责处理其他的业务请求。相对于传统的一个socket一个线程的处理方式,这种方式无疑极大地减少了线程的数量和多个线程之间切换所需的上下文资源。那么,如果一个线程负责大量的socket,会不会造成严重的延迟呢?
一般地,Netty会生成当前服务器的cpu核数*2个worker线程,以一个大型多人在线游戏服务器为例,可以想象如此少的线程处理成千上万个连接会是多么艰巨的任务。关键就在于上文所介绍的NIO,每个worker线程管理着大量的连接通道,通过调用选择器的select()方法,可以及时获取当前需要处理的通道,然后遍历所有这些通道进行处理。处理的流程如下:
可以看到,当处理一个通道的请求的时候,netty采用的是一条由上而下和一条由下而上的流水线模式。当服务器端收到一个通道请求,netty会按顺序调用Up_handler1、Up_handler2……,最后业务处理完毕,需要写回信息的时候,会按顺序调用Down_handler1、Down_handler2……这些handler都是在初始化netty的时候,用户自己添加上去的。
然而,如果在处理的过程中,某个操作需要大量时间,那么就大大地会影响整个网络服务器的性能,所以,为了减少延迟,遍历每个通道并处理的过程中不能存在耗费大量时间的操作。一般的做法是,handler负责一些简单的工作,如解包压包还有一些验证之类的工作,验证过后使用一个队列保存请求包就立刻返回,请求包就由业务的其他线程来处理。