为什么要学Netty?
其实我们每学一样东西,就要了解学这个的必要性。那么为什么要学Netty呢。 其实但凡涉及网络通信就必然离不开网络编程。Netty目前作为JAVA网络编程最热门的框架,毫不夸张的说是Java程序员必备的技能之一。
为什么说学好Netty很有必要呢?
- Netty基于NIO(NIO)是一种同步非阻塞的I/O模型,在Java1.4中引入NIO)。使用Netty可以极大地简化了TCP和UDP套接字服务器等网络编程,摒弃性能以及安全性等很多方面都非常优秀。
- 我们平常经常接触的Dubbo、RocketMQ、Elasticsearch、grpcSpark等等热门开源项目都用到了Netty。
- 大部分微服务框架底层涉及网络通信的部分都是基于Netty来做的,比如说Spring Cloud生态系统中网关Spring Cloud Gateway。
IO模型是什么
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO NIO AIO
同步异步阻塞非阻塞
同步:两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。比如在 A -> B 事件模型中,你需要先完成 A 才能执行B。再换句话说,同步调用中被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
异步: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用是一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情,
阻塞:就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
再用很流行的水壶的比喻来讲解下
1 老皮把水壶放到火上,立等水开。(同步阻塞)
老皮觉得自己有点傻
2 老皮把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)老皮还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。3 老皮把响水壶放到火上,立等水开。(异步阻塞)
老皮觉得这样傻等意义不大
4 老皮把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老皮觉得自己聪明了。
总结
所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老皮水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老皮效率的低下。
所谓阻塞非阻塞,仅仅对于老皮而言。
立等的老皮,阻塞;看电视的老皮,非阻塞。
BIO | NIO | AIO | |
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
BIO
同步阻塞模型,一个客户端连接对应一个处理线程。
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。
其实这也是所有使用多线程的本质:
- 利用多核。
- 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
现在的多线程一般都使用线程池 ,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:
- 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系
统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。 - 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,
然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行
的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以
上),导致系统几乎陷入不可用的状态。 - 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程
数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大
量阻塞线程从而使系统负载压力过大。
所以,当 面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的 。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
BIO的代码例子
server
public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待连接。。"); Socket socket = serverSocket.accept(); //阻塞方法 System.out.println("有客户端连接了。。"); new Thread(new Runnable() { @Override public void run() { try { handler(socket); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } private static void handler(Socket socket) throws IOException { System.out.println("thread id = " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("准备read。。"); //接收客户端的数据,阻塞方法,没有数据可读时就阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read完毕。。"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); System.out.println("thread id = " + Thread.currentThread().getId()); } socket.getOutputStream().write("HelloClient".getBytes()); socket.getOutputStream().flush(); }}
client
public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("localhost", 9000); //向服务端发送数据 socket.getOutputStream().write("HelloServer".getBytes()); socket.getOutputStream().flush(); System.out.println("向服务端发送数据结束"); byte[] bytes = new byte[1024]; //接收服务端回传的数据 socket.getInputStream().read(bytes); System.out.println("接收到服务端的数据:" + new String(bytes)); socket.close(); }}
server端输出
client输出
NIO
同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理。
I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现,他们的区别如下表:
select | poll | epoll(jdk1.5以上) | |
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 线性 O(n) | 线性O(n) | 事件通知方式 O(1) |
最大链接 | 有上限 | 有上限 | 无上限 |
1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
3、selector 可以对应一个或多个线程
4、NIO 的 Buffer 和 channel 都是既可以读也可以写
自我总结:
channel 是双向的
selector 里面先是注册服务端的ServerSocketChannel再通过accept注册客户端的socketchannel并且关心channel的read或者write事件,方便下次selector循环。
NIO服务端程序详细分析:
1、创建一个 ServerSocketChannel 和 Selector ,并将 ServerSocketChannel 注册到 Selector 上
2、 selector 通过 select() 方法监听 channel 事件,当客户端连接时,selector 监听到连接事件, 获取到 ServerSocketChannel 注册时绑定的 selectionKey
3、selectionKey 通过 channel() 方法可以获取绑定的 ServerSocketChannel
4、ServerSocketChannel 通过 accept() 方法得到 SocketChannel
5、将 SocketChannel 注册到 Selector 上,关心 read 事件
6、注册后返回一个 SelectionKey, 会和该 SocketChannel 关联
7、selector 继续通过 select() 方法监听事件,当客户端发送数据给服务端,selector 监听到read事件,获取到 SocketChannel 注册时绑定的 selectionKey
8、selectionKey 通过 channel() 方法可以获取绑定的 socketChannel
9、将 socketChannel 里的数据读取出来
10、用 socketChannel 将服务端数据写回客户端
总结:NIO模型的selector 就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理
NIO相对于BIO非阻塞的体现就在,BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据线程就要阻塞,NIO把等待客户端操作的事情交给了大总管 selector,selector 负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。还有就是 channel 的读写是非阻塞的。
Redis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端。
代码实例:
server
public class NIOServer { //public static ExecutorService pool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws IOException { // 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式 ServerSocketChannel ssc = ServerSocketChannel.open(); //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式 ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(9000)); // 创建一个选择器selector Selector selector = Selector.open(); // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { System.out.println("等待事件发生。。"); // 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的 int select = selector.select(); System.out.println("有事件发生了。。"); // 有客户端请求,被轮询监听到 Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); //删除本次已处理的key,防止下次select重复处理 it.remove(); handle(key); } } } private static void handle(SelectionKey key) throws IOException { if (key.isAcceptable()) { System.out.println("有客户端连接事件发生了。。"); ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞 //处理完连接请求不会继续等待客户端的数据发送 SocketChannel sc = ssc.accept(); sc.configureBlocking(false); //通过Selector监听Channel时对读事件感兴趣 sc.register(key.selector(), SelectionKey.OP_READ); } else if (key.isReadable()) { System.out.println("有客户端数据可读事件发生了。。"); SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件 int len = sc.read(buffer); if (len != -1) { System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else if (key.isWritable()) { SocketChannel sc = (SocketChannel) key.channel(); System.out.println("write事件"); // NIO事件触发是水平触发 // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件, // 在有数据往外写的时候再注册写事件 key.interestOps(SelectionKey.OP_READ); //sc.close(); } }}
client
public class NioClient { //通道管理器 private Selector selector; /** * 启动客户端测试 * * @throws IOException */ public static void main(String[] args) throws IOException { NioClient client = new NioClient(); client.initClient("127.0.0.1", 9000); client.connect(); } /** * 获得一个Socket通道,并对该通道做一些初始化的工作 * * @param ip 连接的服务器的ip * @param port 连接的服务器的端口号 * @throws IOException */ public void initClient(String ip, int port) throws IOException { // 获得一个Socket通道 SocketChannel channel = SocketChannel.open(); // 设置通道为非阻塞 channel.configureBlocking(false); // 获得一个通道管理器 this.selector = Selector.open(); // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调 //用channel.finishConnect();才能完成连接 channel.connect(new InetSocketAddress(ip, port)); //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * * @throws IOException */ public void connect() throws IOException { // 轮询访问selector while (true) { selector.select(); // 获得selector中选中的项的迭代器 Iterator it = this.selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); // 删除已选的key,以防重复处理 it.remove(); // 连接事件发生 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); // 如果正在连接,则完成连接 if (channel.isConnectionPending()) { channel.finishConnect(); } // 设置成非阻塞 channel.configureBlocking(false); //在这里可以给服务端发送信息哦 ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes()); channel.write(buffer); //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件 } else if (key.isReadable()) { read(key); } } } } /** * 处理读取服务端发来的信息 的事件 * * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException { //和服务端的read方法一样 // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(512); int len = channel.read(buffer); if (len != -1) { System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); } }}
AIO
异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
应用场景:
AIO方式适用于连接数目多且连接比较长(重操作) 的架构,JDK7 开始支持
AIO其实可以通过代码去理解就行,AIO是基于NIO所写,所以整体差不多。
AIO代码示例:
server//服务端代码public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000)); serverChannel.accept(null, new CompletionHandler() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try { // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端 serverChannel.accept(attachment, this); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer, buffer, new CompletionHandler() { @Override public void completed(Integer result, ByteBuffer buffer) { buffer.flip(); System.out.println(new String(buffer.array(), 0, result)); socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer buffer) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); Thread.sleep(Integer.MAX_VALUE); }}
client//客户端代码public class AIOClient { public static void main(String... args) throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get(); socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes())); ByteBuffer buffer = ByteBuffer.allocate(512); Integer len = socketChannel.read(buffer).get(); if (len != -1) { System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); } }}
Netty第一篇文章就是这些了,主要是了解各个模型的原理,为了理解Netty做准备。