【Java框架】手写实现简单的微服务之RMI框架

Ⅰ什么是RMI

  Remote Method Invoke(远程方法调用)。RMI就是用Java实现的RPC(远程过程调用)。其目的就是:利用网络通信,客户端远程调用服务器的方法,客户端本地并不存在此方法。

  在细致点来说,为了实现我们的短连接。短连接是相对于长连接而言的。CSFramework的实现手段就是长连接,即,对于服务器而言,每一个客户端连接都由一个独立线程维护其会话过程,直至会话结束,方断开连接。

  短连接是:由客户端向服务器端发起“请求”,服务器连接客户端,并解析客户端传来的请求内容,并执行相关操作以得到“响应”信息,并将响应回送给该客户端,然后就立刻关闭网络连接!可以说,这是一个短平快的”请求/响应“过程。再进一步思考这个短平快的”请求/响应“过程,其本质是:客户端远程执行了一个在服务器端的方法!也就是RMI的基本意思了。

ⅡRMI框架需求分析

基于上述内容,可以更加干脆的如是思考:

  1. 客户端执行一个方法method(),这个方法的执行过程实质上是:向服务器发送要执行的方法名称、实参等这个方法赖以执行的信息;
  2. 服务器端连接客户端,并接收到要执行的方法的相关信息;
  3. 服务器端执行相关方法,并将执行的结果返回给客户端;
  4. 客户端将服务器返回的执行结果,当成method()的执行结果提交给用户!

  在这种思维中,客户端实质上相当于用一个代理执行method();而这个method()在执行过程中要做的事,与method()本身的逻辑没有任何关系;

RMI客户端要做的事如下:

  1. 连接RMI服务器;
  2. 向服务器发送方法执行的所有必要信息;
  3. 等待服务器返回这个方法在服务器端执行的后果!

RMI服务器端要做的事如下:

  1. 建立RMI服务器并且accept()侦听客户端连接请求;
  2. 连接客户端;
  3. 接受客户端发送的相关方法的所有必要信息;
  4. 找到这个方法,并反射机制执行该方法;
  5. 将方法执行的结果通过网络传输给客户端
  6. 断开网络连接!

(一)准备工作:使用哪种代理

  经过上述需求分析讨论,我们知道需要用到代理。那么问题来了,该使用JDKProxy还是CglibProxy?

  我们知道两者的差别是在是否需要接口,CglibProxy不需要接口,没有接口不能告知双方各自同步的地方,提供个接口,客户端和服务器都按接口来走,是个规范和标杆。因此我们使用JDKProxy。

  在RMI实际的工作过程中,客户端只有接口,而不存在也不需要接口的实现类!客户端对RMI方法的执行,实质上都是传递方法信息和方法参数,再从服务器端接受结果!因此,在客户端获取proxy的时候,能提供的仅仅是接口,没有可能提供该接口的实现类,因为它根本没有也不需要。

ⅢRMI框架的实现

(一)实现思路

  其核心思想涉及到两个网络端(客户端和服务器端)。一个端可以通过调用另一个端的方法,实现相关功能。一个端“执行”一个方法,而这个方法的实际执行是在另一端进行的!

  当然,如果两个端都应该有相同的类,那么自然会拥有相同的方法。但是,我们知道这在实际上是很难做到同步的,一个端升级更新,另一个端也需要尽快更新代码,这是很难达到一致的。因此,又要使用我们的接口了!接口作为我们两端共同遵守的约定,并且对于相关操作好控制,规定了可以调用什么,不可以调用什么。

  一个端所谓的执行这个方法,其实是通过调用这个类的代理对象的方法,在这个方法中实际上是将执行这个方法的参数和类名称、方法名称,通过网络通讯传输给另一端;另一端根据得到的方法名称、类名称和参数,实际执行那个方法,再将方法执行结果回传给对端。

下面是我实现RMI的具体思维导图

在这里插入图片描述
实现思路差不多就是这个图了,但是在具体编写的时候遇到许多问题和单元测试查漏补缺。(如果感兴趣想自己实现的,具体看RMI框架一步步实现)。

(二)RMI服务器启动前准备工作

a. RMIBeanDefinition

  将所要执行的方法所需要的一些东西封装成一个类,面向对象编程思想。

import java.lang.reflect.Method;

public class RMIBeanDefinition {

	private Class<?> klass;//要执行的方法的所属的接口的实现类
	private Object object;//要执行的方法的对象
	private Method method;//要执行的方法
	
	//一开始我以为klass和obejct可以为单例,为了节省空间只保存一份,因为方法的调用只需要一个对象啊,后来我经过点拨
	//发现单例需谨慎!如果你多次扫描改变了实现类呢?以后替换就不行了,会出错,所以成员单例一定要思考在思考!
	//点拨:单例就全部单例了,不同接口的实现类的对象肯定是不一样的!
	// 所以,思考单例问题一定要谨慎。为了不出现更大的问题,对象重复仅仅四个字节的问题,就浪费吧。
	
	RMIBeanDefinition() {
	}

	Class<?> getKlass() {
		return klass;
	}

	void setKlass(Class<?> klass) {
		this.klass = klass;
	}

	Object getObject() {
		return object;
	}

	void setObject(Object object) {
		this.object = object;
	}

	Method getMethod() {
		return method;
	}

	void setMethod(Method method) {
		this.method = method;
	}

	@Override
	public String toString() {
		return "RMIBeanDefinition [klass=" + klass + ", object=" + object + ", method=" + method + "]";
	}
}

b. RMIBeanFactory

存放RMIBeanDefinition的工厂,因为有可能有许多实现类,所以是非单例的。

import java.util.HashMap;
import java.util.Map;

public class RMIBeanFactory {

	/**
	 * 该类最主要就是存放接口对应某个实现类里的所有方法,键为String类型的beanId,是method.toString.hashcode
	 */
	private Map<String, RMIBeanDefinition> beanMap;
	
	public RMIBeanFactory() {
		beanMap = new HashMap<>();
	}
	
	public Map<String, RMIBeanDefinition> getBeanPool() {
		return beanMap;
	}
	
	public void addBean(String beanId, RMIBeanDefinition rmiBeanDefinition) {
		beanMap.put(beanId, rmiBeanDefinition);
	}

	public RMIBeanDefinition getBean(String beanId) throws Exception {
		RMIBeanDefinition beanDefinition = beanMap.get(beanId);
		if (beanDefinition == null) {
			throw new Exception("未找到相对应的方法");
		}
		return beanDefinition;
	}
}

c. RMIBeanFactoryBuilder

工厂模式。根据扫描到的XML文件,建立相关的RMIBeanFactory。对于其中需要的XML解析小工具,可以点击这里

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.w3c.dom.Element;

import com.mec.util.XMLParse;

/**
 * 
 * 该类最主要就是扫描相关XML文件
 *
 */
public class RMIBeanFactoryBuilder {
	/**
	 * 键为接口某方法的接口名字,值为一个填充好的beanFactory
	 */
	private static final Map<String, RMIBeanFactory> factoryPool = new HashMap<>();
	
	/*因为但凡是实现接口的实现类,其会将接口所规定的方法实现
	 * 因此在扫描的时候,我们需要根据扫描的XML文件把接口和实现类的方法的BeanFactory映射关系填充好
	 * */
	public static void scanXMLConfig(String xmlPath) {
		
		new XMLParse() {
			
			@Override
			public void dealElement(Element element, int index) {
				String interfaceName = element.getAttribute("interfaceName");
				String className = element.getAttribute("className");
				RMIBeanFactory beanFactory = new RMIBeanFactory(); //每扫描到一个interface标签需要创建一个factory
				try {
					Class<?> interfaceKlass = Class.forName(interfaceName);
					Class<?> implementKlass = Class.forName(className);
					filterMethod(interfaceKlass, implementKlass, beanFactory);
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (NoSuchMethodException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					e.printStackTrace();
				}
				factoryPool.put(interfaceName, beanFactory);
			}
		}.parseTag(XMLParse.getDocument(xmlPath), "interface");
	}
	
	/*筛选方法
	 * 接口方法.getName = 实现类相关方法.getName
	 * 接口方法.getParameterTypes()=实现类相关方法的ParameterTypes
	 * 这两个条件就卡死了唯一的方法,就把映射关系完全填充好了!
	 * */
	private static void filterMethod(Class<?> interfClass,Class<?> implementKlass ,RMIBeanFactory beanFactory) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		Method[] iMethods = interfClass.getDeclaredMethods();
		for (Method iMethod : iMethods) {
			String methodHashCode = String.valueOf(iMethod.toString().hashCode());
			String methodName = iMethod.getName();
			Class<?>[] paraTypes = iMethod.getParameterTypes();
			Method implementMethod = implementKlass.getDeclaredMethod(methodName, paraTypes);
			RMIBeanDefinition beanDefinition = new RMIBeanDefinition();
			Constructor<?> noneConstructor = implementKlass.getConstructor(new Class<?>[] {});
			Object implementKlassObject = noneConstructor.newInstance(new Object[] {});
			beanDefinition.setKlass(implementKlass);
			beanDefinition.setObject(implementKlassObject);
			beanDefinition.setMethod(implementMethod);
			beanFactory.addBean(methodHashCode, beanDefinition);
		}
	}
	
	public static RMIBeanFactory getBeanFactory(String interfaceName) throws Exception {
		
		RMIBeanFactory beanFactory = factoryPool.get(interfaceName);
		if (beanFactory == null) {
			throw new Exception("没有找到相关接口的实现类,请检查相关XML文件");
		}
		return beanFactory;
	}
}

(三)RMI服务器端

a. RMIServer

对外提供的主要API,包括对服务器的设置,以及侦听客户端连接,启动RMIServerWorker去处理客户端连接请求。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class RMIServer implements Runnable {

	public static final int DEFAULT_PORT = 54189;
	private int port;
	private ServerSocket server;
	private volatile boolean goon;
	
	
	public RMIServer() {
		this.port = DEFAULT_PORT;
		this.goon = false;
	}
	
	public void setPort(int port) {
		this.port = port;
	}
		
	public void startup() throws IOException {
		if (this.goon == true) {
			System.out.println("服务器已经启动,请勿重复启动");	//如果为了以后输出多样性这里可以使用ISpeaker和IListener模式
			return;
		}
		this.server = new ServerSocket(this.port);
		this.goon = true;
		System.out.println("RMI服务器成功启动");
		new Thread(this).start();
	}

	public void shutdown() {
		if (this.goon == false) {
			System.out.println("服务器已经宕机,请勿重复宕机");
			return;
		}
		close();
	}
	
	@Override
	public void run() {
		System.out.println("服务器成功启动开始侦听客户端连接");
		while (goon) {
			try {
				Socket client = server.accept();
				// 收到了一个RMI客户端的连接,下面要处理接受客户端发送过来的有关方法信息
				// 如果一下全写到下面,我们的服务器就不纯粹了,我们的服务器仅仅只是侦听客户端连接的作用
				// 所以新写个类分离出去处理
				// 同时这里会遇到大量客户端连接,所以要用线程处理
				new RMIServerWorker(client);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	private void close() {
		this.goon = false;
		if (server != null && !server.isClosed()) {
			try {
				server.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				server = null;
			}
		}
	}
}

b. RMIServerWorker

  RMI服务器内部处理者,不对外提供,由服务器侦听到客户端请求自动去建立,其主要作用是处理客户端请求的内部逻辑。

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Socket;

import com.mec.util.ArgumentMaker;

public class RMIServerWorker implements Runnable{

	private Socket socket;
	private DataOutputStream dos;
	private DataInputStream dis;
	
	RMIServerWorker(Socket socket) {
		this.socket = socket;
		new Thread(this).start();
	}
	
	@Override
	public void run() {
		
		try {
			this.dis = new DataInputStream(socket.getInputStream());
			this.dos = new DataOutputStream(socket.getOutputStream());
			//开始接受来自客户端的有关将要执行的方法的众多信息
			// 查找相关方法,反射机制执行该方法,并将方法执行结果返回个客户端		
			String headerMessage = dis.readUTF();
			int colonIndex = headerMessage.indexOf(':'); 
			String interfaceName = headerMessage.substring(0, colonIndex);
			String beanId = headerMessage.substring(colonIndex + 1);
			RMIBeanFactory beanFactory;
			try {
				beanFactory = RMIBeanFactoryBuilder.getBeanFactory(interfaceName);
				RMIBeanDefinition beanDefinition = beanFactory.getBean(beanId);
				Object invokeObject = beanDefinition.getObject();
				Method invokeMethod = beanDefinition.getMethod();
				String argsString = dis.readUTF();
				Object[] paraValues = getArgsValue(argsString, invokeMethod);
				Object result = invokeMethod.invoke(invokeObject, paraValues);
				if (invokeMethod.getReturnType().equals(void.class)) {
					dos.writeUTF("null");
				} else {
					dos.writeUTF(ArgumentMaker.gson.toJson(result));
				}
			} catch (Exception e) {
				e.printStackTrace();
				dos.writeUTF("ERROR");
			} finally {//如果发生异常,客户端就在那边傻傻的等,就直接关闭通信信道
				close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private Object[] getArgsValue(String argsString, Method method) {
		int cnt = method.getParameterCount();
		if (cnt <= 0) {
			return new Object[] {};
		}
		Object[] result = new Object[cnt];
		ArgumentMaker argumentMaker = new ArgumentMaker(argsString);
		for (int i = 0; i < cnt; i++) {
			result[i] = argumentMaker.getArgumentByName("arg" + i, method.getParameters()[i].getParameterizedType());
		}
		return result;
	}
	
	private void close() {
		if (dis != null) {
			try {
				dis.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				dis = null;
			}
		}
		if (dos != null) {
			try {
				dos.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				dos = null;
			}
		}
		if (socket != null && !socket.isClosed()) {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				socket = null;
			}
		}
	}
}

(四)RMI客户端

a. RMIClient

  对外提供的主要API,只需要用户使用这个类得到相应的代理,便可以使用这个代理远端执行已经扫描填充好的方法。methodInvoker方法是内部的,里面有和服务器的连接,以及传递和接受对端信息。

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.Socket;
import java.net.UnknownHostException;

import com.mec.util.ArgumentMaker;

public class RMIClient {

	public static final String DEFAULT_RMI_SERVER_IP = "127.0.0.1";
	public static final int DEFAULT_RMI_SERVER_PORT = 54189;
	private String rmiServerIp;
	private int rmiServerPort;
	private Socket socket;
	private DataOutputStream dos;
	private DataInputStream dis;
	
	public RMIClient() {
		this.rmiServerIp = DEFAULT_RMI_SERVER_IP;
		this.rmiServerPort = DEFAULT_RMI_SERVER_PORT;
	}
	
	public String getRmiServerIp() {
		return rmiServerIp;
	}

	public void setRmiServerIp(String rmiServerIp) {
		this.rmiServerIp = rmiServerIp;
	}

	public int getRmiServerPort() {
		return rmiServerPort;
	}

	public void setRmiServerPort(int rmiServerPort) {
		this.rmiServerPort = rmiServerPort;
	}
	
	/*实际上是通过得到代理,才可以点那些接口方法,才可以使远端执行*/
	@SuppressWarnings("unchecked")
	public <T> T getRMIProxy(Class<?> klass) {
		if (!klass.isInterface()) {
			throw new RMIClientProxyMustBeInterfaceExeception("必须是接口");
			//从最开始的分析我们知道,客户端这边只有接口,如果提供的不是接口,说明错误了,我们给个自定义异常
		}
		ClassLoader classLoader = klass.getClassLoader();
		Class<?>[] interfaces = new Class<?> [] { klass };
		
		return (T) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
			
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				Object result = methodInvoker(method, args,klass); 
				return result;
			}
		});
	}
	
	/*客户端这边所谓执行方法,就是把相关方法信息和参数信息传递给服务器端,然后等着结果*/
	@SuppressWarnings("unchecked")
	private <T> T methodInvoker(Method method, Object[] args,Class<?> interfacer) throws UnknownHostException, IOException {
		this.socket = new Socket(rmiServerIp, rmiServerPort);
		this.dis = new DataInputStream(socket.getInputStream());
		this.dos = new DataOutputStream(socket.getOutputStream());
		
		dos.writeUTF(interfacer.getName() + ":" + String.valueOf(method.toString().hashCode())); //发送的内容就要改为接口名和方法的hashcode了
		dos.writeUTF(getArgsString(args)); 
		String strResult = dis.readUTF();
		if (strResult.equals("null")) {
			close();
			return null;
		} else if(strResult.equals("ERROR")) {
			System.out.println("方法执行异常!");
			close();
			return null;
		}
		Type returnType = method.getGenericReturnType();
		close();
		return (T) ArgumentMaker.gson.fromJson(strResult, returnType);//得到的结果根据返回值类型转化为相关对象
	}
	
	private String getArgsString(Object[] args) {
		ArgumentMaker argumentMaker = new ArgumentMaker();
		if(args == null) {
			return argumentMaker.toString();
		}
		for (int i = 0; i < args.length; i++) {	//在反射机制的情况下,我们已经丧失了方法的参数的形参的名字
			argumentMaker.add("arg" + i, args[i]);
		}
		
		return argumentMaker.toString();
	}
	
	private void close() {
		if (dis != null) {
			try {
				dis.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				dis = null;
			}
		}
		if (dos != null) {
			try {
				dos.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				dos = null;
			}
		}
		if (socket != null && !socket.isClosed()) {
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				socket = null;
			}
		}
	}
}

Ⅳ 后记和收获

(一)RMI的诡异及其犀利之处

  诡异:表面上我客户端只是调用个方法,而事实上调用的不是本地方法,而是调用服务器的方法,真实方法的执行是在服务器端执行的。这使得开发客户端只需要知道接口,通过这个接口产生相关代理,该方法执行的具体过程在遥远的服务器端执行。

  犀利之处:客户端操作完完全全是“黑箱”,只知道方法执行了,但不知道具体方法执行的过程,同时本地也没有该方法,只有接口该方法的名字一具空壳。服务器这边拥有真正的相关方法的操作,因此服务器这边可以尽最大可能的提高自己的性能,升级相关代码。对于接口的修改需要双方通知。

(二)收获

  1. 关于将功能分离出去,保持类的功能单一。例如RMIServer只是连接客户端功能,关于连接成功后的其他逻辑操作,分离出个新类(ServerWorker)去处理。
  2. 其落到最底层是对代理机制的使用和认识(代理机制)。
  3. 其中还使用到了自己编写十分强有力的工具Argumentmaker(ArgumentMaker)。
  4. 对于RMIBeanDefinition的成员是否设置为单例思考,在一个JVM下,如果有第二遍赋值,单例就不行了,单例需谨慎。
  5. 服务器侦听客户端那里可以使用线程池,因为来来回回大量客户端就是个(短平快)短连接请求,来回创建线程限制了服务器性能,因此可以使用线程池来提高效率。
  6. 有了RMI后,在网络通讯过程中,有多个客户机,客户机即就可以既做RMI客户端,也可以做RMI服务器,再用一个服务器来”管理“他们,该服务器就相当于一个相关服务(方法)管理员,所有客户端来整体合作完成同一个任务,以达到”云“的目的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 实现简单的RPC框架需要遵循以下步骤: 1. 定义接口:首先需要定义一个接口,其包含所有可远程调用的方法。 2. 服务实现服务端需要实现定义的接口,并通过服务端的程序来向客户端提供远程访问的服务。 3. 客户端实现:客户端需要消息代理,用于将请求发送到服务端并接收服务端的响应。 4. 网络通信:需要在服务端和客户端之间进行网络通信,以传递请求和响应。 Java提供了很多用于网络通信的工具,例如Java RMIJava Sockets,可以使用它们来实现RPC框架。 参考代码: ``` 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.ServerSocket; import java.net.Socket; public class RpcFramework { /** * 暴露服务 * * @param service 服务实现 * @param port 服务端口 * @throws Exception */ public static void export(final Object service, int port) throws Exception { if (service == null) throw new IllegalArgumentException("service instance == null"); if (port <= 0 || port > 65535) throw new IllegalArgumentException("Invalid port " + port); System.out.println("Export service " + service.getClass().getName() + " on port " + port); ServerSocket server = new ServerSocket(port); for (; ; ) { try { final Socket socket = server.accept(); new Thread(() -> { try { try { ObjectInputStream input = new ObjectInputStream(socket.getInputStream ### 回答2: 简单RPC(远程过程调用框架是用于实现分布式系统通信的技术。使用Java编程语言可以很方便地实现一个简单的RPC框架。下面是使用Java实现简单RPC框架的步骤: 1. 定义接口:首先,需要定义服务接口,描述需要远程调用的方法。接口定义了需要远程调用的方法以及方法的参数和返回值。 2. 实现接口:编写实现类,实现定义的接口。这个实现类通常是服务提供者的角色,用于处理并提供远程调用所需的服务。 3. 服务注册:创建一个服务注册心,用于服务提供者的信息。服务提供者在启动的时候,向注册心注册自己提供的服务。 4. 远程调用服务消费者需要进行远程调用时,首先需要到注册心查询可用的服务提供者,并选择其一个进行调用。 5. 网络通信:服务消费者通过网络与选择的服务提供者建立通信连接,发送请求,并等待响应。可以使用Java的Socket或HTTP等通信协议进行数据传输。 6. 序列化与反序列化:为了将Java对象在网络传输,需要将对象进行序列化成字节流进行传输,并在接收端将字节流反序列化为对象。可以使用Java提供的对象流或第三方库进行序列化和反序列化操作。 7. 执行远程方法:服务提供者接收到网络请求后,根据请求的方法名和参数,执行对应的方法,并将结果序列化后返回给服务消费者。 8. 异常处理:在远程调用过程可能会出现各种异常,需要在框架考虑异常的处理方式,比如将异常信息序列化后返回给调用方。 通过以上步骤,使用Java就可以实现一个简单的RPC框架。在实际应用,可以根据需求进行优化和扩展,比如支持负载均衡、服务发现、容错机制等,以满足分布式系统的需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值