NIO
程序启动时在第一个断点阻塞等待客户端连接,当有客户端连接接入时,如果没有发生数据读写,将阻塞在第三个断点。
客户端可以通过telnet localhost 8080 ->ctrl+] ->send ss 进行测试
缺点:
1.数据的读取和写都是阻塞的,当且仅当有数据可读、可用数据已读取完毕、发生I/O异常时才会往下执行
2.一个线程对应一个客户端,即使使用线程池控制线程池大小(假设为500),如果500个客户端都不发送数据,那这500个线程将一直被占用(处于闲置状态)
基于Reactor模型统一调度的长连接和短链接协议栈,无论是性能、可靠性还是可维护性,都可以“秒杀”传统基于BIO开发的应用服务器和各种协议栈。
epoll所支持的FD(文件描述符)上线时操作系统的最大文件句柄数,具体的值可以通过cat /proc/sys/fs/filemax查看。
缓冲区Buffer
在面向流的I/O中,可以将数据直接写入或将数据直接读到Stream对象中。在NIO库中,加入了Buffer对象,所有数据都是用缓冲区处理的;在读取数据时,它是直接读到缓冲区中的:在写入数据时,写入到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数据。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。
通道Channel
Channel是一个通道,可以通过它读取和写入数据,它就想自来水管一样,网络数据通过Channel读取和写入。通道与流的不同指出在于通道时双向的,流只是在一个方向上移动(一个流必须时InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时用于读写。因为Channel时全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。Channel可以分委两大类:分别时用于网络读写的SelectableChannel和用于文件操作的FileChannel。
多路复用器Selector
多路复用器提供选择已经就绪的任务的能力。Selector会不断地轮询注册在起上的Channel,如果某个Channel上面有新的TCP连接介入、读和写时间,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
NIO编程优点
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像BIO的客户端那样被同步阻塞。
- SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需熬同步等待这个链路可用。
- 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,他没有连接句柄数限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。
NIO1.0实现
- 服务端
public class TimeServer {
public static void main(String[] args) {
MultiplexerTimeServer server = new MultiplexerTimeServer(8080);
new Thread(server,"NIO-multiplexer-001").start();
}
}
public class MultiplexerTimeServer implements Runnable {
private ServerSocketChannel ssc;
private Selector selector;
private volatile boolean stop;
int port;
public MultiplexerTimeServer(int port) {
try {
//创建多路复用器
selector = Selector.open();
//打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道
ssc = ServerSocketChannel.open();
//绑定监听端口
//1024: requested maximum length of the queue of incoming connections,即最大连接数
ssc.socket().bind(new InetSocketAddress(port), 1024);
//设置连接为非阻塞模式
ssc.configureBlocking(false);
//将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件。
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time Server is start in port:" + port);
} catch (IOException e) {
e.printStackTrace();
}
this.port = port;
}
public void stop() {
this.stop = false;
}
@Override
public void run() {
//轮询准备就绪的Key
while (!stop) {
SelectionKey key = null;
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
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) {
if (selector != null) {
try {
selector.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//处理新接入的请求
if(key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
SocketChannel sc = ssc.accept();
//设置客户端链路为非阻塞模式
sc.configureBlocking(false);
sc.socket().setReuseAddress(true);
//将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作用来读取客户端发送的网络消息
sc.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//异步读取客户端请求消息到缓冲区
int readBytes = sc.read(readBuffer);
if(readBytes>0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("Receive message from client:"+ body);
String res = "hello client";
doWrite(sc,res);
}
}
}
}
private void doWrite(SocketChannel sc, String res) throws IOException {
byte[] bytes = res.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
- 客户端
public class TimeClient {
public static void main(String[] args) {
new Thread(new TimeClientHandler("127.0.0.1",8080),"NIO-TimeClient-001").start();
}
}
public class TimeClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandler(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
//打开SocketChannel,绑定客户端本地地址
socketChannel = SocketChannel.open();
//设置SocketChannel为非阻塞模式
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.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 (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
// 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if (selector != null)
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 判断是否连接成功
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else
System.exit(1);// 连接失败,进程退出
}
if (key.isReadable()) {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//异步读取客户端请求消息到缓冲区
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Receive message from server : " + body);
this.stop = true;
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
//异步连接服务器
private void doConnect() throws IOException {
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else
//异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立
//向Reactor线程的多路复用注册OP_CONNECT状态位,监听服务端的TCP ACK应答
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
private void doWrite(SocketChannel sc) throws IOException {
byte[] req = "Hello Server".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining())
System.out.println("Send order 2 server succeed.");
}
}
NIO2.0引入新的异步通道的概念,并提供了异步文件通道和异步套接字痛殴感到的实现。异步通道提供两种方式获取操作结果。
- 通过java.util.concurrent.Future类来表示异步操作的结果;
- 在执行异步操作的时候 传入一个java.nio.channels。CompletionHandler接口的实现类作为操作完成的回调。
NIO2.0的异步套接字通道是真正的异步非阻塞I/O,它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
NIO2.0实现
- 服务端
public class TimeServer {
public static void main(String[] args) {
new Thread(new AsynTimerServerHandler(8080),"Aio-001").start();
}
}
public class AsynTimerServerHandler implements Runnable {
private int port;
CountDownLatch latch = new CountDownLatch(1);
AsynchronousServerSocketChannel serverChannel;
public AsynTimerServerHandler(int port) {
this.port = port;
try {
serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
System.out.println("The server is start at port: "+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
doAccept();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private void doAccept() {
serverChannel.accept(this,new AcceptCompletionHandler());
}
}
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsynTimerServerHandler> {
@SuppressWarnings("unchecked")
@Override
public void completed(AsynchronousSocketChannel result, AsynTimerServerHandler attachment) {
attachment.serverChannel.accept(attachment,this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer,buffer,new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable t, AsynTimerServerHandler attachment) {
t.printStackTrace();
attachment.latch.countDown();
}
}
public class ReadCompletionHandler implements CompletionHandler<Integer,ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
if(this.channel == null) {
this.channel = channel;
}
}
@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("Receive message from client: "+ req);
doWrite("Hello Client");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String string) {
byte[] bytes = string.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer,writeBuffer,new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if(buffer.hasRemaining()) {
channel.write(buffer,buffer,this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
test(1000);
}
public static void test(int nThread) throws InterruptedException {
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch end = new CountDownLatch(nThread);
for (int i = 0; i < nThread; i++) {
Thread t = new Thread() {
public void run() {
try {
start.await();
} catch (InterruptedException e) {
}
new AsyncTimeClientHandler("127.0.0.1", 8080).run();
end.countDown();
}
};
t.start();
}
long startTimie = System.nanoTime();
start.countDown();
end.await();
long endTime = System.nanoTime();
System.out.println((endTime - startTimie)/1000000+"μs");
}
}
public class AsyncTimeClientHandler implements
CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public AsyncTimeClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
client.connect(new InetSocketAddress(host, port), this, this);
try {
latch.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void completed(Void result, AsyncTimeClientHandler attachment) {
byte[] req = "Hello Server".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
client.write(writeBuffer,
writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
client.write(buffer, buffer, this);
} else {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
client.read(
readBuffer,
readBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result,
ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer
.remaining()];
buffer.get(bytes);
String body;
try {
body = new String(bytes,
"UTF-8");
System.out.println("Receive message from server : "
+ body);
latch.countDown();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc,
ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
// ingnore on close
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
// ingnore on close
}
}
});
}
@Override
public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
exc.printStackTrace();
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
基于Netty开发异步I/O
Netty相比于传统的NIO程序,代码更加简洁,开发难度更低,扩展性也好。
- 服务端
public class TimeServer {
public static void main(String[] args) {
new TimeServer().bind(8080);
}
private void bind(int port) {
//NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上他们
//就是Reactor线程组。
EventLoopGroup boosGroup = new NioEventLoopGroup();//接受客户端的连接
EventLoopGroup workerGroup = new NioEventLoopGroup();//进行SocketChannel的网络读写
//用于启动NIO服务端的辅助启动类,降低服务端的开发复杂度
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
//设置创建的Channel为NioServerSocketChannel,对应JDK NIO类库中的ServerSocketChannel
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
//绑定I/O事件的处理类,作用类似于Reactor模式中的handler,主要用于网络I/O事件,例如记录日志,对消息进行编解码等。
.childHandler(new ChildChannelHandler());
try {
//绑定监听端口
ChannelFuture future = bootstrap.bind(port).sync();
//调用sync等待绑定操作完成
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new TimeServerHandler());
}
}
/**
*TimeServerHandler继承自ChannelHadnlerAdapter,它用于对网络事件进行读写操作
*/
public class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//将message转换成Netty的ByteBuf对象,ByteBuf类似于JDK中的java.nio.ByteBuffer对象
//不过提供了更加强大和灵活的功能。通过BYteBuf的readableBytes方法可以获取缓冲区可读
//的字节数组。
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Receive message from client: " + body);
ByteBuf res = Unpooled.copiedBuffer("Hello Netty Client!".getBytes());
ctx.write(res);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将消息发送队列中的消息写入到SocketChannel中发送给对方。从性能的角度考虑
//为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不是直接将消息
//写入SocketChannel,调用write方法只是把待发送的消息放到发送缓存数组中,再
//通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中。
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 客户端
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
new TimeClient().connect(8080, "127.0.0.1");
}
public void connect(int port, String host) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
//
b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(
new ChannelInitializer<SocketChannel>() {
//实现initChannel方法,作用是当创建NioSocketChannel成功之后,在初始化它的时候将它的
//ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
//调用connect方法发起异步连接,然后调用同步方法等待连接成功
ChannelFuture f = b.connect(host, port).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class TimeClientHandler extends ChannelHandlerAdapter {
private final ByteBuf firstMessage;
public TimeClientHandler() {
byte[] req = "Hello Netty Server".getBytes();
this.firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@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("Receive message from netty server:" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
TCP粘包/拆包
TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
TCP粘包/拆包发生的原因
- 应用程序write写入的字节大小大于套接口发送缓冲区大小
- 进行MSS大小的TCP分段
- 以太网帧的payload大于MTU进行IP分片
TCP粘包/拆包解决策略
由于底层TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下:
- 消息定长,例如每个报文的大小固定长度200字节,如果不够,空格补齐
- 在包尾增加回车换行符进行分割,例如FTP协议
- 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度
- 更复杂的协议
基于LineBasedFrameDecoder和StringDecoder解决粘包/拆包问题
LineBasedFrameDecoder的工作原理是它依次便利Bytebuf中的刻度字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。
StringDecoder就是将接收到的对象转换成字符串,然后继续调用后面的handler。这两者组合起来就是按行切换的文本解码器。
- 服务端
public class TimeServer {
public static void main(String[] args) {
new TimeServer().bind(8080);
}
private void bind(int port) {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
try {
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new TimeServerHandler());
}
}
}
public class TimeServerHandler extends ChannelHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("The time server receive order:" + body + ";the counter is:" + ++counter);
String currentTime =
"QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime = currentTime+System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
- 客户端
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
new TimeClient().connect(8080, "127.0.0.1");
}
public void connect(int port, String host) throws InterruptedException {
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 channel) throws Exception {
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
public class TimeClientHandler extends ChannelHandlerAdapter {
private byte[] req;
private int counter;
public TimeClientHandler() {
req = ("Query Time Order"+System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
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 + ";the counter is:"+ ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
此外,Netty还提供了DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解码。
Netty使用HTTP协议开发应用
HTTP(超文本传输协议)协议是建立在TCP传输协议之上的应用层协议,是目前Web开发的主流协议。
由于Netty的HTTP协议栈是基于Netty的NIO 通信框架开发的,因此Netty的HTTP协议也是异步非阻塞的。
HTTP协议的主要特点:
- 支持Client/Server模式
- 简单-客户像服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体
- 灵活–HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type标记
- 无状态–HTTP协议时无状态协议,无状态是值协议对于事物处理没有记忆能力。缺少状态以为这如果后续处理需要之前的信息,则它必须重传,这样可能导致每次连接传送的数据增大。
HTTP请求由三部分组成:
- HTTP请求头
- HTTP消息头
- HTTP请求正文
HTTP文件服务器简单实现
public class FileServer {
public static void main(String[] args) {
new FileServer().run(8080, "/src/main/resources/");
}
public void run(final int port,final String url) {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boosGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());
ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-encoder",new HttpResponseEncoder());
//支持异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
//用于文件服务器的业务逻辑处理
ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
}
});
ChannelFuture future = b.bind("192.168.91.1",port).sync();
System.out.println("HTTP文件目录服务器启动,网址是:"+ "192.168.91.1:"+port+url);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class HttpFileServerHandler extends
SimpleChannelInboundHandler<FullHttpRequest> {
private final String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
public void messageReceived(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
if (!request.getDecoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
if (request.getMethod() != GET) {
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
final String uri = request.getUri();
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {
if (uri.endsWith("/")) {
sendListing(ctx, file);
} else {
sendRedirect(ctx, uri + '/');
}
return;
}
if (!file.isFile()) {
sendError(ctx, FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
} catch (FileNotFoundException fnfe) {
sendError(ctx, NOT_FOUND);
return;
}
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,
fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future,
long progress, long total) {
if (total < 0) { // total unknown
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + " / "
+ total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = ctx
.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, INTERNAL_SERVER_ERROR);
}
}
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String sanitizeUri(String uri) {
try {
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException e1) {
throw new Error();
}
}
if (!uri.startsWith(url)) {
return null;
}
if (!uri.startsWith("/")) {
return null;
}
uri = uri.replace('/', File.separatorChar);
if (uri.contains(File.separator + '.')
|| uri.contains('.' + File.separator) || uri.startsWith(".")
|| uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
return null;
}
return System.getProperty("user.dir") + File.separator + uri;
}
private static final Pattern ALLOWED_FILE_NAME = Pattern
.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
private static void sendListing(ChannelHandlerContext ctx, File dir) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append(" 目录:");
buf.append("</title></head><body>\r\n");
buf.append("<h3>");
buf.append(dirPath).append(" 目录:");
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
for (File f : dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
buf.append("<li>链接:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx,
HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
status, Unpooled.copiedBuffer("Failure: " + status.toString()
+ "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,
mimeTypesMap.getContentType(file.getPath()));
}
}
本博客基于李林峰老师著的《Netty权威指南》做的个人总结