网络编程的无冕之王-Netty入门和核心组件介绍

2023-11-10

最近我在研究Netty,之前只是经常听说,并没有实际做过研究,为什么突然要好好研究一下它,主要是因为前段时间,我在看RocketMQ底层原理的时候发现它的底层的网络通信都是基于Netty,然后网上一查,果然,大家太多的耳熟能详的工具组件,都是基于Netty做的开发。大家看看:
在这里插入图片描述
可以看到太多的分布式或者微服务组件都是基于它,因为分布式/微服务的根基在于网络编程,而Netty就是一款非常成熟的网络编程工具。所以它在网络编程学习中不可避免的学习目标。

接下来我会用几篇文章为大家分享一下我最近的学习成果,给大家做了很好的总结,希望能够给大家带来帮助。

1. 概述

1.1 概念

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

Netty框架是基于Java原生NIO技术的进一步封装,对Java-NIO技术做了进一步增强,充分结合了Reactor线程模型,将Netty变为了一个基于异步事件驱动的网络框架。

Netty至今共发布了五个大版本,目前最常用的并非是最新的5.x系列,而是4.x系列的版本,原因是Netty本身就是基于Java-NIO封装的,而JDK本身又很稳定,再加上5.x版本并未有太大的性能差异,因此4.x系列才是主流。

1.2 应用场景

  • 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的有:阿里的Dubbo,Rocketmq底层也是用的Netty作为基础通信组件。

  • 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。

  • 大数据领域:经典的Hadoop 的高性能通信,默认采用 Netty 进行跨界点通信,它的Netty Service 基于Netty 框架二次封装实现。

1.3 入门实操

说千遍还不如让我们一起来动手感受一遍,现在就直接先实操一番快速入门。

1 引入依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.43.Final</version>
</dependency>

2 服务端

然后先创建NettyServer服务端,代码如下:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.CharsetUtil;

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        // 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
        ServerBootstrap server = new ServerBootstrap();
        try {
            // 将前面创建的两个EventLoopGroup绑定在server上
            server.group(boss,worker)
                    // 指定服务端的通道为Nio类型
                    .channel(NioServerSocketChannel.class)
                    // 为到来的客户端Socket添加处理器
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        // 这个只会执行一次(主要是用于添加更多的处理器)
                        @Override
                        protected void initChannel(NioSocketChannel ch) {
                            // 添加一个字符解码处理器:对客户端的数据解码
                            ch.pipeline().addLast(
                                new StringDecoder(CharsetUtil.UTF_8));
                            // 添加一个入站处理器,对收到的数据进行处理
                            ch.pipeline().addLast(
                                new SimpleChannelInboundHandler<String>() {
                                // 读取事件的回调方法
                                @Override
                                protected void channelRead0(ChannelHandlerContext
                                    ctx, String msg) {
                                    System.out.println("收到客户端信息:" + msg);
                                }
                            });
                        }
                    });
            // 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
            ChannelFuture cf = server.bind("127.0.0.1",8888).sync();
            // 关闭服务端的方法(之后不会在这里关闭)
            cf.channel().closeFuture().sync();
        }finally {
            // 优雅停止之前创建的两个Group
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

3 客户端

再构建一个NettyClient客户端,代码如下:

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class NettyClient {
    public static void main(String[] args) {
        // 由于无需处理连接事件,所以只需要创建一个EventLoopGroup
        EventLoopGroup worker = new NioEventLoopGroup();
        // 创建一个客户端(同之前的Socket、SocketChannel)
        Bootstrap client = new Bootstrap();
        try {
            client.group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc)
                            throws Exception {
                            // 添加一个编码处理器,对数据编码为UTF-8格式
                            sc.pipeline().addLast(new
                                    StringEncoder(CharsetUtil.UTF_8));
                        }
                    });
            // 与指定的地址建立连接
            ChannelFuture cf = client.connect("127.0.0.1", 8888).sync();
            // 建立连接成功后,向服务端发送数据
            System.out.println("正在向服务端发送信息......");
            cf.channel().writeAndFlush("我是技术闲聊DD!");
        } catch (Exception e){
          e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }
}

4 启动运行

我们先启动服务端,再启动客户端,就会看到如下:
在这里插入图片描述
在这里插入图片描述
上面的案例,就是利用了Netty实现了简单的对端通信,实现的功能很简单。

5 代码流程

接下来我为大家解释一下Netty里面的一些核心概念。

  • EventLoopGroup:负责管理Channel的事件处理任务。管理多个EventLoop的组件。它为网络应用程序提供了更好的并发支持和处理请求的能力
  • ServerBootstrap/Bootstrap:意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
  • childHandler:可以理解成过滤器,在我们以前学习的Servlet编程中,新请求到来都会经过一个个的过滤器,而这个处理器也类似于之前的过滤器,新连接到来时,也会经过添加好的一系列处理器。

然后我为大家把上面案例的完整流程分析一下:

  1. 先创建两个EventLoopGroup事件组,然后创建一个ServerBootstrap服务端。
  2. 将创建的两个事件组bossworker绑定在服务端上,并指定服务端通道为NIO类型
  3. 在server上添加处理器,对新到来的Socket连接进行处理,在这里主要分为两类:
    • ChannelInitializer:连接到来时执行,主要是用于添加更多的处理器(只触发一次)
    • addLast():通过该方式添加的处理器不会立马执行,而是根据处理器类型择机执行
  4. 为创建好的服务端绑定IP及端口号,调用sync()意思是阻塞至绑定成功为止。
  5. 再创建一个EventLoopGroup事件组,并创建一个Bootstrap客户端。
  6. 将事件组绑定在客户端上,由于无需处理连接事件,所以只需要一个事件组。
  7. 指定Channel通道类型为NIO、添加处理器…(同服务端类似)
  8. 与前面服务端绑定的地址建立连接,由于默认是异步的,也要调用sync()阻塞。
  9. 建立连接后,客户端将数据写入到通道准备发送,首先会先经过添加好的编码处理器,将数据的格式设为UTF-8。
  10. 服务器收到数据后,会先经过解码处理器,然后再去到入站处理,执行对应的Read()方法逻辑。
  11. 客户端完成数据发送后,先关闭通道,再优雅关闭创建好的事件组。
  12. 同理,服务端工作完成后,先关闭通道再停止事件组。

大家可以先看完我上面写的流程,然后再回到案例代码上去看,就会感觉到比较清晰。
注意:Netty大部分操作都是异步的,比如地址绑定、客户端连接。就类似于调用connect()方法与服务端建立连接时,主线程自己并不会去执行这个动作,而是会把这个工作交给事件组中的线程去完成,所以此刻如果主线程直接去向通道中写入数据,有几率会出现报错,在实际生产环境中,可能由于网络延迟导致连接建立的时间有些长,此时通道并未建立成功,因此尝试发送数据时就会有问题。

2. 核心组件 - 启动器与事件组

2.1 启动器 ServerBootstrap和Bootstrap

首先我们先看一下ServerBootstrap及Bootstrap的类继承结构图:
在这里插入图片描述
我们可以看一个表格,就会对其有所了解。
在这里插入图片描述

2.2 事件组EventLoopGroupEventLoop

EventLoopGroup 是一组 EventLoop 的集合,可以理解为 EventLoop 的管理器。其作用是负责创建和管理一组 EventLoop 对象,其中每个 EventLoop 对象都对应一个线程,用于处理网络 I/O 事件(例如读写数据、连接建立、连接关闭等)。在 Netty 中,通常会使用两种不同的 EventLoopGroupBossEventLoopGroupWorkerEventLoopGroup。其中 BossEventLoopGroup 负责处理连接建立事件,而 WorkerEventLoopGroup 负责处理连接已经建立后的数据读写事件。这样可以将连接建立处理与数据读写处理分开,提高并发性能。

EventLoop 则是一个重要的核心组件,用于处理网络 I/O 事件。它负责执行一系列的 I/O 操作,例如读取数据、写入数据、注册感兴趣的事件以及调度用户自定义的任务等。每个 EventLoop 都有一个独立的任务队列和计时器队列,并且运行在一个独立的线程上。同时,每个 EventLoop 还会被绑定到一个 NIO Selector 对象上,用于轮询网络 I/O 事件,处理事件后再将相应的任务加入到任务队列中。

注意:既然EventLoop/EventLoopGroup继承自JDK原生的定时线程池,那也就代表着,它拥有JDK线程池中所有提供的方法,同时也应该会支持执行异步任务、定时任务的功能。

 public static void main(String[] args) {
        EventLoopGroup threadPool = new NioEventLoopGroup();
        // 递交Runnable类型的普通异步任务
        threadPool.execute(()->{
            System.out.println("execute()方法提交的任务....");
        });
        // 递交Callable类型的有返回异步任务
        threadPool.submit(() -> {
            System.out.println("submit()方法提交的任务....");
            return "我是执行结果噢!";
        });
        // 递交Callable类型的延时调度任务
        threadPool.schedule(()->{
            System.out.println("schedule()方法提交的任务,三秒后执行....");
            return "调度执行后我会返回噢!";
        },3, TimeUnit.SECONDS);
        // 递交Runnable类型的延迟间隔调度任务
        threadPool.scheduleAtFixedRate(()->{
            System.out.println("scheduleAtFixedRate()方法提交的任务....");
        },3,1,TimeUnit.SECONDS);
    }

在这里插入图片描述

执行结果如下:
    立即执行:
        execute()方法提交的任务....
        submit()方法提交的任务....
    
    延时三秒后执行:
        schedule()方法提交的任务....
        scheduleAtFixedRate()方法提交的任务....
    
    之后没间隔一秒执行:
        scheduleAtFixedRate()方法提交的任务....
        scheduleAtFixedRate()方法提交的任务....

上述我们创建了一个EventLoopGroup事件循环组,然后通过之前JDK线程池提供的一系列的提交任务的方法,向其递交了几个异步任务,然后运行该程序,显然,EventLoopGroup确实可以当做JDK原生线程池来使用。
还有几个方法:

  • EventLoop.inEventLoop(Thread):判断一个线程是否属于当前EventLoop。
  • EventLoop.parent():判断当前EventLoop属于哪一个事件循环组。
  • EventLoopGroup.next():获取当前事件组中的下一个EventLoop(线程)。

大家还记得在前面的示例代码中,我们定义了两个组,为什么吗?主要是定义两个组的好处在于:可以让Group中的每个EventLoop分工更加明确,不同的Group分别处理不同类型的事件,各司其职。

为服务端绑定了两个事件循环组,也就代表着会根据ServerSocketChannel上触发的不同事件,将对应的工作分发到这两个Group中处理,其中boss主要负责客户端的连接事件,而worker大多数情况下负责处理客户端的IO读写事件。

过程是:当客户端的SocketChannel连接到来时,首先会将这个注册事件的工作交给boss处理,boss会调用worker.register()方法,将这条客户端连接注册到worker工作组中的一个EventLoop上。前面提到过,EventLoop内部会维护一个Selector选择器,因此实际上也就是将客户端通道注册到其内部中的选择器上。

注意:将一个Socket连接注册到一个EventLoop上之后,这个客户端连接则会和这个EventLoop绑定,以后这条通道上发生的所有事件,都会交由这个EventLoop处理。

大家有没有思考一个问题,就是由于EventLoopGroup本质上可以理解成一个线程池,其中存在的线程资源自然是有限的,那如果过来的客户端连接大于线程数量怎么办呢?答案是因为Netty本身是基于Java-NIO封装的,而NIO底层又是基于多路复用模型实现的,天生就能实现一条线程管理多个连接的功能,所以就算连接数大于线程数,也完全可以Hold住。

总结:简单来说可以EventLoop理解成有一条线程专门维护的Selector选择器,而EventLoopGroup则可以理解成一个有序的定时调度线程池,负责管理所有的EventLoop

下一篇会为大家介绍Netty的通道处理器以及缓冲区,希望大家多多关注。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

网络编程的无冕之王-Netty入门和核心组件介绍 的相关文章

  • 抽象超类的默认接口方法

    可以说我有以下结构 abstract class A abstract boolean foo interface B default boolean foo return doBlah class C extends A implemen
  • 带有来自 Selenium 2 / WebDriver 的 Id 的 jQuery 元素选择器

    我可以在 Selenium 中获取元素的 ID RemoteWebElement webElement getId 它返回一个像这样的字符串 e9b6a1cc bb6f 4740 b9cb b83c1569d96d 我想知道这个ID的来源
  • 按下按钮时清除编辑文本焦点并隐藏键盘

    我正在制作一个带有编辑文本和按钮的应用程序 当我在 edittext 中输入内容然后单击按钮时 我希望键盘和焦点在 edittext 上消失 但我似乎无法做到这一点 我在 XML 中插入了这两行代码 android focusable tr
  • 警告:跳过条目,因为它不是绝对 URI。 NetBeans 中的 GlassFish

    我成功安装了 GlassFish 但是 当我启动服务器时 我收到两条警告消息 警告 跳过条目 因为它不是绝对 URI 那是关于什么的 Launching GlassFish on Felix platform Aug 09 2014 10
  • Java 9 中可以使用提前编译吗?

    As per JEP 295 http openjdk java net jeps 295 任何 JDK 模块 类或用户代码的 AOT 编译都是实验性的 JDK 9 中不支持 要使用 AOT 化的 java base 模块 用户必须编译该模
  • JPA:如何将字符串持久保存到数据库字段中,输入 MYSQL Text

    需求是用户可以写文章 所以我选择typeText为了contentmysql数据库内的字段 我怎样才能转换Java String into MySQL Text 干得好Jim Tough Entity public class Articl
  • 适用于 Solaris 的 Java 8 中缺少 javaws

    看起来 Oracle 从 Java 8 for Solaris 中删除了 Java Web Start javaws 在 Java 8u51 中不再可用 来自兼容性指南 http www oracle com technetwork jav
  • 根据哈希值确认文件内容

    我需要 检查完整性 content文件数量 文件将写入 CD DVD 可能会被复制多次 这个想法是识别正确复制的副本 在从 Nero 等中删除它们之后 我对此很陌生 但快速搜索表明Arrays hashCode byte http down
  • 在java.util中获取错误ArrayList不带参数[重复]

    这个问题在这里已经有答案了 我已经创建了一个类 Student 现在我尝试将我的 Student 对象存储在 ArrayList 中 但在编译 ArrayList 不接受参数时出现错误 我已经检查了我的代码很多次 但找不到问题所在 我的学生
  • perl 和 java 正则表达式功能之间有什么区别?

    perl 和 java 在支持哪些正则表达式术语方面有什么区别 这个问题仅涉及正则表达式 并且特别排除了how可以使用正则表达式 即使用正则表达式的可用函数 方法 以及语言之间的语法差异 例如java要求转义反斜杠等 特别令人感兴趣的是 j
  • 使用 include 进行 JAXB 剧集编译不起作用

    我有 2 个模式 A B 我在 B 中重用了一些 A 元素 我不使用命名空间 我在用着
  • 链表中的虚拟节点

    问 什么时候使用它们 作业问题 列表中的第一个和最后一个节点 有时用作列表中的第一个和最后一个节点 从未用作列表中的第一个和最后一个节点 维基百科说 哨兵节点是与链接一起使用的专门指定的节点 列表和树作为遍历路径终止符 哨兵节点的作用是 不
  • 将变量从 jenkins 传递到 testng.xml

    我想根据从詹金斯传递的变量运行测试用例 例如 选择您要运行的测试用例 测试用例一 测试用例二 在 pom xml maven 中
  • 使用链接列表插入优先级队列的方法

    首先 我觉得我应该提到这是一项作业 我并不是在寻找直接的代码答案 只是为了指出正确的方向 我们被要求在链表中实现优先级队列 我正在努力编写 insert 函数的第一部分 在代码中我尝试检查是否head包含任何内容 如果没有则设置为head
  • 计算移动的球与移动的线/多边形碰撞的时间(2D)

    我有一个多边形 里面有一个移动的球 如果球撞到边界 它应该反弹回来 My current solution I split the polygon in lines and calculate when the ball hits the
  • Apache Kafka 是否提供异步订阅回调 API?

    我的项目正在将 Apache Kafka 视为老化的基于 JMS 的消息传递方法的潜在替代品 为了让这个过渡尽可能的顺利 如果替代的排队系统 Kafka 有一个异步订阅机制那就更理想了 类似于我们当前项目使用的JMS机制MessageLis
  • Drools:为什么是无状态会话?

    Drools 使用会话来存储运行时数据 为此 有两种会话 无状态和有状态 与无状态会话相比 有状态会话允许迭代调用 并且似乎比无状态会话具有所有优势 那么为什么会有无状态会话呢 他们服务的目的是什么 与有状态会话相比 它们的优势是什么 谢谢
  • spring data jpa 过滤 @OneToMany 中的子项

    我有一个员工测试实体是父实体并且FunGroup信息子实体 这两个实体都是通过employeeId映射 我需要一种方法来过滤掉与搜索条件匹配的子实体 以便结果仅包含父实体和子实体 满足要求 员工测试类 Entity name Employe
  • 如何隐藏或删除 Android HoneyComb 中的状态栏?

    如何隐藏或删除 Android HoneyComb 中的状态栏 每次运行应用程序时 我都会发现某些内容必须被状态栏覆盖 我尝试改变AndroidManifest xml 但没有任何改变 你不知道 它被认为是永久的屏幕装饰 就像电容式主页 菜
  • 线程“main”中出现异常 java.lang.UnsatisfiedLinkError: ... \jzmq.dll: 找不到依赖库

    我有一个使用 ZMQ 的 java 应用程序 我已经能够在我的 Win7 PC 上运行它 我将 jzmq dll 放在 jar 可执行文件所在的同一文件夹中 然后通过命令 java jar myapp jar 运行它 我的下一步是将其移至服

随机推荐

  • Ubuntu22.04使用中文输入法

    安装的时候选择了英文安装 之后切换到中文 忘记还要写中文注释 发现在语言设置里不能添加输入法 仔细找了以下发现输入法的设置改到了键盘设置里 网络上查到的大部分都是老版本的ubuntu 这个是2204版本 输入法设置位置不同
  • 闪回事务查询+闪回事务查询案例

    闪回事务查询 1闪回事务查询是闪回版本查询的一个扩充 2闪回事务查询可以审计某个事务或者撤销一个已经提交的事务 闪回事务查询案例 测试数据 create table sct4 id number 4 name varchar2 20 ins
  • uos,qt,linuxdeployqt,qt-installer-framework, 生成安装包的记录

    注 使用源码生成安装包的环境要求 已安装QT v5 5 24 DTK QTcreator linuxdeployqt qt installer framework v5 9 的UOS v20 1 打开QTcreator 新建项目 2 选择侧
  • python随机生成验证码,数字+大小写字母

    ASCII码的对照链接 大写字母的十进制范围是 65 91 小写字母的十进制范围是 97 123 数字的十进制范围是 48 58 思路 1 先在空链表中添加大小写字母和数字 2 从列表中随机选择四个验证码 3 将列表转化成字符串输出 代码如
  • python 进行排序的两种方式 sort和sorted

    方法1 用List的成员函数sort进行排序 方法2 用内建函数sorted进行排序 sort函数定义 sort cmp None key None reverse False sorted函数定义 sorted iterable cmp
  • Cannot invoke “String.equalsIgnoreCase(String)“ because “code“ is null

    问题 同时开启多个项目 端口号不一致导致项目前后端错乱匹配 解决办法 后端 ruoyi admin下的application yml中的port 端口号 前端 vue config js里的port 端口号修改一致
  • cpp 解析HTML之 htmlcxx

    html与xml格式上比较相似 但xml不并一定能支持html的解析 这里介绍一个c 解析html的开源项目 htmlcxx 一 代码示例 1 项目源码下载之后 使用vs打开即可 默认为生成 lib静态库及MTd模式 可以在属性中修改指定为
  • httprunner测试框架3--har2case录制脚本

    har2case录制脚本 录制脚本 只是一个过渡 可以将录制的 har脚本快速转化成httprunner脚本文件 不能依靠录制 har2case可以将 har文件转化成yaml格式或者json格式的httprunner的脚本 可以借助fid
  • java代码kafka初始化producer和consumer

    目录 一 初始化producer对象 序列化消息 生产者发送消息的三种方式 kafka生产者其它详细知识 二 初始化consumer对象 反序列化消息 consumer取消订阅的方式consumer unsubscribe 使用自定义的序列
  • 回溯法解决地图填色问题

    目录 回溯法 最大度优先 最少可选颜色优先 向前探测 随机产生不同规模的图 分析算法效率与图规模的关系 四色 回溯法 回溯法的基本思想是采用递归和深度优先搜索的方法 尝试在一组可能的解中搜索出符合要求的解 在搜索过程中 若发现当前所选的方案
  • 颜色值不透明度对应表(0%-100%)

    不透明度 ps 可以理解为alpha 0 1的值
  • 腾讯大佬告诉你,写Python到底用什么IDE合适

    不管你是 Python 新手还是老鸟 肯定纠结过一个问题 到底用什么编辑器写 Python 代码好 为此 我们调查了数十位鹅厂程序猿们爱用的 Python IDE 从他们对每款编辑器的看法中 也许能给你一点启示 入门Python其实很容易
  • lzma sdk文件压缩与解压

    最新版的lzma sdk可以去SourceForge上面获取 具体地址为点击打开链接 7z官网点击打开链接 英文版 点击打开链接 中文版 我用lzma sdk主要是为了用来压缩和解压文件用的 其实适当的用法可以用来制作安装包 因为安装包也是
  • 在Linux的Ubuntu系统下安装QT及相关环境配置

    安装QT 从官网下载QT的安装包 在本地安装 本次使用的是Ubuntu18 06以及QT5 14 2 下文皆使用此版本作为示例 首先安装QT 具体操作如下 1 使用cd命令 cd home usr download 切换至安装包所在的目录下
  • Proteus仿真时数码管不能动态显示的问题及解决方法

    今天做Proteus仿真时遇到了数码管不能动态显示的问题 我的程序是用单片机P1口控制数码管段选 P2口低四位控制数码管位选 数码管1ms显示一位 全部刷新需要4ms 正常情况下人眼是感受不到这个速度下数码管的刷新过程的 所以他应该是这个样
  • 【图文解析 】Java中的Liu、继承、组合

    Alt Shift s 快捷键
  • Vue Element-ui el-table sortablejs 表格拖拽排序

    首先要在项目中本地安装 sortablejs 执行 npm install sortablejs save 然后在要实现表格拖拽的 vue文件中 引入 sortablejs import Sortable from sortablejs 基
  • PPPOE协议工作流程

    PPPoE Point to Point Protocol over Ethernet 基于以太网的点对点协议 的工作流程包含发现 Discovery 和会话 Session 两个阶段 发现阶段是无状态的 目的是获得PPPoE 终端 在局端
  • A5M2使用

    目录 一 修改语言 二 连接数据库 三 SQL相关 3 1 新建SQL 3 2 生成增删改查SQL 方式一 方式二 3 3 生成批量插入insert语句 3 4 生成DDL 3 5 SQL整形 3 6 SELECT 之后修改数据 3 7 生
  • 网络编程的无冕之王-Netty入门和核心组件介绍

    最近我在研究Netty 之前只是经常听说 并没有实际做过研究 为什么突然要好好研究一下它 主要是因为前段时间 我在看RocketMQ底层原理的时候发现它的底层的网络通信都是基于Netty 然后网上一查 果然 大家太多的耳熟能详的工具组件 都