网络编程3

网络通信基本常识

  • 编程中的Socket是什么?

    • Socket是应用层与TCP/IP协议族通信的间软件抽象层,它是一组接口,其实就是一个门面模式。TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)

    • socket是一种门面模式,屏蔽了与机器的交流过程,方便业务程序员开发代码

    • A的socket ---- B的socket

      两个socket建立关系,从而A和B就可以通信了

  • 短连接

    • 一次通讯完了马上就关闭连接

    • 服务器要服务成千上亿的服务

    • 短连接和长连接到底如何选择,需要根据实际情况

  • 长连接

    • 一次连接内会进行多次通讯

    • 维持住长连接主要消耗的是内存和FD文件描述符

    • 如果服务器性能好,可以维持上百万的长连接

    • 操作频繁、或者点对点的通讯上,连接池来维护这种长连接

  • InetAddress类 --> 只表示地址(主机)

    • InetAddress address = InetAddress.getByName(“www.baidu.com”)

    • 可以获得百度的ip地址

    • 怎么获得的?-- dns解析

    • InetAddress address2 = InetAddress.getByName(“124.232.170.22”)

      System.out.println(address2.getHostName())

      同样可以根据ip地址获取域名,如果找不到,就会打印源ip地址

    • InetAddress[] allIp= InetAddress.getAllByName(“www.baidu.com”)

      获取百度域名下面所有的ip地址

    • 把一个ip地址包装成InetAddress对象

      byte[] bytes = {(byte)192,(byte)168,56,1};

      InetAddress address = InetAddress.getByAddress(bytes);

    InetSocketAddress类 --> 主机名+端口

    NetoworkInterface类

    • 打开设备管理器-网络适配器

      NetoworkInterface会把这些全部找出来,除了这些,还有别的

      // 127.0.0.1-------本地回环接口

      InetAddress address = InetAddress.getByName(“127.0.0.1”);

      NetworkInterface byInetAddress = NetoworkInterface.getByInetAddress(address);

      // 获取所有的网络通讯接口类

      // 包括本地回环、网络适配器、wifi、广域网、硬件设备通讯

      NetoworkInterface.getNetoworkInterfaces();

  • 服务端、客户端、通信编程关注的三件事

    • 所有网络通讯一定有服务端和客户端这两样

    • 提供服务的称为服务端

    • 连接服务的称为客户端

    • 某个类有Server、ServerSocket,那么这个类往往是给服务端使用的

      ServerSocket只是个容器,容纳网络服务用的

    • 某个类只有socket,一般是负责具体的网络读写

      真实网络通讯中,真正进行网络读写的都是socket

  • 网络编程中一定要关注的点

    • 网络连接
    • 读网络数据
    • 写网络数据
  • jdk网络通讯

    • bio

    • nio

    • aio—非主流

原生JDK网络编程-BIO

  • BIO:block ,即阻塞式io

  • ServerSocket负责绑定IP地址,启动监听端口;

  • Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信

  • ServerSocket启动了处于监听状态
    
    通过accept()方法来响应请求的socket()
    
    新生成一个线程,在线程中生成一个socket()来与请求的socket()通讯
    

服务器端

  • package WangLuoBianCheng.wangLuoBianCheng3;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server{
    
        public static void main(String[] args)throws IOException {
    
            ServerSocket server = new ServerSocket();
            server.bind(new InetSocketAddress(10001));
            System.out.println("Server is started...");
            while(true){
                // 来一个socket请求,就会new一个线程,同时new 一个socket
                // 执行server.accept()成功,如果三次握手完成会返回一个Socket
                // 更优的写法是用线程池,实现复用
                new Thread(new ServerTask(server.accept())).start();
    
            }
    
        }
    
    
        private static class ServerTask implements Runnable{
    
            private Socket socket = null;
    
            public ServerTask(Socket socket) {
                this.socket = socket;
            }
    
            @Override
            public void run(){
                try (ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                     ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream())){
    
                    String userName = input.readUTF();
                    System.out.println("Accept client Message:"+userName);
    
                    output.writeUTF("Hello,"+userName);
                    // 上一句只是写入缓存,准备发送
                    // 强制刷出
                    output.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    
    }
    
    

客户端

  • package WangLuoBianCheng.wangLuoBianCheng3;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    
    public class Client {
    
        public static void main(String[] args) throws IOException {
            // 用来通讯的socket
            Socket socket = null;
    
            // 注意客户端必须是输出在上,输入在下,确保流通道建立起来
            // 因为如果两端都是输入,怎么传递?
            // 或者两端都是先创建输出流,再创建输入流,未测试?
            ObjectOutputStream objectOutputStream = null;
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 10001);
            ObjectInputStream objectInputStream = null;
    
            try {
                socket = new Socket();
                // 建立连接
                socket.connect(inetSocketAddress);
    
                objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectInputStream = new ObjectInputStream(socket.getInputStream());
    
                // 发出消息
                objectOutputStream.writeUTF("James");
                objectOutputStream.flush();
    
                // 接受服务器响应的信息并打印
                // 执行完后关闭了网络连接
                System.out.println(objectInputStream.readUTF());
    
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (socket!=null) socket.close();
    
                if (objectOutputStream!=null) objectOutputStream.close();
    
                if (objectInputStream!=null) objectInputStream.close();
            }
        }
    }
    
    

改进—伪异步IO模型

  • 上述服务器端是来一个socket请求,就会new一个线程,同时new 一个socket

  • 为了实现线程复用,应该采用线程池的线程来处理任务,而这种模式又称为伪异步IO模型

  • package WangLuoBianCheng.wangLuoBianCheng3;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ServerPool {
    
        private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2);
    
    
        public static void main(String[] args)throws IOException {
    
            ServerSocket server = new ServerSocket();
            server.bind(new InetSocketAddress(10001));
            System.out.println("Server is started...");
            while(true){
                // 执行server.accept()成功,如果三次握手完成会返回一个Socket
                executorService.execute(new ServerTask(server.accept()));
            }
    
        }
    
    
        private static class ServerTask implements Runnable{
    
            private Socket socket = null;
    
            public ServerTask(Socket socket) {
                this.socket = socket;
            }
    
            @Override
            public void run(){
                try (ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                     ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream())){
    
                    String userName = input.readUTF();
                    System.out.println("Accept client Message:"+userName);
    
                    output.writeUTF("Hello,"+userName);
                    // 强制刷出
                    output.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    }
    
    

BIO应用-RPC框架

  • 单WEB项目

    • 1.订单服务
    • 2.扣减库存服务
    • 3.发送短信提示服务
  • 之前上述三个服务全是单线程执行,当调用服务的用户数很大时,就不能充分发挥并发编程的高效,因此考虑将三个服务拆分,都分别由单独的线程来执行

  • 进一步把不同的服务部署到不同的服务器上,而且每个服务是通过服务器集群的形式来部署,从而形成了分布式的架构

    • 这时不同服务之间的调用就需要引入rpc,因为是跨服务器跨网络的调用,不再是单机内部的方法调用

什么是RPC?

  • RPC(Remote Procedure Call ——远程过程调用),它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络的技术。–来自《百度百科》
客户端存根–server stub
  • 扣减库存服务,为了使调用远端服务和调用本地服务没有什么区别,就需要引入客户端存根

  • 客户端存根就是远程方法在本地的模拟,包括方法名、方法参数

服务端存根–client stub
  • 负责把客户端请求发来的请求体,解析成跟服务器实际方法相同的方法名、方法参数
总结
  • rpc方法就是把上述过程全部包装起来,使得调用远程方法就跟调用本地方法一样

RPC和HTTP

  • TCP/IP概念层模型功能TCP/IP协议族
    文件传输、电子邮件、文件服务、虚拟终端TFTP、HTTP、SNMP、FTP、SMTP、DNS、Telnet
    应用层数据格式化、代码转换、数据加密
    解除或建立与别的接点的联系
    传输层提供端与端的接口TCPUDP
    网络层为数据包选择路由IP、ICMP、RIP、OSPF、BGP、IGMP
    链路层传输有地址的帧以及错误检测功能SLIP、CSLIP、PPP、ARP、RARP、MTU
    以二进制数据形式在物理媒体上传输数据ISO2110、IEEE802、IEEE802.2
rpc究竟在上面协议分层的哪一层?
  • rpc只是一种思想,对不同服务调用的一种描述,既可以通过http实现,也可以通过tcp、htp,所以rpc和http不是一个层级的东西

实现RPC框架需要解决的那些问题?

  • 通信问题

    • 不要每次方法调用,都需要建立socket连接
  • 代理问题

    • 可以不可以通过代理的方式,每次的通信都通过指定的代理来解决

    • 代理问题的解决

      代理模式,用动态代理,为什么不用静态代理?-- 针对每一个服务都要创建相应的代理类,而rpc框架不知道要建立哪些静态代理,所以使用动态代理,不管你要调用什么服务,通过动态代理一把解决

  • 序列化问题

    • 消息在网络中传输是字节的形式,怎么把01字节变成javaBean的形式

    • 序列化问题的解决

      实现Serializable接口,但是jdk的序列化性能很差

      测试结果1-----jdk的字节码长度:133 自己字节码长度:24

      测试结果2-----jdk序列化耗时:1364ms 自己序列化耗时:108ms

  • 服务实例化

    • 方法调用时只有一个方法名和方法参数,怎么转化成调用具体的服务

    • 服务实例化的解决

      通过反射解决

序列化耗时测试

测试1
  • package WangLuoBianCheng.wangLuoBianCheng3.rpc.prepare.serial.protogenesis;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    
    /**
     * @author Mark老师   享学课堂 https://enjoy.ke.qq.com
     * 类说明:测试序列化后字节大小
     */
    public class TestUserInfo {
    
        /**
         * @param args
         * @throws IOException
         */
        public static void main(String[] args) throws IOException {
    		UserInfo info = new UserInfo();
    		info.buildUserID(100).buildUserName("Welcome to Netty");
    		//使用jdk的序列化
    		ByteArrayOutputStream bos = new ByteArrayOutputStream();
    		ObjectOutputStream os = new ObjectOutputStream(bos);
    		os.writeObject(info);
    		os.flush();
    		os.close();
    		byte[] b = bos.toByteArray();
    		System.out.println("The jdk serializable length is : " + b.length);
    		bos.close();
    		//使用自行的序列化
    		System.out.println("-------------------------------------");
    		System.out.println("The byte array serializable length is : "
    			+ info.codeC().length);
        }
    
    }
    
    
测试2
  • package WangLuoBianCheng.wangLuoBianCheng3.rpc.prepare.serial.protogenesis;
    
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    import java.nio.ByteBuffer;
    
    /**
     * @author Mark老师   享学课堂 https://enjoy.ke.qq.com
     * 类说明:测试序列化性能差异
     */
    public class PerformTestUserInfo {
    
        public static void main(String[] args) throws IOException {
            UserInfo info = new UserInfo();
            info.buildUserID(100).buildUserName("Welcome to Netty");
            int loop = 1000000;
    
            //使用jdk的序列化
            ByteArrayOutputStream bos = null;
            ObjectOutputStream os = null;
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < loop; i++) {
                bos = new ByteArrayOutputStream();
                os = new ObjectOutputStream(bos);
                os.writeObject(info);
                os.flush();
                os.close();
                byte[] b = bos.toByteArray();
                bos.close();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("The jdk serializable cost time is  : "
                + (endTime - startTime) + " ms");
    
            //使用自行的序列化
            System.out.println("-------------------------------------");
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            startTime = System.currentTimeMillis();
            for (int i = 0; i < loop; i++) {
                byte[] b = info.codeC(buffer);
            }
            endTime = System.currentTimeMillis();
            System.out.println("The byte array serializable cost time is : "
                + (endTime - startTime) + " ms");
    
            }
    
    }
    
    

实现rpc框架

  • 1.服务端定义接口和服务实现类并且注册服务
  • 2.客户端使用动态代理调用服务(动态代理)
  • 3.客户端代理把调用对象、方法、参数序列化成数据
  • 4.客户端代理与服务端通过Socket通讯传输数据
  • 5.服务端反序列化数据成对象、方法、参数。
  • 6.服务端代理拿到这些对象和参数后通过反射的机制调用服务的实例。

将短信服务拆分成rpc服务—服务端

  • 服务实体类,服务需要的相关参数类型 -----> 这些可以看出服务端存根

所有服务使用的服务框架----RpcServerFrame

  • package cn.enjoyedu.rpc.rpc.base;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Method;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     *@author Mark老师  
     *
     *类说明:rpc框架的服务端部分
     */
    @Service
    public class RpcServerFrame {
    
    //    @Autowired
    //    private RegisterService registerService;
        @Autowired
        private RegisterServiceWithRegCenter registerServiceWithRegCenter;
    
        //服务的端口号
        private int port;
    
        /*处理服务请求任务*/
        private static class ServerTask implements Runnable{
    
            private Socket socket;
            private RegisterServiceWithRegCenter registerServiceWithRegCenter;
    
            public ServerTask(Socket client,
                              RegisterServiceWithRegCenter registerServiceWithRegCenter) {
                this.socket = client;
                this.registerServiceWithRegCenter = registerServiceWithRegCenter;
            }
    		
            // 接受客户端的请求,并调用实际的方法
            // 接受的内容---1.方法所在的类名接口名 2.调用的方法名  3.方法参数及方法属性、具体的参数值
            // 
            @Override
            public void run() {
                try(
                     ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                      ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){
    
                    /*方法所在类名接口名*/
                    // 读字符串inputStream.readUTF()
                    String serviceName = inputStream.readUTF();
                    /*方法的名字*/
                    String methodName = inputStream.readUTF();
                    /*方法的入参类型*/
                    // 读对象inputStream.readObject()
                    // 强制转型,转成类
                    Class<?>[] paramTypes = (Class<?>[]) inputStream.readObject();
                    /*方法的入参的值*/
                    Object[] args = (Object[]) inputStream.readObject();
    
                    /*从容器中拿到服务的Class对象*/
                    Class serviceClass = registerServiceWithRegCenter.getLocalService(serviceName);
                    if(serviceClass == null){
                        throw new ClassNotFoundException(serviceName+ " not found");
                    }
    
                    /*通过反射,执行实际的服务*/
                    // 通过反射获取具体的方法Method对象
                    // 入参包括两个,方法名和参数类型数组
                    Method method = serviceClass.getMethod(methodName, paramTypes);
                    // 通过动态代理调用响应的方法
                    Object result  = method.invoke(serviceClass.newInstance(),args);
    
                    /*将服务的执行结果通知调用者*/
                    // 将调用结果输出给调用者,即客户端
                    outputStream.writeObject(result);
                    outputStream.flush();
    
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public void startService(String serviceName, String host, int port, Class impl) throws Throwable{
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(port));
            System.out.println("RPC server on:"+port+":运行");
           // registerService.regRemote(ServiceName, impl);
            registerServiceWithRegCenter.regRemote(serviceName,host,port,impl);
            try{
                while(true){
                    new Thread(new ServerTask(serverSocket.accept(),
                            registerServiceWithRegCenter)).start();
                }
            }finally {
                serverSocket.close();
            }
        }
    
    }
    
    
    

服务注册中心

  • package cn.enjoyedu.rpc.rpc.base;
    
    import org.springframework.stereotype.Service;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 类说明:注册服务到本地缓存
     */
    @Service
    public class RegisterService {
    
        /*本地可以提供服务的一个容器*/
        private static final Map<String,Class> serviceCache = new ConcurrentHashMap<>();
    
        /*注册本服务*/
        public void regService(String serviceName,Class impl){
            serviceCache.put(serviceName,impl);
        }
    
        /*获取服务*/
        public Class getLocalService(String serviceName){
            return serviceCache.get(serviceName);
        }
    
    
    }
    
    

短信服务启动

SmsRpcServer
  • package cn.enjoyedu.rpc.rpc.sms;
    
    import cn.enjoyedu.rpc.rpc.base.RpcServerFrame;
    import cn.enjoyedu.rpc.remote.SendSms;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import java.util.Random;
    
    /**
     * 类说明:rpc的服务端,提供服务
     */
    @Service
    public class SmsRpcServer {
    
        // rpc的base框架部分注入
        @Autowired
        private RpcServerFrame rpcServerFrame;
    
        @PostConstruct
        public void server() throws Throwable {
            Random r = new Random();
            // 端口号
            int port = 8778+r.nextInt(100);
            // 启动服务
            rpcServerFrame.startService(SendSms.class.getName(),
                    "127.0.0.1",port,SendSmsImpl.class);
    
        }
    
    }
    
    
SendSmsImpl—具体的服务实现类
  • package cn.enjoyedu.rpc.rpc.sms;
    
    
    import cn.enjoyedu.rpc.remote.vo.UserInfo;
    import cn.enjoyedu.rpc.remote.SendSms;
    
    /**
     *@author Mark老师   享学课堂 https://enjoy.ke.qq.com 
     *
     *类说明:短信息发送服务的实现
     */
    public class SendSmsImpl implements SendSms {
    
        @Override
        public boolean sendMail(UserInfo user) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("已发送短信息给:"+user.getName()+"到【"+user.getPhone()+"】");
            return true;
        }
    }
    
    

将短信服务拆分成rpc服务—客户端

  • 类似于服务器端存根,也需要有客户端存根

客户端框架类

  • package cn.enjoyedu.rpc.client.rpc;
    
    import cn.enjoyedu.rpc.remote.vo.RegisterServiceVo;
    import org.springframework.stereotype.Service;
    
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import java.util.Set;
    
    /**
     *@author Mark老师   
     *类说明:rpc框架的客户端代理部分
     */
    @Service
    public class RpcClientFrame {
    
        /*远程服务的代理对象,参数为客户端要调用的的服务*/
        // 传递的应该是方法所在的类名或接口名
        public static<T> T getRemoteProxyObject(final Class<?> serviceInterface) throws Exception {
            /*获得远程服务的一个网络地址*/
            InetSocketAddress addr = //new InetSocketAddress("127.0.0.1",8778);
               getService(serviceInterface.getName());
    
            /*拿到一个代理对象,由这个代理对象通过网络进行实际的服务调用*/
            // 应该产生一个代理对象,和服务器进行通讯
            // 需要实现了InvocationHandler的类DynProxy
            return (T)Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                    new Class<?>[]{serviceInterface},
                    new DynProxy(serviceInterface,addr));
        }
    
    
        /*动态代理,实现对远程服务的访问*/
        // 连接服务器,发送接口名、方法名、方法参数
        private static class DynProxy implements InvocationHandler{
            private Class<?> serviceInterface;
            private InetSocketAddress addr;
    
            public DynProxy(Class<?> serviceInterface, InetSocketAddress addr) {
                this.serviceInterface = serviceInterface;
                this.addr = addr;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                Socket socket = null;
                ObjectInputStream inputStream = null;
                ObjectOutputStream outputStream = null;
                try{
                    
                    // 1.建立网络连接
                    socket = new Socket();
                    socket.connect(addr);
                    outputStream = new ObjectOutputStream(socket.getOutputStream());
    
                    // 2.发送接口名、方法名、参数类型、参数值
                    //方法所在类名接口名
                    outputStream.writeUTF(serviceInterface.getName());
                    //方法的名字
                    outputStream.writeUTF(method.getName());
                    //方法的入参类型
                    outputStream.writeObject(method.getParameterTypes());
                    //方法入参的值
                    outputStream.writeObject(args);
    
                    outputStream.flush();
    
                    // 3.接受服务器传回的结果
                    inputStream = new ObjectInputStream(socket.getInputStream());
                    /*接受服务器的输出*/
                    System.out.println(serviceInterface+" remote exec success!");
                    return inputStream.readObject();
    
                }finally {
                    if(socket!=null) socket.close();
                    if(outputStream!=null) outputStream.close();
                    if(inputStream!=null) inputStream.close();
    
                }
            }
        }
    
    
    
        /*----------------以下和动态获得服务提供者有关------------------------------*/
    
        private static Random r = new Random();
    
        /*获得远程服务的地址*/
        private static InetSocketAddress getService(String serviceName)
                throws Exception {
            //获得服务提供者的地址列表
            List<InetSocketAddress> serviceVoList = getServiceList(serviceName);
            InetSocketAddress addr
                    = serviceVoList.get(r.nextInt(serviceVoList.size()));
            System.out.println("本次选择了服务器:"+addr);
            return addr;
        }
    
        /*获得服务提供者的地址*/
        private static List<InetSocketAddress> getServiceList(String serviceName)
                throws Exception {
            Socket socket = null;
            ObjectOutputStream output = null;
            ObjectInputStream input = null;
    
            try{
                socket = new Socket();
                socket.connect(new InetSocketAddress("127.0.0.1",9999));
    
                output = new ObjectOutputStream(socket.getOutputStream());
                //需要获得服务提供者
                output.writeBoolean(true);
                //告诉注册中心服务名
                output.writeUTF(serviceName);
                output.flush();
    
                input = new ObjectInputStream(socket.getInputStream());
                Set<RegisterServiceVo> result
                        = (Set<RegisterServiceVo>)input.readObject();
                List<InetSocketAddress> services = new ArrayList<>();
                for(RegisterServiceVo serviceVo : result){
                    String host = serviceVo.getHost();//获得服务提供者的IP
                    int port = serviceVo.getPort();//获得服务提供者的端口号
                    InetSocketAddress serviceAddr = new InetSocketAddress(host,port);
                    services.add(serviceAddr);
                }
                System.out.println("获得服务["+serviceName
                        +"]提供者的地址列表["+services+"],准备调用.");
                return services;
            }finally{
                if (socket!=null) socket.close();
                if (output!=null) output.close();
                if (input!=null) input.close();
            }
    
        }
    
    }
    
    

远端的服务怎么通过bean接管

  • package cn.enjoyedu.rpc.client.config;
    
    import cn.enjoyedu.rpc.client.rpc.RpcClientFrame;
    import cn.enjoyedu.rpc.remote.SendSms;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 类说明:
     */
    @Configuration
    public class BeanConfig {
    
        @Autowired
        private RpcClientFrame rpcClientFrame;
    
        // 获取服务端的方法名
        @Bean
        public SendSms getSmsService() throws Exception{
            return rpcClientFrame.getRemoteProxyObject(SendSms.class);
        }
    }
    
    

rpc测试

启动服务端
  • package cn.enjoyedu.rpc;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class RpcServerSmsApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(RpcServerSmsApplication.class, args);
        }
    
    }
    
    
客户端测试方法
  • package cn.enjoyedu.rpc.client;
    
    import cn.enjoyedu.rpc.client.service.NormalBusi;
    import cn.enjoyedu.rpc.remote.SendSms;
    import cn.enjoyedu.rpc.remote.StockService;
    import cn.enjoyedu.rpc.remote.vo.UserInfo;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class RpcClientApplicationTests {
    
        @Autowired
        private NormalBusi normalBusi;
    //    @Autowired
    //    private StockService stockService;
        @Autowired
        private SendSms sendSms;
    
        @Test
        void rpcTest() {
            long start = System.currentTimeMillis();
            normalBusi.business();
    
            /*发送邮件*/
            UserInfo userInfo = new UserInfo("Mark","Mark@qq.com");
            System.out.println("Send mail: "+ sendSms.sendMail(userInfo));
            System.out.println("共耗时:"+(System.currentTimeMillis()-start)+"ms");
    
    
            /*扣减库存*/
    //        stockService.addStock("A001",1000);
    //        stockService.deduceStock("B002",50);
        }
    
    }
    
    

测试总结

  • 现在在客户端只有SendSms这样一个接口,并没有实现类
  • 把代理类作为实现类注入到spring的容器里面
  • 代理类里面,把对方法的调用通过网络转给了远端短信发送服务器上
  • 而远端短信发送服务器上,通过一个map容器保存了改服务器上能提供的服务
  • 在服务器端启动rpc服务时,把具体的实现放到hashmap中,并且启动了一个网络相关的服务,专门接受客户端传递过来的方法调用请求
  • 从而实现了一次rpc的远程方法调用

注册中心

  • dubbo等rpc框架存在一个注册中心,而现在的rpc调用只有一个,如果要实现集群化,如果要提供多个rpc服务,难道客户端调用的时候要写多个服务器地址进去?

  • 注册中心本质也是提供rpc服务,只有两种,提供服务的注册,提供服务的查询

RegisterCenter
  • package cn.enjoyedu.rpc.rpc.reg.service;
    
    import cn.enjoyedu.rpc.remote.vo.RegisterServiceVo;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * @author Mark老师  
     * 类说明:服务注册中心,服务提供者在启动时需要在注册中心登记自己的信息
     */
    @Service
    public class RegisterCenter {
        /*key表示服务名,value代表服务提供者地址的集合*/
        private static final Map<String,Set<RegisterServiceVo>> serviceHolder
                = new HashMap<>();
    
        /*注册服务的端口号*/
        private int port;
    
        /*服务注册,考虑到可能有多个提供者同时注册,进行加锁*/
        private static synchronized void registerService(String serviceName,
                                    String host,int port){
            //获得当前服务的已有地址集合
            Set<RegisterServiceVo> serviceVoSet = serviceHolder.get(serviceName);
            if(serviceVoSet==null){
                //已有地址集合为空,新增集合
                serviceVoSet = new HashSet<>();
                serviceHolder.put(serviceName,serviceVoSet);
            }
            //将新的服务提供者加入集合
            serviceVoSet.add(new RegisterServiceVo(host,port));
            System.out.println("服务已注册["+serviceName+"]," +
                    "地址["+host+"],端口["+port+"]");
        }
    
        /*取出服务提供者*/
        private static Set<RegisterServiceVo> getService(String serviceName){
            return serviceHolder.get(serviceName);
        }
    
        /*处理服务请求的任务,其实无非就是两种服务:
        1、服务注册服务
        2、服务查询服务
        */
        private static class ServerTask implements Runnable{
            private Socket client = null;
    
            public ServerTask(Socket client){
                this.client = client;
            }
    
            public void run() {
    
                try(ObjectInputStream inputStream =
                            new ObjectInputStream(client.getInputStream());
                    ObjectOutputStream outputStream =
                            new ObjectOutputStream(client.getOutputStream())){
    
                    /*检查当前请求是注册服务还是获得服务*/
                    boolean isGetService = inputStream.readBoolean();
                    /*服务查询服务,获得服务提供者*/
                    if(isGetService){
                        String serviceName = inputStream.readUTF();
                        /*取出服务提供者集合*/
                        Set<RegisterServiceVo> result = getService(serviceName);
                        /*返回给客户端*/
                        outputStream.writeObject(result);
                        outputStream.flush();
                        System.out.println("将已注册的服务["+serviceName+"提供给客户端");
                    }
                    /*服务注册服务*/
                    else{
                        /*取得新服务提供方的ip和端口*/
                        String serviceName = inputStream.readUTF();
                        String host = inputStream.readUTF();
                        int port = inputStream.readInt();
                        /*在注册中心保存*/
                        registerService(serviceName,host,port);
                        outputStream.writeBoolean(true);
                        outputStream.flush();
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }finally {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        /*启动注册服务*/
        public void startService() throws IOException {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(port));
            System.out.println("服务注册中心 on:"+port+":运行");
            try{
                while(true){
                    new Thread(new ServerTask(serverSocket.accept())).start();
                }
            }finally {
                serverSocket.close();
            }
        }
    
        // 服务注册中心端口是9999
        @PostConstruct
        public void init() {
            this.port = 9999;
            new Thread(new Runnable() {
                public void run() {
                    try{
                        startService();
                    }catch(IOException e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    
    
测试使用服务注册中心
  • package cn.enjoyedu.rpc;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class RpcRegApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(RpcRegApplication.class, args);
        }
    
    }
    
    
修改响应的服务提供者
  • 之前的服务都是注册在本地,现在除了在本地存一份,现在改成注册到注册中心,在哪个ip地址上提供了哪些服务
RegisterServiceWithRegCenter
  • package cn.enjoyedu.rpc.rpc.base;
    
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 类说明:注册服务,引入了服务的注册和发现机制
     */
    @Service
    public class RegisterServiceWithRegCenter {
    
        /*本地可提供服务的一个名单,用缓存实现*/
        private static final Map<String,Class> serviceCache
                = new ConcurrentHashMap<>();
    
        /*往远程注册服务器注册本服务,同时在本地注册本服务*/
        public void regRemote(String serviceName, String host, int port, Class impl)
                throws Throwable{
            //登记到注册中心
            Socket socket = null;
            ObjectOutputStream output = null;
            ObjectInputStream input = null;
    
            try{
                socket = new Socket();
                socket.connect(new InetSocketAddress("127.0.0.1",9999));
    
                output = new ObjectOutputStream(socket.getOutputStream());
                /*注册服务*/
                output.writeBoolean(false);
                /*提供的服务名*/
                output.writeUTF(serviceName);
                /*服务提供方的IP*/
                output.writeUTF(host);
                /*服务提供方的端口*/
                output.writeInt(port);
                output.flush();
    
                input = new ObjectInputStream(socket.getInputStream());
                if(input.readBoolean()){
                    System.out.println("服务["+serviceName+"]注册成功!");
                }
    
                /*可提供服务放入本地缓存*/
                serviceCache.put(serviceName,impl);
    
            } catch (IOException e) {
                e.printStackTrace();
            }  finally{
                if (socket!=null) socket.close();
                if (output!=null) output.close();
                if (input!=null) input.close();
            }
        }
    
        /*获取服务*/
        public Class getLocalService(String serviceName) {
            return serviceCache.get(serviceName);
        }
    
    }
    
    
修改rpcServerFrame
  • package cn.enjoyedu.rpc.rpc.base;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Method;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     *@author Mark老师  
     *
     *类说明:rpc框架的服务端部分
     */
    @Service
    public class RpcServerFrame {
    
    //    @Autowired
    //    private RegisterService registerService;
        // 修改1
        @Autowired
        private RegisterServiceWithRegCenter registerServiceWithRegCenter;
    
        //服务的端口号
        private int port;
    
        /*处理服务请求任务*/
        private static class ServerTask implements Runnable{
    
            private Socket socket;
            // 修改3
            private RegisterServiceWithRegCenter registerServiceWithRegCenter;
    
            // 修改4
            public ServerTask(Socket client,
                              RegisterServiceWithRegCenter registerServiceWithRegCenter) {
                this.socket = client;
                this.registerServiceWithRegCenter = registerServiceWithRegCenter;
            }
    		
            // 接受客户端的请求,并调用实际的方法
            // 接受的内容---1.方法所在的类名接口名 2.调用的方法名  3.方法参数及方法属性、具体的参数值
            // 
            @Override
            public void run() {
                try(
                     ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
                      ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())){
    
                    /*方法所在类名接口名*/
                    // 读字符串inputStream.readUTF()
                    String serviceName = inputStream.readUTF();
                    /*方法的名字*/
                    String methodName = inputStream.readUTF();
                    /*方法的入参类型*/
                    // 读对象inputStream.readObject()
                    // 强制转型,转成类
                    Class<?>[] paramTypes = (Class<?>[]) inputStream.readObject();
                    /*方法的入参的值*/
                    Object[] args = (Object[]) inputStream.readObject();
    
                    // 修改5
                    /*从容器中拿到服务的Class对象*/
                    Class serviceClass = registerServiceWithRegCenter.getLocalService(serviceName);
                    if(serviceClass == null){
                        throw new ClassNotFoundException(serviceName+ " not found");
                    }
    
                    /*通过反射,执行实际的服务*/
                    // 通过反射获取具体的方法Method对象
                    // 入参包括两个,方法名和参数类型数组
                    Method method = serviceClass.getMethod(methodName, paramTypes);
                    // 通过动态代理调用响应的方法
                    Object result  = method.invoke(serviceClass.newInstance(),args);
    
                    /*将服务的执行结果通知调用者*/
                    // 将调用结果输出给调用者,即客户端
                    outputStream.writeObject(result);
                    outputStream.flush();
    
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public void startService(String serviceName, String host, int port, Class impl) throws Throwable{
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(port));
            System.out.println("RPC server on:"+port+":运行");
            // 修改2
           // registerService.regRemote(ServiceName, impl);
    	
                  registerServiceWithRegCenter.regRemote(serviceName,host,port,impl);
            try{
                while(true){
                    new Thread(new ServerTask(serverSocket.accept(),
                            registerServiceWithRegCenter)).start();
                }
            }finally {
                serverSocket.close();
            }
        }
    
    }
    
    
    

测试注册中心

  • 为了多注册服务,端口号取随机的

  • 启动sms

  • 再启动一次sms

  • 客户端启动

    • 动态代理的地址就不能写死了,应该从注册中心获取地址

    • package cn.enjoyedu.rpc.client.rpc;
      
      import cn.enjoyedu.rpc.remote.vo.RegisterServiceVo;
      import org.springframework.stereotype.Service;
      
      import java.io.ObjectInputStream;
      import java.io.ObjectOutputStream;
      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.Method;
      import java.lang.reflect.Proxy;
      import java.net.InetSocketAddress;
      import java.net.Socket;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Random;
      import java.util.Set;
      
      /**
       *@author Mark老师   
       *类说明:rpc框架的客户端代理部分
       */
      @Service
      public class RpcClientFrame {
      
          /*远程服务的代理对象,参数为客户端要调用的的服务*/
          // 传递的应该是方法所在的类名或接口名
          public static<T> T getRemoteProxyObject(final Class<?> serviceInterface) throws Exception {
              /*获得远程服务的一个网络地址*/
              // 修改1
              // InetSocketAddress addr = new InetSocketAddress("127.0.0.1",8778);
              InetSocketAddress addr =   getService(serviceInterface.getName());
      
              /*拿到一个代理对象,由这个代理对象通过网络进行实际的服务调用*/
              // 应该产生一个代理对象,和服务器进行通讯
              // 需要实现了InvocationHandler的类DynProxy
              return (T)Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                      new Class<?>[]{serviceInterface},
                      new DynProxy(serviceInterface,addr));
          }
      
      
          /*动态代理,实现对远程服务的访问*/
          // 连接服务器,发送接口名、方法名、方法参数
          private static class DynProxy implements InvocationHandler{
              private Class<?> serviceInterface;
              private InetSocketAddress addr;
      
              public DynProxy(Class<?> serviceInterface, InetSocketAddress addr) {
                  this.serviceInterface = serviceInterface;
                  this.addr = addr;
              }
      
              @Override
              public Object invoke(Object proxy, Method method, Object[] args)
                      throws Throwable {
                  Socket socket = null;
                  ObjectInputStream inputStream = null;
                  ObjectOutputStream outputStream = null;
                  try{
                      
                      // 1.建立网络连接
                      socket = new Socket();
                      socket.connect(addr);
                      outputStream = new ObjectOutputStream(socket.getOutputStream());
      
                      // 2.发送接口名、方法名、参数类型、参数值
                      //方法所在类名接口名
                      outputStream.writeUTF(serviceInterface.getName());
                      //方法的名字
                      outputStream.writeUTF(method.getName());
                      //方法的入参类型
                      outputStream.writeObject(method.getParameterTypes());
                      //方法入参的值
                      outputStream.writeObject(args);
      
                      outputStream.flush();
      
                      // 3.接受服务器传回的结果
                      inputStream = new ObjectInputStream(socket.getInputStream());
                      /*接受服务器的输出*/
                      System.out.println(serviceInterface+" remote exec success!");
                      return inputStream.readObject();
      
                  }finally {
                      if(socket!=null) socket.close();
                      if(outputStream!=null) outputStream.close();
                      if(inputStream!=null) inputStream.close();
      
                  }
              }
          }
      
      
      
          /*----------------以下和动态获得服务提供者有关------------------------------*/
      
          private static Random r = new Random();
      
          /*获得远程服务的地址*/
          private static InetSocketAddress getService(String serviceName)
                  throws Exception {
              //获得服务提供者的地址列表
              List<InetSocketAddress> serviceVoList = getServiceList(serviceName);
              InetSocketAddress addr
                      = serviceVoList.get(r.nextInt(serviceVoList.size()));
              System.out.println("本次选择了服务器:"+addr);
              return addr;
          }
      
          /*获得服务提供者的地址*/
          private static List<InetSocketAddress> getServiceList(String serviceName)
                  throws Exception {
              Socket socket = null;
              ObjectOutputStream output = null;
              ObjectInputStream input = null;
      
              try{
                  socket = new Socket();
                  socket.connect(new InetSocketAddress("127.0.0.1",9999));
      
                  output = new ObjectOutputStream(socket.getOutputStream());
                  //需要获得服务提供者
                  output.writeBoolean(true);
                  //告诉注册中心服务名
                  output.writeUTF(serviceName);
                  output.flush();
      
                  input = new ObjectInputStream(socket.getInputStream());
                  Set<RegisterServiceVo> result
                          = (Set<RegisterServiceVo>)input.readObject();
                  List<InetSocketAddress> services = new ArrayList<>();
                  for(RegisterServiceVo serviceVo : result){
                      String host = serviceVo.getHost();//获得服务提供者的IP
                      int port = serviceVo.getPort();//获得服务提供者的端口号
                      InetSocketAddress serviceAddr = new InetSocketAddress(host,port);
                      services.add(serviceAddr);
                  }
                  System.out.println("获得服务["+serviceName
                          +"]提供者的地址列表["+services+"],准备调用.");
                  return services;
              }finally{
                  if (socket!=null) socket.close();
                  if (output!=null) output.close();
                  if (input!=null) input.close();
              }
      
          }
      
      }
      
      
    • 取服务时,如果有多个,做负载均衡?

      用随机数

高并发RPC解决方案

  • 基于TCP的RPC实现

    • Dubbo:
      阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。
  • Provider: 暴露服务的服务提供方。

  • Consumer: 调用远程服务的服务消费方。

  • Registry: 服务注册与发现的注册中心。

  • Monitor: 统计服务的调用次调和调用时间的监控中心。

  • Container: 服务运行容器。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值