本章使用Netty開發一個入門程式,使用ServerBootstrap開發時間服務TimeServer,使用Bootstrap開發客戶端TimeClient請求TimeServer獲取時間。
開發 TimeServer之前,先回顧一下使用 NIO 進行服務端開發的步驟。
(1)建立 ServerSocketChannel,配置它為非阻塞模式;
(2)繫結監聽,配置 TCP 引數,例如 backlog 大小;
(3)建立一個獨立的 IO執行緒,用於輪詢多路複用器 Seleetor;
(4)建立 Selector,將之前建立的 ServerSocketChannel 註冊到 Selector 上, 監聽SelectionKey.ACCEPT;
(5)啟動IO執行緒,在迴圈體中執行 Selector.select()方法,輪詢就緒的 Channel;
(6)當輪詢到了處於就緒狀態的 Channel 時,需要對其進行判浙,如果是 OP_ACCEPT狀態,說明是新的客戶端接入,則呼叫ServerSocketChannel.accept()方法接受新的客戶端;
(7)設定新接入的客戶端鏈路 SocketChannel 為非阻塞模式,配置其他的一些 TCP 引數;
(8)將 SocketChannel 註冊到 Seleetor,監聽 OP READ 操作位;
(9)如果輪詢的 Channel 為 OP READ,則說明 SocketChannel 中有新的就緒的資料包需要讀取,則構造 Byte Buffer 物件,讀取資料包;
(10)如果輪詢的 Channel 為 OP WRITE,說明還有資料沒有傳送完成,需要繼續傳送。
詳見 JAVA非阻塞IO、非同步IO(NIO、AIO)
一、Netty入門程式
1.1 Netty開發服務端程式碼的流程:
1.建立EventLoopGroup EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); 2.初始化ServerBootstrap ServerBootstrap bootstrap = new ServerBootstrap(); 3.配置ServerBootstrap bootstrap.group(bossGroup, workerGroup); bootstrap.channel(NioServerSocketChannel.class); bootstrap.option(ChannelOption.SO_BACKLOG, 128); bootstrap.childHandler() 4.繫結埠並啟動服務 try { ChannelFuture future = bootstrap.bind(PORT).sync(); future.channel().closeFuture().sync(); } catch (InterruptedException e) { ... } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } 在這個過程中,bind方法會同步阻塞,直到服務端繫結成功。當關閉future的channel時,表示服務端已關閉,這時可以安全地關閉EventLoopGroup,釋放資源。
1.1.1 服務端完整程式碼
TimeServer
package Netty.Server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; /** * Created by Idea 14 whih "netty" * * @Auhor: karl.zhao * @Email: karl.zhao@qq.com * @Date: 2015-11-28 * @Time: 15:54 */ public class TimeServer { public void bind(int port)throws Exception{ /* 配置服務端的NIO執行緒組 */ // NioEventLoopGroup類 是個執行緒組,包含一組NIO執行緒,用於網路事件的處理 // (實際上它就是Reactor執行緒組)。 // 建立的2個執行緒組,1個是服務端接收客戶端的連線,另一個是進行SocketChannel的 // 網路讀寫 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup WorkerGroup = new NioEventLoopGroup(); try { // ServerBootstrap 類,是啟動NIO伺服器的輔助啟動類 ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup,WorkerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG,1024) .childHandler(new ChildChannelHandler()); // 繫結埠,同步等待成功 ChannelFuture f= b.bind(port).sync(); // 等待服務端監聽埠關閉 f.channel().closeFuture().sync(); }finally { // 釋放執行緒池資源 bossGroup.shutdownGracefully(); WorkerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{ @Override protected void initChannel(SocketChannel arg0)throws Exception{ arg0.pipeline().addLast(new TimeServerHandler()); } } public static void main(String[]args)throws Exception{ int port = 8080; if(args!=null && args.length>0){ try { port = Integer.valueOf(args[0]); } catch (NumberFormatException ex){} } new TimeServer().bind(port); } }
TimeServerHandler
package Netty.Server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import java.util.Date; /** * Created by Idea 14 whih "netty" * * @Auhor: karl.zhao * @Email: karl.zhao@qq.com * @Date: 2015-11-28 * @Time: 16:13 */ public class TimeServerHandler extends ChannelHandlerAdapter { // 用於網路的讀寫操作 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("the time server order : " + body); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date( System.currentTimeMillis()).toString() : "BAD ORDER"; ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.write(resp); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); // 它的作用是把訊息傳送佇列中的訊息寫入SocketChannel中傳送給對方 // 為了防止頻繁的喚醒Selector進行訊息傳送,Netty的write方法,並不直接將訊息寫入SocketChannel中 // 呼叫write方法只是把待傳送的訊息發到緩衝區中,再呼叫flush,將傳送緩衝區中的訊息 // 全部寫到SocketChannel中。 } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } }
1.2 Netty開發客戶端程式碼的流程:
1.建立EventLoopGroup EventLoopGroup group = new NioEventLoopGroup(); 2.初始化Bootstrap Bootstrap bootstrap = new Bootstrap(); 3.配置Bootstrap bootstrap.group(group); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000); bootstrap.handler() 4.連線伺服器 String host = "localhost"; int port = 8080; ChannelFuture future = bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { System.out.println("Connected to the server successfully."); } else { System.err.println("Failed to connect to the server."); future.cause().printStackTrace(); } }); // 等待連線成功或失敗 future.syncUninterruptibly(); 5.關閉連線和EventLoopGroup Channel channel = future.channel(); ... channel.closeFuture().sync(); group.shutdownGracefully();
1.2.1 客戶端完整程式碼
TimeClient
package Netty.Client; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; /** * Created by Idea 14 whih "netty" * * @Auhor: karl.zhao * @Email: karl.zhao@qq.com * @Date: 2015-11-28 * @Time: 16:56 */ public class TimeClient { public void connect(String host, int port) throws Exception { // 配置服務端的NIO執行緒組 EventLoopGroup group = new NioEventLoopGroup(); try { // Bootstrap 類,是啟動NIO伺服器的輔助啟動類 Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); // 發起非同步連線操作 ChannelFuture f = b.connect(host, port).sync(); // 等待客服端鏈路關閉 f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException ex) { } } new TimeClient().connect("127.0.0.1", port); } }
TimeClientHandler
package Netty.Client; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import java.util.logging.Logger; /** * Created by Idea 14 whih "netty" * * @Auhor: karl.zhao * @Email: karl.zhao@qq.com * @Date: 2015-11-28 * @Time: 16:58 */ public class TimeClientHandler extends ChannelHandlerAdapter { // 寫日誌 private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName()); private final ByteBuf firstMessage; public TimeClientHandler() { byte[] req = "QUERY TIME ORDER".getBytes(); firstMessage = Unpooled.buffer(req.length); firstMessage.writeBytes(req); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("Now is : " + body); } @Override public void channelActive(ChannelHandlerContext ctx) { // 當客戶端和服務端建立tcp成功之後,Netty的NIO執行緒會呼叫channelActive // 傳送查詢時間的指令給服務端。 // 呼叫ChannelHandlerContext的writeAndFlush方法,將請求訊息傳送給服務端 // 當服務端應答時,channelRead方法被呼叫 ctx.writeAndFlush(firstMessage); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { logger.warning("message from:" + cause.getMessage()); ctx.close(); } }
1.3 執行結果
啟動TimeServer,再啟動TimeClient ,執行結果如下:
服務端:
客戶端:
摘自: 李林峰《netty權威指南》