《码出高效:Java开发手册》第1章 计算机基础
《码出高效:Java开发手册》第1章 计算机基础
大道至简,盘古生其中。计算机的绚丽世界一切都是由 0 与 1 组成的。
1.1 走进 0 与 1 的世界
原码:符号位与数字实际值的结合。正数数值部分是数值本身,符号位为 0 ;复数数值部分是数值本身,符号位为 1 。8 位二进制的表示范围是 [-127, 127] 。
反码:正数数值部分是数值本身,符号位为 0 ;负数的数值部分是在正数表示的基础上对各个位取反,符号位为 1 。8 位二进制的表示范围是 [-127, 127] 。
补码:正数数值部分是数值本身,符号位为 0 ;负数的数值部分是在正数表示的基础上对各个位取反后加 1 ,符号位为 1 。8 位二进制的表示范围是 [-128, 127] 。
三种编码方式对比:
| 正数 / 负数 | 原码 | 反码 | 补码 |
|---|---|---|---|
| 1 | 0000 0001 | 0000 0001 | 0000 0001 |
| -1 | 1000 0001 | 1111 1110 | 1111 1111 |
| 2 | 0000 0010 | 0000 0010 | 0000 0010 |
| -2 | 1000 0010 | 1111 1101 | 1111 1110 |
为了加速计算机对加减乘除的运算速度,减少额外的识别成本,反码和补码应运而生。
计算机 CPU 种没有减法运算器,只有加法运算器,比如 1 - 2 = 1 + (-2) = -1 。
如果使用原码计算 [0000 0001] + [1000 0010] = [1000 0011] = -3 ,这显然是不对的。
如果使用反码计算 [0000 0001] + [1111 1101] = [1111 1110] ,而 [1111 1110] 正是 -1 的反码,结果正确。
但在某些特殊的情况,反码存在认知方面的问题,例如 2 - 2 = 2 + (-2) = [0000 0010] + [1111 1101] = [1111 1111] = -0,但实际上 0 不存在 +0 和 -0 两种表达方式。
补码正是为了解决上面 +0 和 -0 的问题,而且表示区间更大(虽然只大了一个数值)。 -128 对应的补码是 [1000 0000] 。
如果使用补码计算 [0000 0001] + [1111 1110] = [1111 1111] ,而 [1111 1111] 正是 -1 的补码。
位移运算
在代码种经常使用这种方式进行高低位的截取、哈希计算,甚至运用在乘除法运算中。向右移动一位,近似表示除以 2 。在左移 << 与右移 >> 两种运算中,符号位均参与移动,除负数往右移动,高位补 1 之外,其它情况在空位处补 0 。
左移运算由于符号位参与向左移动,在移动的结果中,最左位可能为 1 或者 0,即正数左移的结果可能是正数,也可能是负数,负数同理。
对于三个见大于号的 >>> 无符号右移(注意,不存在三个小于号 <<< 的无符号左移运算符),当向右移动时,符号位参与位移,正负高位均补 0 ,正数不断向右移动的最小值为 0 ,而负数不断向右移的最小值为 1 。无符号即蔑视符号位,符号位失去特权,必须像其它正常的数字一样参与位移,高位直接补 0 ,根本不关心是正数还是负数。此运算常用于高位转地位的场景中。
为何负数不断地无符号右移地最小值为 1 呢?在实际编码中,位移运算仅作用于整型(32位)和长整型(64位)数上,假如在整型数上移动的位数是字长的整数倍,无论是否带符号位以及移动方向,均为本身。因为移动的位数是一个 mod 32 的结果,。长整型是 mod 64。
位运算还包括:
- 按位取反(
~) - 按位与(
&) - 按位或(
|) - 按位异或(
^)
其中按位与(&)运算典型的场景是获取网段值,IP 地址与掩码 255.255.255.0 进行按位与运算得到高 24 位,即为当前 IP 网段。
按位运算的左右两边都是整型数,true & false 这样的方式也是合法的,因为 boolean 底层表示也是 0 和 1 。
按位与和逻辑与(&&)运算都可以作用于条件表达式,但是后者有短路功能。
boolean a = true;
boolean b = true;
boolean c = (a=(1==2)) && (b=(1==2));
由于 && 有短路功能,执行结果为 a 为 fasle , b 为 true 。
同样的逻辑,按位或对应的逻辑或(||)也具有短路功能。
逻辑或、逻辑与运算只能对布尔类型的条件表达式进行运算。
异或运算(^)没有短路功能。
1.2 浮点数
计算机定义了两种小数:定点数和浮点数。
定点数:小数点位置固定,在确定长度的系统中,一旦确定小数点的位置后,整数和小数部分也随之确定。二者之间独立表示,互不干扰。订单数能表示的范围非常有限。
浮点数:采用科学计数法表示,由符号位、有效数字、指数三部分组成。
使用浮点数存储和计算时,若使用不当容易造成计算值和理论值不一致:
float a = 1f;
float b = 0.9f;
// c = 0.100000024
float c = a -b;
双精度型浮点数也一样:
double a = 0.1d;
double b = 0.2d;
// c = 0.30000000000000004
double c = a + b;
科学计数法
浮点数在计算机中用以近似表示任意某个实数。在数学中,采用科学计数法来近似表示一个极大或极小且位数较多的数。如 -4.86e11 ,等价于 -4.86 * 10^11 。
科学计数法的有效数字为从第 1 个非零数字开始的全部数字,指数决定了小数点的位置,符号表示该数的正与负。十进制科学计数法要求有效数字的整数部分必须在 [1,9] 区间内,满足这个要求的表示形式被称为“规格化”。
浮点数表示
当前业界流行的浮点数标准是 IEEE754 ,该标准规定了 4 中浮点数类型:单精度、双精度、延伸单精度、延伸双精度。前两种最常见。
浮点数无法表示零值,所以取值范围分为两个区间:正数区间和负数区间。
用于存储符号、解码、尾数的二进制位分别称为符号位、阶码位、尾数位。
- 符号位
在最高二进制位上分配 1 位表示浮点数的符号,0 表示正数,1 表示负数。 - 阶码位
在符号位右侧分配 8 位用来存储指数,IEEE754 标准规定阶码位存储的是指数对应的移码,而不是指数的原码或补码。根据计算机组成原理中对移码的定义可知,移码是将一个真值在数轴上正向移动一个偏移量之后得到的,即[x]移 = x + 2^(n-1)(n 为 x 的二进制位数,含符号位)。移码的集合意义是把真值映射到一个正数域,其特点是可以直观地反映两个真值的大小,即移码大的真值也大。基于这个特点,对计算机来说用移码比较两个真值的大小非常简单,只要高位对齐后逐个比较即可,不用考虑负号的问题,这也是阶码会采用移码来表示的原因。
假设指数的真值为 e ,阶码为 E ,则有E = e + (2^(n-1) - 1),其中2^(n-1) - 1是 IEEE754 标准规定的偏移量, n = 8 是阶码的二进制位数。
为什么偏移量是2^(n-1) - 1而不是2^(n-1)呢?因为 8 个二进制位能表示指数的取值范围为[-128,127],变成移码表示后阶码范围为[0,255]。由于计算机规定阶码全为 0 或全为 1 两种情况被当作特殊值(全 0 被认为是机器零,全 1 被认为是无穷大),去除这两个特殊值,阶码的取值范围就是[1,254]。如果偏移量不变,仍是 128 的话,则指数的取值范围就是[-127,126],指数最大只能到 126 ,显然会缩小浮点数的取值范围。所以 IEEE754 标准规定单精度的阶码偏移量为2^(n-1) - 1,这样指数范围为[-126,127],指数最大能到 127 。这样会导致阶码的取值范围变成
[-1,254],正好 -1 对应的补码就是11111111,特殊值之一,仍然可以满足根据阶码判断数值大小的功能。 - 尾数位
最右侧分配连续的 23 位用来存储有效数字, IEEE754 标准规定尾数以原码表示。正指数和有效数字的最大值决定了 32 位存储空间能够表示浮点数的十进制最大值。
科学计数法进行规格化的目的是保证浮点数表示的唯一性。二进制数值规格化后的位数形式为 1.xyz 。为了节约存储空间,将符合规格化首尾的首个 1 省略掉,所以尾数表明上是 23 位,却表示了 24 位二进制数。
加减运算
- 零值检测
如果其中一个数为 0 ,可以直接得出结果。 - 对阶操作
通过比较阶码的大小判断小数点位置是否对齐。当阶码不相等时,将阶码小的尾数位右移,直到阶码相等。 - 尾数求和
当对阶完成后,直接按位相加即可完成求和(如果是负数,则需要先转换成补码再进行运算)。 - 结果规格化
如果计算的结果不满足规格化,则需要将尾数左移或右移以达到规格化的目的。 - 结果舍入
在对阶过程或右规中,尾数需要右移,最右端被移出的位会被丢弃,从而导致结果精度的损失。为了减少精度损失,先将移出的这部分数据保护起来,称为保护位,等到规格化后再根据保护位进行舍入。
浮点数使用
使用浮点数时推荐使用双精度。
在要求绝对精确表示的业务场景下,比如金融行业的货币表示,推荐使用整型存储其最小单位的值,展示时可以转换成该货币的常用单位,比如人民币使用分存储,美元使用美分存储。
在要求精确表示小数点 n 位的业务场景下,推荐采用数组保存小数部分的数据。
禁止通过判断两个浮点数是否相等来控制某些业务流程。
在数据库中保存小数时,推荐使用 decimal 类型,禁止使用 float 类型和 double 类型。因为这两种类型在存储的时候,存在精度损失的问题。
1.3 字符集与乱码
ASCII 码
在 ASCII 码中,有两个特殊的控制字符 10 和 13 ,前者是 LF 即 \n ,后者是 CR 即 \r 。
- UNIX:
\n - Windows:
\r\n - 旧版 MacOS:
\r - 新版 Mac OS:
\n
汉字
- 早期使用 GB22312 收录了 6763 个常用汉字。
- GBK (K 表示扩展的意思):支持繁体,兼容 GB2312。
- GB18030 是国家标准,在技术上是 GBK 的超集,并与之兼容。
- Unicode 为每种语言的每个字符都设定了唯一编码,以满足跨语言的交流。
编码格式:UTF-8、UTF-16、UTF-32。UTF(Unicode Transformation Fromat)即 Unicode 字符集转换格式,可以理解为对 Unicode 的压缩方式。
UTF-8 是一种以字节为单位,针对 Unicode 的可变长度字符编码,用 1 ~ 6 个字节对 Unicode 字符进行编码压缩,目的是用较少的字节表示最常用的字符。此规则能有效的降低数据存储和传输成本。
在日常开发中,字符集如果不兼容则会造成乱码。数据库是存储字符之源,在不同层次上都能够设置独立的字符集。为了减少麻烦,所有情况下的字符集设置最好一致。
1.4 CPU 与内存
CPU (Central Processing Unit)是一块超大规模的集成电路板,是计算机的核心部件,承载着计算机的主要运算和控制功能,是计算机指令的最终解释模块和执行模块。
总的来说,CPU 就是由控制器和运算器组成的,内部寄存器使这两者协同更加高效。
1.5 TCP/IP
网络协议
TCP/IP ( Transmission Control Protocol / Internet Protocol )中文译为传输控制协议 / 因特网互联协议,这个大家族中的其它知名协议还有 HTTP、HTTPS、FTP、SMTP、UDP、ARP、PPP、IEEE 802.x 等。 TCP/IP 是当前最流行的网络传输协议框架,从严格意义上讲它是一个协议族,因为 TCP、IP 是其中最为核心的协议,所以就把该协议称为 TCP/IP .
另一个耳熟能详的是 ISO/OSI 的七层传输协议,其中 OSI ( Open System Interconnection )的出发点是想设计出计算机世界通用的网络通信基本框架,它已经被淘汰,本节略过。
- 链路层:链路层以字节为单位把 0 和 1 进行分组,定义数据帧,写入源和目标机器的物理地址、数据、校验位来传输数据。MAC 地址长 6 个字节共 48 位,通常使用十六进制数表示。使用
ifconfig -a命令可以看到 MAC 地址。前 24 位由管理机构统一分配,后 24 位由厂商自己分配,保证网卡全球唯一。网卡就像家庭地址一样,是计算机世界范围内的唯一标识。 - 网络层:根据 IP 定义网络地址,区分网段。子网内根据地址解析协议(ARP)进行 MAC 寻址,子网外进行路由转发数据包,这个数据包即 IP 数据包。
- 传输层:数据包通过网络层发送到目标计算机后,应用程序在传输层定义逻辑端口,确认身份后,讲数据包交给应用程序,实现端口到端口间通信。最典型的传输层协议是 UDP 和 TCP 。 UDP 只是在 IP 数据包上增加端口等部分信息,是面向无连接的,是不可靠传输,多用于视频通信、电话会议等(即使少一帧数据也无妨)。与之相反,TCP 是面向连接的。所谓面向连接,是一种端到端通过失败机制建立的可靠数据传输方式,给人感觉是有一条固定的通路承载着数据的可靠传输。
- 应用层:传输层的数据到达应用程序时,以某种统一的协议格式解读数据。
简而言之,就是按“端口→IP地址→MAC地址”这样的路径进行数据的封装和发送,解包的时候反过来即可。
IP 协议
IP 地址是面向无连接、无状态的,没有额外的机制保证发送的包是否有序到达。 IP 首先规定出 IP 地址格式,该地址相当于在逻辑意义上进行了网段的划分,给每台计算机额外设置一个唯一的详细地址。
为什么还要通过唯一的 IP 地址再来标识?简单地说,在世界范围内,不可能通过广播的方式,从数以千万计的计算机中找到目标 MAC 地址的计算机而不超时。在数据投递时就需要对地址进行分层管理。
IP 地址属于网络层,主要功能在 WLAN 内进行路由寻址,选择最佳路由。IP 协议在 IP 报头中记录源 IP 地址和目标 IP 地址。
IP 报文格式:
- 版本号(4位) + 头长度(4位) + 服务类型TOS(8位) + 总长度(16位)
- 标识(16位) + 标志(3位) + 分段偏移(13位)
- 生存时间TTL(8位) + 挂载协议标识(8位) + 校验和(16位)
- 数据包的生存时间,即 TTL ( Time To Live ),该字段表示 IP 报文被路由器丢弃之前可经过的最多路由总数。 TTL 初始值由源主机设置后,数据包在传输过程中每经过一个路由器 TTL 值则减 1 ,当该字段为 0 时,数据包被丢弃,并发送 ICMP 报文通知源主机,以防止源主机无休止地发送报文。 ICMP ( Internet Control Message Protocol ) 是检测传输网络是否通畅、主机是否可达、路由是否可用等网络运行状态的协议。 ICMP 虽然并不传输用户数据,但是对评估网络健康状态非常重要,经常使用的 ping、tracert 命令就是基于 ICMP 检测网络状态的有力工具。
- 源IP地址(32位)
- 目标IP地址(32位)
- 选项 + 填充
- 数据(TCP 报文或 UDP 报文)
由于不同硬件的物理特性不同,对数据帧的最大长度都有不同的限制,这个最大长度被称为最大传输单元,即 MTU ( Maximum Transmission Unit )。
IP 是 TCP/IP 的基石,几乎所有其它协议都建立在 IP 所提供的服务基础上进行传输,其中包括实际应用中用于传输稳定有序数据的 TCP 。
TCP 建立连接
传输控制协议( Transmission Control Protocol, TCP ),是一种面向连接、确保数据在端到端间可靠传输的协议。面向连接是指在数据传输前,需要先建立一条虚拟的链路,然后让数据在这条链路上“流动”完成传输。
为了确保数据的可靠传输,不仅需要对发出的每一个字节进行编号确认,校验每一个数据包的有效性,在出现超时情况时进行重传,还需要通过实现滑动窗口和拥塞控制等机制,避免网络状况恶化而最终影响数据传输的极端情形。
每个 TCP 数据包是封装在 IP 包中的,每个 IP 头的后面紧接着的是 TCP 头。
TCP 报文格式:
- 源机器端口号(16位) + - 目标机器端口号(16位)
- 这两个端口号与 IP 报头中的源 IP 地址和目标 IP 地址所组成的四元组可唯一标识一条TCP连接。
- 序列号(32位)
- 所发送数据包中数据部分第一个字节的序号。
- 确认序号(32位)
- 期望收到来自对方的下一个数据包中数据部分第一个字节的序号。
- 头部长度(4位) + - 保留(6位) + 标志位:SYN、ACK、FIN、PSH、RST、URG(每个标志1位,共6位) + 本方滑动窗口大小(16位)
- SYN:Synchronize Sequence Numbers 用作建立连接时的同步信号;
- ACK:Acknowledgement 用于对收到的数据进行确认,所确认的数据由确认序列号标识;
- FIN:Finish 标识后面没有数据需要发送,通常意味着所建立的连接需要关闭。
- 校验和(16位) + 紧急指针(16位)
- 选项(发送方最大报文段长度和扩大接收方滑动窗口)
- 数据(最大荷载是由网络设备的 MTU( Maxinum Transmission Unit ) 决定的)
TCP 报头中其它字段可以阅读 RFC793 来掌握。
TCP 建立连接的过程
- A 机器发出一个数据包并将 SYN 置为 1.标识希望建立连接。这个包中的序列号假设为 x 。
- B 机器收到 A 机器发过来的数据包后,通过 SYN 得知这是一个建立连接的请求,于是发送一个响应并将 SYN 和 ACK 标记都置 1 。假设这个包中的序列号是 y ,而确认序列号必须是 x + 1 ,表示收到了 A 发过来的 SYN 。在 TCP 中, SYN 被当作数据部分的一个字节。
- A 收到 B 的响应包后需进行确认,确认包中将 ACK 置 1,并将确认序列号设置为 y + 1 ,表示收到了来自 B 的 SYN 。
三次握手的主要目的是为了:信息对等和防止超时,同时也防止出现请求超时导致脏连接。
从编程的角度讲, TCP 连接的建立是通过文件描述符( File Descriptor, fd )完成的。通过创建套接字获得一个 fd ,然后服务端和客户端需要基于所获得的 fd 调用不同的函数分别进入监听状态和发起连接请求。由于 fd 的数量讲决定服务端进程所能建立连接的数量,对于大规模分布式服务来说,当 fd 不足时就会出现 “open too many files” 错误而使得无法建立更多的连接。为此,需要注意调整服务端进程和操作系统所支持的最大文件句柄数。通过使用 ulimit -n 命令来查看单个进程可以打开文件句柄的数量。如果想查看当前系统各个进程产生了多少句柄,可以使用如下的命令:
lsof - n |awk '{print $2}' |sort|uniq -c |sort -nr|more
想知道具体的 PID 对应的具体应用程序是谁,使用如下命令即可:
ps -ax|grep [pid]
TCP 在协议层面支持 Keep Alive 功能,即隔段时间通过向对方发送数据表示连接处于健康状态。
TCP 断开连接
TCP 是全双工通信,双方都能作为数据的发送方和接收方,但 TCP 连接也会有断开的时候。断开连接需要四次挥手。
- A 机器传递 FIN 信号给 B 机器;
- B 机器应答 ACK ;
- B 机器处理完数据,并做好连接关闭前的准备工作后,发送 FIN 给 A 机器;
- A 机器应答 ACK 。
此时 A 机器处于 TIME_WAIT 状态,经过 2MSL (Maximum Segment Lifetime)后,没有收到 B 机器传来的报文,则确定 B 机器已经接收到 A 机器最后发送的 ACK 指令,此时 TCP 连接正式释放。
在 RFC793 中规定 MSL 为 2 分钟。
因为 TIME_WAIT 状态无法真正释放资源,在此期间 Socket 中使用的本地端口在默认情况下不能再被使用。该限制对于高并发服务器来说,会极大的限制有效连接的创建数量,称为性能瓶颈。所以,建议将高并发服务器的 TIME_WAIT 超时时间调小。
在服务器上通过变更 /etc/sysctl.conf 文件来修改该缺省值(秒): net.ipv4.tcp_fin_timeout = 30 (建议小于 30 秒为宜)。
修改后执行 /sbin/sysctl -p 让参数生效即可。
连接池
服务器可以快速创建和断开连接,但对于高并发的后台服务器而言,连接的频繁创建与断开,是非常重的负担。在客户端和服务器之间可以事先创建若干连接并提前放置在连接池中,需要时可以从连接池直接获取,数据传输完成后,将连接归还至连接池中,从而减少频繁创建和释放连接所造成的开销。
重点提一下数据库连接池,连接资源在数据库端是一种非常关键且有限的系统资源。连接过多往往会严重影响数据库性能。数据库连接池负责分配、管理和释放连接,是一种以内存空间换取时间的策略,能够明显地提升数据库操作和性能。
以 Druid 为例, Druid 是阿里巴巴地一个数据库连接池开源框架,准确来说它不仅仅包含数据库连接池,还提供了强大地监控和扩展功能。当应用启动时,连接池初始化最小连接数(MIN);当外部请求到达时,直接使用空闲连接即可。假如并发数达到最大(MAX),则需要等待,直到超时。如果一直未拿到连接,就会抛出异常。
- 如果 MIN 过小,可能会出现过多请求排队等待获取连接;
- 如果 MIN 过大,会造成资源浪费;
- 如果 MAX 过小,则峰值情况下仍有很多请求处于等待状态;
- 如果 MAX 过大,可能导致数据库连接被占满,大量请求超时,进而影响其它应用,引发服务器连环雪崩。
另外,连接数地创建受到服务器操作系统地 fd (文件描述符)数量限制。创建更多地活跃连接,就需要消耗更多地 fd ,系统默认单个进程可同时拥有 1024 个 fd,该值虽然可以调整,但如果无限制增加,会导致服务器在 fd 维护和切换上消耗过多的精力,从而减低应用吞吐量。
一般可以把连接池的最大连接数设置在 30 个左右,理论上还可以设置的更大,但是 DBA 一般不会允许,因为往往只有出现了慢 SQL ,才需要使用更多的连接数。这时候需要优化应用层逻辑或创建数据库索引,而不是一味地采用加大连接数这种治标不治本的做法。
从经验上来看,在数据库层面的请求应答时间必须在 100ms 以内,秒级的 SQL 查询通常存在巨大的性能提升空间。
- 建立高效且合适的索引。
要事先明确业务场景,建立合适的索引。 - 排查连接资源未明显关闭的情形。
要特别注意在ThreadLocal或流式计算中使用数据库连接的地方。 - 合并短的请求。
- 合理拆分多个表 JOIN 的 SQL ,若是超过三个表则禁止 JOIN 。
对于需要 JOIN 的字段,数据类型应保持绝对一致。
多表关联查询时,应确保被关联的字段需要有索引。 - 使用临时表。
某种情况下,该方法是一种比较好的选择。 - 应用层优化。
包括进行数据结构的优化、并发多线程改造等。 - 改用其他数据库。
不同的数据库针对的业务场景不同。
1.6 信息安全
黑客与安全
黑客是音译词,译自 Hacker 。现代黑客攻击的特点是分布式、高流量、深度匿名。现今云端提供商的优势在于提供一套完整的安全解决方案。小企业要从头到尾地搭建一套安全防御体系,技术成本和资源成本将是难以承受的。
完整的信息安全体系遵循 CIA 原则:
- 保密性:对需要保护的数据进行保密操作,无论是存储还是传输,都要保证用户数据及相关资源的安全。还要防止从内部窃取数据,用户敏感信息不以明文方式存储。
- 完整性:访问的数据需要是完整的,而不是缺失的,或者被篡改的,不然用户访问的数据就是不正确的。通常的做法是对数据进行签名和校验。
- 可用性:服务需要是可用的。如果连服务都不可用,也就没有安全这一说了。通常使用访问控制、限流、数据清洗等手段解决。
SQL 注入
SQL 注入是注入式攻击中的常见类型。SQL 注入式攻击是未将代码与数据进行严格的隔离,导致在读取用户数据的时候,错误地把数据作为代码的一部分执行,从而导致一些安全问题。
- 过滤用户输入参数中的特殊字符,从而降低被 SQL 注入的风险。
- 禁止通过字符串拼接的 SQL 语句,严格使用参数绑定传入的 SQL 参数。
- 合理使用数据库访问框架提供的防注入机制。
比如 MyBatis 提供的#{}绑定参数,从而防止 SQL 注入。同时谨慎使用${},这相当于直接使用字符串拼接 SQL 。
XSS 与 CSRF
跨站脚本攻击( Cross-Site Scripting ),为了不和 CSS 冲突,简称为 XSS 。 XSS 是指通过技术手段,向正常用户请求的 HTML 页面中插入恶意脚本,从而可以执行任意脚本。 XSS 主要分为反射型XSS、存储型XSS 和 DOM型XSS 。 XSS 主要用于信息窃取、破坏等目的。
从技术原理上看,后端 Java 开发人员、前端开发人员都有可能造成 XSS 漏洞。
在防范 XSS 上,主要通过对用户输入数据做过滤或者转义。比如 Java 开发人员可以使用 Jsoup 框架对用户输入字符串做 XSS 过滤,或者使用框架提供的工具对用户输入的字符串做 HTML 转义,例如 Spring 框架提供的 HtmlUtils 。前端浏览器展示数据时,也需要使用安全的 API 展示数据,比如使用 innerText 而不是 innerHTML 。
除了开发人员造成的漏洞,近年来出现了一种 Self-XSS 的攻击方式。 Self-XSS 是利用部分非开发人员不懂技术,黑客通过红包、奖品或优惠券等形式,诱导用户复制攻击者提供的恶意代码,并粘贴到浏览器的 Console 中运行,从而导致 XSS 。 由于 Self-XSS 属于社会工程学攻击,技术上目前尚无有效防范机制,因此只能通过在 Console 中展示提醒文案来阻止用户执行未知代码。
CSRF
跨站请求伪造( Cross-Site Request Forgery ),简称 CSRF,也被称为 One-click Attack ,即在用户并不知情的情况下,冒充用户发起请求,在当前已登录的 Web 应用程序上执行恶意代码,如恶意发帖、修改密码、发邮件等。
CSRF 有别于 XSS ,从技术原理上两者有本质的不同,XSS 是在正常用户请求的 HTML 页面中执行了黑客提供的恶意代码; CSRF 是黑客直接盗用用户浏览器中的登录信息,冒充用户去执行黑客指定的操作。 XSS 问题出在用户数据没有过滤、转移; CSRF 问题出在 HTTP 接口没有防范不受信任的调用。
防范 CSRF 主要通过以下方式:
- CSRF Token 验证:利用浏览器的同源限制,在 HTTP 接口执行前验证页面或者 Cookie 中设置的 Token ,只有验证通过才继续执行请求。
- 人机交互:比如在调用接口时验校验短信验证码。
HTTPS
HTTPS 的全称是 HTTP over SSL ,简单的理解就是在之前的 HTTP 传输上增加了 SSL 协议的加密能力。 SSL(Secure Socket Layer, 安全套接字层)协议工作于传输层和应用层之间,为应用提供数据的加密传输。
RSA 算法把密码革命性的分成公钥和私钥,由于两个密钥并不相同,所以称为非对称加密。私钥是用来对公钥加密的信息进行解密的,是需要严格保密的。公钥是对信息进行加密,任何人都知道。
非对称加密的安全性是基于大质数分解的困难性,在非对称加密中,公钥和私钥是一对大质数函数。计算两个大质数的乘积是简单的,但是这个过程的逆运算(即将两个乘积分解为两个质数)是非常困难的。而在 RSA 算法中,从一个公钥和密文中解密出明文的难度等同于分解两个大质数的难度。因此在传输过程中,可以把公钥发给对方。一方发送信息时,使用另一方的公钥进行加密生成密文。收到密文的以防再用私钥进行解密,这样一来,传输就相对安全了。
但是非对称加密并不是完美的,它有一个很明显的缺点是加密和解密耗时长,只适合对少量数据进行处理。
HTTPS 使用非对称加密的方式传输密钥来建立安全的 SSL 连接。
- 甲告诉乙,使用 RSA 算法进行加密。乙说,好的。
- 甲和乙分别根据 RSA 生成一对密钥,互相发送公钥。
- 甲使用乙的公钥给乙加密报文信息。
- 乙收到信息,并用自己的私钥进行解密。
- 乙使用同样方式给甲发送信息,甲使用相同方式进行解密。
但是在第 2 步中,若甲发往乙的公钥请求被拦截,则可以伪装成甲发送一个自己的公钥给乙。若乙信了,则可能会把一些敏感信息发送给中间的拦截者。这种情况就导致了信任危机的发生。为了解决这个问题,CA(Certificate Authority)应运而生。 CA 就是颁发 HTTPS 证书的阻止。
访问一个 HTTPS 网站的大致流程如下:
- 浏览器向服务器发送请求,请求中包括浏览器支持的协议,并附带一个随机数。
- 服务器收到请求后,选择某种非对称算法,把数字证书签名公钥、身份信息发送给浏览器,同时也附带一个随机数。
- 浏览器收到后,验证证书的真实性,用服务器的公钥发送握手信息给服务器。
- 服务器解密后,使用之前的随机数计算出一个对称加密的密钥,以此作为加密信息并发送。
- 后续所有的信息发送都是以对称加密方式进行的。
TLS ( Transport Layer Security ) 传输层安全协议
TLS 和 SSL 的区别: TLS 可以理解成 SSL 协议 3.0 版本的升级,所以 TLS 的 1.0 版本也被标志为 SSL 3.1 版本。但对于大的协议栈而言, SSL 和 TLS 并没有太大的区别,因此在 Wireshark 里,分层依然用的是安全套接字层(SSL)标识。
在整个 HTTPS 的传输过程中,主要分为两个部分:
- HTTPS 的握手:建立一个 HTTPS 的通道,并确定连接使用的加密套件即数据传输使用的密钥。
- 数据的传输:使用密钥对数据加密并传输。
HTTPS 握手流程:
- 客户端发送一个 Client Hello 协议的请求:在 Client Hello 中最重要的信息是 Cipher Suites 字段,这里客户端会告诉服务器自己支持哪些加密的操作。
- 服务端在收到客户端发来的 Client Hello 的请求后,会返回一系列的协议数据,并以一个没有数据内容的 Server Hello Done 作为结束。这些协议有的是单独发送,有的则是合并发送。
几个比较重要的协议:- Server Hello 协议:主要告知客户端后续协议中要使用的 TLS 协议版本,这个版本主要和客户端与服务端支持的最高版本有关。此外,还会确认后续采用的加密套件(Cipher Suites)。
- Certificate 协议:主要传输服务端的证书内容。
- Server Key Exchange:如果在 Certificate 协议中未给出客户端足够的信息,则会在 Server Key Exchange 进行补充。
- Certificate Request:这个协议是一个可选项,当服务端需要对客户端进行证书验证的时候,才会向客户端发送一个证书请求( Certificate Request )。
- 最后以 Server Hello Done 作为结束信息,告诉客户端整个 Hello Done 过程结束。
- 客户端在收到服务端的握手信息后,根据服务端的请求,也会发送一系列的协议。
- Certificate:它是可选项。因为上文中服务端发送了 Certificate Request 需要对客户端进行验证,所以客户端需要发送自己的证书信息。
- Client Key Exchange:它与上文中的 Server Key Exchange 类似,是对客户端 Certificate 信息的补充。
- Certification Verity:对服务端发送的证书信息进行确认。
- Change Cipher Spec:该协议不同于其他握手协议( Handshake Protocol ),而是作为一个独立协议告知服务端,客户端已经接收之前服务端确认的加密套件,并会在后续通信中使用该加密套件进行加密。
- Encrypted Handshake Message:用于客户端给服务端加密套件加密一段 Finish 的数据,用以验证这条建立起来的加解密通道的正确性。
- 服务端在接收客户端的确认信息及验证信息后,会对客户端发送的数据进行确认,这里也分为几个协议进行回复。
- Change Cipher Spec:通过使用私钥对客户端发送的数据进行解密,并告知后续将使用协商好的加密套件进行加密传输数据。
- Encrypted Handshake Message:与客户端的操作相同,发送一段 Finish 的加密数据验证加密通道的正确性。
- 如果客户端和服务端都确认解密无误后,各自按照之前约定的 Session Secret 对 Application Data 进行加密传输。
1.7 编程语言的发展
从编程语言类型的角度,可划分为三个编程语言时代:
第一代,机器语言时代
机器语言的优点是可以直接对芯片进行指令操作,最大的问题也来源于此。不同的硬件环境,机器语言也并不相同。另外,指令不便于记忆,语言的生产率非常低。
汇编语言本质上同机器语言处于同一个时代,只是在与机器指令对应的字符编程方式,以及助记符之上增加了编译功能。
第二代,高级语言时代
无论是面向过程,还是面向对象,都是面向问题编程,不是描述计算机具体应该执行什么样的分步操作,而是更倾向于描述需要解决的问题本身。
面向过程更多描述的是解决问题的步骤,在实际步骤中协调各个参与方达成最后的目标;
面向对象是抽象问题各方的参与者,包括领域对象、问题域、运行环境等,然后定义各个参与者的属性与行为,最后合力解决问题。
第三代,自然语言时代
自然语言编程是面向思维或模糊语义的编程方式,软件生产只是思考问题本身的存在性和合理性,而不是定义问题的解决方式和解决步骤。这个时代很遥远,但很唯美。相信随着 AI 科技的不断进步,一定会实现。
优秀的程序员至少需要掌握 3 门语言,这有助于知晓不同语言的各自特性,更重要的是洞悉语言的共性和编程语言思想,跨越语言的抽象思维和架构掌控力。但是掌握不等于精通,真正的大师,需要醉心在某种语言,不断研究、不断打磨、不断回炉,才能达到炉火纯青、登峰造极的境界。