Netty 源码中对 Redis 协议的实现

2023-05-16

原文地址: haifeiWu的博客
博客地址:www.hchstudio.cn
欢迎转载,转载请注明作者及出处,谢谢!

近期一直在做网络协议相关的工作,所以博客也就与之相关的比较多,今天楼主结合 Redis的协议 RESP 看看在 Netty 源码中是如何实现的。

RESP 协议

RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现非常简单,解析性能极好。

Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符号\r\n,来表示该单元的结束。
1. 单行字符串 以 + 符号开头。
2. 多行字符串 以 $ 符号开头,后跟字符串长度。
3. 整数值 以 : 符号开头,后跟整数的字符串形式。
4. 错误消息 以 - 符号开头。
5. 数组 以 * 号开头,后跟数组的长度。

关于 RESP 协议的具体介绍感兴趣的小伙伴请移步楼主的另一篇文章Redis协议规范(译文)

Netty 中 RESP 协议的定义

如下面代码中所表示的,Netty中使用对应符号的ASCII码来表示,感兴趣的小伙伴可以查一下ASCII码表来验证一下。


public enum RedisMessageType {
    // 以 + 开头的单行字符串
    SIMPLE_STRING((byte)43, true),
    // 以 - 开头的错误信息
    ERROR((byte)45, true),
    // 以 : 开头的整型数据
    INTEGER((byte)58, true),
    // 以 $ 开头的多行字符串
    BULK_STRING((byte)36, false),
    // 以 * 开头的数组
    ARRAY_HEADER((byte)42, false),
    ARRAY((byte)42, false);

    private final byte value;
    private final boolean inline;

    private RedisMessageType(byte value, boolean inline) {
        this.value = value;
        this.inline = inline;
    }

    public byte value() {
        return this.value;
    }

    public boolean isInline() {
        return this.inline;
    }

    public static RedisMessageType valueOf(byte value) {
        switch(value) {
        case 36:
            return BULK_STRING;
        case 42:
            return ARRAY_HEADER;
        case 43:
            return SIMPLE_STRING;
        case 45:
            return ERROR;
        case 58:
            return INTEGER;
        default:
            throw new RedisCodecException("Unknown RedisMessageType: " + value);
        }
    }
}

Netty 中 RESP 解码器实现

解码器,顾名思义,就是将服务器返回的数据根据协议反序列化成易于阅读的信息。RedisDecoder 就是根据 RESP 将服务端返回的信息反序列化出来。下面是指令的编码格式

SET key value => *3\r\n$5\r\nSET\r\n$1\r\nkey\r\n$1\r\nvalue\r\n

指令是一个字符串数组,编码一个字符串数组,首先需要编码数组长度*3\r\n。然后依次编码各个字符串参数。编码字符串首先需要编码字符串的长度$5\r\n。然后再编码字符串的内容SET\r\n。Redis 消息以\r\n作为分隔符,这样设计其实挺浪费网络传输流量的,消息内容里面到处都是\r\n符号。但是这样的消息可读性会比较好,便于调试。RESP 协议是牺牲性能换取可读,易于实现的一个经典例子。

指令解码器的实现,网络字节流的读取存在拆包问题。所拆包问题是指一次Read调用从套件字读到的字节数组可能只是一个完整消息的一部分。而另外一部分则需要发起另外一次Read调用才可能读到,甚至要发起多个Read调用才可以读到完整的一条消息。对于拆包问题感兴趣的小伙伴可以查看楼主的另一篇文章TCP 粘包问题浅析及其解决方案

如果我们拿部分消息去反序列化成输入消息对象肯定是要失败的,或者说生成的消息对象是不完整填充的。这个时候我们需要等待下一次Read调用,然后将这两次Read调用的字节数组拼起来,尝试再一次反序列化。

问题来了,如果一个输入消息对象很大,就可能需要多个Read调用和多次反序列化操作才能完整的解包出一个输入对象。那这个反序列化的过程就会重复了多次。

针对这个问题,Netty 中很巧妙的解决了这个问题,如下所示,Netty 中通过 state 属性来保存当前序列化的状态,然后下次反序列化的时候就可以从上次记录的 state 直接继续反序列化。这样就避免了重复的问题。

    // 保持当前序列化状态的字段
    private RedisDecoder.State state;

    public RedisDecoder() {
        this(65536, FixedRedisMessagePool.INSTANCE);
    }

    public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool) {
        this.toPositiveLongProcessor = new RedisDecoder.ToPositiveLongProcessor();
        // 默认初始化状态为,反序列化指令类型
        this.state = RedisDecoder.State.DECODE_TYPE;
        if (maxInlineMessageLength > 0 && maxInlineMessageLength <= 536870912) {
            this.maxInlineMessageLength = maxInlineMessageLength;
            this.messagePool = messagePool;
        } else {
            throw new RedisCodecException("maxInlineMessageLength: " + maxInlineMessageLength + " (expected: <= " + 536870912 + ")");
        }
    }

    // 解码器的主要业务逻辑
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        try {
            // 循环读取信息,将信息完成的序列化
            while(true) {
                switch(this.state) {
                case DECODE_TYPE:
                    if (this.decodeType(in)) {
                        break;
                    }

                    return;
                case DECODE_INLINE:
                    if (this.decodeInline(in, out)) {
                        break;
                    }

                    return;
                case DECODE_LENGTH:
                    if (this.decodeLength(in, out)) {
                        break;
                    }

                    return;
                case DECODE_BULK_STRING_EOL:
                    if (this.decodeBulkStringEndOfLine(in, out)) {
                        break;
                    }

                    return;
                case DECODE_BULK_STRING_CONTENT:
                    if (this.decodeBulkStringContent(in, out)) {
                        break;
                    }

                    return;
                default:
                    throw new RedisCodecException("Unknown state: " + this.state);
                }
            }
        } catch (RedisCodecException var5) {
            this.resetDecoder();
            throw var5;
        } catch (Exception var6) {
            this.resetDecoder();
            throw new RedisCodecException(var6);
        }
    }

下面代码中,是针对每种数据类型进行反序列化的具体业务逻辑。有小伙伴可能会想,没有看到解码胡数组类型的逻辑呢?实际上在 RESP 协议中数组就是其他类型的组合,所以完全可以循环读取,按照单个元素解码。

// 解码消息类型
 private boolean decodeType(ByteBuf in) throws Exception {
        if (!in.isReadable()) {
            return false;
        } else {
            this.type = RedisMessageType.valueOf(in.readByte());
            this.state = this.type.isInline() ? RedisDecoder.State.DECODE_INLINE : RedisDecoder.State.DECODE_LENGTH;
            return true;
        }
    }

    // 解码单行字符串,错误信息,或者整型数据类型
    private boolean decodeInline(ByteBuf in, List<Object> out) throws Exception {
        ByteBuf lineBytes = readLine(in);
        if (lineBytes == null) {
            if (in.readableBytes() > this.maxInlineMessageLength) {
                throw new RedisCodecException("length: " + in.readableBytes() + " (expected: <= " + this.maxInlineMessageLength + ")");
            } else {
                return false;
            }
        } else {
            out.add(this.newInlineRedisMessage(this.type, lineBytes));
            this.resetDecoder();
            return true;
        }
    }

    // 解码消息长度
    private boolean decodeLength(ByteBuf in, List<Object> out) throws Exception {
        ByteBuf lineByteBuf = readLine(in);
        if (lineByteBuf == null) {
            return false;
        } else {
            long length = this.parseRedisNumber(lineByteBuf);
            if (length < -1L) {
                throw new RedisCodecException("length: " + length + " (expected: >= " + -1 + ")");
            } else {
                switch(this.type) {
                case ARRAY_HEADER:
                    out.add(new ArrayHeaderRedisMessage(length));
                    this.resetDecoder();
                    return true;
                case BULK_STRING:
                    if (length > 536870912L) {
                        throw new RedisCodecException("length: " + length + " (expected: <= " + 536870912 + ")");
                    }

                    this.remainingBulkLength = (int)length;
                    return this.decodeBulkString(in, out);
                default:
                    throw new RedisCodecException("bad type: " + this.type);
                }
            }
        }
    }

    // 解码多行字符串
    private boolean decodeBulkString(ByteBuf in, List<Object> out) throws Exception {
        switch(this.remainingBulkLength) {
        case -1:
            out.add(FullBulkStringRedisMessage.NULL_INSTANCE);
            this.resetDecoder();
            return true;
        case 0:
            this.state = RedisDecoder.State.DECODE_BULK_STRING_EOL;
            return this.decodeBulkStringEndOfLine(in, out);
        default:
            out.add(new BulkStringHeaderRedisMessage(this.remainingBulkLength));
            this.state = RedisDecoder.State.DECODE_BULK_STRING_CONTENT;
            return this.decodeBulkStringContent(in, out);
        }
    }

Netty 中 RESP 编码器实现

编码器,顾名思义,就是将对象根据 RESP 协议序列化成字节流发送到服务端。编码器的实现非常简单,不用考虑拆包等问题,就是分配一个ByteBuf,然后将将消息输出对象序列化的字节数组塞到ByteBuf中输出就可以了。

下面代码中就是 encode 方法直接调用 writeRedisMessage 方法,根据消息类型进行写buffer操作。

    @Override
    protected void encode(ChannelHandlerContext ctx, RedisMessage msg, List<Object> out) throws Exception {
        try {
            writeRedisMessage(ctx.alloc(), msg, out);
        } catch (CodecException e) {
            throw e;
        } catch (Exception e) {
            throw new CodecException(e);
        }
    }

    private void writeRedisMessage(ByteBufAllocator allocator, RedisMessage msg, List<Object> out) {
        // 判断消息类型,然后调用写相应消息的方法。
        if (msg instanceof InlineCommandRedisMessage) {
            writeInlineCommandMessage(allocator, (InlineCommandRedisMessage) msg, out);
        } else if (msg instanceof SimpleStringRedisMessage) {
            writeSimpleStringMessage(allocator, (SimpleStringRedisMessage) msg, out);
        } else if (msg instanceof ErrorRedisMessage) {
            writeErrorMessage(allocator, (ErrorRedisMessage) msg, out);
        } else if (msg instanceof IntegerRedisMessage) {
            writeIntegerMessage(allocator, (IntegerRedisMessage) msg, out);
        } else if (msg instanceof FullBulkStringRedisMessage) {
            writeFullBulkStringMessage(allocator, (FullBulkStringRedisMessage) msg, out);
        } else if (msg instanceof BulkStringRedisContent) {
            writeBulkStringContent(allocator, (BulkStringRedisContent) msg, out);
        } else if (msg instanceof BulkStringHeaderRedisMessage) {
            writeBulkStringHeader(allocator, (BulkStringHeaderRedisMessage) msg, out);
        } else if (msg instanceof ArrayHeaderRedisMessage) {
            writeArrayHeader(allocator, (ArrayHeaderRedisMessage) msg, out);
        } else if (msg instanceof ArrayRedisMessage) {
            writeArrayMessage(allocator, (ArrayRedisMessage) msg, out);
        } else {
            throw new CodecException("unknown message type: " + msg);
        }
    }

下面代码主要是实现对应消息按照 RESP 协议 进行序列化操作,具体就是上面楼主说的,分配一个ByteBuf,然后将将消息输出对象序列化的字节数组塞到ByteBuf中输出即可。

private static void writeInlineCommandMessage(ByteBufAllocator allocator, InlineCommandRedisMessage msg,
                                                 List<Object> out) {
        writeString(allocator, RedisMessageType.INLINE_COMMAND, msg.content(), out);
    }

    private static void writeSimpleStringMessage(ByteBufAllocator allocator, SimpleStringRedisMessage msg,
                                                 List<Object> out) {
        writeString(allocator, RedisMessageType.SIMPLE_STRING, msg.content(), out);
    }

    private static void writeErrorMessage(ByteBufAllocator allocator, ErrorRedisMessage msg, List<Object> out) {
        writeString(allocator, RedisMessageType.ERROR, msg.content(), out);
    }

    private static void writeString(ByteBufAllocator allocator, RedisMessageType type, String content,
                                    List<Object> out) {
        ByteBuf buf = allocator.ioBuffer(type.length() + ByteBufUtil.utf8MaxBytes(content) +
                                         RedisConstants.EOL_LENGTH);
        type.writeTo(buf);
        ByteBufUtil.writeUtf8(buf, content);
        buf.writeShort(RedisConstants.EOL_SHORT);
        out.add(buf);
    }

    private void writeIntegerMessage(ByteBufAllocator allocator, IntegerRedisMessage msg, List<Object> out) {
        ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                         RedisConstants.EOL_LENGTH);
        RedisMessageType.INTEGER.writeTo(buf);
        buf.writeBytes(numberToBytes(msg.value()));
        buf.writeShort(RedisConstants.EOL_SHORT);
        out.add(buf);
    }

    private void writeBulkStringHeader(ByteBufAllocator allocator, BulkStringHeaderRedisMessage msg, List<Object> out) {
        final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH +
                                        (msg.isNull() ? RedisConstants.NULL_LENGTH :
                                                        RedisConstants.LONG_MAX_LENGTH + RedisConstants.EOL_LENGTH));
        RedisMessageType.BULK_STRING.writeTo(buf);
        if (msg.isNull()) {
            buf.writeShort(RedisConstants.NULL_SHORT);
        } else {
            buf.writeBytes(numberToBytes(msg.bulkStringLength()));
            buf.writeShort(RedisConstants.EOL_SHORT);
        }
        out.add(buf);
    }

    private static void writeBulkStringContent(ByteBufAllocator allocator, BulkStringRedisContent msg,
                                               List<Object> out) {
        out.add(msg.content().retain());
        if (msg instanceof LastBulkStringRedisContent) {
            out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
        }
    }

    private void writeFullBulkStringMessage(ByteBufAllocator allocator, FullBulkStringRedisMessage msg,
                                            List<Object> out) {
        if (msg.isNull()) {
            ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
                                             RedisConstants.EOL_LENGTH);
            RedisMessageType.BULK_STRING.writeTo(buf);
            buf.writeShort(RedisConstants.NULL_SHORT);
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        } else {
            ByteBuf headerBuf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.BULK_STRING.writeTo(headerBuf);
            headerBuf.writeBytes(numberToBytes(msg.content().readableBytes()));
            headerBuf.writeShort(RedisConstants.EOL_SHORT);
            out.add(headerBuf);
            out.add(msg.content().retain());
            out.add(allocator.ioBuffer(RedisConstants.EOL_LENGTH).writeShort(RedisConstants.EOL_SHORT));
        }
    }

    /**
     * Write array header only without body. Use this if you want to write arrays as streaming.
     */
    private void writeArrayHeader(ByteBufAllocator allocator, ArrayHeaderRedisMessage msg, List<Object> out) {
        writeArrayHeader(allocator, msg.isNull(), msg.length(), out);
    }

    /**
     * Write full constructed array message.
     */
    private void writeArrayMessage(ByteBufAllocator allocator, ArrayRedisMessage msg, List<Object> out) {
        if (msg.isNull()) {
            writeArrayHeader(allocator, msg.isNull(), RedisConstants.NULL_VALUE, out);
        } else {
            writeArrayHeader(allocator, msg.isNull(), msg.children().size(), out);
            for (RedisMessage child : msg.children()) {
                writeRedisMessage(allocator, child, out);
            }
        }
    }

    private void writeArrayHeader(ByteBufAllocator allocator, boolean isNull, long length, List<Object> out) {
        if (isNull) {
            final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.NULL_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.ARRAY_HEADER.writeTo(buf);
            buf.writeShort(RedisConstants.NULL_SHORT);
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        } else {
            final ByteBuf buf = allocator.ioBuffer(RedisConstants.TYPE_LENGTH + RedisConstants.LONG_MAX_LENGTH +
                                                   RedisConstants.EOL_LENGTH);
            RedisMessageType.ARRAY_HEADER.writeTo(buf);
            buf.writeBytes(numberToBytes(length));
            buf.writeShort(RedisConstants.EOL_SHORT);
            out.add(buf);
        }
    }

小结

对于 Netty 源码,楼主一直是一种敬畏的态度,没想到今天竟然从另一个方面对 Netty 的冰山一角展开解读,毕竟万事开头难,有了这一次希望之后可以更顺利,在技术成长的道路上一起加油。

参考链接

  • Redis协议规范(译文)
  • TCP 粘包问题浅析及其解决方案
  • 基于Netty实现Redis协议的编码解码器
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Netty 源码中对 Redis 协议的实现 的相关文章

  • Linux中kill -2、kill -9等区别 && kill signal汇总

    xfeff xfeff kill号令用于终止指定的过程 xff08 terminate a process xff09 xff0c 是Unix Linux下过程经管的常用号令 凡是 xff0c 我们在须要终止某个或某些过程时 xff0c 先
  • Linux下查找文件(find、grep命令)

    目录 一 find命令 1 按文件名 2 按文件类型查询 3 按照文件大小查找 4 按照文件日期查找 4 1按照创建日期查找 4 2按照修改日期查找 4 3按照访问日期查找 5 按深度查找 5 1查找起始点以下n层的目录 xff0c 不超过
  • 深度剖析数据在内存中的存储

    小编认为要想成为一个好的程序员 xff0c 不能仅仅只做到会使用 xff0c 而要做到理解其本质 做到可持续发展 接下来小编会向大家介绍数据在内存中究竟是如何存储与运算的 xff0c 也算是修炼内功了吧 目录 一 数据类型介绍 1 整型家族
  • Linux下进程控制详解

    目录 一 进程创建 1 1 初识fork 1 2 函数返回值 1 3 写时拷贝技术 1 4 fork函数的使用场景 1 5 fork函数的失败原因 二 进程终止 2 1 进程退出场景 2 2 进程退出码 2 3 进程正常退出方法 2 3 1
  • 基础IO详解

    目录 一 系统文件IO 1 1 open 1 1 1 open的第一个参数 1 1 2 open的第二个参数 1 1 3 open的第三个参数 1 1 4 open的返回值 1 2 close 1 3 write 1 4 read 二 文件
  • HTTPS协议

    一 HTTPS介绍 HTTPS是 个应用层协议 xff0c 是在HTTP协议的基础上引入了加密层 HTTP协议内容都是按照文本的方式明文传输 xff0c 这就导致在传输过程中可能出现被篡改的情况 二 加密 2 1 加密概念 加密就是把明文
  • 传输层 — UDP协议

    目录 一 传输层 1 1 端口号 1 2 关于端口的常见问题 1 3 netstat amp amp pidof 二 UDP协议 2 1 UDP协议格式 2 2 UDP协议特点 2 3 UDP缓冲区 2 4 基于UDP的应用层协议 一 传输
  • 传输层——TCP协议

    目录 一 初步认识 二 TCP协议格式 2 1 初识协议格式 2 2 序号与确认序号 2 3 16位窗口大小 2 4 六个标志位 三 确认应答机制 四 超时重传机制 五 连接管理机制 5 1 三次挥手 5 2 四次挥手 六 流量控制 七 滑
  • Android 8.1 App开机自启动、注册为无障碍服务、实现悬浮窗

    xff08 欢迎转载 xff0c 只需注明本文来源 xff1a https blog csdn net actionwind article details 103619688 xff09 以下各方法大多来自于网上诸多朋友的无私分享 xff
  • 安卓的ATV系统

    Android系统根据是否需要认证分为AOSP系统和ATV系统 AOSP Android开源系统 xff0c 全称为Android Open Source Project ATV 产品依照 Android TV 制式标准提供统一的操作体验
  • ubuntu18 为什么安装的时候选择自动登陆,但是还是不能自动登陆呢

    2019 03 12 16 16 1 点击右上角的向下三角形 2 在出现的对话框里面 xff0c 点击右下角的扳手图标 3 点击 隐私 gt 锁屏 4 自动锁屏 L xff0c 保持不变 5 黑屏至锁屏的等的时间 A 改为 30分钟 6 显
  • zabbix 5.4 添加监控主机

    现在我们已经有了一个客户端主机了 xff0c 接下来我们要在zabbix server管理页面添加这台主机 xff0c 让server对agent进行监控 登录zabbix server的管理页面 xff0c 点击左侧操作树的配置选项 xf
  • (转载)环形缓冲区的实现原理(ring buffer)

    环形缓冲区的实现原理 xff08 ring buffer xff09 在通信程序中 xff0c 经常使用环形缓冲区作为数据结构来存放通信中发送和接收的数据 环形缓冲区是一个先进先出的循环缓冲区 xff0c 可以向通信程序提供对缓冲区的互斥访
  • 力扣295. 数据流的中位数

    中位数是有序列表中间的数 如果列表长度是偶数 xff0c 中位数则是中间两个数的平均值 例如 xff0c 2 3 4 的中位数是 3 2 3 的中位数是 2 43 3 2 61 2 5 设计一个支持以下两种操作的数据结构 xff1a voi
  • virt-manager cannot open display问题

    1 首先查看本机sshd是否开启X11转发 grep X11Forwarding etc ssh sshd config X11Forwarding yes X11Forwarding no 2 检查xorg x11是否安装 rpm qa
  • 2023RSAC创新沙盒大赛十强公布,软件供应链与开源软件安全再成焦点

    近日 xff0c 2023RSAC大会公布了第18届RSAC创新沙盒竞赛的决赛 十强企业 软件安全企业Endor Labs成功入围 xff0c 软件供应链安全和开源安全问题再次成为国际焦点 Endor Labs提供了一个可依赖的生命周期管理
  • 如何找到现有研发体系的「内耗问题」?| 谈效风生

    第3期 xff1a 如何找到现有研发体系的 内耗问题 xff1f 在上一期 谈到提升效能 xff0c 我们应该如何下手 xff1f 我们聊到开始做研发效能的四个要点 xff1a 评估现有流程 引入自动化工具 建立度量指标 持续改进 本期就围
  • 「自动化」聊起来简单,做起来难 | 谈效风生

    第4期 xff1a 自动化 聊起来简单 xff0c 做起来难 在上一期 如何找到现有研发体系的 内耗问题 中 xff0c 我们聊了评估现有研发体系 xff0c 正确的找到 体系内耗问题 xff0c 是改变研发体系的第一步 本期我们继续聊下一
  • GitChat·前端 | 从软件工程角度看大前端技术栈

    GitChat 作者 xff1a 韩亦乐 前言 我们都知道 xff0c 大学几乎是没有 Web 前端课的 以我所在的大学为例 xff0c 唯一引导我们了解 JavaScript 的也只是 人机界面 和 Web应用开发 选修课 再者 xff0
  • 当我说要做大数据工程师时他们都笑我,直到三个月后……

    作者 xff1a Fickr孫啟誠 原文 xff1a 三个月大数据研发学习计划实战解析 关注微信公众号 xff1a GitChat 技术杂谈 一本正经的讲技术 不要错过文末彩蛋 申明 xff1a 本文旨在为普通程序员 xff08 Java程

随机推荐

  • 带你从零开始,快速学会 Matlab GUI

    本文来自作者 木木小迷哥 在 GitChat 上分享 Matlab GUI 零基础学员快速入门 xff0c 阅读原文 查看交流实录 文末高能 编辑 洛肯 Matlab广泛地应用于数学分析 计算 自动控制 系统仿真 数字信号处理 图像处理 数
  • 修改订单金额!?0.01 元购买 iPhoneX?| Web谈逻辑漏洞

    本文来自作者 肖志华 在 GitChat 上分享 Web 安全恩仇录 xff1a 再谈逻辑漏洞 xff0c 阅读原文 查看交流实录 文末高能 编辑 哈比 Web 漏洞里有 SQL 注入 XSS 等漏洞 xff0c 但是逻辑漏洞等问题也是一个
  • zabbix 5.4 配置Email报警媒介

    我们使用zabbix主要的目标就是实现运维的自动化 xff0c 当被监控的系统出现问题时zabbix能够自动的提示我们 xff0c 要实现这个功能就必需设置报警媒介 本文带领大家配置一个Email报警媒介 我们使用zabbix主要的目标就是
  • 如何开发一款游戏:游戏开发流程及所需工具

    本文来自作者 goto先生 在 GitChat 上分享 如何开发一款游戏 xff1a 游戏开发流程及所需工具 编辑 哈比 游戏作为娱乐生活的一个方面 xff0c 参与其中的人越来越多 xff0c 而大部分参与其中的人都是以玩家的身份 他们热
  • 如何用 Python 做自动化测试

    本文来自作者 Vant 在 GitChat 上分享 如何用 Python 做自动化测试 编辑 哈比 一 Selenium 环境部署 1 window 环境部署 1 1 当前环境 Win10 64 位系统 xff1b Python3 6 2
  • 我真的要做一辈子的程序员吗?

    本文来自作者 王鹏 在 GitChat 上分享 程序员跳槽时 xff0c 如何正确做好职业规划 xff1f 阅读原文 查看交流实录 编辑 贝吉塔 经常听一些同学说 xff1a 不知道下一份工作该去哪类公司做些什么 xff0c 我的职场人际一
  • 20万到100万年薪的算法工程师有什么区别?

    公元七世纪 xff0c 在车迟国国家气象局组织的一次求雨活动中 xff0c 虎力 鹿力 羊力三位大仙成功地祈下甘霖 xff0c 救黎民于水火中 老国王虽然不明就里 xff0c 却从此尊他们为国师 xff0c 奉道教为圭臬 本世纪 xff0c
  • 关于handler的内存泄漏的总结

    本文并非完全是原创 有网络上边的 也有自己的思考与总结 本文主要有三部分来介绍handler的介绍 第一 熟悉一下handler的流程 1 当Android程序第一次创建的时候 xff0c 在主线程同时会由ActivityThread 创建
  • Android 设置应用的底部导航栏(虚拟按键)背景颜色

    Android手机机型种类繁多 但是虚拟按键也就是底部的导航栏 不外乎两种设计方式 一种是作为虚拟按键设计到屏幕内部 一种是作为系统按键设计到屏幕外面 对于按键在屏幕内部的机型 因为虚拟按键也是屏幕内容的一部分 它也实际占据屏幕一定的高度
  • Adobe XD使用心得及简单使用技巧

    关于Adobe XD Adobe XD xff0c 全称Adobe Experience Design 这是一款集原型 设计和交互于一体的小清新时代风格的设计软件 虽然传言中 xff0c Adobe XD将会是skech的劲敌 然而在win
  • 生产者消费者问题(代码实现)

    生产者 消费者问题 xff08 也被称为有界缓冲器问题 xff09 是一个典型的例子多线程同步的问题 问题描述了两个进程 xff0c 生产者和消费者 xff0c 谁都有一个共同的 xff0c 固定大小的缓冲区作为一个队列 制片人的工作是生成
  • BIO与NIO的方式实现文件拷贝

    面试题 编程实现文件拷贝 xff08 这个题目在笔试的时候经常出现 xff0c 下面的代码给出了两种实现方案 xff09 span class hljs keyword import span java io FileInputStream
  • Linux上jmeter-server启动失败

    贴个广告 楼主的博客已全部搬迁至自己的博客 xff0c 感兴趣的小伙伴请移步haifeiWu与他朋友们的博客专栏 Jmeter server启动失败 xff1a Cannot start Unable to get local host I
  • zabbix 5.4 添加报警动作

    我们先来看一下zabbix动作事件的整个流程 xff0c 通过下面的流程图希望大家对zabbix的报警功能要有一个整体上的认识 一个完整的报警功能必要条件 xff1a 监控项 xff1a 用于实时采集主机的数据 xff08 CPU xff0
  • Mysql的七种join

    对于SQL的Join xff0c 在学习起来可能是比较乱的 我们知道 xff0c SQL的Join语法有很多inner的 xff0c 有outer的 xff0c 有left的 xff0c 有时候 xff0c 对于Select出来的结果集是什
  • shell脚本实现自动保留最近n次备份记录

    贴个广告 楼主的博客已全部搬迁至自己的博客 xff0c 感兴趣的小伙伴请移步haifeiWu与他朋友们的博客专栏 项目中出现的问题 某天上午服务器出现卡顿特别严重 xff0c 页面加载速度奇慢 xff0c 并且某些页面刷新出现404的问题
  • Java实现终止线程池中正在运行的定时任务

    贴个广告 楼主的博客已全部搬迁至自己的博客 xff0c 感兴趣的小伙伴请移步haifeiWu与他朋友们的博客专栏 源于开发 最近项目中遇到了一个新的需求 xff0c 就是实现一个可以动态添加定时任务的功能 说到这里 xff0c 有人可能会说
  • TCP 粘包问题浅析及其解决方案

    最近一直在做中间件相关的东西 xff0c 所以接触到的各种协议比较多 xff0c 总的来说有TCP xff0c UDP xff0c HTTP等各种网络传输协议 xff0c 因此楼主想先从协议最基本的TCP粘包问题搞起 xff0c 把计算机网
  • Redis协议规范(译文)

    原文地址 xff1a haifeiWu的博客 博客地址 xff1a www hchstudio cn 欢迎转载 xff0c 转载请注明作者及出处 xff0c 谢谢 xff01 Redis客户端使用名为RESP xff08 Redis序列化协
  • Netty 源码中对 Redis 协议的实现

    原文地址 xff1a haifeiWu的博客 博客地址 xff1a www hchstudio cn 欢迎转载 xff0c 转载请注明作者及出处 xff0c 谢谢 xff01 近期一直在做网络协议相关的工作 xff0c 所以博客也就与之相关