Netty简单介绍(非原创)
文章大纲
一、netty基础介绍
二、netty代码实战
三、项目源码下载
四、参考文章
一、netty基础介绍
1. 简介
官方定义为:”netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端”
2. 主要特性
netty有很多重要的特性,主要特性如下:
(1)优雅的设计
(2)统一的api接口,支持多种传输类型,例如oio,nio
(3)简单而强大的线程模型
(4)丰富的文档
(5)卓越的性能
(6)拥有比原生java api 更高的性能与更低的延迟
(7)基于池化和复用技术,使资源消耗更低
(8)安全性
(9)完整的ssl/tls以及starttls支持
(10)可用于受限环境,如applet以及osgi
netty的以上特性,比较适合客户端数据较大的请求/处理场景,例如web服务器等,要想知道有哪些系统使用了netty,可以参考:http://netty.io/wiki/adopters.html
3. 主要名词介绍
3.1 bio(blocking io):阻塞io
早期的java api(java.net)提供了由本地系统套接字库提供的所谓的阻塞函数,样例代码如下:
serversocket serversocket = new serversocket(portnumber); socket clientsocket = serversocket.accept(); bufferedreader in = new bufferedreader(new inputstreamreader(clientsocket.getinputstream())); printwriter out =new printwriter(clientsocket.getoutputstream(), true); string request, response; while ((request = in.readline()) != null) { if ("done".equals(request)) { break; } response = processrequest(request); out.println(response); }
这段代码片段将只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端 socket 创建一个新的 thread,线程模型如下图所示:
该种模型存在以下两个问题:
(1)在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费
(2)需要为每个线程的调用栈都分配内存
(3)即使 java 虚拟机(jvm) 在物理上可以支持非常大数量的线程, 但是远在到达该极限之前, 上下文切换所带来的开销就会带来麻烦
3.2 nio(non blocking io):非阻塞io
java的nio特性在jdk 1.4中引入,其结构如下:
从该图可以看出selector 是java 的非阻塞 i/o 实现的关键。它使用了事件通知 api以确定在一组非阻塞套接字中有哪些已经就绪能够进行 i/o 相关的操作。因为可以在任何的时间检查任意的读操作或者写操作的完成状态。该种模型下,一个单一的线程便可以处理多个并发的连接。 与bio相比,该模型有以下特点:
(1)使用较少的线程便可以处理许多连接,因此也减少了内存管理和上下文切换所带来开销
(2)当没有 i/o 操作需要处理的时候,线程也可以被用于其他任务
虽然java 的nio在性能上比bio已经相当的优秀,但是要做到如此正确和安全并
不容易。特别是,在高负载下可靠和高效地处理和调度 i/o 操作是一项繁琐而且容易出错的任务,此时就是netty上场的时间了
4. netty好处
(1)使用多路复用技术,提高处理连接的并发性
(2)零拷贝:
a. netty的接收和发送数据采用direct buffers,使用堆外直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝
b. netty提供了组合buffer对象,可以聚合多个bytebuffer对象进行一次操作
c. netty的文件传输采用了transferto方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题
d. 内存池:为了减少堆外直接内存的分配和回收产生的资源损耗问题,netty提供了基于内存池的缓冲区重用机制
e. 使用主从reactor多线程模型,提高并发性
f. 采用了串行无锁化设计,在io线程内部进行串行操作,避免多线程竞争导致的性能下降
g. 默认使用protobuf的序列化框架
h. 灵活的tcp参数配置
详细说明,可参考: http://www.infoq.com/cn/articles/netty-high-performance#anch111813
二、netty代码实战
在本节中,我们将前面讲解nio编程时的时间服务案例,改成用netty来实现。timeclient发送“query time order”请求,timeserver接受到这个请求后,返回当前时间。
1. 新建maven项目
创建后项目结构如下:
2. pom.xml文件添加依赖
<?xml version="1.0" encoding="utf-8"?> <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <groupid>com.wxc</groupid> <artifactid>netty-test</artifactid> <version>1.0-snapshot</version> <dependencies> <dependency> <groupid>io.netty</groupid> <artifactid>netty-all</artifactid> <version>4.0.28.final</version> <scope>compile</scope> </dependency> </dependencies> </project>
3. 编写服务端代码
3.1 创建timeserver.java
时间服务器timeserver在8080端口监听客户端请求,如下
package server; import io.netty.bootstrap.serverbootstrap; import io.netty.channel.channelfuture; import io.netty.channel.channelinitializer; import io.netty.channel.eventloopgroup; import io.netty.channel.nio.nioeventloopgroup; import io.netty.channel.socket.socketchannel; import io.netty.channel.socket.nio.nioserversocketchannel; import io.netty.handler.codec.linebasedframedecoder; import io.netty.handler.codec.string.stringdecoder; public class timeserver { private int port=8080; public void run() throws exception { eventloopgroup bossgroup = new nioeventloopgroup(); // (1) eventloopgroup workergroup = new nioeventloopgroup(); try { serverbootstrap b = new serverbootstrap(); // (2) b.group(bossgroup, workergroup) .channel(nioserversocketchannel.class) // (3) .childhandler(new channelinitializer<socketchannel>() { // (4) @override public void initchannel(socketchannel ch) throws exception { ch.pipeline().addlast(new linebasedframedecoder(1024)); ch.pipeline().addlast(new stringdecoder()); ch.pipeline().addlast(new timeserverhandler()); } }); channelfuture f = b.bind(port).sync(); // (5) system.out.println("timeserver started on 8080..."); f.channel().closefuture().sync(); } finally { workergroup.shutdowngracefully(); bossgroup.shutdowngracefully(); } } public static void main(string[] args) throws exception { new timeserver().run(); } }
说明:
1、首先我们创建了两个eventloopgroup实例:bossgroup和workergroup,目前可以将bossgroup和workergroup理解为两个线程池。其中bossgroup用于接受客户端连接,bossgroup在接受到客户端连接之后,将连接交给workergroup来进行处理。
2、接着,我们创建了一个serverbootstrap实例,从名字上就可以看出来这是一个服务端启动类,我们需要给设置一些参数,包括第1步创建的bossgroup和workergroup。
3、我们通过channel方法指定了nioserversocketchannel,这是netty中表示服务端的类,用于接受客户端连接,对应于java.nio包中的serversocketchannel。
4、我们通过childhandler方法,设置了一个匿名内部类channelinitializer实例,用于初始化客户端连接socketchannel实例。在第3步中,我们提到nioserversocketchannel是用于接受客户端连接,在接收到客户端连接之后,netty会回调channelinitializer的initchannel方法需要对这个连接进行一些初始化工作,主要是告诉netty之后如何处理和响应这个客户端的请求。在这里,主要是添加了3个channelhandler实例:linebasedframedecoder、stringdecoder、timeserverhandler。其中linebasedframedecoder、stringdecoder是netty本身提供的,用于解决tcp粘包、解包的工具类。
linebasedframedecoder在解析客户端请求时,遇到字符”\n”或”\r\n”时则认为是一个完整的请求报文,然后将这个请求报文的二进制字节流交给stringdecoder处理。
stringdecoder将字节流转换成一个字符串,交给timeserverhandler来进行处理。
timeserverhandler是我们自己要编写的类,在这个类中,我们要根据用户请求返回当前时间。
5、在所有的配置都设置好之后,我们调用了serverbootstrap的bind(port)方法,开启真正的监听在8080端口,接受客户端请求。
3.2 创建timeserverhandler.java
timeserverhandler用户处理客户端的请求,每当接收到"query time order”请求时,就返回当前时间,否则返回"bad request”。
package server; import io.netty.buffer.bytebuf; import io.netty.buffer.unpooled; import io.netty.channel.channelhandlercontext; import io.netty.channel.channelinboundhandleradapter; import java.util.date; public class timeserverhandler extends channelinboundhandleradapter { @override public void channelread(channelhandlercontext ctx, object msg) throws exception { // 1 string request = (string) msg; //2 string response = null; if ("query time order".equals(request)) { // 3 response = new date(system.currenttimemillis()).tostring(); } else { response = "bad request"; } response = response + system.getproperty("line.separator"); // 4 bytebuf resp = unpooled.copiedbuffer(response.getbytes()); // 5 ctx.writeandflush(resp); // 6 } }
说明:
1、timeserverhandler继承了channelinboundhandleradapter,并覆盖了channelread方法,当客户端发送了请求之后,channelread方法会被回调。参数channelhandlercontext包含了当前发送请求的客户端的一些上下文信息,msg表示客户端发送的请求信息。
2、我们直接msg强制转换成了string类型。这是因为我们在前面已经添加过了stringdecoder,其已经将二进制流转换成了一个字符串
3、构建响应。会判断请求是否合法,如果请求信息是"query time order”,则返回当前时间,否则返回"bad request”
4、在响应内容中加上了system.getproperty("line.separator”),也就是所谓的换行符。在linux操作系统中,就是”\n”,在windows操作系统是”\r\n”。加上换行符,主要是因为客户端也要对服务端的响应进行解码,当遇到一个换行符时,就认为是一个完整的响应。
5、调用了unpooled.copiedbuffer方法创建了一个缓冲区对象bytebuf。在java nio包中,使用bytebuffer类来表示一个缓冲区对象。在netty中,使用bytebuf表示一个缓冲区对象。在后面的章节中,我们会对bytebuf进行详细讲解。
6、调用channelhandlercontext的writeandflush方法,将响应刷新到客户端
4. 编写客户端代码
4.1 创建timeclient .java
timeclient负责与服务端的8080端口建立连接
public class timeclient { public static void main(string[] args) throws exception { string host = "localhost"; int port = 8080; eventloopgroup workergroup = new nioeventloopgroup(); try { bootstrap b = new bootstrap(); // (1) b.group(workergroup); // (2) b.channel(niosocketchannel.class); // (3) b.handler(new channelinitializer<socketchannel>() {// (4) @override public void initchannel(socketchannel ch) throws exception { ch.pipeline().addlast(new linebasedframedecoder(1024)); ch.pipeline().addlast(new stringdecoder()); ch.pipeline().addlast(new timeclienthandler()); } }); // start the client. channelfuture f = b.connect(host, port).sync(); // (5) // wait until the connection is closed. f.channel().closefuture().sync(); } finally { workergroup.shutdowngracefully(); } } }
说明:
1、首先我们创建了一个bootstrap实例,与serverbootstrap相对应,这表示一个客户端的启动类
2、我们调用group方法给bootstrap实例设置了一个eventloopgroup实例。前面提到,eventloopgroup的作用是线程池。前面在创建serverbootstrap时,设置了一个bossgroup,一个wrokergroup,这样做主要是为将接受连接和处理连接请求任务划分开,以提升效率。对于客户端而言,则没有这种需求,只需要设置一个eventloopgroup实例即可。
3、通过channel方法指定了niosocketchannel,这是netty在nio编程中用于表示客户端的对象实例。
4、类似server端,在连接创建完成,初始化的时候,我们也给socketchannel添加了几个处理器类。其中timeclienthandler是我们自己编写的给服务端发送请求,并接受服务端响应的处理器类。
5、所有参数设置完成之后,调用bootstrap的connect(host, port)方法,与服务端建立连接。
4.2 创建timeclienthandler.java
timeclienthandler主要用于给server端发送"query time order”请求,并接受服务端响应。
public class timeclienthandler extends channelinboundhandleradapter { private byte[] req=("query time order" + system.getproperty("line.separator")).getbytes(); @override public void channelactive(channelhandlercontext ctx) {//1 bytebuf message = unpooled.buffer(req.length); message.writebytes(req); ctx.writeandflush(message); } @override public void channelread(channelhandlercontext ctx, object msg) throws exception { string body = (string) msg; system.out.println("now is:" + body); } }
说明:
1、timeclienthandler继承了channelinboundhandleradapter,并同时覆盖了channelactive和channelread方法。
2、当客户端与服务端连接建立成功后,channelactive方法会被回调,我们在这个方法中给服务端发送"query time order”请求。
3、当接受到服务端响应后,channelread方法会被会回调,我们在这个方法中打印出响应的时间信息。
创建后项目结构如下:
5. 项目运行与访问
5.1 运行服务端
5.2 运行客户端
三、项目源码下载
链接:https://pan.baidu.com/s/1v7qur0ycwsckdpaad-tzhq
提取码:0nyf
四、参考文章
下一篇: Java面试必问通信框架NIO,原理详解