Netty In Action中文版 - 第四章:Transports(傳輸)

 本章內容

  • Transports(傳輸)
  • NIO(non-blocking IO,New IO), OIO(Old IO,blocking IO), Local(本地), Embedded(嵌入式)
  • Use-case(用例)
  • APIs(接口)
        網絡應用程序一個很重要的工作是傳輸數據。傳輸數據的過程不一樣取決是使用哪種交通工具,但是傳輸的方式是一樣的:都是以字節碼傳輸。Java開發網絡程序傳輸數據的過程和方式是被抽象了的,我們不需要關注底層接口,只需要使用Java API或其他網絡框架如Netty就能達到傳輸數據的目的。發送數據和接收數據都是字節碼。Nothing more,nothing less。

        如果你曾經使用Java提供的網絡接口工作過,你可能已經遇到過想從阻塞傳輸切換到非阻塞傳輸的情況,這種切換是比較困難的,因爲阻塞IO和非阻塞IO使用的API有很大的差異;Netty提供了上層的傳輸實現接口使得這種情況變得簡單。我們可以讓所寫的代碼儘可能通用,而不會依賴一些實現相關的APIs。當我們想切換傳輸方式的時候不需要花很大的精力和時間來重構代碼。

        本章將介紹統一的API以及如何使用它們,會拿Netty的API和Java的API做比較來告訴你爲什麼Netty可以更容易的使用。本章也提供了一些優質的用例代碼,以便最佳使用Netty。使用Netty不需要其他的網絡框架或網絡編程經驗,若有則只是對理解netty有幫助,但不是必要的。下面讓我們來看看真是世界裏的傳輸工作。

4.1 案例研究:切換傳輸方式

        爲了讓你想象如何運輸,我會從一個簡單的應用程序開始,這個應用程序什麼都不做,只是接受客戶端連接併發送“Hi!”字符串消息到客戶端,發送完了就斷開連接。我不會詳細講解這個過程的實現,它只是一個例子。

4.1.1 使用Java的I/O和NIO

        我們將不用Netty實現這個例子,下面代碼是使用阻塞IO實現的例子:
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

        下面代碼是使用Netty作爲網絡框架編寫的一個阻塞IO例子:
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中實現異步支持

        下面代碼是使用Netty實現異步,可以看出使用Netty由OIO切換到NIO是非常的方便。
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

        傳輸API的核心是Channel接口,它用於所有出站的操作。Channel接口的類層次結構如下

如上圖所示,每個Channel都會分配一個ChannelPipeline和ChannelConfig。ChannelConfig負責設置並存儲配置,並允許在運行期間更新它們。傳輸一般有特定的配置設置,只作用於傳輸,沒有其他的實現。ChannelPipeline容納了使用的ChannelHandler實例,這些ChannelHandler將處理通道傳遞的“入站”和“出站”數據。ChannelHandler的實現允許你改變數據狀態和傳輸數據,本書有章節詳細講解ChannelHandler,ChannelHandler是Netty的重點概念。
        現在我們可以使用ChannelHandler做下面一些事情:
  • 傳輸數據時,將數據從一種格式轉換到另一種格式
  • 異常通知
  • Channel變爲有效或無效時獲得通知
  • Channel被註冊或從EventLoop中註銷時獲得通知
  • 通知用戶特定事件
        這些ChannelHandler實例添加到ChannelPipeline中,在ChannelPipeline中按順序逐個執行。它類似於一個鏈條,有使用過Servlet的讀者可能會更容易理解。
        ChannelPipeline實現了攔截過濾器模式,這意味着我們連接不同的ChannelHandler來攔截並處理經過ChannelPipeline的數據或事件。可以把ChannelPipeline想象成UNIX管道,它允許不同的命令鏈(ChannelHandler相當於命令)。你還可以在運行時根據需要添加ChannelHandler實例到ChannelPipeline或從ChannelPipeline中刪除,這能幫助我們構建高度靈活的Netty程序。此外,訪問指定的ChannelPipeline和ChannelConfig,你能在Channel自身上進行操作。Channel提供了很多方法,如下列表:
  • eventLoop(),返回分配給Channel的EventLoop
  • pipeline(),返回分配給Channel的ChannelPipeline
  • isActive(),返回Channel是否激活,已激活說明與遠程連接對等
  • localAddress(),返回已綁定的本地SocketAddress
  • remoteAddress(),返回已綁定的遠程SocketAddress
  • write(),寫數據到遠程客戶端,數據通過ChannelPipeline傳輸過去
後面會越來越熟悉這些方法,現在只需要記住我們的操作都是在相同的接口上運行,Netty的高靈活性讓你可以以不同的傳輸實現進行重構。
        寫數據到遠程已連接客戶端可以調用Channel.write()方法,如下代碼:
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包含的傳輸實現

        Netty自帶了一些傳輸協議的實現,雖然沒有支持所有的傳輸協議,但是其自帶的已足夠我們來使用。Netty應用程序的傳輸協議依賴於底層協議,本節我們將學習Netty中的傳輸協議。
        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

        NIO傳輸是目前最常用的方式,它通過使用選擇器提供了完全異步的方式操作所有的I/O,NIO從Java 1.4才被提供。NIO中,我們可以註冊一個通道或獲得某個通道的改變的狀態,通道狀態有下面幾種改變:
  • 一個新的Channel被接受並已準備好
  • Channel連接完成
  • Channel中有數據並已準備好讀取
  • Channel發送數據出去
        處理完改變的狀態後需重新設置他們的狀態,用一個線程來檢查是否有已準備好的Channel,如果有則執行相關事件。在這裏可能只同時一個註冊的事件而忽略其他的。選擇器所支持的操作在SelectionKey中定義,具體如下:
  • OP_ACCEPT,有新連接時得到通知
  • OP_CONNECT,連接完成後得到通知
  • OP_READ,準備好讀取數據時得到通知
  • OP_WRITE,寫入數據到通道時得到通知
        Netty中的NIO傳輸就是基於這樣的模型來接收和發送數據,通過封裝將自己的接口提供給用戶使用,這完全隱藏了內部實現。如前面所說,Netty隱藏內部的實現細節,將抽象出來的API暴露出來供使用,下面是處理流程圖:

        NIO在處理過程也會有一定的延遲,若連接數不大的話,延遲一般在毫秒級,但是其吞吐量依然比OIO模式的要高。Netty中的NIO傳輸是“zero-file-copy”,也就是零文件複製,這種機制可以讓程序速度更快,更高效的從文件系統中傳輸內容,零複製就是我們的應用程序不會將發送的數據先複製到JVM堆棧在進行處理,而是直接從內核空間操作。接下來我們將討論OIO傳輸,它是阻塞的。

4.3.2 OIO - Old blocking I/O

        OIO就是java中提供的Socket接口,java最開始只提供了阻塞的Socket,阻塞會導致程序性能低。下面是OIO的處理流程圖,若想詳細瞭解,可以參閱其他相關資料。

4.3.3 Local - In VM transport

         Netty包含了本地傳輸,這個傳輸實現使用相同的API用於虛擬機之間的通信,傳輸是完全異步的。每個Channel使用唯一的SocketAddress,客戶端通過使用SocketAddress進行連接,在服務器會被註冊爲長期運行,一旦通道關閉,它會自動註銷,客戶端無法再使用它。
        連接到本地傳輸服務器的行爲與其他的傳輸實現幾乎是相同的,需要注意的一個重點是只能在本地的服務器和客戶端上使用它們。Local未綁定任何Socket,值提供JVM進程之間的通信。

4.3.4 Embedded transport

        Netty還包括嵌入傳輸,與之前講述的其他傳輸實現比較,它是不是一個真的傳輸呢?若不是一個真的傳輸,我們用它可以做什麼呢?Embedded transport允許更容易的使用不同的ChannelHandler之間的交互,這也更容易嵌入到其他的ChannelHandler實例並像一個輔助類一樣使用它們。它一般用來測試特定的ChannelHandler實現,也可以在ChannelHandler中重新使用一些ChannelHandler來進行擴展,爲了實現這樣的目的,它自帶了一個具體的Channel實現,即:EmbeddedChannel。

4.4 每種傳輸方式在什麼時候使用?

        不多加贅述,看下面列表:
  • OIO,在低連接數、需要低延遲時、阻塞時使用
  • NIO,在高連接數時使用
  • Local,在同一個JVM內通信時使用
  • Embedded,測試ChannelHandler時使用
發佈了32 篇原創文章 · 獲贊 11 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章