使用Netty编写一个时间服务器
服务端程序
public class TimeServer {
public void bind(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//backlog是阻塞队列
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).
option(ChannelOption.SO_BACKLOG, 1024).childHandler(new ChildChannelHandler());
ChannelFuture f = bootstrap.bind(port).sync();
//绑定端口,同步等待成功
f.channel().closeFuture().sync();
}finally {
//优雅退出
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeServer().bind(port);
}
}
服务端程序从bind()开始,使用 NioEventLoopGroup()创建了两个实例,NioEventLoopGroup是个线程组,专门用于处理网络事件,本质上就是Reactor线程组。这里的两个线程组,一个用于接收客户端的连接,一个用于网络数据的读写。
ServerBootstrap是Netty中用于NIO服务端启动的辅助配置类,降低了服务端开发的复杂度。group方式设置NIO处理线程组,channel方法设置Channel类型,option方法配置各种参数,childHandler用于绑定IO事件的处理类。
childHandler继承ChannelInitializer类,实现了initChannel方法,在Channel创建完成后,进行初始化时,将它的handle加入到pipeline中,用来处理网络事件。
ServerBootstrap辅助类配置完成后,使用bind方法绑定端口,然后调用sync等待绑定完成。
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("The Time server receive order: " + body);
String cuurentTime = "QUERY TIME".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD QUERY";
ByteBuf resp = Unpooled.copiedBuffer(cuurentTime.getBytes());
//不是写到channel,而是写到buffer,通过下面的flush真正写到SocketChannel
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
TimeServerHandler用于处理网络数据读写,这里我们主要看channelRead方法。
首先对msg进行类型转换,这里的ByteBuf跟JDK NIO中的ByteBuffer很像,在原有基础上提供了更多的功能。通过buf.readableBytes()获取可读取的字节,然后通过readBytes将缓冲区的字节复制到新的自己数组中,最后通过new String获取请求信息。判断如果是正确的指令则通过ChannelHandlerContext的write将应答数据写到 缓存区。
客户端程序
public class TimeClient {
public void connect(int port, String host) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
System.out.println("use default port");
}
}
new TimeClient().connect(port, "127.0.0.1");
}
}
客户端的逻辑:首先也是先进行启动辅助类Bootstrap的配置,客户端的事件处理类直接使用匿名内部类。配置完成后,调用connect方法发起到服务端程序的异步连接,然后调用sync方法等待连接成功。
当客户端连接关闭时,客户端主函数退出,释放所有NIO资源。
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf firstMessage;
public TimeClientHandler() {
byte[] req = "QUERY TIME".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[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("Now is :" + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("Exception ");
cause.printStackTrace();
ctx.close();
}
}
这里我们主要看channelActive和channelRead。
当客户端和服务端的链接建立成功后,Netty的NIO线程会调用channelActive方法,这个方法的实现中主要就是使用ChannelHandlerContext的writeAndFlush方法发送请求数据。
当服务端返回应答数据后,Netty会调用channelRead方法,这个方法就是把应答数据从ByteBuf中读取出来并打印。