2.1 开发环境配置
安装jdk7 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html
下载Netty包 下载地址:http://netty.io/
Maven依赖地址
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
2.2 Netty客户端和服务端概述
一个Netty程序的工作图如下:
1.Client端连接到服务器
2.建立连接后,发送或接受数据
3.服务器处理所有的Client连接
从上图可以看出,服务器会写数据到客户端并且处理多个客户端的并发连接。从理论上将,限制性能的因素只有系统资源和JVM。为了方便理解,这里举了个生活例子,在山谷或高山上大声喊,你会听见回声,回声是山返回的;在这个例子中,你是客户端,山是服务器。喊的行为就类似于一个Netty客户端将数据发送到服务器,听到回声就类似于服务器将相同的数据返回给你,你离开山谷就断开了连接,但是你可以返回进行重连服务器并且可以发送更多的数据。
2.3 编写一个应答器
一个Netty服务器主要有两部分组成:
- 配置服务器程序,如线程,端口
- 实现服务器处理程序,它包含业务逻辑,决定当有一个请求连接或接受数据时该做什么
2.3.1 启动服务器
通过创建ServerBootstrap对象来启动服务器,然后配置这个对象的相关选项,如端口,线程模式,时间循环,并且添加逻辑处理程序用来处理业务逻辑
package com.example.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class EchoServer {
public void bind(int port) throws Exception {
//配置服务端的NIO线程组
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(nioEventLoopGroup, eventLoopGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.localAddress(port)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
//绑定端口,同步等待成功
ChannelFuture f = bootstrap.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
} catch(Exception e){
} finally {
nioEventLoopGroup.shutdownGracefully();
eventLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8081;
if(args != null && args.length > 0){
try {
port = Integer.valueOf(args[0]);
} catch(Exception e){
}
}
new EchoServer().bind(port);
}
}
从上面代码可以看出,启动器应先创建一个ServerBootstrap对象,因为使用NIO,所说义指定NioEventLoopGroup来接受和处理新连接,指定通道类型为NioServerSocketChannel,设置InerSocketAddress让服务器监听某个端口以等待苦短连接。
接下来,调用childHandler放来指定连接后调用的ChannelHandler,这个方法传ChannelInitializer类型的参数,ChannelInitializer是个抽象类,所以需要实现initChannel方法,这个方法就是用来设置ChannelHandler。
最后绑定服务器等待知道绑定成功,调用sync()方法会阻塞知道服务器完成绑定,然后服务器等待通道关闭,因为使用sync(),所以关闭操作也会被阻塞。现在你可以关闭EventLoopGroup和释放所有资源,包括创建的线程。
这个例子使用NIO,因为它是目前最常用的传输方式,你可能会使用NIO很长时间,但是你可以选择不同的传输实现。例如,这个例子使用IO方式传输,你需要指定OioServerSocketChannel,Netty框架实现了多重传输方式,后面再说。
代码重点:
- 创建ServerBootstrap实力来引导绑定和启动服务器
- 创建NIoEventLoopGroup对象来处理事件,如接受新连接,接收数据,写数据等
- 指定InetSocketAddress,服务器监听此端口
- 设置childHandler执行所有的连接请求
- 都设置完毕了,最后调用ServerBootstrap.bind()方法来绑定服务器
2.3.2 实现服务器逻辑
Netty使用futures和回调概念,它的设计允许你处理不同的事件类型,更详细的介绍后面部分再讲述,但是我们可以接受数据。你的ChannelHandler必须继承ChannelInboundHandlerAdapter并且重写channelRead方法,这个方法在任何时候都会被调用来接收数据,在这个例子中接收的是字节。
下面是handler的事先,其实现的功能是将客户端发送给服务器的数据返回给客户端:
package com.example.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class EchoServerHandler extends ChannelHandlerAdapter {
int counter = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("This is " + ++ counter + " times receive client: [" + body + "]");
body += "$_";
ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(echo);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Netty使用多个ChannelHandler来打到对时间处理的分离,因为可以很容易的添加,更新,删除业务逻辑处理handler。Hndler很简单,它的每个方法都可以被重写,channelRead方法必须被重写。
2.3.3 捕获异常
重写ChannelHandler的exceptionCaught方法可以捕获服务器的异常,比如客户端连接服务器后强制关闭,服务器会抛出”客户端主机强制关闭错误”,通过重写exceptionCaught方法就可以处理异常,比如发生异常后关闭ChannelHandlerContext。
2.4 编写应答程序的客户端
应答程序的客户端包括以下几步:
- 连接服务器
- 写数据到服务器
- 等待接受服务器返回相同的数据
- 关闭连接
2.4.1 引导客户端
引导客户端启动和引导服务器类似,客户端需同时制定host和port来告诉客户端连接哪个服务器。
package com.example.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class EchoClient {
public void connect(int port, String host) throws Exception {
//配置客户端NIO线程组
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(nioEventLoopGroup).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.remoteAddress(host, port)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
//发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
} catch(Exception e){
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8081;
if(args != null && args.length > 0){
try {
port = Integer.valueOf(args[0]);
} catch(Exception e){
}
}
new EchoClient().connect(port, "127.0.0.1");
}
}
创建启动一个客户端包含一下面几步:
- 创建Bootstrap对象来引导启动客户端
- 创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接,接受数据,发送数据
- 创建InetServerAddress并设置到Bootstrap中,InetSocketAddress是指定连接的服务器地址
- 添加一个ChannelHandler,客户端成功连接服务器后就会被执行
- 调用Bootstrap.connect()来连接服务器
- 最后关闭EventLoopGroup来释放资源
2.4.2 实现客户端的业务逻辑
和编写ChannelHandler一样,在这里自定义一个继承ChannelHandlerAdapter的ChannelHandler来处理业务,通过重写父类的三个方法来处理感兴趣的事件:
- channelActive():客户端连接服务器后被调用
- channelRead():从服务器接收到数据后调用
- exceptionCaught():发生异常时被调用
实现代码如下:
package com.example.netty;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class EchoClientHandler extends ChannelHandlerAdapter {
private int counter;
static final String ECHO_REQ = "Hi, Welcome to Netty.$_";
public EchoClientHandler() {
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++){
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("This is " + ++counter + " times receive server: [" + msg + "]");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
2.5 编译和运行exho程序客户端和服务器
服务端运行结果:
客户端运行结果:
2.6 总结
本章介绍了如何编写一个简单的基于Netty的服务器和客户端并进行通信发送数据。介绍了如何创建服务器和客户端以及Netty的异常处理机制。