开心一刻

  一实习小护士给我挂针,拿着针在我胳膊上扎了好几针也没找到血管

  但这位小姑娘真镇定啊,表情严肃认真,势有不扎到血管不罢休的意思

  十几针之后,我忍着剧痛,带着敬畏的表情问小护士:你这针法跟容嬷嬷学的么?

写在前面

  单机应用中的方法调用很简单,直接调用就行,像这样

  因为调用方与被调用方在一个进程内

  随着业务的发展,单机应用会越来越力不从心,势必会引入分布式来解决单机的问题,那么调用方如何调用另一台机器上的方法呢 ?

  这就涉及到分布式通信方式,从单机走向分布式,产生了很多通信方式

  而 RPC 就是实现远程方法调用的方式之一;说 RPC 不是协议,可能很多小伙伴难以置信,以为我在骗你们

  看着你们这一身腱子肉,我哪敢骗你们;只要你们把下面的看完,骗没骗你们,你们自己说了算

RPC 的演进过程

  先说明一下,下文中的示例虽然是 Java 代码实现的,但原理是通用的,重点是理解其中的原理

  第一版

    两台机器之间进行交互,那么肯定离不开网络通信协议,TCP / IP 也就成了绕不开的点,所以先辈们最初想到的方法就是通过 TCP / IP 来实现远程方法的调用

    而操作系统是没有直接暴露 TCP / IP 接口的,而是通过 Socket 抽象了 TCP / IP 接口,所以我们可以通过 Socket 来实现最初版的远程方法调用

    完整示例代码:rpc-01,核心代码如下

    Server:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4. import com.qsl.rpc.server.UserServiceImpl;
  5. import com.qsl.rpc.service.IUserService;
  6.  
  7. import java.io.DataInputStream;
  8. import java.io.DataOutputStream;
  9. import java.io.InputStream;
  10. import java.io.OutputStream;
  11. import java.net.ServerSocket;
  12. import java.net.Socket;
  13.  
  14. /**
  15. * @author 青石路
  16. * @date 2021/1/16 19:49
  17. */
  18. public class Server {
  19. private static boolean is_running = true;
  20.  
  21. public static void main(String[] args) throws Exception {
  22. ServerSocket serverSocket = new ServerSocket(8888);
  23. while (is_running) {
  24. System.out.println("等待 client 连接");
  25. Socket client = serverSocket.accept();
  26. System.out.println("获取到 client...");
  27. handle(client);
  28. client.close();
  29. }
  30. serverSocket.close();
  31. }
  32.  
  33. private static void handle(Socket client) throws Exception {
  34. InputStream in = client.getInputStream();
  35. OutputStream out = client.getOutputStream();
  36. DataInputStream dis = new DataInputStream(in);
  37. DataOutputStream dos = new DataOutputStream(out);
  38.  
  39. // 从 socket 读取参数
  40. int id = dis.readInt();
  41. System.out.println("id = " + id);
  42.  
  43. // 查询本地数据
  44. IUserService userService = new UserServiceImpl();
  45. User user = userService.getUserById(id);
  46.  
  47. // 往 socket 写响应值
  48. dos.writeInt(user.getId());
  49. dos.writeUTF(user.getName());
  50. dos.flush();
  51.  
  52. dis.close();
  53. dos.close();
  54. }
  55. }

    Client:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4.  
  5. import java.io.DataInputStream;
  6. import java.io.DataOutputStream;
  7. import java.net.Socket;
  8.  
  9. /**
  10. * @author 青石路
  11. * @date 2021/1/16 19:49
  12. */
  13. public class Client {
  14.  
  15. public static void main(String[] args) throws Exception {
  16. Socket s = new Socket("127.0.0.1", 8888);
  17.  
  18. // 网络传输数据
  19. // 往 socket 写请求参数
  20. DataOutputStream dos = new DataOutputStream(s.getOutputStream());
  21. dos.writeInt(18);
  22. // 从 socket 读响应值
  23. DataInputStream dis = new DataInputStream(s.getInputStream());
  24. int id = dis.readInt();
  25. String name = dis.readUTF();
  26. // 将响应值封装成 User 对象
  27. User user = new User(id, name);
  28. dos.close();
  29. dis.close();
  30. s.close();
  31.  
  32. // 进行业务处理
  33. System.out.println(user);
  34. }
  35. }

    代码很简单,就是一个简单的 Socket 通信;如果看不懂,那就需要去补充下 Socket 和 IO 的知识

    测试结果如下

    可以看到 Client 与 Server 之间是可以进行通信的;但是,这种方式非常麻烦,有太多缺点,最明显的一个就是

      Client 端业务代码 与 网络传输代码 混合在一起,没有明确的模块划分

      如果有多个开发者同时进行 Client 开发,那么他们都需要知道 Socket、IO

  第二版

    针对第一版的缺点,演进出了这一版,引进 Stub (早期的叫法,不用深究,理解成代理就行)实现 Client 端网络传输代码的封装

    完整示例代码:rpc-02,改动部分如下

    Stub:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4.  
  5. import java.io.DataInputStream;
  6. import java.io.DataOutputStream;
  7. import java.net.Socket;
  8.  
  9. /**
  10. * 相当于一个静态代理,封装了网络数据传输
  11. * @author 青石路
  12. * @date 2021/1/17 9:38
  13. */
  14. public class Stub {
  15.  
  16. public User getUserById(Integer id) throws Exception {
  17. Socket s = new Socket("127.0.0.1", 8888);
  18.  
  19. // 网络传输数据
  20. // 往 socket 写请求参数
  21. DataOutputStream dos = new DataOutputStream(s.getOutputStream());
  22. dos.writeInt(id);
  23. // 从 socket 读响应值
  24. DataInputStream dis = new DataInputStream(s.getInputStream());
  25. int userId = dis.readInt();
  26. String name = dis.readUTF();
  27. // 将响应值封装成 User 对象
  28. User user = new User(userId, name);
  29. dos.close();
  30. dis.close();
  31. s.close();
  32.  
  33. return user;
  34. }
  35. }

    Client:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4.  
  5. /**
  6. * @author 青石路
  7. * @date 2021/1/16 19:49
  8. */
  9. public class Client {
  10.  
  11. public static void main(String[] args) throws Exception {
  12.  
  13. // 不再关注网络传输
  14. Stub stub = new Stub();
  15. User user = stub.getUserById(18);
  16.  
  17. // 进行业务处理
  18. System.out.println(user);
  19. }
  20. }

    Client 不再关注网络数据传输,一心关注业务代码就好

    有小伙伴可能就杠上了:这不就是把网络传输代码移了个位置嘛,这也算改进?

    迭代开发是一个逐步完善的过程,而这也算是一个改进哦

    但这一版还是有很多缺点,最明显的一个就是

      Stub 只能代理 IUserService 的一个方法 getUserById ,局限性太大,不够通用

      如果想在 IUserService 新增一个方法: getUserByName ,那么需要在 Stub 中新增对应的方法,Server 端也需要做对应的修改来支持

  第三版

    第二版中的 Stub 代理功能太弱了,那有没有什么方式可以增强 Stub 的代理功能了?

    前面的 Stub 相当于是一个静态代理,所以功能有限,那静态代理的增强版是什么了,没错,就是:动态代理

    不熟悉动态代理的小伙伴,一定要先弄懂动态代理:设计模式之代理,手动实现动态代理,揭秘原理实现

    JDK 有动态代理的 API,我们就用它来实现

    完整示例代码:rpc-03,相较于第二版,改动比较大,大家需要仔细看

    Server:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4. import com.qsl.rpc.server.UserServiceImpl;
  5. import com.qsl.rpc.service.IUserService;
  6.  
  7. import java.io.InputStream;
  8. import java.io.ObjectInputStream;
  9. import java.io.ObjectOutputStream;
  10. import java.io.OutputStream;
  11. import java.lang.reflect.Method;
  12. import java.net.ServerSocket;
  13. import java.net.Socket;
  14.  
  15. /**
  16. * @author 青石路
  17. * @date 2021/1/16 19:49
  18. */
  19. public class Server {
  20. private static boolean is_running = true;
  21.  
  22. public static void main(String[] args) throws Exception {
  23. ServerSocket serverSocket = new ServerSocket(8888);
  24. while (is_running) {
  25. System.out.println("等待 client 连接");
  26. Socket client = serverSocket.accept();
  27. System.out.println("获取到 client...");
  28. handle(client);
  29. client.close();
  30. }
  31. serverSocket.close();
  32. }
  33.  
  34. private static void handle(Socket client) throws Exception {
  35. InputStream in = client.getInputStream();
  36. OutputStream out = client.getOutputStream();
  37. ObjectInputStream ois = new ObjectInputStream(in);
  38. ObjectOutputStream oos = new ObjectOutputStream(out);
  39.  
  40. // 获取方法名、方法的参数类型、方法的参数值
  41. String methodName = ois.readUTF();
  42. Class[] parameterTypes = (Class[]) ois.readObject();
  43. Object[] args = (Object[]) ois.readObject();
  44.  
  45. IUserService userService = new UserServiceImpl();
  46. Method method = userService.getClass().getMethod(methodName, parameterTypes);
  47. User user = (User) method.invoke(userService, args);
  48.  
  49. // 往 socket 写响应值;直接写可序列化对象(实现 Serializable 接口)
  50. oos.writeObject(user);
  51. oos.flush();
  52.  
  53. ois.close();
  54. oos.close();
  55. }
  56. }

    Stub:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4. import com.qsl.rpc.service.IUserService;
  5.  
  6. import java.io.ObjectInputStream;
  7. import java.io.ObjectOutputStream;
  8. import java.lang.reflect.InvocationHandler;
  9. import java.lang.reflect.Method;
  10. import java.lang.reflect.Proxy;
  11. import java.net.Socket;
  12.  
  13. /**
  14. * 动态代理,封装了网络数据传输
  15. * @author 青石路
  16. * @date 2021/1/17 9:38
  17. */
  18. public class Stub {
  19.  
  20. public static IUserService getStub() {
  21. Object obj = Proxy.newProxyInstance(IUserService.class.getClassLoader(),
  22. new Class[]{IUserService.class}, new NetInvocationHandler());
  23. return (IUserService)obj;
  24. }
  25.  
  26. static class NetInvocationHandler implements InvocationHandler {
  27.  
  28. /**
  29. *
  30. * @param proxy
  31. * @param method
  32. * @param args
  33. * @return
  34. * @throws Throwable
  35. */
  36. @Override
  37. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  38. Socket s = new Socket("127.0.0.1", 8888);
  39.  
  40. // 网络传输数据
  41. ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
  42. // 传输方法名、方法参数类型、方法参数值;可能会有方法重载,所以要传参数列表
  43. oos.writeUTF(method.getName());
  44. Class[] parameterTypes = method.getParameterTypes();
  45. oos.writeObject(parameterTypes);
  46. oos.writeObject(args);
  47.  
  48. // 从 socket 读响应值
  49. ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
  50. User user = (User) ois.readObject();
  51.  
  52. oos.close();
  53. ois.close();
  54. s.close();
  55.  
  56. return user;
  57. }
  58. }
  59. }

    Client:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.User;
  4. import com.qsl.rpc.service.IUserService;
  5.  
  6. /**
  7. * @author 青石路
  8. * @date 2021/1/16 19:49
  9. */
  10. public class Client {
  11.  
  12. public static void main(String[] args) throws Exception {
  13.  
  14. IUserService userService = Stub.getStub();
  15. //User user = userService.getUserById(23);
  16.  
  17. User user = userService.getUserByName("李小龙");
  18. // 进行业务处理
  19. System.out.println(user);
  20. }
  21. }

    我们来看下效果

    此时, IUserService 接口的方法都能被代理了,即使它新增接口, Stub 不用做任何修改也能代理上

    另外, Server 端的响应值改成了对象,而不是单个属性逐一返回,那么无论 User 是新增属性,还是删减属性,Client 和 Server 都不受影响了

    这一版的改进是非常大的进步;但还是存在比较明显的缺点

      只支持 IUserService ,通用性还是不够完美

      如果新引进了一个 IPersonService ,那怎么办 ?

  第四版

    第三版相当于 Client 与 Server 端约定好了,只进行 User 服务的交互,所以 User 之外的服务,两边是通信不上的

    如果还需要进行其他服务的交互,那么 Client 就需要将请求的服务名作为参数传递给 Server,告诉 Server 我需要和哪个服务进行交互

    所以,Client 和 Server 都需要进行改造

    完整示例代码:rpc-04,相较于第三版,改动比较小,相信大家都能看懂

    Server:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.server.PersonServiceImpl;
  4. import com.qsl.rpc.server.UserServiceImpl;
  5.  
  6. import java.io.InputStream;
  7. import java.io.ObjectInputStream;
  8. import java.io.ObjectOutputStream;
  9. import java.io.OutputStream;
  10. import java.lang.reflect.Method;
  11. import java.net.ServerSocket;
  12. import java.net.Socket;
  13. import java.util.HashMap;
  14.  
  15. /**
  16. * @author 青石路
  17. * @date 2021/1/16 19:49
  18. */
  19. public class Server {
  20. private static boolean is_running = true;
  21.  
  22. private static final HashMap<String, Object> REGISTRY_MAP = new HashMap();
  23.  
  24. public static void main(String[] args) throws Exception {
  25.  
  26. // 向注册中心注册服务
  27. REGISTRY_MAP.put("com.qsl.rpc.service.IUserService", new UserServiceImpl());
  28. REGISTRY_MAP.put("com.qsl.rpc.service.IPersonService", new PersonServiceImpl());
  29.  
  30. ServerSocket serverSocket = new ServerSocket(8888);
  31. while (is_running) {
  32. System.out.println("等待 client 连接");
  33. Socket client = serverSocket.accept();
  34. System.out.println("获取到 client...");
  35. handle(client);
  36. client.close();
  37. }
  38. serverSocket.close();
  39. }
  40.  
  41. private static void handle(Socket client) throws Exception {
  42. InputStream in = client.getInputStream();
  43. OutputStream out = client.getOutputStream();
  44. ObjectInputStream ois = new ObjectInputStream(in);
  45. ObjectOutputStream oos = new ObjectOutputStream(out);
  46.  
  47. // 获取服务名
  48. String serviceName = ois.readUTF();
  49. System.out.println("serviceName = " + serviceName);
  50.  
  51. // 获取方法名、方法的参数类型、方法的参数值
  52. String methodName = ois.readUTF();
  53. Class[] parameterTypes = (Class[]) ois.readObject();
  54. Object[] args = (Object[]) ois.readObject();
  55.  
  56. // 获取服务;从服务注册中心获取服务
  57. Object serverObject = REGISTRY_MAP.get(serviceName);
  58.  
  59. // 通过反射调用服务的方法
  60. Method method = serverObject.getClass().getMethod(methodName, parameterTypes);
  61. Object resp = method.invoke(serverObject, args);
  62.  
  63. // 往 socket 写响应值;直接写可序列化对象(实现 Serializable 接口)
  64. oos.writeObject(resp);
  65. oos.flush();
  66.  
  67. ois.close();
  68. oos.close();
  69. }
  70. }

    Stub:

  1. package com.qsl.rpc;
  2.  
  3. import java.io.ObjectInputStream;
  4. import java.io.ObjectOutputStream;
  5. import java.lang.reflect.InvocationHandler;
  6. import java.lang.reflect.Method;
  7. import java.lang.reflect.Proxy;
  8. import java.net.Socket;
  9.  
  10. /**
  11. * 动态代理,封装了网络数据传输
  12. * @author 青石路
  13. * @date 2021/1/17 9:38
  14. */
  15. public class Stub {
  16.  
  17. public static Object getStub(Class clazz) {
  18. Object obj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new NetInvocationHandler(clazz));
  19. return obj;
  20. }
  21.  
  22. static class NetInvocationHandler implements InvocationHandler {
  23.  
  24. private Class clazz;
  25.  
  26. NetInvocationHandler(Class clazz){
  27. this.clazz = clazz;
  28. }
  29.  
  30. @Override
  31. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  32. Socket s = new Socket("127.0.0.1", 8888);
  33.  
  34. // 网络传输数据
  35. ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
  36.  
  37. // 传输接口名,告诉服务端,我要哪个服务
  38. oos.writeUTF(clazz.getName());
  39.  
  40. // 传输方法名、方法参数类型、方法参数值;可能会有方法重载,所以要传参数列表
  41. oos.writeUTF(method.getName());
  42. Class[] parameterTypes = method.getParameterTypes();
  43. oos.writeObject(parameterTypes);
  44. oos.writeObject(args);
  45.  
  46. // 从 socket 读响应值
  47. ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
  48. Object resp = ois.readObject();
  49.  
  50. oos.close();
  51. ois.close();
  52. s.close();
  53.  
  54. return resp;
  55. }
  56. }
  57. }

    Client:

  1. package com.qsl.rpc;
  2.  
  3. import com.qsl.rpc.entity.Person;
  4. import com.qsl.rpc.entity.User;
  5. import com.qsl.rpc.service.IPersonService;
  6. import com.qsl.rpc.service.IUserService;
  7.  
  8. /**
  9. * @author 青石路
  10. * @date 2021/1/16 19:49
  11. */
  12. public class Client {
  13.  
  14. public static void main(String[] args) throws Exception {
  15.  
  16. /*IUserService userService = (IUserService)Stub.getStub(IUserService.class);
  17. User user = userService.getUserByName("青石路");
  18. System.out.println(user);*/
  19.  
  20. IPersonService personService = (IPersonService)Stub.getStub(IPersonService.class);
  21. Person person = personService.getPersonByPhoneNumber("123");
  22. System.out.println(person);
  23. }
  24. }

    此版本抽象的比较好了,屏蔽了底层细节,支持任何服务的任意方法,算是一个比较完美的版本了

    至此,一个最基础的 RPC 就已经实现了

    但是,还是有大量的细节可以改善,序列化与反序列化就是其中之一

      网络中数据的传输都是二进制,所以请求参数需要序列化成二进制,响应参数需要反序列化成对象

      而 JDK 自带的序列化与反序列化,具有语言局限性、效率慢、序列化后的长度太长等缺点

    序列化与反序列化协议非常多,常见的有

    

    这些协议孰好孰坏,本文不做过多阐述,这里提出来只是想告诉大家:序列化与反序列化协议是 RPC 中的重要一环

总结

  1、RPC 的演进过程

  2、RPC 的组成要素

    三要素:动态代理、序列化与反序列化协议、网络通信协议

    网络通信协议可以是 TCP、UDP,也可以是 HTTP 1.x、HTTP 2,甚至有能力可以是自定义协议

  3、RPC 框架

    RPC 不等同于 RPC 框架,RPC 是一个概念,是一个分布式通信方式

    基于 RPC 产生了很多 RPC 框架:Dubbo、Netty、gRPC、BRPC、Thrift、JSON-RPC 等等

    RPC 框架对 RPC 进行了功能丰富,包括:服务注册、服务发现、服务治理、服务监控、服务负载均衡等功能

  现在回到标题:RPC 是通信协议吗 ?  欢迎评论区留言

参考

  36行代码透彻解析RPC

  序列化和反序列化

  你应该知道的RPC原理

RPC 是通信协议吗 ?→ 我们来看下它的演进过程的更多相关文章

  1. (4)一起来看下mybatis框架的缓存原理吧

    本文是作者原创,版权归作者所有.若要转载,请注明出处.本文只贴我觉得比较重要的源码,其他不重要非关键的就不贴了 我们知道.使用缓存可以更快的获取数据,避免频繁直接查询数据库,节省资源. MyBatis ...

  2. Windows操作系统下创建进程的过程

    进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位.程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体.而进程则 ...

  3. Linux环境下Python的安装过程

    Linux环境下Python的安装过程 前言 一般情况下,Linux都会预装 Python了,但是这个预装的Python版本一般都非常低,很多 Python的新特性都没有,必须重新安装新一点的版本,从 ...

  4. MySQL5.7.25(解压版)Windows下详细的安装过程

    大家好,我是浅墨竹染,以下是MySQL5.7.25(解压版)Windows下详细的安装过程 1.首先下载MySQL 推荐去官网上下载MySQL,如果不想找,那么下面就是: Windows32位地址:点 ...

  5. Linux系统CentOS6.2版本下安装JDK7详细过程

    Linux系统CentOS6.2版本下安装JDK7详细过程 分类: Linux 2014-08-25 09:17 1933人阅读 评论(0) 收藏 举报 前言:        java 是一种可以撰写 ...

  6. [oracle] oracle的三种密码验证机制以及在windows和linux下的不同启动过程

    oracle数据库的密码验证机制: ① 操作系统验证 拥有SYSDBA和SYSOPER的用户用该方式验证此时数据库无需启动,也无需开启监听和实例服务. 要求:本地组ora_dba中有该操作系统的登录用 ...

  7. Windows下OSGEarth的编译过程

    目录 1. 依赖 1) OpenSceneGraph 2) GDAL 3) CURL 4) GEOS 5) 其他 2. 编译 1) 设置参数 2) 配置路径 3) 生成编译 3. 参考文献 1. 依赖 ...

  8. Day28--Python--网络通信协议 tcp与udp下的socket

    昨日内容回顾: 1. CS架构 服务端客户端架构 软件CS架构: 京东,淘宝,QQ,微信,暴风影音,快播 硬件CS架构: 打印机 服务端: 提供服务的 客户端: 享受服务的 BS架构: 浏览器和服务端 ...

  9. 【已解决】checkout 配置无效的问题可以进来看下

    在日常工作中,我们经常会遇到要更新一个项目,但是由于更改了配置,需要将这些配置commit或者checkout,但是有的同学不想commit怎么办呢,只能通过checkout,那么问题又来了,改了很多 ...

随机推荐

  1. springboot中使用h2数据库(内存模式)

    使用H2的优点,不需要装有服务端和客户端,在项目中包含一个jar即可,加上初始化的SQL就可以使用数据库了 在springboot中引入,我的版本是2.1.4,里面就包含有h2的版本控制 <!- ...

  2. tornado 作业 简单首页 登录页 个人中心

    s4 index.py 1 import tornado.ioloop 2 import tornado.web 3 import time 4 5 6 class IndexHandler(torn ...

  3. vmvare workstation虚拟机连接外网

    在使用网上的yum源的时候,我们就需要我们的虚拟机能连接外网,在这里记录下配置vmvare workstation虚拟机连接外网的方法. 配置步骤: 1.打开主机的 "网络和Internet ...

  4. python 的基本语法

    python3 默认的编码格式 :utf-8 标识符: 1.可以是数字,字母,下划线组成 2.不能以数字开头 3.不能以保留字命名(保留字即是关键字,比如"print") 4.区分 ...

  5. js下 Day09、事件(二)

    一.事件流 事件流描述的是从页面中接收事件的顺序,目前主要有三个模型: #1. 事件冒泡: 事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的元素

  6. Spark内核-任务调度机制

    作者:十一喵先森 链接:https://juejin.im/post/5e1c414fe51d451cad4111d1 来源:掘金 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. ...

  7. 算法(Java实现)—— KMP算法

    KMP算法 应用场景 字符串匹配问题 有一个字符串str1 = " hello hello llo hhello lloh helo" 一个子串str2 = "hello ...

  8. openstack高可用集群20-openstack计算节点宕机迁移方案

    openstack计算节点宕机迁移方案   情景一:/var/lib/nova/instances/ 目录不共享的处理方法(类似手动迁移云主机到其他节点)

  9. 【python爬虫】一个简单的爬取百家号文章的小爬虫

    需求 用"老龄智能"在百度百家号中搜索文章,爬取文章内容和相关信息. 观察网页 红色框框的地方可以选择资讯来源,我这里选择的是百家号,因为百家号聚合了来自多个平台的新闻报道.首先看 ...

  10. pygal之掷骰子 - 2颗面数为6的骰子

    python之使用pygal模拟掷两颗面数为6的骰子的直方图,包含三个文件,主文件,die.py,dice_visual.py,20200527.svg.其中最后一个文件为程序运行得到的结果. 1,d ...