项目地址: https://gitee.com/bigzibo/pp-netty-rpc
构建Netty应用
client
这里只说明与Netty有关的, 其他功能会略作解释
用了spring框架, 获得上下文容器
因为我们需要获取yml中配置的zookeeper地址
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.bootstrap = new Bootstrap();
// 这里的group中的eventLoop只负责监听channel状态
// 如果channel空闲则可以发送数据
// 如果channel中有回传则接收数据
this.group = new NioEventLoopGroup(1);
this.bootstrap.group(group).
channel(NioSocketChannel.class).
option(ChannelOption.TCP_NODELAY, true).
option(ChannelOption.SO_KEEPALIVE, true).
handler(this);
String zkAddress = applicationContext.getEnvironment().getProperty("zookeeper.address");
ZookeeperClient zkClient = new ZookeeperClient(zkAddress);
// manager是获取服务地址并缓存的类
// 访问服务的时候, 从manager获取具体服务对应的channel
this.manager = new NettyChannelManager(this, zkClient);
this.handler = new NettyClientHandler();
}
// 初始化channel的方法
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// 设置心跳时间 1分钟发送一次心跳
pipeline.addLast(new IdleStateHandler(0, 0, 60));
// 设置编码解码器
pipeline.addLast(new JSONEncoder());
pipeline.addLast(new JSONDecoder());
// 设置处理器
pipeline.addLast(this.handler);
}
// 获取channel连接的方法
public Channel connect(SocketAddress address) throws InterruptedException {
ChannelFuture future = bootstrap.connect(address);
// 这里添加了监听器
// 这个监听器的作用只是为了将连接成功或者断开的信息打印在控制台上
future.addListener(new ConnectionListener(this.manager));
// 如果通道已经关闭, 这里会抛出异常
Channel channel = null;
try {
channel = future.sync().channel();
} catch(Exception e) {
e.printStackTrace();
return null;
}
return channel;
}
通过上面的方法我们已经可以获取到一个具体的channel了, 只是connect方法缺少了一个入参address
connect方法是被manager调用的, 我们在manager里面缓存了服务地址->channel的一个map
channel过期后, 我们需要重新连接, 就会调用connect方法传入map中的服务地址
Handler
我们知道每个出入站事件都需要经过handler的处理, 所以handler中是写我们业务逻辑的地方
// 发起调用请求
public SynchronousQueue<Object> sendRequest(Request request, Channel channel) {
SynchronousQueue<Object> queue = new SynchronousQueue<>();
// 每个进入的线程都有独特的id
queueMap.put(request.getId(), queue);
// 发送信息, 这里不阻塞
// 在客户端中, 这一步执行完毕后, 在netty这一层上的客户端的下一步
// 就是进入channelRead方法
channel.writeAndFlush(request);
// 这里直接返回阻塞队列, 外部使用阻塞队列的take方法会阻塞等待结果
return queue;
}
// 处理入站事件, 客户端调用一个远程方法后, 服务端返回的结果在这里被处理
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Response response = JSON.parseObject(msg.toString(), Response.class);
// requestId是我们给每一个调用设置的一个唯一Id, 用于并发下判断结果归属
String requestId = response.getRequestId();
// 阻塞队列, 获取该调用的队列, 并且把结果放入
SynchronousQueue<Object> queue = queueMap.get(requestId);
queue.put(response);
// 到此为止, 该调用已经算是结束了
queueMap.remove(requestId);
}
我们的每个Handler都是共享的, 所以在并发的情况下, 会同时有很多请求需要处理,
所以在Handler中使用了一个Map存储每一个调用, 并且结果返回使用阻塞队列, 在handler中
就可以不需要做同步处理
我做了一个Handler的抽象类来把一些公共的通知提取了出来, 连接的断开与关闭的通知都在此处
客户端和服务端的handler都继承这个抽象类, 可以继续在子类中写自己的业务逻辑
public abstract class AbstractNettyHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("与{}的连接已建立", ctx.channel().remoteAddress());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.info("与{}的连接已关闭", ctx.channel().remoteAddress());
}
}
server
因为服务端必须向外开放一个端口用以netty连接, 所以跟客户端的处理方式也不同
在上下文中获取服务端地址+端口然后new了一个ServerWorker类, 该类会做netty的有关处理
public void open() throws Exception {
ServerBootstrap bootstrap = new ServerBootstrap();
// 无参构造器使用默认个数
//DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
// "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
// cpu核心数*2
this.bossGroup = new NioEventLoopGroup();
// 一个channel的生命周期位于一个eventLoop中, 一个LoopGroup下有许多loop, 一个loop与一个Thread绑定
// 但是一个eventLoop可能会有多个channel
this.workerGroup = new NioEventLoopGroup();
try {
// bossGroup用于分配新建立的连接给workerGroup中的eventLoop
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(this);
String[] hostAndPort = this.remoteAddress.split(":");
if (hostAndPort == null || hostAndPort.length != 2) {
throw new RuntimeException("remoteAddress是一个错误的地址.");
}
ChannelFuture cf = bootstrap.bind(hostAndPort[0], Integer.parseInt(hostAndPort[1])).sync();
logger.info("netty 服务器启动.监听端口:" + hostAndPort[1]);
// 监听closeFuture事件, 并且sync阻塞当前线程
cf.channel().closeFuture().sync();
} catch (Exception e) {
logger.log(Level.SEVERE, "netty服务端启动失败", e);
}
}
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// 设置心跳
pipeline.addLast(new IdleStateHandler(0, 0, 60));
// 设置编码解码器
pipeline.addLast(new JSONEncoder());
pipeline.addLast(new JSONDecoder());
// 设置handler
pipeline.addLast(this.handler);
}
服务端与客户端的不同之处在于, 服务端需要启动netty并且绑定端口
handler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Request request = JSON.parseObject(msg.toString(), Request.class);
if ("heartBeat".equals(request.getMethodName())) {
log.info("客户端心跳信息..."+ctx.channel().remoteAddress());
return;
}
Response response = new Response();
response.setRequestId(request.getId());
try {
// 这里做我们的业务逻辑处理, 解析request找到具体服务调用返回
Object res = this.handler(request);
response.setData(res);
} catch (Exception e) {
response.setCode(-1);
response.setError(e.getMessage());
log.error("请求调用失败", e);
}
ctx.writeAndFlush(response);
}
编码解码器
我们希望我们的数据作为json格式传输, 所以需要自定义编码解码器
// 解码器
public class JSONDecoder extends LengthFieldBasedFrameDecoder {
public JSONDecoder() {
super(65535, 0, 4,0,4);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf decode = (ByteBuf) super.decode(ctx, in);
if (decode==null){
return null;
}
int dataLen = decode.readableBytes();
byte[] bytes = new byte[dataLen];
decode.readBytes(bytes);
// 字节流解码, 使用json转为javaBean
Object parse = JSON.parse(bytes);
return parse;
}
}
// 编码器
public class JSONEncoder extends MessageToMessageEncoder {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, List out){
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// javaBean转json
byte[] bytes = JSON.toJSONBytes(msg);
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);
out.add(byteBuf);
}
}
经过编码解码器之后, 我们的handler中的入参才能是java对象
统一的数据接收发送
因为我们是服务的调用, 所以Request类中需要这个类的名称, 调用的方法, 参数和参数类型
而Response中则只需要提供结果和请求信息网络代码(502, 200 这一类的
@Data
public class Request {
private String id;
private String className;
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
}
@Data
public class Response {
private String requestId;
private int code;
private String error;
private Object data;
@Override
public String toString() {
return super.toString();
}
}
如何找到具体调用方法
在客户端发起一个请求之后, 我们发送的信息应该是:
- 方法名称
- 方法所属的类的名称
- 参数
- 参数类型
我们最起码需要拥有这些信息, 服务端才能知道你需要调用的具体方法是什么,
并且获取入参来给你返回结果
上面的一切的前提是, 客户端知道某个服务在某个具体的服务器上, 才可以去获取channel
然后发起请求, 所以在不使用zookeeper的情况下, 我们这些ip需要在配置中加入,
然后在客户端启动的时候, 生成服务名->List(ip)这样的数据结构, 某个服务名对应一系列ip
这些ip下的服务端都存在这个服务的具体实现逻辑
后记
由于我们使用spring框架, 所以我们应该将我们的程序与spring进行整合,
对我们的service接口进行我们自己定义的代理, 代理内容就是通过netty发起请求
去找到具体实现了该业务的服务端获取结果
通过上述操作, 一个简单的netty应用其实已经有了一个雏形了, 我们后续操作就像是
在一个毛坯房内进行装修和家具的添置