Netty学习笔记(二)编写Echo服务端与客户端
这一节将利用Netty编写简单的Echo服务端与客户端。
一、Echo服务端/客户端概览
我们所要实现的功能很简单,即服务端监听端口,客户端连接服务端后发送消息,服务端将接收的消息作为响应返回给客户端。

二、编写Echo服务端
所有的 Netty 服务器都需要以下两部分。
- 至少一个ChannelHandler— 该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑
- 引导— 这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上
接下来就是Echo服务端的业务逻辑以及引导代码。
1、ChannelHandler和业务逻辑
由于我们的Echo服务器会响应传入的消息,所以它需要实现 ChannelInboundHandler 接口,用来定义响应入站事件的方法。由于我们的服务端逻辑很简单,所以继承ChannelInboundHandlerAdapter类也就足够了,它提供了ChannelInboundHandler的默认实现。
我们要实现的方法是:
- channelRead()— 对于每个传入的消息都要调用
- channelReadComplete()— 通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息
- exceptionCaught()— 在读取操作期间,有异常抛出时会调用
@ChannelHandler.Sharable 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) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
我们对EchoServerHandler类使用@ChannelHandler.Sharable注解,表示我们的一个ChannelHandler可以被多个Channel安全的使用。
ChannelInboundHandlerAdapter有一个直观的API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以我们重写了channelRead()方法。在这个服务器应用程序中,我们将数据简单地回送给了远程节点。
重写exceptionCaught()方法允许我们对 Throwable 的任何子类型做出反应,在这里我们打印了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场景下,只是通过简单地关闭连接来通知远程节点发生了错误。
2、引导服务器
我们的Echo服务器启动后应该作如下两件事:
- 绑定到服务器将在其上监听并接受传入连接请求的端口;
- 配置 Channel,以将有关的入站消息通知给EchoServerHandler实例。
代码如下:
public class EchoServer { private final int port; EchoServer(int port) { this.port = port; } public void start() throws Exception{ final EchoServerHandler serverHandler = new EchoServerHandler(); EventLoopGroup group = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(group) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception{ ch.pipeline().addLast(serverHandler); } }); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); }finally { group.shutdownGracefully().sync(); } } public static void main(String[] args) throws Exception{ if (args.length!=1){ System.err.println("Usage: " + EchoServer.class.getSimpleName() + " <port>"); } int port = Integer.parseInt(args[0]); new EchoServer(port).start(); } }
我们的引导服务器的代码主要逻辑如下:
- 创建一个 ServerBootstrap 的实例以引导和绑定服务器
- 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/写数据
- 指定服务器绑定的本地的 InetSocketAddress
- 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel
- 调用 ServerBootstrap.bind()方法以绑定服务器
此时我们的Echo服务端已经实现完成,接下来是客户端代码。
二、编写Echo客户端
我们的Echo 客户端启动后将会做如下几件事:
- 连接到服务器
- 发送一个或者多个消息
- 对于每个消息,等待并接收从服务器发回的相同的消息
- 关闭连接
编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,跟服务端很像。
1、通过ChannelHandler实现客户端逻辑
如同服务器,客户端将拥有一个用来处理数据的ChannelInboundHandler。在这个场景 下,我们将扩展SimpleChannelInboundHandler类以处理所有必须的任务。这要求重写下面的方法:
- channelActive()——在到服务器的连接已经建立之后将被调用
- channelRead0()——当从服务器接收到一条消息时被调用
- exceptionCaught()——在处理过程中引发异常时被调用
代码如下:
@ChannelHandler.Sharable public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush(Unpooled.copiedBuffer("hello netty", CharsetUtil.UTF_8)); } @Override public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { System.out.println("Client received:" + in.toString(CharsetUtil.UTF_8)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
首先,我们重写了channelActive()方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"hello netty"的字节缓冲区。接下来,重写了channelRead0()方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了 5 字节,那么不 能保证这 5 字节会被一次性接收。即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf,第二次使用一个 持有2字节的ByteBuf。作为一个面向流的协议,TCP 保证了字节数组将会按照服务器发送它们的顺序被接收。重写的第三个方法是exceptionCaught()。如同在EchoServerHandler中所示,记录Throwable,关闭Channel,在我们这个场景下,终止服务器的连接。
2、引导客户端
引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址,也就是这里的Echo服务器的地址,而不是绑定到一个一直被监听的端口。
public class EchoClient { private final String host; private final int port; EchoClient(String host, int port) { this.host = host; this.port = port; } public void start() throws Exception{ EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .remoteAddress(new InetSocketAddress(host, port)) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception{ ch.pipeline().addLast(new EchoClientHandler()); } }); ChannelFuture f = b.connect().sync(); f.channel().closeFuture().sync(); }finally { group.shutdownGracefully().sync(); } } public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println( "Usage: " + EchoClient.class.getSimpleName() + " <host> <port>"); return; } String host = args[0]; int port = Integer.parseInt(args[1]); new EchoClient(host, port).start(); } }
我们客户端引导流程如下:
1)为初始化客户端,创建了一个 Bootstrap 实例;
2)为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
3)为服务器连接创建了一个InetSocketAddress实例;
4)当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中;
5)在一切都设置完成后,调用 Bootstrap.connect()方法连接到远程节点;
三、测试
接下来我们验证下Echo服务端与客户端。
首先我们启动Echo服务端:
![]()
接着启动Echo客户端:

我们看到客户端启动后立即想服务端发消息并收到服务端响应,所以我们服务端也是能正常工作的,它必然收到了客户端的消息:


浙公网安备 33010602011771号