对项目进行简单梳理总结
1.使用 Netty(基于 NIO)替代 BIO 实现网络传输;
这块比较简单,其实就是之前写的一个Netty基本实现。 对部分代码进行了重构,优化
2. 使用 Zookeeper 管理相关服务地址信息
使用Zookeeper作为注册中心,写了一个工具类
-
实现了四种序列化算法,Json 方式、Kryo 算法、Hessian 算法与 Google Protobuf 方式(默认采用 Kryo方式序列化);
-
Netty 重用 Channel 避免重复连接服务端
-
使用
CompletableFuture包装接受客户端返回结果(之前的实现是通过AttributeMap绑定到 Channel 上实现的) 详见:使用 CompletableFuture 优化接受服务提供端返回结果 -
增加 Netty 心跳机制 : 保证客户端和服务端的连接不被断掉,避免重连。
-
客户端调用远程服务的时候进行负载均衡,实现了两种负载均衡算法:随机算法与轮转算法 :
-
服务提供侧自动注册服务,使用注解进行服务消费
-
实现自定义的通信协议 可以将原有的
RpcRequest和RpcReuqest对象作为消息体:- 魔数 : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。
- 序列化器编号 :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kyro 等序列化方式。
- 消息体长度 : 运行时计算出来。
整个流程(梳理):
第一步是: 通过传统的方式,服务端只能注册一个服务,用java序列化的方式,客户端生成一个代理对象,把request封装好传递过去,然后服务端这边根据你要调的方法和参数,直接去调用,然后返回调用结果,这里用到了线程池,方法实现了runnable接口 ,线程池参数怎么设置的呢?(I/O密集型,两倍,cpu密集型,核心数就可以了)
**第二部是改造(可以注册多个服务):**可以注册多个服务,把服务名和提供对象的对应关系保存在一个hashmap里面,并且用一个set来保存哪些对象被注册;如果某个对象实现了两个接口,那么两个服务名会对应这个对象
为了降低耦合度,服务端接收到以后,开启线程池去处理,这里用一个RequestHandlerThread包装好,这个类把真正要实现的方法从注册表里面拿出来,然后交给RequestHandler 去真正的处理,通过反射的方法去调用,获得方法名和方法对象,然后调用返回结果(这里反射机制到底是什么样的呢?)
**第三步:改用netty传输:**常规的改造,把服务器端改成netty就可以了,一些配置,编码器和解码器,绑定一个端口,监听;客户端也是一样,基本的配置,然后绑定对应的端口和host地址;channle把request对象写出去,然后非阻塞的去读取结果,AttributeKey ,把结果放到上下文里去,可以得到结果
自定义的协议和编码器: 首先是4字节的魔数,然后一个package type表明这个包是请求还是响应,还有一个序列化type表明用的是哪种序列化协议,最后是实际数据的长度,防止粘包,以及序列化好的数据
CommonEncoder 是自己定义的编码器类,转成byte数组; 读取端通过协议上的序列化编码来选择使用哪种协议解码,还有判断是请求类的包,还是响应类的包。并且读取数据的长度,防止粘包
(这里有一个需要注意的点,就是在 RpcRequest 反序列化时,由于其中有一个字段是 Object 数组,在反序列化时序列化器会根据字段类型进行反序列化,而 Object 就是一个十分模糊的类型,会出现反序列化失败的现象,这时就需要 RpcRequest 中的另一个字段 ParamTypes 来获取到 Object 数组中的每个实例的实际类,辅助反序列化,这就是 handleRequest() 方法的作用。
上面提到的这种情况不会在其他序列化方式中出现,因为其他序列化方式是转换成字节数组,会记录对象的信息,而 JSON 方式本质上只是转换成 JSON 字符串,会丢失对象的类型信息。)
然后NettyServerHandler 用来处理,相当于之前的那个workThread? 读取通道的信息,然后读取对应的服务信息,再去处理,把处理的结果返回
第四步:如何实现Kryo序列化
jason的弊端:如果属性声明为 Object 的,就会造成反序列化出错,通常会把 Object 属性直接反序列化成 String 类型,就需要其他参数辅助序列化。并且,JSON 序列化器是基于字符串(JSON 串)的,占用空间较大且速度较慢。
这里 Kryo 可能存在线程安全问题,文档上是推荐放在 ThreadLocal 里,一个线程一个 Kryo。
第五步:基于Zookeeper的服务器注册于发现
服务端一开始会把自己能提供的服务注册到注册中心上,还有自己的端口和ip地址,然后客户端想要调用什么服务,就去注册中心上面找,找到以后再进行调用即可
然后写两个接口,一个是注册服务,,一个是调用服务
一个是注册在自己的本地注册表上, 一个是注册到注册中心上
客户端通过ServiceRegistry 方法获取到服务端的地址和端口
第六步:自动注销服务和负载均衡策略
为什么要实现自动注销服务?如果启动了服务端后把服务端关闭了,那么客户端请求的时候会调用失败,因此要实现一个功能,当服务端关闭的时候把服务注销; 使用一个钩子来实现,把注销服务的方法写到关闭系统的钩子方法里面去
Runtime 对象是 JVM 虚拟机的运行时环境,调用其 addShutdownHook 方法增加一个钩子函数,创建一个新线程调用 clearRegistry 方法完成注销工作。这个钩子函数会在 JVM 关闭之前被调用。
负载均衡策略:客户端找到服务的所有提供者列表,从里面选择一个,思路也不是很难,在选择的时候,加入一个负载均衡实现就行
第七步:服务端自动注册服务
服务端需要自己手动创建服务对象,并且手动进行注册,如果服务比较多,就会很麻烦,实现自动注册服务
@Service 放在一个类上,标识这个类提供一个服务,@ServiceScan 放在启动的入口类上(main 方法所在的类),标识服务的扫描的包的范围。Service 注解的值定义为该服务的名称,默认值是该类的完整类名,而 ServiceScan 的值定义为扫描范围的根包,默认值为入口类所在的包,扫描时会扫描该包及其子包下所有的类,找到标记有 Service 的类,并注册。
在网上找的一个工具类,用来实现扫描,传入一个包名,来获得下面所有的类,并且放到set里面返回,然后进行扫描
要获得扫描包的范围,先需要获取到 ServiceScan 注解的值,这个注解类是加在启动类上面的,获得启动类 就是通过调用栈,main方法是栈的最底端,获取到main所在的类,然后通过Class 对象的 isAnnotationPresent 方法来判断该类是否有 ServiceScan 注解。如果没有就报错,有的话就读取注解的值;通过startClass.getAnnotation(ServiceScan.class).value(); 获取注解的值。
然后再通过之前的那个工具类去获得所有的class类,判断上面是否有注册,如果有注解,就反射创建该对象,并且进行注册即可;
在 NettyServer 的构造方法最后,调用 scanServices 方法,即可自动注册所有服务
第八步:心跳机制,如何用注解进行服务消费? netty重用channle
心跳机制: 一端去检测,如果服务器很久没有操作,客户端会发送ping包给服务端,服务器端就知道客户端是在线的,然后回复一个pong包给客户端,客户端就知道服务器是在线的(检测网络是否正常和通畅)
发送空闲的时候,通过一个handler去重写方法,然后判断,可以发送一个ping包过去
这里是客户端和服务端都会有一个处理!
服务器端配置,如果6s时间没有收到客户端发送来的消息,就抛出读空闲时间,然后服务器端会发送包去检测 客户端是否在线;
服务器端,也是一样,如果一定时间内没有获得服务器端发送来的消息,就响应的处理;
如果真的掉了,就抛出异常,在捕获异常的方法里面调用重新连接;加个定时器,如果连接失败,一定时间内重连
这里是这样的!! 客户端和服务器端都可以添加心跳机制,当检测到对面很久没有读或者写了,就发送心跳包过去