文章大纲

一、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

四、参考文章

最新文章

  1. 【.net 深呼吸】设置序列化中的最大数据量
  2. 关于C#使用Dllimport 导入vc++动态库后网站部署提示 “无法加载 DLL,找不到指定模块”的解决方法。
  3. CSS 布局入门
  4. Netty writeAndFlush() 流程与异步
  5. hasOwnProperty,in
  6. 利用反射将Datatable、SqlDataReader转换成List模型
  7. 详述Linux ftp命令的使用方法
  8. [Everyday Mathematics]20150116
  9. iOS开发RunTime之函数调用
  10. SQL注入(二)
  11. jap页面获取struts2中action中变量的值
  12. permissions is only granted to system apps 错误
  13. Django REST framework 中的序列化器
  14. android:layout_margin真实含义 及 自己定义复合控件 layout()运行无效的问题解决
  15. TCPDUMP学习笔记。
  16. js 毫秒转换为标准时间
  17. CLR寄宿和AppDomain
  18. untiy 2d游戏平面直角坐标系的旋转应用
  19. Docker常用命令汇总,和常用操作举例
  20. Docker 简单运用

热门文章

  1. Linux while和for循环简单分析
  2. ubuntu 16.04 安装QT问题
  3. OpenCV实现灰度直方图和直方图拉伸
  4. MFC和OpenCV结合
  5. swiper和Navigator组件
  6. webpack学习(三)
  7. Java中String类的常用方法
  8. WebLogic的服务搭建
  9. kernel panic必备知识
  10. tesuto-Mobius