客户端和服务器之前的交互相对比较简单。在客户端与服务器建立连接之后,它会向服务器发送一个或多个消息。反过来,服务器也会将每个消息回送给客户端。接下来将从具体的实例代码来简单看下客户端和服务器具体是怎么交互的。
服务器端
所有Netty服务器都需要以下两部分:
-
至少需要一个ChannelHandler:该组件实现了服务器对从客户端接收的数据的处理,也就是它的业务逻辑。
-
引导:配置服务器的启动代码。至少它会将服务器绑定到它要监听连接请求的端口上。
ChannelHander的代码实现
/**
* 用于响应事件处理
*/
@ChannelHandler.Sharable //标示一个Channel-Handler可以被多个Channel安全地共享
public class FirstNettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 每个传入的消息都会调用该方法
*
* @param ctx
* @param msg
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
//将接收到的消息进行打印输出
System.out.println("Server received:" + in.toString());
//将接收到的消息写给发送者
ctx.write(in);
}
/*
*通知ChannelInboundHander最后一次对channelRead()的调用是当前批量读取中的最后一条消息
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
/**
* 将目前暂存于ChannelOutboundBuffer中的消息
*冲刷到远程节点,并且关闭该Channel
*/
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
/*
* 在读取期间,有异常抛出时会调用
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 打印异常栈跟踪
cause.printStackTrace();
//关闭该channel
ctx.close();
}
}
因为服务器需要响应传入的消息,所以需要实现ChannelInboundHandler接口,用来定义响应入站事件的方法。
ChannelInboundHandlerAdapter类提供了ChannelInboundHandler的默认实现。
-
重写channelRead()方法,用于处理所有接收到的数据,我们这里只是将数据简单回送给了远程调用节点。
-
重写channelReadComplete()方法,当时当前数据的读取的最后一条消息,会进行调用,表示已完成最后的读取
-
重写exceptionCaught()方法,记录了异常并关闭了连接。
假如我们如果不捕获异常,那会发生什么呢?
每个Channel都拥有一个与之相关联的ChannelPipeline,其持有一个ChannelHandler 的实例链。在默认的情况下,ChannelHandler会把对它的方法的调用转发给链中的下一个ChannelHandler。因此,如果exceptionCaught()方法没有被该链中的某处实现,那么 所接收的异常将会被传递到ChannelPipeline的尾端并被记录。为此,应用程序应该提 供至少有一个实现了exceptionCaught()方法的ChannelHandler。
引导服务器代码实现
/**
* 第一个Netty程序服务端
*/
public class FirstNettyServer {
private final int port;
public FirstNettyServer(int port) {
this.port = port;
}
public static void main(String[] args) {
//启动服务器
new FirstNettyServer(6666).start();
}
public void start() throws Exception {
final FirstNettyServerHandler serverHandler = new FirstNettyServerHandler();
//1. 创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.创建Server-Bootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(group)
//3.指定所使用的NIO传输Channel
.channel(NioServerSocketChannel.class)
//4.使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//5.添加一个FirstNettyServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(serverHandler);
}
});
//6. 异步绑定服务器,调用sync()方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
//7. 获取Channel的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
//8. 关闭EventLoopGroup,释放所有资源
group.shutdownGracefully().sync();
}
}
}
-
首先我们创建了一个ServerBootstrap实例用来引导和绑定服务器
-
创建并分配一个NioEventLoopGroup来进行事件的处理,如接受新的连接以及读/写数据
-
指定服务器使用InetSocketAddress来绑定端口,用于监听这个地址的连接请求
-
通过ChannelInitializer把FirstNettyServerHandler的实例添加到该Channel的ChannelPipeline中
-
调用ServerBootstrap.bind()方法来绑定服务器
客户端
客户端主要会做三件事:
-
连接到服务器
-
发送一个或多个消息
-
对于每个消息,等待并接收从服务器发回的相同的消息
-
关闭连接
客户端ChannelHandler代码具体实现
和服务器一样,客户端将拥有一个用来处理数据的ChannelInboundHandler,这里通过继承SimpleChannelInboundHandler类来处理所有必须的任务。
@ChannelHandler.Sharable //标记该类的实例可以被多个Channel共享
public class FirstNettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 和服务器连接建立后将被调用
*/
public void channelActive(ChannelHandlerContext ctx) {
//当被通知Channel是活跃的时候,发送一条消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
// ctx.writeAndFlush("Netty rocks!");
}
/**
* 当从服务器接收到一条消息时被调用
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
System.out.println("Client received:" + in.toString(CharsetUtil.UTF_8));
}
/**
* 引发异常时会被调用
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("Can't connect server");
//发生异常时,记录错误并关闭Channel
cause.printStackTrace();
ctx.close();
}
}
-
首先重写channelActive方法,将在连接建立时被调用,确保了数据将会被尽可能快速地写入服务器。
-
重写channelRead0方法,每当接收数据时,都会调用。但需要注意的是服务器发送的消息可能会被分块接受
例如服务器发送了6个字节,不能保证这6个字节数据被一次性接收。有可能会调用多次channelRead0方法。
可能第一次接收4个字节的ByteBuf,第二次接收2个字节的ByteBuf。TCP保证了字节数组将按照服务器发送他们的顺序去接收。
-
重写exceptionCaught方法,当连接不上服务器时,会被调用,进行异常处理。
引导客户端代码实现
public class FirstNettyClient {
private final String host;
private final int port;
public FirstNettyClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建Bootstrap
Bootstrap b = new Bootstrap();
//指定EventLoopGroup以处理客户端事件;需要适用于NIO的实现
b.group(group)
//适用于NIO传输的Channel类型
.channel(NioSocketChannel.class)
//设置服务器的连接地址
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
//在创建Channel时,向ChannelPipeline中添加一个FirstNettyClientHandler实例
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FirstNettyClientHandler());
}
});
//连接到远程节点,阻塞等待直到连接完成
ChannelFuture f = b.connect().sync();
//阻塞,直到Channel关闭
f.channel().closeFuture().sync();
} finally {
//关闭线程池并且释放所有的资源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new FirstNettyClient("127.0.0.1",6666).start();
}
}
-
为了初始化客户端,创建了一个Bootstrap实例
-
分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据
-
为服务器连接创建了一个InetSocketAddress实例
-
当连接被创建时,FirstNettyClientHandler实例会被安装到ChannelPipeline
-
在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点
运行服务器和客户端
首先先启动服务器端:
启动完可以看到一直是阻塞状态,等待客户端的连接。
启动客户端:
通过控制台可以看到,当客户端启动成功后,服务器端就接收到了客户端的数据传送,接着客户端接收到了服务器端返回的数据。
总结
这个Netty应用程序整体就很简单,主要是体验下如何使用Ne t ty创建一个简单的应用程序。虽然很简单,但是它可以伸缩到支持数千个并发连接,这就是Netty的魅力所在,整个应用程序可能你还是对某些组件的还是不够清晰,后面的文章会陆陆续续给大家讲解,保证把Netty给吃透。