最近读了许多基于Netty的代码,发现Netty实在是网络通信开发的不二之选,值得深入学习。它的性能和可靠性已经有许多应用实践的背书,单从代码来讲,也是极简洁优雅的。
本文提供一个完整入门实例,实现一个简单的客户端和服务器通信的例子:客户端发送数据到服务端,服务端稍微加工然后再发送回客户端。后面也简单提了下出现粘包拆包问题的原因,以及通过Netty编码解码器解决该问题的思路。
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。参见百度百科或Netty官网。
简单的服务器客户端例子。客户端向服务器发送10条记录(Hello Netty!),服务器接收到后发回给客户端,并加上服务器标识,如From Server: Hello Netty!
Github代码链接如下:https://github.com/prufeng/hellowork/tree/master/netty
额,好简单,具体类和参数意义可以查看官网文档的例子。主要是运用Builder模式链式调用的形式,支持按需添加参数、编码解码器(Encoder/Decoder)和处理器(Handler)。
本例使用了StringDecoder
,所以在Handler里可以直接将接收到的数据转换为String
,否则要通过ByteBuf
来转换。
/**
* Created by PanRufeng on 2018/6/22.
*/
public class DemoServer {
private final int port;
public DemoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length == 1) {
port = Integer.parseInt(args[0]);
}
new DemoServer(port).run();
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder()).addLast(new DemoServerHandler());
}
});
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7)
// Wait until the server socket is closed.
System.out.println(DemoServer.class.getName() + " started and listen on " + port);
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
DemoServerHandler
负责事件处理,处理具体的业务逻辑,从方法名看就知道是完全的事件驱动模式。本例读取客户端输入后,后台显示出来,然后添加“From Server:”前缀,再发送回客户端。
/**
* Created by PanRufeng on 2018/6/22.
*/
public class DemoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String inStr = (String) msg;
System.out.println("Received: " + inStr);
String outStr = "From Server: " + inStr + System.lineSeparator();
ByteBuf outBb = Unpooled.copiedBuffer(outStr.getBytes());
ctx.write(outBb);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
PS. 客户端未完成时,可以使用telnet进行测试。telnet
, o localhost 8080
,[Ctrl]+],send hello
telnet发送数据:
Microsoft Telnet> send hello world1
发送字符串 hello world1
Microsoft Telnet> send hello world2
发送字符串 hello world2
Microsoft Telnet> send hello world3
发送字符串 hello world3
服务端返回:
From Server: hello world1
From Server: hello world2
From Server: hello world3
服务端日志:
Received: hello world1
Received: hello world2
Received: hello world3
客户端定义所使用类名与服务端稍微不同,形式基本一致。另外因为是客户端,需要指定host,和定义对应的Handler。
/**
* Created by PanRufeng on 2018/6/22.
*/
public class DemoClient {
private String host;
private int port;
public DemoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws Exception {
String host = "localhost";
int port = 8080;
if (args.length == 2) {
host = args[0];
port = Integer.parseInt(args[1]);
}
new DemoClient(host, port).run();
}
public void run() throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder()).addLast(new DemoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
System.out.println(DemoClient.class.getName() + " started and connected to " + host + ":" + port);
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
DemoClientHandler
负责处理客户端事件,注意父类跟服务端并不相同。方法channelRead0()
读取服务端发送过来的数据。channelActive()
当Channel连接成功就会调用。本例为了简单把客户端发送数据的逻辑放在这里,效果就是当客户端启动时,立即就往服务端发送10条数据。
/**
* Created by PanRufeng on 2018/6/22.
*/
public class DemoClientHandler extends SimpleChannelInboundHandler {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Channel Active");
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer((i + " Hello Netty!").getBytes()));
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) {
String inStr = (String) msg;
System.out.println("Received: " + inStr);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
运行DemoServer,然后启动DemoClient,可得结果如下。
服务端:
pan.rufeng.netty.DemoServer started and listen on 8080
Received: 0 Hello Netty!
Received: 1 Hello Netty!2 Hello Netty!3 Hello Netty!4 Hello Netty!
Received: 5 Hello Netty!6 Hello Netty!7 Hello Netty!
Received: 8 Hello Netty!9 Hello Netty!
客户端:
Channel Active
pan.rufeng.netty.demo.DemoClient started and connected to localhost:8080
Received: From Server: 0 Hello Netty!
From Server: 1 Hello Netty!2 Hello Netty!3 Hello Netty!4 Hello Netty!
From Server: 5 Hello Netty!6 Hello Netty!7 Hello Netty!
From Server: 8 Hello Netty!9 Hello Netty!
至此,一个简单的可以跑起来的客户端服务器通信模型就完成了。
但是,你可能已经发现了这个结果跟预期有些出入,而且,重启客户端,得到的结果又会不一样。结果是不可预测的!
我们可能更希望看到的是客户端每次发送到服务端都得到一次响应,依次打印出10行的结果,可是这里的有些结果却粘在一起,如1,2,3和4,这就是粘包问题。
粘包问题发生的原因是程序每次写入的数据小于Socket缓冲区的大小,这样网卡会把应用程序多次写入的数据一次发送。相应的,如果程序写入的数据大于Socket缓冲区,则会出现拆包问题。表现为完整的句子被断开。
粘包拆包的问题,可以通过编码解码器来解决。具体方法有很多,比如可以定义每个包的前4个字节为包的长度,后面才是真正的数据。
Netty提供了很多内置的工具来解决此类问题,也包括Protobuf等序列化的方式。
对于字符串类型消息,可结合StringDecoder和以下其中一个Decoder来解决,其实质是通过特殊分隔符、分行符或固定长度来判断数据包的结束。
DelimiterBasedFrameDecoder
LineBasedFrameDecoder
FixedLengthFrameDecoder
其他比较有用的类如:
ObjectDecoder
ObjectEncoder
ProtobufDecoder
ProtobufVarint32FrameDecoder
ProtobufEncoder
ProtobufVarint32LengthFieldPrepender
MarshallingDecoder
MarshallingEncoder
ByteToMessageDecoder
ByteToMessageCodec
相关代码和更多方案可以在这个包下面找。
io\netty\handler\codec