【RPC】 ---- RPC入门了解 & 最简单的RPC的实现

本文详细介绍了RPC(远程过程调用)的历史背景、理论知识和实现方法,探讨了RPC与HTTP远程调用的优缺点。通过实例讲解了基于TCP和HTTP协议的RPC实现,包括对象序列化、网络通信协议等关键步骤,分析了RPC框架如Dubbo和gRPC的特点。

一、历史背景

  • 从单机走向分布式,产生了很多分布式的通信方式

    • 最古老也是最有效的,并且永不过时的,TCP/UDP的二进制传。事实上所有通信方式归根到底都是TCP/UDP

    • CORBA(Common Object Reqeust Broker Architecute),古老而复杂的,支持面向对象的通信协议

    • Web Service (SOA SOAP RDDI WSDL)

      基于http + xml的标准化Web API

    • RestFul (Representational State Transfer)

      回归简单化本源的Web API的事实标准

      http://www.baidu.com/people/zhangsan
      

      http + json

    • RMI(Remote Method Invocation)

      Java内部的分布式通信协议

    • JMS(Java Message Service)

      JavaEE中的消息框架标准,为很多MQ所支持

    • RPC(Remote Procedure Call)

      远程方法调用,重点在于方法调用(不支持对象的概念),具体实现甚至可以用RMI、RestFul等去实现,但一般不用,因为RMI不能跨语言,而RestFul效率太低。多用于服务器集群间的通信,因此常使用更加高效、短小精悍的传输模式以提高效率

二、理论知识

1、概念

RPC(Remote Procedure Call),远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

通俗点:

  • 就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据

2、为什么要用RPC

  • 可以做到分布式,现代化的微服务
  • 部署灵活
  • 解耦服务
  • 扩展性强

RPC的目的是让你在本地调用远程的方法,而对你来说这个 调用是透明的,你并不知道这个调用的方法是部署哪里。通过RPC能解耦服务,这才是使用RPC的真正目的。

单台服务器处理能力有限,RPC可提升系统处理能力和吞吐量,也是实现分布式计算的基础。

3、工作原理

RPC采用 客户机/服务器模式

请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续执行。

image-20210521154121693

远程调用过程:

  1. 调用客户端句柄:执行传送参数
  2. 调用本地系统内核发送网络消息
  3. 消息传送到远程主机
  4. 服务器句柄得到消息并取得消息
  5. 执行远程过程
  6. 执行的过程将结果返回服务器句柄
  7. 服务器句柄返回结果,调用远程系统内核
  8. 消息传回本地主机
  9. 客户句柄向内核接收消息
  10. 客户接收句柄返回的数据

4、RPC解决了什么问题?

  • 通讯问题

    即A与B之间的通信,建立TCP连接

  • 寻址问题

    A通过RPC框架连接到B的服务器及特定端口和调用的方法名

  • 参数序列化与反序列化

    发起远程调用参数值需要二进制化,服务器收到二进制参数需要反序列化

5、RPC vs HTTP 远程调用方式

常见的远程调用方式有以下几种:

RPC服务

自定义数据格式,基于原生TCP通信,速度快,效率高。

RPC的框架:webservie(cxf)、dubbo

HTTP服务

http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿

现在热门的Rest风格,就可以通过http协议来实现。

http的实现技术:HttpClient

两者比较

  • 相同点:底层通讯都是基于socket,都可以实现远程调用,都可以实现服务调用服务。

  • 不同点:

    • RPC

      • 框架有:dubbo、cxf、(RMI远程方法调用)Hessian

      • 当使用RPC框架实现服务间调用的时候,要求服务提供方和服务消费方 都必须使用统一的RPC框架,要么都dubbo,要么都cxf。

      • 跨操作系统同一编程语言内使用

      • 优势:调用快、处理快

    • HTTP

      • 框架有:httpClient

      • 当使用http进行服务间调用的时候,无需关注服务提供方使用的编程语言,也无需关注服务消费方使用的编程语言,服务提供方只需要提供restful风格的接口,服务消费方,按照restful的原则,请求服务即可

      • 跨系统跨编程语言的远程调用框架

      • 优势:通用性强

补充:

  • 从传输速度上来看,因为HTTP封装的数据量更多所以数据传输量更大,所以RPC的传输速度是比RESTFUL更快的。

  • 因为HTTP协议是各个框架都普遍支持的。

    • 在不知道情况来源的框架、数据形势是什么样的,所以在网关可以使用Restful利用http来接受。

    • 而在微服务内部的各模块之间因为各协议方案是公司内部自己定的,所以知道各种数据方式,可以使用TCP传输以使各模块之间的数据传输更快。

    • 所以可以网关和外界的数据传输使用RESTFUL,微服务内部的各模块之间使用RPC。

  • RESTFUL的API的设计上是面向资源的,对于同一资源的获取、传输、修改可以使用GET、POST、PUT来对同一个URL进行区别,而RPC通常把动词直接体现在URL上

image-20210522204036764

从图上可以看出,二者其实都可以看出在进行实现RPC的时候,底层的通信协议是可以使用HTTP协议的,另外单独的使用HTTP协议也是可以直接的实现调用的功能,理论上,HTTP的请求也是一种方法的调用,通过get或者post方法和url去调用方法,但是HTTP为了更高的可读性将请求头变得非常的臃肿,传输效率比较低,而RPC则是牺牲了可读性使得效率更高。

换句话说其实二者是一种功能,只不过应用的场景不同,而且针对点不同,HTTP是针对Client和Server ;而 RPC是针对 Server 与Server之间的调用,另外RPC之间的调用效率更加的高,而且增加了ZK进行服务的自动配置与管理。

换句话说,上层的协议还是都是依赖于TCP/IP协议,总的来说就是基于字节流来进行编码。

6、常用RPC框架

  • Dubbo

    Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案

  • gRPC

    是Google开发的高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言,本身它不是分布式的,所以要实现上面的框架的功能需要进一步的开发。

三、实现

实现RPC主要是做到两点:

  • 实现远程调用其它计算机的服务

    • 要实现远程调用,就需要通过网络传输数据。A程序提供服务,B程序通过网络请求参数传递给A,A本地执行后得到结果,再将结果返回给B程序。

      • 何种网络通信协议?

        现在比较流行的RPC框架,都会采用TCP协议作为底层传输协议。

        RPC通讯协议:http、http2.0(gRPC)、TCP(同步/异步;阻塞/非阻塞)、WebService。
        
      • 数据传输格式?

        两个程序进行通讯,必须约定好数据传输格式,即定义好

        请求和响应格式,另外,数据在网络中传输需要进行序列化,所以还需要约定统一的序列化的方式

  • 像调用本地服务一样调用远程服务

    • 如果仅仅是远程调用,还不算是RCP,因为RPC强调的是过程调用,调用的过程对用户而言应该是透明的,用户不应该关心调用的细节,可以像调用本地服务一样调用远程服务。所以RPC一定要对调用的过程进行封装

调用流程图

image-20210522151013694

1、对象序列化

1.1 序列化的原因

任何形式的数据都需要转换成二进制流在网络传输

1.2 概念

  • 对象序列化

    将对象转换为二进制流的过程

  • 对象反序列化

    将二进制流恢复为对象的过程

1.3 解决方案(RPC序列化框架)

  • Google的Protocol Buffers

  • Java内置的序列化方式 java.io.Serializable

  • Hessian

  • json序列化框架

    • Jackson
    • google Gson
    • Ali FastJson
  • kyro

  • xmlrpc (xstream)

1.4 代码实现

public class JdkSerializable {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		User user = new User("zhangsan", 12);
		byte[] bytes = serializableObject(user);
		User newUser = (User) deSerializableObject(bytes);
		System.out.println(newUser);
	}

	public static byte[] serializableObject(Object obj) throws IOException {

		User user = (User)obj;

		// 定义一个字节数组输出流 (作用:获取内存中的缓存数据并转化为数组)
		ByteArrayOutputStream os = new ByteArrayOutputStream();

		// 对象输出流
		// 构造方法,传递字节输出流
		ObjectOutputStream out = new ObjectOutputStream(os);

		// 将对象写入到字节数组输出,进行序列化
		out.writeObject(user);
		byte[] zhangsanByte = os.toByteArray();

		return zhangsanByte;
	}

	public static Object deSerializableObject(byte[] bytes) throws IOException, ClassNotFoundException {
		// 字节数组输入流
		ByteArrayInputStream is = new ByteArrayInputStream(bytes);

		// 从流中获取对象
		ObjectInputStream in = new ObjectInputStream(is);

		return 	in.readObject();
	}
}

2、网络通讯协议

2.1 基于TCP协议实现的RPC

2.1.1 版本一

通过Socket编程,实现网络间的二进制字节数组(ByteArray)传输

image-20210522163426968

image-20210522010825575

2.1.1.1 原理
  • 基于Java的反射机制和Socket API实现

  • 方法的调用使用反射机制,消费者把需要调用的接口名称方法参数通过Socket通信传到服务端,服务端再通过反射机制调用对应的方法获取到值,然后再通过相同方式把结果返回给消费者

2.1.1.2 场景

服务消费者调用服务提供者的SayHelloService接口的sayHello(String helloArg)方法具体实现获取结果

2.1.1.3 实现代码
  • 服务提供者

    /**
     * @ClassName: Server
     * @Author: whc
     * @Date: 2021/05/22/0:52
     *
     * 阻塞式I/O,实际生产环境中出于性能的考虑,往往使用非阻塞I/O,以提供更大的吞吐量
     */
    public class Server {
    
    	private static HashMap<String, Class> registerTable = new HashMap<>();
    	static {
    		// key类型是接口,value是具体实现类
    		registerTable.put(SayHelloService.class.getName(), SayHelloServiceImpl.class);
    	}
    
    	public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    		ServerSocket server = new ServerSocket(8080);
    
    		while(true) {
    			Socket socket = server.accept();
    			// 读取服务信息
    			ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
    			String interfacename = input.readUTF(); // 接口名称
    			String methodName = input.readUTF(); // 方法名称
    			Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); // 参数类型
    			Object[] arguments = (Object[])input.readObject(); // 参数对象
    
    			// 执行调用
    			Class serviceinterfaceclass = Class.forName(interfacename); // 得到接口的class
    			Object service = registerTable.get(interfacename).newInstance();//取得服务实现的对象
    			Method method = serviceinterfaceclass.getMethod(methodName, parameterTypes); // 获得要调用的方法
    			Object result = method.invoke(service, arguments);
    
    			ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
    			output.writeObject(result);
    		}
    	}
    }
    
  • 服务消费者

    /**
     * @ClassName: Consumer 服务消费者
     * @Author: whc
     * @Date: 2021/05/22/0:43
     */
    public class Consumer {
    	public static void main(String[] args) throws NoSuchMethodException, IOException, ClassNotFoundException {
    		// 接口名称
    		String interfacename = SayHelloService.class.getName();
    
    		// 需要远程执行的方法
    		Method method = SayHelloService.class.getMethod("sayHello", String.class);
    
    		// 需要传递到远端的参数
    		Object[] arguments = {"hello"};
    
    		Socket socket = new Socket("127.0.0.1", 8080);
    
    		// 将方法名称和参数传递到远端
    		ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
    		output.writeUTF(interfacename); // 接口名称
    		output.writeUTF(method.getName()); // 方法名称
    		output.writeObject(method.getParameterTypes()); // 参数类型
    		output.writeObject(arguments); // 参数
    
    		// 从远端读取方法执行结果
    		ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
    		Object result = input.readObject();
    		System.out.println(result);
    	}
    }
    
2.1.1.4 缺陷
  • 问题一

    版本一是一个最简单的RPC思想的例子,客户端要实现一堆通信的逻辑,耦合度太高,能不能客户端只负责调用接口,中间的网络细节不用去实现呢?接下来的版本二中的红色stub就是干这活的。

  • 问题二

    版本一只有SayHelloService一个接口的方法调用,假如Client端要调用的Server端接口有很多个呢?

    在版本一中可以通过反射机制,将Socket把要调用的接口名传给Server端,Server端再通过接口名反射去调用已实现的接口方法,但是这种实现方式;

    版本二则用动态代理,将Client要调用的接口名称传递给stub动态代理生成一个Client要调用的接口类。

    动态代理能解决繁琐编程

2.1.2 版本二

(标准的RPC在Client和Server都需要有stub,为了一步一步理解RPC,所以版本二只在Client端加stub)

image-20210522155139131

2.1.2.1 原理
  • Client把调用Server端的具体细节交给了stub类,stub动态代理生成一个Client要调用的接口类,通过这个类去实现跟Server端的交互,让Client端实现只负责接口方法的调用,不用去关心一堆网络细节。
  • 当消费者调用服务接口的方法时,实际上调用的是接口代理类InvocationHandlerinvoke方法,把需要调用的接口名称、方法、参数通过代理类封装后传到服务端,服务端接收到信息后,找到对应的接口实现类,调用对应的方法获取到值,然后把结果返回给消费者。
2.1.2.2 场景

服务消费者传递对应的接口名称,调用对应接口的方法具体实现获取结果。

比如IUserService接口的方法findUserById,根据传入的id查找对应的用户

比如IProductService接口的方法findProductByName,根据传入的name查找对应的产品

2.1.2.3 实现代码
  • 服务提供者

    public class Server {
    
    	private static boolean running = true;
    
    	private static HashMap<String, Class> registerTable = new HashMap<>();
    	static {
    		// key类型是接口,value是具体实现类
    		registerTable.put(IUserService.class.getName(), IUserServiceImpl.class);
    		registerTable.put(IProductService.class.getName(), IProductServiceImpl.class);
    	}
    
    	public static void main(String[] args) throws Exception {
    		ServerSocket server = new ServerSocket(8080);
    		while(running){
    			Socket client = server.accept();
    			process(client);
    			client.close();
    		}
    		server.close();
    	}
    
    	public static void process(Socket socket) throws Exception {
    		ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
    		ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
    
    		//为了适应客户端通用化而做的改动
    		String className = ois.readUTF();
    		String methodName = ois.readUTF();
    		Class[] parameterTypes = (Class[]) ois.readObject();
    		Object[] parameters = (Object[]) ois.readObject();
    
    //		IUserService service = new IUserServiceImpl();//服务类型暂时还是写死的,不够灵活
    		Object service = registerTable.get(className).newInstance();
    		//考虑到Client端可能调用Server端的多个方法,不仅仅是一个方法的情况
    		//这时可以根据方法名和参数类型获取Method对象供后面反射调用接口实现类的方法
    		Method method = service.getClass().getMethod(methodName, parameterTypes);
    		// 反射调用方法查询出结果
    		Object o = method.invoke(service, parameters);
    		oos.writeObject(o);
    		// flush() 是清空,而不是刷新
    		oos.flush();
    	}
    
    }
    
  • 服务消费者

    public class Client {
    	public static void main(String[] args) throws IOException {
    		// Client这里不用关注一堆网络交互的细节,直接调用Stub产生的代理对象的方法,既可完成整个链路的调用
    		IUserService service = (IUserService)Stub.getStub(IUserService.class);
    		IProductService service1 = (IProductService)Stub.getStub(IProductService.class);
    		System.out.println(service.findUserById(123));
    		System.out.println(service1.findProductByName("Bob"));
    	}
    }
    
  • Stub

    在RPC里面是代理的意思,是个约定俗称的东西,所以不叫Proxy

    /**
     * @ClassName: Stub  服务类型能够改变,变成通用的,主要是将服务类型作为参数传入getStub
     * @Author: whc
     * @Date: 2021/05/21/16:37
     */
    public class Stub {
    	public static Object getStub(Class c){
    		InvocationHandler h = new InvocationHandler() {
    			@Override
    			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    				Socket socket = new Socket("127.0.0.1",8080);
    
    				ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
    
    				// 通用化的改变
    				oos.writeUTF(c.getName()); // 服务类型
    				oos.writeUTF(method.getName()); // 方法名字
    				oos.writeObject(method.getParameterTypes()); // 方法参数类型
    				oos.writeObject(args); // 参数
    				oos.flush();
    
    				// 接收服务端返回的结果,object读入
    				ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
    				Object o = ois.readObject();
    				return o; // 返回通用对象
    			}
    		};
    		// 动态代理产生一个实现了c接口的代理对象
    		// 参数1是类加载器,参数2传入被代理的接口类,参数3是InvocationHandler:被代理时反射调用的方法,也就是Stub给Client端处理的一堆细节
    		Object o = Proxy.newProxyInstance(c.getClassLoader(), new Class[]{c}, h);
    		System.out.println(o.getClass().getName());
    		System.out.println(o.getClass().getInterfaces()[0]);
    		// 返回实现接口类的代理对象
    		return o;
    	}
    }
    
2.1.2.4 缺陷
  • 问题一:在调用过程中,对象里面的所有字段细节,万一增减字段呢?
  • 问题二:Server端逻辑依旧很复杂,能不能做到像Client一样也通过Stub去处理细节问题呢?
2.1.3 总结

真实环境中,多个客户端多个请求到服务端,服务端需要同时接收和处理多个客户端请求,涉及并发处理、服务路由、负载均衡等问题,以上代码无法满足

2.2 基于HTTP协议实现的RPC

优点:

  • 简单、实用、开发方便

缺点:

  • 性能不是很稳定,在海量数据时,完全顶不住,容易宕机
  • 因为不是走的注册中心,不便于维护、监控以及统计分析
2.2.1 HTTP协议
  • Hypertext Transfer Protocol的缩写(超文本传输协议)
  • 属于应用层协议,构建在TCP与IP协议之上,处于TCP/IP体系架构顶端
  • 无需处理丢包补发、握手及数据的分段和重新组装等细节
2.2.2 设计一个简单应用层协议(Servlet)

原理

  1. 设计一个工具类ProtocolUtil,提供readRequest、readResponse、writeRequest、writeResponse方法

  2. 协议请求Request(包括编码、命令和命令长度三个字段)

  3. 协议响应Response(包括编码、响应内容和响应内容长度三个字段)

  4. 服务端和客户端

    客户端向服务端发送一条命令,服务端收到命令后,会判断命令是否为"HELLO",如果是"HELLO",则返回给客户端的响应为"hello!",否则返回给客户端的响应为"bye bye!"。

image-20210521222146144

工具类ProtocolUtil
  • readRequest(InputStream input)方法 : 协议请求Request : 包括编码、命令和命令长度三个字段
  • readResponse(InputStream input)方法:协议响应Response: 包括编码、长度和内容三个字段
  • writeRequest(OutputStream output, Request request)方法:将Request对象中的字段根据对应的编码写入输入流中
  • writeResponse(OutputStream output, Response response)方法:将Response对象中的字段根据对应的编码写入输入流中
public class ProtocolUtil {

	/**
	 * 协议请求Request : 包括编码、命令和命令长度三个字段
	 * readRequest方法将从传递进来的输入流中读取请求的编码、命令和长度三个参数,
	 * 进行相应的编码转换,并构造成Request对象返回
	 * @param input
	 * @return
	 */
	public static Request readRequest(InputStream input){
		Request request = new Request();

		try {
			// 读取编码
			byte[] encodeByte = new byte[1];
			input.read(encodeByte);
			byte encode = encodeByte[0];

			// 读取命令长度
			byte[] commandLengthBytes = new byte[4]; // 缓冲区
			input.read(commandLengthBytes);

			// 将字节数组转化为整数
			int commandLength = ByteUtil.bytes2Int(commandLengthBytes);

			// 读取命令
			byte[] commandBytes = new byte[commandLength];
			input.read(commandBytes);
			String command = Encode.GBK.getEncode() == encode ? new String(commandBytes, "GBK") : new String(commandBytes, "UTf8");

			// 组装请求返回
			request.setEncode(encode);
			request.setCommand(command);
			request.setCommandLength(commandLength);
		} catch (IOException e) {
			System.err.println(e.getMessage());
		}

		return request;
	}

	/**
	 * 协议响应Response: 包括编码、长度和内容三个字段
	 * readResponse方法将从传递进来的输入流中读取响应的编码、长度和内容三个参数,
	 * 进行相应的编码转换,并构造成Response对象返回
	 * @param input
	 * @return
	 */
	public static Response readResponse(InputStream input){
		Response response = new Response();
		try {
			// 读取编码
			byte[] encodeByte = new byte[1];
			input.read(encodeByte);
			byte encode = encodeByte[0];

			// 读取响应长度
			byte[] responseLengthByte = new byte[4]; // 缓冲区
			input.read(responseLengthByte);
			int commandLength = ByteUtil.bytes2Int(responseLengthByte);

			// 读取响应
			byte[] responseByte = new byte[commandLength];
			input.read(responseByte);
			String resContent = Encode.GBK.getEncode() == encode ? new String(responseByte, "GBK") : new String(responseByte, "UTF8");

			// 组装响应返回
			response.setEncode(encode);
			response.setResponse(resContent);
			response.setResponseLength(commandLength);
		} catch (IOException e) {
			System.err.println(e.getMessage());
		}

		return response;
	}

	/**
	 * 作用是将Response对象中的字段根据对应的编码写入输入流中
	 * @param output
	 * @param response
	 */
	public static void writeResponse(OutputStream output, Response response){
		try {
			// 编码
			output.write(response.getEncode());
			// 响应长度
			output.write(ByteUtil.int2ByteArray(response.getResponseLength()));
			// 响应
			output.write(Encode.GBK.getEncode() == response.getEncode() ? response.getResponse().getBytes("GBK") : response.getResponse().getBytes("UTF8"));
			output.flush();
		} catch (IOException e) {
			System.err.println(e.getMessage());
		}
	}

	/**
	 * 作用是将Request对象中的字段根据对应的编码写入输入流中
	 * @param output
	 * @param request
	 */
	public static void writeRequest(OutputStream output, Request request){
		try {
			// 编码
			output.write(request.getEncode());
			// write一个int值会截取其低8位传输,丢弃其高24位,因此需要将基本类型转化为字节流
			// 请求命令长度
			output.write(ByteUtil.int2ByteArray(request.getCommandLength()));
			// 请求命令
			output.write(Encode.GBK.getEncode() == request.getEncode() ? request.getCommand().getBytes("GBK") : request.getCommand().getBytes("UTF8"));
			output.flush();
		} catch (IOException e) {
			System.err.println(e.getMessage());
		}
	}

}

客户端实现
  1. 新建一个Request,并给字段都赋值
  2. 新建一个Socket连接8080端口
  3. 调用ProtocolUtil.writeRequest(output,request)方法发送请求
public class Client {

	public static void main(String[] args) throws IOException {
		// gbk编码
		Request request = new Request();
		request.setCommand("HELLO");
		request.setCommandLength(request.getCommand().length());
		request.setEncode(Encode.UTF8.getEncode());

		Socket client = new Socket("127.0.0.1", 8080);

		OutputStream output = client.getOutputStream();
		// 发送请求
		ProtocolUtil.writeRequest(output, request);

		// 读取响应数据
		InputStream input = client.getInputStream();
		Response response = ProtocolUtil.readResponse(input);
		System.out.println(response.getResponse());
	}
}
服务端实现
  1. 新建一个ServerSocket端口8080
  2. 一个死循环调用ProtocolUtil.readRequest方法读取响应数据
  3. 处理数据之后组装响应实体Response
  4. 通过ProtocolUtil.writeResponse方法,把响应实体发送回去
public class Server {

	private static boolean running = true;

	/**
	 * 服务端接收客户端的请求,根据请求命令的不同,响应不同的消息
	 */
	public static void main(String[] args) throws IOException {
		ServerSocket server = new ServerSocket(8080);
		while(running) {
			Socket client = server.accept();
			process(client);
			client.close();;
		}
	}

	private static void process(Socket client) throws IOException {
		// 读取请求数据
		InputStream input = client.getInputStream();
		Request request = ProtocolUtil.readRequest(input);

		OutputStream output = client.getOutputStream();

		// 组装响应实体Response
		Response response = new Response();
		response.setEncode(Encode.UTF8.getEncode());
		if(request.getCommand().equals("HELLO")) {
			response.setResponse("hello!");
		} else {
			response.setResponse("bye bye!");
		}
		response.setResponseLength(response.getResponse().length());

		ProtocolUtil.writeResponse(output, response);
	}
}

2.2.3 通过HttpClient发送HTTP请求
2.2.3.1 使用原因
  • 需要自己实现向服务器发送请求,以及解析服务端响应这个过程
  • 使用Socket API工作量大,并且是重复工作
  • HttpClient对HTTP协议通信过程进行了封装。提供了一套成熟的客户端编程工具包
2.2.3.2 代码
  • 导入依赖

    <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore -->
            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpcore</artifactId>
                <version>4.4.10</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
            <dependency>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
                <version>4.5.6</version>
            </dependency>
    
public class Client {

	public static void main(String[] args) throws IOException {
		// 加上http协议头,标明请求为HTTP请求
		String url = "http://www.baidu.com";

		// 组装请求
		HttpClient httpClient = new DefaultHttpClient();
		HttpGet httpGet = new HttpGet(url);

		// 接收请求
		HttpResponse response = httpClient.execute(httpGet);
		HttpEntity entity = response.getEntity();

		byte[] bytes = EntityUtils.toByteArray(entity);
		String result = new String(bytes, "UTF8");
		System.out.println(result);
	}
}

2.3 TCP协议 vs HTTP协议 实现的RPC

  • 使用TCP协议实现RPC

    • 处于协议栈的下层,更灵活对协议字段进行定制,减少网络传输字节数,降低网络开销,提升性能,实现更大吞吐量和并发数
    • 但 需要关注底层复制细节,实现代价更高。难显现跨平台的调用
  • 使用HTTP协议实现RPC

    • JSON与XML成为通用的格式标准,开源解析工具成熟
    • 很多成熟的开源Web容器已经处理好,如tomcat、jboss、apache等
    • 但由于上层协议,发送同等内容需传输字节数更多,效率较TCP差

当然,通过优化代码实现和使用gzip数据压缩,能够缩小这一差距。通过权衡利弊,结合实际环境中其性能对于用户体验的影响来看,基于HTTP协议的RPC还是有很大优势的。

参考:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值