本章内容
- 获取最新稳定版Netty
- 设置编译和运行示例环境
- 创建基于Netty的服务端和客户端
- 拦截和处理错误
- 编译和运行基于Netty的服务端和客户端
一、建立开发环境
建立开发环境,需要以下几步:
1、安装JDK
2、安装构建工具,建议Maven或Gradle,本翻译作品采用Gradle
大家都已经在学Netty了,相信上面两步应该都会很快完成。
二、Netty客户端与服务端概述
本小节的目标是引导大家创建一个完整的基于Netty的客户端和服务端。可能有的人只对写服务端感兴趣,例如写一个HTTP服务端,这样就可以直接使用浏览器作为客户端。但我们这个例子,如果你同时实现了客户端和服务端,你就会学到Netty更多的知识。
一个Netty应用的基本结构如下:
1.客户端连接服务端,2.建立连接收发数据,3.服务端处理所有客户端过来的连接
有一件事需要说一下,你实现的Netty应用的服务端,会自动处理多个并发客户端。理论上来说,这里的并发限制主要就是你系统的可用资源限制和一些JDK限制。
为了更容易理解,可以想像一下回声系统,比如你在山谷里大喊一声,不久你会听到山谷给你的回声。这种情况下,你就是客户端,山谷就是服务端。进入山谷,就相当于建立了一个连接。你大喊一声就类似Netty客户端向服务端发送数据。听到回声就类似服务端把客户端发来的数据又返回去。你离开山谷,类似断开连接,如果你又回到山谷类似重连到服务端就可以继续发送更多数据。
虽然实际工作中不会直接将客户端发来的数据再送回去,不过不管什么样的业务需求,客户端与服务端的工作模式都是这样。本章后面的例子将会慢慢变复杂来说明这一点。
接下来的几个小节将要介绍开发基于Netty的客户端和服务端的流程。
三、基于Netty的EchoServer
编写基于Netty的服务端主要包括下面两部分:
- Bootstrapping-配置服务端,例如线程和端口
- 实现服务端Handler-实现建立连接和接收到数据后应该执行什么样的业务逻辑
3.1、Bootstrapping
启动服务端需要创建一个ServerBootstrap实例。这个实例用来配置属性,例如端口,事件线程,以及业务逻辑Handler,这么这个EchoServer的业务逻辑就是将客户端发送来的数据再发送回去,不过也很容易写出更复杂的逻辑。
首先在Maven或者Gradle的配置文件中加上Netty最新稳定版的依赖,目前最新版是4.1.15.Final,例如我的build.gradle如下。
group 'com.nan.netty'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile(
'io.netty:netty-all:4.1.15.Final'
)
testCompile group: 'junit', name: 'junit', version: '4.12'
}
服务端主类代码如下。
package com.nan.netty.one;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
//服务端启动器
ServerBootstrap b = new ServerBootstrap();
//配置服务端使用NIO传输,绑定本地端口
b.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
//Channel增加业务处理类
ch.pipeline().addLast(new EchoServerHandler());
}
});
//服务端绑定并等待应用关闭释放资源
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(9999).start();
}
}
例子里面EchoServerHandler还没定义,所以可能上面的代码会在IDE中报错,不过没关系,先不用管。这个例子看起来没什么特别之处,不过第一章的例子里面每件事它也都做了。为了启动服务端,首先要创建一个ServerBootstrap实例。因为要使用NIO的传输方式,所以配置了NioEventLoopGroup去接受并处理新连接,配置NioServerSocketChannel作为Channel类型,并且配置InetSocketAddress绑定地址和端口来接收新连接。
下一步,配置了一个ChannelHandler当来了一个新连接后会回调它,它会创建一个子Channel。这里还用到了一个重要的类ChannelInitializer。
虽然上一章的NIO或NIO.2的例子也有很强的扩展性,但是它们还是有可能产生问题。比如线程,JDK NIO不就太容易使用,但是Netty通过设计和封装抽象做了大部分线程工作,主要通过使用EventLoopGroup,SocketChannel和ChannelInitializer,后面的章节会详细讨论这些知识。
一个Channel的ChannelPipeline包含了所有的ChannelHandlers,所以事先将EchoServerHandler加入到ChannelPipeline中。然后绑定服务知道绑定完成,sync()方法会阻塞,只到绑定完成。然后应用就会一直运行只到服务端的Channel关闭(因为在Channel关闭的future我们调用了sync()方法)。这个时候就可以关闭EventLoopGroup然后释放所有资源,包括创建的所有线程。
上面的例子中我们使用了NIO传输方式因为这是目前使用量比较多的,不过你也可以选择使用其他的。例如你要是选择使用OIO传输方式,就要使用OioServerSocketChannel。他们的区别以及到底什么是传输方式,后面的章节会进行详解。
下面我们来列一下编写Netty服务端几个重要的步骤:
- 创建一个ServerBootstrap实例用来启动服务端,然后绑定
- 创建NioEventLoopGroup实例并指定用它处理事件,包括接收连接,读写数据等等
- 设置一个Handler来处理业务逻辑
- 上面都准备就绪后调用ServerBootstrap.bind()方法绑定服务器地址端口来启动服务
3.2、实现业务逻辑
Netty使用上一章介绍的Future、回调等技术,再加上其他设计,用来响应不同类型的事件。这些后面再讨论,目前我们的焦点是收到数据后下一步怎么做。想要处理收到的数据,必须编写一个类继承ChannelInboundHandlerAdapter类,并且重写messageReceived方法。这个方法会在每次收到数据后调用,这个例子里我们收到的是字节数组。EchoServer这里要做的就是在这个方法里将收到的内容重新发送给客户端,请看下面的代码。
package com.nan.netty.one;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.Charset;
//Sharable注解可以让此Handler在不同Channel之间共享
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//Netty的ByteBuf转成字符串
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("Server received: " + byteBuf.getCharSequence(0, byteBuf.readableBytes(), Charset.forName("UTF-8")));
//收到数据并将数据发送到客户端,但是此时数据还没有刷到对等连接点
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
//读操作完成后,将所有写入的数据(等待中的)刷新到对等连接点,然后关闭此Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
//出现异常关闭此Channel
ctx.close();
}
}
现在我们先使用第一章写的BIO的EchoClient连接上面的服务端,可以发现EchoServer的功能已经实现了。
Netty使用的Handler有更大的解耦性,所以非常容易进行增加,修改或删除业务逻辑的项目演进。Handler的作用是很明确的,里面的每个方法都可以重写以参与到收发的数据的生命周期,但是一般情况下只有channelRead方法是需要重写的。
3.3、拦截异常
除了重写了channelRead方法之外,有一个exceptionCaught方法也被重写了。这个方法是用来处理异常的,或者其他Throwable的子类。上面的例子中,记录了异常并关闭掉到客户端的连接,因为出现异常这个链接的状态可能是不确定的。一般情况下可能你也会这么干,不过有一些场景,需要从错误中恢复过来,这就需要发挥你的指挥找到一个聪明的方法去实现。重要的就是你要有一个ChannelHandler实现exceptionCaught方法来处理各种各样的错误。
Netty提供的这个拦截异常的方法更容易处理不同线程发生的错误。通过统一的简单、集中API将不同线程发生的错误尽可能放在一起处理。
如果你需要用Netty写一个实际生产的应用或者一个框架就要知道很多其他ChannelHandler的子类和实现,这些后面会讨论。现在只需要记住ChannelHandler的实现会在发生不同的事件调用,你可以继承或实现它们来参与事件的生命周期。
四、编写EchoClient
- 连接服务端
- 发送数据到服务端
- 等待服务端返回相同的数据
- 关闭连接
4.1、客户端启动器
客户端的启动器和服务端的启动器很类似。有一点区别,服务器启动器一般只需要端口号,而客户端需要服务端的IP地址和端口,因为它要去连接这个IP和端口。
package com.nan.netty.one;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
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 b = new Bootstrap();
//使用NioEventLoopGroup接收事件,和服务端一样,这里使用NIO传输
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
//指定ChannelHandler,一旦连接简历就会创建Channel
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
//指定EchoClientHandler处理业务逻辑
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", 9999).start();
}
}
这里我们用的还是NIO传输。这里先提一下,暂时也不用关系你用的什么传输方式;客户端和服务端也可以使用不同的传输方式。有些开发者就是在服务端使用NIO传输并在客户端使用OIO传输。我们将会在第四章学习到底什么是传输方式。
我们列一下客户端的重点:
- 创建一个启动器实例启动客户端
- 使用NioEventLoopGroup处理事件,包括新连接、读写数据等
- InetSocketAddress里面的地址和端口就是要连接的服务端的地址和端口
- 还注册了一个Handler作为连接成功后的回调,目前还没写这个Handler,所以代码会在IDE里报错
- 上面的步骤完成之后,调用ServerBootstrap.connect()方法去连接服务端
4.2、客户端业务逻辑
这里实现的客户端逻辑尽量简单,里面用到的一些类的细节会在后面的章节详解。
客户端的Handler首先继承了SimpleChannelInboundHandler,然后重写了下面三个方法,这三个方法对我们现在而言都是有用的。
- channelActive()- 连接服务端成功后回调
- channelRead0()- 从服务端收到数据后回调
- exceptionCaught()- 出现异常时回调
package com.nan.netty.one;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
//不同Channel之间共享此Handler
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//连接成功后向服务端发送数据
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello server 110", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
//打印从服务端收到的数据,然后关闭连接
System.out.println("Received from server: " + msg.getCharSequence(0, msg.readableBytes(), CharsetUtil.UTF_8));
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//打印遇到的异常,然后关闭连接
cause.printStackTrace();
ctx.close();
}
}
这里重写的三个方法在一般应用中都是有必要重写的。
一旦连接成功就会回调 channelActive()。上面的例子很简单,连接成功后向服务端发送一串字符。发送的内容目前不重要,目前这个不是重点。重写这个方法就是保证尽快将信息发送给服务端。
五、总结
这一章我们使用Netty实现了一个简单的服务端和客户端,对于启动,配置,读写数据以及捕获异常都有介绍,现在对于Netty算是有一些基本的了解,这些知识对于后面的学习是个很好的基础。