本章简单介绍Netty的核心概念,这个核心概念就是学习Netty是如何拦截和处理异常,对于刚开始学习Netty的读者,利用Netty的异常拦截机制来调试程序的问题很有帮助。本章还会介绍其他一些核心概念,如服务器和客户端的启动以及分离通道的处理程序。本章节中将编写一个基于Netty的服务器和客户端端来相互通信。
1.设置开发环境
- 安装jdk1.8
- 配置netty4
- 安装Idea
2.Netty客户端和服务器描述
本节构建一个完整的Netty服务器。在这个例子中,同时实现了服务器和客户端,你会对他们的原理更加清晰。
Netty的程序工作:
- 客户端连接到服务器
- 建立连接后,发送或接受数据
- 服务器处理所有的客户端连接
从上述可以看出,服务器会写数据到客户端并处理多个客户端的并发连接。从理论上来说,限制程序的因素只有系统资源和JVM。
虽然将相同的数据返回给客户端不是一个典型的例子,但是客户端和服务器之间数据的来来回回的传输和这个例子是一样的。本章的例子会证明这一点,它们会越来越复杂。
3.编写一个应答服务器
写一个Netty服务器主要由两部分组成:
- 配置服务器功能,如线程、端口
- 实现服务器处理程序,它包含业务逻辑,决定当有一个请求连接或接受数据时该做什么
3.1启动服务器
通过创建ServerBootstrap对象来启动服务器,然后配置这个对象的相关选项,如端口、线程模式、事件循环,并添加逻辑处理程序用来处理业务逻辑(下面是个简单的应答服务器的例子)
package netty.in.action.chapter2.echo;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class EchoServer {
private final int port;
public EchoServer(int port){
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
//创建ServerBootstrap实例
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println(EchoServer.class.getName()+" started and listen on "+ future.channel().localAddress());
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new EchoServer(8090).start();
}
}
从上面这个简单的服务器例子可以看出,启动服务器应先创建ServerBootstrap对象,因为使用NIO,所以指定NioEventLoopGroup来接受和处理新连接,指定通道类型为NioServerSocketChannel,设置InetSocketAddress让服务器箭筒某个端口等待客户端连接。
接下来,调用childHandler()来指定连接后调用的ChannelHandler,这个方法传ChannelInitializer类型的参数,ChannelInitializer是一个抽象类,所以需要实现initChannel()方法,这个方法就是用来设置ChannelHandler。
最后绑定服务器等待直到绑定完成,调用sync()方法会阻塞直到服务器完成绑定,然后服务器等待通道关闭,因为使用sync,所以关闭操作也会阻塞。现在你可以关闭EventLoopGroup和释放所有资源,就包括创建的线程。
这个例子中使用NIO,因为它是最常用的方式,你可能会使用NIO很长时间,但是你可以选择不同的传输实现。例如,这个例子使用OIO方式传输,你需要指定OioServerSocketChannel。Netty框架实现了多重传输方式,将后面讲述。
本小节重点:
- 创建ServerBootstrap实例来引导绑定和启动服务
- 创建NioEventLoopGroup对象来处理事件,如接受新连接、接受数据、写数据等等
- 设置childHandler()执行所有的连接请求
- 都设置完毕了,最后调用ServerBootstrap.bind()方法来绑定服务器。
3.2实现服务器业务逻辑
Netty使用futures和回调概念,它的设计允许你处理不同的事件类型,更详细的介绍将在后面的章节讲述,但是我们可以接收数据。你的channelHandler必须继承ChannelInboundHandlerAdapter并且重写channelRead方法,这个方法在任何时候都会被调用来接收数据,在这个例子中接收的是字节。
下面是handler的实现,其实现的功能是将客户端发送给服务器的数据返回给客户端:
package netty.in.action.chapter2.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] buf = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(buf);
System.out.println("服务器接收到信息:"+new String(buf,"UTF-8"));
ctx.write(Unpooled.copiedBuffer("客户端,你好,我是服务器", CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("读取结束,响应内容给客户端");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Netty使用多个ChannelHandler来达到对事件处理的分离,因为可以很容易的添加、删除业务逻辑处理handler。Handler很简单,它的每个方法都可以被重写,它的所有方法中只有channelRead()方法是必须要重写的。
3.3捕获异常
重写ChannelHandler的exceptionCaught()方法可以捕获服务器的异常,比如客户端连接服务后强制关闭,服务器会抛出“客户端主机强制关闭错误”,通过重写exceptionCaught()方法就可以处理异常,比如发生异常后关闭ChannelHandlerContext。
4.编写应答程序的客户端
应答程序的客户端包括以下几步:
- 连接服务器
- 写数据到服务器
- 等待接收服务器返回的相同数据
- 关闭连接
4.1引导客户端
引导客户端启动和引导服务器类似,客户端需要同时指定host和port来告诉客户端连接哪个服务器。看下面的代码:
package netty.in.action.chapter2.echo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
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() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = bootstrap.connect(host,port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new EchoClient("localhost",8090).start();
}
}
创建启动一个客户端包含下面几步:
- 创建Bootstrap对象用来引导客户端
- 创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为一个线程池,这个线程池用来处理连接、接收数据、发送数据。
- 添加一个ChannelHandler,客户端成功连接服务器后就会被执行
- 调用Bootstrap.connect()来连接服务器
- 最后关闭EventLoopGroup来释放资源
4.2实现客户端业务逻辑
客户端的业务逻辑实现亦然很简单,更复杂的用法在后面接收。和编写服务器的ChannelHandler一样,在这里将自定义一个继承SimpleChannelInboundHandler的ChannelHandler来处理业务;通过重写父类的三个方法来处理感兴趣的事件:
- channelActive() 客户端连接服务器后被调用
- channelRead0() 从服务器接收到数据后调用
- exceptionCaught() 发生异常时被调用
package netty.in.action.chapter2.echo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("连接上服务器,并发送信息");
ctx.writeAndFlush(Unpooled.copiedBuffer("你好,服务器!", CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.close();
}
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] buf = new byte[msg.readableBytes()];
msg.readBytes(buf);
String info = new String(buf,"UTF-8");
System.out.println("客户端收到服务器应答:"+ info);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
使用SimpleChannelInboundHandler而不使用ChannelInboundHandlerAdapter 的原因是,ChannelInboundHandlerAdapter 在处理完消息后需要负责释放资源。在这里将调用ByteBuf.release()来释放资源。SimpleChannelInboundHandler会在完成channelRead0()后释放消息,这是通过Netty处理所有消息的ChannelHandler实现了ReferenceCounted接口达到的。
为什么在服务器中不使用SimpleChannelInboundHandler呢?在服务器执行完成写操作之前不能释放调用读取到的消息,因为写操作是异步的,一旦写操作完成后,Netty中会自动释放消息。