我们在网络编程——NIO编程中,就曾介绍过直接使用NIO进行编程,这里我们介绍下如何使用Netty框架,来完成我们之前直接使用NIO完成的功能,就是一个简单的客户端和服务端的通信。
在这之前,我们先来简单了解一下Netty框架的核心组件:
Channel
Channel 是Java NIO 的一个基本构造。它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。
目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
回调和Future
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。
Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler 的实现处理。
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操
作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
JDK 预置了interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。在并发编程中已有相关介绍。
ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。每个Netty 的出站I/O 操作都将返回一个ChannelFuture。
事件和ChannelHandler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
Netty事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法。可以认为每个Channel-Handler 的实例都类似于一种为了响应特定事件而被执行的回调。
Netty 提供了大量预定义的可以开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。
了解过我们在网络编程使用NIO完成的小例子,以及上述Netty核心组件之后,我们来看一看如何使用Netty来完成网络通信,首先来看一看客户端的实现:
public class NettyClient {
private final String host;
private final int port;
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); //线程组
Bootstrap bootstrap = new Bootstrap(); //客户端启动所需
try {
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class) //指定使用NIO进行网络通讯
.remoteAddress(new InetSocketAddress(host, port)) //配置远程服务器地址
.handler(new NettyClientHandler());
ChannelFuture channelFuture = bootstrap.connect().sync(); //连接远程服务器,阻塞,直到连接完成
channelFuture.channel().closeFuture().sync(); //阻塞关闭,直到channel关闭
} finally {
eventLoopGroup.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyClient("127.0.0.1", 8888).start();
}
}
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/**
* 客户端得知Channel活跃后的处理
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//向服务器发送数据
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, Netty", Charset.forName("UTF-8")));
}
/**
* 客户端接收数据后的处理
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("客户端接收到数据,内容为:" + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 异常处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
然后我们在看一看Netty服务端的实现,在服务端接受到客户端发送的消息之后,打印出来,并原封不动的再次发送给客户端,如下:
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws InterruptedException {
final NettyServerHandler serverHandler = new NettyServerHandler();
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); //线程组
ServerBootstrap serverBootstrap = new ServerBootstrap(); //服务端启动所需
try {
serverBootstrap.group(eventLoopGroup)
.channel(NioServerSocketChannel.class) //指定使用NIO进行网络通讯
.localAddress(new InetSocketAddress(port)) //指定服务器监听端口
//接收到连接请求,新启一个socket通信,即channel,每个channel都有处理自己事件的handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(serverHandler);
}
});
ChannelFuture channelFuture = serverBootstrap.bind().sync(); //绑定端口,阻塞,直到连接完成
channelFuture.channel().closeFuture().sync(); //阻塞关闭,直到channel关闭
} finally {
eventLoopGroup.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyServer(8888).start();
}
}
@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 服务端读取到网络数据后的处理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器接收到数据,内容为:" + byteBuf.toString(Charset.forName("UTF-8")));
ctx.write(byteBuf);
}
/**
* 服务器读取完成网络数据后的处理
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) //flush所有需发送的数据
.addListener(ChannelFutureListener.CLOSE); //当flush完成,关闭连接
}
/**
* 发生异常后的处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
其中若接收到的客户端的数据很大,需要多次调用上述的channelRead
方法来进行发送,完成后会调用channelReadComplete
方法,来flush所有的数据,并关闭与客户端的连接,客户端中监测到与服务端连接的channel被关闭后,随之客户端就会关闭了,如下
另外我们在服务端上添加了 @ChannelHandler.Sharable 注解,这里其实可以理解为该类是一个共享的,即我们在Spring中提到的单例的意思,所以我们在使用时,只需参加一个实例即可,如下
而我们在客户端的实现上就没有添加该注解,所以我们每次使用都会参加一个新的实例出来
在上述的实现中,我们可以将服务端不添加该注解,每次使用和客户端一样,创建出一个新的实例即可,或者我们也可以在客户端使用添加该注解,但是需要注意的是,我们在使用共享一个实例的时候,我们需要保证该类的线程安全性,至于如何保证一个类的线程安全,我们在并发编程中也以及说过了,这里就不重复介绍了。