0%

Go编译相关

Go的一些环境变量

在我们安装Go语言的时候,都要设置一些环境变量,最重要的就是GOROOT和GOPATH,那么他们分别代表的是什么呢?

下图是所有的Go环境变量

随便挑几个

  • GOROOT是GO的安装路径,有一些关于操作系统方面的支持和编译器、链接器等等

  • GOPATH是一个工作空间,目的是提供一个寻找.go源码的路径,可以设置多个目录,GO官方要求需要包含三个文件夹,src存放源文件,pkg存放编译后的库文件,后缀为.a,bin则存放可执行文件。

  • GOOS是Go当前所在的操作系统

  • GOARCH是Go所在的计算机架构

  • GO111MODULE表示是否开启Gomod

  • GOPROXY表示代理,direct表示是否直接走代理

  • CGO表示是否开启CGO

在交叉编译的时候就要使用到GOOS和GOARCH,交叉编译指的是:编译的平台和代码最终运行的平台不一样,我们都知道,不同机器的机器码是不一样的,不是说一个二进制文件,在所有机器上都可以执行的。

比如这么一个场景:你的开发环境是Win10,但是服务器是Centos7,这时候你想要让代码在服务器上运行,可以有这么两种解决方案

  1. 把整个工程文件传到服务器上,在服务器上进行编译和运行,但这显然不方便,因为你还要在服务器上安装Go
  2. 在开发环境进行编译,编译出的二进制文件能够在服务器上运行

显然第二种解决方案更加简单

那么该怎么做呢?

  1. 修改GOOS和GOARCH为对应的平台与计算机架构,在Go1.13之后修改环境变量要用到go env -w
  2. GOOS=linux GOARCH=amd64 go build //分别指定对应的平台和机器的位数

当然,在实际开发中一般环境都是和服务器相同的(操作系统和机器位数),这样就不会有类似的问题。

Go代码编译链接过程

从上图可以看出来,一份Go代码需要先经过编译器的编译称为汇编程序,再通过汇编器成为二进制可执行程序,再经过链接器的链接,最后才成为了一份二进制可执行文件。

具体过程:go build其实就是编译和链接的过程,编译是指对源文件进行词法分析、语法分析、语义分析、优化,最后生成汇编代码文件,以.s作为文件的后缀。之后,汇编器会将汇编代码转变成机器可以执行的指令,每一条汇编语句都与一条机器指令对应。编译是一个很智能的过程,里面还包含了优化的部分,而汇编则是比较机械的部分,将汇编语句转换成机器指令。

编译过程

主要过程就是:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化

词法分析(扫描):将源代码字符序列转换为标记(token)序列的过程 进行这个步骤的程序叫做词法分析器,也叫作扫描器(scanner),一般以函数的方式存在,.go文件被输入到scanner,它使用一种类似于有限状态机的算法,将源代码的字符系列分割成一系列的token。token分为这几种:关键字、标识符、字面量、特殊符号等等

Go语言scanner的具体逻辑就是通过next函数,获取下一个未被解析的字符,并且跳过之后的空格,回车,换行,tab这些字符,进入一个大的switch-case语句,匹配不同的情况。

语法分析:上一步生成的token序列,需要经过进一步的处理,生成一颗以表达式为结点的语法树(把符号组成一个句子)

语义分析:检查常量、类型、函数声明等等,可以把这一步看成静态检查,如果有很明显的语法错误,就会报错。

中间代码生成:编译过程可以分为前端和后端,前端生成和平台无关的中间代码,而后端会针对不同的平台,生成不同的机器码,前面的词法分析、语法分析、语义分析都属于编译器前端,后面的阶段属于编译器后端。

目标代码生成与优化:不同机器的机器字长、寄存器都不一样,意味着在不同机器上跑的机器码是不一样的,最后一步的目的就是要生成能在不同CPU架构上运行的代码。目标代码优化器会对一些指令进行优化,提升程序的效率。

链接:将编译器生成的一个个目标文件链接成可执行文件,最后得到的文件是分成各种段的。

其实关于编译链接这部分的内容还有很多需要学习的,这里只是简单的说明了一下,具体可以参考《程序员自我修养》这本书

Go编译相关命令

Go语言的源码分为三类:命令源码、库源码、测试源码

命令源码:Go程序的入口,包含func main() 函数,且第一行用package main声明属于main包

库源码:主要是各种函数、各种接口,例如工具类的函数

测试源码:以_test.go为后缀,用于测试功能、性能等

与编译相关的Go命令主要有三个

  • go build
  • go install
  • go run

go build的一些参数

  • -a 强制重新编译所有涉及到的包
  • -n 打印命令执行过程 不真正执行
  • -p n 打印命令执行的并行数 n默认为cpu核数
  • -race 检测并报告程序中的数据竞争问题
  • -v 打印命令执行中涉及到的代码包名称
  • -x 打印命令过程中涉及到的命令 并执行
  • -work 打印编译过程中的临时文件夹,编译完成后会被删除

go build:编译过程会忽略掉测试源码 执行过程是递归寻找main.go所依赖的包,以及依赖的依赖,直至最低层的包,如果有循环依赖,则直接退出

go install :编译并安装指定的代码包,相比于Go build,它多了一个“安装编译后的结果文件到指定目录”的步骤

go run :先编译,再链接,再执行

Go程序启动过程

1.检查运行平台的CPU 设置好程序运行需要相关标志

2.TLS的初始化

3.runtime包进行变量和调度器的设置

4.创建新的goroutine绑定用户的main方法

5.开始进行goroutine的调度

Go defer详解

什么是defer

在进行编程的时候,经常需要在代码中申请一些资源,比如数据库连接,文件句柄等等,这些资源需要在用完之后进行释放,否则会造成内存泄漏(内存一直被占用而不被释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果),而Go就提供了一个defer关键字,可以实现延迟调用。defer可以让函数或者语句在当前函数执行完毕后执行(包括程序正常退出和panic导致的异常结束)

以下代码有什么问题?

r.mu.Lock()

rand.Intn(param)

r.mu.Unlock()

看起来这段代码是没有什么问题的,但这是建立在程序正常运行的基础上,如果rand.Intn出现了异常而造成了panic,就会出现r一直占用着锁而不释放的问题,所以即使是简单的释放资源的代码,使用defer也是很有必要的。

defer的执行顺序

defer的语句并不会马上执行,而是会进入一个栈,所以执行的顺序也会和栈一样,即先进后出,也就是说最先被定义的defer语句最后执行,这个原因是后面定义的函数可能对前面的资源有依赖,所以要先执行;否则如果前面先执行了,对某些资源进行释放,后面的函数就会出错。

在defer函数定义时,对外部变量的引用有两种方式,函数参数和闭包引用,前者在defer定义时就把值(广义的值,如果传的是引用类型,那么和定义的时候可能不一样)传给defer,并被存起来,而后者则会在defer真正调用的时候根据上下文确定值。

如果是闭包的话,则会和定义的时候不一样,看下面这个栗子

1
2
3
4
5
6
7
8
9
10
11
func main() {
var t [3]struct{}
for i := range t {
defer func() {
fmt.Println(i) //defer 后面接的是闭包 在for 循环结束后的i的值为2
}()
}
// 2
// 2
// 2
}

注意:在Go使用close()关闭某些资源的时候,最好提前判断调用主体是否为空,否则很可能会解引用一个空指针,从而造成panic的问题

return与defer

return并不是一条原子指令,return的执行顺序是这样的

1.返回值=xxx

2.调用defer函数

3.ret指令

下面是两道defer经典题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func f1() int {  //返回值没有取名
x := 5
defer func() {
x++
}()
return x //1.返回值赋值 2.defer 3.执行RET返回 //开辟了一份空间 return语句先将5赋值给x defer修改了x的值 但是RET命令执行的对象还是那个‘5’
}
func f2() (x int) { //返回值有名字
defer func() {
x++ //闭包
}()
return 5 //修改的是x的值
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x //y = x = 5 defer修改的是 x 真正返回的y还是5
}
func f4() (x int) {
defer func(x int) {
x++ //改的是函数值的副本 //如果这里是 defer func(x *int) { x ++ }(x)
}(x) //那么结果就会变成6
return 5
}
func main() {
fmt.Println(f1()) //5
fmt.Println(f2()) //6
fmt.Println(f3()) //5
fmt.Println(f4()) //5
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
//1.调用calc中的calc("A",x,y) 输出:"A",1,2,3 因为函数调用时会确定每个参数的值
//2.defer calc("AA",x,3)
//3.调用第二个calc中的calc("B",x,y) 此时x y是20 输出:"B",10,2,12
//4.defer calc("BB",x,12)
//5.先进后出 调用第二个calc 输出:"BB",10,12,22
//6.调用第二个calc 输出:"AA" 1,3,4
}

什么是闭包

闭包=函数+引用环境 不推荐在生产环境中使用,就算你自己对闭包的使用很熟练,但是代码写出来是给人看的,不能苛求你的同事、测试对这部分也很熟悉,而且闭包很容易出现错误。

Go中的所有匿名函数都是闭包程序

defer配合recover使用

执行初始化的时候出问题,最好直接panic掉,避免上线之后出现更大的问题,有些时候,需要从异常中恢复,比如服务器的严重问题产生了panic,这个时候需要在程序崩溃之前做一些扫尾工作,比如关闭客户端的连接。并且一个panic不应该影响整个服务器的运行,这时候就需要defer 和 recovery进行配合

defer配合recovery使用:recovery必须在defer的函数中 才是标准格式

比如这种

func main() {
defer f()
panic(404)
}

func f() {
if err := recover(); err != nil {
fmt.Println("recover")
return
}
}

或者采用匿名函数

func main() { defer func() { if err := recover(); err != nil { fmt.Println("recover") return } }() panic(404) }

但是注意,一定要在函数里,像这样是不行的

func main() { defer recover() panic(404) }//还是会panic

defer链是如何被执行的

前面我们说到过,一个函数中的defer语句是按照栈的顺序执行的,每一条defer语句都会创建一个_defer结构体,这些结构体以链表的形式挂载在G下(Goroutine)。

defer首先会调用deferporc函数,new一个_defer结构体,挂到G上,当然,调用new之前会从当前G绑定的P中的defer pool中取,如果没有的话则会去全局的defer pool中取,是在没有就新建一个,这是Go runtime的常规操作,也就是设置多级缓存,提高运行效率。

之后按照顺序,处理一个个_defer结构体,即完成了defer链的执行。

RSA详解

1.RSA思想

RSA公开密钥密码体制是一种使用不同的加密密钥和解密密钥,由“已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。

在这种体制中,加密秘钥(公钥)PK是公开信息,而解密密钥(私钥)SK是需要保密的,加密算法E和解密算法D也都是公开的,这样的话虽然SK是由PK决定的,但是不能通过PK计算出SK。

​ 基于这种思想,在1978年出现了著名的RSA算法。

  1. 通常是先生成一对RSA密钥
  2. 其中的一个是私钥,用户保存,另一个是公钥,可对外公开,甚至可以在网络服务器中注册
  3. 为了提高保密强度,密钥至少为500位,一般推荐1024位
  4. 为了减少计算量,传送信息的时候通常采用传统加密与公开密钥加密结合的方式

算法原理:根据数论的理论,找两个大素数比较简单,对他们的乘积进行因式分解却很困难,因此可以将乘积公开作为加密秘钥

2.算法描述

  1. 任意选两个不同的大素数p和q计算乘积n=pq,欧拉函数n=p-1 * q-1
  2. 任意选一个大整数e,满足 gcd (e,欧拉n)=1,整数e用作加密钥
  3. 确定的解密要d 满足(de) mod 欧拉n =1 很容易计算出d
  4. 公开n和e 保存d
  5. 将明文m(m<n)加密成c 加密算法为 c=m^e mod n
  6. 将密文c解密为明文m 解密算法为 m=c^d mod n

通俗易懂的解释:

1.第一步 随机选择两个不相同的质数 p 和 q 比如61和53,在实际应用中,这两个质数越大,就越难破解

2.第二步 计算p和q的乘积n 上面的栗子 n=61*53=3233

这个n就是密钥,写成二进制位110010100001,一共12位,那么这个密钥长度就是12位,在实际应用中,RSA密钥长度要到1024位,才具有足够的保密性

3.第三步,计算n的φ(n)

欧拉n=p-1* q-1 上面的栗子为3120

4.第四步 随机选择一个整数e 条件为1<e<φ(n),且e与欧拉n互质,实际应用中常用65537,我们这边随便选个17

5.计算e对于φ(n)的模反元素d

所谓模反元素就是指有一个整数d 可以让ed 被 φ(n) 除的余数为1

可以通过扩展欧几里得算法求解

6.将n和e封装成公钥,n和d封装成私钥

可靠性:能不能在已知n和e的情况下,推出d,也就是能不能在已知公钥的情况下,推出私钥?

结论:不行,需要对大整数进行因式分解,这个过程很困难,目前被破解的最长RSA密钥为768位

7.加密过程:

通过公钥(n,e)对m进行加密,m必须为整数,且m必须小于n

加密过程

m ^ e ≡ c (mod n)

8.解密过程

c ^ d ≡ m (mod n)

3.安全性描述

  • 保密强度随着密钥的长度增加而增强,但是密钥越长,加解密的时间也就夜场
  • RSA安全性依赖于大数分解
  • 因为进行的都是大数计算,使得RSA的速度比DES慢上好几倍甚至好几十倍,因为AES这类算法通常厂商会在硬件方面进行优化,RSA一般只用于少量的数据加密,RSA的速度比对应同样安全级别的对称密码算法要慢1000倍左右

工业界的几种发布模式

蓝绿发布

  • 蓝绿发布的本质上是通过系统冗余来解决上线风险的问题,通常生产环境中配置两套完全一样的环境,一组是active的生产环境配置(绿色),一组是inactivate的准备环境配置(蓝色),两套系统都是功能完善的,并且正在运行的系统,只是系统版本和对外服务情况不同
  • 最初始的状态下,没有任何系统,也没有蓝绿之分,然后第一套系统开发完成直接上线,只有一个系统,后来业务更新,要用新版本替换线上的旧版本,这时候就有两个系统,正在对外提供服务的是老系统为绿色,新部署的系统为蓝色

为什么要有蓝色系统?因为蓝色其实是为了对新版本进行全方位的测试

  • 如果测试没出问题,可以通过负载均衡器/反向代理/路由指向蓝色环境,之后只要检测这个环境是否有故障,如果运行良好, 就可以删除v1.0使用的资源,为下一次蓝绿部署空出可用资源
  • 如果测试有问题,直接修改指向,快速回滚到绿色环境

优点:部署过程中,应用始终在线,并且新版本没有修改老版本的任何内容,在部署期间,老版本的转改不收影响,风险很小,并且理论上可以在任何时间回滚到老版本

缺点:需要两套环境,冗余的基础设施。而且对于设计数据表结构变更等不可逆转的升级,不太适合

金丝雀发布

金丝雀发布就是灰度发布,这两个东西是同一类策略,策略就是只有一套系统,并且逐渐替换这套系统。

指的是增量发布的一种类型,在原有版本可用的情况下,同时部署一个新版本应用作为“金丝雀”,测试新版本的性能和表现,以保证整体系统的稳定性。

发布过程

  • 从负载均衡列表里下掉
  • 升级金丝雀应用
  • 重新添加到负载均衡列表中
  • 如果测试成功,升级其他的服务器,否则回滚

通常会AB测试一起使用

AB测试

AB测试是效果测试,同一时间有多个版本的服务对外服务,这些服务都是经过足够测试,达到了上线标准的服务,有差异但是没有新旧之分,核心目的是用来测试应用功能表现的方法。比如页面的样式、颜色不一样,最后通过分析各个服务的实际效果,选择效果最好的版本。

滚动发布

所谓滚动发布一般是取出一个或者多个服务器停止服务,执行更新,并重新投入使用,周而复始,直到集群中的所有实例都更新成新版本,比如每次选出集群中的20%进行升级。比蓝绿部署更节省资源。

缺点:

  • 没有一个确定OK的环境
  • 需要回滚的话很麻烦

红黑部署

这是Netfix采用的部署手段,主要基础设施在AWS上,它利用了AWS的特性,在部署新的版本的时候,通过AutoScaling Group 用包含新版本的AMI创建新的服务器,服务始终在线,同时采用不可变部署的方式。

容错、高可用和灾备

容错:指的是在发生故障时,系统仍然能够继续运行,比如飞机的设计就遵从了容错的思想,如果一个引擎坏了,剩下几个引擎,仍然能够飞。容错的目的是:发生故障时,系统的运行水平可能有所下降,但是依然可用,不会完全失效。

高可用:系统能比正常时间更久地保持一定的运行水平,骑车的备胎就是一个高可用的栗子,如果没有备胎,轮胎坏了,车就开不久,备胎延长了骑车行驶的可用时间。指的是一旦中断能够快速恢复,且中断必须是短暂的。

灾备:发生灾难时恢复业务的能力。如果飞行员是业务的话,飞机就是基础设施,飞行员的弹射装置就是灾备措施,一旦飞机即将坠毁,基础设施就要没了,灾备可以让业务幸存下来。目的:保留系统的核心部分,一个好的灾备方案,就是从失败的基础佘时仲获取企业最宝贵的数据,然后在新的基础设施上恢复它们。

总结可以通过以上的三个方面结合起来,设计一个可靠的系统

  • 容错:发生故障的时候,如何让系统继续运行
  • 高可用:系统中断时,如何尽快恢复
  • 灾备:系统毁灭时,如何抢救数据

CPU和GPU的区别

CPU:Central Processing Unit 中央处理器,是计算机的大脑,包括运算器、控制器、寄存器、高速缓存以及总线

GPU:Graphics Processing Unit 图形处理器,最初是用在个人电脑、工作站、游戏机和一些移动设备上运行绘图运算工作的微处理器。

CPU和GPU之所以大不相同,主要是由于设计目标的不同,分别针对两种不同的应用场景。

CPUI需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断引入大量的分支跳转和中断的处理,这些使得CPU的内部结构是很复杂的。

image-20221018203400386

而GPU所面对的数据则是类型高度统一、相互无依赖的大规模数据和不需要被打断的纯净的计算环境,所以GPU的构成相对简单,有数量众多的计算单元和超长的流水线,特别适合处理量大且同一的数据。在设计上采用了众多的计算单元和超长的流水线,但只有简单的控制逻辑并且省去了cache

除了渲染图形的能力,在并行处理方面GPU是很强的,因此在密码破译,大数据处理,金融分析等领域应用广泛(所以为什么会有那么多矿卡知道了吧)。其实GPU并没有专门为图像服务的部件。

为什么GPU擅长处理图像数据呢?这是因为图像上的每一个像素带你都有被处理的需要,而且每个像素点处理的过程和方式都十分类似,很适合机械的GPU进行处理。但是GPU无法单独工作,必须由CPU进行控制调用,CPU可单独作用,处理复杂的逻辑运算和不同的数据类型。

打个比方,GPU的运算速度取决于雇佣了多少小学生,CPU的运算速度取决于请了多么厉害的教授,教授处理复杂任务的能力是碾压小学生的,但是对于没那么复杂的任务,还是顶不住人多。

为什么在人工智能领域GPU十分盛行呢?因为深度学习是模拟人脑神经系统简历的数学网络模型,这个模型的最大特点是需要大数据来训练。所以需要大量的并行的重复计算,在深度学习方面GPU有以下特点

  • 提供了多核并行计算的基础结构,且核心数很多(通常有成百上千个核心),可以支撑大量数据的并行计算。
  • 用于更高的访存带宽和速度
  • 具备更高的浮点运算能力

独立显卡:把GPU焊死在了显卡的电路板上,上面有一个散热风扇

集成显卡:把GPU和CPU放在一起,共用缓存来工作,并共用风扇。

JSON的几种标准格式

今天在使用mongoimport导入json格式的时候发现出问题了,需要我去了解一下json的标准格式,在这之前我一直以为json是用{}括起来的键值对,且最后一个键值对不能有逗号。(弱者的自嘲)

什么是JSON

  • json是JavaScript Object Notation(JavaScript对象表示法)
  • json是轻量级的文本数据交换格式
  • json独立于语言
  • json具有自我描述性,更衣理解
  • json使用js语法来描述数据对象,但是json仍然独立于语言和平台,json解析器和json库支持许多不同的变成语言

json是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成,之所以json这么流行,是因为json的结构和多级结构体刚好能对应上,而前后端交互的时候后端通常会返回给前端一个多级的结构体,于是json慢慢开始流行了,且json是跨语言和跨平台的,自身也足够轻量级。

json的标准格式

json数值

//每个key对应的是一个value

{

“k1": 1,

"k2": 2 //注意结尾的这个不能有逗号

}

json字符串

{

"k1": "1",

"k2": "2"

}

json数组

{

​ “k1”: [1,2],

​ “k2”: [3,4]

}

json对象

{

​ “k1”: {“1”: “haihai”},

​ “k2”: {“2”:”haihahai”}

}

json对象数组

{

​ “k1”: [

​ {“k11”: “hellohello”},

​ {“k12”: “badbad”}

]

}

json数组对象

{

​ “k2”: {

​ “hello”: [1,2,3]

}

}

net/rpc初识

RPC:远程调用过程协议,相关理论可以说是分布式和微服务的基础了,有需要的可以自行深入学习,简单来说RPC就是为了本机可以调用远程机器的子服务而产生的这么一种通信协议,基本上在后端开发中,RPC接口和HTTP接口的出现次数是最多的。

RPC通常和protobuf联系在一起,这么说其实不太准确,其实是grpc和protobuf联系在一起,因为protobuf序列化和反序列化的速度比json快,而且protobuf更加轻量级,更适合进行轻量的rpc通信(restful风格接口)

不管是轻量的GRPC框架,还是基于GRPC框架的微服务框架Go-Micro、Go-Kit等,它们在RPC这一块的实现肯定都是基于Go语言内建库:net/rpc上的,很多人上来就直接去撸框架,我其实并不认同这种学习方法,从语言的底层开始学起显然是一种更优的方法。

来看看Go内置的rpc包

服务1:求圆形面积

server

//结构体 作为服务的接收者

type MathUtil struct{

}

//向外暴露的服务 对应着Go的方法 方法:有接收者的函数

//net/rpc规定 服务的参数只能有两个 第二个参数必须为指针 返回类型必须为error

func (mu *MathUtil) CalculateCircleArea(req float32,resp *float32) error {

*resp=math.Pi * req * req

return nil

}

func main() {

//初始化指针类型数据

mathUtil:=new(MathUtil)

//调用 rpc注册服务

 `err:=rpc.Register(mathutil)`

if err!=nil {

panic(err.Error())

return

}

//将服务注册到http协议上,调用者可以通过http的方式进行调用

rpc.HandleHTTP()

//进行特定端口的监听

listen,err:=net.Listen("tcp",":8087")

if err!=nil {

panic(err.Error())

}

http.Serve(listen,nil)

}

client

func main() {

//拨通HTTP

client, err := rpc.DialHTTP("tcp", "localhost:8087")

if err != nil {

panic(err.Error())

}

var req float32

req = 2

var resp *float32

//同步调用 Call的参数分别为服务名 注意这里的字符串是定死的 必须为 结构体.服务 和参数

err = client.Call("MathUtil.CalculateCircleArea", req, &resp)

if err != nil {

panic(err.Error())

}

fmt.Println(resp)

//异步调用

//syncCall := client.Go("MathUtil.CalculateCircleArea", req, &resp, nil)

//replayDone := <-syncCall.Done

//fmt.Println(replayDone)

//fmt.Println(*resp)

}

ssh登录及原理

​ 首先我们都知道ssh是一个远程授权的协议,通过ssh可以进行一些远程登录等操作,而scp就是在连接上远端服务器的ssh后有启动了一个scp进程,通过操作系统的stdin和stdout进行数据通信。

​ 而火热的xshell和xftp其实就是对这两个命令的封装,xshell用来远程登录,xftp用来远程传输文件。

​ 在ssh之前,其实就有一些能够实现远程登录的方式了,比如telnet(终端仿真协议),rcp(scp就是对rcp的升级),ftp等,但它们都是不安全的,并且会用明文传输密码,即使在私网上也有篡改和窃取的风险。

​ 传统的远程服务如ftp很容易受到中间人攻击,所谓中间人就是存在另一台机器冒充真正的服务器接收用户传给服务器的数据,然后再冒充用户将

​ ssh:secure shell 是由IETF制定的建立在应用层基础上的安全网络协议,专为远程登录会话和其他网络服务提供安全性的协议,通过SSH可以将所有传输的数据进行加密(RSA),同时能防止DNS欺骗和IP欺骗,并且数据经过压缩,可以加快传输的速度。

ssh组成
  1. 传输层协议 【SSH-TRANS】: 这部分提供了服务器认证,保密性及完整性,此外它还能够提供压缩功能,SSH-TRANS通常运行在TCP/IP智商,也可能用于其他可靠的数据流上,TRANS提供了强力的加密技术、密码主机认证及完整性保护
  2. 用户认证协议:用于向服务器提供客户端用户鉴别功能,运行在运输层协议上,从低层协议接受会话标识符
  3. 连接协议:将多个加密隧道分成逻辑通道,运行在用户认证智商,提供交互式登录话路、远程命令执行。转发TCP IP连接等
ssh协议的具体实现:
  • 服务器建立公钥,每次启动sshd服务时,sshd会主动去找/etc/ssh/ssh_host*文件,如果系统刚创建还没有这些文件,那么sshd会主动计算出这些公钥,同时也会计算出自己需要的私钥
  • 客户端主动联机请求,如果客户端想要联机到ssh服务器,需要一个客户端程序,比如已经可以使用ssh的shell或者xshell这种使用了ssh的应用程序
  • 服务器传输公钥给客户端,在拿到客户端请求之后,服务器把公钥给客户端
  • 客户端记录并比对服务器公钥的数据,如果是第一次连接,则会把服务器公钥记录下来,如果不是第一次连接,则会去对比之间的记录,查看是否有差异。如果没有问题,客户端会接受这个公钥,然后开始计算客户端自己的公钥和私钥
  • 客户端计算出自己的公钥和私钥会后回传给服务器,此时服务器就具有了服务器的私钥和客户端的公钥,客户端就有了服务器的公钥和客户端的私钥,开始非对称加密
ssh两种级别的验证方法
  • 第一级(基于口令):只要知道账号和口令,就能够登录到远程主机。所有的传输数据都会被加密,但不能保证连接的服务器是真实想要连接的服务器
  • 第二级(基于密钥):客户端需要为自身创建一对密钥,并把公钥放在需要访问的服务器上,如果要连接到ssh服务器,客户端会向服务器发出请求,需要用密钥进行安全验证。服务器收到之后,在目录下寻找保存的客户端公钥,再与现在客户端发过来的公钥进行对比。如果两个密钥一致,服务器就会用这个密钥加密“chanllenge”并发送回客户端,客户端收到之后用自己的私钥解密,然后再发送给服务器完成登录。(显然安全性比第一级高)
ssh和scp命令格式

ssh 登录远程服务器

ssh username@ip

然后输入密码

scp从远程复制文件

//传输一个文件

scp username@ip:remotefile file

//传输一个目录

scp -r username@ip:remotefile file

如果需要用scp将本地文件复制到远程 只要将命令格式反过来就行

私网及NAT协议

在构建局域网时,通常局域网内部要使用私有IP地址,而管理员可以根据需求,选择一类、二类、三类的私有地址,这种私有地址我们一般称作私网地址,和公网相对,是没有真正连接到互联网上的。A B C三类网络都有一小块地址供全世界各地建设局域网使用,也就是每个企业都可以用私网,不同局域网的私网地址对应的主机是不一样的,这样就能够有效解决IPV4地址不足的问题。

私网地址分别为:

  • A类 10.0.0.0~10.255.255.255
  • B类 172.16.0.0~172.31.255.255
  • C类 192.168.0.0~192.168.255.255

不难看出来A类私网网址是B类数量的16倍,而B类又是C类数量的16倍,C类私网网址的数量为2^16,已经足够大部分的公司或企业进行IP地址的分配了,如果是特大型系统,也可以考虑使用A类或者B类,不过现实中这种情况比较少,所以我们看到的私网IP地址通常都是192.168开头的。

同时我们还要将私网与公网的关系和内网和外网的关系区分开来,

私网和公网是通过IP地址进行区分的,从特征上看私网IP并没有连接到互联网上,只是通过NAT协议在路由器层面连接到互联网,也就是一个局域网中的所有私网IP,对外看来是一致的,对方并不知道你是私网,也不知道你有多少台主机。而公网就是真正接入到互联网中的,我们可以通过网络对公网资源进行访问。

而内网和外网对应的是防火墙这个概念,在防火墙内的网络我们称之为内网,防火墙外的网络则为外网。我们常说的梯子,翻墙等就是通过这些工具暴露出来的接口(通常是socks5)的代理接口,翻越了国家防火墙(GFW)的限制,与国家防火墙外的代理服务器进行通讯。

那么私网中的计算机是如何访问到局域网之外的网络的呢?这就需要利用到NAT协议(网络转换协议)

NAT;NetWork Address Translation 在IP数据包通过路由器或者防火墙的时候重写来源IP地址或者目的IP地址的技术

典型情况

一个私有网络比如192.168.xx.xx和连接在这个网络上的一个路由器,路由器占用这个网络地址空间的一个专有地址,同时通过一个ISP提供的IP地址连接到因特网上,当信息从本地向互联网传递的时候,源地址从私有地址转换成公钥地址

缺点:一个具有NAT功能的路由器下的主机并没有创建真正的IP地址,因此不能参与某些因特网协议

优点:NAT能够减少IP地址不足的情况,使用方便。成本较低,并且可以阻止网络上的部分恶意活动

那么路由器是怎么知道哪个主机发送给外界消息的呢?当路由器接收到响应的时候,它又是怎么知道该转发给哪台主机的呢?

在一个私网IP的主机发送给NAT路由器消息的时候,路由器记录了它的内网地址和端口,并且分配给他一个全局地址和全局端口,这个地址关系记录在NAT路由表中,之后按照目的地址发送给服务器,一段时间后服务器响应,那么路由器根据目的地址和端口按照路由表转换成对应的主机地址,再发送给主机。简单来说就是有两次映射,针对出境包源地址进行替换,在NAT路由表中记录映射关系,针对入境包进行地址替换,找到对应的主机。