BootStrap

Netty 中的 BootStrap 分为两种:一种是客户端的 BootStrap;一种是服务端的 ServerBootStrap

  • 客户端的 BootStrap

    初始化客户端,该 BootStrap 只有一个 EventLoopGroup,用于连接远程主机

  • 服务端的 ServerBootStrap

    用于绑定本地端口,一般存在两个 EventLoopGroup(一个用于包含单例的 ServerChannel,用于接收和分发请求;另一个是包含了所有创建的 Channel,处理服务器接受到的所有来自客户端的连接)。

启动流程

BootStrap 的启动流程(以 ServerBootStrap 为例):

具体的代码如下所示:

  1. NioEventLoopGroup group = new NioEventLoopGroup(); // 创建一个 NioEventLoopGroup
  2. ServerBootstrap bootstrap = new ServerBootstrap();
  3. bootstrap.group(group)
  4. .channel(NioServerSocketChannel.class) // 设置 Channel 的类型为 NioServerSocketChannel
  5. .localAddress(new InetSocketAddress(9098)) // 监听本地的 9098 端口
  6. .option(ChannelOption.TCP_NODELAY, true) // 设置启用 Nagle 算法
  7. .option(ChannelOption.SO_BACKLOG, 1024) // 设置最大等待连接队列长度
  8. .childHandler(new ChannelInitializer<SocketChannel>() {
  9. @Override
  10. protected void initChannel(SocketChannel ch) throws Exception {
  11. // 对当前的 Channel 中的 pieline 添加一系列的 Handler
  12. ch.pipeline().addLast(new ProtobufDecoder(null));
  13. ch.pipeline().addLast(new ProtobufEncoder());
  14. }
  15. });

Channel

以客户端连接到服务端为例

客户端 Channel 的初始化过程

  1. 通过 BootStrap 实例对象设置对应的 Channel的类型,即上文的 bootStrap.channel(NioSocketChannel.class) 就是将当前的 BootStrapChannel 类型设置为 NioSocketChannel。这个过程只是指定了 Channel 的类型,同时设置了对应的 ChannelFactory,并没有真正实例化 Channel

  2. 通过调用 Bootstrapconnect()方法完成 Channel 的实例化。具体调用链(在 Boostrap):connect() ——> doResolveAndConnect() ——> initAndRegister() ——> channelFactory.newChannel() 完成 Channel 的初始化工作

  3. Channel 的初始化过程中,首先会触发 NioSocketChannel 的构造器,使用默认的 selector 来创建一个 NioSocketChannel,然后依次调用父类的方法进行一系列的初始化操作。

实例化的具体流程如下图所示:

服务端 Channel 的初始化过程

服务端的 Channel 初始化与客户端的 Channel 类似,不同的地方在于服务端的 Channel 是绑定到对应的主机和端口,而 客户端的 Channnel 是需要连接到服务器的 Channel。

客户端 Channel 的注册过程

在调用 BootStrapbind() 方法的过程中,会调用到 initRegister() 方法,在这个方法中会对 Channel 进行初始化;在调用 register() 方法之后,会再次调用 EventLoopGroup 对象的 register(Channel channel) 方法将 Channel 注册到对应的 EventLoopGroup 中。具体源代码如下所示:

  1. // 移除了处理异常等不相关的代码
  2. final ChannelFuture initAndRegister() {
  3. Channel channel = null;
  4. // 使用对应的 ChannelFactory 对象创建一个 Channel
  5. channel = channelFactory.newChannel();
  6. init(channel);
  7. // 将 Channel 注册到对应的 EventLoopGroup 中
  8. ChannelFuture regFuture = config().group().register(channel);
  9. return regFuture;
  10. }

调用 EventLoopGroupregister(Channel channel) 方法会调用 AbstractUnsaferegister(EventLoop eventLoop, ChannelPromise promise) 方法来进行注册。具体源代码如下所示:

  1. public final void register(EventLoop eventLoop, final ChannelPromise promise) {
  2. // 省略一部分检查的代码
  3. AbstractChannel.this.eventLoop = eventLoop;
  4. if (eventLoop.inEventLoop()) {
  5. register0(promise);
  6. }
  7. // 省略一部分代码。。。
  8. }

register0 源代码如下所示:

  1. private void register0(ChannelPromise promise) {
  2. // 省略一些无用的代码
  3. doRegister();
  4. // 省略一些无用的代码
  5. }

由于当前使用的是 NioSocketChannel,因此会调用到 AbstractNioChanneldoRegister() 方法,具体的源代码如下所示:

  1. protected void doRegister() throws Exception {
  2. boolean selected = false;
  3. for (;;) {
  4. try {
  5. /*
  6. javaChannel() :由于这里注册的 Channel 是 NioSocketChannel,因此会得到当前正要注册的 NioSocketChannel
  7. 通过调用 Channel 的 register() 方法,将这个 Channel 注册到对应的 Selector 中,这里的 selector 对应着 NioEventLoop,即分配到的 EventLoop 对象,到此 Channel 的注册完成
  8. */
  9. selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
  10. return;
  11. } catch (CancelledKeyException e) {
  12. // 省略一部分异常捕获的代码
  13. }
  14. }
  15. }

总的流程图如下所示:

服务端 Channel 的注册过程

同样地,服务端的 Channel 的注册和客户端的也十分类似,不同的地方在于服务端的 Channel 会注册到“主” EventLoopGroup ,而客户端的 Channel 则只是注册到一个普通的 EventLoopGroup

客户端 Channel 的连接过程

服务端 Channel 接受连接的过程

服务端的 Channel 在启动时会首先创建一个 NioServerSocketChannel 并注册到 “主” EventLoopGroup,用于创建对应的处理对应的连接请求。处理连接的请求在初始化 NioServerSocketChannel 的时候就已经准备好了,初始化服务端的 Channel 的源代码如下所示:

  1. // 该代码位于 ServerBootStrap
  2. @Override
  3. void init(Channel channel) {
  4. // 获取当前 Channel 的 Pipeline
  5. ChannelPipeline p = channel.pipeline();
  6. // 这里的 childGroup 是 “从”—EvenLoopGroup,用于真正处理请求
  7. final EventLoopGroup currentChildGroup = childGroup;
  8. final ChannelHandler currentChildHandler = childHandler;
  9. final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
  10. final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
  11. p.addLast(new ChannelInitializer<Channel>() {
  12. @Override
  13. public void initChannel(final Channel ch) {
  14. final ChannelPipeline pipeline = ch.pipeline();
  15. ChannelHandler handler = config.handler();
  16. // 将在 bootStrap 中添加的 ChannelHandler 对象添加到 Pipeline 的末尾
  17. if (handler != null) {
  18. pipeline.addLast(handler);
  19. }
  20. ch.eventLoop().execute(new Runnable() {
  21. @Override
  22. public void run() {
  23. // ServerBootstrapAcceptor 中重写了 channelRead() 方法,每个连接的请求都会首先调用这个方法以读取请求的内容
  24. pipeline.addLast(new ServerBootstrapAcceptor(
  25. ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
  26. }
  27. });
  28. }
  29. });
  30. }

ServerBootstrapAcceptorchannelRead() 的源代码如下所示:

  1. @Override
  2. @SuppressWarnings("unchecked")
  3. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  4. final Channel child = (Channel) msg;
  5. // childHandler 由 ServerBootStrap 对象进行指定
  6. child.pipeline().addLast(childHandler);
  7. /*
  8. 注意这里的 childGroup 对应的是 “从”—EventLoopGroup,由于 “从”—EventLoopGroup 是处理请求的,
  9. 因此在这里得到的请求(即 msg)会为它新创建一个 Channel 注册到“从”—EventLoopGroup 的某个 EventLoop 中
  10. 这个请求转发到 “从”—EventLoopGroup 的工作是由 “主”—EventLoopGroup 来完成的
  11. */
  12. childGroup.register(child).addListener();
  13. // 省略部分异常检查代码。。。。
  14. }

channelRead() 的工作已经介绍完了,现在问题是 “主”—EventLoopGroup 是如何处理请求的了。这个和使用 NIO 进行网络编程有关系,当绑定当前的一个 NioServerSocketChannel时,Netty 的底层会监听对应的事件。当 NioServerSocketChannel 的状态为 SelectionKey.OP_ACCEPT时,表示当前的 NioServerSocketChannel 是可以接收连接请求的,当一个请求到达时,便会执行对应的请求,NioServerSocketChannel 执行对应请求的源代码如下所示:

  1. /*
  2. 因为每个 EventLoop 都是通过单个线程的方式来处理对应的任务的,同样地,NioServerSocketChannel 也是通过 EventLoop 来进行任务处理的
  3. 该方法位于 NioEventLoop 中,因为初始化 NioServerSocketChannel 时指定了 NioEventLoop 来处理每个任务
  4. */
  5. @Override
  6. protected void run() {
  7. for (;;) {
  8. // 省略一大段异常处理和其它不是很关键的代码
  9. processSelectedKeys();
  10. }
  11. }

processSelectedKeys 源代码如下所示:

  1. private void processSelectedKeys() {
  2. if (selectedKeys != null) {
  3. processSelectedKeysOptimized();
  4. } else {
  5. processSelectedKeysPlain(selector.selectedKeys());
  6. }
  7. }

尽管存在一个判断条件,但实际上,由于当前处理的 Channel 是 NioServerSocketChannel,最终都会调用 processSelectedKey 方法进行进一步的处理。具体的源代码如下所示:

  1. private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
  2. // 省略一大段异常检查的代码
  3. int readyOps = k.readyOps();
  4. // 这里就是 NioServerSocketChannel 在可以读取时要进行处理的代码块。当当前的 Channel 是可读的或者是可接收请求是则执行对应的逻辑
  5. if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
  6. /*
  7. 这里的 Unsafe 对象由 NioServerSocketChannel 在实例化时创建,
  8. 具体由 AbstractNioMessageChannel 通过重写 newSafe() 方法来实现
  9. 这里的 Unsafe 类为 AbstractNioMessageChannel 的内部类
  10. */
  11. unsafe.read();
  12. }
  13. }

unsafe.read()方法的源代码:

  1. @Override
  2. public void read() {
  3. // 核心代码如下,已经省略大部分其它不太重要的代码
  4. do {
  5. /*
  6. doReadMessages 由具体的子类来实现,在这里具体的子类为 NioServerSocketChannel
  7. 每次调用 doReadMessages 都将创建一个新的 SocketChannel,即一个新的连接放入 readBuf 列表中
  8. */
  9. int localRead = doReadMessages(readBuf);
  10. if (localRead == 0) {
  11. break;
  12. }
  13. if (localRead < 0) {
  14. closed = true;
  15. break;
  16. }
  17. allocHandle.incMessagesRead(localRead);
  18. } while (continueReading(allocHandle));
  19. int size = readBuf.size();
  20. // 将读取到的数据进行相应的处理
  21. for (int i = 0; i < size; i ++) {
  22. /*
  23. 由于只有一个服务线程处理请求数据,因此就不会因为同时有多个请求进行访问而造成数据混乱
  24. */
  25. readPending = false;
  26. pipeline.fireChannelRead(readBuf.get(i));
  27. }
  28. readBuf.clear();
  29. allocHandle.readComplete();
  30. pipeline.fireChannelReadComplete();
  31. }

doReadMessages() 方法源代码如下所示:

  1. @Override
  2. protected int doReadMessages(List<Object> buf) throws Exception {
  3. // 根据当前的 NioServerSocketChannel 创建一个新的 SocketChannel
  4. SocketChannel ch = SocketUtils.accept(javaChannel());
  5. if (ch != null) {
  6. // 可以看到,每次读取时都会为当前读取的数据段分配一个新的 NioSocketChannel 来进行相应的任务处理
  7. buf.add(new NioSocketChannel(this, ch)); // 新分配的 NioSocketChnnel 的
  8. return 1;
  9. }
  10. // 省略部分异常检测代码
  11. return 0;
  12. }

至此,NioServerSocketChannel 是如何分发请求到 NioSocketChannel 就已经非常清楚了

接收请求的具体流程如下所示:

ChannelPipeline

ChannelPipelineChannelHandler 链的容器,用于存储一系列的 ChannelHandler

每当一个新的 Channel 被创建了,都会建立一个新的 ChannelPipeline,并且这个 ChannelPipeline 会绑定到 Channel 上。具体对应关系如下图所示:

ChannelPipeline 是双向的,但是同一时刻只能有一个方向的任务进行,如下图所示:

当一个入站事件被触发时,将会按照 ChannelInboundHandler 的顺序进行对应的处理。Netty 一般会认为入站的开始位置为 ChannelPiepeline 的开始位置,及上图上 SocketChannelChannelPipeline 的开始位置,因此在添加对应的 ChannelHandler 时需要注意这一点。

ChannelPipeline 的初始化

初始化 ChannelPipeline 的源代码如下所示:

  1. /*
  2. 这里的 Channel 是之前初始化的 NioServerSocketChannel
  3. */
  4. protected DefaultChannelPipeline(Channel channel) {
  5. this.channel = ObjectUtil.checkNotNull(channel, "channel");
  6. succeededFuture = new SucceededChannelFuture(channel, null);
  7. voidPromise = new VoidChannelPromise(channel, true);
  8. tail = new TailContext(this); // 上问 ChannelPipeline 示意图中的 head 节点,这是一个哑节点
  9. head = new HeadContext(this); // Pipeline 中的尾节点,同样也是一个哑节点
  10. // 组成一个双向链表,注意上文提到一个 Pipeline 可以是双向的,因此这里的数据结构采用双向链表
  11. head.next = tail;
  12. tail.prev = head;
  13. }

即初始化 ChannelPipeline 时,会维护一个由 ChannelHandlerContext 组成的双向链表,这个链表的作用是对相应的 Channel 进行i相关的事件处理。

ChannelHandler

服务端的 ChannelHandler

由于服务端存在两种类型的 EventLoopGroup,一个用于接收和分发请求,一个用于真正处理请求,因此 ChannelHandler 也分为两种,一类是用于接收请求时使用,一类是用于处理请求时使用。

ServerBootStrap通过 handler(ChannelHandler handler) 方法来指定接收请求是要执行的 ChannelHandler,这里的ChannelHandler添加是发生在初始化 Channel 的过程中(注意是初始化 Channel 而不是实例化 Channel)。具体的源代码如下所示:

  1. @Override
  2. void init(Channel channel) { // 这里的 Channel 是实例化之后的 Channel,即 NioServerSocketChannel
  3. ChannelPipeline p = channel.pipeline();
  4. final EventLoopGroup currentChildGroup = childGroup;
  5. final ChannelHandler currentChildHandler = childHandler;
  6. final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
  7. final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
  8. p.addLast(new ChannelInitializer<Channel>() {
  9. @Override
  10. public void initChannel(final Channel ch) {
  11. final ChannelPipeline pipeline = ch.pipeline();
  12. // 这里的 handler 是通过 ServerBootStrap 调用 handler(...) 来指定的
  13. ChannelHandler handler = config.handler();
  14. if (handler != null) {
  15. pipeline.addLast(handler);
  16. }
  17. ch.eventLoop().execute(new Runnable() {
  18. @Override
  19. public void run() {
  20. pipeline.addLast(new ServerBootstrapAcceptor(
  21. ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
  22. }
  23. });
  24. }
  25. });
  26. }

初始化之后,服务端 NioServerSocketChannel 的 Pipeline 的底层 Handler 结构如下图所示:

当接收到一个新的客户端请求时,会调用 ServerBootstrapAcceptor.channelRead 方法,具体源代码如下图所示:

  1. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  2. // 这里的 child 是分配到 workGroup 中的一个 Channel,不是服务端处理连接的 NioServerSocketChannel
  3. final Channel child = (Channel) msg;
  4. // 这里的 chilHandler 是在 ServerBootStrap 对象中通过 childHandler() 方法设置的
  5. child.pipeline().addLast(childHandler);
  6. // 省略一部分不重要的代码
  7. }

EventLoop

EventLoop 的初始化

NioEvenLoopGroup有几个构造器,但是最终都是调用父类 MultithreadEventLoopGroup 的构造器

调用父类的 MultithreadEventLoopGroup 构造器初始化:

  1. private static final int DEFAULT_EVENT_LOOP_THREADS;
  2. static {
  3. // 初始化 EventLoop 线程数,这里设置的线程数为处理器的数量 * 2
  4. DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
  5. "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
  6. if (logger.isDebugEnabled()) {
  7. logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
  8. }
  9. }
  10. // 确定 EventLoop 的线程数,再交给父类进行构造
  11. protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
  12. super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
  13. }

再次调用父类的构造函数进行初始化:

  1. protected MultithreadEventExecutorGroup(int nThreads,
  2. Executor executor,
  3. EventExecutorChooserFactory chooserFactory,
  4. Object... args) {
  5. if (executor == null) {
  6. executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
  7. }
  8. children = new EventExecutor[nThreads];
  9. for (int i = 0; i < nThreads; i ++) {
  10. children[i] = newChild(executor, args);
  11. }
  12. chooser = chooserFactory.newChooser(children);
  13. }

主要任务:

  • 创建一个大小为 nThreadsEventExecutor 数组

  • 通过调用 newChild 来初始化 children 数组的每个元素

  • 根据 nThreads 的大小,创建不同的 EventExecutorChooser,如果 nThreads 是 2 的整数幂,则使用 PowerOfTwoEventExecutorChooser,否则,使用 GenericEventExecutorChooser。它们的功能够一样,都是从 children 数组中选出一个合适的 EventExecutor 实例

    源代码如下:

    1. public EventExecutorChooser newChooser(EventExecutor[] executors) {
    2. if (isPowerOfTwo(executors.length)) {
    3. return new PowerOfTwoEventExecutorChooser(executors);
    4. } else {
    5. return new GenericEventExecutorChooser(executors);
    6. }
    7. }

总的初始化流程:

  • EventLoopGroup(实际上是 MultithreadEventExecutorGroup)内部维护了一个类型为 EventExecutor 的 children 数组,其大小为 nThreads,这样就创建了一个线程池
  • MultithreadEventLoopGroup 中会确定要选择的 EventLoop 线程数,默认为可用的处理器大小 * 2
  • MultithreadEventExecutorGroup 会调用 newChild 方法来初始化 children 数组的每个元素

具体的流程如下所示:

Netty 组件介绍的更多相关文章

  1. Netty组件介绍(转)

    http://www.tuicool.com/articles/mEJvYb 为了更好的理解和进一步深入Netty,我们先总体认识一下Netty用到的组件及它们在整个Netty架构中是怎么协调工作的. ...

  2. Netty快速入门(09)channel组件介绍

    书接上回,继续介绍组件. ChannelHandler组件介绍 ChannelHandler组件包含了业务处理核心逻辑,是由用户自定义的内容,开发人员百分之九十的代码都是ChannelHandler. ...

  3. 从netty-example分析Netty组件续

    上文我们从netty-example的Discard服务器端示例分析了netty的组件,今天我们从另一个简单的示例Echo客户端分析一下上个示例中没有出现的netty组件. 1. 服务端的连接处理,读 ...

  4. 开源免费且稳定实用的.NET PDF打印组件itextSharp(.NET组件介绍之八)

    在这个.NET组件的介绍系列中,受到了很多园友的支持,一些园友(如:数据之巅. [秦时明月]等等这些大神 )也给我提出了对应的建议,我正在努力去改正,有不足之处还望大家多多包涵.在传播一些简单的知识的 ...

  5. 免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)

    前面介绍了六种.NET组件,其中有一种组件是写文件的压缩和解压,现在介绍另一种文件的解压缩组件SharpZipLib.在这个组件介绍系列中,只为简单的介绍组件的背景和简单的应用,读者在阅读时可以结合官 ...

  6. 免费高效实用的.NET操作Excel组件NPOI(.NET组件介绍之六)

    很多的软件项目几乎都包含着对文档的操作,前面已经介绍过两款操作文档的组件,现在介绍一款文档操作的组件NPOI. NPOI可以生成没有安装在您的服务器上的Microsoft Office套件的Excel ...

  7. 免费开源的DotNet任务调度组件Quartz.NET(.NET组件介绍之五)

    很多的软件项目中都会使用到定时任务.定时轮询数据库同步,定时邮件通知等功能..NET Framework具有“内置”定时器功能,通过System.Timers.Timer类.在使用Timer类需要面对 ...

  8. 免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)

    在生活中有一种东西几乎已经快要成为我们的另一个电子”身份证“,那就是二维码.无论是在软件开发的过程中,还是在普通用户的日常中,几乎都离不开二维码.二维码 (dimensional barcode) , ...

  9. 最好的.NET开源免费ZIP库DotNetZip(.NET组件介绍之三)

    在项目开发中,除了对数据的展示更多的就是对文件的相关操作,例如文件的创建和删除,以及文件的压缩和解压.文件压缩的好处有很多,主要就是在文件传输的方面,文件压缩的好处就不需要赘述,因为无论是开发者,还是 ...

  10. 高效而稳定的企业级.NET Office 组件Spire(.NET组件介绍之二)

    在项目开发中,尤其是企业的业务系统中,对文档的操作是非常多的,有时几乎给人一种错觉的是”这个系统似乎就是专门操作文档的“.毕竟现在的很多办公中大都是在PC端操作文档等软件,在这些庞大而繁重的业务中,单 ...

随机推荐

  1. spark修改控制台输出日志级别

    spark修改控制台输出日志级别 修改conf/log4j.properties cd $SPARK_HOME/conf cp log4j.properties.template ./log4j.pr ...

  2. 微服务使用openfeign调用单点的会话失效问题

    项目Springcloud,认证中心方式实现SSO使用开源框架Sa-Token 本身的单独访问每个客户端服务的单点就没有问题.然后单点通过Fegin调用就不好使了! 主要使用的Sa-Token的微服务 ...

  3. slate源码解析(三)- 定位

    接口定义 能够对于文字.段落乃至任何元素的精准定位 并做出增删改查,都是在开发一款富文本编辑器时一项最基本也是最重要的功能之一.让我们先来看看Slate中对于如何在文档树中定位元素是怎么定义的[源码] ...

  4. 使用Java统计gitlab代码行数

    一.背景: 需要对当前公司所有的项目进行代码行数的统计 二. 可实现方式 1.脚本:通过git脚本将所有的项目拉下来并然后通过进行代码行数的统计 样例: echo 创建项目对应的文件夹 mkdir 项 ...

  5. 10月TIOBE榜Java跌出前三!要不我转回C#吧

    前言 Java又要完了,又要没了,你没看错,10月编程语言榜单出炉,Java跌出前三,并且即将被C#超越,很多资深人士预测只需两个月,Java就会跌出前五. 看到这样的文章,作为一名Java工程师我感 ...

  6. CF1352D

    题目简化和分析: 这题可以直接按照题意进行模拟,当然有些细节需要注意. 翻译的不足:这里的回合指任意一个人吃掉都算,而不是双方一个回合,最后一个人即使不满足也算一个回合. 我们可以采用两个指针模拟两个 ...

  7. 全局关闭Unity编译的CS警告

    实现方式 Editor和Game的全局CSharp编译配置文件名: Assets/mcs.rsp 添加如下内容可屏蔽对应的警告信息 -nowarn:1234 常用内容 CS0219 未使用的publi ...

  8. C# -WebAPIOperator.cs

    说明:一个用C#编写的WebAPI操作类,只写了Get Post 部分. using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System ...

  9. 【scipy 基础】--积分和微分方程

    对于手工计算来说,积分计算是非常困难的,对于一些简单的函数,我们可以直接通过已知的积分公式来求解,但在更多的情况下,原函数并没有简单的表达式,因此确定积分的反函数变得非常困难. 另外,相对于微分运算来 ...

  10. STM32F407 MCO输出的配置问题

    当前使用IDE: RT-Thread Studio 版本: 2.1.0 构建ID: 202103221400 配置如下: int MCO1_GPIO_INIT(void) { GPIO_InitTyp ...