本章节我们介绍下如何使用netty实现RPC
场景引入
我们先来想象一下这样一幅场景:
现在有一个用户A想买辆车,但是A对车子不是很熟悉,于是他打电话给他的朋友B进行咨询:
A:兄弟,我想买辆车
B:想买辆什么样的车啊
A:我不是很懂车子,我说几个品牌你帮我参考下。法拉利怎么样?
B:911,跑车啊,是好车!
A:那兰博基尼呢?
B:那是男人心中的梦想!
A:那我大概知道了,那就兰博基尼好了,那我买什么颜色好呢?红色咋样?
B:红色啊,红艳艳的,不错哦
A:我看还有一辆紫色的,紫色怎么样?
B:紫色高贵的很呐,紫色好
A:好唻,就选紫色了!
RPC
什么是RPC呢?简单来讲,就是节点A远程调用节点B获取数据。结合上面的场景,用户A通过电话B获取到车子品牌、颜色等信息的过程,就叫RPC。
我们对上面的过程进行抽象,用户A对应于Client,朋友B对应于Server,电话即对应于netty。
代码
下面我们分析下场景,一点点把代码编写出来。
在上面的例子中,用户A其实提了两个需求:
- 通过汽车品牌的获取到汽车评价
- 通过具体的颜色,获取B对颜色的评价
为此,我们可以抽象出两个接口:
public interface Car {
String getCar(String carName);
}
public interface Color {
String getColor(String color);
}
接口的返回值,可以是车辆的描述,也可以是颜色的描述。
现在接口有了,我们需要为朋友B的回复进行编码:
public class CarImpl implements Car {
@Override
public String getCar(String carName) {
if ("兰博基尼".equals(carName)) {
return "兰博基尼, 好车,快,男人的梦想";
}
if ("法拉利".equals(carName)) {
return "法拉利, 跑车,911!";
}
return "";
}
}
public class ColorImpl implements Color {
@Override
public String getColor(String color) {
if ("红色".equalsIgnoreCase(color)) {
return "红色,红艳艳";
}
if ("紫色".equalsIgnoreCase(color)) {
return "紫色,高贵";
}
return "";
}
}
至此,我们已经将服务端的业务逻辑书写完毕了。
下面我们需要定义一下我们的数据通信协议。网络传输协议就走TCP好了,这个就暂时不管了。
下面我们来定义一下Request和Response。
注:此处为了简单,我们直接定义为一个对象了,实际的通信协议并非如此,以常用的http协议为例:
我们可以看到head部分会有host参数,connection参数,agent参数等,其他还有session等,这些都属于head部分,后续还有body部分,返回报文也是类似。为了简便,我们统一将其定义为Request和Response对象。
我们来看下Request:
@Data
@Builder
public class Request {
private RequestHeader header;
private RequestBody body;
}
@Data
@Builder
public class RequestHeader implements Serializable {
private long flag;
private long requestID;
private long dataLength;
}
@Data
@Builder
public class RequestBody implements Serializable {
private String methodName;
Class<?>[] parameterTypes;
Object[] args;
/**
* 具体的接口类型
*/
Class inter;
}
RequestBody中我们定义了需要调用的方法名称,入参信息
Response定义如下:
@Builder
@Data
public class Response {
private ResponseHeader header;
private ResponseBody body;
}
@Data
@Builder
public class ResponseHeader {
private long flag;
private long requestID;
private long dataLength;
}
@Data
@Builder
public class ResponseBody {
private String desc;
}
body中的描述,既可能是车辆信息的描述,也可能是颜色信息的描述。
至此,我们的通信协议已经有了,Service端的业务实现也已经有了,下面我们来定义下netty服务:
public class Service {
public void start() {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(1);
ChannelFuture bind = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
System.out.println("server accept request , client port : " + nioSocketChannel.remoteAddress().getPort());
ChannelPipeline pipeline = nioSocketChannel.pipeline();
pipeline.addLast(new ByteDecoder());
pipeline.addLast(new ServerRequestHandler());
}
}).bind(new InetSocketAddress("127.0.0.1", 9090));
try {
bind.sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们定义出两个NioEventLoopGroup,boss用于监听客户端连接,worker用于业务处理,关注的channel为NioServerSocketChannel,同时加入两个handler:ByteDecoder用于字节解析,ServerRequestHandler用于处理解析出来的request。
/**
* @author ralap
* @date 2021-02-07 12:29
* 定义解码器
*/
public class ByteDecoder extends ByteToMessageDecoder {
/**
* 从byteBuf中将解析出来的对象放入list中
*
* @param channelHandlerContext
* @param buf
* @param list
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf buf, List<Object> list) throws Exception {
// 根据通信协议,从buf中解析出数据,真实情况应该是按照buf的字节去读取数据,这边为了方便,直接反序列了
if (buf != null && buf.readableBytes() >= 4) {
String message = buf.toString(CharsetUtil.UTF_8);
// 将buf 的 readindex索引随读取的数据向前移动
buf.skipBytes(buf.readableBytes());
// 解析出当前是request,将字节解码出request信息
if (message.contains(String.valueOf(HeaderCode.REQUEST.getCode()))) {
Request request = JsonUtil.deserialize(message, Request.class);
if (request != null) {
list.add(request);
}
}
// 解析出当前是response,将字节解码出response信息
if (message.contains(String.valueOf(HeaderCode.RESPONSE.getCode()))) {
Response response = JsonUtil.deserialize(message, Response.class);
if (response != null) {
list.add(response);
}
}
}
}
}
无论是server端的从buf中解析出Reqeust,还是client端从buf中解析数Response,都通过ByteDecoder进行字节码解析。
/**
* @author zji-Ralap
* @date 2021/2/6 16:30
*/
public class ServerRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (msg == null) {
return;
}
Request request = (Request) msg;
System.out.println("服务端收到请求数据 : " + JsonUtil.serialize(request));
// 使用netty的eventLoop来处理业务和返回数据,也可以使用自己定义的线程池
ctx.executor().execute(() -> {
// 将受到的Request请求,分发到各自的实现类中进行业务处理
String desc = new Dispatcher().dispatcherRequest(request);
Response response = Response.builder()
.header(ResponseHeader.builder()
.flag(HeaderCode.RESPONSE.getCode())
.requestID(request.getHeader().getRequestID())
.build())
.body(ResponseBody.builder()
.desc(desc)
.build())
.build();
byte[] responseBytes = JsonUtil.serialize(response).getBytes();
ByteBuf responseBuf = PooledByteBufAllocator.DEFAULT.directBuffer(responseBytes.length);
responseBuf.writeBytes(responseBytes);
try {
ctx.writeAndFlush(responseBuf).sync();
} catch (InterruptedException e) {
// e.printStackTrace();
}
});
} catch (Exception ex) {
}
}
}
ServerRequestHandler,在解码器成功解析出Request之后,对request进行处理。这边使用Dispatcher对请求进行分发,如果是car的则分发到car的实现类carImpl,如果是color的请求,则分发到colorImpl实现类中进行实际业务处理。
public class Dispatcher {
private List<Object> implList;
public Dispatcher() {
Car carImpl = new CarImpl();
Color colorImpl = new ColorImpl();
// 将实现类注册进去
implList = new ArrayList<>();
implList.add(carImpl);
implList.add(colorImpl);
}
/**
* 将请求分发到实际业务中
*
* @param request
* @return
*/
public String dispatcherRequest(Request request) {
for (Object obj : implList) {
if (request.getBody().getInter().isInstance(obj)) {
Method method = getMethod(request.getBody().getMethodName(), obj.getClass(),
request.getBody().getParameterTypes());
if (method != null) {
try {
return (String) method.invoke(obj, request.getBody().getArgs());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
return "";
}
public static Method getMethod(String methodName, Class testClass, Class<?>... parameterTypes) {
try {
Method method = testClass.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
} catch (Exception ex) {
return null;
}
}
}
现在我们已经处理完了server端的解析和具体业务处理,下面我们需要创建出一个连接,用于client和server建立通信:
public class ConnectFactory {
private static ConnectFactory instance;
private ConcurrentHashMap<InetSocketAddress, ConnectPool> map;
private ConnectFactory() {
map = new ConcurrentHashMap<>();
}
private int pollCount = 1;
public static ConnectFactory getInstance() {
if (instance == null) {
synchronized (ConnectFactory.class) {
if (instance == null) {
instance = new ConnectFactory();
}
}
}
return instance;
}
/**
* 根据当前address获取channel
*
* @param address
* @return
*/
public NioSocketChannel getSocketChannel(InetSocketAddress address) throws InterruptedException {
ConnectPool pool = map.getOrDefault(address, null);
if (pool == null) {
// 根据当前的address创建一个连接线程池,此处我们假设线程池中仅有一个连接
pool = new ConnectPool(pollCount);
map.putIfAbsent(address, pool);
}
// 从线程池中取出一个连接
int index = new Random().nextInt(pollCount);
Connect connect = pool.getConnects().get(index);
if (connect.getChannel() != null && connect.getChannel().isActive()) {
// 连接是可以直接使用的
return connect.getChannel();
}
// 连接不可以直接使用
synchronized (connect.getLock()) {
// 创建出一个可用的连接
connect.setChannel(createChannel(address));
return connect.getChannel();
}
}
private NioSocketChannel createChannel(InetSocketAddress address) throws InterruptedException {
NioEventLoopGroup workGroup = new NioEventLoopGroup(1);
ChannelFuture connect = new Bootstrap()
.group(workGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
ChannelPipeline pipeline = nioSocketChannel.pipeline();
pipeline.addLast(new ByteDecoder());
pipeline.addLast(new ClientResponseHandler());
}
}).connect(address);
// 保证连接建立完成之后再返回
return (NioSocketChannel) connect.sync().channel();
}
}
@Data
public class ConnectPool {
private List<Connect> connects;
public ConnectPool(int poolSize) {
connects = new ArrayList<>(poolSize);
for (int index = 0; index < poolSize; index++) {
Connect connect = Connect.builder()
.channel(null)
.lock(new Object())
.build();
connects.add(connect);
}
}
}
我们通过ConnectFactory从连接池中随机找出一条可用的channel进行通信。
最后便是我们的client端实现,通过动态代理的方式实现car和color的统一调用:
public class MyProxy {
/**
* 获取接口的动态代理
*
* @param classz
* @param <T>
* @return
*/
public <T> T getProxy(Class<T> classz) {
ClassLoader loader = classz.getClassLoader();
Class<?>[] methodInfo = {classz};
return (T) Proxy.newProxyInstance(loader, methodInfo, (proxy, method, args) -> {
Request request = createRequest(method, args, classz);
byte[] requestBytes = JsonUtil.serialize(request).getBytes(CharsetUtil.UTF_8);
// 从连接池中获取一个连接
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9090);
NioSocketChannel socketChannel = ConnectFactory.getInstance().getSocketChannel(address);
// 将数据发送出去
ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(requestBytes.length);
byteBuf.writeBytes(requestBytes);
ChannelFuture channelFuture = socketChannel.writeAndFlush(byteBuf);
channelFuture.sync();
// 因为是同步调用,睡1s,模拟等待netty服务端处理数据
Thread.sleep(1000);
Response response = (Response) ResponseProcess.get(request.getHeader().getRequestID());
return response.getBody().getDesc();
});
}
private Request createRequest(Method method, Object[] args, Class classz) {
// 构建请求的主题
RequestBody body = RequestBody.builder()
.args(args)
.methodName(method.getName())
.parameterTypes(method.getParameterTypes())
.inter(classz)
.build();
// 构建请求head
RequestHeader header = RequestHeader.builder()
.flag(HeaderCode.REQUEST.getCode())
.dataLength(JsonUtil.serialize(body).getBytes().length)
.requestID(UUID.randomUUID().getLeastSignificantBits())
.build();
return Request.builder()
.header(header)
.body(body)
.build();
}
}
public class CarClient {
public void getCarInfo(String carName) {
Car car = new MyProxy().getProxy(Car.class);
String response = car.getCar(carName);
System.out.println("CarClient receive :" + JsonUtil.serialize(response));
}
}
public class ColorClient {
public void getColor(String color) {
Color car = new MyProxy().getProxy(Color.class);
String response = car.getColor(color);
System.out.println("ColorClient receive :" + JsonUtil.serialize(response));
}
}
到此处,我们已经写完了全部逻辑。
我们再写一个方法,让功能跑起来试下:
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
Service service = new Service();
service.start();
}).start();
Thread.sleep(100);
new Thread(() -> {
CarClient client = new CarClient();
client.getCarInfo("兰博基尼");
}).start();
new Thread(() -> {
CarClient client = new CarClient();
client.getCarInfo("法拉利");
}).start();
new Thread(() -> {
ColorClient client = new ColorClient();
client.getColor("红色");
}).start();
new Thread(() -> {
ColorClient client = new ColorClient();
client.getColor("紫色");
}).start();
}
}
先启动server端服务,然后发起四次问询,运行结果如下: