序言
我们前面简简单单的聊了一下Netty的核心。俗话说:光说不练假把式。这把我们来玩一下Netty。再深入一点点。
development environment
IDEA + MAVEN + JDK8
Netty客户端/服务端
我们有一个服务器端 + 多个客户端。虽然Netty是一个很牛逼的框架,但是理论上,还是受限于系统资源的。Netty的客户端和服务器之间的交互比较简单,在客户端建立一个连接够,它会向服务器发送一个/多个 消息。服务器会把消息会送给客户端。
My first Echo Server
所有的Netty服务器都需要两个部分:
至少一个ChannelHandler - 实现了服务器对从客户端接受的数据的处理,即它的业务逻辑。
配置服务器的启动代码,将服务器绑定到他要监听的连接请求的端口上。
ChannelHandler和业务逻辑
之前曾经介绍过Future和回调,并且阐述了它们在事件驱动设计中的应用。ChannelHandler是一个借口,他的实现负责接收和响应事件通知。在Netty应用中,所有的数据处理逻辑都在这里。
因为Echo服务器会响应传入的消息,所以他至少需要实现ChannelInboundHandler接口,用于定义响应入站的事件的方法。这个简单的应用程序只需少量的方法。所以继承ChannelInboundHandlerAdapter类足够,里面有默认实现。看看代码:
public interface ChannelInboundHandler extends ChannelHandler {
void channelRegistered(ChannelHandlerContext var1) throws Exception;
void channelUnregistered(ChannelHandlerContext var1) throws Exception;
void channelActive(ChannelHandlerContext var1) throws Exception;
void channelInactive(ChannelHandlerContext var1) throws Exception;
void channelRead(ChannelHandlerContext var1, Object var2) throws Exception;
void channelReadComplete(ChannelHandlerContext var1) throws Exception;
void userEventTriggered(ChannelHandlerContext var1, Object var2) throws Exception;
void channelWritabilityChanged(ChannelHandlerContext var1) throws Exception;
void exceptionCaught(ChannelHandlerContext var1, Throwable var2) throws Exception;
}
定义了一些事件
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
// do something
}
每个传入的消息都要调用它
@Skip
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
通知ChannelInboundHandler最后一次对ChannelRead()的调用时当前批量读取中的最后一条消息。
@Skip
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
}
在读取操作期间,有异常抛出的时候会调用。
@Skip
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
}
该Echo服务器的ChannelHandler实现是EchoServerHandler.
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
//将消息记录到控制台
System.out.println(
"Server received: " + in.toString(CharsetUtil.UTF_8));
//将接收到的消息写给发送者,而不冲刷出站消息
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将未决消息冲刷到远程节点,并且关闭该 Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 打印异常跟踪栈
cause.printStackTrace();
// 关闭该通道
ctx.close();
}
}
先重写这个,其实不止这个,还有很多,后面慢慢看,不过记住:
针对不同类型的事件来调用ChannelHandler。
应用程序通过实现ChannlerHandler挂钩到生命周期,且提供应用程序的逻辑。
在架构上,ChannlerHandler能够保持业务逻辑和网络处理代码的分离,这简化了并发过程。
引导服务器
我们实现了ChannelInboundHandlerAdapter 的核心逻辑之后,可以讨论引导服务器的本身了,具体设计以下内容:
绑定到服务器将在其上监听并接受传入连接请求的端口。
配置Channel,将入站消息通知给EchoServerHandler。
public class EchoServer {
private final int port;
public EchoServer(int port){
this.port = port;
}
public static void main(String[] args)
throws Exception {
if (args.length != 1) {
System.err.println("Usage: " + EchoServer.class.getSimpleName() +
" <port>"
);
return;
}
//设置端口值(如果端口参数的格式不正确,则抛出一个NumberFormatException)
int port = Integer.parseInt(args[0]);
//调用服务器的 start()方法
new EchoServer(port).start();
}
public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
//(1) 创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
//(2) 创建ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(group)
//(3) 指定所使用的 NIO 传输 Channel
.channel(NioServerSocketChannel.class)
//(4) 使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//(5) 添加一个EchoServerHandler到于Channel的 ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
//EchoServerHandler 被标注为@Shareable,所以我们可以总是使用同样的实例
//这里对于所有的客户端连接来说,都会使用同一个 EchoServerHandler,因为其被标注为@Sharable,
ch.pipeline().addLast(serverHandler);
}
});
//(6) 异步地绑定服务器;调用 sync()方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
System.out.println(EchoServer.class.getName() +
" started and listening for connections on " + f.channel().localAddress());
//(7) 获取 Channel 的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
//(8) 关闭 EventLoopGroup,释放所有的资源
group.shutdownGracefully().sync();
}
}
}
ServerBootstrap b = new ServerBootstrap();创建了一个实例,因为使用的是NIO传输,所以指定了EventLoopGroup 来接受和处理新的连接,并且设置channel类为NioServerSocketChannel。将本地地址设置为一个具有指定端口的InetSocketAddress。服务器绑定到这个端口可以监听新的连接请求。使用ChannelInitializer。当有了一个新连接,将会创建一个新的Channel,它会把我们EchoServerHandler 加入到channel的pipelineLine中。这个handler将会收到又换入站消息的通知。接下来绑定了服务器ChannelFuture f = b.bind().sync();并且设置为sync,就是将当前线程阻塞直到服务器绑定完成为止,随后就关闭了EventLoopGroup 并且释放所有资源。
Echo客户端
echo客户端会先连接到客户端,然后发送消息,对于消息等待并且接受然后从服务端发回来,最后不要忘记关闭连接。
客户端也拥有一个channelInboundHandler,我们至少需要重写 channelActive(),channelRead0()和exceptionCaught()。他们分别是在服务器的连接已经建立之后被调用。当从服务器接受到一条消息被调用和在处理过程中引发异常被调用。
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println(" i get " + msg.toString(CharsetUtil.UTF_8));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Herche Jane", CharsetUtil.UTF_8));
}
}
很简单,我们第一个先是服务器开启的时候,调用了exceptionCaught方法,每当接受数据的时候,调用了channelRead0方法,当发生异常的时候,调用了exceptionCaught方法,此时,channel被关闭,连接断掉了。
引导客户端
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final EchoClient localhost = new EchoClient("localhost", 9999);
localhost.start();
}
然后运行就会发现客户端连接上了服务器就有了一条消息,然后服务器收到了,写回来一条。
结束语
一个如此简单的例子,让我对netty有了基本的认识,但是我还是不能很好的运用它,接下来的日子,我会学习更多的关于netty的知识。