原文部落格地址: pjmike的部落格
前言
這篇主要介紹一個Netty 客戶端與服務端的示例程式碼,對Netty有一個直觀感受,看看如何使用Netty,後續文章會對Netty的各個元件進行詳細分析
Netty簡介
Netty是一款非同步的事件驅動的網路應用程式框架,支援快速開發可維護的高效能的面向協議的伺服器和客戶端。Netty主要是對java 的 nio包進行的封裝
為什麼要使用 Netty
上面介紹到 Netty是一款 高效能的網路通訊框架,那麼我們為什麼要使用Netty,換句話說,Netty有哪些優點讓我們值得使用它,為什麼不使用原生的 Java Socket程式設計,或者使用 Java 1.4引入的 Java NIO。接下來分析分析 Java Socket程式設計和 Java NIO。
Java 網路程式設計
首先來看一個Java 網路程式設計的例子:
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = "";
while ((line = reader.readLine()) != null) {
System.out.println("received: " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
上面展示了一個簡單的Socket服務端例子,該程式碼只能同時處理一個連線,要管理多個併發客戶端,需要為每個新的客戶端Socket建立一個 新的Thread。這種併發方案對於中小數量的客戶端來說還可以接受,如果是針對高併發,超過100000的併發連線來說該方案並不可取,它所需要的執行緒資源太多,而且任何時候都可能存在大量執行緒處於阻塞狀態,等待輸入或者輸出資料就緒,整個方案效能太差。所以,高併發的場景,一般的Java 網路程式設計方案是不可取的。
Java NIO
還是先來看一個 Java NIO的例子:
public class ServerSocketChannelDemo {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
public ServerSocketChannelDemo(int port) throws IOException {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void listener() throws IOException {
while (true) {
int n = selector.select();
if (n == 0) {
continue;
}
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel server_channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = server_channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (selectionKey.isReadable()) {
//如果通道處於讀就緒的狀態
//讀操作
//TODO
}
}
}
}
}
複製程式碼
NIO的核心部分主要有:
- 通道 Channel
- 緩衝區 Buffer
- 多路複用器 Selector
選擇器 Selector 是 Java 非阻塞 I/O實現的關鍵,將通道Channel註冊在 Selector上,如果某個通道 Channel傳送 讀或寫事件,這個Channel處於就緒狀態,會被Selector輪詢出來,進而進行後續I/O操作。這種I/O多路複用的方式相比上面的阻塞 I/O模型,提供了更好的資源管理:
- 使用較少的執行緒便可以處理很多連線,因此也減少了記憶體管理和上下文切換所帶來的開銷
- 當沒有I/O操作需要處理的時候,執行緒也可以被用於其他任務。
儘管使用 Java NIO可以讓我們使用較少的執行緒處理很多連線,但是在高負載下可靠和高效地處理和排程I/O操作是一項繁瑣而且容易出錯的任務,所以才引出了高效能網路程式設計專家——Netty
Netty特性
Netty有很多優秀的特性值得讓我們去使用它(摘自《Netty實戰》):
設計
- 統一的API,適用於不同的協議(阻塞和非阻塞)
- 基於靈活、可擴充套件的事件驅動模型
- 高度可定製的執行緒模型
- 可靠的無連線資料Socket支援(UDP)
效能
- 更好的吞吐量,低延遲
- 更低的資源消耗
- 最少的記憶體複製
健壯性
- 不再因過快、過慢或超負載連線導致OutOfMemoryError
- 不再有在高速網路環境下NIO讀寫頻率不一致的問題
安全性:
- 完整的SSL/TLS和STARTTLS的支援
- 可用於受限環境下,如 Applet 和OSGI
易用:
- 詳實的Javadoc和大量的示例集
- 不需要超過 JDK 1.6+的依賴
Netty示例程式碼
下面是server 和client的示例程式碼,先來看看Netty程式碼是怎麼樣的,後續文章會詳細分析各個模組。
Server程式碼
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
new EchoServer(8888).start();
}
public void start() throws InterruptedException {
final EchoServerHandler serverHandler = new EchoServerHandler();
//建立EventLoopGroup,處理事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker)
//指定所使用的NIO傳輸 Channel
.channel(NioServerSocketChannel.class)
//使用指定的埠設定套接字地址
.localAddress(new InetSocketAddress(port))
//新增一個EchoServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//EchoServerHandler標誌為@Shareable,所以我們可以總是使用同樣的例項
socketChannel.pipeline().addLast(serverHandler);
}
});
//非同步的繫結伺服器,呼叫sync()方法阻塞等待直到繫結完成
ChannelFuture future = b.bind().sync();
future.channel().closeFuture().sync();
} finally {
//關閉EventLoopGroup,釋放所有的資源
group.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
}
複製程式碼
EchoServerHandler
@ChannelHandler.Sharable //標識一個 ChannelHandler可以被多個Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
//將訊息記錄到控制檯
System.out.println("Server received: " + buffer.toString(CharsetUtil.UTF_8));
//將接受到訊息回寫給傳送者
ctx.write(buffer);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//將未訊息沖刷到遠端節點,並且關閉該 Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//列印異常棧跟蹤
cause.printStackTrace();
//關閉該Channel
ctx.close();
}
}
複製程式碼
程式碼要點解讀:
ServerBootStrap
是引導類,幫助服務啟動的輔助類,可以設定 Socket引數EventLoopGroup
是處理I/O操作的執行緒池,用來分配 服務於Channel的I/O和事件的EventLoop
,而NioEventLoopGroup
是EventLoopGroup
的一個實現類。這裡例項化了兩個NioEventLoopGroup
,一個boss
,主要用於處理客戶端連線,一個worker
用於處理客戶端的資料讀寫工作EchoServerHandler
實現了業務邏輯- 通過呼叫
ServerBootStrap.bind()
方法以繫結伺服器
Client 程式碼
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture channelFuture = b.connect().sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1", 8888).start();
}
}
複製程式碼
EchoClientHandler
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Client received: "+byteBuf.toString());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks",CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
複製程式碼
程式碼要點解讀:
- 為初始化客戶端,建立了一個BootStrap例項,與
ServerBootStrap
一樣,也是一個引導類,主要輔助客戶端 - 分配了一個
NioEventLoopGroup
例項,裡面的EventLoop
,處理連線的生命週期中所發生的事件 EchoClientHandler
類負責處理業務邏輯,與服務端的EchoSeverHandler
作用相似。