编程导航RPC话题讨论

RPC

3 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡

在国企干了 5 年 Java,居然不知道 RPC?这正常吗?

在国企干了 5 年 Java,居然不知道 RPC 是什么?这正常吗? 这很正常。 如果你没接触过分布式微服务项目,基本是接触不到 RPC 这玩意的,并不是个人能力的问题。 不过 RPC 是程序员需要掌握的知识,也是面试官可能会问的题目。 什么是 RPC?RPC 和 HTTP 有什么区别? 下面用 2 分钟给大家讲清楚! > 推荐观看视频版:https://bilibili.com/video/BV1y2aPzJEZY ## 什么是 RPC? 你饿了,想吃鱼皮。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/fbOHtMao3hRqSeqg.webp) 如果是在 20 年前,你只能自己吭哧吭哧跑到店里去买。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/lCO8iMAe0VYZdgmG.webp) 但现在有了手机、网络和外卖平台,你只需要在家动动手指点个外卖,骑手就能直接把鱼皮配送到家。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/3iWO0bF7Ghd9kXCE.webp) 你不需要关注网络是怎么传输的、平台是怎么操作的、骑手是怎么配送的,只负责享受鲜嫩多汁、丝滑爽口的鱼皮就行了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/6UfLPncfLK9Tr2J1.webp) 这个过程其实就是 RPC 的核心思想。 RPC 的全称是远程过程调用(Remote Procedure Call),允许一个项目 **像调用自己本地的方法一样**,调用另一个远程项目的接口,而不需要了解数据的传输处理过程和底层网络通信的细节。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Zf7KFEqcQP5uqsZw.webp) 举个例子,项目 A 提供了点餐服务,项目 B 想要调用它完成下单。 如果没有 RPC 框架,项目 B 作为服务消费者,需要找到项目 A 的地址、自己构造请求参数、给项目 A 发送请求并解析响应结果。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/NMr95YvQ1Zkxb1sq.webp) 如果项目 B 要调用很多第三方服务,每个都这么写,是不是很麻烦? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/cbZrx6cazwqTOGey.webp) 但如果使用 RPC 框架,只需要一行代码就能完成调用! ![](https://pic.code-nav.cn/post_picture/1601072287388278786/OF5J61eEbHMwehVA.webp) 看起来就跟调用自己项目内的方法没有任何区别!是不是很丝滑? 这就是 RPC 框架的作用,**隐藏了服务调用的通信细节,让程序员专注于业务逻辑**,快速开发分布式、微服务系统。 ## RPC 和 HTTP 的区别? 有同学会问了:“HTTP 协议不也能请求别的服务么,RPC 跟 HTTP 有什么区别呢?” 首先,HTTP 是一种网络通信协议,而 RPC 是一种 “远程调用本地化” 的思想,就像我想吃饭的时候,点外卖找个骑手帮我送,至于骑手是谁、从哪找到骑手、骑手是开车还是骑电动车,可以有不同的选择。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/2yiS2uRqAmVF5T1G.webp) 因此,RPC 完全可以 **基于 HTTP 协议来实现数据的传输**,只不过主流的 RPC 实现更多的使用基于 TCP 的二进制格式,传输的数据更紧凑,传输效率也更高。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/FHaH7miEvI52JEi9.webp) 一般来说,HTTP 适用于前端和后端的交互、**对外** 提供 的 RESTful API 服务;而 RPC 更适合分布式系统的服务间的 **内部** 通信。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/oI6Zbj1DMFmFiiL6.webp) 除了数据传输外,RPC 的实现一般还需要依赖注册中心、序列化器、负载均衡、重试容错机制等等。 - 注册中心:就像服务的通讯录,记录着各个服务在哪台机器上,消费者想找服务时查一下就能知道地址。 - 序列化器:相当于数据翻译官,把内存里的对象转换成能在网络上传输的格式(比如二进制),到了对方那里再翻译回去,确保双边都能看懂。 - 负载均衡:就像调度员,当多个机器都提供同一个服务时,它来决定把请求发给哪台机器,避免有的机器累死,有的机器闲着。 - 重试容错机制:备用方案,调用服务失败时会自动重试几次(比如网络卡了、宕机了),如果一直失败就用其他方法(比如返回缓存数据),不让整个系统因为一个小故障就崩溃。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/OGUx02LRKioNaowy.webp) 完整的 RPC 框架工作流程: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/dyBPCr0ldPNPGi0k.webp) 服务消费者和提供者都需要引入 RPC 框架: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/O9z6DL0EbczUBRzl.webp) ## 有哪些 RPC 框架? 听起来想实现 RPC 很复杂啊! 但别担心,市面上有很多强大的 RPC 框架,比如 gRPC、Dubbo、Thrift、OpenFeign 等,几乎可以满足我们对 RPC 的一切需求。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/l7LuJuTmryLGCid4.webp) 你们更喜欢用哪个框架呢? 个人建议,对于 Java 开发者来说,首选 Dubbo。 ## 开源项目 我之前带大家手写过一套 [RPC 框架](https://github.com/liyupi/yu-rpc) 并且完全开源,感兴趣的同学可以来学习~ 开源仓库:https://github.com/liyupi/yu-rpc ![](https://pic.code-nav.cn/post_picture/1601072287388278786/UJXJdmTmeh7YzvT6.webp) OK 以上就是本期分享,还有疑问的话欢迎大家在评论区留言,没疑问的话求个点赞三连 🌹👨🏻‍💻🧑🏻‍🦲 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/EEHUOC3NedKQeYOd.webp) ## 更多 💻 编程学习交流:[编程导航](https://www.codefather.cn/) 📃 简历快速制作:[老鱼简历](https://laoyujianli.com) ✏️ 面试刷题神器:[面试鸭](https://mianshiya.com) 📖 AI 学习指南:[AI 知识库](https://ai.codefather.cn/)

SpringBoot整合Dubbo(Nacos注册中心)

## SpringBoot项目整合Dubbo(Nacos作注册中心) 项目目录: ![image.png](https://pic.code-nav.cn/post_picture/1830958011788279809/FQNZpx397vwGt4pn.webp) - contract——约定的接口类 - provider——接口提供方 - consumer——接口调用方 需要引入的依赖——>在consumer和provider中引入即可: ```xml <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-nacos-spring-boot-starter</artifactId> </dependency> ``` 在contract中定义接口信息: ![image.png](https://pic.code-nav.cn/post_picture/1830958011788279809/jlXeEbf3kavR5JEQ.png) 在provider中定义接口实现类: ```java package com.jerry.dubboprovider.Service; import com.jerry.Service.DemoService; import org.apache.dubbo.config.annotation.DubboService; @DubboService public class DemoServiceImpl implements DemoService { @Override public String sayHello(String name) { return name+",Hello Dubbo"; } } ``` 注意使用Dubbo的注解`@DubboService` provider,consumer的启动类:使用Dubbo的注解`@EnableDubbo`,如下 ```java package com.jerry.dubboprovider; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableDubbo @SpringBootApplication public class DubboProviderApplication { public static void main(String[] args) { SpringApplication.run(DubboProviderApplication.class, args); } } ``` 在consumer中需要引入的地方使用 ``` @DubboReference private DemoService demoService; ``` ### 配置注册中心 因为本质上是consumer去调用provider内部的方法,那么如何在不直接引入provider的情况下,能使用他内部的方法呢? 我们就需要把provider提供的方法注册到注册中心,然后consumer去注册中心拉取 这里我直接使用Docker拉取nacos镜像,并且运行容器,在本地8848端口能够登录成功即可 ![image.png](https://pic.code-nav.cn/post_picture/1830958011788279809/HxTd15eVna5aKK6T.webp) 修改yaml文件—— 这里以provider的yaml配置为例,consumer的配置也类似 但是需要修改**dubbo.protocol.port**、**server.port**两个属性(避免端口冲突) ```yaml # 以下配置指定了应用的名称、使用的协议(Dubbo)、注册中心的类型(Nacos)和地址 dubbo: application: # 设置应用的名称 name: dubbo-springboot-demo-provider # 指定使用 Dubbo 协议,且端口设置为 -1,表示随机分配可用端口 protocol: name: dubbo port: 22220 registry: # 配置注册中心为 Nacos,使用的地址是 nacos://localhost:8848 id: nacos-registry address: nacos://localhost:8848 server: port: 8081 ``` 就可以正常调用demoService内部的方法了 ### 不同项目引入dubbo 其他步骤不变,只需要单独定义一个contract(接口)项目,并发布到本地(或线上)maven仓库中 在项目中引入contract即可,这里以maven为例,gradle同理 依次执行clean、package、install ![image.png](https://pic.code-nav.cn/post_picture/1830958011788279809/1zSn6ZWnO4UfF2ZU.webp) 可以去本地maven仓库看看是否上传成功 我的: ![image.png](https://pic.code-nav.cn/post_picture/1830958011788279809/MOqvVa2ASSFULNDJ.webp) 确定上传成功之后 从本地仓库导入 ```xml <dependency> <groupId>com.jerry</groupId> <artifactId>dubbo-contract</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> ``` 其他代码不变

今天学习了掘金小册里面的《Java开发者的RPC实战课》,写了一些笔记给大家分享一下(还没看完所以笔记还不完全✨)

<html> <head></head> <body> <div class="content ql-editor"> <p><br></p> <h1><strong style="color: rgb(51, 51, 51);">Java开发者的RPC</strong></h1> <p><br></p> <p><br></p> <h2><strong style="color: rgb(51, 51, 51);">远程调用</strong></h2> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>HTTP :HTTP协议</li> <li data-list="bullet"><span class="ql-ui"></span>RPC : TCP/IP协议</li> </ol> <p>性能:RPC&gt;HTTP</p> <p><br></p> <h2><strong style="color: rgb(51, 51, 51);">PRC常用框架</strong></h2> <p><br></p> <p>Dobbo、Grpc、Thritf</p> <p>合格的rpc功能包含:服务发现机、负载均衡策略、容错降级、调用重试、异步队列、事件解耦</p> <p><br></p> <h2><strong style="color: rgb(51, 51, 51);">认识PRC</strong></h2> <p><br></p> <p>rpc的核心组件:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>Server :服务端</li> <li data-list="bullet"><span class="ql-ui"></span>Client:客户端</li> <li data-list="bullet"><span class="ql-ui"></span>Server Stub:服务端收到Client发送的数据进行<strong>消息解包</strong>,调用本地方法</li> <li data-list="bullet"><span class="ql-ui"></span>Client Stub: 将客户端的<strong>请求参数、服务名称、服务地址</strong>进行统一打包,发送给Server方</li> </ol> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/y9y9lrfm.jpeg"></p> <p><br></p> <h2><strong style="color: rgb(51, 51, 51);">整体结构分析</strong></h2> <h3><strong style="color: rgb(51, 51, 51);">调用流程分析</strong></h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> 首先本地客户端通知到了本地存根(sub),接着本地存根需要对数据格式进行包装、网络请求进行封装,然后按照一定的规则将数据包发送的目标机器上 </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> 服务端收到传过来的数据包然后对其按照事先约定好的规则进行解码,从而识别到数据包内部信息。将请求转发到本地函数中进行处理,处理完的数据进行返回给客户端 </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> 客户端存根接收到服务的时候需要对其进行解码。最后得到了最终结果。 </div> </div> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/9ykgdwus.jpeg"></p> <p>简单的一个设计流程,在此基础上对功能进行不断地扩充,最终落实成一个成熟的rpc框架</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">代理层的设计</strong></h3> <p><br></p> <p>从使用角度来说,希望使用这款框架期间,在远程调用的时候能隐藏其内部细节,让其像调用本地方法一样方便</p> <p><strong>通过代码理解</strong></p> <div class="ql-code-block-container"> <div class="ql-code-block"> public class Client { </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public static void main(String[] args) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;//调用一次远程服务 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;Server server = new Server("127.0.0.1",9999); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;server.doConnect(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;Object sendResponse = server.doRef("sendSms","这是一条短信信息",10001); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(sendResponse); </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; </div> <div class="ql-code-block"> } </div> </div> <p>我们希望在本地应用进行服务的连接,调用短信发送功能,进行远程调用,然后得到数据的返回,这段代码看似是简单的调用,实则隐藏了许多内部的细节</p> <p><br></p> <p><strong>server.doRef</strong> 内部就是一个代理的手段,内部可以设计一个统一的代理组件,辅助开发者发送远程服务的调用,最后获取数据的返回</p> <p>在此可以联想到代理模式,需要给某对象提供一个访问的代理,访问对象不适合或不能直接访问目标对象,就可以将代理对象作为访问对象跟目标对象之间的媒介</p> <p><br></p> <p>代码模式的优点:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>代理对象能作为客户端与目标对象的媒介,能保护目标对象;</li> <li data-list="bullet"><span class="ql-ui"></span>代理对象能扩展目标对象的功能;</li> <li data-list="bullet"><span class="ql-ui"></span>代理对象能将客户端与目标对象分离,起到了解耦的作用,增加了程序的可扩展性</li> </ol> <p><br></p> <p>面对客户端发送请求我们可以设计一个代理层,处理一些内部细节,将内部细节隐蔽起来,让调用者无感知。以下是模拟一张请求调用的流程图</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/xhdvds7f.jpeg"></p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">路由层的设计</strong></h3> <p><br></p> <p>问题的引出:当目标服务众多,客户端需要怎么确认最终的请求服务</p> <p>设计思路:引入一个路由的角色,由他选择最终的请求服务</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/o8tnjqg9.jpeg"></p> <p>客户端请求都会经过一个路由层,由路由层内部规则去匹配对应的provider服务</p> <p>路由层设计的时候需要考虑的点:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>如何获取到provider服务</li> <li data-list="bullet"><span class="ql-ui"></span>如何从集群服务中做筛选</li> <li data-list="bullet"><span class="ql-ui"></span>如何设计能较好地兼容后期路由的扩展功能</li> </ol> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">协议层的设计</strong></h3> <p><br></p> <p>客户端在使用RPC框架进行远程调用的时候,需要对数据进行统一的包装和组织,最终才发送到目标机器进行接收解析。因此对数据的各种序列化、反序列化、协议的组装可以统一封装在协议层中。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/05kjfcsk.jpeg"></p> <p>router模块会负责提供最终需要调用服务提供者的具体信息,然后将对应的地址信息、请求参数传入到Protocol中,最终由协议层对数据封装成对应的协议体,然后序列化处理后发送给服务端</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">链路层的设计</strong></h3> <p><br></p> <p>从本地请求,到protocol发送数据,整个链路中要考虑一个可扩展性,比如一些自定义的过滤、服务分组等,可通过新增一个链路模块,类似于责任链的模式</p> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/b78b4di8.jpeg"></p> <h3><strong style="color: rgb(51, 51, 51);">注册中心层的设计</strong></h3> <p><br></p> <p>当服务提供者呈现集群模式的时候,客户端需获取到Provider的多个信息,这时我们需引入一个注册中心的角色</p> <p>服务提供者会将自己的地址、接口、分组等详细信息都上报到注册中心模块,并且当服务上线、下线的时候通知到注册中心,服务调用方只需要订阅注册中心即可</p> <p>市面上常见的注册中心:Nacos、Zookeeper、etcd、Eureka、Redis等</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/7nxkbioh.jpeg"></p> <p>需要重点关注的点:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>如何与注册中心进行基本的连接访问</li> <li data-list="bullet"><span class="ql-ui"></span>如何监听服务数据在注册中心的实时变化</li> <li data-list="bullet"><span class="ql-ui"></span>如果注册中心出现了异常,需要有哪些安全手段</li> </ol> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">容错层的设计</strong></h3> <p><br></p> <p>在远程调用的时候可能会出现一些异常情况,市面上常见的一些RPC框架都会提供一些容错方面的处理手段:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>超时重试</li> <li data-list="bullet"><span class="ql-ui"></span>快速失败</li> <li data-list="bullet"><span class="ql-ui"></span>无线重试</li> <li data-list="bullet"><span class="ql-ui"></span>出现异常后回调指定方法</li> <li data-list="bullet"><span class="ql-ui"></span>无视失败</li> </ol> <p>面对这些异常,我们可以抽象出一层 容错层 进行处理</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/al3visoy.jpeg"></p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">服务提供者的线程池设计</strong></h3> <p><br></p> <p>当请求发送到了服务提供者的时候,服务提供方需要对其进行解码,然后再本地进行核心处理,在解码这一步骤中我们统一交给专门的线程进行处理</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/c83qad7k.jpeg"></p> <p>涉及的相关技术点:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>io线程与worker线程的拆分</li> <li data-list="bullet"><span class="ql-ui"></span>调用结果和客户端请求的唯一匹配</li> <li data-list="bullet"><span class="ql-ui"></span>客户端请求后的同步转为异步处理</li> <li data-list="bullet"><span class="ql-ui"></span>单一请求队列和多请求队列的设计差异性</li> </ol> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">接入层的设计</strong></h3> <p><br></p> <p>整套RPC组件基本设计实现后,要考虑如何将其接入到实际开发项目当中,团队主要技术使用Spring,所以这款RPC框架也应该介入到starter组件中,让使用Spring的技术团队更好的接入使用,最后是整体的架构图</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/7h0dikag.jpeg"></p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">小结</strong></h3> <p><br></p> <p>对RPC框架整体进行基本的分层:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>代理层:负责底层调用细节的封装</li> <li data-list="bullet"><span class="ql-ui"></span>路由层:通过调用策略来选择目标服务</li> <li data-list="bullet"><span class="ql-ui"></span>协议层:服务数据的转码封装、序列化、反序列化等</li> <li data-list="bullet"><span class="ql-ui"></span>链路层:负责执行一些自定义的过滤链路,可供后期的二次扩展</li> <li data-list="bullet"><span class="ql-ui"></span>注册中心层:关注服务的上下线,以及一些权重,配置动态调整等功能</li> <li data-list="bullet"><span class="ql-ui"></span>容错层:当出现服务调用失败,应该执行的一些策略、兜底等功能</li> <li data-list="bullet"><span class="ql-ui"></span>接入层:如何将框架接入常用框架Spring中</li> <li data-list="bullet"><span class="ql-ui"></span>公共层:主要存放一些通用配置、工具类、缓存等信息</li> </ol> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">本地调用和RPC调用的区别</strong></h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> 形象的比喻: </div> <div class="ql-code-block"> 事:人要吃苹果,尝试苹果的甜度 </div> <div class="ql-code-block"> 本地调用:我在果园,苹果摘下来我就可以吃了,当场发表意见,苹果甜不甜。 </div> <div class="ql-code-block"> PRC调用:人不在果园,果农将苹果摘下,打包,装箱,运输,最后送达接收人的手中,接收人拆箱,尝试苹果,通过微信发消息,告诉我苹果的甜度。 </div> </div> <p><br></p> <h2><strong style="color: rgb(51, 51, 51);">理解网络通信模型的核心</strong></h2> <h3><strong style="color: rgb(51, 51, 51);">阻塞IO技术</strong></h3> <p><br></p> <p>基于BIO实现的阻塞IO服务端程序代码</p> <div class="ql-code-block-container"> <div class="ql-code-block"> /** </div> <div class="ql-code-block"> * </div> <div class="ql-code-block"> * @author cong </div> <div class="ql-code-block"> * @date 2023/07/25 </div> <div class="ql-code-block"> */ </div> <div class="ql-code-block"> public class BioServer { </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;private static ExecutorService executors = Executors.newFixedThreadPool(10); </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public static void main(String[] args) throws IOException { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;ServerSocket serverSocket = new ServerSocket(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;serverSocket.bind(new InetSocketAddress(1009)); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;while (true) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //堵塞状态点--1 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Socket socket = serverSocket.accept(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println("获取新连接"); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;executors.execute(new Runnable() { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;@Override </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;public void run() { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;while (true){ </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;InputStream inputStream = null; </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//堵塞的状态点--2 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;inputStream = socket.getInputStream(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;byte[] result = new byte[1024]; </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int len = inputStream.read(result); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(len!=-1){ </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println("[response] "+new String(result,0,len)); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;OutputStream outputStream = socket.getOutputStream(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;outputStream.write("response data".getBytes()); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;outputStream.flush(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (IOException e) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;break; </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; }catch (Exception e){ </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> ​ </div> </div> <p>当前代码主要关注两个函数 accept、read 在传统的BIO技术中会在这两个模块发生阻塞</p> <p>服务端创建了Socket之后会堵塞住等待accept函数的连接</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/wgmt08h9.jpeg"></p> <p>当客户端连接上了服务端后,accept的堵塞状态就会放开进入到read环节(读取客户端传过来的网络数据)</p> <p>客户端如果一直没有发送数据过来,那么服务端的read调用方法就会一直处于堵塞状态,如果数据通过网络抵达了网卡缓冲区,此时则会将数据从内核态拷贝至用户态,然后返回给read调用方。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/o00tqxw7.jpeg"></p> <p>出现问题:如果客户端只是连上了服务端,但是没有进行发送数据,那么服务端就会一直处于阻塞状态,此时引出一种技术叫 非阻塞IO技术。</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">非阻塞IO技术</strong></h3> <p><br></p> <p>对阻塞IO进行性能改造,可以尝试让其在accept函数获取到客户端连接后,专门创建一个线程来处理read函数,整体流程如下图:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/9t6r0bzm.jpeg"></p> <p>是否疑惑?为什么来一个请求再创建一个线程,这部分工作交给线程池去完成不好吗?</p> <p><br></p> <p>没错,线程池能提强帮助我们创建好一点数量的线程,当请求量大的时候还能有队列缓冲,以及增加worker线程的效果,从而降低这块实现的难度。但是这样的设计依然存在不足点,因为在用户态层面调用的<strong>read</strong>函数依然是阻塞的。就现阶段而言,这种技术方案还不能称之为非阻塞IO技术。</p> <p>如果<strong>read</strong>函数在调用的时候没有数据抵达,他还是处于阻塞状态,如何对其进行设计升级非阻塞效果呢</p> <p><br></p> <p>JDK的NIO模型中就有相关的设计存在,下面是一段简单的NIO服务端代码</p> <div class="ql-code-block-container"> <div class="ql-code-block"> public class NioSocketServer extends Thread { </div> <div class="ql-code-block"> &nbsp; &nbsp;ServerSocketChannel serverSocketChannel = null; </div> <div class="ql-code-block"> &nbsp; &nbsp;Selector selector = null; </div> <div class="ql-code-block"> &nbsp; &nbsp;SelectionKey selectionKey = null; </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public void initServer() throws IOException { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;selector = Selector.open(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;serverSocketChannel = ServerSocketChannel.open(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;//设置为非阻塞模式,默认serverSocketChannel是采用了阻塞模式 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;serverSocketChannel.configureBlocking(false); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;serverSocketChannel.socket().bind(new InetSocketAddress(8888)); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;@Override </div> <div class="ql-code-block"> &nbsp; &nbsp;public void run() { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;while (true) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//默认这里会堵塞 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int selectKey = selector.select(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (selectKey &gt; 0) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//获取到所有的处于就绪状态的channel,selectionKey中包含了channel的信息 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Set&lt;SelectionKey&gt; keySet = selector.selectedKeys(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Iterator&lt;SelectionKey&gt; iter = keySet.iterator(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//对selectionkey进行遍历 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;while (iter.hasNext()) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;SelectionKey selectionKey = iter.next(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//需要清空,防止下次重复处理 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;iter.remove(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//就绪事件,处理连接 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (selectionKey.isAcceptable()) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;accept(selectionKey); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//读事件,处理数据读取 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (selectionKey.isReadable()) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;read(selectionKey); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//写事件,处理写数据 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (selectionKey.isWritable()) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (IOException e) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;serverSocketChannel.close(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (IOException e1) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e1.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public void accept(SelectionKey key) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;SocketChannel socketChannel = serverSocketChannel.accept(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println("conn is acceptable"); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;socketChannel.configureBlocking(false); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//将当前的channel交给selector对象监管,并且有selector对象管理它的读事件 </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;socketChannel.register(selector, SelectionKey.OP_READ); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } catch (IOException e) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public void read(SelectionKey selectionKey) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;SocketChannel channel = (SocketChannel) selectionKey.channel(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ByteBuffer byteBuffer = ByteBuffer.allocate(100); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int len = channel.read(byteBuffer); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (len &gt; 0) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;byteBuffer.flip(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;byte[] byteArray = new byte[byteBuffer.limit()]; </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;byteBuffer.get(byteArray); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.println("NioSocketServer receive from client:" + new String(byteArray,0,len)); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;selectionKey.interestOps(SelectionKey.OP_READ); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } catch (Exception e) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;try { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;serverSocketChannel.close(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;selectionKey.cancel(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (IOException e1) { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e1.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;e.printStackTrace(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; } </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> ​ </div> <div class="ql-code-block"> &nbsp; &nbsp;public static void main(String args[]) throws IOException { </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;NioSocketServer server = new NioSocketServer(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;server.initServer(); </div> <div class="ql-code-block"> &nbsp; &nbsp; &nbsp; &nbsp;server.start(); </div> <div class="ql-code-block"> &nbsp; } </div> <div class="ql-code-block"> } </div> </div> <p>以下是上述代码的流程图</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/ohguk5af.jpeg"></p> <p>思考一下接下来代码会怎么走,当socket的服务端启动之后,会对每一个socket连接的对象都开启一个线程,然后在循环里面去调read函数,此时的read函数调用不会进入阻塞状态额,但是似乎没有解决掉根本性问题:每次请求来的时候都要创建一个线程来监听客户端的请求。如果客户端在建立连接之后长时间都没有传输数据,那对于服务端而言就会造成资源浪费的情况。</p> <p><strong>每个请求都要建立一个线程,如何进行优化</strong>?</p> <p>我们不妨可以将accept和read分成两个模块来处理,当accept函数接收到了新的接连(本质就是一个文件描述符fd)之后,将其放入一个集合中,然后会有一个后台任务统一对集合中的fd进行遍历执行read操作。 流程如下:</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/9e05gz1u.jpeg"></p> <p>看到这里有些疑问,循环调用read方法岂不是循环进行用户态和内核态之间切换?这样不断地进行上下文切换也不是什么好的设计思路啊,能否将这个循环的操作交给内核态处理呢</p> <p><br></p> <p>待续。。。。</p> <p><br></p> </div> </body> </html>

下载 APP