客户端将要做的事情
- 连接到服务器
- 发送一个或者多个消息
- 对于每个消息,等待并接收从服务器发回的相同的消息
-
通过ChannelHandler实现客户端逻辑
跟服务器一样,客户端也需要一个用来处理数据的ChannelInboundHandler。在这个场景下需要扩展SimpleChannelInboundHandler类以处理所有必须的任务
主要重写的方法: channelActive()—在到服务器的连接已经建立之后将被调用
- channelRead0()—当从服务器接收到一条消息时被调用
- exceptionCaught()—在处理过程中引发异常时被调用 ```java package cn.linguo.netty.handler;
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil;
/**
- @apiNote 标识该类的实例可以被多个Channel安全地共享
- @apiNote 客户端处理
@author linguo / @Sharable public class EchoClientHandler extends SimpleChannelInboundHandler
{ /**
当被通知Channel是活跃的时间,发送一条消息 */
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
/**
记录已接受消息的转储 */ @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { System.out.println(“客户端接收:” + msg.toString(CharsetUtil.UTF_8)); }
/**
- 在发生异常时,记录错误并关闭Channel */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
ChannelActive()方法将在一个连接建立时被调用,确保了数据将会被尽可能快地写入服务器,在这个场景下是一个变了字符串“Netty rocks!”的字节缓冲区<br />ChannelRead0()方法会在每次接收数据时被调用,需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了5个字节,那么不能保证这5字节被一次性接收。即使对于这么少量的数据,channelRead0()方法也可能被调用多次,第一次使用持有3字节的ByteBuf(Netty的字节容器),第二次使用一个持有2字节的ByteBuf。作为一个面向流的协议,TCP保证了字节数组将会按照服务器发送它们的顺序被接收。<a name="Q13ql"></a>### 思考为什么客户端使用的是SimpleChannelInboundHandler,而不是EchoServerHandler中所使用的ChannelInboundHandlerAdapter?<br />和两个因素的相互左右有关:业务逻辑如何处理消息以及Netty如何管理资源<br />在客户端当channelRead0()方法完成时,已经有了传入的消息并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用<br />在EchoServerHandler中,仍然需要将传入消息回送给发送者,而write()是异步的,直到channelRead()方法返回后可能仍然没有完成,为此EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。<br />--这个思考内容可能会比较模糊,不着急<a name="wUwfk"></a>### 引导客户端引导客户端类似于引导服务器,不同的是,客户端使用主机和端口参数来连接远程地址,也就是这里的Echo服务器的地址,而不是绑定到一个一直被监听的端口。```javapackage cn.linguo.netty.client;import java.net.InetSocketAddress;import cn.linguo.netty.handler.EchoClientHandler;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;/*** @apiNote 引导客户端** @author linguo**/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 InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {// 创建BootStrapBootstrap b = new Bootstrap();b.group(group) // 指定EventLoopGroup以处理客户端事件,需要适用于NIO的实现.channel(NioSocketChannel.class) // 适用于NIO传输的Channel类型.remoteAddress(new InetSocketAddress(host, port)) // 设置服务器的InetSockerAddress.handler(new ChannelInitializer<SocketChannel>() { // 在创建Channel时,向ChannelPipeline中添加一个EchoClientHandler实例@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new EchoClientHandler());}});// 连接到远程节点,阻塞等待直到连接完成ChannelFuture f = b.connect().sync();// 阻塞,直到Channel关闭f.channel().closeFuture().sync();} finally {// 关闭线程池并且释放所有的资源group.shutdownGracefully().sync();}}public static void main(String[] args) throws InterruptedException {if (args.length != 2) {System.err.println("Usage:" + EchoClient.class.getSimpleName() + "<host><port>");return;}String host = args[0];int port = Integer.parseInt(args[1]);new EchoClient(host, port).start();}}
主要步骤和关注要点
- 为初始化客户端,创建了一个Bootstrap实例
- 为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据
- 为服务器连接创建了一个InetSocketAddress实例
- 当连接被建立时,一个EchoClientHandler实例会被安装到该Channel的ChannelPipeline中
- 在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点
主要测试流程
根据自己项目的类型打包运行,maven直接jar包运行就好,注意port和localhost两个参数别缺失了。
依次启动Server\Client观察控制台打印的内容是否符合设计的要求
