任何数据类型想在网络中进行传输,都得经过编解码转换成字节流

在netty中,服务端和客户端进行通信的其实是下面这样的

程序 ---编码--> 网络

网络 ---解码--> 程序

对应服务端:

  • 入站数据, 经过解码器解码后给后续的handler使用
  • 出站数据, 经过编码器编码成字节流给在网络上传播

在netty中的编码器其实就是一个handler,回想一下,无论是编写服务端的代码,还是客户端的代码,总会通过一个channelIniteializer往pipeline中动态的添加多个处理器,在添加我们自定义的处理器之前,往往会添加编解码器,其实说白了,编解码器其实就是特定功能的handler

我们这样做是有目的的,因为第一步就得需要把字节流转换成我们后续的handler中能处理的常见的数据类型

Netty中的编解码器太多了,下面就用常用的ByteToMessageDecoder介绍他的体系

编码器的模板基类ByteToMessageDecoder

ByteToMessageDecoder继承了ChannelInboundHandlerAdapter 说明它是处理入站方向数据的编码器,而且它也因此是一个不折不扣的Handler,再回想,其实In开头的handler都是基于事件驱动的,被动的处理器,当客户端发生某种事件时,它对应有不同的动作回调,而且它的特色就是 fireXXX往下传递事件, 待会我们就能看到,netty用它把处理好的数据往下传递

架构概述

ByteToMessageDecoder本身是一个抽象类,但是它只有一个抽象方法 decode()

netty中的解码器的工作流程如下:

  • 累加字节流
  • 调用子类的decode()方法进行解码
  • 将解析完成的ByteBuf往后传递

既然是入栈处理器,有了新的数据,channelRead()就会被回调,我们去看一下它的channelRead()

下面是它的源码,

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) { // todo 在这里判断, 是否是 ByteBuf类型的,如果是,进行解码,不是的话,简单的往下传播下去
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
// todo 进入查看 cumulation是类型 累加器,其实就是往 ByteBuf中 write数据,并且,当ByteBuf 内存不够时进行扩容
first = cumulation == null; // todo 如果为空, 则说明这是第一次进来的数据, 从没累加过
if (first) {
cumulation = data; // todo 如果是第一次进来,直接用他将累加器初始化
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); // todo 非第一次进来,就进行累加
}
// todo , 这是第二部, 调用子类的decode()进行解析
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
} int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
// todo 调用 fireChannelRead,向后船舶channelRead事件, 前面的学习也知道, 她会从当前节点,挨个回调pipeline中处理器的CHannelRead方法
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}

其实三步工作流程就在上面的代码中

  • 累加字节流 cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
  • 调用子类的decode()进行解析 callDecode(ctx, cumulation, out);
  • 将解析完成的ByteBuf往后传递 fireChannelRead(ctx, out, size);

它的设计很清晰, 由ByteToMessageDecoder完成整个编码器的模板,规定好具体的处理流程,首先它负责字节流的累加工作,但是具体如何进行解码,由不同的子类去实现,因此它设及成了唯一的抽象方法,在他的模板中,子类将数据解码完成后,它再将数据传播下去

什么是累加器cumulation?

源码如下:我们可以看到,其实他就是一个辅助对象, 里面维护了一个 ByteBuf的引用

  • 所谓累加,就是往ByteBuf中write数据
  • 所谓维护,就是 动态判断ByteBuf中可写入的区域大小和将写入的字节的关系
  • 最后,为了防止内存泄露,将收到的ByteBuf 释放
// todo 创建一个累加器
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
final ByteBuf buffer;
// todo 如果 writerIndex + readableBytes > cumulation.maxCapacity 说明已经无法继续累加了
if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
|| cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
// todo 扩容
buffer = expandCumulation(alloc, cumulation, in.readableBytes());
} else {
buffer = cumulation;
}
// todo 往 ByteBuf中写入数据 完成累加
buffer.writeBytes(in);
// todo 累加完成之后,原数据 释放掉
in.release();
return buffer;
}
};

第二步, callDecode(ctx, cumulation, out)

我们直接跟进源码: 可以看到,在把ByteBuf真正通过下面的 decodeRemovalReentryProtection(ctx, in, out);的子类进行解码时, 它记录下来了当时ByteBuf中可读的字节数, 它用这个标记和经过子类处理之后的ByteBuf的可读的字节数进行比对,从而判断出子类是否真的读取成功

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size(); if (outSize > 0) {// todo 如果盛放解析完成后的数据的 out集合中有数据
fireChannelRead(ctx, out, outSize); /// todo 传播channelRead事件,数据也传递进去
out.clear(); // todo 清空out 集合 if (ctx.isRemoved()) {
break;
}
outSize = 0;
} // todo 记录 子类使用in之前, in中的可读的字节
int oldInputLength = in.readableBytes(); //todo 调用子类重写的 decode()
decodeRemovalReentryProtection(ctx, in, out);
if (ctx.isRemoved()) {
break;
} if (outSize == out.size()) { // todo 0 = 经过上面的decode解析后的 out.size()==0 , 说明没解析出任何东西
if (oldInputLength == in.readableBytes()) { // todo 第一种情况就是 可能字节数据不够, 根本没从in中读
break;
} else {
continue; // todo 情况2: 从in中读了, 但是没来得及继续出 内容
}
}
// todo 来到这里就说明,已经解析出数据了 ,
// todo 解析出数据了 就意味着in中的readIndex被子类改动了, 即 oldInputLength != in.readableBytes()
// todo 如下现在还相等, 肯定是出问题了
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Throwable cause) {
throw new DecoderException(cause);
}
}

如何实现自己的解码器?

实现自己的解码器, 就得了解这三个参数分别是什么

  • ctx: 当前的hander所在的 Context
  • cumulation: 累加器,其实就是ByteBuf
  • out: 她其实是个容器, 用来盛放 经过编码之后的数据,也就是可以被后续的处理器使用 类型

实现的思路就是继承ByteToMessageDecoder然后重写它唯一的抽象方法,decode(), 实现的逻辑如下:

protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyDeCoderHandler invoke...");
System.out.println(in.readableBytes());
if (in.readableBytes()>=8){
out.add(in.readLong());
}
}

常用的编解码器

固定长度的解码器FixedLengthFrameDecoder

他里面只维护着一个private final int frameLength;

使用时,我们通过构造函数传递给他,他就会按照下面的方式解码

我们看一下它的javaDoc

 原始数据
* +---+----+------+----+
* | A | BC | DEFG | HI |
* +---+----+------+----+ 如果frameLength==3
* +-----+-----+-----+
* | ABC | DEF | GHI |
* +-----+-----+-----+

它的decode() 实现如下

protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in.readableBytes() < frameLength) {
return null;
} else {
// 从in中截取 frameLength 长度的 字节流
return in.readRetainedSlice(frameLength);
}
}

行解码器LineBasedFrameDecoder

她会根据换行符进行解码, 无论用户发送过来的数据是以 \r\n 还是 \n 类型的换行符LineBasedFrameDecoder

使用:


public LineBasedFrameDecoder(final int maxLength) {
this(maxLength, true, false);
} public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
this.maxLength = maxLength;
this.failFast = failFast;
this.stripDelimiter = stripDelimiter;
}

第一个构造函数

  • 入参位置是我们指定的每一行最大的字节数, 超过了这个大小的所有行,将全部被丢弃
  • 默认跳过分隔符
  • 出现了超过最大值的行,不报异常

第二个构造函数

  • 入参1 是我们指定的每一行最大的字节数, 超过了这个大小的所有行,将全部被丢弃
  • 入参2 指定每次解析是否跳过换行符
  • 入参3 指定出现大于规定的最大字节数时是否报异常

看它重写的decode()的实现逻辑如下:

它总起来分成四种情况

  • 非丢弃模式

    • 找到了换行符

      • 如 readIndex + 换行符的位置 < maxLength 的关系 --> 解码
      • 如 readIndex + 换行符的位置 > maxLength的关系 --> 丢弃
    • 未找到换行符
      • 如果可解析的长度 > maxLength --> 丢弃
  • 丢弃模式
    • 找到了换行符

      • 丢弃
    • 未找到换行符
      • 丢弃

基于分隔符的解码器DelimiterBasedFrameDecoder

它主要有这几个成员变量, 根据这几个成员变量,可以选出使用它哪个构造函数

private final ByteBuf[] delimiters;  分隔符,数组
private final int maxFrameLength; 每次能允许的最大解码长度
private final boolean stripDelimiter; 是否跳过分隔符
private final boolean failFast; 超过最大解码长度时,是否抛出异常
private boolean discardingTooLongFrame; 是否丢弃超过最大限度的帧
private int tooLongFrameLength; 记录超过最大范围的字节数值

分三步

  • 第一, 判断我们传递进入的分隔符是否是\n \r\n 如果是的话,就是用上面的, 行解码器
  • 第二步, 按照最细的力度进行解码, 比如, 我们有两个解码器, AB, 当前的readIndex 到A, 有2个字节, 到B有3个字节, 就会按照A进行解码
  • 解码

基于长度域的解码器LengthFieldBasedFrameDecoder

通常我们在对特定的网络协议进行解码时会用到它,比如说,最典型的http协议, 虽然http协议看起来, 又有请求头,又有请求体,挺麻烦的,它在网络中依然是以字节流的方式进行传输

基于长度域,指的是在传输的协议中有一个 length字段,这个十六进制的字段记录的可能是整个协议的长度,也可能是消息体的长度, 我们根据具体情况使用不同的构造函数

如何使用呢? 最常用它下面的这个构造函数

public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip) {
this(
maxFrameLength,
lengthFieldOffset, lengthFieldLength, lengthAdjustment,
initialBytesToStrip, true);
}

使用它的前提是,知道这五个参数的意思

  • maxFrameLength 每次解码所能接受的最大帧的长度
  • lengthFieldOffset 长度域的偏移量

听着挺高大尚的, 偏移量, 说白了,就是在现有的这段字节数据中找个开始解码的位置, 大多数设为0, 意为,从o位置 开始解码

  • lengthFieldLength 字段域的长度, 根据lengthFieldOffset的初始值往后数lengthFieldLength个字节,这段范围解析出来的数值 可能是 长度域的大小,也可能是整个协议的大小(包括header,body...) 根据不同的协议不同
  • lengthAdjustment 矫正长度
  • initialBytesToStrip 需要取出的长度

下面是javaDoc给的例子

基于长度的拆包

* BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
* +--------+----------------+ +--------+----------------+
* | Length | Actual Content |----->| Length | Actual Content |
* | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
* +--------+----------------+ +--------+----------------+
这是最简单的情况, 假定 Length的长度就是后面的 真正需要解码的内容 现在的字节全部解码后是这样的 12HELLO, WORLD
我们要做的就是区分出 12和HELLO, WORLD * lengthFieldOffset = 0
* lengthFieldLength = 2 // todo 每两个字节 表示一个数据包
* lengthAdjustment = 0
* initialBytesToStrip = 0 意思就是:
字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,就是后面的字段域的长度
00 0C ==> 12
这个12 意思就是 长度域的长度, 说白了 就是我们想要的 HELLO, WORLD 的长度 这样一算,就分开了
基于长度的阶段拆包

* BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
* +--------+----------------+ +----------------+
* | Length | Actual Content |----->| Actual Content |
* | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
* +--------+----------------+ +----------------+
情况2: * lengthFieldOffset = 0
* lengthFieldLength = 2 // todo 每两个字节 表示一个数据包
* lengthAdjustment = 0
* initialBytesToStrip = 2 意思就是
字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,就是后面的字段域的长度是
00 0C ==> 12
这个12 意思就是 长度域的长度, 说白了 就是我们想要的 HELLO, WORLD 的长度 然后, 从0开始 忽略 initialBytesToStrip, 就去除了 length ,只留下 HELLO, WORLD 有时, 在某些其他协议中, length field 可能代表是整个消息的长度, 包括消息头
在这种情况下,我们就得指定一个 非零的 lengthAdjustment 去调整 * BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
* +--------+----------------+ +--------+----------------+
* | Length | Actual Content |----->| Length | Actual Content |
* | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
* +--------+----------------+ +--------+----------------+ * lengthFieldOffset = 0
* lengthFieldLength = 2 // todo 每两个字节 表示一个数据包
* lengthAdjustment = -2
* initialBytesToStrip = 0 意思就是 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,表示整个协议的长度
00 0C ==> 14 意味,协议全长 14
现在还是不能区分开 Length 和 Actual Content 公式: 数据包的长度 = 长度域 + lengthFieldOffset + lengthFieldLength +lengthAdjustment 通过他可以算出 lengthAdjustment = -2
基于偏移长度的拆包
    

 * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
* +----------+----------+----------------+ +----------+----------+----------------+
* | Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
* | 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
* +----------+----------+----------------+ +----------+----------+----------------+
这个例子和第一个例子很像,但是多了头 我们想拿到后面消息长度的信息,就偏移过header * lengthFieldOffset = 2
* lengthFieldLength = 3 // todo 每两个字节 表示一个数据包
* lengthAdjustment = 0
* initialBytesToStrip = 0 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制, 表示长度域的长度 在这里 整好跳过了 header 1, 0x00 00 0C 是三个字节
也就是 字节数组[lengthFieldOffset,lengthFieldLength]=>[0,3]
0x00 00 0C == 12 表示长度域是 12 现在也成功区分开了 Header 1 和 Length 和 Actual Content
分别是 2 3 12
基于可调整长度的拆包
  

  BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
* +----------+----------+----------------+ +----------+----------+----------------+
* | Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
* | 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
* +----------+----------+----------------+ +----------+----------+----------------+ * lengthFieldOffset = 0
* lengthFieldLength = 3 // todo 每两个字节 表示一个数据包
* lengthAdjustment = 2
* initialBytesToStrip = 0 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制, 表示长度域的长度 也就是 字节数组[lengthFieldOffset,lengthFieldLength]=>[0,3]
0x00 00 0C 是三个字节
0x00 00 0C == 12 表示长度域是 12 == 长度域的长度 就是 HELLO, WORLD的长度
但是上面的图多了一个 两个字节长度的 Header 1
下一步进行调整 公式: 数据包的长度 = 长度域 + lengthFieldOffset + lengthFieldLength +lengthAdjustment lengthAdjustment= 17-12-0-3=2
基于偏移可调整长度的截断拆包

* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
* +------+--------+------+----------------+ +------+----------------+
* | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
* | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
* +------+--------+------+----------------+ +------+----------------+ * lengthFieldOffset = 1
* lengthFieldLength = 2 // todo 每两个字节 表示一个数据包
* lengthAdjustment = 1
* initialBytesToStrip = 3 lengthFieldOffset =1 偏移1字节 跨过 HDR1 lengthFieldLength =2 从[1,2] ==> 0x000C =12 表示长度域的值 看拆包后的结果,后面明显还多了个 HDR2 ,进行调整
公式: 数据包值 = 长度域 + lengthFieldOffset+ lengthFieldLength + lengthAdjustment
算出 lengthAdjustment = 16 - 12 - 1 - 2 = 1 结果值只有 HDR2 和 Actual Content , 说明,前面通过 initialBytesToStrip 进行忽略
initialBytesToStrip =3
基于偏移可调整长度的 变种 截断拆包

* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
* +------+--------+------+----------------+ +------+----------------+
* | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
* | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
* +------+--------+------+----------------+ +------+----------------+ * lengthFieldOffset = 1
* lengthFieldLength = 2 // todo 每两个字节 表示一个数据包
* lengthAdjustment = -3
* initialBytesToStrip = 3 同样
看结果,保留 HDR2 和 Actual Content lengthFieldOffset = 1 表示跳过开头的 HDR1
[1,2] ==> 00 10 , 算出的 长度域的值==10 很显然这不对 10 < 13 我们要想拆出后面的数据包就得在现有的基础上往左移动三个字节 -3个调整量

Netty-解码器架构与常用解码器的更多相关文章

  1. 普适注意力:用于机器翻译的2D卷积神经网络,显著优于编码器-解码器架构

    现有的当前最佳机器翻译系统都是基于编码器-解码器架构的,二者都有注意力机制,但现有的注意力机制建模能力有限.本文提出了一种替代方法,这种方法依赖于跨越两个序列的单个 2D 卷积神经网络.该网络的每一层 ...

  2. 这可能是目前最透彻的Netty原理架构解析

    https://juejin.im/post/5be00763e51d453d4a5cf289 本文基于 Netty 4.1 展开介绍相关理论模型,使用场景,基本组件.整体架构,知其然且知其所以然,希 ...

  3. Netty原理架构解析

    Netty原理架构解析 转载自:http://www.sohu.com/a/272879207_463994本文转载关于Netty的原理架构解析,方便之后巩固复习 Netty是一个异步事件驱动的网络应 ...

  4. 【Netty】最透彻的Netty原理架构解析

    这可能是目前最透彻的Netty原理架构解析 本文基于 Netty 4.1 展开介绍相关理论模型,使用场景,基本组件.整体架构,知其然且知其所以然,希望给大家在实际开发实践.学习开源项目方面提供参考. ...

  5. ios系统架构及常用框架

    1.iOS基于UNIX系统,因此从系统的稳定性上来说它要比其他操作系统的产品好很多 2.iOS的系统架构分为四层,由上到下一次为:可触摸层(Cocoa Touch layer).媒体层(Media l ...

  6. netty源码分析(十八)Netty底层架构系统总结与应用实践

    一个EventLoopGroup当中会包含一个或多个EventLoop. 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定. 所有由EventLoop所处理的各种I/O ...

  7. 深入Netty逻辑架构,从Reactor线程模型开始

    本文是Netty系列第6篇 上一篇文章我们从一个Netty的使用Demo,了解了用Netty构建一个Server服务端应用的基本方式.并且从这个Demo出发,简述了Netty的逻辑架构,并对Chann ...

  8. 精通并发与 Netty (二)常用的 rpc 框架

    Google Protobuf 使用方式分析 对于 RPC 协议来说,最重要的就是对象的发送与接收,这就要用到序列化与反序列化,也称为编码和解码,序列化与反序列化和网络传输一般都在对应的 RPC 框架 ...

  9. iOS 系统架构及常用框架

    1.iOS基于UNIX系统,因此从系统的稳定性上来说它要比其他操作系统的产品好很多 2.iOS的系统架构分为四层,由上到下一次为:可触摸层(Cocoa Touch layer).媒体层(Media l ...

随机推荐

  1. Linux文件系统操作与磁盘管理

    简单文件操作 df---->report file system disk space usage du---->estimate file space usage 2.简单的磁盘管理 d ...

  2. Android实现简单音乐播放器(startService和bindService后台运行程序)

    Android实现简单音乐播放器(MediaPlayer) 开发工具:Andorid Studio 1.3运行环境:Android 4.4 KitKat 工程内容 实现一个简单的音乐播放器,要求功能有 ...

  3. 还可以使用Q_SIGNAL,Q_EMIT,Q_SLOT避免第三方库的关键字冲突

    You can define the QT_NO_KEYWORDS macro, that disables the “signals” and “slots” macros. If you use ...

  4. MySql 小内存优化

    MySql5.6启动内存近500M,如在小型机内存敏感的环境可能较大,下边配置会减少较多内存,至150M以下. performance_schema = OFF innodb_buffer_pool_ ...

  5. 使用EurekaLog时遇到的问题

    1.在DLL项目中千万不要加入EurekaLog,不然在主程序调用时就会出现莫名其妙的内存问题. 2.要使用EurekaLog发邮件的功能,发邮件的SMTP服务器必须支持8bit MIME编码.如SI ...

  6. New,Getmem,ReallocMem联系与区别(转)

    procedure New(var P: Pointer);  {为一个指针变量分配内存,会自动计算指针所指数据结构需要空的空间大小} procedure GetMem(var P: Pointer; ...

  7. IT安全军火库-转

    全球有260万信息安全专业人士,渗透测试工具是他们“安全军火库”中最常使用的装备,但直到最近,可用的渗透测试工具才丰富起来,但这也带来一个问题,挑选合适的渗透测试工具成了一件麻烦事,一个最简单的方法就 ...

  8. SQL 游标知识整理

    游标声明格: declare 游标名称 cursor (游标关键字) for 游标操作对象(select * from 表名称)游标使用: open 游标名称; fetch first from 游标 ...

  9. hadoop之hbase基本操作

    hbase shell 进入hbase命令行 list 显示HBASE表 status 系统上运行的服务器的细节和系统的状态 version 返回HBase系统使用的版本 table_help 引导如 ...

  10. rm、shutdown、磁盘挂载、vi使用方法

    1. 系统管理文件 1.1 rm 文件与目录有关命令 删除命令 (慎用)    --- 数据是否备份了 rm === remove rm /oldboy/oldboy.txt  --- 删除文件 rm ...