《netty权威指南》学习笔记
《netty实战》
传统的BIO编程(多线程版)
- 客户端:Socket
服务端:ServerSocket
采用BIO通信模型的服务端,通常创建一个 独立的Acceptor线程负责监听客户端连接,它接收到客户端请求之后为每一个客户端创建一个新的线程进行链路处理,通过输出流返回应答给客户端,线程销毁。这是典型的一请求一应答通信模型。
Server代码
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port:" + port);
Socket socket = null;
while (true) {
socket = server.accept(); //阻塞,一直等待有连接
new Thread(new TimeServerHandler(socket)).start();
}
} catch (Exception e) {
// e.printStackTrace();
} finally {
//关闭资源(略)
}
弊端
服务端的线程个数和客户端的并发访问数呈1:1的正比关系。当并发数上升后, 系统的性能将急剧下降,最终导致服务宕机或者僵死,无法正常提供服务。
伪异步I/O编程
- 客户端:Socket
- 服务端:ServerSocket
- 线程池: ThreadPool
采用线程池和任务队列实现一种伪异步I/O通信框架。
当有新的客户端接入时,将客户端的Socket封装成一个Task(实现Runnable接口),投递到后端的线程池进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列大小和最大线程数,因此资源是可控的,无论多少并发请求,都不会造成资源耗尽和宕机。
线程池代码
public class TimeServerHandlerExecutePool {
private ExecutorService executor ;
public TimeServerHandlerExecutePool(int maxPoolSize ,int queueSize) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task) {
executor.execute(task);
}
}
server代码
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port:" + port);
Socket socket = null;
TimeServerHandlerExecutePool pool = new TimeServerHandlerExecutePool(80, 1000);
while (true) {
socket = server.accept(); //阻塞,一直等待有连接
// new Thread(new TimeServerHandler(socket)).start();
pool.execute(new TimeServerHandler(socket));
}
} catch (Exception e) {
// e.printStackTrace();
} finally {
//关闭资源(略)
}
弊端
伪异步I/O仅仅是对之前的I/O线程模型的一个简单优化,它无法从根本上解决同步I/O导致的通信阻塞问题。
如何解决这个难题?下一节NIO将给出答案。
NIO编程
NIO到底是什么简称? 有人称之为New IO,原因它相对于之前的I/O库是新增的,这也是官方的叫法。但是,相对于之前老的I/O类库是阻塞的,更多的人喜欢称之为非阻塞IO(Non-block I/O).
NIO涉及到一些基本概念
- 缓冲区Buffer
- 通道Channel
- 多路复用Selector
Buffer
buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。
这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
一个Buffer有三个属性是必须掌握的:
容量(Capacity)
作为一块内存,buffer有一个固定的大小,叫做capacity容量。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。
位置(Position)
当写入数据到Buffer的时候需要中一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1.
当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。
上限(Limit)
在写模式,limit的含义是我们所能写入的最大数据量。它等同于buffer的容量。
一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。
数据读取的上限时buffer中已有的数据,也就是limit的位置(原position所指的位置)。
利用Buffer读写数据,通常遵循四个步骤:
- 把数据写入buffer;
- 调用flip;
- 调用buffer.clear()或者buffer.compact()
Channel
java NIO Channel通道和流非常相似,主要有以下几点区别:
- 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
- 通道可以异步读写。
通道总是基于缓冲区Buffer来读写。
下面列出Java NIO中最重要的集中Channel的实现:FileChannel 从文件中读写数据。
- DatagramChannel 能通过UDP读写网络中的数据。
- SocketChannel 能通过TCP读写网络中的数据。
- ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
Selector多路复用选择器
Selector 是Java 的非阻塞 I/O 实现的关键。它使用了事件通知 API以确定在一组非阻塞套接字中有哪些已经就绪能够进行 I/O 相关的操作。
因为可以在任何的时间检查任意的读操作或者写操作的完成状态,一个单一的线程便可以处理多个并发的连接。
Server
package cn.jhs.chap02.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
/**
* @author: jhs
* @desc:
* @date: Create in 2018/5/15 17:22
*/
public class MultiplexerTimerServer implements Runnable {
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
public MultiplexerTimerServer(int port) throws IOException {
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.bind(new InetSocketAddress(port), 1024);
selector = Selector.open();
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port:" + port);
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
selector.select(1000); //阻塞
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//处理新接入的请求
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
//有可读数据
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length;
while ((length = sc.read(buffer)) > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The time server receive order:" + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc, currentTime);
}
}
}
}
private void doWrite(SocketChannel sc, String response) throws IOException {
if (response != null && !response.isEmpty()) {
byte[] bytes = response.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
sc.write(buffer);
}
}
}
Client
关于NIO的学习,推荐并发编程网的nio专题相关内容:java-nio-all
总结
NIO编程相比较于BIO的难度大了很多,代码复杂度也增加了许多,为什么它的应用却越来越广泛呢?使用NIO的优点总结如下:
- 客户端发起的连接操作是异步的,可以通过多路复用选择器OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞
- SocketChannel的读写操作是异步的,如果没有可读写的数据它不会同步等待,直接返回。
线程模型优化:由于JDK的Selector在Liunx等主流操作系统上通过epoll实现,没有连接句柄的限制,这意味着一个selector线程可以同时处理成千上万个客户端连接,且性能不会随着客户端连接数的增加而线性下降。
目前支持I/O多路复用的系统调用有select,pselect,poll,epoll,在linux网络编程过程中,很长一段时间都是用select做轮询和网络事件通知,然而select的一些固有缺陷(如:单个进程打开的Socket描述符(FD)有一定的限制,它由FD_SETSIZE设置,默认为1024),linux不得不在内核版本中寻找select的替代方案,最终选择了epoll,它没有这个限制。
很多人喜欢将NIO称之为异步非阻塞I/O,但是严格按照UNIX网络编程模型和JDK的实现进行划分,它只能诚挚为非阻塞I/O,不能称之为异步非阻塞I/O.它是通过selector的轮询来监控是否有事件发生,所以并不是异步的。
AIO编程(NIO2.0)
在JDK1.7之后提供的NIO2.0新增了异步套接字通道,它是真正的异步I/O,在异步操作的时候可以传递信号变量,当操作完成之后会回调相关方法,异步I/O也被成为AIO.它不需要使用多路复用器(selector)对注册通道进行轮询操作即可实现异步操作的读写,简化了NIO的编程模型。
异步通道通过如下两种方式获取操作结果:
- 通过java.util.current.Future类表示异步操作的结果
- 在执行异步操作的时候传入一个java.nio.channels.CompletionHandler接口的实现类作为操作完成的回调。
AIO编程主要涉及到的类:
- AsynchronousServerSocketChannel
- AsynchronousSocketChannel
- CompletionHandler
Server的创建
package cn.jhs.chap02.aio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
/**
* @author: jhs
* @desc:
* @date: Create in 2018/5/15 21:24
*/
public class AsyncTimeServerHandler implements Runnable {
private int port;
CountDownLatch latch;
AsynchronousServerSocketChannel asynchronousServerSocketChannel;
public static void main(String[] args) {
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(8080);
new Thread(timeServer,"AsyncTimeServerHandler-thread-01").start();
}
public AsyncTimeServerHandler(int port) {
this.port = port;
try {
asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("The server is start in port:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAccept() {
asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
}
}
//accept连接CompletionHandler
class AcceptCompletionHandler implements java.nio.channels.CompletionHandler<java.nio.channels.AsynchronousSocketChannel, AsyncTimeServerHandler> {
@Override
public void completed(AsynchronousSocketChannel result, AsyncTimeServerHandler attachment) {
//因为一个asynchronousServerSocketChannel可以处理成千上万个客户端连接请求,所以在这里继续调用它,来接收新的客户端连接
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
attachment.latch.countDown();
}
}
//读取数据CompletionHandler
class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel result) {
if (this.channel == null) {
this.channel = result;
}
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
String req = new String(body, "utf-8");
System.out.println("The time server receive order :"+req);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
byte[] bytes = currentTime.getBytes();
final ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
channel.write(buffer, buffer, new WriteCompletionHandler(channel));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//写取数据CompletionHandler
class WriteCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public WriteCompletionHandler(AsynchronousSocketChannel result) {
if (this.channel == null) {
this.channel = result;
}
}
@Override
public void completed(Integer result, ByteBuffer attachment) {
if (attachment.hasRemaining()) {
channel.write(attachment, attachment, this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
不同IO模型对比
Netty
Netty的环境搭建
直接引入netty的maven依赖即可(这里使用5.0.0.Alphal版本):
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha1</version>
</dependency>
</dependencies>
Netty实现Server
下面给出Netty实现服务器的Demo,这里只写代码实现,原理留作以后再详谈。
Server
package cn.jhs.chap03;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.util.Date;
/**
* @author: jhs
* @desc:
* @date: Create in 2018/5/20 14:09
*/
public class TimeServer {
public static void main(String[] args) {
new TimeServer().bind(8080);
}
public void bind(int port) {
//配置服务端的NIO 线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
//Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128。
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildHandler());
//绑定端口,同步等待成功
ChannelFuture future = b.bind(port).sync();
//等待监听服务端-监听端口关闭
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
class ChildHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeServerHandler());
}
}
@ChannelHandler.Sharable
class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("The time server receive order:" + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// super.exceptionCaught(ctx, cause);
ctx.close();
}
}
Client
package cn.jhs.chap03;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
/**
* @author: jhs
* @desc:
* @date: Create in 2018/5/21 9:12
*/
public class TimeClient {
public static void main(String[] args) {
new TimeClient().connect("localhost",8080);
}
public void connect(String host, int port) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = b.connect(host, port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
@ChannelHandler.Sharable
class TimeClientHandler extends ChannelHandlerAdapter {
private final ByteBuf firstMsg;
public TimeClientHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
this.firstMsg = Unpooled.copiedBuffer(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().writeAndFlush(firstMsg);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] resp = new byte[buf.readableBytes()];
buf.readBytes(resp);
String body = new String(resp, "UTF-8");
System.out.println("Now is:" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
总结
通过Netty开发的NIO服务端和客户端非常简单,短短几十行代码就能实现NIO程序需要几百行才能完成的功能。基于Netty的应用不仅API简单, 而且扩展性和定制性非常好。之后我们就要开启Netty的旅程了。