为什么选择Netty?开发高质量的NIO程序并不是一件简单的事情,出去NIO的复杂性和BUG不谈,作为一个NIO服务器,要能处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写情况,如果没有足够的NIO编程经验累积,一个NIO框架的稳定往往需要半年甚至更长的实际。并且从维护性角度而言,NIO采用了异步非阻塞编程模型,而且是一个I/O线性处理多条链路,调试和跟踪非常麻烦,我们无法进行有效的调试和跟踪,定位难度很大。同时NIO编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
而Netty具有以下优点:1、API使用简单,开发门槛低。2、预置了多种编解码功能。3、定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展。4、Netty修复了已经发现的NIO的BUG。Netyy在大数据、互联网等众多行业进行了成功的商用,正因这些优点,Netty成为了 Java NIO编程的首选框架。
Netty相比原生的NIO做了哪些改变?
从官网上 https://netty.io/downloads.html /maven repostory下载压缩包:新建java项目,将all in one中的netty-all .jar拷贝至工程lib下,并添加至构建路径即可。
服务端时间服务器:
1、先创建两个NioEventLoopGroup线程组(Reactor线程组),一个NioEventLoopGroup包含了一组NIO线程,一个线程组用于服务端接收客户端的连接,另一个用于进行SocketChannel的网络读写。
EventLoopGroup bossGroup = new NioEventLoopGroup();// 用户服务端接收客户端的链接
EventLoopGroup workerGroup = new NioEventLoopGroup();// 用户进行SocketChannel的网络读写
2、创建ServerBootstrap对象,用于辅助NIO服务端启动,目的是降低服务端开发的复杂度。
ServerBootstrap b = new ServerBootstrap();
3、调用ServerBootstrap的group方法,将上面的两个NIO线程组传入该方法,同时要配置NioServerSocketChannel的TCP参数,还要绑定I/O事件的处理类ChildChannelHandler,事件处理类ChildChannelHandler类似Reactor模式中Handler类,处理网络I/O事件,例如记录日志、对消息进行编码等。ChildChannelHandler需要继承ChannelInitializer抽象类,实现initChannel方法。
b.group(bossGroup, workerGroup)// 将两个NIO线程组当做入参传递到ServerBootstrap中
.channel(NioServerSocketChannel.class)
//创建NioServerSocketChannel 对应JDK NIO库中的ServerSocketChannel类 , 指定通道类型为NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG, 1024)
//然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024
//最后绑定IO事件的处理类ChildChannelHandler,类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,消息编解码等
.childHandler(new ChildChannelHandler());
4、服务器启动辅助类配置完成后,调用它的bind方法绑定监听端口,然后调用它的同步阻塞方法sync等待绑定完成,之后Netty返回一个ChannelFuture,功能类似java.util.concurrent.Future,用于异步操作的通知回调
ChannelFuture f = b.bind(port).sync();
5、使用f.channel().closeFuture().sync()方法进行阻塞,等待服务端链路关闭之后main函数才退出。
f.channel().closeFuture().sync();
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
public class TimeServer {
public void bind(int port) throws Exception {
// 配置服务端的NIO线程组 ,专门用于网络事件的处理,实际上他们就是Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();// 用户服务端接收客户端的链接
EventLoopGroup workerGroup = new NioEventLoopGroup();// 用户进行SocketChannel的网络读写
try {
// ServerBootstrap对象实际上是netty用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)// 将两个NIO线程组当做入参传递到ServerBootstrap中
.channel(NioServerSocketChannel.class)//创建NioServerSocketChannel 对应JDK NIO库中的ServerSocketChannel类 , 指定通道类型为NioServerSocketChannel
.option(ChannelOption.SO_BACKLOG, 1024)//然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024
//最后绑定IO事件的处理类ChildChannelHandler,类似于Reactor模式中的Handler类,主要用于处理网络IO事件,例如记录日志,对消息进行编解码等
.childHandler(new ChildChannelHandler());
// 绑定端口,同步方法等待绑定操作完成 ,返回ChannelFuture主要用于异步操作的通知回调.
ChannelFuture f = b.bind(port).sync();
// 等待阻塞,等待服务端链路关闭之后main函数才退出
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 调用childHandler 来指定连接后调用的ChannelHandler,这个方法传ChannelInitalizer类型的参数
// ChannelInitalizer是个抽象类,所以要实现initChannel方法, 这个方法及时用来设置ChannelHandler的
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new TimeServerHandler());
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 30000;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new TimeServer().bind(port);
}
}
1、首先将接收到的数据做类型转换,将msg消息转换成Netty的ByteBuf对象,即相当于jdk中的java.nio.ByteBuffer对象,通过ByteBuf的readableBytes方法可以获取缓冲区可读的字节数,根据字节数创建byte数组,通过ByteBuf的readBytes方法将缓冲区的字节数组复制到新建的byte数组中,最后通过NewString构造字符串。由于没有加StringDecoder、StringEncoder因此,此处需要类型转换。
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
2、在读完后,会调用channelReadComplete方法,由于在读方法中有异步发送应答操作,因此channelReadComplete方法中调用ChannelHandlerContext的flush方法,将消息发送队列中的消息写入到SocketChannel中发送给对象,为了防止频繁的换下Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中,调用write方法只是把待发送的消息放到发送缓冲数组中,再次调用flush方法,将缓冲区中的消息全部写入到SocketChannel。
ctx.write(resp);// 异步发送应答消息给客户端
// 再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中
ctx.flush();
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;
public class TimeServerHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger
.getLogger(TimeServerHandler.class.getName());
@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 receive order : " + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);// 异步发送应答消息给客户端
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 将发送缓冲区中的消息全部写到SocketChannel中发送给对方。
// 从性能角度考虑,为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中
// 调用write方法只是把待发送的消息放到发送缓冲区数中,
// 再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.warning("Unexpected exception from downstream : "
+ cause.getMessage());
/**
* 当发生异常时,关闭ChannelHandlerContext,释放ChannelHandlerContext相关联的句柄等资源
*/
ctx.close();
}
}
客户端类:
1、客户端首先创建I/O读写的NioEventLoopGroup线程组,然后继续创建客户端辅助启动类Bootstrap,随后对其配置,与服务端不同的是,它的Channel需要设置为NioSocketChannel,然后为其添加Handler,此处为了简单直接创建了匿名内部类,实现initChannel方法,作用是当创建NioSocketChannel,成功之后,进行初始化时,将ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();// 客户端辅助启动类,然后对其进行配置
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
// 作用是当创建NioSocketChannel成功之后,在进行初始化时,
// 将ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
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;
public class TimeClient {
public void connect(int port, String host) throws Exception {
// 配置客户端 NIO 线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();// 客户端辅助启动类,然后对其进行配置
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
// 作用是当创建NioSocketChannel成功之后,在进行初始化时,
// 将ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
.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 {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 30000;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new TimeClient().connect(port, "127.0.0.1");
}
TimeClientHandler重点关注三个方法:channelActive、channelRead和exceptionCaught,当客户单与服务器TCP链路建立成功后,Netty的NIO线程调用channelActive方法,发送查询时间的指令给服务器端,调用ChannelHandlerContext的writeAndFlush方法将请求发送给服务端。当服务端返回应答消息,channelRead方法被调用。
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;
public class TimeClientHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger
.getLogger(TimeClientHandler.class.getName());
private final ByteBuf firstMessage;
/**
* Creates a client-side handler.
*/
public TimeClientHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
/**
* 当客户端和服务端TCP链路简历成功之后,Netty的NIO线路会调用channelActive方法,发送查询时间的指令给服务器
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(firstMessage);// 调用 writeAndFlush 方法将请求消息发送给服务端
}
/**
* 当服务端返回应答消息时,channelRead 方法被调用
* @param ctx
* @param msg
* @throws Exception
*/
@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);
}
/**
* 发生异常时,打印异常日志,释放客户端资源
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 当发生异常时,打印异常日志,释放客户端资源
logger.warning("Unexpected exception from downstream : "
+ cause.getMessage());
ctx.close();
}
}
一个完整的业务可能会被TCP拆分成多个包进行发送(拆包), 也有可能把多个小的包封装成一个大的数据包发送(粘包), 这个就是TCP的拆包和封包问题。
解析上图:
由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包/粘包的问题。
Server改造上例主要体现在:TimeServerHandler上,每接收到一条消息,就记一次数,然后发送应答消息给客户端。按照设计服务端接收的消息总数应该与客户端发送的消息总数相同,
TimeServerHandler:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class TimeServerHandler extends ChannelHandlerAdapter{
private int counter;
@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 receive order : "+body
+" ; the counter is : " + ++counter);
String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
currentTime=currentTime + System.getProperty("line.separator");
ByteBuf resp =Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
TimeServer:不改造
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
public class TimeServer {
public void bind(int port)throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try{
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;
new TimeServer().bind(port);
}
}
客户端在上面的例子中主要更改为客户端与服务端链路创建成功后,在TimeClientHandler中循环发送100条消息,每发送一条就刷新一次,保证每条消息偶读会被写入Channel中,按照这个设计,服务器端应该接收到100条查询事件指令的请求消息。
TimeClient :与上例不变
import io.netty.bootstrap.Bootstrap;
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;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
public class TimeClient {
public void connect(int port,String host) throws Exception{
EventLoopGroup group=new NioEventLoopGroup();
try{
Bootstrap b=new Bootstrap();
//Channel需要设置为NioSocketChannel,然后为其添加Handler
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>(){
//为了简单直接创建匿名内部类,实现initChannel方法
//其作用是当创建NioSocketChannel成功之后,在进行初始化时,
//将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(new TimeClientHandler());
}
});
//发起异步连接,然后调用同步方法等待连接成功
ChannelFuture f=b.connect(host,port).sync();
//当客户端连接关闭之后,客户端主函数退出,退出前释放NIO线程组的资源
f.channel().closeFuture().sync();
}finally{
}
}
public static void main(String[] args) throws Exception {
int port=30000;
new TimeClient().connect(port, "127.0.0.1");
}
}
TimeClientHandler如下:
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class TimeClientHandler extends ChannelHandlerAdapter{
private static final Logger logger=Logger.getLogger(TimeClientHandler.class.getName());
private int counter;
private byte[] req;
public TimeClientHandler(){
req=("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
/**
* 当客户端和服务器TCP链路建立成功后,NIO线程会调用channelActive方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx){
//发送查询时间的指令给服务端
ByteBuf message=null;
//循环发送100条消息,每发送一条就刷新一次
for(int i=0;i<100;i++){
message=Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
/**
* 当服务端返回应答消息时调用
*/
@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 + " ; the counter is : " + ++counter);
}
/**
* 当发生异常时
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
logger.warning("Unexpected exception from downstrea : " + cause.getMessage());
ctx.close();
}
}
运行结果:client实际上发了100条“QUERY TIME ORDER”指令,但是服务端运行结果表明只接收到了两条消息(经管这两条中包含了100次的QUERY TIME ORDER),所以count只执行了两次。
同时:服务端由于只收到2条请求消息,所以也只反馈了两条BAD ORDER给服务端。但是Client以为是收到一条包含2条“BAD ORDER”的指令消息,所以服务端返回的应答消息也发生了粘包。
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器,只要数列掌握这些类库,TCP粘包问题变得非常容易,这也是其他NIO框架和原生的NIO API无法匹敌的。
我们在原来存在粘包的TimeServer中的ChildChannelHandler新增两个解码器:LineBasedFrameDecoder和StringDecoder。LineBasedFrameDecoder 是依次遍历ByteBuf中的可读字节,判断看是否有\n 或 \r\n,如果有,就以此位置为结束位置,以换行符为结束标志的解码器。它支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。
private class ChildChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
//增加的两个解码器
arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new TimeServerHandler());
}
}
此时在TimeServerHandler中的channelRead读取到的msg直接强制转换成String,就是删除回车换行符的请求消息,就是我们需要的请求消息。
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
String body=(String)msg;
System.out.println("The time server receive order : "+body
+" ; the counter is : " + ++counter);
String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
currentTime=currentTime + System.getProperty("line.separator");
ByteBuf resp =Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
客户端也类似在TimeClient的ChannelInitializer< SocketChannel >也新增LineBasedFrameDecoder和StringDecoder解码器。
public void connect(int port,String host) throws Exception{
EventLoopGroup group=new NioEventLoopGroup();
try{
Bootstrap b=new Bootstrap();
//Channel需要设置为NioSocketChannel,然后为其添加Handler
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer(){
//为了简单直接创建匿名内部类,实现initChannel方法
//其作用是当创建NioSocketChannel成功之后,在进行初始化时,
//将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
}
});
//发起异步连接,然后调用同步方法等待连接成功
ChannelFuture f=b.connect(host,port).sync();
//当客户端连接关闭之后,客户端主函数退出,退出前释放NIO线程组的资源
f.channel().closeFuture().sync();
}finally{
}
}
TimeClientHandler中的channelRead方法读取到的msg也是解码后的应答消息,相比之前的简洁了许多:
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
String body=(String)msg;
System.out.println("Now is : " + body + " ; the counter is : " + ++counter);
}
完整源码:
TimerClient:
import io.netty.bootstrap.Bootstrap;
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;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
public class TimeClient {
public void connect(int port,String host) throws Exception{
EventLoopGroup group=new NioEventLoopGroup();
try{
Bootstrap b=new Bootstrap();
//Channel需要设置为NioSocketChannel,然后为其添加Handler
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>(){
//为了简单直接创建匿名内部类,实现initChannel方法
//其作用是当创建NioSocketChannel成功之后,在进行初始化时,
//将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
}
});
//发起异步连接,然后调用同步方法等待连接成功
ChannelFuture f=b.connect(host,port).sync();
//当客户端连接关闭之后,客户端主函数退出,退出前释放NIO线程组的资源
f.channel().closeFuture().sync();
}finally{
}
}
public static void main(String[] args) throws Exception {
int port=30000;
new TimeClient().connect(port, "127.0.0.1");
}
}
TimeClientHandler:
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class TimeClientHandler extends ChannelHandlerAdapter{
private static final Logger logger=Logger.getLogger(TimeClientHandler.class.getName());
private int counter;
private byte[] req;
public TimeClientHandler(){
req=("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
/**
* 当客户端和服务器TCP链路建立成功后,NIO线程会调用channelActive方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx){
//发送查询时间的指令给服务端
ByteBuf message=null;
//循环发送100条消息,每发送一条就刷新一次
for(int i=0;i<100;i++){
message=Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
/**
* 当服务端返回应答消息时调用
*/
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
String body=(String)msg;
System.out.println("Now is : " + body + " ; the counter is : " + ++counter);
}
/**
* 当发生异常时
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
logger.warning("Unexpected exception from downstrea : " + cause.getMessage());
ctx.close();
}
}
TimeServer:
import io.netty.bootstrap.ServerBootstrap;
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.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class TimeServer {
public void bind(int port)throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try{
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 LineBasedFrameDecoder(1024));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws Exception{
int port=30000;
new TimeServer().bind(port);
}
}
TimeServerHandler:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class TimeServerHandler extends ChannelHandlerAdapter{
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg)throws Exception{
String body=(String)msg;
System.out.println("The time server receive order : "+body
+" ; the counter is : " + ++counter);
String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
currentTime=currentTime + System.getProperty("line.separator");
ByteBuf resp =Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
TCP以流的形式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下的4种方式。
(1)消息长度固定,累计读到长度总和为定长len的报文后,就认为读取到了一个完整的消息;然后重新开始读取下一个“完整”的数据包;
(2)将回车换行符作为消息结束符,如ftp协议;
(3)将特殊的分隔符作为消息的结束标识,回车换行符就是一种特殊的结束分割符;
(4)通过在消息头中定义的长度字段表示消息的总长度;
Netty对以上4种应用做了抽象,提供了4种解码器,有了解码器,码农们不用考虑TCP的粘包、拆包的问题了。
LineBasedFrameDecoder:依次编译bytebuf中的可读字符,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持单行的最大长度。如果连续读取到最大长度后,仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
FixedLengthFrameDecoder:是固定长度解码器,它能按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包等问题。利用FixedLengthFrameDecoder解码,无论一次性接收到多少的数据,他都会按照构造函数中设置的长度进行解码;如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包,到达后进行拼包,直到读取完整的包。
DelimiterBasedFrameDecoder:是自定义的分隔符解码,构造函数的第一个参数表示单个消息的最大长度,当达到该长度后仍然没有查到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出。
下例中,DelimiterBasedFrameDecoder以$_作为分隔符,EchoServer.java关键代码:
@Override
public void initChannel(SocketChannel ch) throws Exception{
//创建分隔符缓冲对象ByteBuf,以$_为分隔符
ByteBuf delimiter=Unpooled.copiedBuffer("$_".getBytes());
//1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符
//就抛出TooLongFrameException异常
//第二个参数是分隔符缓冲对象
new DelimiterBasedFrameDecoder(1024,delimiter)); //后续的ChannelHandler接收到的msg对象将会是完整的消息包
ch.pipeline().addLast(new StringDecoder()); //将ByteBuf解码成字符串对象
ch.pipeline().addLast(new EchoServerHandler()); //接收到的msg消息就是解码后的字符串对象
}
EchoServerHandler.java 关键代码:
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
String body=(String)msg;
System.out.println("This is " + ++counter + " times receive client : [" + body + "]");
body+="$_"; //$_已被过滤掉了,所以这里要拼接上
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
}
EchoClient.java 关键代码:
@Override
public void initChannel(SocketChannel ch) throws Exception{
ByteBuf delimiter=Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoClientHandler());
}
EchoClientHandler.java 关键代码:
@Override
public void channelActive(ChannelHandlerContext ctx){
for(int i=0;i<10;i++){
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHOREQ.getBytes()));
}
}
FixedLengthFrameDecoder应用开发:固定长度解码器,按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题。
EchoServer.java 关键代码:
@Override
public void initChannel (SocketChannel ch) throws Exception{
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler()));
}
EchoServerHandler.java 关键代码:
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
System.out.println("Receive client : [" + msg + "]");
}
完整代码客户端:
EchoClient:
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class EchoClient {
public void connection(int port,String host) throws InterruptedException {
NioEventLoopGroup workGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(workGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("#".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter))
.addLast(new StringDecoder())
.addLast(new EchoClientHandler());
//
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host,port).sync();
// 等待客户端链路关闭
f.channel().closeFuture().sync();
} finally {
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 30000;
if(args.length>0&&args!=null){
System.out.println(args[0]);
port = Integer.parseInt(args[0]);
}
new EchoClient().connection(port,"127.0.0.1");
}
}
EchoClientHandler:
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private int count;
static final String ECHO_REQ = "hello,MrRight,welcome here!#";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=0;i<10;i++){
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("this is client receive msg"+ ++count +"times:【"+body+"】");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
客户端:
EchoServer:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class EchoServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,100)
.childHandler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("#".getBytes());//创建一个分隔符,确定为结束标志
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter))
.addLast(new StringDecoder())
.addLast(new EchoServerHandler());
}
});
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 30000;
if(args.length>0&&args!=null){
port = Integer.parseInt(args[0]);
}
new EchoServer().bind(port);
}
}
EchoServerHandler:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("This is"+ ++count +" times server receive client request.");
body += "#";
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override![在这里插入图片描述](https://img-blog.csdnimg.cn/20190402191322394.png)
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}