写在开头:Netty是个什么玩意?这里摘抄官网的一段话:Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。
总结一句话:Netty是用于开发客户端、服务端通信系统的一套框架。
我们学习Netty能用来干什么呢?或者说有哪些我们常用的东西使用Netty开发的呢?举两个例子:Hadoop和dubbo,Hadoop底层使用netty做通信架构的,dubbo底层也是用netty写的。netty在中间件系统开发中用的特别多,可能你会认为我用不到啊,我平时都是SofaMVC或者SpringMVC,Mybatis,Spring一类的,然后基本上都是和数据库打交道,天天crud。对,说的没错,但是你别忘了,互联网的技术本质其实就是通信,你操作数据库其实也是在不断和数据库在通信,而且随着系统规模增大,大到现有的框架已经不能满足要求了,那么你就需要开发自己的通信系统。
在没有Netty之前我们是怎么开发通信系统的,记得以前做过一个项目,是替有关部门做一个车辆监控系统,每个车上面有个GPS设备(与卫星通信的专业设备),他们希望能够监控车辆的运行状态。当时在做服务端的时候用的是BIO框架,也就是阻塞IO,采用一端一线程来解决,在开发的时候遇到了很多坑,比如说客户端(GPS模块)与服务端链接超时,网络闪断,板报读写,网络拥塞等,并且一台机器的线程数有限,还好当时只监控500辆车,当时遇到的最恶心的是GPS这个模块他们用了一个厂家做的嵌入式设备,因为我以前是开发单片机的,对着还比较了解,当时他们的协议栈有问题,虽然客户端与服务器连接断了,但是服务器这边还是现实连接着,后来发现是他们通过基站连接着服务器,总之,特别多的问题。基本上好多代码都不是去解决业务本身而且在构建一个通信框架,这当然不是我想要的。
再后来,使用了NIO框架,好处不用多说,单机支撑上万线程,采用linux的enpoll模型,多路io复用,但是在采用原生的NIO框架开发通信系统的时候,依旧大量代码用于构建通信框架而不是业务本身。并且一直崩溃,不稳定,这其实也是NIO框架的问题所在,主要有以下几点:
1. NIO的类库和API繁杂。
2. 需要具备其他的额外技能做铺垫。
3. 可靠性能力不齐,工作了和难度大。例如:客户端断线重连,网络闪断,半包读写,失败缓存,网络拥塞,异常码流等。
4. JDK的BUG,例如:epoll的bug会导致selector空轮询,导致CPU 100%,该问题到JDK1.7版本还是一直存在。
废话不多说,先看一个简单的采用原生NIO框架开发的服务器例子(简单)。
package com.dlb.note.server.nio; import java.io.Closeable; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.nio.charset.Charset; import java.util.Iterator; /** * <li> * <b>nio做服务端开发的问题:</b> * 1. NIO的类库和API繁杂。 * 2. 需要具备其他的额外技能做铺垫。 * 3. 可靠性能力不齐,工作了和难度大。例如:客户端断线重连,网络闪断,半包读写,失败缓存,网络拥塞,异常码流等。 * 4. JDK的BUG,例如:epoll的bug会导致selector空轮询,导致CPU 100%,该问题到JDK1.7版本还是一直存在。 * </li> * * 功能:nio服务端 * 版本:1.0 * 日期:2016/12/21 11:38 * 作者:馟苏 */ public class NioServer { // 本地字符集 private static final String LocalCharsetName = "gbk"; // 缓冲区大小 private static final int Buffer_Size=1024; /** * 主函数 * @param args */ public static void main(String []args) throws Exception { Selector selector = null; ServerSocketChannel serverSocketChannel = null; try { selector = Selector.open(); // 开启IO多路复用轮询 serverSocketChannel = ServerSocketChannel.open(); // 打开服务器channel serverSocketChannel.configureBlocking(false); // 配置为非阻塞 serverSocketChannel.socket().bind(new InetSocketAddress(8888)); // 绑定服务器本地地址和端口 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 在多路复用轮询器上注册操作 System.out.println("服务器在8888端口监听"); while (selector.select() > 0) { // 如果没有事件发生,那么在这里阻塞 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); // 获取键的迭代器 while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); // 移除key,防止多次遍历 try { /** * 处理 */ handler(key); } catch (Exception e) { e.printStackTrace(); // 打印远程客户端的ip SocketChannel channel = (SocketChannel) key.channel(); System.out.println(channel.socket().getRemoteSocketAddress()); // 取消注册,关闭客户端的channel key.cancel(); closeConnects(key.channel()); } System.out.println("处理结束"); } } } catch (Exception e) { e.printStackTrace(); } finally { try { closeConnects(serverSocketChannel, selector); } catch (Exception e) { e.printStackTrace(); } } } /** * 处理 * @param key */ private static void handler(SelectionKey key) throws Exception { if(key.isAcceptable()) { // 客户端连接到来 SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); // 客户端注册为读事件并且设置与该键关联的附加对象 clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(Buffer_Size)); } else if(key.isReadable()) { // 收到客户端消息 // 获得与客户端通信的信道 SocketChannel clientChannel = (SocketChannel) key.channel(); System.out.println(key.hashCode()); // 获取此键的附加对象 ByteBuffer buffer = (ByteBuffer) key.attachment(); buffer.clear(); // 读取信息获得读取的字节数 long bytesRead = clientChannel.read(buffer); if(bytesRead == -1) { // 打印远程客户端的ip SocketChannel channel = (SocketChannel) key.channel(); System.out.println(channel.socket().getRemoteSocketAddress()); // 取消注册,关闭客户端的channel key.cancel(); closeConnects(clientChannel); } else { // 将缓冲区准备为数据传出状态 buffer.flip(); // 将获得字节字符串(使用Charset进行解码) String receivedString = Charset.forName(LocalCharsetName).newDecoder().decode(buffer).toString(); // 控制台打印出来 System.out.println("接收到信息:" + receivedString); // 准备发送的文本 String sendString = "你好,客户端. 已经收到你的信息" + receivedString; // 将要发送的字符串编码(使用Charset进行编码)后再进行包装 buffer = ByteBuffer.wrap(sendString.getBytes(LocalCharsetName)); // 发送给客户端 clientChannel.write(buffer); // 设置为下一次读取或是写入做准备 // key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } } } /** * 关闭连接 * @param closeables */ public static void closeConnects(Closeable ...closeables) throws Exception { if (closeables == null) { return; } for (Closeable c : closeables) { c.close(); } } }
看完之后什么感觉,是不是特别恶心,对吧,写个简单的接受字符串程序写了这么多代码,而且也没有解决半包等问题,这岂不是要疯的节奏,那么如何解决这个问题?Netty这个犹如救世主的框架给我们带来了曙光!预知后事如何,待我下回分解。
参考书籍《netty权威指南》