基于Netty实现Redis协议的编码解码器

Redis:redission 源代码剖析1 连接建立 redission作为redis 官方推荐的java客户端。 redission使用netty4.x作为网络层。 redission使用异步io方式操作。这与jedis 同步io操作方式完全不同。 但是redission也提供了同步操作方式。 阅读详情

640?wx_fmt=jpeg

640?wx_fmt=jpeg

Netty消息处理结构

上面是Netty的服务器端基本消息处理结构,为了便于初学者理解,它和真实的结构有稍许出入。Netty是基于NIO的消息处理框架,用来高效处理网络IO。处理网络消息一般走以下步骤

  1. 监听端口 Bind & Listen

  2. 接收新连接 Accept

  3. 通过连接收取客户端发送的字节流,转换成输入消息对象 Read & Decode

  4. 处理消息,生成输出消息对象 Process

  5. 转换成字节,通过连接发送到客户端 Encode & Write

步骤2拿到新连接之后,如果是开启了新线程进入步骤3,那就是走传统的多线程服务器模式。一个线程一个连接,每个线程是都阻塞式读写消息。如果并发量比较大,需要的线程资源也是比较多的。

Netty的消息处理基于NIO的多路复用机理,一个线程通过NIO Selector非阻塞地读写非常多的连接。传统的多线程服务器需要的线程数到了NIO这里就可以大幅缩减,节省了很多操作系统资源。

Netty的线程划分为两种,一种是用来监听ServerSocket并接受新连接的Acceptor线程,另一种用来读写套件字连接上的消息的IO线程,两种线程都是使用NIO Selector异步并行管理多个套件字。Acceptor线程可以同时监听多个ServerSocket,管理多个端口,将接收到的新连接扔到IO线程。IO线程可以同时读写多个Socket,管理多个连接的读写。

IO线程从套件字上读取到的是字节流,然后通过消息解码器将字节流反序列化成输入消息对象,再传递到业务处理器进行处理,业务处理器会生成输出消息对象,通过消息编码器序列化成字节流,再通过套件字输出到客户端。

Redis协议编码解码的实现

本文的重点是教读者实现一个简单的Redis Protocol编码解码器。

640?wx_fmt=png

首先我们来介绍一下Redis Protocol的格式,Redis协议分为指令和返回两个部分,指令的格式比较简单,就是一个字符串数组,比如指令setnx a b就是三个字符串的数组,如果指令中有整数,也是以字符串的形式发送的。Redis协议的返回就比较复杂了,因为要支持复杂的数据类型和结构嵌套。本文是以服务端的角色来处理Redis协议,也就是编写指令的解码器和返回对象的编码器。而客户端则是反过来的,客户端需要编写指令的编码器和返回对象的解码器。

指令的编码格式

setnx a b => *3\r\n$5\r\nsetnx\r\n$1\r\na\r\n$1\r\nb\r\n

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

指令解码器的实现

网络字节流的读取存在半包问题。所谓半包问题是指一次Read调用从套件字读到的字节数组可能只是一个完整消息的一部分。而另外一部分则需要发起另外一次Read调用才可能读到,甚至要发起多个Read调用才可以读到完整的一条消息。

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

问题来了,如果一个输入消息对象很大,就可能需要多个Read调用和多次反序列化操作才能完整的解包出一个输入对象。那这个反序列化的过程就会重复了多次。比如第一次完成了30%,然后第二次从头开始又完成了60%,第三次又从头开始完成了90%,第四次又从头开始总算完成了100%,这下终于可以放心交给业务处理器处理了。

640?wx_fmt=png

Netty使用ReplayingDecoder引入检查点机制[Checkpoint]解决了这个重复反序列化的问题。

640?wx_fmt=png

在反序列化的过程中我们反复打点记录下当前读到了哪个位置,也就是检查点,然后下次反序列化的时候就可以从上次记录的检查点直接继续反序列化。这样就避免了重复的问题。

这就好比我们玩单机RPG游戏一样,这些游戏往往有自动保存的功能。这样就可以避免进程不小心退出时,再进来的时候就可以从上次保存的状态直接继续进行下去,而不是像Flappy Bird一样重新玩它一遍又一遍,这简直要把人虐死。

640?wx_fmt=gif

import java.util.ArrayList;
import java.util.List;

import com.google.common.base.Charsets;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.ReplayingDecoder;

class InputState {
    public int index;
}

public class RedisInputDecoder extends ReplayingDecoder<InputState> {

    private int length;
    private List<String> params;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        InputState state = this.state();
        if (state == null) {
            length = readParamsLen(in);
            this.params = new ArrayList<>(length);
            state = new InputState();
            this.checkpoint(state);
        }
        for (int i = state.index; i < length; i++) {
            String param = readParam(in);
            this.params.add(param);
            state.index = state.index + 1;
            this.checkpoint(state);
        }
        out.add(new RedisInput(params));
        this.checkpoint(null);
    }

    private final static int CR = '\r';
    private final static int LF = '\n';
    private final static int DOLLAR = '$';
    private final static int ASTERISK = '*';

    private int readParamsLen(ByteBuf in) {
        int c = in.readByte();
        if (c != ASTERISK) {
            throw new DecoderException("expect character *");
        }
        int len = readLen(in, 3); // max 999 params
        if (len == 0) {
            throw new DecoderException("expect non-zero params");
        }
        return len;
    }

    private String readParam(ByteBuf in) {
        int len = readStrLen(in);
        return readStr(in, len);
    }

    private String readStr(ByteBuf in, int len) {
        if (len == 0) {
            return "";
        }
        byte[] cs = new byte[len];
        in.readBytes(cs);
        skipCrlf(in);
        return new String(cs, Charsets.UTF_8);
    }

    private int readStrLen(ByteBuf in) {
        int c = in.readByte();
        if (c != DOLLAR) {
            throw new DecoderException("expect character *");
        }
        return readLen(in, 6); // string maxlen 999999
    }

    private int readLen(ByteBuf in, int maxBytes) {
        byte[] digits = new byte[maxBytes]; // max 999个参数
        int len = 0;
        while (true) {
            byte d = in.getByte(in.readerIndex());
            if (!Character.isDigit(d)) {
                break;
            }
            in.readByte();
            digits[len] = d;
            len++;
            if (len > maxBytes) {
                throw new DecoderException("params length too large");
            }
        }
        skipCrlf(in);
        if (len == 0) {
            throw new DecoderException("expect digit");
        }
        return Integer.parseInt(new String(digits, 0, len));
    }

    private void skipCrlf(ByteBuf in) {
        int c = in.readByte();
        if (c == CR) {
            c = in.readByte();
            if (c == LF) {
                return;
            }
        }
        throw new DecoderException("expect cr ln");
    }

}

输出消息编码器的实现

高能预警:前方有大量代码,请酌情观看

输出消息的结构要复杂很多,要支持多种数据类型,包括状态、整数、错误、字符串和数组,要支持数据结构嵌套,数组里还有数组。相比解码器而言它简单的地方在于不用考虑半包问题,编码器只负责将消息序列化成字节流,剩下的事由Netty偷偷帮你搞定。

首先我们定义一个输出消息对象接口,所有的数据类型都要实现该接口,将对象内部的状态转换成字节数组放置到ByteBuf中。

import io.netty.buffer.ByteBuf;

public interface IRedisOutput {

    public void encode(ByteBuf buf);

}

整数输出消息类,整数的序列化格式为:value\r\n,value是整数的字符串表示。

import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;

public class IntegerOutput implements IRedisOutput {

    private long value;

    public IntegerOutput(long value) {
        this.value = value;
    }

    @Override
    public void encode(ByteBuf buf) {
        buf.writeByte(':');
        buf.writeBytes(String.valueOf(value).getBytes(Charsets.UTF_8));
        buf.writeByte('\r');
        buf.writeByte('\n');
    }

    public static IntegerOutput of(long value) {
        return new IntegerOutput(value);
    }

    public static IntegerOutput ZERO = new IntegerOutput(0);
    public static IntegerOutput ONE = new IntegerOutput(1);

}

状态输出消息类,序列化格式为+status\r\n

import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;

public class StateOutput implements IRedisOutput {

    private String state;

    public StateOutput(String state) {
        this.state = state;
    }

    public void encode(ByteBuf buf) {
        buf.writeByte('+');
        buf.writeBytes(state.getBytes(Charsets.UTF_8));
        buf.writeByte('\r');
        buf.writeByte('\n');
    }

    public static StateOutput of(String state) {
        return new StateOutput(state);
    }

    public final static StateOutput OK = new StateOutput("OK");

    public final static StateOutput PONG = new StateOutput("PONG");

}

错误输出消息类,序列化格式为-type reason\r\n,reason必须为单行字符串

import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;

public class ErrorOutput implements IRedisOutput {

    private String type;
    private String reason;

    public ErrorOutput(String type, String reason) {
        this.type = type;
        this.reason = reason;
    }

    public String getType() {
        return type;
    }

    public String getReason() {
        return reason;
    }

    @Override
    public void encode(ByteBuf buf) {
        buf.writeByte('-');
        // reason不允许多行字符串
        buf.writeBytes(String.format("%s %s", type, headOf(reason)).getBytes(Charsets.UTF_8));
        buf.writeByte('\r');
        buf.writeByte('\n');
    }

    private String headOf(String reason) {
        int idx = reason.indexOf("\n");
        if (idx < 0) {
            return reason;
        }
        return reason.substring(0, idx).trim();
    }

    // 通用错误
    public static ErrorOutput errorOf(String reason) {
        return new ErrorOutput("ERR", reason);
    }

    // 语法错误
    public static ErrorOutput syntaxOf(String reason) {
        return new ErrorOutput("SYNTAX", reason);
    }

    // 协议错误
    public static ErrorOutput protoOf(String reason) {
        return new ErrorOutput("PROTO", reason);
    }

    // 参数无效
    public static ErrorOutput paramOf(String reason) {
        return new ErrorOutput("PARAM", reason);
    }

    // 服务器内部错误
    public static ErrorOutput serverOf(String reason) {
        return new ErrorOutput("SERVER", reason);
    }

}

字符串输出消息类,字符串分为null、空串和普通字符串。null的序列化格式为$-1\r\n,普通字符串的格式为$len\r\ncontent\r\n,空串就是一个长度为0的字符串,格式为$0\r\n\r\n。

import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;

public class StringOutput implements IRedisOutput {

    private String content;

    public StringOutput(String content) {
        this.content = content;
    }

    @Override
    public void encode(ByteBuf buf) {
        buf.writeByte('$');
        if (content == null) {
            // $-1\r\n
            buf.writeByte('-');
            buf.writeByte('1');
            buf.writeByte('\r');
            buf.writeByte('\n');
            return;
        }
        byte[] bytes = content.getBytes(Charsets.UTF_8);
        buf.writeBytes(String.valueOf(bytes.length).getBytes(Charsets.UTF_8));
        buf.writeByte('\r');
        buf.writeByte('\n');
        if (content.length() > 0) {
            buf.writeBytes(bytes);
        }
        buf.writeByte('\r');
        buf.writeByte('\n');
    }

    public static StringOutput of(String content) {
        return new StringOutput(content);
    }

    public static StringOutput of(long value) {
        return new StringOutput(String.valueOf(value));
    }

    public final static StringOutput NULL = new StringOutput(null);

}

最后一个数组输出消息类,支持数据结构嵌套就靠它了。数组的内部是多个子消息,每个子消息的类型是不定的,类型可以不一样。比如scan操作的返回就是一个数组,数组的第一个子消息是游标的offset字符串,第二个子消息是一个字符串数组。它的序列化格式开头为*len\r\n,后面依次是内部所有子消息的序列化形式。

import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Charsets;
import io.netty.buffer.ByteBuf;

public class ArrayOutput implements IRedisOutput {

    private List<IRedisOutput> outputs = new ArrayList<>();

    public static ArrayOutput newArray() {
        return new ArrayOutput();
    }

    public ArrayOutput append(IRedisOutput output) {
        outputs.add(output);
        return this;
    }

    @Override
    public void encode(ByteBuf buf) {
        buf.writeByte('*');
        buf.writeBytes(String.valueOf(outputs.size()).getBytes(Charsets.UTF_8));
        buf.writeByte('\r');
        buf.writeByte('\n');
        for (IRedisOutput output : outputs) {
            output.encode(buf);
        }
    }

}

下面是ArrayOutput对象使用的一个实例,是从小编的某个项目里扒下来的,嵌套了三层数组。读者可以不用死磕下面的代码,重点看看ArrayOutput大致怎么使用的就可以了。

ArrayOutput out = ArrayOutput.newArray();
for (Result result : res) {
    if (result.isEmpty()) {
        continue;
    }
    ArrayOutput row = ArrayOutput.newArray();
    row.append(StringOutput.of(new String(result.getRow(), Charsets.UTF_8)));
    for (KeyValue kv : result.list()) {
        ArrayOutput item = ArrayOutput.newArray();
        item.append(StringOutput.of("family"));
        item.append(StringOutput.of(new String(kv.getFamily(), Charsets.UTF_8)));
        item.append(StringOutput.of("qualifier"));
        item.append(StringOutput.of(new String(kv.getQualifier(), Charsets.UTF_8)));
        item.append(StringOutput.of("value"));
        item.append(StringOutput.of(new String(kv.getValue(), Charsets.UTF_8)));
        item.append(StringOutput.of("timestamp"));
        item.append(StringOutput.of(kv.getTimestamp()));
        row.append(item);
    }
    out.append(row);
}
ctx.writeAndFlush(out);

最后,有了以上清晰的类结构,解码器类的实现就非常简单了。

import java.util.List;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;

@Sharable
public class RedisOutputEncoder extends MessageToMessageEncoder<IRedisOutput> {

    @Override
    protected void encode(ChannelHandlerContext ctx, IRedisOutput msg, List<Object> out) throws Exception {
        ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer();
        msg.encode(buf);
        out.add(buf);
    }

}

因为解码器对象是无状态的,所以它可以被channel共享。解码器的实现非常简单,就是分配一个ByteBuf,然后将将消息输出对象序列化的字节数组塞到ByteBuf中输出就可以了。

最后感谢技术大牛 @逆水寒 提供的md2all工具非常好用,该工具可以将markdown转换成HTML形式,功能非常强大,可以给代码提供多种颜色的主题。欢迎大家关注他,他平时会提供一些Python基础的培训教学,微信号是hg_liuzl。

640?wx_fmt=png

阅读相关文章,关注公众号【码洞】

Redisson 3.11.3 - redis协议处理器(使用netty-client和redis-server交互) . public class RedisChannelInitializer extends ChannelInitializer<Channel> { public enum Type {PUBSUB, PLAIN} // .... @Override protected void initChannel(Channel ch) throws Exception { initSsl(config, ch); // 初始化 阅读详情

相关推荐

Netty面试核心:从网络编程原理到高并发实战调优

网络编程是构建高并发分布式系统的基石,其核心在于解决传统阻塞IO模型在应对海量连接时的性能瓶颈。通过多路复用技术,一个线程可以高效管理多个网络连接,大幅减少线程资源消耗。在此基础上,Reactor线程模型进一步将连接建立与数据处理解耦,形成了高性能网络框架的骨架。Netty作为Java领域最主流的NIO框架,其价值在于将复杂的底层网络通信抽象为Channel、EventLoop、Pipeline等核心组件,并通过零拷贝、内存池等机制优化数据传输效率。在实际应用中,Netty不仅解决了粘包拆包、内存泄漏等工程

weixin_33795093的博客 455

io.netty.handler.codec.DecoderException: java.lang.UnsupportedOperationException: direct buffer

自定义netty的解码器decode读取数据报错,版本: <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.77.Final</version> </dependency> 报错的是这行代码: // 读取消息内容 byte[] content = in.

山塘小鱼儿的博客 9310

Redis:redission 源代码剖析3 future模式

Redission利用Netty 的Promise模式来实现的。在Redission向Redis服务器发送一个消息时,此时会产生一个MainPromise对象,对这个MainPromise既可以同步操作也可以异步方式操作。 在Redission内部,会首先创建链接,然后创建一个attempPromise对象,然后发送消息。attempPromise对象和mainPromise会关联在一起。所有发送的对象包括参数都会保存在队列中。 当Redission接受到Redis服务器消息时,会取出发送时的信息。这个

sunning9001的专栏 4256

Netty-协议应用和设计

一、协议的重要性 客户端和服务器端在传递消息的过程中,必然需要约定消息,否则双方是没有办法理解相互之间传递的信息。 redis的协议 redis协议遵循的规则是 位数-命令,位数-命令,位数-命令.... 例子: package com.test.netty.c7; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import

liming0025的专栏 197

spring-boot2.x使用redis

一、依赖引用并配置 implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' 参考配置: https://spring.io/guides/gs/spring-data-reactive-redis/ spring.redis.database=0 spring.redis.hos...

zhibin的程序日记 2937

【netty】 io.netty.handler.codec.TooLongFrameException

背景 做netty权威指南的私有协议开发,案例报错如 io.netty.handler.codec.TooLongFrameException: Adjusted frame length exceeds 1048576: 795898487 - discarded at io.netty.handler.codec.LengthFieldBasedFrameDecoder.fail(Lengt...

九师兄 2万+

码洞技术文章大全

全栈虚拟机GraalVM初体验基于Netty自己动手编写RPC框架自己动手实现一个极简Web框架水塘抽样与阶层固化深度解析某头条的一道面试题基于Netty实现Redis协...

codehole_的博客 284

netty-codec-redis-4.1.73.Final-API文档-中文版.zip

赠送jar包:netty-codec-redis-4.1.73.Final.jar; 赠送原API文档:netty-codec-redis-4.1.73.Final-javadoc.jar; 赠送源代码:netty-codec-redis-4.1.73.Final-sources.jar; 赠送Maven依赖信息文件:netty-codec-redis-4.1.73.Final.pom; 包含翻译后的API文档:netty-codec-redis-4.1.73.Final-javadoc-API文档-中文(简体)版.zip; Maven坐标:io.netty:netty-codec-redis:4.1.73.Final; 标签:codec、redis、netty、jar包、java、中文文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。

netty系列之:netty中的懒人编码解码器

简介 netty之所以强大,是因为它内置了很多非常有用的编码解码器,通过使用这些编码解码器可以很方便的搭建出非常强大的应用程序,今天给大家讲讲netty中最基本的内置编码解码器。 netty中的内置编码器 在对netty的包进行引入的时候,我们可以看到netty有很多以netty-codec开头的artifactId,统计一下,有这么多个: netty-codec netty-codec-http netty-codec-http2 netty-codec-memcache netty-codec

qq_38082146的博客 559

netty-codec-redis-4.1.74.Final-API文档-中文版.zip

赠送jar包:netty-codec-redis-4.1.74.Final.jar; 赠送原API文档:netty-codec-redis-4.1.74.Final-javadoc.jar; 赠送源代码:netty-codec-redis-4.1.74.Final-sources.jar; 赠送Maven依赖信息文件:netty-codec-redis-4.1.74.Final.pom; 包含翻译后的API文档:netty-codec-redis-4.1.74.Final-javadoc-API文档-中文(简体)版.zip; Maven坐标:io.netty:netty-codec-redis:4.1.74.Final; 标签:netty、codec、redis、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。

Cartool实战:手把手教你完成静息态EEG微状态分析的组水平聚类与模板匹配

本文详细解析了使用Cartool进行静息态EEG微状态分析的组水平聚类与模板匹配全流程。从数据准备、算法选择到参数优化,提供了k-means与T-AAHC算法的实操建议,帮助研究者避开常见陷阱,获得可靠分析结果。特别针对最优类别数确定、模板匹配参数设置等核心环节进行深入探讨,适用于认知神经科学领域的EEG微状态研究。

weixin_31220401的博客 263

Netty内置处理器以及编解码器

一、Netty已经为我们提供了常用协议的处理器,我们直接使用就可以 可以看到包括redis协议,http协议等主流协议的编码解码器,netty都为我们提供了。 二、代码测试 使用netty直接对redis进行操作 redis协议的详细规定,这里不深入 @Slf4j public class SimpleRedisProtocol { public static void main(String[] args) { final byte[] LINE = {13, 10};

cristianoxm的博客 434

从Blender舞步到夜空画卷:基于Pix飞控的无人机编队灯光秀全链路开发实践

本文详细介绍了基于Pixhawk飞控的无人机编队灯光秀全链路开发实践。从Blender三维动画设计、QGroundControl地面站二次开发与MAVLink通信桥接,到PX4飞控的OFFBOARD模式路径控制与LED灯光驱动实现,最后通过Gazebo仿真验证并完成真机组装与首飞。文章为无人机爱好者提供了一套完整的、从创意设计到夜空执行的开源解决方案,重点阐述了多机同步与灯光控制的核心技术挑战与应对策略。

zeta9的博客 586

Netty网络框架学习笔记-9(编码与解码器机制)

netty编解码器

志豪的博客 945

使用ONNX Runtime在Java Web应用中部署深度学习模型

当应用场景需要集成深度学习模型进行推理时,直接在Java Web应用中集成深度学习框架可能会面临性能、兼容性等问题。为了将深度学习模型无缝集成到Java Web应用中,是一项广受认可且实用的方法。本篇文档将介绍如何将模型转换为ONNX格式,并通过ONNX Runtime Server进行部署,并通过Java Web应用调用以进行回归或预测任务。本方案的主要目标是实现以下功能:将PyTorch模型转换为ONNX格式。部署ONNX Runtime Server,加载ONNX模型并提供推理服务。

shysea2019的博客 8859

解锁Jetbrains AI助手:中国开发者实战配置与效率跃迁指南

本文为中国开发者提供了一份详细的Jetbrains AI助手实战配置指南。针对服务地域限制,核心解决方案是通过修改Jetbrains账户区域信息并配置网络代理,实现插件的稳定启用。文章进一步分享了AI助手在解读遗留代码、生成中文提交信息、智能问答等场景下的具体应用,旨在帮助开发者深度集成这一工具,实现编码生产力的显著提升。

weixin_28832121的博客 579

‌Netty编解码与粘包拆包

在Netty中,网络传输的数据是字节流,而应用层处理的是Java对象或业务消息,因此需要‌编码(Encoder)‌ 和 ‌解码(Decoder)‌ 来完成类型转换。⚠️ 注意:解码必须处理“半包”情况——数据未到齐时不能抛异常,应保留读指针,等待下一批数据。

BUTCHER 248

Ubuntu20.04系统笔记本网卡AX211无法找到wifi问题

之前电脑的系统是18.04基本上安装好了系统,可以直接连上wifi,在买了拯救者y9000p2022版本之后,因为网卡太新,安装ubuntu18.04或者20.04之后会没有wifi连接,非常不方便。于是上网找了各种帖子去解决这个问题。具体实现有所不同。 主要参考了这个帖子来解决的:http://t.csdn.cn/7807R 首先: sudo apt install flex bison 然后: git clone https://github.com/intel/backpor.

weixin_42825962的博客 1万+
上一篇: 人工稚能之sklearn数据降维
下一篇: 鲜为人知的HTTP协议头字段详解大全
codehole_
博客等级 码龄7年 34粉丝 132原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值