netty实战
1. 背景
此篇主要是针对中文传输时的编码问题进行实战。
2. telnet 命令实验
使用命令行,发送汉字“你好”。
2.1 GBK编码
win10系统下,打开命令行使用chcp命令可以看到显示活动代码页:936,936表示是GBK编码,使用telnet 127.0.0.1 8001命令后,输入“你好”回车,看到服务端接收到4个字节如下:
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| c4 |. |
+--------+-------------------------------------------------+----------------+
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| e3 |. |
+--------+-------------------------------------------------+----------------+
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| ba |. |
+--------+-------------------------------------------------+----------------+
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| c3 |. |
+--------+-------------------------------------------------+----------------+
2.2 UTF-8编码
打开命令行,使用chcp 65001,将命令行切换为utf-8编码环境,使用telnet 127.0.0.1 8001命令后,输入“你好”回车,不知什么原因闪退,所以无法验证,服务端后台输出日志如下:
13:44:12.494 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 - R:/127.0.0.1:1525] REGISTERED
13:44:12.495 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 - R:/127.0.0.1:1525] ACTIVE
13:44:17.153 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 - R:/127.0.0.1:1525] READ COMPLETE
13:44:17.153 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 - R:/127.0.0.1:1525] FLUSH
13:44:17.153 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 ! R:/127.0.0.1:1525] INACTIVE
13:44:17.153 [nioEventLoopGroup-3-2] INFO io.netty.handler.logging.LoggingHandler - [id: 0x03215ed2, L:/127.0.0.1:8001 ! R:/127.0.0.1:1525] UNREGISTERED
3. 客户端单元代码
3.1 jdk原生
写一个main方法,实现java原生socket客户端,头两字节表示报文长度,来验证服务端对两种编码的报文处理。
3.1.1 GBK编码(JDK 原生)
服务端后台日志显示,读取了6B,头俩字节表示报文长度为4B,“你好”汉字的十六进制为“c4 e3 ba c3”,一个汉字占2B。
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 30 34 c4 e3 ba c3 |04.... |
+--------+-------------------------------------------------+----------------+
3.1.2 UTF-8编码(JDK 原生)
服务端后台日志显示,读取了8B,头俩字节表示报文长度为6B,“你好”汉字的十六进制为“e4 bd a0 e5 a5 bd”,一个汉字占3B。
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 30 36 e4 bd a0 e5 a5 bd |06...... |
+--------+-------------------------------------------------+----------------+
3.2 netty客户端
使用netty框架编写一个main方法,直接发送报文内容来验证服务端对两种编码的报文处理。
3.2.1 GBK编码(netty客户端)
客户端指定编码为GBK格式。
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(new StringEncoder(Charset.forName("GBK")));
}
});
服务端后台读取报文日志如下:
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| c4 e3 ba c3 |.... |
+--------+-------------------------------------------------+----------------+
3.2.2 UTF-8编码(netty客户端)
客户端指定编码为UTF-8格式,代码同上,只把编码格式改了即可。
服务端后台读取报文日志如下:
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| e4 bd a0 e5 a5 bd |...... |
+--------+-------------------------------------------------+----------------+
4. 小结
不同编码的中文字节是不同的,我们实际开发中在报文通信标准中会明确表示中文采用哪一种编码方式,避免通信两端出现中文乱码问题。
假如针对老系统升级,面向的客户端是不同的主体,并且因为各种原因出现了编码不一致的问题(实际情况下很难出现的场景),那么我们该如何处理?之前我想到一种办法是使用Nginx代理stream,但是经过验证不可行。后边我还有一种想法是通过程序来实现,根据对端的IP来指定不同的编码方式,采用配置化的方式实现。此种方式有待于进一步学习动态handler来进行验证。
5. 附
5.1 服务端代码
public class FirstHandler extends ChannelInboundHandlerAdapter {
public static final Logger log = LoggerFactory.getLogger(FirstHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服务端接收消息是:{}", msg);
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("exceptionCaught: " + cause.getLocalizedMessage());
}
}
public class FirstServer {
public static final Logger log = LoggerFactory.getLogger(FirstServer.class);
private final String ip = "127.0.0.1";
private final String port = "8001";
public void init(){
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup, workerGroup);
bs.channel(NioServerSocketChannel.class);
bs.childHandler(new ChannelInitializer<Channel>(){
@Override
protected void initChannel(Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(new FirstHandler());
}
});
try {
ChannelFuture channelFuture = bs.bind(ip, Integer.parseInt(port)).sync();
log.info("Netty Server 启动成功! Ip: " + channelFuture.channel().localAddress().toString() + " ! ");;
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
5.2 客户端代码
5.2.1 netty客户端
public class CodeClient {
public void connect(int port, String host) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
}
});
ChannelFuture future = bootstrap.connect("127.0.0.1", 8001).sync();
future.channel().writeAndFlush("你好");
future.syncUninterruptibly();
future.channel().closeFuture().sync();
}
public static void main(String[] args) throws InterruptedException {
new CodeClient().connect(8001, "127.0.0.1");
}
}
5.2.2 jdk原生
public class InData extends BufferedInputStream {
private static final Logger logger = LoggerFactory.getLogger(InData.class);
private byte[] bdat;
private int head;
private int len;
private String decode;
public InData(InputStream in, int head, String decode) {
super(in);
this.head = head;
bdat = new byte[this.head];
this.decode = decode;
}
public String readLine() throws Exception {
String indata;
int ret = 0;
try {
ret = read(bdat, 0, head);
} catch (IOException ioe) {
throw ioe;
}
indata = new String(bdat, decode);
len = (new Integer(indata.trim())).intValue();
logger.info("接收数据长度 <<[" + indata + "]");
bdat = new byte[len];
try {
ret = read(bdat, 0, len);
} catch (IOException e) {
logger.info("ioe : " + e.toString());
throw e;
}
if (ret != len || ret <= 0) {
throw new Exception("报文包头长度错误");
}
indata = new String(bdat, decode);
logger.info("接收数据 <<[" + indata+ "]");
return indata;
}
}
public class OutData extends BufferedOutputStream {
private static final Logger logger = LoggerFactory.getLogger(OutData.class);
private byte[] bdat;
private int head_len;
private String outdata = "";
private String encode;
public OutData(OutputStream out, int head_len, String encode) {
super(out);
this.head_len = head_len;
this.encode = encode;
}
public int writeLine(String outdata) {
String tmp;
int len_src;
try {
tmp = new String(outdata.getBytes(), encode);
} catch (UnsupportedEncodingException ex) {
return -1;
}
len_src = tmp.getBytes().length;
for (int i = 0; i < (head_len - new Integer(len_src).toString().length()); i++)
this.outdata = this.outdata.concat(" ");
this.outdata = this.outdata + new Integer(len_src).toString() + outdata;
len_src = len_src + head_len;
try {
bdat = new byte[len_src];
bdat = this.outdata.getBytes();
this.write(bdat, 0, len_src);
this.flush();
} catch (IOException ioe) {
logger.info("发送服务器数据时 异常 : " + ioe.toString());
return -1;
}
return 0;
}
}
public class SocketUtils {
private Socket serv;
private InData inServer;
private OutData outServer;
private String codeing;
public void init(String ip, int port, int head, String codeing) throws Exception {
try {
serv = new Socket(ip, port);
serv.setSoTimeout(3600000);
} catch (UnknownHostException uhe) {
throw new Exception(uhe.toString());
} catch (IOException ioe) {
throw new Exception(ioe.toString());
} catch (Exception e) {
throw new Exception(e.toString());
}
this.codeing = codeing;
inServer = new InData(serv.getInputStream(), head, this.codeing);
outServer = new OutData(serv.getOutputStream(), head, this.codeing);
}
public String send(String sendline) throws Exception {
String recvline;
try {
if (outServer.writeLine(sendline) != 0) {
throw new Exception("发送服务器数据异常");
}
recvline = inServer.readLine();
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw e;
}
return recvline;
}
public void close() throws Exception {
if (inServer != null) {
try {
inServer.close();
} catch (Exception e) {
throw e;
}
}
if (outServer != null) {
try {
outServer.close();
} catch (Exception e) {
throw e;
}
}
if (serv != null) {
try {
serv.close();
} catch (Exception e) {
throw e;
}
}
}
}