一个程序员的自我修养

基本修养:以太网与 TCP/IP

发布于 2019年10月31日    原创作品,转载请注明来源

以太网(Ethernet)

以太网是一套标准,制定了相当于 OSI 模型 中第一层(物理层)和第二层(数据链路程)的技术规范。

在物理层上,以太网采用 RJ45 接口和双铰线,光纤,电磁波等方式来传递信号。

在数据链路层上,每个通信节点(主机的网络接口)都有 48 位(bit)全局唯一的 MAC 地址。通信数据流被切分并打包成帧(Frame)来发送,每帧都包含来源节点和目的节点的 MAC 地址。

网段(Network Segment)

中继器,集线器是和网线一样工作在物理层的硬件设备。被它们连接在一起的部分被称为网段(日常所说的网段,意义可能与本文中不同,一般指子网)。一个网段是一个冲突域,每次通信时,数据帧会被发送到网段中所有的节点,但只有目的节点(由帧中的目的节点MAC定义)会处理该帧,其它节点则将其忽略。

中继器(Repeater)用于延长网络传输的有效距离。

集线器(Hub)可被视为多口的中继器,用于接通多个节点。由于处于同一冲突域,节点间的通信会互相影响,产生性能问题,现在集线器基本上已经被交换机取代了。

网络(Network)

网桥,交换机是工作在数据链路层的硬件设备,被它们连接在一起的部分被称为网络。网络是一个广播域,其中各个节点都能通过数据链路层直接相互通信。

网桥(Bridge)用于连接两个网段,可以过滤不同网段间的流量。

交换机(Switch)可被视为多口的网桥。它可以动态地为通信双方构造一个单独的网段,保障通信不受其它节点的干扰。网桥和交换机的端口不需要有 MAC 地址。

网际协议(IP, Internet Protocol)

网际协议(IP)也称互联网协议, 位于 OSI 第三层,网络层。它负责在不同的网络中的节点之间传输数据。

在 IP 中,节点拥有IP 地址,数据被打包成数据报(Datagram),通过网络间的路由过程,从一个地址传输另外一个地址。数据报头部包含了来源及目的地的 IP 地址。

IPv6 是 IP 协议的新版本,最终目标是取代 IPv4。它带来了更大的地址空间,更方便的配置管理,更高效的网际路由,更安全的通信技术。

IP 地址

IP 地址是一个数字,用于对 IP 网络中的节点进行标识和寻址。

IPv4 地址是 32 位(bit)无符号整数。为方便记忆和沟通,通常把这 32 位划分为 4 组 8 位,分别写成一个十进制整数(其中每个整数的范围在 0 ~ 255 之间),用小数点连接起来。如地址 3232248321 可写为 192.168.50.1

IPv6 地址是 128 位无符号整数。通常划分为 8 组 16 位,分别写成一个十六进制整数(其中每个整数的范围在 0000 ~ ffff 之间),用冒号连接起来。如地址 42540766464534556858822563802705297408 可写为 2001:0DB8:AC10:FE01:0000:0000:0000:0000。这种写法可以按照标准进行简化:字母写成小写,省略每组中的前导 0 ,多个连续的值为 0 的组可以用两个冒号代替(但是一个 IP 地址中只有最长的连续 0 组能代替一次)。按这样的规则,上面的地址可以简化为:2001:db8:ac10:fe01::

单个 IP 地址可划分为两部分,前一部分是网络标识,后一部分是主机标识。

网络ID

为正常通信,同一个网络内的节点,其 IP 地址中的网络标识应该相同,这部分被称为网络ID。因为出现在主机标识的前方,所以也称为网络前缀。同时,它在路由的过程中使用到了,因此又称为路由前缀。同一网络内,主机标识不应重复。

在 IPv4 中网络 ID 的长度可以用网络掩码描述,如掩码 255.255.0.0 的代表是一个前 16 位为 1,后面为 0 的 32 位数字,因而它描述的网络 ID 长度为 16 位。

网络 ID 的长度也可以使用 CIDR 形式描述。这种格式包含了 IP 地址和网络 ID 长度,如 192.168.1.0/24 代表 ID 长度为 24 位。CIDR 格式可用于表示单个 IP 地址配置, 也可以代表一个 IP 地址块。

在 IPv6 中,表示网络信息只能使用 CIDR 格式,不能使用掩码格式。

子网(Subnet)

大部分情况下,子网等同于网络。子网有时也被人称为“网段”。

在大型网络中,节点较多,出于性能,安全或管理方面的原因,可以将一个网络划分为多个较小的子网络。这种场景下才有必要区分子网与网络。子网的 ID 以网络的 ID 为起始,后面包含额外的若干位。如网络的 ID 为 200.100.0.0,子网的 ID 可以为 200.100.1.0200.100.2.0 等。VLAN就是一种子网划分技术。

子网本身也是单独的网络。划分为子网后,不同子网间的通信需要使用到路由器,或三层交换机等网关设备。

地址解析协议(ARP, Address Resolution Protocol)

ARP 用于在网络层地址(IPv4 地址)和链路层地址(以太网中就是 MAC 地址)间进行翻译。它工作在二层和三层之间,如果一定要安排到七层之中的话,ARP 只能算二层协议。

以太网中两个节点通信需要知道对方的 MAC 地址。因此,每个节点会保存一个缓存的 ARP 表,记录已知的 MAC 地址和 IP 地址的对应关系。需要与同网某个 IP 通信时,如果缓存表中无法找到对应的 MAC 地址,节点就会发出一条 ARP 请求,广播到网络中所有的节点。该 IP 对应的节点会进行回复,原节点根据回复提供的 MAC 地址继续通信,同时将信息记入缓存表。除了这样的请求应答方式以外,每个节点也可以主动发送广播,声明自己的 IP 和 MAC 地址,以更新其它节点的缓存表。

ARP 不对各个节点进行身份验证,因此可能产生 ARP 欺骗问题,即某节点假装自己是其它节点,进行信息窃取或欺骗;或实施拒绝服务攻击。对应的解决方案可以是静态配置 MAC/IP 对应关系,或者缩小网络的范围(如划分成子网)等。

IPv6 中,邻居发现协议(NDP, Neighbor Discovery Protocol)取代了 ARP。NDP 中区分了路由器和普通节点。它不仅能在 IP 地址和链路层地址间进行翻译,还可以为节点配置网络参数如IP地址,网络ID,DNS 等(SLAAC, Stateless address autoconfiguration)。

路由(Routing)

同一个网络内的通信,使用工作于OSI 模型二层及以下的网络硬件进行传输,如交换机,集线器,中继器,网桥等。

跨网络的通信,根据 IP 协议的规定,需要逻辑上的网关的参与。网关同时连接到本地网络和外部网络,且拥有两个分别属于不同网络的 IP 地址。每台主机的网络协议软件栈中都包含一个路由表,用于配置路由规则,即哪些接收方使用哪个网络接口,发送到哪个网关。当所有的路由规则都不匹配时,数据会被发送到默认网关。网关全权负责对网络外部的通信。现实场景下,网关设备有可能进行网络地址翻译(NAT)。如果不同的网络间数据传输方式有区别,网关设备还负责将数据翻译为对应的格式。调制解调器(俗称猫),如ADSL猫,光猫等,都属于网关设备。

路由器是一种多口网关。网关与路由器的关系,类似于网桥与交换机的关系,或中继器与集线器的关系。集线器直接转发所有的信号,交换机根据二层地址(MAC 地址)进行转发,路由器则根据三层地址(IP 地址)来转发。和普通的主机一样,它也配置有路由表。常见的单 WAN 口家用路由器,实际上是双口路由器+交换机+其它功能的集成设备。

TCP 与 UDP

这是两种基于 IP 协议的传输层协议。

用户数据报协议(UDP, User Datagram Protocol)是 IP 层的简单封装。它提供了可选的数据错误检测机制,还增加了端口概念,以区分同一主机上的不同应用或会话。与底层的 IP 一样,它不保证数据的是否重复,乱序或丢失。

传输控制协议(TCP, Transmission Control Protocol) 提供了可靠的,有序的数据流传输服务。它也同样提供端口概念。TCP 将数据流切分包装成段(Segment,也称包, Packet),交给 IP 传输。TCP 通过确认,重传,重新组合等方式,解决数据段丢失,重复,乱序等问题,将接收到的数据重新整合为数据流,提供给应用层协议。TCP 面向连接,因此通信前,双方需要先建立连接。

这两种协议中,双方各自通过一个端口与对方通信。端口使用 16 位正整数(范围 0 ~ 65535)来标识,称为端口号。服务器主动侦听固定的端口,客户端通常不关注自己的端口号,因此经常使用临时端口。

动态主机设置协议(DHCP, Dynamic Host Configuration Protocol)

DHCP 用于动态配置IPv4网络节点。它是一个应用层协议,基于传输层协议 UDP。

节点加入网络时,可以使用手工配置的网络和节点信息(IP 地址,网络ID,DNS 等)。也可以根据 DHCP 协议,临时用一个空IP(0.0.0.0)以 UDP 协议向网络广播特定的请求消息。DHCP 服务器会直接将回复关联到节点的 MAC 地址(这个过程实际上也是跨越协议层次工作的)。节点获得回复后即可根据配置信息进行正常的通信了。

IPv6 中,相关功能主要由 NDP 完成,但是也可以同时使用 DHCPv6 来提供更多其它信息。

网络地址翻译(NAT, Network Address Translation)

NAT 是网关在传输数据的过程中改写其中发送方/接收方 IP 地址(很可能还有端口号)的技术,被大规模应用于缓解 IPv4 地址空间不足的问题。

处于内网的主机,需要与外界通信时,发出的数据包通过拥有外网地址的网关。网关将包中的发送方地址替换为自己的外网地址,同时还可能将端口号替换为新的端口号,然后再转发出去,接收方只能看到替换后的地址和端口号。网关会记录发送方的内网地址,更改前后的端口号,以及接收方的地址和端口号。收到对方回复时,网关根据记录的信息,将数据包中的接收方IP(目前是网关的外网IP)和端口号替换为内网IP和原发起端口,然后转发回内网主机。

通常,处于内网的主机只能作为客户端发起通信,无法作为服务器被动等待外界通信。除非网关配置了端口转发,将外网 IP 某些端口的通信转发给它。网关也有可能配置了 DMZ 主机选项,所有来自外网且未明确指定转发规则的端口通信,都会转发给 DMZ 主机。如果用户从 ISP (电信运营商)那里只能得到内网地址,这意味着 NAT 网关由 ISP 管理 ,此时用户无法配置端口转发等选项,设备也无法作为服务器使用。

Windows 中的公用/专用网络

Windows 中可以把单个网络设置为“公用”或“专用”等不同的类型,并针对各个型执行不同的防火墙规则。

这里的公用网络并不代表外网。和专用网络一样,它也是由二层设备连接起来的网络,只是任何人都可能连接进来,不像家庭/工作网络那样用户相对固定。由于公用网络上更有可能存在恶意节点,所以安全规则需要比在专用网上更严密。

Windows 用以区分不同网络的依据是 DNS 域名后缀和默认网关的 MAC 地址。

基本修养:字符与编码

发布于 2019年9月12日    原创作品,转载请注明来源

字符集和编码

字符集(Character Set)是字符的集合,定义系统能处理哪些字符;编码(Encoding)则规定这些字符在计算机内部的表示方式。

这里字符是抽象的概念,编码将其与二进制数据进行映射。由于编码通常依赖于字符集,实践中两者经常是绑定或互指的。常见的汉字编码方案 GB2312,其全名为《信息交换用汉字编码字符集·基本集》;而 HTML 中的 <meta charset="encoding"> 标签也混用了字符集(charset)和编码的概念。

既然是集合,字符集就会有超集和子集。如 1995 年发布的 GBK(《汉字内码扩展规范》),就是 GB2312 (发布于 1980 年)的超集;而 2000 年发布的 GB18030(《信息技术 中文编码字符集》)又是 GBK 的超集。

代码页

代码页就是字符集加上编码。

Windows 提供了很多代码页选项,用于支持不同的语言文字。每个代码页有一个编号,简体中文对应的编号为 936,其对应的字符集为 GBK。你可以在命令行中执行命令reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage /f ACP,查看系统当前设置的代码页编号:

C:\>reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage /f ACP

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage
    ACP    REG_SZ    936

搜索结束: 找到 1 匹配。

C:>

代码页是 Windows 系统的术语。在 Linux 系统中,类似的概念是 locale,不过当前 Linux 一般默认编码方式都是 utf-8,对应的字符集是 Unicode。

ANSI 编码

ANSI 编码代表 Windows 系统中当前代码页对应的编码,亦即系统默认编码。

这个术语经常在 Windows 平台使用,但实际上是被误用的。理论上,它应该等同于 ANSI 在 1986 年发布的 US-ASCII。现实中,它可以代表任意编码,甚至可以与 ASCII 完全不兼容,只要设置为系统默认编码。

Unicode

Unicode 是一套标准,包含多语言统一的字符集及其相关编码,以及在这个字符集上进行文本处理的相关规则。

Unicode 当前版本(12.1)共规定了 137,929 个字符。它们不仅囊括了当前全球使用的主要语言文字(如拉丁字母,阿拉伯文字,简繁汉字等),还包含了很多符号(货币符号,标点符号,数学符号,几何图形,emoji等),甚至还有仅在史料上使用的文字(如楔形文字,埃及象形文字等)。

在 Unicode 中,每个字符被分配了一个数值(Code Point,代码点)和一个名称。比如字母A的名称是LATIN CAPITAL LETTER A(大写拉丁字母A)。它对应的数值是 65,通常写作 U+0041(41是十六进制数,等于10进制的65)。除此之外,Unicode 还定义了各个字符的一系列属性,比如是否是大写字母,是否代表数字,书写方向(左到右还是右到左),宽度(半角还是全角)等。基于这些属性,Unicode 提供了大小写转换,文本换行,双向书写显示等相关算法。

Unicode 字符集被分为十七个子集(Plane,平面或位面),每个子集最多可包含 65536 个字符,因此总共可以有 1,114,112 个字符。其中第一个子集(Plane 0)包含最常用的字符,被称为 BMP(Basic Multilingual Plane, 基本多文种平面)。 BMP 中为 UTF-16 中的代理对(Surrogate Pair)保留了 2048 个位置,只剩下 63488 个有效字符空间(因此 Unicode 中实际最多有1,112,064个字符)。BMP 中的字符可以用四位十六进制数(U+xxxx)表示,其它的字符需要五位或更多。

UTF-8, UTF-16, UTF-32

这些是 Unicode 标准中规定的几种编码方式。UTF 是 Unicode Transformation Format 的缩写。

UTF-8 是一种变长编码,以字节为基本单位,单个字符占用的字节数可能是 1 (U+0000 ~ U+007F,128个位置,所有的 ASCII 字符),2(U+0080 ~ U+07FF, 1920个位置,主要是各种字母和符号),3(U+0800 ~ U+FFFF,63488个位置,BMP中所有其它字符,包括绝大部分常用汉字)或4(U+10000 ~ U+10FFFF,1048576个位置,所有其他字符)。 UTF-8 的主要优势在于兼容 ASCII,面向字节因而无需考虑字节顺序。现在 UTF-8 是互联网上使用率最高的编码方式。

UTF-16 是另一种变长编码,以 16 位(bit)为基本单位,单个字符可占用单位数可能为 1 (BMP 中的所有字符)或 2(所有其它字符,这两个单位被称为代理对)。相对于另外两种编码,它最大的优势是存储大量东亚文本(中日韩)时占用空间较少。由于基本单位不是字节,而是 16 位,不同的系统通信时需要考虑字节序(Byte Order)问题。Windows, JavaScript, Java 等内部使用了 UTF-16。

UTF-32 (又称 UCS-4) 是一种定长编码,每个字符使用 32 位来表示。它的优势在于实现和处理简单,劣势在于空间效率低。它同样需要考虑字节序问题。

在 Unicode 标准早期版本中,唯一的编码方式是 16 位定长编码(UCS-2),Windows 和 JavaScript 等采用了这种简单高效的标准。后来 Unicode 字符集扩充,16 位空间不足以容纳所有的字符,不得不产生了 UTF-16 的代理对机制,定长编码尴尬地转变成了变成编码。而 Linux 由于反应较慢,躲过一劫,后来逐渐转向了 UTF-8 编码。

字节序 (Byte Order)

不同系统中处理数值时可能采用的大小端序(Endianness)不同。比如在小端序(Little-endian)的机器上,32 位数字 1 在内存中表示为 4 个字节:01 00 00 00;而在大端序(Big-endian)的机器上则表示为 00 00 00 01。常见的 x86/x64, ARM 等处理器架构使用小端序,OpenRISC, SPARC等则使用大端序。不同系统间通信时,或读取其它系统存储的文件时,需要考虑字节顺序问题。

Unicode 编码中可以使用字符 U+FEFF 来作为字节序标记(Byte Order Mark),需要时在字符序列前添加该字符。该字符本身不被视为文本的一部分,但通过分析它的表示方式,处理程序可以判断编码时的字节序,进而做出相应的处理。U+FEFF 名称为ZERO WIDTH NO-BREAK SPACE(零宽度非间断空白),原本是有实际意义的。后来这个意义被新字符 U+2060 (WORD JOINER) 取代,U+FEFF 现在仅用于标明字节序。

编码 大端字节序标记 小端字节序标记
UTF-8 EF BB BF EF BB BF
UTF-16 FE FF FF FE
UTF-32 00 00 FE FF FF FE 00 00

UTF-8 编码的基本单位是字节,无需考虑字节序问题。尽管可以添加字节序标记 EF BB BF(字符 U+FEFF 在 UTF-8 中的表示方式),但标准推荐仅在特殊情况下使用。Windows 下记事本保存为 UTF-8 格式时,会自动添加 BOM,用以与系统默认编码(即记事本所称的 ANSI编码)区分。而 Linux/macOS 等其它操作系统上的软件,一般只支持无字节序标记的 UTF-8 编码文件,这样导致文件交换出现不少问题。 从 Windows 10 v1903 起,记事本也在保存为 UTF-8 编码时默认不附加字节序标记了。

字素(Grapheme)与字素簇(Grapheme Cluster)

字素是文本在书写时最小的单位,可以被理解为单独的“字”。

在 Unicode 标准中,字符(Character)一般指代码点(Code Point)。通常,一个字素就是一个字符。但是,也有些字素是由多个字符序列组合而成的,这样的字符序列被称为字素簇。比如字母 é 可以用字母 e (U+0065) 加上重音符(U+0301) 组合而成。像重音符这样用于修饰前一个字符的字符,被称为组合字符(Combining Character)。可以使用多个组合字符来修饰同一个字符,这就是有一段时间内各个社区很流行的越界文字的技术根源。

超̷̪͓̫͕̳̝̔͐̋͌͑͗́̒̕͟͞越͓̻̗̙̙̠͖̔̆̌͑͐̽̊代̷͉̘̲̺̤͈̀͑̒͗̄͘̕͜码̵̨̟͖͎̉̿͌͜͞͞͞,越界文字̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗

如同前面的例子所示,含重音符的拉丁字母可以使用基本字符加上重音修饰字符来表示。为保持与旧软件系统的兼容性,这种情况下 Unicode 中实际还包含了预先组合好的单个字符。即,某些字素可以有多个表示方式。上面的 é 既可以用字符序列 U+0065 U+0301 表示,也可以用单个 U+00E9 表示。这样也带来了新的问题,在字符串比较,排序等操作前需要首先进行正规化(Normalization)。正规化即把所有可用单个字符表示的字符序列替换为对应的单个字符。