在本例子中使用的是netty5,以及后面的例子中也是。先去官网进下载5.0.0.Alphal,解压后要引用的jar包就是netty-5.0.0.Alphal.jar包。对于用netty进行编写TimeServer的nio程序如下:
package zou;
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 static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (Exception e) {
}
}
new TimeServer().bind(port);
}
public void bind(int port) throws Exception {
//配置服务端的nio线程组,
//EventLoopGroup是一个线程组,它包含了一组nio线程,专门用于网络事件的处理,实际上他们就是Reactor线程组
//这里创建2个的原因是一个用于服务端接受客户的连接,另一个用于SocketChannel的网络读写。
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());
}
}
}
从bind方法看到,这里创建了2个nio的EventLoopGroup实例。EventLoopGroup是一个线程组,它包含了一组NIO线程,专门用于网络事件的处理,实际上他们就是Reactor线程组。这里创建2个的原因是一个用于服务端接受客户端的连接,另一个用于进行SocketChannel的网络读写。ServerBootstrap对象是启动nio服务端的辅助启动类。
接着设置创建的Channel为NioServerSocketChannel,并设置tcp参数backlog。最后绑定i/o事件的处理类ChildChannleHandler,它的作用类似于Reactor模式中的Handler类,主要用于处理网络i/o事件,例如记录日志、对消息进行编解码等。ght
finally里面的代码是线程组的关闭,进行优雅的退出。
TimeServerHandler类继承自ChannelHandlerAdapater,它用于对网络事件进行读写操作。通常我们只需要关注channelRead和exceptionCaught方法。其代码如下:
package zou;
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").substring(0, req.length - System.getProperty("line.separator").length());
System.out.println("the time server receive order :" + body + "the counter:" + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
//进行发送消息到客户端
ctx.writeAndFlush(resp);
}
// @Override
// public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// //通过调用此方法,将发送的缓冲区的消息全部写到SocketChannel中
// ctx.flush();
// }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
在进行读取的时候进行了类型的转换(将字节转换为字符串)。ctx的flush方法是将消息发送队列的消息写入到SocketChannel中发送给对方。从性能的角度思考,为了防止频繁唤醒selector消息进行发送,Netty的write方法并不是直接将消息写入SocketChannel中,而是放入到发送缓冲数组中,再通过调用flush方法将发送缓冲区中的消息全部写到SocketChannel中。另外在捕捉异常的方法中是进行资源的关闭和释放。
其客户端代码如下:
package zou;
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 connet(int port, String host) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
//客户端禁用nangle算法
b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandle());
}
});
//发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
//等待客户端关闭
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (Exception e) {
}
}
new TimeClient().connet(port, "127.0.0.1");
}
}
这里使用了线程组EventLoopGroup,然后借助辅助类来启动nio,这里设置了SocketChannel,然后设置了tcp参数,禁用了nagle算法,此处通过创建匿名内部类来实现一个ChannelInitalizer类。另外需要实现一个网路处理的handler类。
handler代码如下:
package zou;
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 TimeClientHandle extends ChannelHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeClientHandle.class.getName());
private final ByteBuf firstMessage;
public TimeClientHandle() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
//连接成功后发送指令
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(firstMessage);
}
@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);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
这里重点关注三个方法:channelActive,channelRead,和exceptionCaught.当客户端和服务端tcp链路建立成功后,Netty的nio线程会调用channelActive方法,发送查询时间指令给服务端,调用writeAndFlush方法将请求消息发送给服务端。当服务端返回应答的时候,channelRead方法被调用。
需要指出的是这里没有考虑读半包处理问题,对于功能的测试没有问题,但是稍加修改或者压力测试就不能正常工作了。下个例子会进行讲解半包消息的处理。