v1.2
更新事项 以下更新均在非阻塞模块进行更新,阻塞模块可供读者自己尝试
-
进一步减少用户使用的复杂感 之前分辨方法是通过判断传递的字符串、类型中的方法编号字段来抉择的,这次的话,将更加精简,使用注册中心,目前每个服务提供者每人只提供一个服务,其他的日后再完善
-
首先我做的是将zookeeper的地址设置为常量这样,之后每次在创建zookeeper连接中地址参数可以直接拿,同时修改起来也方便,把sessionTimeout也设为常量
package constants; public class RpcConstants { //zookeeper服务器连接地址 public static String ZOOKEEPER_ADDRESS = "zytCentos:2181"; //超时时间 public static int ZOOKEEPER_SESSION_TIMEOUT = 2000; }
-
实现服务提供端将对应地址注册到zookeeper中
package zkService; import constants.RpcConstants; import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import java.io.IOException; import java.nio.charset.StandardCharsets; //该类将对应服务端的方法和相应的端口和地址,注册到zooKeeper中 public class ZkServiceRegistry { private static String connectString = RpcConstants.ZOOKEEPER_ADDRESS; private static int sessionTimeout = RpcConstants.ZOOKEEPER_SESSION_TIMEOUT; private static ZooKeeper zooKeeper; static void createConnect() throws IOException { zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } //创建成功后把方法注册进去 static void register(String RpcServiceName,String hostname,int port) throws InterruptedException, KeeperException { //节点名就是方法名 然后对应的数据就是hostname+":"+port //因为这个地区属于一个临界区 可能会发生线程不安全问题 所以进行上🔒 synchronized (ZkServiceRegistry.class) { Stat exists = zooKeeper.exists("/service", null); if (exists ==null) { zooKeeper.create("/service", "".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT ); } } String date = hostname+":"+port; //权限目前都设置为全放开 创建方式均为持久化 zooKeeper.create("/service/"+RpcServiceName, date.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT ); } /** * * @param RpcServiceName 这是对应的服务名 * @param hostname 和可以调用该服务的ip * @param port 还有对应的端口号 * @throws IOException * @throws InterruptedException */ public static void registerMethod(String RpcServiceName,String hostname,int port) throws IOException, InterruptedException, KeeperException { //先创建对应的额zooKeeper连接客户端再进行相应的注册 createConnect(); register(RpcServiceName,hostname,port); System.out.println("服务端:"+hostname+":"+port+"方法注册完毕"); } }
进行测试
存在问题
测试出现问题 启动两个服务只有一个服务的方法注册进去了;
解决方法
因为我每个服务提供方到后面都是循环着监听 所以第一个进去了就出不来立刻 开了多个线程存在问题
就是会重复进行创建service节点 应该是引入了线程安全的问题 就是单例模式懒汉模式1中类似的问题
解决方法
对方法进行加锁 使用了synchronized锁,该锁jdk1.6之后进行了优化可以使用!成功解决
-
服务提供方启动代码如下(遇到了线程安全问题 之后尝试如何进一步优化)
/* 以nio为网络编程框架的服务提供端启动类 加入了zk 遇到了线程安全问题 成功解决了 */ public class NIOProviderBootStrap12 { public static void main(String[] args) throws IOException, InterruptedException, KeeperException { //启动 new Thread(new Runnable() { @Override public void run() { //因为每个服务提供端内部都是在监听循环阻塞 每个开启一个线程进行监听 try { NIONonBlockingServer12hello.start(6666); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } }).start(); //启动 new Thread(new Runnable() { @Override public void run() { try { NIONonBlockingServer12bye.start(6667); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } }).start(); } }
-
实现消费者端获取所需服务对应的地址
//通过方法名 反过来获取服务对应的地址 public class ZkServiceDiscovery { private static String connectString = RpcConstants.ZOOKEEPER_ADDRESS; private static int sessionTimeout = RpcConstants.ZOOKEEPER_SESSION_TIMEOUT; private static ZooKeeper zooKeeper; //第一步当然是连接到远端服务器上了 public static void getConnect() throws IOException { zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } // 根据所请求的服务地址 获取对应的远端地址 public static String getMethodAddress(String methodName) throws RpcException, InterruptedException, KeeperException { //判断节点中是否存在对应路径 不存在则抛出异常 if (zooKeeper.exists("/service/"+methodName,null)==null) { System.out.println("不存在该方法"); throw new RpcException(); } //到对应节点中获取地址 stat节点状态信息变量 byte[] data = zooKeeper.getData("/service/" + methodName, false,null); String address = new String(data); return address; } public static void getStart(String methodName) throws IOException, RpcException, InterruptedException, KeeperException { //先进行连接 getConnect(); //获取相应的远端地址 String methodAddress = getMethodAddress(methodName); //进行连接 String[] strings = methodAddress.split(":"); //启动 String address = strings[0]; int port = Integer.valueOf(strings[1]); NIONonBlockingClient12.start(address,port); } }
-
-
用代理模式 让用户感觉在调用本地方法一样,调用远端方法。
-
前面的改动尚不能实现,需要加上现在下面这一部分,代理模式,才能更好的实现用户的远端调用
-
设计被代理用户类
//这是之后要被代理的对象 我们会实现它的方法 public interface Customer { String sayBye(String saying); String sayHello(String saying); }
-
设计代理类 //实现了调用后回传的功能 像本身调用一样
package consumer.proxy; import consumer.zkService.ZkServiceDiscovery; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //代理类的实现 public class RpcClientProxy { //获取代理对象 并返回 当前类别 public static Object getBean(final Class<?> serviceClass){ /* 参数详解 1、用哪个类加载器去加载对象 2、动态代理类需要实现的接口 class[]{xxxx.class} 得到的就是对应的类别 3、动态代理类执行方法的时候需要干的事 */ return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{serviceClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //暂时还没有设置回信这个操作 String methodName = method.getName(); String response = ZkServiceDiscovery.getStart(methodName, (String) args[0]); return response; } } ); } }
-
修改了消费者端的方法,和之前的功能不一样了,之前是属于在控制台输入 持续的回复,这次改为一次调用得到回复,像调用自己的方法一样
package consumer.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; //v1.2版本非阻塞nio 真正意义上实现了rpc调用 public class NIONonBlockingClient12 { public static String start(String HostName, int PORT,String msg) throws IOException{ return start0(HostName,PORT,msg); } //真正启动在这 private static String start0(String hostName, int port,String msg) throws IOException { //得到一个网络通道 SocketChannel socketChannel = SocketChannel.open(); System.out.println("-----------服务消费方启动-------------"); socketChannel.configureBlocking(false); //建立链接 非阻塞连接 但我们是要等他连接上 if (!socketChannel.connect(new InetSocketAddress(hostName,port))) { while (!socketChannel.finishConnect()); } //创建选择器 进行监听读事件 Selector selector = Selector.open(); socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); //进行发送 发的太快了 来不及收到 socketChannel.write(ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8))); //直接进行监听 while (true) { //捕获异常 监听读事件 try { if (selector.select(1000)==0) { continue; } Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); ByteBuffer buffer = (ByteBuffer)key.attachment(); SocketChannel channel = (SocketChannel)key.channel(); int read = 1; //用这个的原因是怕 多线程出现影响 StringBuffer stringBuffer = new StringBuffer(); while (read!=0) { buffer.clear(); read = channel.read(buffer); stringBuffer.append(new String(buffer.array(),0,read)); } return stringBuffer.toString(); } } catch (IOException e) { e.printStackTrace(); } } } }
-
启动类代码
package consumer.bootstrap; import consumer.proxy.RpcClientProxy; import method.Customer; import java.io.IOException; /* 以nio为网络编程框架的消费者端启动类 */ public class NIOConsumerBootStrap12 { public static void main(String[] args) throws IOException { RpcClientProxy clientProxy = new RpcClientProxy(); Customer customer = (Customer) clientProxy.getBean(Customer.class); String response = customer.hello("success"); System.out.println(response); System.out.println(customer.bye("fail")); } }
-
实现效果
-