基于Socket实现PRC通信

前言

写这篇博客的目的主要回顾一下学过的知识,避免遗忘,同时希望伙伴一起来指正一些不足,共同学习。文章是基于Socket来实现一个基本的RPC通信框架,并且实现版本控制。功能不会太复杂,主要是为了疏通思路脉路。

背景环境

在分布式中,我们经常会用到dubbo+zookeeper的框架来实现,由于本篇博客并没有对zookeeper的实现,所以我们将由RPC-Server来统一对API进行管理,还请见谅!

一、RPC-Server创建

在这里插入图片描述

1.1 rpc-server-api
1.1.1基本创建

实体类的创建,并实现get&set方法。

//实体类
public class User {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试接口:
这里随便定义两个方法,就不解释方法了,看名字也能猜出来。

public interface IHelloService {

    String sayHello(String content);
    /**
     * 保存用户
     * @param user
     * @return
     */
    String saveUser(User user);
}

RpcRequest:

关于Request对象的解释:

Request对象,又称为请求对象,该对象派生自HTTPResponse类,是ASP中重要的服务器内置对象,它连接着Web服务器和Web客户端程序。该对象用来获取客户端在请求一个页面或者传送一个Form时提供的所有信息,包括能够标识浏览器和用户的HTTP变量、存储在客户端Cookie信息以及附在URL后面的值、查询字符串或页面中Form段HTML控件内的值、Cookie、客户端证书、查询字符串等 。如浏览器和用户的变量,客户端表单中的数据、变量或者客户端的cookie信息等,Request对象对应的类是System、Web、HttpRequest类。

我们这里也简单存储一下请求的基本信息:

public class RpcRequest implements Serializable {
    //类名
    private String className;
    //方法名
    private String methodName;
    //参数
    private Object[] parameters;
    //版本号
    private String version;

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}
1.1.2 打jar包

到此处,api模块的编写就简单的完成了。我们将此模块进行打包,然后添加到prc-server-provider中。
在这里插入图片描述

1.2 rpc-server-provider

在这里我们需要对接口的注册和接收服务端的请求处理,

1.2.1 依赖
		<!-- rpc-server-api 的maven地址-->
        <dependency>
            <groupId>com.ccc</groupId>
            <artifactId>rpc-server-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.13.RELEASE</version>
        </dependency>
1.2.2 定义RpcService注解
@Target(ElementType.TYPE) //修饰范围 //类或接口
@Retention(RetentionPolicy.RUNTIME)
@Component //被spring进行扫描
public @interface RpcService {
    
    Class<?> value(); //拿到服务的接口

    /**
     * 版本号
     */
    String version() default "";
}
1.2.3 对api接口实现

由于我们对版本进行了控制,所以此处简单写两个实现类并标注上述声明的注解

@RpcService(value = IHelloService.class,version = "v1.0")
public class HelloServiceImpl implements IHelloService{


    @Override
    public String sayHello(String content) {
        System.out.println("[v1.0] request in :"+content);
        return "[v1.0]say Hello:"+content;
    }

    @Override
    public String saveUser(User user) {
        System.out.println("request in saveUser :" +user);
        return "[v1.0]SUCCESS";
    }
}
@RpcService(value = IHelloService.class,version = "v2.0")
public class HelloServiceImpl2 implements IHelloService{


    @Override
    public String sayHello(String content) {
        System.out.println("[v2.0] request in :"+content);
        return "[v2.0]say Hello:"+content;
    }

    @Override
    public String saveUser(User user) {
        System.out.println("request in saveUser :" +user);
        return "[v2.0]SUCCESS";
    }
}
1.2.4 PrcServer编写

创建MyRpcServer类,实现ApplicationContextAware,InitializingBean这两个接口,当然这并不一定是必须需要实现这两个接口,主要能实现功能就行。
ApplicationContextAware:加载Spring配置文件时,如果Spring配置文件中所定义或者注解自动注入的Bean类实现了ApplicationContextAware 接口,那么在加载Spring配置文件时,会自动调用ApplicationContextAware 接口中的setApplicationContext方法。
InitializingBean:InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
另外实现的通信是基于Socket套接字来实现的,我们这里使用一个缓存线程池来处理每一次接收到的请求,另外请求的业务逻辑我们也交由ProcessorHandler线程来处理。

public class MyRpcServer implements ApplicationContextAware, InitializingBean {
     //缓存线程池
    ExecutorService executorService = Executors.newCachedThreadPool();
    //存放注解的容器
    private Map<String,Object> handlerMap = new HashMap<>();
    //端口号
    private int port;

    public MyRpcServer(int port){
        this.port = port;
    }

    /**
     * InitializingBean接口方法:
     * 初始化客户端。
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        ServerSocket serverSocket = null;
        try {
        //创建对象并设置端口
            serverSocket = new ServerSocket(port);
            while (true){
                Socket socket = serverSocket.accept(); //此处会阻塞
                //每一个socket交给一个processorHandler处理
                executorService.execute(new ProcessorHandler(handlerMap,socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket !=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * ApplicationContextAware接口方法
     * 将API进行初始化并存放于容器中
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    //获取指定注解
        Map<String,Object> serviceMap = applicationContext.getBeansWithAnnotation(RpcService.class);
        if(! serviceMap.isEmpty()){
        //遍历注解的value
            for(Object serviceBean : serviceMap.values()){
                //拿到注解
                RpcService rpcService = serviceBean.getClass().getAnnotation((RpcService.class));
                //获取name
                String serviceName = rpcService.value().getName();
                //获取版本号
                String version = rpcService.version();
                //添加版本号
                if(!StringUtils.isEmpty(version)){
                /**
                *	将serviceName拼接版本号,在此控制了版本号后,
                *	如果在注册API时候添加了版本号,那么客户端调用接口的时候,就必须传递版本号信息
                *	否则无法进行调用。
                */
                    serviceName += "-"+version;
                }
                //将name作为key class对象作为value存放
                handlerMap.put(serviceName,serviceBean);

            }
        }
    }
}
1.2.5 ProcessorHandler

此类实现了Runnable接口,接收的每个请求都单独用一个线程来处理,该类中主要是从socket中读取客户端发送过来的信息处理然后调用指定的方法。

public class ProcessorHandler implements Runnable {

    private Socket socket;

    private Map<String, Object> handlerMap;

    /**
     * 构造器
     * @param handlerMap 获取注解信息
     * @param socket 获取请求信息
     */
    public ProcessorHandler(Map<String, Object> handlerMap, Socket socket) {
        this.socket = socket;
        this.handlerMap = handlerMap;
    }

    @Override
    public void run() {
        ObjectInputStream objectInputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            // ------------------- InputStream ------------------
            //拿到客户端信息 输入流
            objectInputStream = new ObjectInputStream(socket.getInputStream());
            /**
             * 进行反序列化,将客户端发送的Request信息读取出来。
             * 包括请求哪个类,方法名称,参数
             */
            RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
            //将请求进行处理
            Object result = invoke(rpcRequest);

            //-------------------- OutputStream --------------------
            //可以用于发送广播消息。目前没有使用
            objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(result);
            objectOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Object invoke(RpcRequest rpcRequest) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //得到类名
        String serviceName = rpcRequest.getClassName();
        //获取版本号
        String version = rpcRequest.getVersion();
        //如果版本号不为空,那么将请求数据按照规定拼接
        if (!StringUtils.isEmpty(version)) {
            serviceName += "-" + version;
        }
        System.out.println(serviceName);
        System.out.println("map:"+handlerMap);
        //反射调用
        //根据key获取类对象
        Object service = handlerMap.get(serviceName);

        //根据RpcRequest请求中的serviceName 如果没有找到 抛出异常。
        if (service == null) {
            throw new RuntimeException("service not found:" + serviceName);
        }
        //获取请求参数数组
        Object[] args = rpcRequest.getParameters(); //获得请求参数
        Method method = null;
        if (args != null) { //如果有参数 对参数进行处理,如果没有进行调用加载方法。
            //遍历获得每个参数的类型
            Class<?>[] types = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                //得到参数类型
                types[i] = args[i].getClass();
            }
            //根据请求的类去加载 //HelloServiceImpl
            Class clazz = Class.forName(rpcRequest.getClassName());
           method = clazz.getMethod(rpcRequest.getMethodName(), types);// sayHello saveUser 找到类中的方法
        }else {
            //根据请求的类去加载 //HelloServiceImpl
            Class clazz = Class.forName(rpcRequest.getClassName());
            // sayHello saveUser 找到类中的方法
            method = clazz.getMethod(rpcRequest.getMethodName());
        }

        //反射调用
        return method.invoke(service, args);
    }
}
1.2.6 注入

将代码交由Spring来管理。

@Configuration
@ComponentScan(basePackages = "com.ccc")
public class SpringConfig {

    @Bean(name = "MyRpcServer")
    public MyRpcServer MyRpcServer(){
        return new MyRpcServer(9527);
    }

}
1.2.7 启动类
public class App {
    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        ((AnnotationConfigApplicationContext) context).start();

    }
}
1.2.8 打包

将rpc-server打jar包,方法和上图一样。 到这里简单的rpcserver也就完成了。下面写个prc-client

二、RPC-Client 创建

创建个maven项目就行

2.1导入依赖

将RPC-Server的依赖导入进来

	<dependency>
      <groupId>com.ccc</groupId>
      <artifactId>rpc-server-api</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
2.2 与Server进行连接
public class RpcNetTransport {
    private String host;
    private int prot;

    public RpcNetTransport(String host, int prot) {
        this.host = host;
        this.prot = prot;
    }

    public Object send(RpcRequest request) {
        Socket socket = null;
        Object result = null;
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream inputStream = null;

        try {
            socket = new Socket(host, prot); //建立连接
            /**
             * 将客户的端的request信息进行输出到Server端
             */
            objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(request); //序列化
            objectOutputStream.flush();

            // ----------------- InputStream ---------------
            //接受服务端发来的广播消息,暂时没用上。
            inputStream = new ObjectInputStream(socket.getInputStream());

            result = inputStream.readObject();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }
}
2.3 java动态代理来进行server之间交互。
2.3.1 RemoteInvocationHandler

在这里我使用java的动态代理来实现创建一个RemoteInvocationHandler实现InvocationHandler,并实现invoke方法
InvocationHandler:是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法,该方法有三个参数:
proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
method:我们所要调用某个对象真实的方法的Method对象
args:指代代理对象方法传递的参数

public class RemoteInvocationHandler implements InvocationHandler {

    private String host;
    private int prot;

    public RemoteInvocationHandler(String host, int prot) {
        this.host = host;
        this.prot = prot;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //打桩
        System.out.println("come in");
        RpcRequest rpcRequest = new RpcRequest();
        // --------------- 开始 设置 RpcRequest 请求参数 ------------------
        //设置类名
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        //设置方法名
        rpcRequest.setMethodName(method.getName());
        //参数
        rpcRequest.setParameters(args);
        //版本号
        rpcRequest.setVersion("v1.0");
        // --------------- 结束 设置 RpcRequest 请求参数 ------------------

        //远程通信,将rpcRequest进行输出。
        RpcNetTransport netTransport = new RpcNetTransport(host,prot);
        Object result = netTransport.send(rpcRequest);
        //返回结果
        return result;
    }
}
2.3.2 RpcClient

完成了上述操作,现在我就只需要对接口进行代理处理来实现RPC通信了。

public class RpcProxyClient {

    public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
        /**
        *	使用java自带的代理类 将接口进行代理来进行 RPC远程通信,该方法会自动去调用				 
        *	InvocationHandler 中的invok方法
        */
       return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(),
                new Class<?>[]{interfaceCls},new RemoteInvocationHandler(host,port));
    }
}
2.3.3 启动类

我们只需要写个main方法来启动就行

public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
        RpcProxyClient rpcProxyClient = new RpcProxyClient();
        //传入需要调用的接口,主机地址,端口号
        IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost",9527);
        //接口调用
        String result = iHelloService.sayHello("Ccc");
        //输出返回结果
        System.out.println(result);
    }
}
三、测试。

启动Rpc-Server…没有报错
在这里插入图片描述
启动Rpc-Client…
我们查看一下server接收到的参数
发现客户端的标识是 Ccc
在这里插入图片描述
客户端接收的返回结果

在这里插入图片描述
接口调用成功。。

我们测试一下 不添加版本号调用:
在RemoteInvocationHandler 中取出对version的设置。。

客户端报错:
原因很简单,因为我们在Server端对接口的的注解@RpcService中进行了版本控制所以serverName应该是serverName-version的格式,而且我们是通过serverName作为key来获取类对象的。而在客户端不传递的version版本的时候,我们默认是使用serverName的,所以服务端通过这个key就没法找到对应的value。
在这里插入图片描述
我们在修改version为v2.0
在这里插入图片描述
version2.0 输出成功。

到此,简单的通信功能也就完成,如果您发现有啥错误的地方,恳请指出批评。在此感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈橙橙丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值