这里创建了请求对象和响应对象,来模拟考试作弊的过程。Request是对问题的封装,小明在把问题发给室友时应该告诉室友题目,参数,以及范围,对应的Request应该有参数、解题方法、解题的接口;
小明的室友接收到小明的问题,会根据它发过来的问题的相关信息进行解答,并把答案封装在Response中,Response是对答案的封装,所以Response对应的应有答案属性以及是否会解得信息;
Resquest和Response类可由模块core创建,让consumer和provider都依赖这个模块,从而都可以用到这两个类;
rpc网络传输层采用netty,netty是采用的tcp和udp协议,是对socket封装;所以consumer和provider之间的通讯是采用socket进行的。
domain是放
net是通讯层,用于提供者和消费者之间进行通讯,Provider需要监听消费者发送过来的消息,并向消费者发送消息,所以Provider可以创建一个监听的方法用于监听消费者发出消息;
Povider可以创一个getInputStream方法用于读取消费者发送过来的消息;
Provider可以创建一个getOutputStream方法用于向消费者发送消息;
Provider在等待Consumer发送消息
Consumer可以创建connect方法创建与Provider的连接;
Consumer可以创一个getInputStream方法用于读取Provider发送过来的消息;
Consumer可以创建一个getOutputStream方法用于向Provider发送消息;
创建了以上功能就实现了Consumer和Provider之间的通讯的工具
Provider与Consumer之间的通信功能实现了,那这两者之间就可以进行调用通讯层的工具进行通讯了。
invoke是执行方法的调用,提供者使用LocalInvoker处理问题并返回答案对象,消费者使用RpcInvoker发送问题并接收答案,返回答案对象
public interface MethodInvoke {
/**
* 发送问题获取答案
* @param request
* @return
*/
Response invoker(Request request);
}
小明的室友是解题的一方,由室友提供解题的方法,小明需要用手机发送题目信息给室友,即解题的接口的实现类在提供者处,消费者需要远程调用这个方法,通过代理来获得解题的实现类对象,并调用解题的方法;所以proxy包下的接口很好的解决了这个问题,消费者可以得到了解题的类的代理对象,当调用解题方法是,就会走进代理的方法中去执行这个方法,返回的值是Consumer的执行方法的返回值,这个返回值是通过socket从提供者一端发送而来的,从而实现了远程调用解决问题的效果;
public interface ObjectPoxy {
/**
* 获取代理对象
*/
<T> T getProxyObject(Class<T> clazz);
}
/**
* 消费者需要调用代理
* @author Administrator
*
*/
public class JdkProxy implements ObjectPoxy{
private MethodInvoke methodInvoke = new RpcInvoker();
@SuppressWarnings("unchecked")
@Override
public <T> T getProxyObject(Class<T> clazz) {
return (T) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(), new Class<?>[] {clazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Request request = new Request();
request.setInterfaceName(clazz.getName());
request.setMethodName(method.getName());
request.setArgs(args);
return methodInvoke.invoker(request);
}
});
}
}
rpc 继续升级,引入负载均衡
负载均衡出现的问题
室友宕机的问题
如果室友挂了,就不能访问该室友了,就应该从列表中移除该室友,如果有新室友加入解决问题的队列中,那就应该加入到列表中。
创建一个注册中心,每当一个室友加入答题的行列则往注册中心注册一个服务,每当一个室友宕机了,这个服务会自动删除,在要用到zookeeper来做注册中心
zookeeper有四种节点,
PERSISTENT-持久化目录节点 客户端与zookeeper断开连接后,该节点依旧存在
PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
EPHEMERAL-临时目录节点 客户端与zookeeper断开连接后,该节点被删除
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点 客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
很明显加入一个室友或删除一个室友,注册中心的服务会自动添加或者删除,和临时节点的性质一模一样,所以添加室友的本质是在注册中心创建一个临时节点。
定义注册的接口,室友通过这个接口来加入答题的队列,即提供者在注册中心注册一个服务;
public interface ServerRegistry {
void Register(String serverName, String serverPath);
}
定义发现服务的接口,小明可以通过这个接口找到室友的ip,即消费者可以通过这个接口找到服务获取提供者的ip
public interface ServerDiscovery {
List<String> discovery(String serverName);
}
注册接口的实现类
public class ServerRegistryImpl implements ServerRegistry{
private static final String ZKADDRESS = "192.168.142.142:2181";
private static ZkClient zkClient = null;
static {
zkClient = new ZkClient(ZKADDRESS, 5000, 10000);
}
/**
* 注册服务,本质是在zookeeper中创建节点
*/
@Override
public void Register(String serverName, String serverPath) {
// zkClient.createPersistent("/family");
// zkClient.delete("/family");
// zkClient.deleteRecursive("/family"); 递归删除
if(!zkClient.exists("/" + serverPath)) {
zkClient.createPersistent("/" + serverPath);
}
zkClient.createEphemeral("/" + serverPath + "/" + serverName);
System.out.println(serverName + "注册成功");
}
}
发现接口的实现类
public class ServerDiscoverImpl implements ServerDiscovery {
private static final String ZKADDRESS = "192.168.142.142:2181";
private static ZkClient zkClient = null;
static {
zkClient = new ZkClient(ZKADDRESS, 5000, 10000);
}
@Override
public List<String> discovery(String serverPath) {
if(!zkClient.exists("/" + serverPath)) {
throw new RuntimeException("服务不存在");
}
List<String> children = zkClient.getChildren("/" + serverPath);
return children;
}
}
以上代码实现了注册服务和服务宕机后自动删除的功能,但存在一个缺陷,每次消费者订阅服务时都要访问一次注册中心,如果服务没有太大的变化,则会造成重复的访问,影响性能。因此,在发现的实现类中添加一个缓存的功能;消费者每次执行发现服务操作时,会先去缓存中找,如果缓存中没有则再访问注册中心;添加缓存就要注意缓存的脏读问题,当注册中心的服务发生了变化,需要及时更改缓存中的数据,解决缓存脏读的问题。
public class ServerDiscoverImpl implements ServerDiscovery {
private static Map<String,List<String>> caches = new HashMap<>();
private static final String ZKADDRESS = "192.168.142.142:2181";
private static ZkClient zkClient = null;
static {
zkClient = new ZkClient(ZKADDRESS, 5000, 10000);
}
@Override
public List<String> discovery(String serverPath) {
// 如果缓存中有则从缓存中取,否则从注册中心中取
if(caches.containsKey("/" + serverPath)) {
return caches.get("/" + serverPath);
}
if(!zkClient.exists("/" + serverPath)) {
throw new RuntimeException("服务不存在");
}
List<String> children = zkClient.getChildren("/" + serverPath);
// 解决缓存的脏读问题
zkClient.subscribeChildChanges("/" + serverPath, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
// parentPath 是父节点的路径
// currentChilds是子节点的路径
caches.put(parentPath, currentChilds);
System.out.println("通过订阅,已解决脏读问题");
}
} );
System.out.println(serverPath + "订阅" + serverPath + "服务成功");
return children;
}
}
使用zk 做注册中心有什么亮点?
自带的注册服务和下线服务
服务启动,使用zk 去注册服务
服务消费,使用zk 去发现服务