使用Netty實現文本協議,即直接處理字符串,而不是字節,字節到字符串的編碼解碼工作都由Handler來完成,主要在於以下三個Handler:
1.DelimiterBasedFrameDecoder:基於定界符的幀解碼器,即根據指定的分隔符,來判斷協議中幀的界限,如下
new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter())
第一個參數爲最大幀長度,第二個參數爲幀分隔符,這裏使用的是行分隔符
2.StringDecoder:字符串解碼器,將字節轉換爲字符串,即ByteBuf轉換爲String,可以設置需要的字符集,在接收數據時使用
3.StringEncoder:字符串編碼器,將字符串轉換爲字節,即String轉換爲ByteBuf,可以設置需要的字符串,在寫出數據時使用
TelnetClient
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* telnet協議客戶端
*/
@Slf4j
public final class TelnetClient {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8080"));
public static void main(String[] args) throws Exception {
/* SSL上下文 */
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new TelnetClientInitializer(sslCtx));
/* 啓動連接 */
Channel ch = b.connect(HOST, PORT).sync().channel();
log.info("客戶端啓動");
/* 最後一次異步操作結果 */
ChannelFuture lastWriteFuture = null;
/* 從命令行讀取輸入 */
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
break;
}
/* 發送讀取信息到服務端 */
lastWriteFuture = ch.writeAndFlush(line + "\r\n");
/* 如果命令爲bye,則關閉Channel */
if ("bye".equals(line.toLowerCase())) {
ch.closeFuture().sync();
break;
}
}
/* 等待最後異步操作結果完成 */
if (lastWriteFuture != null) {
lastWriteFuture.sync();
}
} finally {
group.shutdownGracefully();
log.info("客戶端關閉");
}
}
}
1.Channel寫出數據時,使用\r\n結尾,這裏\r\n作爲幀的定界符使用
2.當輸入bye命令時,客戶端關閉
TelnetClientInitializer
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.ssl.SslContext;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TelnetClientInitializer extends ChannelInitializer<SocketChannel> {
/* netty字符串編碼解碼 */
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final TelnetClientHandler CLIENT_HANDLER = new TelnetClientHandler();
private final SslContext sslCtx;
public TelnetClientInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
/* SSL處理器 */
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc(), TelnetClient.HOST, TelnetClient.PORT));
}
/* 首先添加文本行編解碼器組合 */
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
pipeline.addLast(CLIENT_HANDLER);
}
}
1.除了SSL,使用了三個前置Handler,分別是定界符處理器、字符串編碼器、字符串解碼器
2.最後使用的是業務Handler,保證業務Handler接收到的是String,且其寫出的String會被轉換爲字節
TelnetClientHander
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Sharable
public class TelnetClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
log.info("客戶端讀取消息:"+msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("客戶端異常",cause);
cause.printStackTrace();
ctx.close();
}
}
1.業務Handler繼承了SimpleChannelnBoundHandler<String>,這裏的泛型String對應了字節的編碼解碼結果
TelnetServerInitializer
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.ssl.SslContext;
public class TelnetServerInitializer extends ChannelInitializer<SocketChannel> {
private static final StringDecoder DECODER = new StringDecoder();
private static final StringEncoder ENCODER = new StringEncoder();
private static final TelnetServerHandler SERVER_HANDLER = new TelnetServerHandler();
private final SslContext sslCtx;
public TelnetServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
/* 首先添加文本行編解碼器組合 */
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
pipeline.addLast(DECODER);
pipeline.addLast(ENCODER);
pipeline.addLast(SERVER_HANDLER);
}
1.服務端與客戶端的Handler設置相同
TelnetServerHandler
@Slf4j
@Sharable
public class TelnetServerHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel激活,發送問候語");
ctx.write("Welcome to " + InetAddress.getLocalHost().getHostName() + "!\r\n");
ctx.write("It is " + new Date() + " now.\r\n");
ctx.flush();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {
String response;
boolean close = false;
if (request.isEmpty()) {
log.info("客戶端信息爲空");
response = "Please type something.\r\n";
} else if ("bye".equals(request.toLowerCase())) {
log.info("客戶端拜拜");
response = "Have a good day!\r\n";
close = true;
} else {
response = "Did you say '" + request + "'?\r\n";
}
/* 我們不需要在這裏編寫ChannelBuffer。 插入TelnetPipelineFactory的編碼器將進行轉換 */
ChannelFuture future = ctx.write(response);
if (close) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1.服務端每次寫出數據也需要使用\r\n作爲定界符