为 Terminal 配置 ShadowSocks 代理

为 Terminal 开启代理存在下述问题:

  1. Terminal 不会自动使用系统代理,需要手动配置;
  2. ShadowSocks 默认为 PAC 代理,Terminal 无法使用。

下面介绍下如何通过 ShadowSocks 来为 Terminal 提供代理

设置 ShadowSocks 为手动模式

这里使用 ShadowSocksX-NG 客户端来为系统配置代理:

该客户端拥有 PAC、Socks、HTTP 等多种代理方式。

自动模式

通过菜单栏下拉项配置代理模式:

平时一般使用 ShadowSocks 时用的是“PAC 自动模式”,

网络设置里的代理协议是自动代理配置。

手动模式

在 ShadowSocks 的偏好设置里确认 HTTP 代理地址。

  1. 通过菜单栏下拉选项配置 ShadowSocks 为手动模式;
  2. 在系统设置的网络里取消自动代理配置的设置;
  3. 在网络的代理设置里选择“网页代理(HTTP)”和 “安全网页代理(HTTPS)”,并且填入服务器地址。

为 Terminal 配置 HTTP 代理

在 Terminal 配置文件中增加如下内容, all_proxy 后填入 ShadowSocks 的 HTTP 代理地址:

# proxy list
alias proxy='export all_proxy=http://127.0.0.1:1087'
alias unproxy='unset all_proxy'

重新加载配置文件:

$ source .zshrc 或 重新打开 Terminal

开启代理

$ proxy 

关闭代理

$ unproxy

End

通过手动配置 Terminal 代理和配置 ShadowSocks 为手动模式即可成功在 Terminal 中启动代理了。

Android 读取数据不完整的问题

问题描述

低概率性加载图片不完整,如下图:

日志

 D skia    : libjpeg error 105 <  Ss=0, Se=63, Ah=0, Al=0> from Incomplete image data 

分析

搜索

从日志看到大致是使用的图片数据是不完整的。上网搜索错误日志得到类似问题 Problem decoding into existing bitmap,useing V4 #1843
在该链接最后列举了两种可能导致数据不完整的例子:

OutputStream networkStream = ...;
BufferedOutputStream out = new BufferedOutputStream(networkStream);
for (some data) out.write(some data);
networkStream.close(); // this is wrong
// the end of some data may be still in memory of the buffered stream
// you need to close the outer stream (out) first, so it flushes to the underlying stream (networkStream).
byte[] buffer = new byte[2048];
while(in.read(buffer) == 2048) { // == 2048 should be > 0
     out.write(buffer); // and write should be `out.write(buffer, 0, length of last batch of bytes)`
} // the last batch of bytes read to the buffer will be less than 2048

举一反三

查看自己相应的代码:

private byte[] getShopLogoFromSDCard(String shopId) {
    Logger.d(TAG, "sdcard");
    byte[] b = null;
    try {
        File file = new File(Constant.APP_DB_PATH + "logo/" + shopId);
        if (file != null && file.exists()) {
            FileInputStream fis = new FileInputStream(file);
            try {
                b = new byte[fis.available()];
                fis.read(b);
                fis.close();

            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        } else {
            Logger.d(TAG, "sdcard no this image");
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return null;
    }
    return b;
}

返回结果的 b 的大小是根据 fis.available 方法获取的,查看该方法的说明:

/**
 * Returns an estimate of the number of remaining bytes that can be read (or
 * skipped over) from this input stream without blocking by the next
 * invocation of a method for this input stream. Returns 0 when the file
 * position is beyond EOF. The next invocation might be the same thread
 * or another thread. A single read or skip of this many bytes will not
 * block, but may read or skip fewer bytes.
 *
 * <p> In some cases, a non-blocking read (or skip) may appear to be
 * blocked when it is merely slow, for example when reading large
 * files over slow networks.
 *
 * @return     an estimate of the number of remaining bytes that can be read
 *             (or skipped over) from this input stream without blocking.
 * @exception  IOException  if this file input stream has been closed by calling
 *             {@code close} or an I/O error occurs.
 */
public int available() throws IOException {
    if (closed) {
        throw new IOException("Stream Closed");
    }

    return available0();
}

该方法返回的是一个预估的大小,结果不保证正确。而且正好我们的问题就是概率性的,只有低概率的情况下会发生。那么就很可能是该方法导致的结果。

验证

开启两个线程,在每个线程中调用1000次该方法,在日志中打印出 fis.available 的结果。
测试结果真的就出现了 fis.available 结果不一致的情况,2个线程2000次调用中出现了两次。

11-16 11:32:29.180 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.180 18637 18649 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.202 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.203 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.227 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.227 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.245 18637 18657 D CacheManageApiImpl: fis.available: 8082
11-16 11:32:29.246 18637 18650 D CacheManageApiImpl: fis.available: 8082
11-16 11:32:29.272 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.313 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.315 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.338 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.347 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.358 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.372 18637 18654 D CacheManageApiImpl: fis.available: 8186

修正

该方法中只需要正确处理将文件内容转换成 byte[] 即可。
该功能可以参考 File to byte[] in Java
既可以使用 Apache 提供的标准代码,在 Java 1.7 之后 JDK 也提供了库函数供使用。

最终代码如下:

private byte[] getShopLogoFromSDCard(String shopId) {
    Logger.d(TAG, "sdcard");
    byte[] b = null;

    File file = new File(Constant.APP_DB_PATH + "logo/" + shopId);
    if (file.exists()) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            b = IOUtils.toByteArray(fis);
        } catch (IOException e) {
            e.printStackTrace();
            Logger.d(TAG, "exception");
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    } else {
        Logger.d(TAG, "sdcard no this image");
    }

    return b;
}
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * https://commons.apache.org/proper/commons-io/javadocs/api-2.5/src-html/org/apache/commons/io/IOUtils.html
 */

public class IOUtils {

    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
    public static final int EOF = -1;

    public static byte[] toByteArray(final InputStream input) throws IOException {
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        copy(input, output);
        return output.toByteArray();
    }

    public static int copy(final InputStream input, final OutputStream output) throws IOException {
        final long count = copyLarge(input, output);
        if (count > Integer.MAX_VALUE) {
            return -1;
        }
        return (int) count;
    }

    public static long copy(final InputStream input, final OutputStream output, final int bufferSize)
            throws IOException {
        return copyLarge(input, output, new byte[bufferSize]);
    }

    public static long copyLarge(final InputStream input, final OutputStream output)
            throws IOException {
        return copy(input, output, DEFAULT_BUFFER_SIZE);
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)
            throws IOException {
        long count = 0;
        int n;
        while (EOF != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }
}

JobScheduler 测试

JobScheduler 执行时间并不确定,其在满足预先设置的条件时会由系统来调用。

执行周期

通过 setPeriodic 方法可以设置任务执行的周期,在 Android N 及以后的版本中周期需大于15分钟,小于15分钟的按15分钟执行。

查看手机中的任务

adb shell dumpsys jobscheduler

该命令可以列出系统中的各种任务,包含:

  • Settings
  • Registered XX Jobs
  • Connectivity
  • Alarms
  • Idle
  • Battery
  • AppIdle
  • Content
  • Job history
  • Pending queue

等。其中 Registered XX Jobs 列出了系统中 JobScheduler。
例如:

 JOB #u0a7/-100: 88a17c2 com.android.providers.downloads/.DownloadIdleService
    u0a7 tag=*job*/com.android.providers.downloads/.DownloadIdleService
    Source: uid=u0a7 user=0 pkg=com.android.providers.downloads
    JobInfo:
      Service: com.android.providers.downloads/.DownloadIdleService
      PERIODIC: interval=+12h0m0s0ms flex=+12h0m0s0ms
      Requires: charging=true batteryNotLow=false deviceIdle=true
      Backoff: policy=1 initial=+30s0ms
      Has early constraint
      Has late constraint
    Required constraints: CHARGING TIMING_DELAY DEADLINE IDLE
    Satisfied constraints: BATTERY_NOT_LOW TIMING_DELAY APP_NOT_IDLE DEVICE_NOT_DOZING
    Unsatisfied constraints: CHARGING DEADLINE IDLE
    Tracking: BATTERY IDLE
    Enqueue time: -4h43m43s596ms
    Run time: earliest=-4h43m43s596ms, latest=+7h16m16s404ms
    Ready: false (job=false user=true !pending=true !active=true !backingup=true comp=true)

可以查看任务设置的条件,具体信息等。

Job history:
       -25m52s057ms START: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
       -25m36s869ms  STOP: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
       -15m51s422ms START: u0a46 DownloadManager:com.android.providers.downloads
       -15m50s704ms  STOP: u0a46 DownloadManager:com.android.providers.downloads
        -1m17s539ms START: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
        -1m15s576ms  STOP: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService

Job History 可以查看 JobScheduler 的任务调度情况。

强制执行

adb shell cmd jobscheduler run -f package_name jobId

参考链接

What tools are available to test JobScheduler?

Kotlin 笔记

基础语法

list

  1. list.indices list 的 index
  2. list.lastIndex list 的 最后一个 index
  3. list.size list 的大小
  4. for((index, value) in array.withIndex()){…}

null

  1. ?. if not null eg: println(files?.size)
  2. ?. ?: if not null and else eg: println(files?.size ?: "empty")
  3. ?: if null eg: val email = data ?: throw IllegalStateException("Email is missing!")
  4. ?.let if not null eg: data?.let{…}
  5. ?.let ?: if not null and else

编码习惯

  1. 冒号 类型和超类型之间的冒号前有一个空格,实例和类型之间的冒号前没有空格

数字类型

  1. Int Float 4*8
  2. Long Double 8*8 默认 Double
  3. Short 2*8
  4. Byte 1*8

数字装箱

数字装箱不保留同一性(=),保留相等性(

显式转换

较小类型不能隐式转换为较大的类型,如 Int 不能转换为 Long,但可以显示转换。如 toInt()

位运算

用中缀方式调用的命名函数,仅适用于 Int 和 Long

  1. shl – 有符号左移
  2. shr – 有符号右移
  3. ushr – 无符号右移
  4. and – 与
  5. or – 或
  6. xor – 异或
  7. inv – 非

字符类型

  1. Char 不能当数字,但可以显示转换 toInt()
  2. 单引号表示 'a'、'0'、'P'
  3. 反斜杠转义 \t、\b、"、\、$

布尔类型

  1. Boolean true false

数组类型

  1. 数组是不型变的,所以不可以把 Array 赋值给 Array,但可以使用 Array(类型投影)
  2. 无装箱开销的原声类型数组:ByteArray、ShortArray、IntArray 等,和 Array 没有继承关系

字符串

  1. 双引号,可以包含转义 "Hello, world!\n"
  2. 三引号,不包含转义,可以包含任何字符
  3. 双引号和三引号都支持字符串模板,要在三引号中表示 \( 可以用:""" \){'$'} """

  1. 不声明包的在无名字的默认包

标签

  1. 声明 标识符+@ eg: loop@、a@
  2. 使用 return@loop break@a

  1. 如果类有一个主构造函数,每个次构造函数都要委托给主构造函数,可以直接委托也可以通过别的次构造函数间接委托。
    class Person(val name: String) {
        constructor(name: String, parent: Person) : this(name) {
            parent.children.add(this)
        }
    }
    
  2. 同时继承一个成员的多个实现,需要指定具体类型 super<A>.a()

  3. 可以用抽象成员覆盖非抽象成员

    open class Base {
        open fun f() {}
    }
    
    abstract class Derived : Base() {
        override abstract fun f()
    }
    
  4. Kotlin 中没有静态方法

接口

  1. 接口中定义的属性,要么是抽象的,要么需要提供访问器实现,不能有初始化器(直接赋值)

扩展函数

  1. 扩展函数的调用取决于声明扩展函数时的类型,而非传入的接受者类型
  2. 扩展函数和成员函数签名相同时,取成员函数
  3. 可以为可空的接收者类型定义扩展,然后在函数体内进行检测

扩展属性

  1. 扩展属性的行为只能由 get、set 定义,不能有初始化器

密封类 sealed

  1. 密封类的子类必须在同一个文件中声明
  2. 密封类的子类的子类可以在任何位置声明
  3. 不允许有非 private 的构造函数

泛型

  1. out 型变 只可以被生产
  2. in 逆型变 只可以被消费

对象表达式 VS 对象声明

  1. 对象表达式用来创建匿名类的对象,比如注册 View 的点击事件 object : SuperClass
  2. 对象声明用于创建一个单例模式的对象,object A { fun aa() {} }

伴生对象

  1. 类内部的对象声明用 companion 标记
  2. 伴生对象的成员可以通过类名来引用,看起来类似于静态成员,运行时仍然是真实对象的实例

对象表达式和对象声明的区别

  1. 对象表达式在使用的地方立即执行
  2. 对象声明在第一次访问时延迟初始化
  3. 伴生对象在类被加载时初始化

委托

  1. 类委托 class Derived(b: Base) : Base by b
  2. 延迟属性 by lazy,默认包含同步锁 synchronized
  3. 可观察属性 by Delegates.observable(默认值){被赋值的属性, old, new -> …}
  4. 映射属性 by map, val 需要 Map,var 需要 MutableMap

函数

  1. 函数的参数必须有显示类型,即使有默认值
  2. 有代码块的函数必须显示指定返回类型,除非返回 Unit
  3. 可变数量的参数 vararg a: T,伸展操作符 *a

中缀表示法

  1. infix fun Int.shl(x: Int): Int
  2. 中缀函数只有一个参数
  3. 1 shl 2 等同于 1.shl(2)

尾递归

  1. tailrec fun a
  2. 递归函数容易出现堆栈溢出
  3. 尾递归函数必须将其自身的调用作为走后一个操作

lambada

  1. 被大括号包围
  2. 如果 lambada 是最后一个参数,可以在圆括号外指定
  3. 如果 lambada 的参数只有一个,用 it
  4. 如果有未使用的参数,用下划线表示:map.forEach {_, value -> print("$value")}
  5. 如果 lambada 的返回类型不是 Unit,那么最后一个表达式即其返回值

匿名函数

  1. 没有函数名字的函数声明
  2. 匿名函数在圆括号内传递
  3. 匿名函数的 return 从匿名函数返回,lambada 的 return 从包含它的函数返回

带接收者的含数字面值

  1. sum : Int.(other: Int) -> Int

内联函数

  1. inline
  2. 会导致生成的代码增加,但会在性能上有所提升

解构

  1. (a, b) 对应 component1(), component2()

集合

  1. List 不可变,MutableList 可变
  2. 。。。

Google Search Console

配置 Google Analytics 时会收到通知,建议关联 Google Analytics 和 Search Console。如下图所示:

Google Search Console 是由 Google 提供的一项免费服务,可帮助您监控和维护自己网站在 Google 搜索结果中的显示情况。您无需注册 Search Console,您的网站也可以出现在 Google 搜索结果中,但注册有助于您了解 Google 如何找到您的网站并优化其在搜索结果中的显示效果。

添加属性

打开 Google Search Console 首页,如下图所示:

点击添加属性来添加你的网站,如下图所示:

验证添加完成。

提高在搜索结果中的展现率

一般在添加一个网站后会收到一封站内邮件,如下所示:

顺序处理1-5,

  1. 需要你同时在 Search Console 中添加 http://www.example.com 、http://example.com、https://www.example.com、https://example.com 四个网站。
    这需要你的网站同时可以解析 www 和 non-www 两个版本。我这里是在 DNS 解析的设置里配置两条记录,如下图所示:

    第1条 @ 记录匹配 non-www 版本,第4条 www 记录匹配 www 版本。
  2. 点击设置首选版本按钮跳转到设置页面,点击右上角的网址来进行网站的切换。在网站设置里进行设置。如下图所示:
  3. 跳过,因为我不介意你来自哪里。
  4. 跳过,没有别的用户。
  5. 你需要先生成一个地图文件,即 Sitemap。
    这里使用 Yoast SEO 这个插件。
    首先在插件的设置里打开高级设置页面,如下图所示:

    然后插件的设置边栏里就会多出一个 XML Sitemaps 的选项,打开它:

    点击 XML Sitemap,获取地址:

    然后把 sitemap_index.xml 填入 Search Console 的设置里,点击提交就 OK 啦。如下图:

在运行时请求权限

背景资料

https://developer.android.com/training/permissions/requesting.html?hl=zh-cn

从 Android 6.0(API 级别 23)开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要授予权限。它还让用户可以对应用的功能进行更多控制;例如,用户可以选择为相机应用提供相机访问权限,而不提供设备位置的访问权限。用户可以随时进入应用的“Settings”屏幕调用权限。

流程

下面是我根据自己的理解画的流程图。

在运行时请求权限流程图

详细的步骤具体说明在安卓开发者网站上 https://developer.android.com/training/permissions/requesting.html?hl=zh-cn#perm-request 已经有详细的中文说明。

PermissionsDispatcher

PermissionsDispatcher 是一个用来简化系统运行时权限逻辑的库。GitHub 地址是 https://github.com/hotchemi/PermissionsDispatcher

下面简单介绍下用法。

安装

在 build.gradle 中添加

dependencies {
  compile("com.github.hotchemi:permissionsdispatcher:${latest.version}") {
      // if you don't use android.app.Fragment you can exclude support for them
      exclude module: "support-v13"
  }
  annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:${latest.version}"
}

使用

PermissionsDispatcher 主要通过注解来简化运行时权限的逻辑处理。注解如下:

Annotation Required Description
@RuntimePermissions Register an Activity or Fragment(we support both) to handle permissions
@NeedsPermission Annotate a method which performs the action that requires one or more permissions
@OnShowRationale Annotate a method which explains why the permission/s is/are needed. It passes in a PermissionRequest object which can be used to continue or abort the current permission request upon user input
@OnPermissionDenied Annotate a method which is invoked if the user doesn’t grant the permissions
@OnNeverAskAgain Annotate a method which is invoked if the user chose to have the device “never ask again” about a permission

对应流程图的结果如下,使用的时候把相应逻辑处的代码声明为带注解的函数即可:

PermissionsDispatcher

shouldShowRequestPermissionRationale 的两次使用

这个方法用了两次,分别在请求权限之前和被拒绝授予权限之后。

  1. 请求权限之前
    为了判断是否需要向用户展示申请权限的理由。
    如果该值为 false,则是第一次向用户申请权限,没必要加多余的解释;
    而如果该值为 true,则表示用户曾经拒绝过授予,有必要向用户做一些相应的解释。
  2. 被拒绝授予权限之后
    为了判断是否需要手动引导用户到设置里授予权限。
    如果该值为 true,说明以后还有机会向用户申请,现在可以运行其他的功能;
    而如果该值为 false,则说明用户勾选了不再询问的复选框,你已经没机会向用户申请该权限了,你需要引导用户到设置里手动打开。

EasyPermissions

谷歌出品的权限库,也可以供参考。GitHub 地址是:https://github.com/googlesamples/easypermissions。

用 ThreadLocal 实现静态变量线程同步

代码中有一种使用场景:
子类需要调用父类的构造方法来进行实例化,而传给父类构造方法的参数中却有参数需要在子类中先进行加工处理。比如下面的例子:

父类构造方法的第三个参数需要调用 signUrl 这个方法进行加工。因为

Cannot reference ‘signUrl’ before supertype constructor has been called

所以,一种解决方法就是将 signUrl 声明为 static 类型的。

可以看到成员变量 mRequestParamMap 在该方法中进行了引用。所以 mRequestParamMap 也需要声明为 static。(或者可以把 mRequestParamMap 声明为一个 signUrl 内的局部变量,但此处该变量还在别的方法里进行了引用,所以不能改为局部变量)

这同时就导致了多线程调用时 mRequestParamMap 的同步问题。因为不同线程访问的是同一个 mRequestParamMap,一旦同时进行了操作就会导致不同步的问题发生。

这里将介绍一种方法来规避这种问题,即使用 ThreadLocal 来保存该变量。

ThreadLocal 的作用是每个线程都会获得一个只属于线程自己的变量,其他线程无法访问。
如此这般,就实现了静态变量在线程间的同步。

博客切换 HTTPS 安全连接

背景

新建好的 WordPress 博客默认启用了 https 功能,但是它的 CA 证书是自签名的,无法通过浏览器的认证。想通过浏览器认证就必须有一个得到广泛认可的 CA 机构颁发的 CA 证书。

Let’s Encrypt

Let’s Encrypt 是一个于2015年三季度推出的数字证书认证机构,将通过旨在消除当前手动创建和安装证书的复杂过程的自动化流程,为安全网站提供免费的SSL/TLS证书。
Let’s Encrypt 是由互联网安全研究小组(ISRG,一个公益组织)提供的服务。主要赞助商包括电子前哨基金会,Mozilla基金会,Akamai以及思科。

Let’s Encrypt 免费又有这么多机构支持,自然就是我们的首选了。

安装 CA 证书

这一切都是非常轻松的。 因为已经有很方便的脚本来供你使用,了解过命令行的都可以很快完成。

  1. Install
    将脚本下载下来,并赋予执行权限。
    wget https://dl.eff.org/certbot-auto
    chmod a+x certbot-auto
  2. Get Started
    执行脚本,生成 CA 证书,并自动进行配置。
    sudo ./certbot-auto --nginx
  3. Automating renewal
    因为 Let’s Encrypt 的证书有效期只有90天,所以一个自动续期的设置是很有必要的。
    执行下述命令来检测脚本的续期功能是否正常。
    sudo ./certbot-auto renew --dry-run
    如果执行正常,那么就可以给系统增加一个自动定时任务来执行续期功能。
    ./certbot-auto renew

其他问题

有可能你做完上述的一切操作却发现 Google Chrome 中依旧把你的网站标记为不安全,这时候你可以检查一下当前的页面中是否有非 https 的链接,比如图片等存在,将这些链接也替换为 https 的就大功告成了。

在 Vultr 上搭建 WordPress

新建一个 WordPress 主机

这里有两个方法:
1. 在创建主机的时候选择 WordPress Application Server Type:

2. 在已经创建好的主机中更改设置:

设置 WordPress

在 Server Information 页面有 Application Information 的说明,参照说明登录 WordPress:

配置 DNS 解析

在域名解析的地方新增一条 A 记录:

设置 WordPress URL

设置 Nginx 的 Server Name

把 Nginx 配置文件中 server_name 后面的下划线换成你的域名,然后重启 Nginx:

大功告成

访问 https://[YOUR_DOMAIN]/wp-admin/ 就可以管理你的博客了。

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

问题

远程服务器重装系统后,ssh 连接失败,错误提示消息如下:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:54CKbzMi5EZejSTWZxDUAgVdW6E5kBdn5rKnjQBgJNI.
Please contact your system administrator.
Add correct host key in /Users/plusend/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/plusend/.ssh/known_hosts:19
RSA host key for 45.76.xxx.xxx has changed and you have requested strict checking.
Host key verification failed.

解决方法

处理这个问题只需要删除之前保存的 host key 就可以了,方法是:

打开 ~/.ssh/known_hosts 文件;
将相关的那一行(这里是45.76.xxx.xxx)删除就可以了。