Netty入門(4) - 附帶的ChannelHandler和Codec
使用SSL/TLS建立安全的Netty程式
Java提供了抽象的SslContext和SslEngine,實際上SslContext可以用來獲取SslEngine來進行加密和解密。Netty拓展了Java的SslEngine,稱SslHandler,用來對網路資料進行加密和解密。
1、製作自簽證書
#keytool -genkey -keysize 2048 -validity 365 -keyalg RSA -dnam e "CN=gornix.com" -keypass 654321 -storepass 123456 -keystore gornix.jks
keytool為JDK提供的生成證書工具
- -keysize 2048 金鑰長度2048位(這個長度的金鑰目前可認為無法被暴力破解)
- -validity 365 證書有效期365天
- -keyalg RSA 使用RSA非對稱加密演算法
- -dname "CN=gornix.com" 設定Common Name為gornix.com,這是我的域名
- -keypass 654321 金鑰的訪問密碼為654321
- -storepass 123456 金鑰庫的訪問密碼為123456(其實這兩個密碼也可以設定一樣,通常都設定一樣,方便記)
- -keystore gornix.jks 指定生成的金鑰庫檔案為gornix.jks
2、服務端程式
public class SocketServerHelper { private static int WORKER_GROUP_SIZE = Runtime.getRuntime().availableProcessors() * 2; private static EventLoopGroup bossGroup; private static EventLoopGroup workerGroup; private static Class<? extends ServerChannel> channelClass;public static void startSpiderServer() throws Exception { ServerBootstrap b = new ServerBootstrap(); b.childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.SO_REUSEADDR, true) .childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(false)) .childOption(ChannelOption.SO_RCVBUF, 1048576) .childOption(ChannelOption.SO_SNDBUF, 1048576); bossGroup = new NioEventLoopGroup(1); workerGroup = new NioEventLoopGroup(WORKER_GROUP_SIZE); channelClass = NioServerSocketChannel.class; System.out.println("workerGroup size:" + WORKER_GROUP_SIZE); System.out.println("preparing to start spider server..."); b.group(bossGroup, workerGroup); b.channel(channelClass); KeyManagerFactory keyManagerFactory = null; KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream("G:\\ssl.jks"), "123456".toCharArray()); keyManagerFactory = KeyManagerFactory.getInstance("SunX509"); keyManagerFactory.init(keyStore,"123456".toCharArray()); SslContext sslContext = SslContextBuilder.forServer(keyManagerFactory).build(); b.childHandler(new SslChannelInitializer(sslContext)); b.bind(9912).sync(); System.out.println("spider server start sucess, listening on port " + 9912 + "."); } public static void main(String[] args) throws Exception { SocketServerHelper.startSpiderServer(); } public static void shutdown() { System.out.println("preparing to shutdown spider server..."); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); System.out.println("spider server is shutdown."); } }
public class SslChannelInitializer extends ChannelInitializer<Channel> { private final SslContext context; public SslChannelInitializer(SslContext context) { this.context = context; } @Override protected void initChannel(Channel ch) throws Exception { SSLEngine engine = context.newEngine(ch.alloc()); engine.setUseClientMode(false); ch.pipeline().addFirst("ssl", new SslHandler(engine)); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)); pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); //最大16M pipeline.addLast("decoder", new StringDecoder(Charset.forName("UTF-8"))); pipeline.addLast("encoder", new StringEncoder(Charset.forName("UTF-8"))); pipeline.addLast("spiderServerBusiHandler", new SpiderServerBusiHandler()); } }
3、客戶端程式
public class SocketClientHelper { public static void main(String[] args) { Channel channel = SocketClientHelper.createChannel("localhost",9912); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } SocketHelper.writeMessage(channel, "ssh over tcp test 1"); SocketHelper.writeMessage(channel, "ssh over tcp test 2"); SocketHelper.writeMessage(channel, "ssh over tcp test 3"); SocketHelper.writeMessage(channel, "ssh over tcp test 4"); SocketHelper.writeMessage(channel, "ssh over tcp test 5"); } public static Channel createChannel(String host, int port) { Channel channel = null; Bootstrap b = getBootstrap(); try { channel = b.connect(host, port).sync().channel(); System.out.println(MessageFormat.format("connect to spider server ({0}:{1,number,#}) success for thread [" + Thread.currentThread().getName() + "].", host , port)); } catch (Exception e) { e.printStackTrace(); } return channel; } public static Bootstrap getBootstrap(){ EventLoopGroup group; Class<? extends Channel> channelClass = NioSocketChannel.class; group = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); b.group(group).channel(channelClass); b.option(ChannelOption.SO_KEEPALIVE, true); b.option(ChannelOption.TCP_NODELAY, true); b.option(ChannelOption.SO_REUSEADDR, true); b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000); TrustManagerFactory tf = null; try { KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream("G:\\ssl.jks"), "123456".toCharArray()); tf = TrustManagerFactory.getInstance("SunX509"); tf.init(keyStore); SslContext sslContext = SslContextBuilder.forClient().trustManager(tf).build(); b.handler(new SslChannelInitializer(sslContext)); return b; } catch(Exception e) { e.printStackTrace(); } return null; } }
public class SslChannelInitializer extends ChannelInitializer<Channel> { private final SslContext context; public SslChannelInitializer(SslContext context) { this.context = context; } @Override protected void initChannel(Channel ch) throws Exception { SSLEngine engine = context.newEngine(ch.alloc()); engine.setUseClientMode(true); ch.pipeline().addFirst("ssl", new SslHandler(engine)); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4)); pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); //最大16M pipeline.addLast("decoder", new StringDecoder(Charset.forName("UTF-8"))); pipeline.addLast("encoder", new StringEncoder(Charset.forName("UTF-8"))); pipeline.addLast("spiderClientBusiHandler", new SpiderClientBusiHandler()); } }
可見SSL也沒什麼神祕的,就是在普通的TCP連線基礎上包了一層處理而已(但如果要自己實現這層處理那可是相當複雜的),這層處理體現在Netty中就是一個SslHandler,把這個SslHandler加入到TCP連線的處理管線中即可。
PS:我們也可以使用基於認證和報文頭加密的方式實現安全性。
處理空閒和超時
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_SEQ = 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_SEQ.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { super.userEventTriggered(ctx, evt); } } } }
分隔符協議
DelimiterBasedFrameDecoder,解碼器,接收ByteBuf由一個或者多個分隔符拆分,如NUL或者換行符。
LineBasedFrameDecoder,解碼器,接收ByteBuf以分隔符結束,如"\n"和"\r\n"
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(65*1024), new FrameHandler()); } public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override protected void channelRead0(ChannelHandlerContext arg0, ByteBuf arg1) throws Exception { // do something } } }
長度為基礎的協議
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 } } }
寫大資料
寫大量的資料的一個有效的方法就是使用非同步框架,如果記憶體和網路都處於爆滿負荷狀態,你需要停止寫,Netty提供zero-memory-copy機制,這種方法在將檔案內容寫到網路堆疊空間時可以獲得最大的效能:
public class WriteBigData extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { File file = new File(""); 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 } } }); } }
如果只想傳送指定的資料塊,可以使用ChunkedFile、ChunkedNioFile、ChunkedStream、ChunkedNioStream等。
Protobuf序列化傳輸
ProtobufDecoder、ProtobufEncoder、ProtobufVarint32FrameDecoder、ProtobufVarint32LengthPrepender,使用Protobuf需要映入protobuf-java-2.5.0.jar
1、下載編譯器,將protoc.exe配置到環境變數:https://github.com/google/protobuf/releases
2、編寫.proto檔案,參考:https://blog.csdn.net/hry2015/article/details/70766603
syntax = "proto3"; // 宣告可以選擇protobuf的編譯器版本(v2和v3) option java_outer_classname = "MessageProto"; // 指定生成的java類的類名 message Message { // 相當於c語言中的struct語句,表示定義一個資訊,其實也就是類。 string id = 1; // 要傳輸的欄位了,子段後面需要有一個數字編號,從1開始遞增 string content = 2; }
3、CMD執行編譯操作
protoc ./Message.proto --java_out=./
4、引入maven
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.5.1</version> </dependency>
5、服務端
public class ServerPoHandlerProto extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { MessageProto.Message message = (MessageProto.Message) msg; if (ConnectionPool.getChannel(message.getId()) == null) { ConnectionPool.putChannel(message.getId(), ctx); } System.err.println("server:" + message.getId()); ctx.writeAndFlush(message); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { // 實體類傳輸資料,protobuf序列化 ch.pipeline().addLast("decoder", new ProtobufDecoder(MessageProto.Message.getDefaultInstance())); ch.pipeline().addLast("encoder", new ProtobufEncoder()); ch.pipeline().addLast(new ServerPoHandlerProto()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true);
6、客戶端
public class ClientPoHandlerProto extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { MessageProto.Message message = (MessageProto.Message) msg; System.out.println("client:" + message.getContent()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { // 實體類傳輸資料,protobuf序列化 ch.pipeline().addLast("decoder", new ProtobufDecoder(MessageProto.Message.getDefaultInstance())); ch.pipeline().addLast("encoder", new ProtobufEncoder()); ch.pipeline().addLast(new ClientPoHandlerProto()); } });
單元測試
package com.netty.learn.demo6; import java.util.List; import java.util.Random; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.ByteToMessageDecoder; public class EmbeddedChannelInboundTest { public static void main(String[] args) { Random r = new Random(); ByteBuf byteBuf = Unpooled.buffer(); for (int i = 0; i < 3; i++) { int one = r.nextInt(); byteBuf.writeInt(one); System.out.println("generate one: " + one); } EmbeddedChannel embeddedChannel = new EmbeddedChannel(); // 獲取channelPipeLine ChannelPipeline channelPipeline = embeddedChannel.pipeline(); channelPipeline.addFirst(new DecodeTest()); channelPipeline.addLast(new SimpleChannelInBoundHandlerTest()); // 寫入測試資料 embeddedChannel.writeInbound(byteBuf); embeddedChannel.finish(); // 驗證測試資料 System.out.println("embeddedChannel readInbound:" + embeddedChannel.readInbound()); System.out.println("embeddedChannel readInbound:" + embeddedChannel.readInbound()); System.out.println("embeddedChannel readInbound:" + embeddedChannel.readInbound()); } } // 解碼器 class DecodeTest extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() >= 4) { out.add(in.readInt()); } } } // channelHandler @SuppressWarnings("rawtypes") class SimpleChannelInBoundHandlerTest extends SimpleChannelInboundHandler { @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("Received message:" + msg); ctx.fireChannelRead(msg); } }