Netty In Action中文版 – 第二章第一个Netty程序
本章介绍
- 获取Netty4最新版本
- 设置运行环境来构建和运行netty程序
- 创建一个基于Netty的服务器和客户端
- 拦截和处理异常
- 编写和运行Netty服务器和客户端
本章将简单介绍Netty的核心概念这个狠心概念就是学习Netty是如何拦截和处理异常对于刚开始学习netty的读者利用netty的异常拦截机制来调试程序问题很有帮助。本章还会介绍其他一些核心概念如服务器和客户端的启动以及分离通道的处理程序。本章学习一些基础以便后面章节的深入学习。本章中将编写一个基于netty的服务器和客户端来互相通信我们首先来设置netty的开发环境。
2.1 设置开发环境
设置开发环境的步骤如下
- 安装JDK7下载地址http://www.oracle.com/technetwork/java/javase/archive-139210.html
- 下载netty包下载地址http://netty.io/
- 安装Eclipse
《Netty In Action》中描述的比较多没啥用这里就不多说了。本系列博客将使用Netty4需要JDK1.7+
2.2 Netty客户端和服务器概述
本节将引导你构建一个完整的Netty服务器和客户端。一般情况下你可能只关心编写服务器如一个http服务器的客户端是浏览器。然后在这个例子中你若同时实现了服务器和客户端你将会对他们的原理更加清晰。
一个Netty程序的工作图如下
- 客户端连接到服务器
- 建立连接后发送或接收数据
- 服务器处理所有的客户端连接
从上图中可以看出服务器会写数据到客户端并且处理多个客户端的并发连接。从理论上来说限制程序性能的因素只有系统资源和JVM。为了方便理解这里举了个生活例子在山谷或高山上大声喊你会听见回声回声是山返回的在这个例子中你是客户端山是服务器。喊的行为就类似于一个Netty客户端将数据发送到服务器听到回声就类似于服务器将相同的数据返回给你你离开山谷就断开了连接但是你可以返回进行重连服务器并且可以发送更多的数据。
虽然将相同的数据返回给客户端不是一个典型的例子但是客户端和服务器之间数据的来来回回的传输和这个例子是一样的。本章的例子会证明这一点它们会越来越复杂。
接下来的几节将带着你完成基于Netty的客户端和服务器的应答程序。
2.3 编写一个应答服务器
写一个Netty服务器主要由两部分组成
- 配置服务器功能如线程、端口
- 实现服务器处理程序它包含业务逻辑决定当有一个请求连接或接收数据时该做什么
2.3.1 启动服务器
通过创建ServerBootstrap对象来启动服务器然后配置这个对象的相关选项如端口、线程模式、事件循环并且添加逻辑处理程序用来处理业务逻辑(下面是个简单的应答服务器例子)
[java] view plain
copy
package netty.example;
1.
1.
import io.netty.bootstrap.ServerBootstrap;
1.
import io.netty.channel.Channel;
1.
import io.netty.channel.ChannelFuture;
1.
import io.netty.channel.ChannelInitializer;
1.
import io.netty.channel.EventLoopGroup;
1.
import io.netty.channel.nio.NioEventLoopGroup;
1.
import io.netty.channel.socket.nio.NioServerSocketChannel;
1.
1.
public
class EchoServer {
1.
private
final
int port;
1.
public EchoServer(
int port) {
this.port = port;
- }
public
void start()
throws Exception {
- EventLoopGroup group =
new NioEventLoopGroup();
try {
//create ServerBootstrap instance
- ServerBootstrap b =
new ServerBootstrap();
//Specifies NIO transport, local socket address
//Adds handler to channel pipeline
- b.group(group).channel(NioServerSocketChannel.
class).localAddress(port)
- .childHandler(
new ChannelInitializer<Channel>() {
@Override
protected
void initChannel(Channel ch)
throws Exception {
- ch.pipeline().addLast(
new EchoServerHandler());
- }
- });
//Binds server, waits for server to close, and releases resources
- ChannelFuture f = b.bind().sync();
- System.out.println(EchoServer.
class.getName() +
"started and listen on " + f.channel().localAddress());
- f.channel().closeFuture().sync();
- }
finally {
- group.shutdownGracefully().sync();
- }
- }
public
static
void main(String[] args)
throws Exception {
new EchoServer(
65535).start();
- }
- }
从上面这个简单的服务器例子可以看出启动服务器应先创建一个ServerBootstrap对象因为使用NIO所以指定NioEventLoopGroup来接受和处理新连接指定通道类型为NioServerSocketChannel设置InetSocketAddress让服务器监听某个端口已等待客户端连接。
接下来调用childHandler放来指定连接后调用的ChannelHandler这个方法传ChannelInitializer类型的参数ChannelInitializer是个抽象类所以需要实现initChannel方法这个方法就是用来设置ChannelHandler。
最后绑定服务器等待直到绑定完成调用sync()方法会阻塞直到服务器完成绑定然后服务器等待通道关闭因为使用sync()所以关闭操作也会被阻塞。现在你可以关闭EventLoopGroup和释放所有资源包括创建的线程。
这个例子中使用NIO因为它是目前最常用的传输方式你可能会使用NIO很长时间但是你可以选择不同的传输实现。例如这个例子使用OIO方式传输你需要指定OioServerSocketChannel。Netty框架中实现了多重传输方式将再后面讲述。
本小节重点内容
- 创建ServerBootstrap实例来引导绑定和启动服务器
- 创建NioEventLoopGroup对象来处理事件如接受新连接、接收数据、写数据等等
- 指定InetSocketAddress服务器监听此端口
- 设置childHandler执行所有的连接请求
- 都设置完毕了最后调用ServerBootstrap.bind() 方法来绑定服务器
2.3.2 实现服务器业务逻辑
Netty使用futures和回调概念它的设计允许你处理不同的事件类型更详细的介绍将再后面章节讲述但是我们可以接收数据。你的channel handler必须继承ChannelInboundHandlerAdapter并且重写channelRead方法这个方法在任何时候都会被调用来接收数据在这个例子中接收的是字节。
下面是handler的实现其实现的功能是将客户端发给服务器的数据返回给客户端
[java] view plain
copy
package netty.example;
1.
1.
import io.netty.buffer.Unpooled;
1.
import io.netty.channel.ChannelFutureListener;
1.
import io.netty.channel.ChannelHandlerContext;
1.
import io.netty.channel.ChannelInboundHandlerAdapter;
1.
1.
public
class EchoServerHandler
extends ChannelInboundHandlerAdapter {
1.
@Override
public
void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
- System.out.println(
"Server received: " + msg);
- ctx.write(msg);
- }
@Override
public
void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
- ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
- }
@Override
public
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
- cause.printStackTrace();
- ctx.close();
- }
- }
Netty使用多个Channel Handler来达到对事件处理的分离因为可以很容的添加、更新、删除业务逻辑处理handler。Handler很简单它的每个方法都可以被重写它的所有的方法中只有channelRead方法是必须要重写的。
2.3.3 捕获异常
重写ChannelHandler的exceptionCaught方法可以捕获服务器的异常比如客户端连接服务器后强制关闭服务器会抛出"客户端主机强制关闭错误"通过重写exceptionCaught方法就可以处理异常比如发生异常后关闭ChannelHandlerContext。
2.4 编写应答程序的客户端
服务器写好了现在来写一个客户端连接服务器。应答程序的客户端包括以下几步
- 连接服务器
- 写数据到服务器
- 等待接受服务器返回相同的数据
- 关闭连接
2.4.1 引导客户端
引导客户端启动和引导服务器很类似客户端需同时指定host和port来告诉客户端连接哪个服务器。看下面代码
[java] view plain
copy
package netty.example;
1.
1.
import io.netty.bootstrap.Bootstrap;
1.
import io.netty.channel.ChannelFuture;
1.
import io.netty.channel.ChannelInitializer;
1.
import io.netty.channel.EventLoopGroup;
1.
import io.netty.channel.nio.NioEventLoopGroup;
1.
import io.netty.channel.socket.SocketChannel;
1.
import io.netty.channel.socket.nio.NioSocketChannel;
1.
import io.netty.example.echo.EchoClientHandler;
1.
1.
import java.net.InetSocketAddress;
1.
1.
public
class EchoClient {
1.
private
final String host;
private
final
int port;
1.
public 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
protected
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 {
new EchoClient(
"localhost",
20000).start();
- }
- }
创建启动一个客户端包含下面几步
- 创建Bootstrap对象用来引导启动客户端
- 创建EventLoopGroup对象并设置到Bootstrap中EventLoopGroup可以理解为是一个线程池这个线程池用来处理连接、接受数据、发送数据
- 创建InetSocketAddress并设置到Bootstrap中InetSocketAddress是指定连接的服务器地址
- 添加一个ChannelHandler客户端成功连接服务器后就会被执行
- 调用Bootstrap.connect()来连接服务器
- 最后关闭EventLoopGroup来释放资源
2.4.2 实现客户端的业务逻辑
客户端的业务逻辑的实现依然很简单更复杂的用法将在后面章节详细介绍。和编写服务器的ChannelHandler一样在这里将自定义一个继承SimpleChannelInboundHandler的ChannelHandler来处理业务通过重写父类的三个方法来处理感兴趣的事件
- channelActive()客户端连接服务器后被调用
- channelRead0()从服务器接收到数据后调用
- exceptionCaught()发生异常时被调用
实现代码如下
[java] view plain
copy
package netty.example;
1.
1.
import io.netty.buffer.ByteBuf;
1.
import io.netty.buffer.ByteBufUtil;
1.
import io.netty.buffer.Unpooled;
1.
import io.netty.channel.ChannelHandlerContext;
1.
import io.netty.channel.SimpleChannelInboundHandler;
1.
import io.netty.util.CharsetUtil;
1.
1.
public
class EchoClientHandler
extends SimpleChannelInboundHandler<ByteBuf> {
1.
@Override
public
void channelActive(ChannelHandlerContext ctx)
throws Exception {
- ctx.write(Unpooled.copiedBuffer(
"Netty rocks!",CharsetUtil.UTF_8));
- }
@Override
protected
void channelRead0(ChannelHandlerContext ctx, ByteBuf msg)
throws Exception {
- System.out.println(
"Client received: " + ByteBufUtil.hexDump(msg.readBytes(msg.readableBytes())));
- }
@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中会自动释放消息。
客户端的编写完了下面让我们来测试一下
2.5 编译和运行echo(应答)程序客户端和服务器
注意netty4需要jdk1.7+。
本人测试可以正常运行。
2.6 总结
本章介绍了如何编写一个简单的基于Netty的服务器和客户端并进行通信发送数据。介绍了如何创建服务器和客户端以及Netty的异常处理机制。