本章內容
- Transports(傳輸)
- NIO(non-blocking IO,New IO), OIO(Old IO,blocking IO), Local(本地), Embedded(嵌入式)
- Use-case(用例)
- APIs(接口)
如果你曾經使用Java提供的網絡接口工作過,你可能已經遇到過想從阻塞傳輸切換到非阻塞傳輸的情況,這種切換是比較困難的,因爲阻塞IO和非阻塞IO使用的API有很大的差異;Netty提供了上層的傳輸實現接口使得這種情況變得簡單。我們可以讓所寫的代碼儘可能通用,而不會依賴一些實現相關的APIs。當我們想切換傳輸方式的時候不需要花很大的精力和時間來重構代碼。
本章將介紹統一的API以及如何使用它們,會拿Netty的API和Java的API做比較來告訴你爲什麼Netty可以更容易的使用。本章也提供了一些優質的用例代碼,以便最佳使用Netty。使用Netty不需要其他的網絡框架或網絡編程經驗,若有則只是對理解netty有幫助,但不是必要的。下面讓我們來看看真是世界裏的傳輸工作。
4.1 案例研究:切換傳輸方式
4.1.1 使用Java的I/O和NIO
package netty.in.action;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
/**
* Blocking networking without Netty
* @author c.k
*
*/
public class PlainOioServer {
public void server(int port) throws Exception {
//bind server to port
final ServerSocket socket = new ServerSocket(port);
try {
while(true){
//accept connection
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
//create new thread to handle connection
new Thread(new Runnable() {
@Override
public void run() {
OutputStream out;
try{
out = clientSocket.getOutputStream();
//write message to connected client
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));
out.flush();
//close connection once message written and flushed
clientSocket.close();
}catch(IOException e){
try {
clientSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}).start();//start thread to begin handling
}
}catch(Exception e){
e.printStackTrace();
socket.close();
}
}
}
上面的方式很簡潔,但是這種阻塞模式在大連接數的情況就會有很嚴重的問題,如客戶端連接超時,服務器響應嚴重延遲。爲了解決這種情況,我們可以使用異步網絡處理所有的併發連接,但問題在於NIO和OIO的API是完全不同的,所以一個用OIO開發的網絡應用程序想要使用NIO重構代碼幾乎是重新開發。下面代碼是使用Java NIO實現的例子:
package netty.in.action;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.Iterator;
/**
* Asynchronous networking without Netty
* @author c.k
*
*/
public class PlainNioServer {
public void server(int port) throws Exception {
System.out.println("Listening for connections on port " + port);
//open Selector that handles channels
Selector selector = Selector.open();
//open ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
//get ServerSocket
ServerSocket serverSocket = serverChannel.socket();
//bind server to port
serverSocket.bind(new InetSocketAddress(port));
//set to non-blocking
serverChannel.configureBlocking(false);
//register ServerSocket to selector and specify that it is interested in new accepted clients
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
while (true) {
//Wait for new events that are ready for process. This will block until something happens
int n = selector.select();
if (n > 0) {
//Obtain all SelectionKey instances that received events
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
//Check if event was because new client ready to get accepted
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
//Accept client and register it to selector
client.register(selector, SelectionKey.OP_WRITE, msg.duplicate());
}
//Check if event was because socket is ready to write data
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buff = (ByteBuffer) key.attachment();
//write data to connected client
while (buff.hasRemaining()) {
if (client.write(buff) == 0) {
break;
}
}
client.close();//close client
}
} catch (Exception e) {
key.cancel();
key.channel().close();
}
}
}
}
}
}
如你所見,即使它們實現的功能是一樣,但是代碼完全不同。下面我們將用Netty來實現相同的功能。4.1.2 Netty中使用I/O和NIO
package netty.in.action;
import java.net.InetSocketAddress;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.oio.OioServerSocketChannel;
import io.netty.util.CharsetUtil;
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n", CharsetUtil.UTF_8));
//事件循環組
EventLoopGroup group = new NioEventLoopGroup();
try {
//用來引導服務器配置
ServerBootstrap b = new ServerBootstrap();
//使用OIO阻塞模式
b.group(group).channel(OioServerSocketChannel.class).localAddress(new InetSocketAddress(port))
//指定ChannelInitializer初始化handlers
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
//添加一個“入站”handler到ChannelPipeline
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//連接後,寫消息到客戶端,寫完後便關閉連接
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
//綁定服務器接受連接
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
//釋放所有資源
group.shutdownGracefully();
}
}
}
上面代碼實現功能一樣,但結構清晰明瞭,這只是Netty的優勢之一。4.1.3 Netty中實現異步支持
package netty.in.action;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
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.util.CharsetUtil;
import java.net.InetSocketAddress;
public class NettyNioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi!\r\n", CharsetUtil.UTF_8));
// 事件循環組
EventLoopGroup group = new NioEventLoopGroup();
try {
// 用來引導服務器配置
ServerBootstrap b = new ServerBootstrap();
// 使用NIO異步模式
b.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(port))
// 指定ChannelInitializer初始化handlers
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 添加一個“入站”handler到ChannelPipeline
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 連接後,寫消息到客戶端,寫完後便關閉連接
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
// 綁定服務器接受連接
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
// 釋放所有資源
group.shutdownGracefully();
}
}
}
因爲Netty使用相同的API來實現每個傳輸,它並不關心你使用什麼來實現。Netty通過操作Channel接口和ChannelPipeline、ChannelHandler來實現傳輸。4.2 Transport API
- 傳輸數據時,將數據從一種格式轉換到另一種格式
- 異常通知
- Channel變爲有效或無效時獲得通知
- Channel被註冊或從EventLoop中註銷時獲得通知
- 通知用戶特定事件
- eventLoop(),返回分配給Channel的EventLoop
- pipeline(),返回分配給Channel的ChannelPipeline
- isActive(),返回Channel是否激活,已激活說明與遠程連接對等
- localAddress(),返回已綁定的本地SocketAddress
- remoteAddress(),返回已綁定的遠程SocketAddress
- write(),寫數據到遠程客戶端,數據通過ChannelPipeline傳輸過去
Channel channel = ...
//Create ByteBuf that holds data to write
ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8);
//Write data
ChannelFuture cf = channel.write(buf);
//Add ChannelFutureListener to get notified after write completes
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
//Write operation completes without error
if (future.isSuccess()) {
System.out.println(.Write successful.);
} else {
//Write operation completed but because of error
System.err.println(.Write error.);
future.cause().printStacktrace();
}
}
});
Channel是線程安全(thread-safe)的,它可以被多個不同的線程安全的操作,在多線程環境下,所有的方法都是安全的。正因爲Channel是安全的,我們存儲對Channel的引用,並在學習的時候使用它寫入數據到遠程已連接的客戶端,使用多線程也是如此。下面的代碼是一個簡單的多線程例子:final Channel channel = ...
//Create ByteBuf that holds data to write
final ByteBuf buf = Unpooled.copiedBuffer("your data",CharsetUtil.UTF_8);
//Create Runnable which writes data to channel
Runnable writer = new Runnable() {
@Override
public void run() {
channel.write(buf.duplicate());
}
};
//Obtain reference to the Executor which uses threads to execute tasks
Executor executor = Executors.newChachedThreadPool();
// write in one thread
//Hand over write task to executor for execution in thread
executor.execute(writer);
// write in another thread
//Hand over another write task to executor for execution in thread
executor.execute(writer);
此外,這種方法保證了寫入的消息以相同的順序通過寫入它們的方法。想了解所有方法的使用可以參考Netty API文檔。4.3 Netty包含的傳輸實現
- NIO,io.netty.channel.socket.nio,基於java.nio.channels的工具包,使用選擇器作爲基礎的方法。
- OIO,io.netty.channel.socket.oio,基於java.net的工具包,使用阻塞流。
- Local,io.netty.channel.local,用來在虛擬機之間本地通信。
- Embedded,io.netty.channel.embedded,嵌入傳輸,它允許在沒有真正網絡的運輸中使用ChannelHandler,可以非常有用的來測試ChannelHandler的實現。
4.3.1 NIO - Nonblocking I/O
- 一個新的Channel被接受並已準備好
- Channel連接完成
- Channel中有數據並已準備好讀取
- Channel發送數據出去
- OP_ACCEPT,有新連接時得到通知
- OP_CONNECT,連接完成後得到通知
- OP_READ,準備好讀取數據時得到通知
- OP_WRITE,寫入數據到通道時得到通知
4.3.2 OIO - Old blocking I/O
4.3.3 Local - In VM transport
4.3.4 Embedded transport
4.4 每種傳輸方式在什麼時候使用?
- OIO,在低連接數、需要低延遲時、阻塞時使用
- NIO,在高連接數時使用
- Local,在同一個JVM內通信時使用
- Embedded,測試ChannelHandler時使用