http://blog.csdn.net/abc_key/article/details/38061079
本章介紹
- 使用SSL/TLS創建安全的Netty程序
- 使用Netty創建HTTP/HTTPS程序
- 處理空閒連接和超時
- 解碼分隔符和基於長度的協議
- 寫大數據
- 序列化數據
8.1 使用SSL/TLS創建安全的Netty程序
public class SslChannelInitializer extends ChannelInitializer<Channel> {
private final SSLContext context;
private final boolean client;
private final boolean startTls;
public SslChannelInitializer(SSLContext context, boolean client, boolean startTls) {
this.context = context;
this.client = client;
this.startTls = startTls;
}
@Override
protected void initChannel(Channel ch) throws Exception {
SSLEngine engine = context.createSSLEngine();
engine.setUseClientMode(client);
ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
}
}
需要注意一點,SslHandler必須要添加到ChannelPipeline的第一個位置,可能有一些例外,但是最好這樣來做。回想一下之前講解的ChannelHandler,ChannelPipeline就像是一個在處理“入站”數據時先進先出,在處理“出站”數據時後進先出的隊列。最先添加的SslHandler會啊在其他Handler處理邏輯數據之前對數據進行加密,從而確保Netty服務端的所有的Handler的變化都是安全的。- setHandshakeTimeout(long handshakeTimeout, TimeUnit unit),設置握手超時時間,ChannelFuture將得到通知
- setHandshakeTimeoutMillis(long handshakeTimeoutMillis),設置握手超時時間,ChannelFuture將得到通知
- getHandshakeTimeoutMillis(),獲取握手超時時間值
- setCloseNotifyTimeout(long closeNotifyTimeout, TimeUnit unit),設置關閉通知超時時間,若超時,ChannelFuture會關閉失敗
- setHandshakeTimeoutMillis(long handshakeTimeoutMillis),設置關閉通知超時時間,若超時,ChannelFuture會關閉失敗
- getCloseNotifyTimeoutMillis(),獲取關閉通知超時時間
- handshakeFuture(),返回完成握手後的ChannelFuture
- close(),發送關閉通知請求關閉和銷燬
8.2 使用Netty創建HTTP/HTTPS程序
8.2.1 Netty的HTTP編碼器,解碼器和編解碼器
- HttpRequestEncoder,將HttpRequest或HttpContent編碼成ByteBuf
- HttpRequestDecoder,將ByteBuf解碼成HttpRequest和HttpContent
- HttpResponseEncoder,將HttpResponse或HttpContent編碼成ByteBuf
- HttpResponseDecoder,將ByteBuf解碼成HttpResponse和HttpContent
public class HttpDecoderEncoderInitializer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpDecoderEncoderInitializer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
pipeline.addLast("decoder", new HttpResponseDecoder());
pipeline.addLast("", new HttpRequestEncoder());
} else {
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("encoder", new HttpResponseEncoder());
}
}
}
如果你需要在ChannelPipeline中有一個解碼器和編碼器,還分別有一個在客戶端和服務器簡單的編解碼器:HttpClientCodec和HttpServerCodec。在ChannelPipelien中有解碼器和編碼器(或編解碼器)後就可以操作不同的HttpObject消息了;但是HTTP請求和響應可以有很多消息數據,你需要處理不同的部分,可能也需要聚合這些消息數據,這是很麻煩的。爲了解決這個問題,Netty提供了一個聚合器,它將消息部分合併到FullHttpRequest和FullHttpResponse,因此不需要擔心接收碎片消息數據。
8.2.2 HTTP消息聚合
/**
* 添加聚合http消息的Handler
*
* @author c.k
*
*/
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpAggregatorInitializer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
pipeline.addLast("codec", new HttpClientCodec());
} else {
pipeline.addLast("codec", new HttpServerCodec());
}
pipeline.addLast("aggegator", new HttpObjectAggregator(512 * 1024));
}
}
如上面代碼,很容使用Netty自動聚合消息。但是請注意,爲了防止Dos攻擊服務器,需要合理的限制消息的大小。應設置多大取決於實際的需求,當然也得有足夠的內存可用。8.2.3 HTTP壓縮
使用HTTP時建議壓縮數據以減少傳輸流量,壓縮數據會增加CPU負載,現在的硬件設施都很強大,大多數時候壓縮數據時一個好主意。Netty支持“gzip”和“deflate”,爲此提供了兩個ChannelHandler實現分別用於壓縮和解壓。看下面代碼: @Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
pipeline.addLast("codec", new HttpClientCodec());
//添加解壓縮Handler
pipeline.addLast("decompressor", new HttpContentDecompressor());
} else {
pipeline.addLast("codec", new HttpServerCodec());
//添加解壓縮Handler
pipeline.addLast("decompressor", new HttpContentDecompressor());
}
pipeline.addLast("aggegator", new HttpObjectAggregator(512 * 1024));
}
8.2.4 使用HTTPS
網絡中傳輸的重要數據需要加密來保護,使用Netty提供的SslHandler可以很容易實現,看下面代碼:/**
* 使用SSL對HTTP消息加密
*
* @author c.k
*
*/
public class HttpsCodecInitializer extends ChannelInitializer<Channel> {
private final SSLContext context;
private final boolean client;
public HttpsCodecInitializer(SSLContext context, boolean client) {
this.context = context;
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
SSLEngine engine = context.createSSLEngine();
engine.setUseClientMode(client);
ChannelPipeline pipeline = ch.pipeline();
pipeline.addFirst("ssl", new SslHandler(engine));
if (client) {
pipeline.addLast("codec", new HttpClientCodec());
} else {
pipeline.addLast("codec", new HttpServerCodec());
}
}
}
8.2.5 WebSocket
HTTP是不錯的協議,但是如果需要實時發佈信息怎麼做?有個做法就是客戶端一直輪詢請求服務器,這種方式雖然可以達到目的,但是其缺點很多,也不是優秀的解決方案,爲了解決這個問題,便出現了WebSocket。- BinaryWebSocketFrame,包含二進制數據
- TextWebSocketFrame,包含文本數據
- ContinuationWebSocketFrame,包含二進制數據或文本數據,BinaryWebSocketFrame和TextWebSocketFrame的結合體
- CloseWebSocketFrame,WebSocketFrame代表一個關閉請求,包含關閉狀態碼和短語
- PingWebSocketFrame,WebSocketFrame要求PongWebSocketFrame發送數據
- PongWebSocketFrame,WebSocketFrame要求PingWebSocketFrame響應
/**
* WebSocket Server,若想使用SSL加密,將SslHandler加載ChannelPipeline的最前面即可
* @author c.k
*
*/
public class WebSocketServerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec(),
new HttpObjectAggregator(65536),
new WebSocketServerProtocolHandler("/websocket"),
new TextFrameHandler(),
new BinaryFrameHandler(),
new ContinuationFrameHandler());
}
public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// handler text frame
}
}
public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
//handler binary frame
}
}
public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
//handler continuation frame
}
}
}
8.2.6 SPDY
SPDY(讀作“SPeeDY”)是Google開發的基於TCP的應用層協議,用以最小化網絡延遲,提升網絡速度,優化用戶的網絡使用體驗。SPDY並不是一種用於替代HTTP的協議,而是對HTTP協議的增強。新協議的功能包括數據流的多路複用、請求優先級以及HTTP報頭壓縮。谷歌表示,引入SPDY協議後,在實驗室測試中頁面加載速度比原先快64%。- 將頁面加載時間減少50%。
- 最大限度地減少部署的複雜性。SPDY使用TCP作爲傳輸層,因此無需改變現有的網絡設施。
- 避免網站開發者改動內容。 支持SPDY唯一需要變化的是客戶端代理和Web服務器應用程序。
- 單個TCP連接支持併發的HTTP請求。
- 壓縮報頭和去掉不必要的頭部來減少當前HTTP使用的帶寬。
- 定義一個容易實現,在服務器端高效率的協議。通過減少邊緣情況、定義易解析的消息格式來減少HTTP的複雜性。
- 強制使用SSL,讓SSL協議在現存的網絡設施下有更好的安全性和兼容性。
- 允許服務器在需要時發起對客戶端的連接並推送數據。
8.3 處理空閒連接和超時
- IdleStateHandler,當一個通道沒有進行讀寫或運行了一段時間後出發IdleStateEvent
- ReadTimeoutHandler,在指定時間內沒有接收到任何數據將拋出ReadTimeoutException
- WriteTimeoutHandler,在指定時間內有寫入數據將拋出WriteTimeoutException
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
pipeline.addLast(new HeartbeatHandler());
}
public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(
"HEARTBEAT", CharsetUtil.UTF_8));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
super.userEventTriggered(ctx, evt);
}
}
}
}
8.4 解碼分隔符和基於長度的協議
使用Netty時會遇到需要解碼以分隔符和長度爲基礎的協議,本節講解Netty如何解碼這些協議。8.4.1 分隔符協議
- DelimiterBasedFrameDecoder,解碼器,接收ByteBuf由一個或多個分隔符拆分,如NUL或換行符
- LineBasedFrameDecoder,解碼器,接收ByteBuf以分割線結束,如"\n"和"\r\n"
/**
* 處理換行分隔符消息
* @author c.k
*
*/
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(65 * 1204), new FrameHandler());
}
public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// do something with the frame
}
}
}
如果框架的東西除了換行符還有別的分隔符,可以使用DelimiterBasedFrameDecoder,只需要將分隔符傳遞到構造方法中。如果想實現自己的以分隔符爲基礎的協議,這些解碼器是有用的。例如,現在有個協議,它只處理命令,這些命令由名稱和參數形成,名稱和參數由一個空格分隔,實現這個需求的代碼如下:/**
* 自定義以分隔符爲基礎的協議
* @author c.k
*
*/
public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new CmdDecoder(65 * 1024), new CmdHandler());
}
public static final class Cmd {
private final ByteBuf name;
private final ByteBuf args;
public Cmd(ByteBuf name, ByteBuf args) {
this.name = name;
this.args = args;
}
public ByteBuf getName() {
return name;
}
public ByteBuf getArgs() {
return args;
}
}
public static final class CmdDecoder extends LineBasedFrameDecoder {
public CmdDecoder(int maxLength) {
super(maxLength);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
if (frame == null) {
return null;
}
int index = frame.indexOf(frame.readerIndex(), frame.writerIndex(), (byte) ' ');
return new Cmd(frame.slice(frame.readerIndex(), index), frame.slice(index + 1, frame.writerIndex()));
}
}
public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Cmd msg) throws Exception {
// do something with the command
}
}
}
8.4.2 長度爲基礎的協議
一般經常會碰到以長度爲基礎的協議,對於這種情況Netty有兩個不同的解碼器可以幫助我們來解碼:- FixedLengthFrameDecoder
- LengthFieldBasedFrameDecoder
public class LengthBasedInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65*1024, 0, 8))
.addLast(new FrameHandler());
}
public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
//do something with the frame
}
}
}
8.5 寫大數據
寫大量的數據的一個有效的方法是使用異步框架,如果內存和網絡都處於飽滿負荷狀態,你需要停止寫,否則會報OutOfMemoryError。Netty提供了寫文件內容時zero-memory-copy機制,這種方法再將文件內容寫到網絡堆棧空間時可以獲得最大的性能。使用零拷貝寫文件的內容時通過DefaultFileRegion、ChannelHandlerContext、ChannelPipeline,看下面代碼: @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
File file = new File("test.txt");
FileInputStream fis = new FileInputStream(file);
FileRegion region = new DefaultFileRegion(fis.getChannel(), 0, file.length());
Channel channel = ctx.channel();
channel.writeAndFlush(region).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(!future.isSuccess()){
Throwable cause = future.cause();
// do something
}
}
});
}
如果只想發送文件中指定的數據塊應該怎麼做呢?Netty提供了ChunkedWriteHandler,允許通過處理ChunkedInput來寫大的數據塊。下面是ChunkedInput的一些實現類:- ChunkedFile
- ChunkedNioFile
- ChunkedStream
- ChunkedNioStream
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
private final File file;
public ChunkedWriteHandlerInitializer(File file) {
this.file = file;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ChunkedWriteHandler())
.addLast(new WriteStreamHandler());
}
public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
}
}
}
8.6 序列化數據
開發網絡程序過程中,很多時候需要傳輸結構化對象數據POJO,Java中提供了ObjectInputStream和ObjectOutputStream及其他的一些對象序列化接口。Netty中提供基於JDK序列化接口的序列化接口。8.6.1 普通的JDK序列化
- CompatibleObjectEncoder
- CompactObjectInputStream
- CompactObjectOutputStream
- ObjectEncoder
- ObjectDecoder
- ObjectEncoderOutputStream
- ObjectDecoderInputStream
8.6.2 通過JBoss編組序列化
如果你想使用外部依賴的接口,JBoss編組是個好方法。JBoss Marshalling序列化的速度是JDK的3倍,並且序列化的結構更緊湊,從而使序列化後的數據更小。Netty附帶了JBoss編組序列化的實現,這些實現接口放在io.netty.handler.codec.marshalling包下面:- CompatibleMarshallingEncoder
- CompatibleMarshallingDecoder
- MarshallingEncoder
- MarshallingDecoder
/**
* 使用JBoss Marshalling
* @author c.k
*
*/
public class MarshallingInitializer extends ChannelInitializer<Channel> {
private final MarshallerProvider marshallerProvider;
private final UnmarshallerProvider unmarshallerProvider;
public MarshallingInitializer(MarshallerProvider marshallerProvider, UnmarshallerProvider unmarshallerProvider) {
this.marshallerProvider = marshallerProvider;
this.unmarshallerProvider = unmarshallerProvider;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new MarshallingDecoder(unmarshallerProvider))
.addLast(new MarshallingEncoder(marshallerProvider))
.addLast(new ObjectHandler());
}
public final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Serializable msg) throws Exception {
// do something
}
}
}
8.6.3 使用ProtoBuf序列化
- ProtobufDecoder
- ProtobufEncoder
- ProtobufVarint32FrameDecoder
- ProtobufVarint32LengthFieldPrepender
/**
* 使用protobuf序列化數據,進行編碼解碼
* 注意:使用protobuf需要protobuf-java-2.5.0.jar
* @author Administrator
*
*/
public class ProtoBufInitializer extends ChannelInitializer<Channel> {
private final MessageLite lite;
public ProtoBufInitializer(MessageLite lite) {
this.lite = lite;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufEncoder())
.addLast(new ProtobufDecoder(lite))
.addLast(new ObjectHandler());
}
public final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Serializable msg) throws Exception {
// do something
}
}
}