【基础】Flink -- ProcessFunction

news2024/11/16 3:48:36

Flink -- ProcessFunction

  • 处理函数概述
  • 处理函数
    • 基本处理函数 ProcessFunction
    • 按键分区处理函数 KeyedProcessFunction
      • 定时器与定时服务
      • 基于处理时间的分区处理函数
      • 基于事件时间的分区处理函数
    • 窗口处理函数 ProcessWindowFunction
  • 应用案例 -- Top N

处理函数概述

为了使代码拥有更强大的表现力和易用性,Flink 本身提供了多层 API 供我们选择,如下图所示。之前我们所学习的转换、聚合以及窗口函数等操作,都是基于 Flink 核心的 DataStream API 实现的。

在这里插入图片描述

在更底层,Flink 允许我们可以不定义任何具体的算子,而是提炼出了一个统一的处理操作。在这个处理函数中,我们可以对数据进行更加灵活的定制化的处理,其不限定我们具体要做什么,因此在理论再说我们可以实现任何操作。

本文用到的实体类代码以及源算子代码如下:

实体类 Event

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Event {

    public String user;
    public String url;
    public Long timestamp;

}

源算子 EventSource

public class EventSource implements SourceFunction<Event> {

    private Boolean flag = true;

    String[] users = {"曹操", "刘备", "孙权", "诸葛亮"};
    String[] urls = {"/home", "/test?id=1", "/test?id=2", "/play/football", "/play/basketball"};

    @Override
    public void run(SourceContext<Event> sourceContext) throws Exception {
        Random random = new Random();
        while (flag) {
            sourceContext.collect(new Event(
                    users[random.nextInt(users.length)],
                    urls[random.nextInt(urls.length)],
                    Calendar.getInstance().getTimeInMillis()
            ));
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        flag = false;
    }
}

处理函数

Flink 提供了 8 个不同的处理函数:

  • ProcessFunction:最基本的处理函数,基于DataStream调用process()并将该处理函数作为参数传入;

  • KeyedProcessFunction:对按键分区后的流的处理函数,基于KeyedStream调用process()并将该处理函数作为参数传入;

  • ProcessWindowFunction:开窗操作之后的处理函数,也是全窗口函数的代表,基于WindowedStream调用process()并将该处理函数作为参数传入;

  • ProcessAllWindowFunction:开窗操作之后的处理函数,基于AllWindowedStream调用process()并将该处理函数作为参数传入;

  • CoProcessFunction:合并两条流之后的处理函数,基于ConnectedStreams调用process()并将该处理函数作为参数传入;

  • ProcessJoinFunction:间接连接两条流之后的处理函数,基于IntervalJoined调用process()并将该处理函数作为参数传入;

  • BroadcastProcessFunction:广播连接流处理函数,基于BroadcastConnectedStream调用process()并将该处理函数作为参数传入;

  • KeyedBroadcastProcessFunction:基于按键分区的广播连接流的处理函数,基于BroadcastConnectedStream调用process()并将该处理函数作为参数传入;

基本处理函数 ProcessFunction

使用基本处理函数需要我们实例化抽象类ProcessFunction,其内部定义了两个抽象方法:

  • processElement():必须实现,用于处理元素。其传入的三个参数如下

    • value:当前正在被处理的元素,类型与流中的数据类型一致;

    • ctx:内部抽象类,代表当前正在运行的上下文,可以获取当前时间戳,并提供了用于查询时间和注册定时器的“定时服务”,以及可以将数据发送到“侧输出流” 的方法output()

    • out:用于返回输出数据;

  • onTimer():用于定义定时触发的操作,其同样需要传入三个参数

    • timestamp:设定好的时间,在事件时间语义下即水位线;

    • ctx:运行上下文;

    • out:用于返回输出数据;

处理函数都是基于事件触发的。水位线就如同插入流中的一条数据一样。只不过处理真正的数据事件调用的是processElement()方法,而处理水位线事件调用的是onTimer()

基本处理函数的基本使用代码如下:

public class ProcessFunctionDemo {

    public static void main(String[] args) throws Exception {
        // 1. 环境准备
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
        environment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        environment.setParallelism(1);
        // 2. 加载数据源并设置水位线
        environment
                // 2.1 加载数据源
                .addSource(new EventSource())
                // 2.2 获取时间戳、设置水位线
                .assignTimestampsAndWatermarks(WatermarkStrategy
                        .<Event>forMonotonousTimestamps()
                        .withTimestampAssigner((SerializableTimestampAssigner<Event>) (event, l) -> event.timestamp))
                // 2.3 设置处理函数
                .process(new ProcessFunction<Event, String>() {
                    @Override
                    public void processElement(Event event, ProcessFunction<Event, String>.Context context, Collector<String> collector) throws Exception {
                        if ("曹操".equals(event.user)) {
                            collector.collect(event.user + ">>>说曹操曹操到...");
                        } else if ("刘备".equals(event.user)) {
                            collector.collect(event.user + ">>>不可能,我二弟天下无敌!");
                        } else {
                            collector.collect("无关人等~");
                        }
                        System.out.println(longToDate(context.timerService().currentWatermark()));
                    }
                })
                // 2.4 执行输出
                .print();
        // 3. 执行程序
        environment.execute();
    }


    /**
     * long类型转换成日期
     *
     * @param lo 毫秒数
     * @return String yyyy-MM-dd HH:mm:ss
     */
    public static Date longToDate(long lo) throws ParseException {
        SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //long转Date
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(sd.format(new Date(lo)));
    }

}

按键分区处理函数 KeyedProcessFunction

定时器与定时服务

定时器 Timer 是处理函数当中进行时间相关的操作的主要机制,在onTimer()方法中可以自定义定时器触发的逻辑。而定时器触发的前提是该定时器已经注册且当前已经到达了触发时间。定时器的注册通过上下文提供的定时服务 TimerService 实现。

定时服务与当前运行环境有关,上下文 context 提供了timerService()方法可以直接获取TimerService对象。TimerService类中定义了关于时间和定时器的基础服务接口,主要包含以下 6 个方法:

// 获取当前的处理时间
long currentProcessingTime();
// 获取当前的水位线(事件时间)
long currentWatermark();
// 注册处理时间定时器,当处理时间超过 time 时触发
void registerProcessingTimeTimer(long time);
// 注册事件时间定时器,当水位线超过 time 时触发
void registerEventTimeTimer(long time);
// 删除触发时间为 time 的处理时间定时器
void deleteProcessingTimeTimer(long time);
// 删除触发时间为 time 的事件时间定时器
void deleteEventTimeTimer(long time);

这些方法总体上可以分为两大类,根据定义的时间语义的不同,分为基于处理时间的和基于事件时间的。对应的操作主要有三个,即获取当前时间、注册定时器、删除定时器。

基于处理时间的分区处理函数

基本使用代码如下,详细步骤见代码注释:

public class ProcessingTimeTimerDemo {

    public static void main(String[] args) throws Exception {
        // 1. 环境准备
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
        environment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        environment.setParallelism(1);
        // 2. 加载数据源并设置水位线
        environment
                // 2.1 加载数据源
                .addSource(new EventSource())
                // 处理时间语义,不需要分配时间戳和 watermark
                // 2.2 按键分区,这里将所有数据分配到同一区
                // 使用定时器,必须基于 KeyedStream
                .keyBy(event -> true)
                // 2.3 设置按键分区处理函数
                .process(new KeyedProcessFunction<Boolean, Event, Object>() {
                    @Override
                    public void processElement(Event event, KeyedProcessFunction<Boolean, Event, Object>.Context context, Collector<Object> collector) throws Exception {
                        long currTs = context.timerService().currentProcessingTime();
                        collector.collect("数据到达,到达时间>>>" + new Timestamp(currTs));
                        // 注册一个 10 秒后的定时器
                        context.timerService().registerProcessingTimeTimer(currTs + 10 * 1000L);
                    }

                    @Override
                    public void onTimer(long timestamp, KeyedProcessFunction<Boolean, Event, Object>.OnTimerContext ctx, Collector<Object> out) throws Exception {
                        out.collect("定时器触发,触发时间>>>" + new Timestamp(timestamp));
                    }
                })
                // 2.4 执行打印
                .print();
        // 3. 执行程序
        environment.execute();
    }

}

基于事件时间的分区处理函数

基本使用代码如下,详细步骤见代码注释:

public class EventTimeTimerDemo {

    public static void main(String[] args) throws Exception {
        // 1. 环境准备
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
        environment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        environment.setParallelism(1);
        // 2. 加载数据源并设置水位线
        environment
                // 2.1 加载数据源
                .socketTextStream("XXX.XX.XX.XXX", 8080)
                // 2.2 对数据源进行简单处理,封装成对象
                .map(new MapFunction<String, Event>() {
                    @Override
                    public Event map(String s) throws Exception {
                        String[] split = s.split(",");
                        return new Event(
                                split[0].trim(),
                                split[1].trim(),
                                Long.valueOf(split[2].trim())
                        );
                    }
                })
                // 2.3 获取时间戳、设置水位线
                .assignTimestampsAndWatermarks(WatermarkStrategy
                        .<Event>forMonotonousTimestamps()
                        .withTimestampAssigner((SerializableTimestampAssigner<Event>) (event, l) -> event.timestamp))
                // 2.4 设置按键分区处理函数
                .keyBy(event -> true)
                // 2.5 设置处理函数
                .process(new KeyedProcessFunction<Boolean, Event, String>() {

                    @Override
                    public void processElement(Event event, KeyedProcessFunction<Boolean, Event, String>.Context context, Collector<String> collector) throws Exception {
                        collector.collect("数据到达,时间戳>>>" + context.timestamp());
                        collector.collect("数据到达,水位线>>>" + context.timerService().currentWatermark());
                        // 注册一个 10 秒后的定时器
                        context.timerService().registerEventTimeTimer(context.timestamp() + 10 * 1000L);
                    }

                    @Override
                    public void onTimer(long timestamp, KeyedProcessFunction<Boolean, Event, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
                        out.collect("定时器触发,触发时间>>>" + timestamp);
                    }
                })
                // 2.6 执行打印
                .print();
        // 3. 执行程序
        environment.execute();
    }

}

执行测试,对应数据的输出以及定时器对应的数据分别用红色和黄色标注

在这里插入图片描述

窗口处理函数 ProcessWindowFunction

关于窗口处理函数的使用,在之前的Flink – Time and Window已经介绍过其基本的使用方法,示例代码如下:

public class ProcessWindowDemo {

    public static void main(String[] args) throws Exception {
        // 1. 环境准备
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
        environment.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        environment.setParallelism(1);
        // 2. 加载数据源并设置水位线
        SingleOutputStreamOperator<Event> stream = environment
                // 2.1 加载数据源
                .addSource(new EventSource())
                // 2.2 获取时间戳、设置水位线
                .assignTimestampsAndWatermarks(WatermarkStrategy
                        .<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner((SerializableTimestampAssigner<Event>) (event, l) -> event.timestamp));
        // 3. 数据处理及输出
        stream
                // 3.1 分区,将所有数据发送到一个分区进行统计
                .keyBy(item -> true)
                // 3.2 设置滚动事件时间窗口,窗口大小为 10s
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                // 3.3 定义窗口函数处理规则
                .process(new CustomProcessWindow())
                // 3.4 输出结果
                .print();
        // 4. 执行程序
        environment.execute();
    }

    public static class CustomProcessWindow extends ProcessWindowFunction<Event, String, Boolean, TimeWindow> {
        /**
         * 窗口函数处理规则,窗口关闭时执行处理
         */
        @Override
        public void process(Boolean aBoolean, ProcessWindowFunction<Event, String, Boolean, TimeWindow>.Context context,
                            Iterable<Event> iterable, Collector<String> collector) {
            // 创建用户统计Set
            HashSet<String> userSet = new HashSet<>();
            for (Event event: iterable) {
                userSet.add(event.user);
            }
            long start = context.window().getStart();
            long end = context.window().getEnd();
            // 定制输出内容
            collector.collect("窗口【" + new TimeStamp(start) + "~" + new TimeStamp(end)
                    + "】的独立访客数量为>>>" + userSet.size());
        }
    }

}

ProcessWindowFunction继承了AbstractRichFunction抽象类,其存在 4 个类型参数,按顺序分别为:

  • IN:即数据流中窗口函数输入的数据类型;

  • OUT:即窗口函数经过计算后输出的;

  • KEY:即数据中分区键 key 的类型;

  • W:即窗口的类型,一般使用TimeWindow

使用过程中需要实现抽象方法process(),该方法也包含 4 个参数,按序分别为:

  • key:分区字段;

  • context:当前窗口计算的上下文;

  • elements:窗口收集到的所有元素的可迭代集合;

  • out:用于发送数据输出结果的收集器;

应用案例 – Top N

使用之前学习的各种方法可以实现对访问量 Top N 的 url 的计算,使用到的实体类 EventUrlCount 代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class EventUrlCount {

    public String url;
    public Long count;
    public Long windowStart;
    public Long windowEnd;


}

业务实现代码如下:

public class TopNDemo {

    public static void main(String[] args) throws Exception {
        // 1. 配置环境
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
        environment.setParallelism(1);
        // 2. 数据处理
        environment
                // 2.1 添加数据源
                .addSource(new EventSource())
                // 2.2 设置水位线
                .assignTimestampsAndWatermarks(WatermarkStrategy
                        .<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }))
                // 2.3 按照 url 进行分区,统计 10s 的时间窗口内各个 url 的访问量
                .keyBy(event -> event.url)
                // 2.4 设置滑动窗口
                .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                // 2.5 设置窗口处理逻辑
                .aggregate(new UrlCountAgg(), new UrlCountRes())
                // 2.6 按窗口结束时间进行分区,统计相同时间窗口各 url 的访问量
                .keyBy(eventUrlCount -> eventUrlCount.windowEnd)
                // 2.7 设置处理函数计算top n
                .process(new TopN(2))
                // 2.8 执行输出
                .print();
        // 3. 执行程序
        environment.execute();

    }

    /**
     * 自定义增量聚合
     */
    public static class UrlCountAgg implements AggregateFunction<Event, Long, Long> {
        @Override
        public Long createAccumulator() {
            return 0L;
        }

        @Override
        public Long add(Event event, Long aLong) {
            return aLong + 1;
        }

        @Override
        public Long getResult(Long aLong) {
            return aLong;
        }

        @Override
        public Long merge(Long aLong, Long acc1) {
            return null;
        }
    }

    /**
     * 自定义全窗口函数
     */
    public static class UrlCountRes extends ProcessWindowFunction<Long, EventUrlCount, String, TimeWindow> {
        @Override
        public void process(String s, ProcessWindowFunction<Long, EventUrlCount, String, TimeWindow>.Context context,
                            Iterable<Long> iterable, Collector<EventUrlCount> collector) throws Exception {
            collector.collect(
                    new EventUrlCount(
                        s,
                        iterable.iterator().next(),
                        context.window().getStart(),
                        context.window().getEnd()
                    ));
        }
    }

    /**
     * 自定义处理函数,计算 top n
     */
    public static class TopN extends KeyedProcessFunction<Long, EventUrlCount, String> {
        // 定义属性 n
        private final Integer n;
        // 定义状态列表
        private ListState<EventUrlCount> urlCountListState;

        public TopN(Integer n) {
            this.n = n;
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            // 从环境中获取状态列表
            urlCountListState = getRuntimeContext().getListState(
                    new ListStateDescriptor<EventUrlCount>("event-url-count-list", Types.POJO(EventUrlCount.class))
            );
        }

        @Override
        public void processElement(EventUrlCount eventUrlCount, KeyedProcessFunction<Long, EventUrlCount, String>.Context context,
                                   Collector<String> collector) throws Exception {
            // 将数据保存至状态列表
            urlCountListState.add(eventUrlCount);
            // 设置定时器,在窗口关闭 1s 后触发
            context.timerService().registerEventTimeTimer(context.getCurrentKey() + 1L);
        }

        @Override
        public void onTimer(long timestamp, KeyedProcessFunction<Long, EventUrlCount, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
            // 将数据从状态列表取出并放入 ArrayList,方便排序
            ArrayList<EventUrlCount> urlCountArrayList = new ArrayList<>();
            for (EventUrlCount eventUrlCount: urlCountListState.get()) {
                urlCountArrayList.add(eventUrlCount);
            }
            // 清空状态列表
            urlCountListState.clear();
            // 执行排序
            urlCountArrayList.sort(new Comparator<EventUrlCount>() {
                @Override
                public int compare(EventUrlCount o1, EventUrlCount o2) {
                    return o2.count.intValue() - o1.count.intValue();
                }
            });
            // 组装结果并输出
            StringBuilder result = new StringBuilder();
            result.append("========================================\n");
            result.append("窗口结束时间:").append(new Timestamp(timestamp - 1)).append("\n");
            for (int i = 0; i < this.n; i++) {
                EventUrlCount eventUrlCount = urlCountArrayList.get(i);
                String info = "No." + (i + 1) + " " + "url:" + eventUrlCount.url + " "
                        + "浏览量:" + eventUrlCount.count + "\n";
                result.append(info);
            }
            result.append("========================================\n");
            out.collect(result.toString());
        }
    }

}

我们在上面的代码中使用ListState。在open()方法中初始化了列表状态变量,初始化的时候使用了ListStateDescriptor描述符,这个描述符用来告诉 Flink 列表状态变量的名字和类型。列表状态变量是单例,也就是说只会被实例化一次。这个列表状态变量的作用域是当前 key 所对应的逻辑分区。可以使用add()方法向列表状态变量中添加数据,使用get()方法读取列表状态变量中的所有元素。

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

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

相关文章

基于Springbot+微信小程序的购药平台的设计与实现

基于Springbot微信小程序的购药平台的设计与实现 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、…

Jackson 序列化:Cannot deserialize value of type `java.time.LocalDateTime`

问题描述 使用 jackson 反序列化异常如下&#xff1a; Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type java.time.LocalDateTime from String “2023-02-13 19:43:01”: Failed to deserialize java.time.LocalDat…

MySQL的四种事务隔离级别

目录一、事务的基本要素&#xff08;ACID&#xff09;1、原子性&#xff08;Atomicity&#xff09;&#xff1a;2、一致性&#xff08;Consistency&#xff09;&#xff1a;3、隔离性&#xff08;Isolation&#xff09;&#xff1a;4、持久性&#xff08;Durability&#xff09…

使用canvas给上传的整张图片添加平铺的水印

写在开头 哈喽&#xff0c;各位倔友们又见面了&#xff0c;本章我们继续来分享一个实用小技巧&#xff0c;给图片加水印功能&#xff0c;水印功能的目的是为了保护网站或作者版权&#xff0c;防止内容被别人利用或白嫖。 但是网络中&#xff0c;是没有绝对安全的&#xff0c;…

iOS接入Google登录

1.在Google Cloud后台配置客户端ID 首先要在 Google Cloud 中创建一个项目。新创建的Project需要先配置同意屏幕。一共有4步骤需要配置。 1.OAuth 同意屏幕 User Type选择"外部"进行创建。填写必必要的信息&#xff0c;应用名称、用户支持电子邮件地址、开发者电子邮…

chatGPT是什么?chatGPT运用场景有哪些?

大家好&#xff0c;小编来为大家解答以下问题 chatGPT是什么&#xff1f;&#xff0c;chatGPT概念股有哪些&#xff1f;一个有趣的事情&#xff0c;一个有趣的事情&#xff0c;现在让我们一起来看看吧&#xff01; 1、chatpgt是什么 ChatGPT是OpenAI开发的大型预训练语言模型。…

yolov5 onnx 前后处理+运行推理(暂记)

代码在这个基础上改的&#xff0c;虽然跑通了&#xff0c;还是很混乱&#xff0c;这里先简单记录一下处理的流程&#xff1a; yolov5 环境设置yolov5 网络结构ONNX yolov5导出 convert error --grid番外&#xff1a;onnx直接操作番外&#xff1a;yolov5的重新训练 result 0 -…

[安装之4] 联想ThinkPad 加装固态硬盘教程

方案&#xff1a;保留原有的机械硬盘&#xff0c;再加装一个固态硬盘作为系统盘。由于X250没有光驱&#xff0c;这样就无法使用第二个2.5寸的硬盘。还好&#xff0c;X250留有一个M.2接口&#xff0c;这样&#xff0c;就可以使用NGFF M.2接口的固态硬盘。不过&#xff0c;这种接…

短视频时代是靠什么赚钱的,介绍常见的5种方式,简单明了

目前&#xff0c;短视频越来越火热&#xff0c;大家都知道做短视频可以赚钱&#xff0c;那么究竟是靠什么赚钱的&#xff0c;又有几个人知道呢&#xff1f;短视频创业有个人、有团队&#xff0c;怎么实现团队的生存和发展。 常见的几种变现方式有&#xff1a; 1、平台分成 各…

C语言中用rand()函数产生一随机数

在C语言中如何产生一个随机数呢&#xff1f;用rand()函数。 rand()函数在头文件&#xff1a;#include <stdio.h>中&#xff0c;函数原型&#xff1a;int rand(void);。rand()会返回一个范围在0到RAND_MAX&#xff08;32767&#xff09;之间的随机数&#xff08;整数&…

Pytorch 基础之张量数据类型

学习之前&#xff1a;先了解 Tensor&#xff08;张量&#xff09; 官方文档的解释是&#xff1a; 张量如同数组和矩阵一样, 是一种特殊的数据结构。在PyTorch中, 神经网络的输入、输出以及网络的参数等数据, 都是使用张量来进行描述。 说白了就是一种数据结构 基本数据类型…

Python可以解码吗,解码打码是如何实现的

前言 咳咳&#xff0c;进来的铁汁都是抱着学习的心态进来看的吧&#xff0c;咱今天不讲解解码&#xff0c;咱来说说python如何来实现打码功能~ 这一个个进来的 都是标题党吧哈哈哈 有兴趣的可以继续看看哦 最近重温了一档综艺节目 至于叫什么 这里就不细说了 老是看着看着就…

【QT 5 相关实验-示波器-学习笔记-示波器组件练习与使用总结】

【QT 5 相关实验-示波器-学习笔记-示波器组件练习与使用总结】1、概述2、实验环境3、参考资料-致谢4、自我提升实验效果视频演示5、代码练习-学习后拆解-实验步骤&#xff08;1&#xff09;头文件部分-"mwaveview.h"&#xff08;2&#xff09;cpp文件部分-"mwav…

UDP数据报套接字编程

DatagramSocket API DatagramSocket 是UDP Socket&#xff0c;用于发送和接收UDP数据报。 DatagramSocket 构造方法&#xff1a; DatagramSocket 方法&#xff1a; DatagramPacket API DatagramPacket是UDP Socket发送和接收的数据报。 DatagramPacket 构造方法&#xff…

本地生成动漫风格 AI 绘画 图像|Stable Diffusion WebUI 的安装和局域网部署教程

Stable Diffusion WebUI 的安装和部署教程1. 简介2. 安装环境2.1 Windows2.2 Linux3. 运行4. 模型下载链接5. 局域网部署5.1 Windows5.2 Linux6. 其他资源1. 简介 先放一张WebUI的图片生成效果图&#xff0c;以给大家学习的动力 &#xff1a;&#xff09; 怎么样&#xff0c;…

浅析SAS协议(1):基本介绍

文章目录概述SAS协议发展历程SAS技术特性SAS设备拓扑SAS phySAS地址SAS设备类型SAS协议分层参考链接概述 SAS&#xff0c;全称Serial Attached SCSI&#xff0c;即串行连结SCSI&#xff0c;是一种采用了串行总线的高速互连技术。通过物理上使用串行总线连结&#xff0c;在链路…

用一行Python代码,为图片上水印版权!

今天一个朋友跟我吐槽&#xff1a;前段时间&#xff0c;我辛辛苦苦整理的一份XX攻略&#xff0c;分享给自己的一些朋友&#xff0c;结果今天看到有人堂而皇之地拿着这份攻略图片去引流&#xff0c;并声称是自己整理的&#xff0c;真是岂有此理&#xff01;他自己总结吃一堑长一…

超低成本DDoS攻击来袭,看WAF如何绝地防护

一、DDoS攻击&#xff0c;不止于网络传输层 网络世界里为人们所熟知的DDoS攻击&#xff0c;多数是通过对带宽或网络计算资源的持续、大量消耗&#xff0c;最终导致目标网络与业务的瘫痪&#xff1b;这类DDOS攻击&#xff0c; 工作在OSI模型的网络层与传输层&#xff0c;利用协…

【MyBatis】源码学习 03 - 类型处理器 TypeHandler

文章目录前言参考目录学习笔记1、type 包中类的归类总结2、类型处理器2.1、TypeReference 类3、类型注册表3.1、TypeHandlerRegistry#getTypeHandler前言 本文内容对应的是书本第 8 章的内容&#xff0c;主要是关于类型处理器 TypeHandler 的学习。 这一章节的学习有些地方理…

Java爬虫——WebMagic案例

抓取51Job的招聘信息一&#xff0c; Scheduler组件在解析页面的时候&#xff0c;很可能会解析出相同的url地址(例如商品标题和商品图片超链接&#xff0c;而且url一样)&#xff0c;如果不进行处理&#xff0c;同样的url会解析处理多次&#xff0c;浪费资源。所以我们需要有一个…