Netty入站与出站机制
Netty中的ChannelHandler充当了处理入站和出站数据逻辑的容器,只要实现了其下的对应接口,就可以自定义一个处理器用于处理出站或入站的数据
对于客户端而言,如果数据的运动方向是从客户端到服务端的,那么这些事件我们就称之为出站,反之则是出站。当然这跟我们的目标系的选择有关,如果我们选择的是服务器,那么又反过来了
当Netty发送或接受一个消息的时候,会进行对应的编码解码处理。Netty提供了一系列对应的接口,他们都重写了channelRead方法,对于每个入站的信息会调用该方法并调用decode()方法进行解码并将解码的字节转发给下一个处理器
下面是将字节转为对象的解码器的继承图,TCP中囿于可能会分段发送信息,因此会出现粘包拆包的问题。为了解决该问题,该类会对入站数据进行缓冲直到其准备好被处理
举个例子就是我们可以重写对应的解码方法,这里我们假设我们知道客户端传入的对象是以int类型为主,一个int占据四个字节,那么我们每次判断传入的字节数是否大于四,大于我们就将其添加到集合中,由于字节传入时就会执行该方法,因此我们这里不需要写入while,调用ByteBuf对应的方法可以令其读指针后移,这样就能成功将所有的字节都读入到我们指定的集合中
最后我们将对应的集合传入给我们的下一个处理器,这个处理器就会执行对应业务处理
案例演示
接着我们来做一个案例来加深我们的理解,首先我们来看看案例要求
然后我们来看看业务内部的结构图,我们看到客户端的管道中存在ClientHandler处理数据,发送数据时会先发送给编码器,然后通过Socket发送服务器的解码器,解码器处理完后发送给服务器处理器,服务器发送数据时的流程也大差不差
那么接下来我们来正式写代码,首先我们先写服务器,我们这里要往其中加如自定义的处理器,childHandler中的类我们不采用匿名内部类的形式
public class MyServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
往服务端中的管道中添加对应的处理器,加入对应的额编码解码器之后再添加自己的业务逻辑处理器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//入站的handler进行解码 MyByteToLongDecoder
pipeline.addLast(new MyByteToLongDecoder());
//出站的handler,用于进行编码
pipeline.addLast(new MyLongToByteEncoder());
//自定义的handler处理业务逻辑
pipeline.addLast(new MyServerHandler());
}
}
解码器中我们继承ByteToMessageDecoder重写其decode方法,我们这里的解码规则很简单,就是判断传输的字节大小是否大于8,是的话我们就调用ByteBuf的对应的方法将字节写到集合中然后传给下一个handler进行处理
public class MyByteToLongDecoder extends ByteToMessageDecoder {
/**
* decode会根据接收的数据被调用多次,直到确定没有新的元素被添加到list或ByteBuf中没有更多的可读字节
* 如果list不为空,就会将list的内容传递给下一个ChannelInboundHandler处理,该处理器的方法也会被调用多次
* @param ctx 上下文对象
* @param in 入站的ByteBuf
* @param out List集合,将解码的数据传给下一个handler
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyByteToLongDecoder被调用");
//因为Long是8个字节,需要判断有8个字节才能读取一个long
if(in.readableBytes()>=8){
out.add(in.readLong());
}
}
}
编码器的代码需要继承MessageToByteEncode并指定需要处理的对象,逻辑是打印对应的数据之后再将数据写出
public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
/**
* 编码方法
* @param ctx
* @param msg
* @param out
* @throws Exception
*/
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("MyLongToByteEncoder中的encode方法被调用");
System.out.println("msg= " + msg);
out.writeLong(msg);
}
}
这里需要注意的是,解码器会将所有传输的数据都按照指定的规则进行解码,但是编码器则只能编码指定对象,进行入该类MessageToByteEncoder中,我们可以看到其对应的方法先判断传出的对象是否是指定的对象,否则就直接调用写方法写出去,下面是其源码
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {
@SuppressWarnings("unchecked")
I cast = (I) msg;
buf = allocateBuffer(ctx, cast, preferDirect);
try {
encode(ctx, cast, buf);
} finally {
ReferenceCountUtil.release(cast);
}
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release();
}
}
}
最后是我们的自定义的业务逻辑处理器,我们这里做的事情无非就是打印接受的数据然后向客户端发送一个long类型的数据
public class MyServerHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
System.out.println("从客户端"+ctx.channel().remoteAddress()+"读取到long "+msg);
//给客户端发送一个long
ctx.writeAndFlush(98765L);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
接着我们来写客户端,无非是创建一个对应的NioEventLoopGroup对象然后设置对应的参数而已,同样也是自定义一个初始化类
public class MyClient {
public static void main(String[] args) throws Exception{
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
自定义类中加入对应的编码解码器和自定义业务处理器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个出站的handler对数据进行编码
pipeline.addLast(new MyLongToByteEncoder());
//加入一个入站的解码器(入站handler)
pipeline.addLast(new MyByteToLongDecoder());
//加入一个自定义handler用于处理业务
pipeline.addLast(new MyClientHandler());
}
}
自定义处理器中,我们打印对应的数据并重写channelActive方法,让通道准备就绪时就向服务器发送一个数据
public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
System.out.println("服务器的ip="+ctx.channel().remoteAddress());
System.out.println("收到服务器消息="+msg);
}
//重写channelActive方法发送数据
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyClientHandler 发送数据");
ctx.writeAndFlush(123456L); //发送一个long
}
}
最后打开程序我们可以得到下面的结果,我们可以看到调用的顺序时先调用其自定义的处理器发送数据,然后是编码器然后发送数据(msg那一列应该是我跟着课程写代码时搞错了导致在代码上找不到对应的打印代码),然后是解码器调用,最后是业务处理器处理服务器发送过来的消息
服务端的代码首先嗲用解码器然后是自定义处理器类,接着是编码器然后发送数据
这里我们还需要提一点,如果我们发送的属于不是long类型的,那么我们的编码器并不会对其正确编码,而是会直接写出,但我们的解码器会按照规则正确解码,这就导致如果我们发送的数据并不是我们预期的数据,那么就会导致我们最终得到的数据与预期的不一样
同时解码器一旦接收到足够大小的数据就会立刻交由自定义处理器进行处理,也就是说一个大文件发过来可能会有多次访问接收读取的过程
其他编码解码器
实际上Netty还提供了ReplayingDecoder解码器,该编码器可以对起那么的案例进行简化
最直接的简化就是不用判断数据是否足够,内部会自动进行解析,我们直接添加即可,下面是优化后的解码器代码
不过其也有其局限性,比如其并不是所有的ByteBuf操作都支持,并且某些情况下效率可能会变低
Netty还提供了其他很多的编码和解码器,这些我们了解即可
Netty整合Log4j
首先我们要引入对应的依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
然后我们在resource中创建log4j.properties的文件,写入其内容如下
log4j.rootLooger=DEBUG,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] %C{1} - %m%n
接着我们再启动项目就可以看到我们的日志了
TCP粘包拆包
TCP是面向流和连接的协议,而面向流的通信是没有消息保护边界的,因此使用TCP发送数据时会出现粘包拆包问题
下面是对应的图示和解释
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
- 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
粘包拆包案例演示
接下来我们来做一个案例来演示TCP的粘包拆包
首先编写服务端,跟之前的一样
public class MyServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
初始化类中我们添加入自定义的处理器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyServerHandler());
}
}
自定义的处理器中我们处理ByteBuf对象,接收到我们就将其转换为byte数组,将其转换为字符串并打印,我们同时也记录服务器收到的消息量,后面我们设置服务端每次接受数据就会回送给客户端一个随机ID
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
//将buffer转为一个字符串
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("服务器接受到数据 "+message);
System.out.println("服务器接受到消息量="+(++this.count));
//服务器会送数据给客户端,回送一个随机id
ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString()+" ", Charset.forName("utf-8"));
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
然后我们编写客户端,同样传入一个初始化类
public class MyClient {
public static void main(String[] args) throws Exception{
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
添加自定义处理器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
客户端中我们对接受的消息同样进行处理并记录次数,当通道准备完毕时我们往服务端中发送十条数据
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] buffer = new byte[msg.readableBytes()];
msg.readBytes(buffer);
String message = new String(buffer, Charset.forName("utf-8"));
System.out.println("客户端接受到消息=" + message);
System.out.println("客户端接受消息数量"+ (++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送十条数据
for (int i = 0; i < 10; i++) {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,server" + i, StandardCharsets.UTF_8);
ctx.writeAndFlush(byteBuf);
}
}
}
最后我们只要启动服务端和多个客户端就可以得到服务器中接受数据的情况,我们会看到有时候接受到一部分,有时候接收到全部,这就证明了此时发生了TCP粘包拆包问题
问题解决方案
要解决TCP粘包拆包的问题,关键是要解决服务器端每次读取数据长度的问题。下面我们就来实现解决该问题的案例,下图是案例要求
首先我们要写入对应的传输数据的实体类
/**
* 协议包
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageProtocol {
private int len;
private byte[] content;
}
服务端的代码没什么变化
public class MyServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
初始化类里我们需要加入实体类的解码器和编码器
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解码器
pipeline.addLast(new MyMessageDecoder());
//编码器
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyServerHandler());
}
}
实体类的解码里就是读取对应的属性字段数据然后封装到一个对象中并加入到集合
public class MyMessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyMessageDecoder的decode方法被调用");
//需要将得到的二进制字节码转化为MessageProtocol数据包(对象)
int length = in.readInt();
byte[] content = new byte[length];
in.readBytes(content);
//封装成MessageProtocol对象,放入out,传给下一个handler进行业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
}
在服务器的业务处理器中,我们会打印接受的消息并回复一个消息给客户端,这个消息是有UUID随机组成的字符串构建的指定的实体类
/**
* 处理业务的handler
*/
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
//接收到数据并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务端接受到信息如下:");
System.out.println("长度="+len);
System.out.println("内容="+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到消息包数量: "+ (++this.count));
//回复消息
String responseContent = UUID.randomUUID().toString();
int responseLen = responseContent.getBytes("utf-8").length;
byte[] bytes = responseContent.getBytes("utf-8");
//构建一个协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(responseLen);
messageProtocol.setContent(bytes);
ctx.writeAndFlush(messageProtocol);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客户端也没啥变化
public class MyClient {
public static void main(String[] args) throws Exception{
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.handler(new MyClientInitializer()); //自定义一个初始化类
ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
初始类里要加入实体的编码器和解码器
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入对应实体类的编码器
pipeline.addLast(new MyMessageEncoder());
//解码器
pipeline.addLast(new MyMessageDecoder());
pipeline.addLast(new MyClientHandler());
}
}
实体的编码器代码如下,我们这里只是获得对象的属性加入到buffer中
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder的encode方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
客户端的handler里我们就创建对应的对象并发送到服务端中,接受到服务端发送的消息之后就将消息进行打印
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送5条数据
for (int i = 0; i < 5; i++) {
String msg = "永失吾爱,举目破败";
byte[] content = msg.getBytes(StandardCharsets.UTF_8);
int length = msg.getBytes(StandardCharsets.UTF_8).length;
//创建协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("客户端接收到的消息如下:");
System.out.println("长度="+len);
System.out.println("内容="+new String(content,Charset.forName("utf-8")));
System.out.println("客户端接受消息数量"+(++this.count));
}
}
最后我们只要案例就能发现我们的案例已经正确运行了,并且服务器也能正确接受到数据并且组成对象,不会出现粘包拆包的问题,说明解决粘包拆包的问题只要指定好了接受数据的大小就可以解决,并且解决粘包拆包问题之后,我们的客户端的服务端都能按顺序准确接受并组装到我们传入的数据
Netty源码剖析
现在我们进入Netty学习的最后一部分,看源码
我们通过源码分析的方式来看一下Netty的启动过程
我们主要分析Netty调用的doBind方法,需要Debug到NioEventLoop中的run代码,其实无限循环的在服务端运行的一块代码
服务端初始化
我们直接从源码中复制出我们的Debug的源码来运行,这样便于我们分析,客户端代码如下
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.haotongxue.learn;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
/**
* Sends one message when a connection is open and echoes back any received
* data to the server. Simply put, the echo client initiates the ping-pong
* traffic between the echo client and server by sending the first message to
* the server.
*/
public final class EchoClient {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));
public static void main(String[] args) throws Exception {
// Configure SSL.git
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(HOST, PORT).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
}
}
客户端自定义处理器代码如下
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.haotongxue.learn;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Handler implementation for the echo client. It initiates the ping-pong
* traffic between the echo client and server by sending the first message to
* the server.
*/
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf firstMessage;
/**
* Creates a client-side handler.
*/
public EchoClientHandler() {
firstMessage = Unpooled.buffer(EchoClient.SIZE);
for (int i = 0; i < firstMessage.capacity(); i ++) {
firstMessage.writeByte((byte) i);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(firstMessage);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
服务端代码如下
public final class EchoServer {
static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
}
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(new LoggingHandler(LogLevel.INFO));
//p.addLast(new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务器端自定义处理器类代码如下
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.haotongxue.learn;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Handler implementation for the echo server.
*/
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
super.handlerRemoved(ctx);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
服务端启动类中首先创建了SSL的配置类,也就是bossGroup和workerGroup这两个对象,他们是Netty的核心对象,整个Netty的运作都依赖于他们
我们Debug位置定在workerGroup那里,我们能看到如果不指定创建的NioEventLoop数量,则默认为cpu核数*2
内部创建对应的NioEventLoop数组时会往其children属性中创建对应的数组,然后通过for遍历赋值
如果这个赋值过程中出现了异常,那么就会对对应的时间循环组进行关闭,同时生成对应的时间循环组依靠executor对象,因此也会对该对象进行对应的处理
客户端引导类
Bootstrap对象是一个引导类,与ServerChannel关联,其内部中的group方法将bossGroup和workerGroup设置到其属性中,这里重命名为parentGroup和childGroup,因此这两个时间循环组其是也有后面的两个别名
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
ObjectUtil.checkNotNull(childGroup, "childGroup");
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
} else {
this.childGroup = childGroup;
return this;
}
}
Bootstrap内部通过Class对象反射创建ChannelFactory,接着通过下图的步骤运行
Bootstrap启动类中handler方法是将对应的处理器交由bossGroup,而childrenHandler方法则是交由给workGroup
在创建具体的时间循环组数组给其赋值的方法中,使用的是下面的方法,其各个参数有其代表的意义
如果executor为null则使用Netty默认的线程工厂,同时其会将每一个创建的时间循环组都放入到HashSet中
服务端引导类
ServerBootstrap也就是服务器端的引导类,其实一个空构造,但是有默认的成员变量,其属性中的config对象后面会有很大作用
其调用的方法的配置说明大概如下
服务端绑定端口
服务器的端口绑定是在bind方法中完成的,bind方法中的核心方法是doBind
doBind的核心方法是initAndRegister和doBind
initAndRegister方法通过ServerBootstrap创建一个事件循环组,同时有下面的结论
那么最终init初始化NioServerSocketChannel具体追踪源码可以得到如下结论
init(channel)方法是在反射的channelFactory对象创建channel对象之后对该对象进行初始化的方法,其过程如下
addLast处于是Pipeline的核心,其会检查handler是否符合标准并将对应的handler加入到pipeline中
doBind方法需要不断进入断点
当开到下图中的最终方法时就说明到了最终执行绑定端口的方法了
doBind方法会追踪到NioServerSocketChannel中的doBind,说明其底层使用的是Nio
此时继续debug就会进入到时间循环组中监听发生的时间
接受请求源码
接着我们来分析Netty接受请求的源码
首先我们知道,服务器是等待客户端的链接的,也就是说NioServerSocketChannel将自己注册到了boss单例线程池上,也就是EventLoop,因此我们要从该类中分析,进入其processSelectedKey中
我们可以看到其先判断传入的请求是否是接受请求,实际请求的标志数值为16,正好为接受请求,因此执行unsafe.read()方法
read方法中首先检查对应线程是否是当前线程,经过一些步骤之后执行doReamdMessage方法
doReadMessage方法中通过工具类获取内部封装的accept方法,获得对应的SocketChannel进行封装最后添加到容器中
回到read方法中,read方法会循环执行pipeline.fireChannelRead方法
pipeline.fireChannelRead方法会执行管道中handler的ChannelRead方法
该方法会将客户端连接注册到worker线程池并增加监听器
下图是对上面的操作的说明
接着我们继续最终channelRead()方法中的register()方法,其会调用register0()方法
最终其会调用doBeginRead方法,也是AbstractNioChannel,执行完时就说明针对客户端的连接已经完成,接下来执行监听读事件
最后是总体流程的梳理
Pipeline
接着我们来讲管道
ChannelSocket中会联系一个ChannelPipeline,每个管道中有多个ChannelHandlerContext对象,该对象会包装处理器类
当一个请求进来时,会进入Socket对应的pipeline,并经过pipeline所有的handler,为过滤器模式
ChannelPipeline的接口被下面的接口类所继承
下面是ChannelPipeline实现类的部分源码
下面是ChannelPipeline的类的流程图,可以将请求分为入站和出站请求。
不过这里值得一提的是,实际上Netty处理入站和出站时都只使用一个管道中的类来进行处理,上图表示的只是两个不同的过程,而不是说Netty中有两个专门处理出站入站的两个链表
handler在pipeline处理业务请求时会调用其下的fireChannelRead的方法转发给其最近的处理程序进行处理
其下的逻辑时不断进行遍历,直到遍历到下一个对应的处理器进行处理然后继续转发
入站和出站事件的处理逻辑决定了为什么我们要加入编码解码器且它们都必须遵守一定的顺序
最后要注意的是,处理业务的处理器不能阻塞IO线程,如果我们的业务处理的时间真的很长,那就应该要将其设置成异步的
ChannelHandler
ChannelHandler其下有对应的方法可以调用,我们之前已经使用过了
ChannelHandler的作用是要用于处理IO事件,事件分为入站和出站因此其有链各个子接口
入站接口
出站接口
还有可以同时处理入站和出站实现的类,不过我们不用推荐使用
ChannelHandlerContext
接着我们来讲解ChannelHandlerContext对象,首先来看看其UML图,可以看到其实现了入站和出站的接口
其实现的接口就是专门针对入站和出站方法
其不但继承了入站和出站接口的方法,同时还定义了自己的方法
创建过程
再来看看其创建过程,首先创建ChannelSocket时就会同时创建一个pipeline,当调用其下的方法添加handler时,就会对该对象进行包装组成Context对象,该对象在pipeline中组成双向链表
下面我们来看看源码
可以看到我们首先创建pipeline
然后通道同时创建future和promise用于异步回调,然后我们创建两个对应对象形成双向链表
在添加处理器时创建Context
下面是对应的代码
添加handler时做了线程安全的处理,每添加一个handler都会关联一个Context
最后我们来看看总结
调度handler
现在我们来解析Pipeline是如何调度handler处理器的,首先我们要明确,如果是入站事件,则调用的处理方法为fire开头的方法
ChannelPipieline了的真实对象是DefaultChannelPipeline
该类内部都是inbound的方法,传入的也是inbound类型的head里的handler
这些静态方法会调用head里的方法,然后再调用其处理器的真正方法
之所以我们的处理器会处理消息或者是预备时会先执行我们设置的方法,是因为我们的源码里有做个对应的设定
再来看看outbound的fire方法的实现
里面存在的都是出站的实现,当然,也很正常,因为我们这里本来就是处理出站的类
再来看看其执行流程图和说明
最后我们来看看本节梳理
心跳源码
接着来讲Netty的心跳功能的源码
我们的的讲述重点在于IdleStateHandler这个类
该类提供了四个对应的属性用于判断,这里的时间是以纳秒为单位的
当handler被添加到pipeline中时,会调用initialize方法
该方法中通过swtich对对应不同状态的情况进行了处理,比如说读超时,写超时,读写超时等
private void initialize(ChannelHandlerContext ctx) {
switch(this.state) {
case 1:
case 2:
return;
default:
this.state = 1;
this.initOutputChanged(ctx);
this.lastReadTime = this.lastWriteTime = this.ticksInNanos();
if (this.readerIdleTimeNanos > 0L) {
this.readerIdleTimeout = this.schedule(ctx, new IdleStateHandler.ReaderIdleTimeoutTask(ctx), this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (this.writerIdleTimeNanos > 0L) {
this.writerIdleTimeout = this.schedule(ctx, new IdleStateHandler.WriterIdleTimeoutTask(ctx), this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (this.allIdleTimeNanos > 0L) {
this.allIdleTimeout = this.schedule(ctx, new IdleStateHandler.AllIdleTimeoutTask(ctx), this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
}
只要给定的对应参数那么其就会创建对应的定时人物同时将state状态设置为1,防止重复初始化
该类中有三个定时内部类,它们都共有一个AbstractIdleTask父类
这个父类提供了一个处理读写事件的模板方法
private abstract static class AbstractIdleTask implements Runnable {
private final ChannelHandlerContext ctx;
AbstractIdleTask(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
public void run() {
if (this.ctx.channel().isOpen()) {
this.run(this.ctx);
}
}
protected abstract void run(ChannelHandlerContext var1);
}
首先是读事件处理的内部类
private final class ReaderIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
protected void run(ChannelHandlerContext ctx) {
long nextDelay = IdleStateHandler.this.readerIdleTimeNanos;
if (!IdleStateHandler.this.reading) {
nextDelay -= IdleStateHandler.this.ticksInNanos() - IdleStateHandler.this.lastReadTime;
}
if (nextDelay <= 0L) {
IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = IdleStateHandler.this.firstReaderIdleEvent;
IdleStateHandler.this.firstReaderIdleEvent = false;
try {
IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
IdleStateHandler.this.channelIdle(ctx, event);
} catch (Throwable var6) {
ctx.fireExceptionCaught(var6);
}
} else {
IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
该类首先获取到时间,然后用当前的时间减去最后一次读的时间,结果小于0说明超时,此时触发事件,反之则继续放入队列中继续计算时间
触发事件后会创建一个对应的写事件对象并传递给用户的自定义处理器处理
然后是写事件的代码,和读事件扎不多,但是我们这里会判断这次写时间是不是因为业务执行的时间过长导致的(有时发生读空闲并不是因为业务空间,而是因为网络波动的原因导致业务处理时间变长)
private final class WriterIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
protected void run(ChannelHandlerContext ctx) {
long lastWriteTime = IdleStateHandler.this.lastWriteTime;
long nextDelay = IdleStateHandler.this.writerIdleTimeNanos - (IdleStateHandler.this.ticksInNanos() - lastWriteTime);
if (nextDelay <= 0L) {
IdleStateHandler.this.writerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = IdleStateHandler.this.firstWriterIdleEvent;
IdleStateHandler.this.firstWriterIdleEvent = false;
try {
if (IdleStateHandler.this.hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.WRITER_IDLE, first);
IdleStateHandler.this.channelIdle(ctx, event);
} catch (Throwable var8) {
ctx.fireExceptionCaught(var8);
}
} else {
IdleStateHandler.this.writerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
最后是处理读写事件的代码
private final class AllIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
AllIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
protected void run(ChannelHandlerContext ctx) {
long nextDelay = IdleStateHandler.this.allIdleTimeNanos;
if (!IdleStateHandler.this.reading) {
nextDelay -= IdleStateHandler.this.ticksInNanos() - Math.max(IdleStateHandler.this.lastReadTime, IdleStateHandler.this.lastWriteTime);
}
if (nextDelay <= 0L) {
IdleStateHandler.this.allIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = IdleStateHandler.this.firstAllIdleEvent;
IdleStateHandler.this.firstAllIdleEvent = false;
try {
if (IdleStateHandler.this.hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.ALL_IDLE, first);
IdleStateHandler.this.channelIdle(ctx, event);
} catch (Throwable var6) {
ctx.fireExceptionCaught(var6);
}
} else {
IdleStateHandler.this.allIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
这里每次计算时会取两次时间的最大值来计算
最后我们来看看本章小结
最后要注意的是ReadTimeoutHandler继承IdleStateHandler,当触发读空闲时会触发对应方法并关闭Socket。而WriteTimeoutHandler是通过传入的promise的完成情况来判断是否超时的,两者内部的运行逻辑有所不同
EventLoop
接着我们来讲解EventLoop的源码
先来看看其UML图
其间接继承或者实现了许多接口,这些接口使得EventLoop可以实现许多功能,其是一个单例的线程池,内部的死循环不断做着监听端口、处理端口事件、处理队列事件这三件事情。同时每隔EventLoop可以绑定多个Channel,但是每个Channel始终只能有一个EventLoop来处理
来看看Executor其下拥有的类,我们主要分析SingleThreadEventExecutor类
其下的excute方法会启动线程并将任务添加到对应的队列中去,这个队列就是我们之前说过的每个EventLoop都会维护的Task队列,注意任务其实本身是一个新线程来做的,这也是为什么我们这里传入的对象是Runnable对象
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
} else {
boolean inEventLoop = this.inEventLoop();
this.addTask(task);
if (!inEventLoop) {
this.startThread();
if (this.isShutdown()) {
boolean reject = false;
try {
if (this.removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException var5) {
}
if (reject) {
reject();
}
}
}
if (!this.addTaskWakesUp && this.wakesUpForTask(task)) {
this.wakeup(inEventLoop);
}
}
}
下面是其启动线程的步骤
再来看看addTask方法
下面使其添加任务的步骤
在启动线程的方法中首先判断是否已经启动,若没有则更改状态然后调用doStartThread方法,若失败会回滚
doStratThread方法中红框的代码就是真正启动线程的代码
点进run方法会发现这是一个抽象方法,我们具体进入到NioEventLoop中就会发现其实现了对应的方法并进行了监听
这些过程的具体的执行步骤如下
下面是run方法的代码
整个run方法只做了下面三件事情,第一件是select获取感兴趣的事件,第二是processSelectedKeys处理事件,第三件是runAllTasks执行队列中的任务,执行第三件事情的比例还会根据设定的ioRatio来变化
select方法的代码和运行过程如下,其本质是一个死循环用于处理事件
如果有发生下面的事件,则会跳出循环
最后我们来看看小结
加入异步线程池
我们之前讲过在Netty处理耗时操作时需要将该操作设置为异步,我们前面的解决方式是通过调用channel对象的execute方法并new一个Runnable对象来解决的
但实际上这种方式会使得我们的业务处理一直都是使用同一个线程,那这本质其实还是阻塞的,因此我们需要使用别的方式,我们这里有下图两种方式,我们接下来慢慢讲
handler加入线程池
首先我们来讲handler加入线程池的方式,我们直接往对应的处理器类中创建一个线程池属性,这里的DefaultEventExecutorGroup是一个线程池对象,是基于JDK的线程池创建封装而来的对象,实际上我们直接创建JDK的线程池对象也是可以的
然后我们调动对应线程池的提交方法,重写其Callable函数,内部写入我们的业务处理代码
这样我们的代码就不会阻塞了,而且采用这种方式,即使我们同时处理多个业务,也是可以并行的,不需要等待
实际得到的结果里,当我们开启服务端后,我们开启多个客户端,会发现处理请求的线程会变化,而我们具体将任务交由线程池中的线程也会变,都是轮询,之所以会这样是因为我们的workGroup中有多个线程,而这个线程中又有多个线程,因此会有两种变化
我们实际的业务流程图如下,我们Socket接受到请求之后将请求传递给对应的业务处理器类进行处理,接着我们会将其提交给另外的线程进行处理,当我们的业务处理器类处理完后,我们会重新调用workerGroup中原来的线程将结果传递给客户端
下面是流程图的解释
然后我们来看看源码,workerGroup中的业务处理鳄梨中的write方法进入时会判断当前线程是否是业务处理线程,若不是会将当前的任务封装为task加入到队列中,队列中的任务执行完毕后会获得结果
Context加入线程池
第二种方式是直接在服务器类中创建线程池
嗲用addLast方法时传入对应的线程池
这样我们的业务就会直接和线程池中的线程绑定,也能实现调用,显示源码逻辑的说明
这两种方式第一种方式更为灵活,第二种方式更为方便,自己选择吧
Netty实现DubboRPC
到此为止我们的Netty的内容就全部讲完了,最后我们来用Netty做一个DubboRPC的案例作为总结
RPC指的是远程过程调用,是一个计算机通信协议,其允许运行于一台计算机的程序调用另外计算机的子程序而无需额外的编程
下面是其调用的流程图
常见的PRC框架有阿里的Dubbo、Google的gRPC等
RPC调用流程图简化版,这里值得一提的是在RPC中Clinet叫服务消费者,而Server叫服务提供者
RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用
案例需求
底层的网络通信我们使用Netty4.1.20,同时我们需要创建共有的接口以及对应的提供者和消费者以及它们的处理器
下面是其结构图说明
首先我们编写共同的接口
/**
* 该类为接口,是服务提供方和服务都需要的
*/
public interface HelloService {
String hello(String mes);
}
然后我们来编写其实现类,经典接受数据并进行处理
public class HelloServiceImpl implements HelloService {
private int count = 0;
/**
* 当有消费者调用方法时,就返回一个结果
* @param mes
* @return
*/
@Override
public String hello(String mes) {
System.out.println("收到客户端消息="+mes);
//根据mes返回不同的结果
if(mes!=null){
return "Hello Client,我已经收到你的消息["+mes+"] 第"+(++count)+"";
}
return "Hello Client,我已经收到你的消息";
}
}
然后来编写服务端,我们这里做的事情和前面的差不多,同样加入一个自定义处理器
public class NettyServer {
public static void startServer(String hostName,int port){
startServer0(hostName,port);
}
/**
* 编写一个方法来完成NettyServer的初始化和启动
* @param hostname
* @param port
*/
private static void startServer0(String hostname,int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap(1);
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new NettyServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
自定义的handler逻辑很简单,就是规定我们的协议,对对应的消息进行处理并回写到客户端上
/**
* 服务器端的handler逻辑比较简单
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//获取客户端的消息,并调用服务
System.out.println("msg="+msg);
//客户端在调用服务器api时,需要定义规范,要求满足一定条件才允许调用
if(msg.toString().startsWith("HelloService#hello#")) {
String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
ctx.writeAndFlush(result);
}
}
然后我们来写客户端,客户端中同样是有初始化客户端的方法,我们这里同时还编写一个代理方法,可以获取一个代理对象并通过该代理对象将对应的参数传入来获取我们所需要的结果
如果客户端为null,那么会进行对应的初始化,同样也加入了自定义的处理器,这里值得一提是每次请求出现都会新创建一个NettyClinet对象用于处理请求
public class NettyClinet {
//创建线程池
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static NettyClientHandler client;
private int count = 0;
//编写一个方法使用代理模式,获取一个代理对象
public Object getBean(final Class<?> serviceClass,final String providerName) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{serviceClass},(proxy,method,args) -> {
//这部分的代码挥别反复执行,客户端每调用一次api就执行一次
System.out.println("(proxy,method,args)方法被调用..."+(++count)+"次");
if(client==null){
initClient();
}
//设置要发给服务器的信息
//providerName协议头args[0],就是客户端调用api时发送的参数
client.setPara(providerName+args[0]);
return executor.submit(client).get();
});
}
//初始化客户端
private static void initClient() {
client = new NettyClientHandler();
//创建EventLoopGroup
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(client);
}
});
try {
bootstrap.connect("localhost",7000).sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
自定义的处理器中我们有三个属性,我们这里在处理数据的方法中需要唤醒线程,这是因为我们的回调函数在向服务器发送数据之后,由于需要过一段时间才能得到结果,因此先将该线程休眠,等到获得服务端的数据时再唤醒该线程进行处理,休眠的逻辑我们可以在call方法中看到
同时由于接收业务处理和休眠的两份方法存在线程安全问题,因此需要加入synchronized关键字
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
/**
* 上下文
*/
private ChannelHandlerContext context;
/**
* 返回的结果
*/
private String result;
/**
* 客户端调用方式时传入的参数
*/
private String para;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive方法被调用");
//因为在其他方法中会使用到ctx
context = ctx;
}
/**
* 收到服务器的数据后就会调用该方法
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
result = msg.toString();
//该方法或唤醒等待的线程
notify();
}
/**
* 被代理对象调用,发送数据给服务器 -> 等待被唤醒 -> 返回结果
* @return
* @throws Exception
*/
@Override
public synchronized Object call() throws Exception {
System.out.println("call方法被调用");
context.writeAndFlush(para);
//进行wait,等待channelRead方法获取到服务器的结果后唤醒
wait();
System.out.println("call方法再次被调用");
//服务方返回的结果
return result;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
void setPara(String para){
System.out.println("setPara方法被调用");
this.para=para;
}
}
然后我们需要服务端的启动类,我们这里直接调用创建类的启动服务的方法
public class ServerBootstrap {
public static void main(String[] args) {
//代码代填...
NettyServer.startServer("localhost",7000);
System.out.println("服务提供方开始提供服务~~~");
}
}
首先定义协议头,创建消费者,然后创建代理对象,接着循环一直向服务器发送消息
public class ClientBootstrap {
//这里定义协议头
public static final String PROVIDER_NAME = "HelloService#hello#";
public static void main(String[] args) throws InterruptedException {
//创建一个消费者
NettyClinet customer = new NettyClinet();
//创建代理对象
HelloService service = (HelloService) customer.getBean(HelloService.class, PROVIDER_NAME);
while (true){
Thread.sleep(10 * 1000);
//通过代理对象调用服务提供者的方法(服务)
String res = service.hello("你好 dubbo~");
System.out.println("调用的结果res="+res);
}
}
}
我们这里的流程是通过服务器启动类创建启动类,然后我们初始化服务器,接着通过客户端启动类开启客户端,首先创建一个客户端类,然后通过该类获取代理对象,然后通过代理对象不断调用服务器的方法
发送方法时我们指定字符串即可,客户端中重写了动态代理的方法,我们传入对应的对象就可以得到我们需要代理对象,我们调用代理对应的发送方式时,实际上其就在执行我们重写的动态代理中的方法,该方法执行时先判断客户端处理器是否为空,若真为空则初始化该类,同时创建客户端并将该处理器加入到客户端中,然后设置对应的参数之后调用线程中的提交方法,传入该客户端处理器类就可以之后就可以得到我们想要的结果,业务处理器中我们对对应的结果进行了相应的处理