前言
Github:https://github.com/yihonglei/thinking-in-netty
一 Netty概述
Netty 提供異步的、事件驅動的網絡應用程序框架和工具,用以快速簡單開發高性能、高可靠性的網絡服務器和客戶端程序。
它大大簡化了網絡編程,如TCP和UDP套接字服務器。“快速簡單”並不意味着生成的應用程序將受到可維護性或
性能問題的影響。Netty經過精心設計,並積累了許多協議(如ftp、smtp、http)的實施經驗,以及各種二進制和
基於文本的遺留協議。因此,Netty成功地找到了一種方法,可以在不妥協的情況下實現輕鬆的開發、性能、穩定性和靈活性。
二 Netty特性
Netty官方架構圖。
設計
- 統一的API,適用於不同的協議(阻塞和非阻塞)
- 基於靈活、可擴展的事件驅動模型(SEDA)
- 高度可定製的線程模型
- 可靠的無連接數據Socket支持(UDP)
性能
- 更好的吞吐量,低延遲
- 更省資源
- 儘量減少不必要的內存拷貝
安全
- 完整的SSL/ TLS和STARTTLS的支持
易用
- 完善的Java doc,用戶指南和樣例
- 僅依賴於JDK1.6(netty 4.x)
三 Netty核心組件
1、Tansport Channel
對應NIO中的Channel,是NIO的基本構造,可以把Channel看成是傳入(入站)或者傳出(出站)數據的載體。
因此,它可以被打開或者關閉,連接或者斷開連接。
2、EventLoop、ChannelHandler、EventLoopGroup
EventLoop是NIO中selector程序的抽象,消除了在NIO中手動的編寫派發代碼。在Netty內部,將會爲每個Channel
分配一個EventLoop,用以處理所有I/O事件:
- 註冊感興趣的事件;
- 將事件派發給ChannelHandler;
- 安排進一步的處理;
每一個事件觸發後將直接交由ChannelHandler接口實現處理。
EventLoop本身只由一個線程驅動,處理一個Channel的所有I/O事件,並且在該EventLoop的整個生命週期內部不會改變。
一個EventLoopGroup可以包含多個EventLoop。
3、ChannelFutue
Netty提供了另外一種在操作完成時通知應用程序的方式。在未來的某個時刻完成時提供對其結果的訪問。
JDK內置包裏面java.util.concurrent.Future接口提供了實現,但是操作比較麻煩,Netty提供了自己的實現,
ChannelFuture用於在執行異步操作的時候使用。ChannelFutue不會阻塞,完全是異步和事件驅動的。
4、ByteBuf
對應NIO中的ByteBuffer。
5、Bootstrap 和 ServerBootstrap
對應NIO中的Selector、ServerSocketChannel等的創建、配置、啓動等。
Netty裏面有很多的概念,概念說太多就迷糊了,先來個實例,體驗下Netty入門編程。
四 Netty入門實戰
jar包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
1、服務端
1.1 EchoServer
啓動服務端,等待客戶端連接,將連接事件交給EchoServerHandler處理。
package com.jpeony.netty.echo;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
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 java.net.InetSocketAddress;
/**
* 一 Netty Server啓動流程
* 1、設置服務端ServerBootStrap啓動參數
* <p>
* group()方法分配一個NioEventLoopGroup實例以進行事件處理,如接受新連接以及讀/寫數據;
* <p>
* channel()設置通道類型;
* <p>
* localAddress()指定服務器綁定的本地的InetSocketAddress;
* <p>
* childHandler()添加一個EchoServerHandler到Channel的ChannelPipeline;
* 2、調用ServerBootStrap.bind()方法啓動服務端
* 調用ServerBootStrap.bind()方法啓動服務端,bind方法會在group中註冊NioServerScoketChannel,
* 監聽客戶端的連接請求會創建一個NioServerSocketChannel實例,並將其在group中進行註冊;
*
* @author yihonglei
*/
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
/**
* 服務器端啓動方法
*
* @author yihonglei
*/
private void start() throws Exception {
// 創建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 創建ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
// 指定EventLoopGroup以處理服務端事件,需要適用於NIO的實現。
b.group(bossGroup, workerGroup)
// 指定所使用的NIO傳輸Channel
.channel(NioServerSocketChannel.class)
// 使用指定的端口設置套接字地址
.localAddress(new InetSocketAddress(port))
// 添加一個EchoServerHandler到Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// EchoServerHandler被標註爲@Shareable,所以我們可以總是使用同樣的實例
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 異步地綁定服務器,調用sync()方法阻塞直到綁定完成
ChannelFuture f = b.bind().sync();
// 綁定Channel的CloseFuture,並且阻塞當前線程直到它完成
f.channel().closeFuture().sync();
} finally {
// 關閉EventLoopGroup並且釋放所有的資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
// 設置端口值
new EchoServer(9999).start();
}
}
1.2 EchoServerHandler
EchoServerHandler負責服務端具體邏輯處理。
package com.jpeony.netty.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* 服務端業務邏輯處理。
*
* @author yihonglei
*/
@ChannelHandler.Sharable //註解@ChannelHandler.Sharable表示一個ChannelHandler可以被多個Channel安全地共享。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
/**
* 每個傳入的消息都要調用該方法。
*
* @author yihonglei
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 將客戶端發送過來的消息打印到控制檯
ByteBuf in = (ByteBuf) msg;
System.out.println("server received msg from client:" + in.toString(CharsetUtil.UTF_8));
// 寫一條消息響應給客戶端
ByteBuf responseMsg = Unpooled.wrappedBuffer(new String("Hello Client!").getBytes());
ctx.write(responseMsg);
}
/**
* 通知ChannelInboundHandler最後一次對channelRead()的調用時當前批量讀取中的最後一條消息。
*
* @author yihonglei
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// 將未決消息沖刷到遠程節點,並且關閉該Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
/**
* 在讀取操作期間,有異常拋出時會調用。
*
* @author yihonglei
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印異常棧跟蹤信息
cause.printStackTrace();
// 關閉該Channel
ctx.close();
}
}
2、客戶端
2.1 EchoClient
1)連接到服務器;
2)發送一個或者多個消息;
3)對於每個消息,等待並接收從服務器發回的消息;
4)關閉連接;
客戶端邏輯交由EchoClientHandler處理。
package com.jpeony.netty.echo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
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.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* 1、初始化客戶端,將創建一個Bootstrap實例;
* 2、爲事件處理分配一個NioEventLoopGroup實例,其中事件處理包括創建新的連接以及處理入站和出站數據;
* 3、爲服務器連接創建一個InetSocketAddress實例;
* 4、當連接被建立時,一個EchoClientHandler實例會被安裝到ChannelPipeline中;
* 5、在一切都設置完成後,調用Bootstrap.connect()方法連接到遠程節點;
*
* @author yihonglei
*/
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 客戶端啓動方法
*
* @author yihonglei
*/
private void start() throws InterruptedException {
// 創建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
// 創建Bootstrap
Bootstrap b = new Bootstrap();
// 指定EventLoopGroup以處理客戶端事件,需要適用於NIO的實現。
b.group(group)
// 適用於NIO傳輸的Channel類型
.channel(NioSocketChannel.class)
// 設置服務器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host, port))
// 在創建Channel時,向ChannelPipeline中添加一個EchoClientHandler實例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 連接到遠程節點,阻塞等待知道連接完成
ChannelFuture f = b.connect().sync();
// 阻塞,直到Channel關閉
f.channel().closeFuture().sync();
} finally {
// 關閉EventLoopGroup並且釋放所有的資源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1", 9999).start();
}
}
2.2 EchoClientHandler
負責客戶端邏輯處理。
package com.jpeony.netty.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* 客戶端業務邏輯處理
*
* @author yihonglei
*/
@ChannelHandler.Sharable // 標記該類的實例可以被多個Channel共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 連接服務器成功之後被調用
*
* @author yihonglei
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 當被通知Channel是活躍的時候,發送一條消息(向服務端發送一條消息)
String data = "Hello Server!";
ctx.writeAndFlush(Unpooled.copiedBuffer(data, CharsetUtil.UTF_8));
}
/**
* 從服務器接收到消息時被調用
*
* @author yihonglei
* @date 2019/4/20 16:27
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
// 將服務端響應的消息打印出來
System.out.println("client received msg from client:" + in.toString(CharsetUtil.UTF_8));
}
/**
* 在處理過程中拋異常時調用
*
* @author yihonglei
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印異常棧跟蹤信息
cause.printStackTrace();
// 關閉該Channel
ctx.close();
}
}
3、程序啓動
先啓動服務端EchoServer,然後再啓動客戶端EchoClient。
服務端接收到客戶端發送過來的消息Hello Server!
客戶端收到服務端響應的消息Hello Client!