Netty系列-7 Netty编解码器

news2024/11/17 13:52:24

背景

netty框架中,自定义解码器的起点是ByteBuf类型的消息, 自定义编码器的终点是ByteBuf类型。

1.解码器

业务解码器的起点是ByteBuf类型

netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter使用默认方式(不处理,向下传递事件)实现了所有的Inbound接口。因此,MessageToMessageEncoder只需要重写channelRead方法,并在该方法中提取消息、转换消息、通过ChannelInvoker将转换后的消息以channelRead事件发向pipeline即可。
MessageToMessageEncoder抽象类的实现如下:

public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter {

    private final TypeParameterMatcher matcher;

    protected MessageToMessageDecoder() {
        matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I");
    }

    protected MessageToMessageDecoder(Class<? extends I> inboundMessageType) {
        matcher = TypeParameterMatcher.get(inboundMessageType);
    }
    
    public boolean acceptInboundMessage(Object msg) throws Exception {
        return matcher.match(msg);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            if (acceptInboundMessage(msg)) {
                I cast = (I) msg;
                try {
                    decode(ctx, cast, out);
                } finally {
                    ReferenceCountUtil.release(cast);
                }
            } else {
                out.add(msg);
            }
        } catch (DecoderException e) {
            throw e;
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            try {
                int size = out.size();
                for (int i = 0; i < size; i++) {
                    ctx.fireChannelRead(out.getUnsafe(i));
                }
            } finally {
                out.recycle();
            }
        }
    }

    protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

1.1 类型的匹配器

MessageToMessageDecoder内部维护了一个TypeParameterMatcher类型的匹配器对象matcher,用于指定解码器可以处理的消息类型。可通过构造函数为其设置类型,也可通过泛型指定:

// 使用泛型类型
protected MessageToMessageDecoder() {
    matcher = TypeParameterMatcher.find(this, MessageToMessageDecoder.class, "I");
}

// 子类调用MessageToMessageDecoder构造器时,传入类型
protected MessageToMessageDecoder(Class<? extends I> inboundMessageType) {
    matcher = TypeParameterMatcher.get(inboundMessageType);
}

一般,通过泛型指定解码器处理的消息对象,即使用MessageToMessageDecoder的无参构造函数。
acceptInboundMessage方法封装matcher的实现,返回布尔值,表示是否支持处理msg消息类型:

public boolean acceptInboundMessage(Object msg) throws Exception {
    return matcher.match(msg);
}

根据matcher的match方法:

private static final class ReflectiveMatcher extends TypeParameterMatcher {
    private final Class<?> type;

    //...

    @Override
    public boolean match(Object msg) {
        // msg消息是否为type类型或者其子类型
        return type.isInstance(msg);
    }
}

1.2 channelRead方法

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 构造List列表对象,存储解码后的对象
    CodecOutputList out = CodecOutputList.newInstance();
    try {
        // 判断是否支持处理消息
        if (acceptInboundMessage(msg)) {
            I cast = (I) msg;
            try {
                // 处理消息,将cast对象解码后的结果存放到out列表中
                decode(ctx, cast, out);
            } finally {
                ReferenceCountUtil.release(cast);
            }
        } else {
            // 不处理消息,以原样保存
            out.add(msg);
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Exception e) {
        throw new DecoderException(e);
    } finally {
        try {
            int size = out.size();
            // 遍历列表,依次向pipeline触发解码后的对象
            for (int i = 0; i < size; i++) {
                ctx.fireChannelRead(out.getUnsafe(i));
            }
        } finally {
            out.recycle();
        }
    }
}

逻辑较为清晰:
[1] 构造列表对象out,用于临时存放解码后的消息;
[2] 判断当前解码器是否可以处理该消息,不可以处理,直接添加到out中;可以处理,调用decode方法解码消息,解码结果都添加到out中;
[3] 遍历out列表,将消息以ChannelRead事件传递给向pipeline;
[4] out清理、回收再利用;

1.3 decode方法

decode方法是实际进行消息转换的逻辑,由子类根据业务具体实现:

protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

将msg解码,解码后的对象存放在out中;由于out是数组,因此可以从msg中解码出一个对象,也可以解码出多个。如下所示:

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
    out.add(msg.toString(charset));
}

将ByteBuf类型的msg消息转为一个String类型的对象;

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
    String[] decodedMsgs = msg.toString(charset).split(";");
    for (String decodedMsg: decodedMsgs) {
        out.add(decodedMsg);
    }
}

将ByteBuf转为String,并按照;分隔符进行拆分,每个字符串作为一个消息对象。

2.解码器案例

案例的结构图如下所示,消息流入解码器和流出时的消息类型会发生变化:
在这里插入图片描述
引入三个解码器和一个业务Handler:
[1] 编码器1实现ByteBuf->String类型的转换;
[2] 编码器2实现String->Message1类型的转换;
[3] 编码器3实现Message1->Message2类型的转换;
[4] 业务Handler打印消息类型和消息;
实现类依次为:

// MyMessageDecoder1
public class MyMessageDecoder1 extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
        out.add(msg.toString(Charset.defaultCharset()));
    }
}

// MyMessageDecoder2
class MyMessageDecoder2 extends MessageToMessageDecoder<String> {
    @Override
    protected void decode(ChannelHandlerContext ctx, String msg, List<Object> out) {
        String[] decodedMsgs = msg.split(";");
        for (String decodedMsg : decodedMsgs) {
            out.add(new Message1(decodedMsg));
        }
    }
}

// MyMessageDecoder3
class MyMessageDecoder3 extends MessageToMessageDecoder<Message1> {
    @Override
    protected void decode(ChannelHandlerContext ctx, Message1 msg, List<Object> out) {
            out.add(new Message2(msg.getContent()));
    }
}

业务Handler定义如下:

private static class MyHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println(msg);
    }
}

Message1和Message2消息定义如下:

@Data
@RequiredArgsConstructor
pulic class Message1 {
    private final String content;
}

@Data
@RequiredArgsConstructor
pulic class Message2 {
    private final String content;
}

客户端发送消息:"test1;test2;test3"时:

Microsoft Telnet> send test1;test2;test3
发送字符串 test1;test2;test3
Microsoft Telnet>

服务器日志如下所示:

Message2(content=test1)
Message2(content=test2)
Message2(content=test3)

注意:解码的顺序沿着pipeline进行,因此需要注意调整netty解码器在pipeline中的位置。

如果将3和解码器2的顺序调整一下:

protected void initChannel(NioSocketChannel channel) {
    channel.pipeline().addLast(new MyMessageDecoder1());
    channel.pipeline().addLast(new MyMessageDecoder3());
    channel.pipeline().addLast(new MyMessageDecoder2());
    channel.pipeline().addLast(new MyHandler());
}

重复上述操作,服务器日志如下:

Message1(content=test1)
Message1(content=test2)
Message1(content=test3)

此时,解码器1流出的数据为String类型,流入解码器2时-类型校验不通过直接以流入的String类型流出,流入解码器3时,将String类型转为Message1类型,流入业务Handler进行打印。

3.编码器

业务编码器的终点是ByteBuf类型

netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelOutboundHandlerAdapter,ChannelOutboundHandlerAdapter使用默认方式实现(不处理,向前传递事件)了所有的Outbound接口。因此,MessageToMessageEncoder只需要重写write方法,并在该方法中编码消息、并通过ChannelInvoker将编码后的消息发送到pipeline即可。
MessageToMessageEncoder抽象类的实现如下:

public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter {
    private final TypeParameterMatcher matcher;

    protected MessageToMessageEncoder() {
        matcher = TypeParameterMatcher.find(this, MessageToMessageEncoder.class, "I");
    }

    protected MessageToMessageEncoder(Class<? extends I> outboundMessageType) {
        matcher = TypeParameterMatcher.get(outboundMessageType);
    }

    public boolean acceptOutboundMessage(Object msg) throws Exception {
        return matcher.match(msg);
    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        CodecOutputList out = null;
        try {
            if (acceptOutboundMessage(msg)) {
                out = CodecOutputList.newInstance();
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                try {
                    encode(ctx, cast, out);
                } finally {
                    ReferenceCountUtil.release(cast);
                }

                if (out.isEmpty()) {
                    throw new EncoderException(
                            StringUtil.simpleClassName(this) + " must produce at least one message.");
                }
            } else {
                ctx.write(msg, promise);
            }
        } catch (EncoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new EncoderException(t);
        } finally {
            if (out != null) {
                try {
                    final int sizeMinusOne = out.size() - 1;
                    if (sizeMinusOne == 0) {
                        ctx.write(out.getUnsafe(0), promise);
                    } else if (sizeMinusOne > 0) {
                        if (promise == ctx.voidPromise()) {
                            writeVoidPromise(ctx, out);
                        } else {
                            writePromiseCombiner(ctx, out, promise);
                        }
                    }
                } finally {
                    out.recycle();
                }
            }
        }
    }

    private static void writeVoidPromise(ChannelHandlerContext ctx, CodecOutputList out) {
        final ChannelPromise voidPromise = ctx.voidPromise();
        for (int i = 0; i < out.size(); i++) {
            ctx.write(out.getUnsafe(i), voidPromise);
        }
    }
    private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) {
        final PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
        for (int i = 0; i < out.size(); i++) {
            combiner.add(ctx.write(out.getUnsafe(i)));
        }
        combiner.finish(promise);
    }

    protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
}

3.1 类型的匹配器

MessageToMessageEncoder内部维护了一个TypeParameterMatcher类型的匹配器对象matcher,用于指定该编码器器可以处理的消息类型,与解码器中的matcher作用完全相同,不再赘述。

3.2 write方法

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    CodecOutputList out = null;
    try {
        // 判断当前编码器是否可以编码消息
        if (acceptOutboundMessage(msg)) {
            out = CodecOutputList.newInstance();
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            try {
                // 编码消息,并将编码后的消息保存到out列表中
                encode(ctx, cast, out);
            } finally {
                ReferenceCountUtil.release(cast);
            }
            if (out.isEmpty()) {
                throw new EncoderException(
                    StringUtil.simpleClassName(this) + " must produce at least one message.");
            }
        } else {
            // 不能编码的消息不处理,直接沿着pipeline向前传递
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable t) {
        throw new EncoderException(t);
    } finally {
        // 遍历out,依次调用ctx.write,沿着pipeline向前传递
        if (out != null) {
            try {
                final int sizeMinusOne = out.size() - 1;
                if (sizeMinusOne == 0) {
                    ctx.write(out.getUnsafe(0), promise);
                } else if (sizeMinusOne > 0) {
                    if (promise == ctx.voidPromise()) {
                        writeVoidPromise(ctx, out);
                    } else {
                        writePromiseCombiner(ctx, out, promise);
                    }
                }
            } finally {
                // 清理out列表,回收再利用
                out.recycle();
            }
        }
    }
}

逻辑较为清晰:
[1] 构造列表对象out,用于临时存放编码后的消息;
[2] 判断当前编码器是否可以处理该消息,不可以处理,直接通过ctx.write沿着pipeline向前传递;可以处理,调用encode方法编码消息,编码结果添加到out中;
[3] 遍历out列表,将消息以write事件传递给向pipeline;
[4] out清理、回收再利用;

3.2 encode方法

encode方法是实际进行消息转换的逻辑,由子类根据业务具体实现:

protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

将msg消息进行编码,编码后的对象存放在out中;由于out是数组,因此可以从msg中编码出一个对象,也可以编码出多个,与解码器逻辑相同。

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
    out.add(msg.toString(charset));
}

将ByteBuf类型的msg消息转为一个String类型的对象;

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
    String[] decodedMsgs = msg.toString(charset).split(";");
    for (String decodedMsg: decodedMsgs) {
        out.add(decodedMsg);
    }
}

将ByteBuf转为String,并按照;分隔符进行拆分,每个字符串作为一个消息对象。
netty向外发送数据时,一般经过业务Handler->编码器->HeadContext的流程。
向客户端发送消息的底层实现在HeadContext的unsafe对象(NioSocketChannel的unsafe对象)中,而发送前有消息类型判断:

final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler{ 
	public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
		unsafe.write(msg, promise);
	}
}

unsafe对象的write方法如下:

public final void write(Object msg, ChannelPromise promise) {
    //...
    msg = filterOutboundMessage(msg);
    //...
}

在真实写操作前,通过filterOutboundMessage进行消息类型的判断:

@Override
protected final Object filterOutboundMessage(Object msg) {
    // 要求消息必须时ByteBuf或者FileRegion类型或其子类型
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf) msg;
        if (buf.isDirect()) {
            return msg;
        }
        return newDirectBuffer(buf);
    }

    if (msg instanceof FileRegion) {
        return msg;
    }

    throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
}

由此,编码器将消息传递给HeadContext前,需要将消息最终编码为ByteBuf类型。

4.解码器案例

案例结构图如下所示:
在这里插入图片描述

在章节2中的案例基础上新增两个编码器,并修改业务Handler:
[1] 业务Handler,接收客户端消息后,响应相同消息;
[2] 编码器1:将Message2类型的消息转为String类型;
[3] 编码器2: 将String类型消息转为ByteBuf类型;
代码实现如下:
修改业务Handler:

private static class MyHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println(msg);
        // 新增逻辑“将消息对象发送给客户端
        ctx.write(msg);
    }
}

添加编码器:

// 将Message2消息转为String消息
public class MyEncoder1 extends MessageToMessageEncoder<Message2> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message2 msg, List<Object> out) throws Exception {
        out.add(msg.getContent());
    }
}

// 将String消息转为ByteBuf消息
public class MyEncoder2 extends MessageToMessageEncoder<String> {
    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) {
        out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), Charset.defaultCharset()));
    }
}

在MyHandler前依次添加解码器MyEncoder2和MyEncoder1:

protected void initChannel(NioSocketChannel channel) {
    channel.pipeline().addLast(new MyMessageDecoder1());
    channel.pipeline().addLast(new MyMessageDecoder2());
    channel.pipeline().addLast(new MyMessageDecoder3());
    channel.pipeline().addLast(new MyEncoder2());
    channel.pipeline().addLast(new MyEncoder1());
    channel.pipeline().addLast(new MyHandler());
}

可以使用Netty写一个客户端, 也可用客户端工具模拟,这里为了方便,使用SocketTool.exe,控制台日志如下:

14:36:15 发送数据:test1;test2;test3[1次]
14:36:15 收到数据:test1test2test3

注意:客户端收到了test1test2test3消息,在客户端开来是一个消息,但在服务器看来是连续发送的3个消息,消息内容分别为test1和test2和test3。这是TCP的流传输模式导致,可在业务层添加额外处理解决这个问题。将在下一篇文件介绍Netty如何处理粘包和分包问题。

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

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

相关文章

用于高频交易预测的最优输出LSTM

用于高频交易预测的最优输出LSTM J.P.Morgan的python教程 Content 本文提出了一种改进的长短期记忆&#xff08;LSTM&#xff09;单元&#xff0c;称为最优输出LSTM&#xff08;OPTM-LSTM&#xff09;&#xff0c;用于实时选择最佳门或状态作为最终输出。这种单元采用浅层拓…

CSS 盒子属性

1. 盒子模型组成 1.1 边框属性 1.1.1 四边分开写 1.1.2 合并线框 1.1.3 边框影响盒子大小 1.2 内边距 注意&#xff1a; 1.3 外边距 1.3.1 嵌套块元素垂直外边距的塌陷 1.4 清除内外边距 1.5 总结

使用YOLO11训练自己的数据集【下载模型】-【导入数据集】-【训练模型】-【评估模型】-【导出模型】

目录 前言&#xff1a;一、下载模型二、导入数据集三、训练自己的数据集四、验证数据集五、测试数据集 前言&#xff1a; YOLO11于2024年9月30日由YOLOv8团队正式发布&#xff0c;为了让我们能够趁热打铁早发论文&#xff0c;接下来让我们仔细研究一下如何使用YOLO11训练自己的…

通信协议感悟

本文结合个人所学&#xff0c;简要讲述SPI&#xff0c;I2C&#xff0c;UART通信的特点&#xff0c;限制。 1.同步通信 UART&#xff0c;SPI&#xff0c;I2C三种串行通讯方式&#xff0c;SPI功能引脚为CS&#xff0c;CLK&#xff0c;MOSI&#xff0c;MISO&#xff1b;I2C功能引…

六、输入输出管理

1.输入输出程序接口 由于各种设备的操作所提供的参数或者返回值都不同&#xff0c;也很难做到以设备独立性软件向上提供统一的接口&#xff0c;但是可以将设备进行分类&#xff0c;每一类设备由一种统一的接口操作。 ①字符设备接口 get/put 系统调用:向字符设备读/写一个字符…

Redis篇(Redis原理 - RESP协议)

目录 一、简介 二、Redis通信协议 基于Socket自定义Redis的客户端 三、Redis内存回收 1. 过期key处理 1.1. 惰性删除 1.2. 周期删除 1.3. 知识小结 2. 内存淘汰策略 一、简介 Redis是一个CS架构的软件&#xff0c;通信一般分两步&#xff08;不包括pipeline和PubSub&a…

【Linux系统编程】第二十六弹---彻底掌握文件I/O:C/C++文件接口与Linux系统调用实践

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、回顾C语言文件接口 1.1、以写的方式打开文件 1.2、以追加的方式打开文件 2、初步理解文件 2.1、C文件接口 3、进一步理…

街道办事处智慧查询系统方案——未来之窗行业应用跨平台架构

一、政务公开建设的必要性 1.1.1使用便捷 人民的生活因为科学技术的发展&#xff0c;发生了翻天覆地的变化&#xff0c;也是实现智能化社区发展的重要发展环节&#xff0c;并在触摸一体机设备的运用中&#xff0c;“社区办事查询软件”运用&#xff0c;基本能够实现***科学技…

【优选算法】(第十五篇)

目录 和为k的⼦数组&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 和可被K整除的⼦数组&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 和为k的⼦数组&#xff08;medium&#xff09; 题目解析 1.题目链接&#xff1a;. - 力扣&am…

【EXCEL数据处理】000010 案列 EXCEL文本型和常规型转换。使用的软件是微软的Excel操作的。处理数据的目的是让数据更直观的显示出来,方便查看。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000010 案列 EXCEL单元格格式。EXCEL文本型和常规型转…

TypeScript 算法手册 【归并排序】

文章目录 1. 归并排序简介1.1 归并排序定义1.2 归并排序特点 2. 归并排序步骤过程拆解2.1 分割数组2.2 递归排序2.3 合并有序数组 3. 归并排序的优化3.1 原地归并排序3.2 混合插入排序案例代码和动态图 4. 归并排序的优点5. 归并排序的缺点总结 【 已更新完 TypeScript 设计模式…

nature reviews genetics | 基因调控网络方法总结

–https://doi.org/10.1038/s41576-023-00618-5 Gene regulatory network inference in the era of single-cell multi-omics 留意更多内容&#xff0c;欢迎关注微信公众号&#xff1a;组学之心 研究团队和单位 Julio Saez-Rodriguez—Heidelberg University Ricard Arge…

Let‘s Encrypt 的几个常用命令

Lets Encrypt 是免费的 ssl 证书提供商&#xff0c;在当前纷纷收费的形式下&#xff0c;这是一个良心厂家&#xff0c;虽然使用起来略微繁琐。坚决抵制某 cxxn 站&#xff0c;竟然开始有辣么多收费的东西。这里记录几个常用的命令&#xff08;使用环境Ubuntu 24&#xff09;&am…

Proxmox使用tc给虚拟机限速,实现不对等网速——浪浪云

文章目录 前言第一步查看虚拟机的虚拟网卡名字第二部 设置上传速度限制第三部 设置下载速度限制第四部 验证是否成功查看队列调度器查看过滤器 第五步 如何解除网卡限速 前言 由于proxmox虚拟机限速只能限速对等&#xff0c;但是我想让下载和上传速度不对等&#xff0c;例如上传…

录屏软件大比拼:四款必备工具助你轻松录制精彩瞬间!

哎呀&#xff0c;说到电脑录屏这事儿&#xff0c;我这个办公室小文员可是深有体会啊&#xff01;平时工作里&#xff0c;经常需要录个会议啊、做个教程啊&#xff0c;或者分享个操作技巧给同事们看。市面上的录屏软件多得数不清&#xff0c;但我最常用的几款工具。今天就来跟大…

Vue3 proxy跨域代理

一、跨域问题 假设vue项目的运行地址为&#xff1a;http://localhost:5173&#xff0c;此时我们想要调用后端服务的rest api&#xff0c;而后端接口暴露的地址为&#xff1a;https://192.168.1.1:8080/user。 可以发现前端服务与后端服务的域名是不同的&#xff0c;默认情况下…

C语言 | Leetcode C语言题解之第445题两数相加II

题目&#xff1a; 题解&#xff1a; struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){int stack1[100];int stack2[100];int top1 0;int top2 0;int carry 0;int sum 0;struct ListNode* temp NULL;struct ListNode* head NULL;while (l1) {…

基于ssm大学生自主学习网站的设计与实现

文未可获取一份本项目的java源码和数据库参考。 1、毕业论文&#xff08;设计&#xff09;的背景及意义&#xff1a; &#xff08;1&#xff09;研究背景 目前&#xff0c;因特网是世界上最大的计算机互联网络&#xff0c;它通过网络设备将世界各地互相独立的不同规模的局域…

五、Drf权限组件

五、权限组件 权限组件=[权限类,权限类,权限类…] 执行所有权限类的has_permission方法,通过返回True,不通过返回False 默认情况下,所有的权限类都通过,才返回True 5.1简单应用权限组件 #ext.per class MyPermission1(BasePermission):def has_permission(self, requ…

Redis篇(Redis原理 - 网络模型)

目录 一、用户空间和内核态空间 二、阻塞IO 三、非阻塞IO 四、IO多路复用 五、IO多路复用-select方式 六、IO多路复用模型-poll模式 七、IO多路复用模型-epoll函数 八、网络模型-epoll中的ET和LT 九、网络模型-基于epoll的服务器端流程 十、网络模型-信号驱动 异步IO…