2.1 編寫Echo服務器
所有的Netty服務器都需要一下兩個部分:
- 至少一個ChannelHandler
該組件實現了服務器對客戶端接收的數據的處理,即他的業務邏輯。 - 引導
這是配置服務器的啓動代碼. 至少,他會將服務器綁定到它要監聽連接請求的端口上.
2.1.1 ChannelHandler 業務處理邏輯
接口ChannelInboundHandler
用來響應入站事件的方法。使用的時候只需要繼承ChannelInboundHandlerAdapter類就行了。它提供了ChannelInboundHandler的默認實現。
主要的方法:
- channelRead() 對於每個傳入的消息都會調用
- channelReadComplete() 通知ChannelInboundHandler最後一次對channelRead()的調用時當前批量讀取中的最後一個條消息。
- exceptionCaught() 在讀取操作期間,有異常拋出時會調用。
package com.moyang.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;
/**
* nettydemo1 -- 這個組件實現了服務器的業務邏輯,決定了連接創建後和接收到信息後該如何處理
* <p>
* 這種使用 ChannelHandler 的方式體現了關注點分離的設計原則,並簡化業務邏輯的迭代開發的要求
*
* @author 墨陽
* @date 2018-11-09
*/
// 標識ChannelHandler的實例之間可以在Channel裏面共享
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// super.channelRead(ctx, msg);
ByteBuf buf = (ByteBuf) msg;
System.out.println("server has received: " + buf.toString(CharsetUtil.UTF_8));
// 將接受到的消息發送給發送者,注意,這裏並沒有flush。
ctx.write(buf);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 沖刷所有待審消息到遠程節點。並關閉通道
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 打印異常棧跟蹤
cause.printStackTrace();
// 關閉Channel
ctx.close();
}
}
2.2.2 引導服務器
引導服務器主要涉及一下內容:
- 綁定到服務器上,進行監聽,並接受傳入連接請求的端口。
- 配置channel以將有關的入站消息通知給EchoServerHandler實例。
package com.moyang.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;
/**
* nettydemo1
* <p>
* 1.監聽和接收進來的連接請求
* 2.配置 Channel 來通知一個關於入站消息的 EchoServerHandler 實例
*
* @author 墨陽
* @date 2018-11-09
*/
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
int port = 8000;
new EchoServer(port).start();
}
public void start() throws InterruptedException {
// 創建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
// 創建ServerBootStrap
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
// 爲NIO指定使用 NioServerSocketChannel 這種信道類型
.channel(NioServerSocketChannel.class)
// 使用指定的端口設置套接字地址
.localAddress(new InetSocketAddress(port))
// 添加EchoServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() { //添加 EchoServerHandler 到 Channel 的 ChannelPipeline
//當一個新的連接被接受,一個新的子 Channel 將被創建, ChannelInitializer 會添加我們EchoServerHandler 的實例到 Channel 的 ChannelPipeline。
protected void initChannel(SocketChannel ch) throws Exception {
// EchoServerHandler被標註爲@Shareable,所以我們可以總是使用同樣的實例。
ch.pipeline().addLast(new EchoServerHandler());
}
});
// 異步的綁定服務;調用sync()方法阻塞等待直到綁定完成
ChannelFuture f = bootstrap.bind().sync(); // 8
System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
//獲取channel的CloseFuture,調用sync()會導致阻塞當前線程直到它完成。
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
//關閉 EventLoopGroup,釋放所有資源。包括所有創建的線程
group.shutdownGracefully().sync();
}
}
}
ChannelInitializer
,當一個新的連接被接受的時候,一個新的子Channel
將會被建立.ChannelInitializer
將會把一個你的EchoServerHandler
的實例添加到該Channel
的ChannelPipeline
中。這個ChannelHandler
就會收到有關入站消息的通知。
2.2 編寫Echo客戶端
Echo客戶端會完成的工作:
- 連接到服務器
- 發送一個或者多個消息
- 對於每個消息,等待並接受從服務器發回來的消息
- 關閉連接
2.2.1 通過ChannelHandler實現業務邏輯
客戶端將擁有一個用來處理數據庫的ChannelInboundHandler
,客戶端使用SimpleChannelInboundHandler類來完成任務。主要的方法有:
- channelActive() 在到服務器的連接已經建立之後被調用
- channelRead0() 當從服務器接到一條消息的時候被調用
- exceptionCaught() 在處理的過程中已發異常的時候被調用
package com.moyang.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;
/**
* nettydemo1
*
* @author 墨陽
* @date 2018-11-09
*/
// 標記該實例可以被多個Channel共享
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 建立連接後該 channelActive() 方法被調用一次.
* 邏輯很簡單:一旦建立了連接,字節序列被髮送到服務器。
* 該消息的內容並不重要;在這裏,我們使用了 Netty 編碼字符串 “Netty rocks!”
* 通過覆蓋這種方法,我們確保東西被儘快寫入到服務器。
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("netty rocks!!! ", CharsetUtil.UTF_8));
}
/**
* 這種方法會在接收到數據時被調用。
* 注意,由服務器所發送的消息可以以塊的形式被接收。
* 即,當服務器發送 5 個字節是不是保證所有的 5 個字節會立刻收到 - 即使是隻有 5 個字節,
* channelRead0() 方法可被調用兩次,第一次用一個ByteBuf(Netty的字節容器)裝載3個字節和第二次一個 ByteBuf 裝載 2 個字節。
* 唯一要保證的是,該字節將按照它們發送的順序分別被接收。 (注意,這是真實的,只有面向流的協議如TCP)。
*
* @param ctx
* @param msg
* @throws Exception
*/
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
System.out.println("Client Received: " + msg.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
思考
爲什麼客戶端使用的SimpleChannelInboundHandler而服務端使用的ChannelInboundHandler?
這個關係到:業務邏輯如何處理消息以及Netty如何管理資源.
在客戶端,當channelRead0()方法完成時,你已經有了傳入消息,並且已經處理完它了。當該方法返回時,SimpleChannelInboundHandler 負責釋放指向保存該消息的ByteBuf 的內存引用。在EchoServerHandler 中,你仍然需要將傳入消息回送給發送者,而write()操作是異步的,直到channelRead()方法返回後可能仍然沒有完成(如代碼清單2-1 所示)。爲此,EchoServerHandler擴展了ChannelInboundHandlerAdapter,其在這個時間點上不會釋放消息。消息在EchoServerHandler 的channelReadComplete()方法中,當writeAndFlush()方法被調用時被釋放
2.4.2 引導客戶端
package com.moyang.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;
/**
* nettydemo1
*
* @author 墨陽
* @date 2018-11-09
*/
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
Bootstrap bootstrap = new Bootstrap();
// 指定EventLoopGroup以處理客戶端事件;
bootstrap.group(group)
.channel(NioSocketChannel.class)
// 設置服務器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 連接到遠程節點,調用sync()阻塞直到連接完成
ChannelFuture f = bootstrap.connect().sync();
// 阻塞,直到Channel關閉
f.channel().closeFuture().sync();
} finally {
// 關閉線程池,釋放所有資源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
String host = "127.0.0.1";
int port = 8000;
new EchoClient(host, port).start();
}
}
注意:
可以在客戶端和服務端使用不同的傳輸。
最後
如果你覺得寫的還不錯,就關注下公衆號唄,關注後,有點小禮物回贈給你。
你可以獲得5000+電子書,java,springCloud,adroid,python等各種視頻教程,IT類經典書籍,各種軟件的安裝及破解教程。
希望一塊學習,一塊進步!