parallelStream与CompletableFuture

news2024/11/23 23:02:31

1  了解parallelStream

parallelStream怎么实现的并行处理呢?
其底层是Fork/Join并行计算框架的默认线程池,默认线程池的数量就是处理器的数量,可以使用系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism={N} 调整。

1.1 并发问题
            HashMap<String, String> map = new HashMap<>();
         for (Clazz clazz : byClazzIds) {
             map.put(clazz.getId(), clazz.getTeacherNames());
         }
      List<String> unBindedClazzIds = new ArrayList<>();
            rows.stream().forEach(clazz -> {
                String teaNames = map.get(clazz.getId());
                if (StringUtils.isEmpty(teaNames)) {
                    unBindedClazzIds.add(clazz.getId());
                } else {
                    clazz.setTeacherNames(teaNames);
                }
            });
         如果为了提升性能直接将串行的stream()修改为并行的parallelStream(),这样做可以吗?
https://blog.csdn.net/weixin_58670730/article/details/125945959

1.2 共用默认线程池导致的阻塞问题
什么时候使用默认的Fork/Join,什么时候使用自定义的forkJoin
并行流1:教师端rpc获取班级,5秒延迟,此时会占用所有的线程资源
并行流2:学生获取题组下所有题目的详情
并行流3:学生端加载班课时并行处理报名记录
都使用默认的Fork/Join,此时如果并行流1中线程被占用,则并行流2和3的任务会一直处于阻塞状态,即原本不相关的业务因为共用了默认的Fork/Join的线程,会相互影响。
rpc\IO阻塞,使用锁(可能导致死锁)一定要自定义新的ForkJoin

parallelStream的坑, 相互阻塞问题
https://blog.51cto.com/u_14355948/2709545?b=totalstatistic

1.3 并发结果未同步问题
问题代码:
        ForkJoinPool forkJoinPool = new ForkJoinPool(10);
        forkJoinPool.execute(() -> {
            // 已结课 并且大于30天
            rows.parallelStream().forEach(item -> {
                item.setClassImId(classIdToImId.getOrDefault(item.getId(), ""));
                // *特殊:主讲老师的课程列表中,在线大班的班级卡片 不展示未读消息数量。
                Integer capacityLabel = item.getCapacityLabel();
                if (Objects.equals(teacherType, ZeroOrOneEnum.ZERO.getValue()) && Objects.equals(capacityLabel, ZeroOrOneEnum.ONE.getValue())) {
                    item.setUnreadMsgNum(0);
                } else if (Objects.equals(type, ZeroOrOneEnum.ZERO.getValue()) && DateFormatUtil.isGtMonth(item.getEndTime())) {
                    // 已结课 并且大于30天
                    item.setUnreadMsgNum(0);
                } else {
                    item.setUnreadMsgNum(imBiz.getClassIdUnreadMsgNum(item.getId(), classIdToImId));
                }
            });
        });

        dto.setRows(rows);
正确代码:
        ForkJoinPool forkJoinPool = new ForkJoinPool(10);
        forkJoinPool.submit(() -> {
            // 已结课 并且大于30天
            log.info("线程名称P"+ Thread.currentThread().getName());

            rows.parallelStream().forEach(item -> {
                log.info("线程名称P"+ Thread.currentThread().getName());
                item.setClassImId(classIdToImId.getOrDefault(item.getId(), ""));
                // *特殊:主讲老师的课程列表中,在线大班的班级卡片 不展示未读消息数量。
                Integer capacityLabel = item.getCapacityLabel();
                if (Objects.equals(teacherType, ZeroOrOneEnum.ZERO.getValue()) && Objects.equals(capacityLabel, ZeroOrOneEnum.ONE.getValue())) {
                    item.setUnreadMsgNum(0);
                } else if (Objects.equals(type, ZeroOrOneEnum.ZERO.getValue()) && DateFormatUtil.isGtMonth(item.getEndTime())) {
                    // 已结课 并且大于30天
                    item.setUnreadMsgNum(0);
                } else {
                    item.setUnreadMsgNum(imBiz.getClassIdUnreadMsgNum(item.getId(), classIdToImId));
                }
            });
        }).invoke();

        dto.setRows(rows);
由于 execute 方法是异步的,它不会等待任务执行完成就会立即返回,因此可能会导致程序在任务执行之前就已经退出。
submit 方法返回一个 ForkJoinTask 对象,可以使用该对象的 invoke 方法来阻塞当前线程,直到任务执行完成。

1.4 线程池的重复创建销毁导致的性能问题
forkJoinPool.shutdown();

在使用 ForkJoinPool 时,我们通常会在程序的最后调用 shutdown 方法来关闭线程池,以便释放线程和其他资源。如果不调用 shutdown 方法,线程池会一直保持打开状态,直到程序退出或被强制关闭。

forkJoinPool不应该在方法内部创建,应该是静态变量,且不销毁

2 了解CompletableFuture

2.1为什么要引入 CompletableFuture
Java 的 1.5 版本引入了 Future,你可以把它简单的理解为运算结果的占位符,它提供了两个方法来获取运算结果。

get():调用该方法线程将会无限期等待运算结果。
get(long timeout, TimeUnit unit):调用该方法线程将仅在指定时间 timeout 内等待结果,如果等待超时就会抛出 TimeoutException 异常。
Future 可以使用 Runnable 或 Callable 实例来完成提交的任务,通过其源码可以看出,它存在如下几个问题:
1)阻塞 调用 get() 方法会一直阻塞,直到等待直到计算完成,它没有提供任何方法可以在完成时通知,同时也不具有附加回调函数的功能。
2)链式调用和结果聚合处理 在很多时候我们想链接多个 Future 来完成耗时较长的计算,此时需要合并结果并将结果发送到另一个任务中,该接口很难完成这种处理。
3)异常处理 Future 没有提供任何异常处理的方式。
以上这些问题在 CompletableFuture 中都已经解决了,CompletableFuture 类通过实现了Future 接口和CompletionStage接口解决上面的问题,接下来让我们看看如何去使用 CompletableFuture。

2.2创建CompletableFuture对象
四种方式:
// 使用默认线程池
static CompletableFuture<Void> runAsync(Runnable runnable)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 可以指定线程池
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

说明:
1)runAsync未指定线程池,默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。
所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰
2)runAsync没有返回值,而supplyAsync有返回值,因为Runnable 接口的run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。

2.3 任务时序的理解
假设有一个烧水泡茶的整体任务,其分工和任务如下:任务 1 负责洗水壶、烧开水,任务 2 负责洗茶壶、洗茶杯和拿茶叶,任务 3 负责泡茶。其中任务 3 要等待任务 1 和任务 2 都完成后才能开始。这个分工如下图所示.

任务是有时序关系的,比如有串行关系、并行关系、汇聚关系等。其中洗水壶和烧开水就是串行关系,洗水壶、烧开水和洗茶壶、洗茶杯这两组任务之间就是并行关系,而烧开水、拿茶叶和泡茶就是汇聚关系。

 

CompletableFuture实现的CompletionStage 接口可以清晰地描述任务之间的这种时序关系。

2.4 CompletableFuture的对任务时序处理的支持
1)串行时序
CompletionStage 接口里面描述串行关系,主要是 thenApply、thenAccept、thenRun 和 thenCompose 这四个系列的接口。
thenApply 系列函数里参数 fn 的类型是接口 Function<T, R>,这个接口里与 CompletionStage 相关的方法是 R apply(T t),这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是CompletionStage<R>。
thenAccept 系列方法里参数 consumer 的类型是接口Consumer<T>,这个接口里与 CompletionStage 相关的方法是 void accept(T t),这个方法虽然支持参数,但却不支持回值,所以 thenAccept 系列方法返回的是CompletionStage<Void>。
thenRun 系列方法里 action 的参数是 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是CompletionStage<Void>。
这些方法里面 Async 代表的是异步执行 fn、consumer 或者 action。其中,需要你注意的是 thenCompose 系列方法,这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。
CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);
通过下面的示例代码,你可以看一下 thenApply() 方法是如何使用的。首先通过 supplyAsync() 启动一个异步流程,之后是两个串行操作,整体看起来还是挺简单的。不过,虽然这是一个异步流程,但任务①②③却是串行执行的,②依赖①的执行结果,③依赖②的执行结果。
CompletableFuture<String> f0 =
  CompletableFuture.supplyAsync(
    () -> "Hello World")      //①
  .thenApply(s -> s + " QQ")  //②
  .thenApply(String::toUpperCase);//③

System.out.println(f0.join());
// 输出结果
HELLO WORLD QQ

2)AND 汇聚关系
CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。
CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);

3)OR 汇聚关系
CompletionStage 接口里面描述 OR 汇聚关系,主要是 applyToEither、acceptEither 和runAfterEither 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同
CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);

2.5 异常处理
虽然上面我们提到的 fn、consumer、action 它们的核心方法都不允许抛出可检查异常,但是却无法限制它们抛出运行时异常,例如下面的代码,执行 7/0 就会出现除零错误这个运行时异常。非异步编程里面,我们可以使用 try{}catch{}来捕获并处理异常,那在异步编程里面,异常该如何处理呢?
CompletableFuture<Integer>
f0 = CompletableFuture.
.supplyAsync(()->(7/0))
.thenApply(r->r*10);
System.out.println(f0.join());
CompletionStage 接口给我们提供的方案非常简单,比 try{}catch{}还要简单,下面是相关的方法,使用这些方法进行异常处理和串行操作是一样的,都支持链式编程方式。
CompletionStage exceptionally(fn);
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn)
下面的示例代码展示了如何使用 exceptionally() 方法来处理异常,exceptionally() 的使用非常类似于 try{}catch{}中的 catch{},但是由于支持链式编程方式,所以相对更简单。既然有 try{}catch{},那就一定还有 try{}finally{},whenComplete() 和 handle() 系列方法就类
似于 try{}finally{}中的 finally{},无论是否发生异常都会执行 whenComplete() 中的回调函数 consumer 和 handle() 中的回调函数 fn。whenComplete() 和 handle() 的区别在于whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。
CompletableFuture<Integer>
f0 = CompletableFuture
.supplyAsync(()->7/0))
.thenApply(r->r*10)
.exceptionally(e->0);
System.out.println(f0.join());

3 Fork/Join并行计算框架
分治任务模型
这里你需要先深入了解一下分治任务模型,分治任务模型可分为两个阶段:一个阶段是任务分解,也就是将任务迭代地分解为子任务,直至子任务可以直接计算出结果;另一个阶段是结果合并,即逐层合并子任务的执行结果,直至获得最终结果。如下图:

 

Fork/Join 的使用
Fork/Join 是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask,他俩的关系类似于 ThreadPoolExecutor 和 Runnable 的关系,都可以理解为提交任务到线程池,只不过分治任务有自己独特类型 ForkJoinTask。

ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。ForkJoinTask 有两个子类——RecursiveAction 和 RecursiveTask,它们都是用递归的方式来处理分治任务的。
这两个子类都定义了抽象方法 compute(),不过区别是 RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要你定义子类去扩展。

接下来我们就来实现一下,看看如何用 Fork/Join 这个并行计算框架计算斐波那契数列(下面的代码源自 Java 官方示例)。
static void main(String[] args){
  // 创建分治任务线程池 
  ForkJoinPool fjp = new ForkJoinPool(4);
  // 创建分治任务
  Fibonacci fib = new Fibonacci(30);  
  // 启动分治任务 
  Integer result = fjp.invoke(fib);
  // 输出结果 
  System.out.println(result);
}
// 递归任务
static class Fibonacci extends RecursiveTask<Integer>{
  final int n;
  Fibonacci(int n){this.n = n;}
  protected Integer compute(){
    if (n <= 1)
      return n;
    Fibonacci f1 = new Fibonacci(n - 1);
    // 创建子任务 
    f1.fork();
    Fibonacci f2 = new Fibonacci(n - 2);
    // 等待子任务结果,并合并结果   f1通过join() 方法则会阻塞当前线程来等待子任务的执行结果
    return f2.compute() + f1.join();
  }
}

ForkJoinPool 工作原理
一般的线程池都是有多个工作线程,但是这些工作线程都共享一个任务队列。
ThreadPoolExecutor 内部只有一个任务队列,而 ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。

 

如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务,例如上图中,线程 T2 对应的任务队列已经空了,它可以“窃取”线程 T1 对应的任务队列的任务。如此一来,所有的工作线程都不会闲下来了。

ForkJoinPool 中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。

4 总结
CompletableFuture和并行流都是以 ForkJoinPool 为基础的。默认情况下所有并行流和CompletableFuture计算都共享一个 ForkJoinPool,这个共享的 ForkJoinPool 默认的线程数是 CPU 的核数;如果所有的计算都是 CPU 密集型计算的话,完全没有问题,但是如果存在 I/O 密集型的并行流计算,那么很可能会因为一个很慢的 I/O 计算而拖慢整个系统的性能。所以建议使用CompletableFuture和并行流时应该使用指定的线程池。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/667544.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据库】MySQL 高级(进阶) SQL 语句

文章目录 前提条件一、常用查询1. SELECT&#xff08;显示查询&#xff09;2. DISTINCT&#xff08;不重复查询&#xff09;3. WHERE&#xff08;有条件查询&#xff09;4. AND/OR&#xff08;且/或&#xff09;5. IN &#xff08;显示已知值的字段&#xff09;6. BETWEEN&…

自驾出游擅自使用对讲机属于违法行为?

周末或节假日大多数人通常都会选择自驾出游&#xff0c;或是叫上自己的好友一起出游&#xff0c;这个时候就可以组成一个车队。为了沟通起来更方便&#xff0c;大家一般都喜欢配个对讲机。 不过据调查显示&#xff0c;大多数人并不认为擅自使用对讲机算违法行为。在多个电商平…

【计算机视觉】OFA:通过一个简单的seq2seq的学习框架来统一架构、任务和模态

文章目录 一、导读二、摘要三、介绍四、OFA4.1 I/O & Architecture4.1.1 I/O4.1.2 Architecture 4.2 Tasks & Modalities4.3 预训练数据集4.4 训练与推理4.5 缩放模型 五、实验结果5.1 跨模态任务的结果5.2 单模态任务的结果5.3 zero-shot学习和任务迁移 六、测试结果七…

测试不为人知的小秘密,你占了几个?

1、线上出现小bug&#xff0c;小本本记下&#xff0c;后面偷偷给开发提个bug。 2、操作服务器时&#xff0c;把数据库玩坏了&#xff0c;系统玩崩了&#xff0c;加班熬夜默默的抢救修复。 3、和开发吵了一架&#xff0c;然后重点照顾了他负责的模块&#xff0c;找了一堆bug&a…

智慧无线灌溉在园林中的应用

智慧无线灌溉技术是解决人们生活用水与园林灌溉之间用水矛盾的有效措施之一&#xff0c;对提升园林灌溉效率和节约水资源有着重要的意义。 智慧无线灌溉系统可以自动感知园林植物周围的环境温度、水分等要素&#xff0c;并对感知到的要素进行详细分析和判断&#xff0c;以确定…

小白到运维工程师自学之路 第四十一集 (shell脚本的基本使用)

一、概述 Shell是一种命令行解释器&#xff0c;它是一种编程语言&#xff0c;用于在操作系统上执行命令和脚本。Shell语言是一种脚本语言&#xff0c;它可以用于自动化任务、批处理、系统管理和编写简单的程序。Shell语言通常用于Unix和Linux操作系统中&#xff0c;但也可以在其…

MySQL优化--索引创建原则,索引什么时候会失效

目录 索引创建原则 面试回答 索引什么时候会失效 面试回答 索引创建原则 1). 针对于数据量较大&#xff0c;且查询比较频繁的表建立索引。 2). 针对于常作为查询条件&#xff08;where&#xff09;、排序&#xff08;order by&#xff09;、分组&#xff08;group by&…

链路追踪SkyWalking整合项目以及数据持久化

1. 微服务整合SkyWalking 1.1 通过jar包方式整合 首先我们将一个简单的springboot服务打成jar包。 将其上传到Linux服务器中。 准备一个启动脚本&#xff0c;脚本内容如下&#xff1a; #!/bin/sh # SkyWalking Agent配置 export SW_AGENT_NAMEskywalking‐test #Agent名字,一…

QVariant类api及相关用法

目录 1、QVariant对象概述 2、QVariant对象的使用 3.使用QVariant对QT中标准数据类型进行处理 4.使用QVariant对QT中自定义数据类型进行处理 setValue fromValue canConvert 1、QVariant对象概述 在Qt中&#xff0c;QVariant是一个通用的值容器&#xff0c;它可…

【kubernetes】Docker与获取转证书工具安装、kubernetes软件安装

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机准备开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

没有“中间商赚差价”, OpenVINO™ 直接支持 PyTorch 模型对象

点击蓝字 关注我们,让开发变得更有趣 作者 | 杨亦诚 排版 | 李擎 没有“中间商赚差价”&#xff0c; OpenVINO™ 直接支持 PyTorch 模型对象 背景 作为最热门的开源深度学习框架之一&#xff0c;PyTorch 的易用性和灵活性使其深受学术和研究界的喜爱。之前 OpenVINO™ 对于 Py…

人脸检测——基于机器学习2】Haar特性

1.什么是Haar特征 Haar特征是一种反映人脸灰度差异的矩形特征,因其与Haar小波类似而得名。 2.什么是Haar小波 Haar小波: 定义 其波形图为 由图2.1可以看出,Haar小波可以看作二值 分类问题,如同Haar矩阵特征非黑即白,故Haar矩阵特征又称为类Haar特征。 3.Haar特征 H…

React项目引入Arco Design,以及Arco Design Pro 架构

创建项目 创建 react-arco 项目 pnpm create vite my-vue-app --template react安装 arco-design/web-react 安装 react 版的 arco-design 基础使用 添加一个按钮&#xff0c;App.tsx import "./App.css"; import { Button } from "arco-design/web-react…

基本类型与包装类型区别

知识点概括&#xff1a; 简介&#xff1a; Java有八种基本类型&#xff0c;byte&#xff0c; short&#xff0c; int&#xff0c; long&#xff0c; float&#xff0c; double&#xff0c; char&#xff0c; boolean。 对应八种包装类&#xff0c;Byte&#xff0c; Short&…

【Python架构】在 Python 中使用架构模式管理复杂性

你的源代码是不是感觉像一个大泥球&#xff1f;依赖项是否在您的代码库中交织在一起&#xff0c;以至于改变感觉很危险或不可能&#xff1f; 随着业务的增长和领域模型&#xff08;您在应用程序中解决的业务问题&#xff09;变得更加复杂&#xff0c;我们如何在不从头开始重新编…

Android初体验

文章目录 前言一、安卓架构1.1 安卓架构1.2 信息安全1.3 兼容性测试 二、使用WSL编译安卓低版本&#xff08;10&#xff09;总结 前言 Android 是一个适用于移动设备的开源操作系统&#xff0c;也是由 Google 主导的对应开源项目。 最初&#xff0c;Android只在移动设备中流行…

快码住! 帮你巧妙记忆C语言运算符的优先级顺序!

文章目录 C语言中的运算符C语言中运算符的优先级巧妙记忆运算符优先级的方法因不明确优先级而造成的常见问题 C语言中的运算符 C语言中的运算符说多也多&#xff0c;说不多也不多&#xff0c;包括以下运算符&#xff1a; C语言中运算符的优先级 C语言运算符优先级表&#xff…

Springboot--关于自定义stater的yml无法提示

1.前言 在以前在搭建架构的时候就碰到了类似的情况&#xff0c;在使用EnableConfigurationProperties注解的时候&#xff0c;不管怎样&#xff0c;在项目中引入了该starter的情况下依然不发自动的提示properties里面的属性。 Data ConfigurationProperties(prefix "pro…

python selenium2/webdriver自动化测试系列教程!?

以下是 Python Selenium2/WebDriver 自动化测试系列教程&#xff0c;希望能对您有所帮助&#xff1a; 1、安装 Selenium WebDriver 在安装 Python 的基础上&#xff0c;请使用 pip 命令安装 Selenium WebDriver&#xff1a; pip install selenium 2、配置浏览器驱动程序 S…

UDP编程

前置知识 #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);sockfd&#xff1a;表示要发送数据的套接字描述符。它是由socket函数创建的套接字返回的文件描述符。 buf&#xff1a;是一个指向要…