2024年2月

1 前言

​ 本文主要介绍 Unity 打包发布 Android apk 流程、基于 AndroidJavaObject(或 AndroidJavaClass)实现 Unity 调用 Java 代码、基于 UnityPlayer 实现 Java 调用 Unity 代码,官方介绍见→
Android

​ Unity 项目 C# 中获取平台的代码如下,需要引入 UnityEngine 命名空间。

RuntimePlatform platform = Application.platform; 

​ RuntimePlatform 是枚举类型,主要平台如下。

public enum RuntimePlatform {
    OSXEditor = 0, // editor on macOS.
    OSXPlayer = 1, // player on macOS.
    WindowsPlayer = 2, // player on Windows.  
    OSXWebPlayer = 3, // web player on macOS.
    ...
    WindowsWebPlayer = 5, // web player on Windows.
    WindowsEditor = 7, // editor on Windows.
    IPhonePlayer = 8, // player on the iPhone.
    Android = 11, // player on Android.
    ...
    WebGLPlayer = 17, // player on WebGL
    ...
    LinuxPlayer = 13, // player on Linux.
    LinuxEditor = 16, // editor on Linux.
    ...
}

2 Unity 发布 apk

2.1 安装 Android Build Support

​ 在 Unity Hub 中打开添加模块窗口,操作如下。

img

​ 选择 Android Build Support 安装,如下(笔者这里已安装过)。

img

​ 创建一个 Unity 项目,依次点击【File→Build Settings→Android→Switch Platform】,配置如下。

img

​ 依次点击【Edit→Preferences→External Tools】打开 JDK、SDK、NDK、Gradle 配置页面,勾选默认配置,如下。

img

​ Unity Editor 不同版本默认下载的 Gradle 版本如下,官方介绍见→
Gradle for Android

img

​ 用户也可以选择已安装的 JDK、SDK、NDK、Gradle 路径,如下。

img

​ 笔者的具体环境配置如下:

Unity Editor: 2021.3.11f1c2
JDK: 1.8.0_391
SDK Platforms: 29
SDK Build-Tools: 30.0.3
SDK Command-line Tools: 11.0
SDK Platform-Tools: 34.0.5
NDK: 21.3.6528147
Gradle: 6.1.1
Gradle Plugin: 4.0.1

2.2 配置密钥

​ 依次点击【Edit→Project Settings→Player→Keystore Manager】(也可以从【File→Build Settings→Player Settings→Keystore Manager】中进入),操作如下。

img

​ 打开 Keystore Manager 后, 依次点击【Create New→Anywhere】,选择一个目录保存密钥库文件,笔者保存在项目目录下面的【Keystore/user.keystore】中。

img

​ 接着设置密码和别名,其他选项不是必设项。

img

​ Add Key 后,会弹出 “是否将创建的密钥库作为项目的密钥库” 弹窗,点击 yes 确认。

img

​ 设置密钥后,回到 Project Settings 页面,显示如下。

img

​ 创建密钥时,也可以通过以下命令创建。

keytool -genkey -keyalg RSA -alias key_name -keystore keystore_name -validity day_time
keytool -genkey -keyalg RSA -alias first -keystore user -validity 36500

img

2.3 打包 apk

​ 依次点击【File→Build Settings→Player Settings】,配置公司名、项目名、版本号等信息,如下。

img

​ 在 Other Settings 中配置包名、Android SDK 的最小 API 版本、目标 API 版本等信息,如下。

img

​ 关闭 Player Settings,在 Build Settings 页面点击底部的 Build,构建 apk。

2.4 案例

​ 新建一个 Unity 项目,修改 Game 页面的屏幕尺寸,如下。

img

​ 搭建页面如下。

img

​ 给 Button 按钮添加脚本,如下。

​ Test.cs

using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour {
    private Button button;

    private void Start() {
        button = GetComponent<Button>();
        button.onClick.AddListener(OnClick);
    }

    private void OnClick() {
        Debug.Log("Test-OnClick");
    }
}

​ 编译 apk 后,打开命令行窗口,输入以下命令,将 apk 安装到手机上。

adb instll -r -t -d Test.apk

​ 运行 apk 后,在命令行窗口中通过以下命令查看日志。

adb logcat | findstr "Test-OnClick"

​ 点击 Button 按钮,打印日志如下。

img

3 Unity 调 Android 的逻辑

3.1 Unity 项目中部署 Android 代码


1)拷贝 Java 源码到 Unity 项目

​ 可以将 Android 项目中 Java 代码拷贝到 Unity 项目中 Assets 子目录下,如下,接着就可以通过 AndroidJavaClass 或 AndroidJavaObject 访问 Java 代码了。

img


2)打包 Jar 到 Unity 项目

​ 可以将 Android 项目打包为 Jar,再将 Jar 拷贝到 Unity 项目中 Assets 子目录下,接着就可以通过 AndroidJavaClass 或 AndroidJavaObject 访问 Java 代码了。

​ 修改 Android 项目中 Module 的 build.gradle 文件,如下,主要将 id 由 'com.android.application' 修改为 'com.android.library',删除 android { } 模块中的 defaultConfig、buildTypes、compileOptions 等子模块。

​ build.gradle

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion '30.0.3'

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

​ 修改 build.gradle 文件后,需要点击右上角的 Sync Now 同步,接着按以下步骤编译项目。

img

​ 编译结束后,在 Module 的【build\intermediates\aar_main_jar\debug】目录下生成打包的 classes.jar。

img

​ 预览 classes.jar 文件如下。

img

3.2 AndroidJavaObject 和 AndroidJavaClass

​ AndroidJavaObject 和 AndroidJavaClass 是 Unity 提供的调用 Java 代码的 2 个类,AndroidJavaClass 继承 AndroidJavaObject,它们只有构造方法有一点差异,没有其他的差异,因此,本节只介绍 AndroidJavaObject。


1)Set 和 Get 属性

​ JavaTest.java

package com.zhyan8.test;

public class JavaTest {
    public static int intVal = 0;
    private String strVal = "abc";
}

​ 说明:对于 JavaTest 的 private 属性,AndroidJavaObject 也可以访问到。

​ UnityTest.cs

using UnityEngine;

public class UnityTest : MonoBehaviour {
    private void Start() {
        AndroidJavaObject javaObject = new AndroidJavaObject("com.zhyan8.test.JavaTest");
        // 静态属性Set/Get
        javaObject.SetStatic<int>("intVal", 123);
        int intVal = javaObject.GetStatic<int>("intVal");
        Debug.Log("UnityTest, intVal=" + intVal); // 打印: UnityTest, intVal=123
        // 非静态属性Set/Get
        javaObject.Set<string>("strVal", "xyz");
        string strVal = javaObject.Get<string>("strVal");
        Debug.Log("UnityTest, strVal=" + strVal); // 打印: UnityTest, strVal=xyz
    }
}


2)调用方法

​ JavaTest.java

package com.zhyan8.test;

import android.util.Log;

public class JavaTest {
    public static void fun1() {
        Log.d("JavaTest", "fun1"); // 打印: JavaTest: fun1
    }

    private int fun2() {
        Log.d("JavaTest", "fun2"); // 打印: JavaTest: fun2
        return 123;
    }

    public String fun3(int value) {
        Log.d("JavaTest", "fun3, value=" + value); // 打印: JavaTest: fun3, value=235
        return "Call fun3";
    }

    public String fun4(String value1, int value2) {
        Log.d("JavaTest", "fun4, value1=" + value1 + ", value2=" + value2); // 打印: JavaTest: fun4, value1=abc, value2=123
        return value1 + value2;
    }
}

​ 说明:对于 JavaTest 的 private 方法,AndroidJavaObject 也可以访问到。

​ UnityTest.cs

using UnityEngine;

public class UnityTest : MonoBehaviour {
    private void Start() {
        AndroidJavaObject javaObject = new AndroidJavaObject("com.zhyan8.test.JavaTest");
        // 静态方法
        javaObject.CallStatic("fun1");
        // 非静态无参方法
        int val2 = javaObject.Call<int>("fun2");
        Debug.Log("UnityTest, val2=" + val2); // 打印: UnityTest, val2=123
        // 非静单参方法
        string val3 = javaObject.Call<string>("fun3", 235);
        Debug.Log("UnityTest, val3=" + val3); // 打印: UnityTest, val3=Call fun3
        // 非静双参方法
        string val4 = javaObject.Call<string>("fun4", "abc", 123);
        Debug.Log("UnityTest, val4=" + val4); // 打印: UnityTest, val4=abc123
    }
}

​ 运行程序后,打印日志如下。

img

3.3 Unity 调用 Android 的 Toast

​ UnityTest.cs

using UnityEngine;
using UnityEngine.UI;

public class UnityTest : MonoBehaviour {
    private void Start() {
        GetComponent<Button>().onClick.AddListener(() => {
            Toast("Clicked", 1);
        });
    }

    // 调用Android的代码: Toast.makeText(context, msg, durationFlag).show();
    private void Toast(string msg, int durationFlag) { // durationFlag: Toast.LENGTH_SHORT=0, Toast.LENGTH_LONG=1
        AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
        AndroidJavaClass unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject currentActivity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");
        AndroidJavaObject toast = toastClass.CallStatic<AndroidJavaObject>("makeText", currentActivity, msg, durationFlag);
        toast.Call("show");
    }
}

​ UnityPlayer 是 Unity 引擎提供的 Java 类,在 Unity Editor 目录下的【Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar】 中。

​ 运行效果如下。

img

4 Android 调 Unity 的逻辑

4.1 Unity 打包为 Android 项目

​ 在 Build Settings 页面勾选 Export Project 后,点击 Export 按钮,如下。

img

​ 构建成功后,Unity 项目将会被打包成一个 Android 项目,我们可以使用 Android Studio 打开生成的 Android 项目,如下。

img

​ 其中,UnityPlayerActivity 是启动的 Main Activity,unity-classes.jar 是 Unity Editor 中的 Jar 包,位置见【Unity Hub\Unity\Editor\2021.3.11f1c2\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar】,我们常用的 UnityPlayer 类就在该 Jar 文件中。

​ 如果用户想将 Android 项目打包到 Unity 项目中,但是 Android 项目中又要引用 Unity 的接口,用户可以将 Unity Editor 中的 classes.jar 拷贝到 Android 项目中,如下。

img

​ 右键 classes.jar,在弹出菜单中选择 Add As Library。

4.2 UnityPlayer

​ UnityPlayer 继承 FrameLayout,主要用于 Java 调用 Unity 代码,其主要属性和方法如下。

// 在UnityPlayer的构造方法中初始化, currentActivity = this.mActivity
public static Activity currentActivity = null;

// java调用Unity中的方法, gameObject: 游戏对象名, method: 游戏对象上挂载脚本中的方法名, params: 方法参数
public static void UnitySendMessage(String gameObject, String method, String params)

​ 说明:UnitySendMessage 可以调用 private 方法,只能调用无参和单参数方法,不能调用 static 方法,无法获取方法返回值。

4.3 案例

​ UseUnity.cs

using UnityEngine;

public class UseUnity : MonoBehaviour {

    private void Start() {
        AndroidJavaObject javaObject = new AndroidJavaObject("com.zhyan8.test.JavaTest");
        javaObject.Call("start");
    }

    public void Fun1() {
        Debug.Log("UseUnity-Fun1"); // 打印: UseUnity-Fun1
    }

    private void Fun2(string value) {
        Debug.Log("UseUnity-Fun2, value=" + value); // 打印: UseUnity-Fun2, value=xyz
    }
}

​ 说明:UseUnity 脚本组件挂在 TestObj 对象上。

​ JavaTest.java

package com.zhyan8.test;

import android.util.Log;

import com.unity3d.player.UnityPlayer;

public class JavaTest {

    public void start() {
        fun1();
        fun2();
    }

    private void fun1() {
        Log.d("JavaTest", "fun1"); // 打印: JavaTest: fun1
        UnityPlayer.UnitySendMessage("TestObj", "Fun1", "");
    }

    private void fun2() {
        Log.d("JavaTest", "fun2"); // 打印: JavaTest: fun2
        UnityPlayer.UnitySendMessage("TestObj", "Fun2", "xyz");
    }
}

​ 打印日志如下:

img

​ 声明:本文转自
【Unity3D】Unity与Android交互

初探富文本之文档diff算法

当我们实现在线文档的系统时,通常需要考虑到文档的版本控制与审核能力,并且这是这是整个文档管理流程中的重要环节,那么在这个环节中通常就需要文档的
diff
能力,这样我们就可以知道文档的变更情况,例如文档草稿与线上文档的差异、私有化版本
A
与版本
B
之间的差异等等,本文就以
Quill
富文本编辑器引擎为基础,探讨文档
diff
算法的实现。

描述

Quill
是一个现代富文本编辑器,具备良好的兼容性及强大的可扩展性,还提供了部分开箱即用的功能。
Quill
是在
2012
年开源的,
Quill
的出现给富文本编辑器带了很多新的东西,也是目前开源编辑器里面受众非常大的一款编辑器,至今为止的生态已经非常的丰富,可以在
GitHub
等找到大量的示例,包括比较完善的协同实现。

我们首先可以思考一个问题,如果我们描述一段普通文本的话,那么大概直接输入就可以了,比如这篇文章本身底层数据结构就是纯文本,而内容格式实际上是由编译器通过词法和语法编译出来的,可以将其理解为序列化和反序列化,而对于富文本编辑器来说,如果在编辑的时候如果高频地进行序列话和反序列化,那么性能消耗是不能接受的,所以数据结构就需要尽可能是易于读写的例如
JSON
对象,那么用
JSON
来描述富文本的方式也可以多种多样,但归根结底就是需要在部分文字上挂载额外的属性,例如
A
加粗
B
斜体的话,就是在
A
上挂载
bold
属性,在
B
上挂载
italic
属性,这样的数据结构就可以描述出富文本的内容。

对于我们今天要聊的
Quill
来说,其数据结构描述是
quill-delta
,这个数据结构的设计非常棒,并且
quill-delta
同样也可以是富文本
OT
协同算法的实现,不过我们在这里不涉及协同的内容,而我们实际上要关注的
diff
能力更多的是数据结构层面的内容,也就是说我们
diff
的实际上是数据,那么在
quill-delta
中这样
一段文本

数据结构
如下所示。当然
quill-delta
的表达可以非常丰富,通过
retain

insert

delete
操作可以完成对于整个文档的内容描述增删改的能力,我们在后边实现对比视图功能的时候会涉及这部分
Op

{
  ops: [
    { insert: "那么在" },
    { insert: "quill-delta", attributes: { inlineCode: true } },
    { insert: "中这样" },
    { insert: "一段文本", attributes: { italic: true } },
    { insert: "的" },
    { insert: "数据结构", attributes: { bold: true } },
    { insert: "如下所示。\\n" },
  ],
};

看到这个数据结构我们也许会想这不就是一个普通的
JSON
嘛,那么我们直接进行
JSON

diff
是不是就可以了,毕竟现在有很多现成的
JSON
算法可以用,这个方法对于纯
insert
的文本内容理论上可行的只是粒度不太够,没有办法精确到具体某个字的修改,也就是说依照
quill-delta
的设计想从
A
依照
diff
的结果构造
delta
进行
compose
生成到
B
这件事并不那么轻松,是需要再进行一次转换的。例如下面的
JSON
,我们
diff
的结果是删除了
insert: 1
,添加了
"insert": "12", "attributes": { "bold": true }
,而我们实际上作出的变更是对
1
的样式添加了
bold
,并且添加了
2
附带
bold
,那么想要将这个
diff
结果应用到
A
上生成
B
需要做两件事,一是更精细化的内容修改,二是将
diff
的结果转换为
delta
,所以我们需要设计更好的
diff
算法,尽可能减少整个过程的复杂度。

// A
[
  { "insert": "1" }
]

// B
[
  {
    "insert": "12",
    "attributes": { "bold": true } 
  }
]

diff-delta

在这里我们的目标是希望实现更细粒度的
diff
,并且可以直接构造
delta
并且应用,也就是
A.apply(diff(a, b)) = B
,实际上在
quill-delta
中是存在已经实现好的
diff
算法,在这里我们只是将其精简了一些非
insert
的操作以便于理解,需要注意的是在这里我们讨论的是非协同模式下的
diff
,如果是已经实现
OT
的文档编辑器可以直接从历史记录中取出相关的版本
Op
进行
compose + invert
即可,并不是必须要进行文档全文的
diff
算法。

完整
DEMO
可以直接在
https://codesandbox.io/p/devbox/z9l5sl
中打开控制台查看,在前边我们提到了使用
JSON
进行
diff
后续还需要两步处理数据,特别是对于粒度的处理看起来更加费劲,那么针对粒度这个问题上不如我们换个角度思考,我们现在的是要处理富文本,而富文本就是带属性的文本,那么我们是不是就可以采用
diff
文本的算法,然后针对属性值进行额外的处理即可,这样就可以将粒度处理得很细,理论上这种方式看起来是可行的,我们可以继续沿着这个思路继续探索下去。

首先是纯文本的
diff
算法,那么我们可以先简单了解下
diff-match-patch
使用的的
diff
算法,该算法通常被认为是最好的通用
diff
算法,是由
Eugene W. Myers
设计的
https://neil.fraser.name/writing/diff/myers.pdf
,其算法本身在本文就不展开了。由于
diff-match-patch
本身还存在
match

patch
能力,而我们将要用到的算法实际上只需要
diff
的能力,那么我们只需要使用
fast-diff
就可以了,其将匹配和补丁以及所有额外的差异选项都移除,只留下最基本的
diff
能力,其
diff
的结果是一个二维数组
[FLAG, CONTENT][]

// diff.INSERT === 1;
// diff.EQUAL === 0;
// diff.DELETE === -1;
const origin = "Hello World";
const target = "Hello Diff";
console.log(fastDiff(origin, target)); // [[0, "Hello "], [-1, "World"], [1, "Diff"]]

那么我们接下来就需要构造字符串了,
quill-delta
的数据格式在上边以及提到过了,那么构造起来也很简单了,并且我们需要先构造一个
Delta
对象来承载我们对于
delta

diff
结果。

export const diffOps = (ops1: Op[], ops2: Op[]) => {
  const group = [ops1, ops2].map((delta) =>
    delta.map((op) => op.insert).join(""),
  );
  const result = diff(group[0], group[1]);

  const target = new Delta();
  const iter1 = new Iterator(ops1);
  const iter2 = new Iterator(ops2);
  // ...
}

这其中的
Iterator
是我们接下来要进行迭代取块结构的迭代器,我们可以试想一下,因为我们
diff
的结果是
N
个字的内容,而我们的
Delta

insert
块也是
N
个字,在
diff
之后就需要对这两个字符串的子字符串进行处理,所以我们需要对整个
Delta

N
个字的子字符串迭代处理,这部分数据处理方法我们就封装在
Iterator
对象当中,我们需要先来整体看一下整代器的处理方法。

export class Iterator {
  // 存储`delta`中所有`ops`
  ops: Op[];
  // 当前要处理的`ops index`
  index: number;
  // 当前`insert`字符串偏移量
  offset: number;

  constructor(ops: Op[]) {
    this.ops = ops;
    this.index = 0;
    this.offset = 0;
  }

  hasNext(): boolean {
    // 通过剩余可处理长度来判断是否可以继续处理
    return this.peekLength() < Infinity;
  }

  next(length?: number): Op {
    // ...
  }

  peek(): Op {
    // 取的当前要处理的`op`
    return this.ops[this.index];
  }

  peekLength(): number {
    if (this.ops[this.index]) {
      // 返回当前`op`剩余可以迭代的`insert`长度
      // 这里如果我们的索引管理正确 则永远不应该返回`0`
      return Op.length(this.ops[this.index]) - this.offset;
    } else {
      // 返回最大值
      return Infinity;
    }
  }
}

这其中
next
方法的处理方式要复杂一些,在
next
方法中我们的目标主要就是取
insert
的部分内容,注意我们每次调用
insert
是不会跨
op
的,也就是说每次
next
最多取当前
index

op
所存储的
insert
长度,因为如果取的内容超过了单个
op
的长度,其
attributes
的对应属性是不一致的,所以不能直接合并,那么此时我们就需要考虑到如果
diff
的结果比
insert
长的情况,也就是是需要将
attributes
这部分兼容,其实就是将
diff
结果同样分块处理。

next(length?: number): Op {
  if (!length) {
    // 这里并不是不符合规则的数据要跳过迭代
    // 而是需要将当前`index`的`op insert`迭代完
    length = Infinity;
  }
  // 这里命名为`nextOp`实际指向的还是当前`index`的`op`
  const nextOp = this.ops[this.index];
  if (nextOp) {
    // 暂存当前要处理的`insert`偏移量
    const offset = this.offset;
    // 我们是纯文档表达的`InsertOp` 所以就是`insert`字符串的长度
    const opLength = Op.length(nextOp);
    // 这里表示将要取`next`的长度要比当前`insert`剩余的长度要长
    if (length >= opLength - offset) {
      // 处理剩余所有的`insert`的长度
      length = opLength - offset;
      // 此时需要迭代到下一个`op`
      this.index += 1;
      // 重置`insert`索引偏移量
      this.offset = 0;
    } else {
      // 处理传入的`length`长度的`insert`
      this.offset += length;
    }
    // 这里是当前`op`携带的属性
    const retOp: Op = {};
    if (nextOp.attributes) {
      // 如果存在的话 需要将其一并放置于`retOp`中
      retOp.attributes = nextOp.attributes;
    }
    // 通过之前暂存的`offset`以及计算的`length`截取`insert`字符串并构造`retOp`
    retOp.insert = (nextOp.insert as string).substr(offset, length);
    // 返回`retOp`
    return retOp;
  } else {
    // 如果`index`已经超出了`ops`的长度则返回空`insert`
    return { insert: "" };
  }
}

当前我们已经可以通过
Iterator
更细粒度地截取
op

insert
部分,接下来我们就回到我们对于
diff
的处理上,首先我们先来看看
attributes

diff
,简单来看我们假设目前的数据结构就是
Record<string, string>
,这样的话我们可以直接比较两个
attributes
即可,
diff
的本质上是
a
经过一定计算使其可以变成
b
,这部分的计算就是
diff
的结果即
a + diff = b
,所以我们可以直接将全量的
key
迭代一下,如果两个
attrs
的值不相同则通过判断
b
的值来赋给目标
attrs
即可。

export const diffAttributes = (
  a: AttributeMap = {},
  b: AttributeMap = {},
): AttributeMap | undefined => {
  if (typeof a !== "object") a = {};
  if (typeof b !== "object") b = {};
  const attributes = Object.keys(a)
    .concat(Object.keys(b))
    .reduce<AttributeMap>((attrs, key) => {
      if (a[key] !== b[key]) {
        attrs[key] = b[key] === undefined ? "" : b[key];
      }
      return attrs;
    }, {});
  return Object.keys(attributes).length > 0 ? attributes : undefined;
};

因为前边我们实际上已经拆的比较细了,所以最后的环节并不会很复杂,我们的目标是构造
a + diff = b

diff
的部分,所以在构造
diff
的过程中要应用的目标是
a
,我们需要带着这个目的去看整个流程,否则容易不理解对于
delta
的操作。在
diff
的整体流程中我们主要有三部分需要处理,分别是
iter1

iter2

text diff
,而我们需要根据
diff
出的类型分别处理,整体遵循的原则就是取其中较小长度作为块处理,在
diff.INSERT
的部分是从
iter2

insert
置入
delta
,在
diff.DELETE
部分是从
iter1

delete
的长度应用到
delta
,在
diff.EQUAL
的部分我们需要从
iter1

iter2
分别取得
op
来处理
attributes

diff

op
兜底替换。

// `diff`的结果 使用`delta`描述
const target = new Delta();
const iter1 = new Iterator(ops1);
const iter2 = new Iterator(ops2);

// 迭代`diff`结果
result.forEach((item) => {
  let op1: Op;
  let op2: Op;
  // 取出当前`diff`块的类型和内容
  const [type, content] = item;
  // 当前`diff`块长度
  let length = content.length; 
  while (length > 0) {
    // 本次循环将要处理的长度
    let opLength = 0; 
    switch (type) {
      // 标识为插入的内容
      case diff.INSERT: 
        // 取 `iter2`当前`op`剩下可以处理的长度 `diff`块还未处理的长度 中的较小值
        opLength = Math.min(iter2.peekLength(), length); 
        // 取出`opLength`长度的`op`并置入目标`delta` `iter2`移动`offset/index`指针
        target.push(iter2.next(opLength));
        break;
      // 标识为删除的内容
      case diff.DELETE: 
        // 取 `diff`块还未处理的长度 `iter1`当前`op`剩下可以处理的长度 中的较小值
        opLength = Math.min(length, iter1.peekLength());
        // `iter1`移动`offset/index`指针
        iter1.next(opLength);
        // 目标`delta`需要得到要删除的长度
        target.delete(opLength);
        break;
      // 标识为相同的内容
      case diff.EQUAL:
        // 取 `diff`块还未处理的长度 `iter1`当前`op`剩下可以处理的长度 `iter2`当前`op`剩下可以处理的长度 中的较小值
        opLength = Math.min(iter1.peekLength(), iter2.peekLength(), length);
        // 取出`opLength`长度的`op1` `iter1`移动`offset/index`指针
        op1 = iter1.next(opLength);
        // 取出`opLength`长度的`op2` `iter2`移动`offset/index`指针
        op2 = iter2.next(opLength);
        // 如果两个`op`的`insert`相同
        if (op1.insert === op2.insert) {
          // 直接将`opLength`长度的`attributes diff`置入
          target.retain(
            opLength,
            diffAttributes(op1.attributes, op2.attributes),
          );
        } else {
          // 直接将`op2`置入目标`delta`并删除`op1` 兜底策略
          target.push(op2).delete(opLength);
        }
        break;
      default:
        break;
    }
    // 当前`diff`块剩余长度 = 当前`diff`块长度 - 本次循环处理的长度
    length = length - opLength; 
  }
});
// 去掉尾部的空`retain`
return target.chop();

在这里我们可以举个例子来看一下
diff
的效果,具体效果可以从
https://codesandbox.io/p/devbox/z9l5sl

src/index.ts
中打开控制台看到效果,主要是演示了对于
DELETE EQUAL INSERT
的三种
diff
类型以及生成的
delta
结果,在此处是
ops1 + result = ops2

const ops1: Op[] = [{ insert: "1234567890\n" }];
const ops2: Op[] = [
  { attributes: { bold: "true" }, insert: "45678" },
  { insert: "90123\n" },
];
const result = diffOps(ops1, ops2);
console.log(result);

// 1234567890 4567890123
// DELETE:-1 EQUAL:0 INSERT:1
// [[-1,"123"], [0,"4567890"], [1,"123"], [0,"\n"]]
// [
//   { delete: 3 }, // DELETE 123 
//   { retain: 5, attributes: { bold: "true" } }, // BOLD 45678
//   { retain: 2 }, // RETAIN 90 
//   { insert: "123" } // INSERT 123 
// ];

对比视图

现在我们的文档
diff
算法已经有了,接下来我们就需要切入正题,思考如何将其应用到具体的文档上。我们可以先从简单的方式开始,试想一下我们现在是对文档
A

B
进行了
diff
得到了
patch
,那么我们就可以直接对
diff
进行修改,构造成我们想要的结构,然后将其应用到
A
中就可以得到对比视图了,当然我们也可以
A
视图中应用删除内容,
B
视图中应用增加内容,这个方式我们在后边会继续聊到。目前我们是想在
A
中直接得到对比视图,其实对比视图无非就是红色高亮表示删除,绿色高亮表示新增,而富文本本身可以直接携带格式,那么我们就可以直接借助于富文本能力来实现高亮功能。

依照这个思路实现的核心算法非常简单,在这里我们先不处理对于格式的修改,通过将
DELETE
的内容换成
RETAIN
并且附带红色的
attributes
,在
INSERT
的类型上加入绿色的
attributes
,并且将修改后的这部分
patch
组装到
A

delta
上,然后将整个
delta
应用到新的对比视图当中就可以了,完整
DEMO
可以参考
https://codepen.io/percipient24/pen/eEBOjG

const findDiff = () => {
  const oldContent = quillLeft.getContents();
  const newContent = quillRight.getContents();
  const diff = oldContent.diff(newContent);
  for (let i = 0; i < diff.ops.length; i++) {
    const op = diff.ops[i];
    if (op.insert) {
      op.attributes = { background: "#cce8cc", color: "#003700" };
    }
    if (op.delete) {
      op.retain = op.delete;
      delete op.delete;
      op.attributes = { background: "#e8cccc", color: "#370000",  };
    }
  }
  const adjusted = oldContent.compose(diff);
  quillDiff.setContents(adjusted);
}

可以看到这里的核心代码就这么几行,通过简单的解决方案实现复杂的需求当然是极好的,在场景不复杂的情况下可以实现同一文档区域内对比,或者同样也可以使用两个视图分别应用删除和新增的
delta
。那么问题来了,如果场景复杂起来,需要我们在右侧表示新增的视图中可以实时编辑并且展示
diff
结果的时候,这样的话将
diff-delta
直接应用到文档可能会增加一些问题,除了不断应用
delta
到富文本可能造成的性能问题,在有协同的场景下还需要处理本地的
Ops
以及
History
,非协同的场景下就需要过滤相关的
key
避免
diff
结果落库。

如果说上述的场景只是在基本功能上提出的进阶能力,那么在搜索/查找的场景下,直接将高亮应用到富文本内容上似乎并不是一个可行的选择,试想一下如果我们直接将在数据层面上搜索出的内容应用到富文本上来实现高亮,我们就需要承受上边提到的所有问题,频繁地更改内容造成的性能损耗也是我们不能接受的。在
slate
中存在
decorate
的概念,可以通过构造
Range
来消费
attributes
但不会改变文档内容,这就很符合我们的需求。所以我们同样需要一种能够在不修改富文本内容的情况下高亮部分内容,但是我们又不容易像
slate
一样在编辑器底层渲染时实现这个能力,那么其实我们可以换个思路,我们直接在相关位置上加入一个半透明的高亮蒙层就可以了,这样看起来就简单很多了,在这里我们将之称为虚拟图层。

理论上实现虚拟图层很简单无非是加一层
DOM
而已,但是这其中有很多细节需要考虑。首先我们考虑一个问题,如果我们将蒙层放在富文本正上方,也就是
z-index
是高于富文本层级的话,如果此时我们点击蒙层,富文本会直接失去焦点,固然我们可以使用
event.preventDefault
来阻止焦点转移的默认行为,但是其他的行为例如点击事件等等同样会造成类似的问题,例如此时富文本中某个按钮的点击行为是用户自定义的,我们遮挡住按钮之后点击事件会被应用到我们的蒙层上,而蒙层并不会是嵌套在按钮之中的不会触发冒泡的行为,所以此时按钮的点击事件是不会触发的,这样并不符合我们的预期。那么我们转变一个思路,如果我们将
z-index
调整到低于富文本层级的话,事件的问题是可以解决的,但是又造成了新的问题,如果此时富文本的内容本身是带有背景色的,此时我们再加入蒙层,那么我们蒙层的颜色是会被原本的背景色遮挡的,而因为我们的富文本能力通常是插件化的,我们不能控制用户实现的背景色插件
必须要带一个透明度,我们的蒙层也需要是一个通用的能力,所以这个方案也有局限性。其实解决这个问题的方法很简单,在
CSS
中有一个名为
pointer-events
的属性,当将其值设置为
none
时元素永远不会成为鼠标事件的目标,这样我们就可以解决方案一造成的问题,由此实现比较我们最基本的虚拟图层样式与事件处理,此外使用这个属性会有一个比较有意思的现象,右击蒙层在控制台中是无法直接检查到节点的,必须通过
Elements
面板才能选中
DOM
节点而不能反选。

<div style="pointer-events: none;"></div>
<!-- 
  无法直接`inspect`相关元素 可以直接使用`DOM`操作来查找调试
  [...document.querySelectorAll("*")].filter(node => node.style.pointerEvents === "none"); 
-->

在确定绘制蒙层图形的方法之后,紧接着我们就需要确认绘制图形的位置信息。因为我们的富文本绘制的
DOM
节点并不是每个字符都带有独立的节点,而是有相同
attributes

ops
节点是相同的
DOM
节点,那么此时问题又来了,我们的
diff
结果大概率不是某个
DOM
的完整节点,而是其中的某几个字,此时想获取这几个字的位置信息是不能直接用
Element.getBoundingClientRect
拿到了,我们需要借助
document.createRange
来构造
range
,在这里需要注意的是我们处理的是
Text
节点,只有
Text
等节点可以设置偏移量,并且
start

end

node
可以直接构造选区,并不需要保持一致。当然
Quill
中通过
editor.getBounds
提供了位置信息的获取,我们可以直接使用其获取位置信息即可,其本质上也是通过
editor.scroll
获取实际
DOM
并封装了
document.createRange
实现,以及处理了各种边缘
case

const el = document.querySelector("xxx");
const textNode = el.firstChild;

const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, 2);

const rect = range.getBoundingClientRect();
console.log(rect);

接下来我们还需要探讨一个问题,
diff
的时候我们不能够确定当前的结果的长度,在之前已经明确我们是对纯文本实现的
diff
,那么
diff
的结果可能会很长,那么这个很长就有可能出现问题。我们直接通过
editor.getBounds(index, length)
得到的是
rect

rectangle
,这个
Range
覆盖的范围是矩形,当我们的
diff
结果只有几个字的时候,直接获取
rect
是没问题的,而如果我们的
diff
结果比较长的时候,就会出现两个获取位置时需要关注的问题:一个是单行内容过长,在编辑器中一行是无法完整显示,由此出现了折行的情况;另一个是内容本身就是跨行的,也就是说
diff
结果是含有
\n
时的情况。

|  这里只有一行内容内容内容内容内容 |
|内容内容内容内容内容内容内容内容内 |
|内容内容内容内容。              |

|  这里有多行内容内容内容。       |
|  这里有多行内容内容内容内容。    |
|  这里有多行内容内容内容内容内容。 |

在这里假设上边的内容就是
diff
出的结果,至于究竟是
INSERT/DELETE/RETAIN
的类型我们暂时不作关注,我们当前的目标是实现高亮,那么在这两种情况下,如果直接通过
getBounds
获取的
rect
矩形范围作高亮的话,很明显是会有大量的非文本内容即空白区域被高亮的,在这里我们的表现会是会取的最大范围的高亮覆盖,实际上如果只是空白区域覆盖我们还是可以接受的,但是试想一个情况,如果我们只是其中部分内容做了更改,例如第
N
行是完整的插入内容,在
N+1
行的行首同样插入了一个字,此时由于我们
N+1
行的
width
被第
N
行影响,导致我们的高亮覆盖了整个行,此时我们的
diff
高亮结果是不准确的,无论是折行还是跨行的情况下都存在这样的情况,这样的表现就是不能接受的了。

那么接下来我们就需要解决这两个问题,对于跨行位置计算的问题,在这里可以采取较为简单的思路,我们只需要明确地知道究竟在哪里出现了行的分割,在此处需要将
diff
的结果进行分割,也就是我们处理的粒度从文档级别变化到了行级别。只不过在
Quill
中并没有直接提供基于行
Range
级别的操作,所以我们需要自行维护行级别的
index-length
,在这里我们简单地通过
delta insert
来全量分割
index-length
,在这里同样也可以
editor.scroll.lines
来计算,当文档内容改变时我们同样也可以基于
delta-changes
维护索引值。此外如果我们的管理方式是通过多
Quill
实例来实现
Blocks
的话,这样就是天然的
Line
级别管理,维护索引的能力实现起来会简单很多,只不过
diff
的时候就需要一个
Block
树级别的
diff
实现,如果是同
id

Block
进行
diff
还好,但是如果有跨
Block
进行
diff
的需求实现可能会更加复杂。

const buildLines = (content) => {
  const text = content.ops.map((op) => op.insert || "").join("");
  let index = 0;
  const lines = text.split("\n").map((str) => {
    // 需要注意我们的`length`是包含了`\n`的
    const length = str.length + 1;
    const line = { start: index, length };
    index = index + length;
    return line;
  });
  return lines;
}

当我们有行的
index-length
索引分割之后,接下来就是将原来的完整
diff-index-length
分割成
Line
级别的内容,在这里需要注意的是行标识节点也就是
\n

attributes
需要特殊处理,因为这个节点的所有修改都是直接应用到整个行上的,例如当某行从二级标题变成一级标题时就需要将整个行都高亮标记为样式变更,当然本身标题可能也会存在内容增删,这部分高亮是可以叠加不同颜色显示的,这也是我们需要维护行粒度
Range
的原因之一。

return (index, length, ignoreLineMarker = true) => {
  const ranges = [];
  // 跟踪
  let traceLength = length;
  // 可以用二分搜索查找索引首尾 `body`则直接取`lines` 查找结果则需要增加`line`标识
  for (const line of lines) {
    // 当前处理的节点只有`\n`的情况 标识为行尾并且有独立的`attributes`
    if (length === 1 && index + length === line.start + line.length) {
      // 如果忽略行标识则直接结束查找
      if (ignoreLineMarker) break;
      // 需要构造整个行内容的`range`
      const payload = { index: line.start, length: line.length - 1 };
      !ignoreLineMarker && payload.length > 0 && ranges.push(payload);
      break;
    }
    // 迭代行 通过行索引构造`range`
    // 判断当前是否还存在需要分割的内容 需要保证剩余`range`在`line`的范围内
    if (
      index < line.start + line.length &&
      line.start <= index + traceLength
    ) {
      const nextIndex = Math.max(line.start, index);
      // 需要比较 追踪长度/行长度/剩余行长度
      const nextLength = Math.min(
        traceLength,
        line.length - 1,
        line.start + line.length - nextIndex
      );
      traceLength = traceLength - nextLength;
      // 构造行内`range`
      const payload = { index: nextIndex, length: nextLength };
      if (nextIndex + nextLength === line.start + line.length) {
        // 需要排除边界恰好为`\n`的情况
        payload.length--;
      }
      payload.length > 0 && ranges.push(payload);
    } else if (line.start > index + length || traceLength <= 0) {
      // 当前行已经超出范围或者追踪长度已经为`0` 则直接结束查找
      break;
    }
  }
  return ranges;
};

那么紧接着我们需要解决下一个问题,对于单行内容较长引起折行的问题,因为在上边我们已经将
diff
结果按行粒度划分好了,所以我们可以主要关注于如何渲染高亮的问题上。在前边我们提到过了,我们不能直接将调用
getBounds
得到的
rect
直接绘制到文本上,那么我们仔细思考一下,一段文本实际上是不是可以拆为三段,即首行
head
、内容
body
、尾行
tail
,也就是说只有行首与行尾才会出现部分高亮的墙狂,这里就需要单独计算
rect
,而
body
部分必然是完整的
rect
,直接将其渲染到相关位置就可以了。那么依照这个理论我们就可以用三个
rect
来表示单行内容的高亮就足够了,而实际上
getBounds
返回的数据是足够支撑我们分三段处理单行内容的,我们只需要取得首
head

tail

rect

body
部分的
rect
可以直接根据这两个
rect
计算出来,我们还是需要根据实际的折行数量分别讨论的,如果是只有单行的情况,那么只需要
head
就足够了,如果是两行的情况那么就需要借助
head

tail
来渲染了,
body
在这里起到了占位的作用,如果是多行的时候,那么就需要
head

body

tail
渲染各自的内容,来保证图层的完整性。

// 获取边界位置
const startRect = editor.getBounds(range.index, 0);
const endRect = editor.getBounds(range.index + range.length, 0);
// 单行的块容器
const block = document.createElement("div");
block.style.position = "absolute";
block.style.width = "100%";
block.style.height = "0";
block.style.top = startRect.top + "px";
block.style.pointerEvents = "none";
const head = document.createElement("div");
const body = document.createElement("div");
const tail = document.createElement("div");
// 依据不同情况渲染
if (startRect.top === endRect.top) {
  // 单行(非折行)的情况 `head`
  head.style.marginLeft = startRect.left + "px";
  head.style.height = startRect.height + "px";
  head.style.width = endRect.right - startRect.left + "px";
  head.style.backgroundColor = color;
} else if (endRect.top - startRect.bottom < startRect.height) {
  // 两行(折单次)的情况 `head + tail` `body`占位
  head.style.marginLeft = startRect.left + "px";
  head.style.height = startRect.height + "px";
  head.style.width = startRect.width - startRect.left + "px";
  head.style.backgroundColor = color;
  body.style.height = endRect.top - startRect.bottom + "px";
  tail.style.width = endRect.right + "px";
  tail.style.height = endRect.height + "px";
  tail.style.backgroundColor = color;
} else {
  // 多行(折多次)的情况 `head + body + tail`
  head.style.marginLeft = startRect.left + "px";
  head.style.height = startRect.height + "px";
  head.style.width = startRect.width - startRect.left + "px";
  head.style.backgroundColor = color;
  body.style.width = "100%";
  body.style.height = endRect.top - startRect.bottom + "px";
  body.style.backgroundColor = color;
  tail.style.marginLeft = 0;
  tail.style.height = endRect.height + "px";
  tail.style.width = endRect.right + "px";
  tail.style.backgroundColor = color;
}

解决了上述两个问题之后,我们就可以将
delta
应用到
diff
算法获取结果,并且将其按行划分构造出新的
Range
,在这里我们想要实现的是左视图体现
DELETE
内容,右视图体现
INSERT + RETAIN
的内容,在这里我们只需要根据
diff
的不同类型,分别将构造出的
Range
存储到不同的数组中,最后在根据
Range
借助
editor.getBounds
获取位置信息,构造新的图层
DOM
在相关位置实现高亮即可。

const diffDelta = () => {
  const prevContent = prev.getContents();
  const nextContent = next.getContents();
  // ...
  // 构造基本数据
  const toPrevRanges = buildLines(prevContent);
  const toNextRanges = buildLines(nextContent);
  const diff = prevContent.diff(nextContent);
  const inserts = [];
  const retains = [];
  const deletes = [];
  let prevIndex = 0;
  let nextIndex = 0;
  // 迭代`diff`结果并进行转换
  for (const op of diff.ops) {
    if (op.delete !== undefined) {
      // `DELETE`的内容需要置于左视图 红色高亮
      deletes.push(...toPrevRanges(prevIndex, op.delete));
      prevIndex = prevIndex + op.delete;
    } else if (op.retain !== undefined) {
      if (op.attributes) {
        // `RETAIN`的内容需要置于右视图 紫色高亮
        retains.push(...toNextRanges(nextIndex, op.retain, false));
      }
      prevIndex = prevIndex + op.retain;
      nextIndex = nextIndex + op.retain;
    } else if (op.insert !== undefined) {
      // `INSERT`的内容需要置于右视图 绿色高亮
      inserts.push(...toNextRanges(nextIndex, op.insert.length));
      nextIndex = nextIndex + op.insert.length;
    }
  }
  // 根据转换的结果渲染`DOM`
  buildLayerDOM(prev, deleteRangeDOM, deletes, "rgba(245, 63, 63, 0.3)");
  buildLayerDOM(next, insertRangeDOM, inserts, "rgba(0, 180, 42, 0.3)");
  buildLayerDOM(next, retainRangeDOM, retains, "rgba(114, 46, 209, 0.3)");
};
// `diff`渲染时机
prev.on("text-change", _.debounce(diffDelta, 300));
next.on("text-change", _.debounce(diffDelta, 300));
window.onload = diffDelta;

总结一下整体的流程,实现基于虚拟图层的
diff
我们需要
diff
算法、构造
Range
、计算
Rect
、渲染
DOM
,实际上想要做好整个能力还是比较复杂的,特别是有很多边界
case
需要处理,例如某些文字应用了不同字体或者一些样式,导致渲染高度跟普通文本不一样,而
diff
的边缘又恰好落在了此处就可能会造成我们的
rect
计算出现问题,从而导致渲染图层节点的样式出现问题。在这里我们还是没有处理类似的问题,只是将整个流程打通,没有特别关注于边缘
case
,完整的
DEMO
可以直接访问
https://codesandbox.io/p/sandbox/quill-diff-view-369jt6
查看。

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://quilljs.com/docs/api/
https://zhuanlan.zhihu.com/p/370480813
https://www.npmjs.com/package/quill-delta
https://github.com/quilljs/quill/issues/1125
https://developer.mozilla.org/zh-CN/docs/Web/API/Range
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createRange

通讯测试是用于评估网络性能、稳定性和安全性的过程
。它的主要作用包括:

  1. 连通性测试

  2. 故障排查

  3. 安全评估

  4. 取证
    (没错就是取证)

  • 本文分享一下我常用的测试工具,部分来自群友的推荐

  • 本文介绍的软件均可在windows系统上运行、
    功能单一且轻量

1. ping

  • 作为最常用的命令,
    用来测量两台设备是否可达和往返时间
    (RTT)

  • 它使用
    ICMP
    协议(
    OSI模型的第3层
    即网络层)

  • windows上的
    ping
    默认通讯4次,你可以在结尾增加
    -t
    来持续
    ping

使用:
ping baidu.com -t

返回:

2. tracert

  • 同样使用ICMP协议,它可以用于
    跟踪数据包从本地主机到目标主机之间的路由路径
    ,以及每一跳的延迟

  • 你也可以使用
    -d
    参数来禁用域名解析,只显示ip地址

使用:
tracert -d baidu.com

返回:

3. telnet

  • 用来
    检测应用和服务(端口)的可用性
    ,它是基于TCP/IP协议的应用层协议(OSI模型的第7层)

  • 如检测西门子plc s7协议102端口,或者modbus-tcp协议502端口的可用性

  • 你可以在windows系统->控制面板->类别->程序->启用或关闭windows功能中,勾选
    安装
    telnet客户端

使用:
telnet 172.20.10.7 502

返回:

4. netstat

  • 用来显示当前系统的所有网络连接信息
    ,包括本地地址、外部地址、连接状态、协议、数据包数量等

  • 例如:查看某个端口当前有几路连接;端口被哪个进程占用等

  • 下面来查询本机502端口的连接情况

使用:
netstat -nao | findstr 502

返回:
分别是协议、本机地址、远程地址、状态、进程编号

5. psping

  • PsPing 不仅可以测试 ICMP 协议,还可以测试 TCP 和 UDP 端口的连通性和延迟,更
    全面地评估网络性能

  • 类似于
    ping+telnet

  • 详见

    https://learn.microsoft.com/zh-cn/sysinternals/downloads/psping

  • 解压后放在
    C:\Windows\System32
    下;或者
    添加环境变量

使用:
psping baidu.com:80

返回:

6. arp

  • 用来显示与当前计算机有过通讯的设备的IP和MAC

  • 它位于OSI模型的第2层(
    数据链路层

  • 如果你遇到过莫名其妙的通讯问题,有可能是IP冲突了,你可以试一试arp来检查MAC地址是否匹配

  • 可以使用
    arp -d
    来清除arp表,也可以使用
    arp -s
    手动添加

使用:
arp -a

返回:
按照接口显示IP和MAC

7. IPScanner

  • 用来
    搜索某个网段内的网络设备名、IP和MAC地址

  • 为后续端口扫描做准备

8. PortScan

  • 用来探索设备暴露的端口
    ,也可以进行
    ping统计和测速

  • 如果你需要采集未知设备的数据,那端口扫描工具是必要的

  • 支持批量扫描
    多个网络设备的多个端口

9. inSSIDer

  • 用来查看当前和诊断WiFi的状态
    ,可以
    查看每个SSID的信道、信号强度和模式

  • 如果你的
    家庭无线网络或者工厂的AP丢包、延迟
    ,可以试一试它

  • 至少你可以
    选择一个不太拥挤的信道

10. TCP&UDP测试工具

来自于
周立功
,并没有多好用,但
它足够小

支持tcp客户端和服务端、udp

可以
计数和自动发送和保存

11. WireShark

万能抓包工具
,没人能离开它,现在开始也不晚

你可以用来抓包取证、数据包分析等

建议你
使用过滤器抓包
,否则数据量可能会很大

12. Fiddler 4

  • 对http协议的交互进行抓包

  • 有了它,你
    不再需要打开浏览器的F12
    进行调试

  • 可以保存
    你想要的记录

13. VSPD

网络相关的介绍差不多了,下面就是
串口相关
的了

  • Virtual Serial Port Driver,就是在你的电脑上
    模拟一对直连的串口

  • 方便测试和调试

14. 串口测试工具

  • commix
    ,它足够小

  • 支持
    modbus rtu crc16

15. 串口抓包

  • serial-monitor

  • 和网口一样,
    串口一样可以抓包监测

16. 其他

下面几个工具均来自群友推荐,可以下载测试

ctsTraffic、Nirsoft、QtSwissArmyKnife、COMMBox、NETworkManager

17. 下载地址

地址:
百度网盘:https://pan.baidu.com/s/1fEYB4UmydQTxyeaJhaxPsA?pwd=iotg

解压密码:
iotgateway.net

深度学习框架

  • 一小点作者内心os:24年春节已过完,从熟悉的地方又回到陌生的地方谋生,愿新的一年都得偿所愿,心想事成。

学到这会儿会发现,除非应用更复杂的模型,例如卷积神经网络,或者循环神经网络,或者当开始应用很大的模型,否则它就越来越不实用了,至少对大多数人而言,从零开始全部靠自己实现并不现实。

幸运的是,现在有很多好的深度学习软件框架,可以帮助实现这些模型。类比一下,猜知道如何做矩阵乘法,还应该知道如何编程实现两个矩阵相乘,但是当在建很大的应用时,很可能不想用自己的矩阵乘法函数,而是想要访问一个数值线性代数库,它会更高效,但如果明白两个矩阵相乘是怎么回事还是挺有用的。认为现在深度学习已经很成熟了,利用一些深度学习框架会更加实用,会使的工作更加有效,那就让来看下有哪些框架。

现在有许多深度学习框架,能让实现神经网络变得更简单,来讲主要的几个。每个框架都针对某一用户或开发群体的,觉得这里的每一个框架都是某类应用的可靠选择,有很多人写文章比较这些深度学习框架,以及这些深度学习框架发展得有多好,而且因为这些框架往往不断进化,每个月都在进步,如果想看看关于这些框架的优劣之处的讨论,留给自己去网上搜索,但认为很多框架都在很快进步,越来越好,因此就不做强烈推荐了,而是与分享推荐一下选择框架的标准。

一个重要的标准就是便于编程,这既包括神经网络的开发和迭代,还包括为产品进行配置,为了成千上百万,甚至上亿用户的实际使用,取决于想要做什么。

第二个重要的标准是运行速度,特别是训练大数据集时,一些框架能让更高效地运行和训练神经网络。

还有一个标准人们不常提到,但觉得很重要,那就是这个框架是否真的开放,要是一个框架真的开放,它不仅需要开源,而且需要良好的管理。不幸的是,在软件行业中,一些公司有开源软件的历史,但是公司保持着对软件的全权控制,当几年时间过去,人们开始使用他们的软件时,一些公司开始逐渐关闭曾经开放的资源,或将功能转移到他们专营的云服务中。因此会注意的一件事就是能否相信这个框架能长时间保持开源,而不是在一家公司的控制之下,它未来有可能出于某种原因选择停止开源,即便现在这个软件是以开源的形式发布的。但至少在短期内,取决于对语言的偏好,看更喜欢
Python

Java
还是
C++
或者其它什么,也取决于在开发的应用,是计算机视觉,还是自然语言处理或者线上广告,等等,认为这里的多个框架都是很好的选择。

程序框架就写到这里,通过提供比数值线性代数库更高程度的抽象化,这里的每一个程序框架都能让在开发深度机器学习应用时更加高效。

1.简介

上一篇主要讲解一下软件的介绍以及软件的抓包原理。

2.安装部署环境

2.1操作系统

1.宏哥的环境是Windows 10版本 64位系统(32位的同学自己想办法哦),其实宏哥觉得无论在什么平台,多少位,其实安装都是类似的,非常easy的。如下图所示:

2.2软件版本

1.Wireshark的版本,宏哥在官网下载了最新的版本 4.0.2,如下图所示:

3.WireShark下载安装

3.1WireShark下载

wireshark的官方下载网站:
https://www.wireshark.org/
s

wireshark是非常流行的网络封包分析软件,功能十分强大。可以截取各种网络封包,显示网络封包的详细信息。

wireshark是开源软件,可以放心使用。可以运行在Windows和Mac OS上。对应的,linux下的抓包工具是 tcpdump。使用wireshark的人必须了解网络协议,否则就看不懂wireshark了。

1.打开网址
https://www.wireshark.org
,进入 Wireshark 官网,如下图所示:

2.单击图中的下载图标进入下载页面,如下图所示:

在 Stable Release 部分可以看到目前 Wireshark 的最新版本是 4.0.2,并提供了 Windows( 64 位)、Mac OS 和源码包的下载地址。用户可以根据自己的操作系统下载相应的软件包。

这里下载的是最新版本即4.0.2版本,如果你想下载旧版本的,可以点击下载界面下的【Old Stable Release】,如下图所示:

3.这里下载 Windows 64 位的安装包。单击 Windows Installer(64-bit) 链接,进行下载。下载后的文件名为 Wireshark-win64-4.0.2.exe。如下图所示:

3.2安装

1.双击下载的软件包进行安装。安装比较简单,只要使用默认值,单击 Next 按钮,即可安装成功。

2.点击“Next”,如下图所示:

3.点击“Next”,进行协议许可认证,点击“Noted”,如下图所示:

4.安装时,默认选择所有组件,不要修改,点击“Next”,如下图所示:

5.第一个是让选择软件的快捷方式,第二个勾选是为了以支持多种其他网络包分析工具工具支持的文件格式,默认即可!点击“Next”,如下图所示:

6.选择Wireshark安装目录(
尽量使用英文路径不要出现中文
),然后点击“Next”,如下图所示:

7.Wireshark安装文件自带Npcap最新版本(安装过程中会检查本机没有Npcap或者WinPcap这个驱动,勾选安装),选择安装,默认即可!点击“Next”,如下图所示:

8.USBPcap是一个开源的USB数据包抓取工具。如果你有需要也可以勾选安装,否则保持默认即可!点击“Install”,开始安装...,如下图所示:

9.稍等一会,等待安装完毕,如下图所示:

10.安装好以后,点击“Next”,如下图所示:

11.勾选立即重启电脑或者是稍后自己手动重启电脑,下面如果选择Rebot now,电脑会重启,重启后安装结束。如果现在不方便重启,可先选择I want to manually reboot later选项。点击“Finish”,如下图所示:

12.安装好以后,在 Windows 的“开始”菜单中会出现 Wireshark 图标,如下图所示:

4.新版特性

有兴趣的小伙伴或者童鞋们可以到这个网址:
http;//wireshark.org/docs/relnotes/
自己查看。

5.小试牛刀

软件已经安装部署好了,我们要迫不及待地跃跃欲试,那就和宏哥一起尝尝鲜。

5.1初始界面

启动 Wireshark 时,会出现以下启动界面,如下图所示:

宏哥将整个界面分为六个部分:

  • 标题栏(Wireshark网络分析器)宏哥没有标出来
  • 主菜单
  • 工具栏
  • 过滤工具
  • 接口列表(网卡列表)
  • 状态工具栏

5.2快速抓包

1.选中要抓包的一个网卡,右键点击“Start capture”,如下图所示:

2.开始抓包了(标题显示:正在捕获那个网卡),如下图所示:

5.3停止抓包

点击左上角的
「红色按钮」

即可停止抓包。如下图所示:

5.4保存数据

敲黑板!!!保存数据之前一定要停止抓包,否则“保存”按钮,就是灰色的不允许保存,这个很好理解,就想其他软件Word打开不允许删除,修改文件名一样。如下图所示:

1.首先要停止抓包,然后保存数据,如下图所示:

2.点击“保存”后,我们下拉保存文件类型可以看到好多格式的,可能是其他抓包工具需要的格式,保存到相应格式,就可以用其他的抓包工具打开,这里Wireshark默认第一种格式,如下图所示:

3.保存到桌面,如下图所示:

6.小结

这一篇主要讲解和分享了WireShark的安装以及简单操作应用。安装很简单傻瓜式的下一步下一步即可,难的是后期的使用。好了,今天时间也不是很早了,宏哥今天就讲解和分享到这里,感谢您耐心的阅读