在现代社会网络通信的隐私和安全性已成为人们日益关注的重要话题，无论是浏览购物网站、在线银行交易，还是日常的社交媒体互动，我们的个人信息都面临着潜在的风险和威胁。在这篇文章中，将探讨在浏览购物网站时，如何保护我们的隐私和确保信息安全，是谁在保护我们的数据隐私和通信的安全性。在谈论网络通信的安全性之前，让我们回顾一下人类在通信保密性的发展史。通信的保密性自古以来就是一个重要的问题，人类不断发展新的方法来保护信息的隐私和安全，最早的加密技术可以追溯到古代文明，古希腊和罗马使用了各种简单的加密方法，如凯撒密码（Caesar Cipher），其中每个字母被替换为其后固定位置的字母。凯撒密码也称为移位密码（Shift Cipher），是以古罗马将军 Julius Caesar 的名字命名的，据说凯撒在军事通信中使用这种密码来加密敏感信息。在战争期间使用这种加密方法来保护军事命令和机密信息，确保即使信息被截获，敌人也无法轻易破译，凯撒通常使用 N <= 25 位移位（即每个字母向后移 N <= 25 个位置）来加密消息。当消息被敌军获取了，敌军在不知道消息加密的方式情况下，只能得到一堆胡乱排列的字母。凯撒密码的数学公式为如下：凯撒密码的核心原理是通信的双方有一个固定字母顺序表，以这个固定顺序的字母表作为消息加密的基础，通过将原本的消息进行移动位置的方式进行加密。凯撒密码易于破译，尤其是使用频率分析法，通过统计文本中各个字母出现的频率，可以很容易地推测出移位量并解密消息。比如，在加密文本中可以通过寻找加密文本中最常出现的字母来推断移位量，当找到这个字母偏移量就很容易破解出来密文信息了。[scode type="blue"]下面是通过 Java 自定义码表实现凯撒密码加密和解密逻辑：[/scode]public class CaesarCipher { private static final char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); public static void main(String[] args) { String text = "HELLO"; int shift = 3; String encrypted = encrypt(text, shift); String decrypted = decrypt(encrypted, shift); System.out.println("Original: " + text); System.out.println("Encrypted: " + encrypted); System.out.println("Decrypted: " + decrypted); } // 加密函数 public static String encrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { // 拿到对应字母的下标 int originalPosition = findPosition(ch); // 找到偏移量之后字母下标，A 至 Z 之间循环 int newPosition = (originalPosition + shift) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 解密函数 public static String decrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { int originalPosition = findPosition(ch); int newPosition = (originalPosition - shift + ALPHABET.length) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 查找字符在自定义码表中的位置 private static int findPosition(char ch) { for (int i = 0; i < ALPHABET.length; i++) { if (ALPHABET[i] == ch) { return i; } } return -1; // 应该永远不会发生 } } 这种古代的加密方法，在现代计算机面前已经变得非常脆弱，凯撒密码只是在字母表上进行简单的移位操作，加密之后的密文与明文没有本质上的区别，只是信息的错位混淆处理。本质上密文依然是明文，现代计算机可以轻易地编写程序，快速破解这样的简单加密方式。现代的程序员在存储用户密码时，通常使用 MD5 或者 SHA-1 等不可逆的哈希函数进行存储。这些哈希函数将密码转换成固定长度的哈希值，使得即使密码被窃取，也很难通过哈希值逆推出原始密码，这种方法存储的才是真正的密文。通过将要传递的信息和语言文本信息通过特殊手段进行加密存储，消息在传递过程中能够达到通信完整性和机密性。实际上，这就是一种消息编码和解码的规则。类似于人类语言中使用中文交流的人，听不懂或看不懂用法语表示的语言信息，反之亦然。出现这种情况的原因是缺乏对方语言的编解码器。在计算机通信中，加密和解密的过程也是类似的问题。[scode type="yellow"]如果两台计算机要通过网络进行安全通信，首先需要设计出一套双方能够理解并使用的编解码程序，任何第三者计算机都无法破解或知道编解码的规则。这是保证通信机密性和安全性的前提条件。只有在这个前提条件成立的情况下，通信双方的安全性才能得到保障。总结一下两台计算机在不可靠网络链通信要面临的 3 个问题：如何确认对方真实身份？如何保证数据不能被截获篡改？如何保证数据机密性？这是整个网络通信要面临和解决的问题？从这 3 个问题提取出来的，要保证通信安全的 3 个核心要素：机密性、完整性、可用性 ，换一种说法就是：防窃听 、防篡改 、防伪造 ，满足这些条件才能保证通信隐私安全性。[/scode]对称加密在上面的 Caesar Cipher 故事中使用的是传统对称加密方式，只要密钥不被公开或被黑客窃取就能保证安全性。在现代的计算机中通常都是程序员或者用户设置一段固定长度字符串作为密钥，或者在程序运行期间动态生成一段随机字符串作为加密使用的密钥，这个密钥将被用于针对明文的数据块进行加密。常见的加密算法有 AES 和 DES 算法，可以看作为是一个接收明文数据和密钥返回密文特殊函数，数学函数公式表示为 C=E(P,K) ，对称加解密过程如下图：对称加密一般用于单体的应用或者本机运行的程序中，密钥一般都被存储在运行时的程序内存中，但也需要采取措施保护这些密钥。例如使用硬件安全模块（HSM）或者操作系统提供的安全存储机制（如 Windows 的 DPAPI 或 macOS 的 Keychain）来保护密钥。再者不要长时间使用一个固定的密钥，定期更换密钥，减少单个密钥被破解或泄露后造成的影响，实现自动化的密钥轮换机制，并确保旧密钥的数据能够正确迁移到新密钥上。非对称加密传统对称加密在密钥管理时会遇到如何防止加密密钥泄露的问题，导致对称密钥泄露的原因有很多，例如：密钥过于简单被黑客使用彩虹表攻击；或者将密钥明文硬编码在程序中，当程序被打包为二进制就连密钥一起被打包发布出去了，这样黑客可以通过反汇编技术查看到密钥。[scode type="yellow"]另外一个问题就是非对称加密如果用于在互联网上的两台不同计算机通信，会出现一个问题，两台要进行通信的计算机要协商一个用于数据加密的密钥。如果密钥由互相通信两台计算机中一台生成，那么密钥就要通过网络先发送给另外一台计算机，但在互联网上传输的密钥是明文文本内容，在传输的过程中很有可能被其他的恶意的计算机所截获到密钥，此时就失去了加密通信的意义。[/scode]要建立安通的通信通道就要解决：机密性、完整性、可用性，这 3 个问题。目前计算机网络 OSI 标准中的会话层 TLS（Transport Layer Security）协议，它就是基于非对称加密来保证通信安全性。该协议的前身是网景公司的浏览器为了解决 HTTP 协议在通信过程中明文传输数据时，容易造成数据泄露的问题所开发出的 SSL 协议 （Security Socket Layer）。TLS 协议通过使用对称加密和非对称加密、数字证书和公钥基础设施（PKI），为应用层提供了安全的会话通信机制，开发者在实现安全通信时，通常可以直接利用现有的 TLS 实现来确保通信的安全性。TLS 协议是 TCP/UDP 传输层的安全扩展，旨在确保传输层的数据传输安全。TLS 协议涉及到很多关于密码学和数学相关知识，它基于 RSA（Rivest-Shamir-Adleman）加密算法设计开发的，和传统对称加密解密所使用同一个密钥不同，它需要两个不同的密钥互相配合才能完整加解密的工作，分别为公钥和私钥，工作原理如下图：非对称加密的密钥是一对密钥，由公钥和私钥组成，公钥用于加密数据，生成的密文只能通过对应的私钥进行解密，反之亦然。公钥和私钥之间存在数学上的强绑定关系，确保数据的加密和解密过程是安全。具体运作过程，公钥可以公开分发，而私钥则必须严格保密，以保证只有合法的私钥持有者能够解密通过公钥加密的数据，从而确保数据的保密性、完整性和发送者的身份认证。TLS 的工作原理是首先使用非对称加密技术进行密钥交换，协商出一个用于当前会话的对称加密密钥，随后通信的数据报将通过这个对称密钥进行加密和解密。这样 TLS 协议不仅确保了数据的机密性，还有效地防止了中间人攻击在密钥交换过程中的威胁。TLS 依赖于非对称加密体系，在非对称加密体系中有一个严重容易忽略的问题就是，一对密钥中公钥都公开的，任何人都可以获取得到对方的公钥和使用对方公钥来加密数据发送给对方，这就使得任何人都可以冒充另外一个人的身份来使用公钥，对于解密方来说无非确定是谁使用了自己的公钥。数字签名为了确保通信数据的机密性，首先必须建立信任关系。在 TLS 协议中，使用了非对称加密的 RSA 算法，解决了加密与解密过程中的密钥分发问题。然而，尽管非对称加密在密钥传输上具有优势，它并没有解决公钥可能被伪造或冒充使用的问题。为防止这一风险，TLS 协议通常结合证书颁发机构（CA）来验证公钥的合法性，确保通信双方的身份真实可信。信任关系的建立是保障通信安全的前提，要建立通信双方的信任关系，就要解决公钥公共使用的问题，自己公钥被谁使用了？接收到的数据包是谁使用了自己公钥？解决问题这个方法就是使用数字签名，数字签名原理和日常生活中的纸张上盖章一样。下图为数字签名工作原理：[scode type="yellow"]实现使用不可逆向哈希函数对要发送的数据包进行哈希运算，生产这个数据包内容的摘要信息，也就是对应的哈希码。再使用自己的私钥对摘要信息进行加密，生成对应的密文（数字签名），将其和自定的公钥、数据文件内容打包发送给对方。对方收到数据包取出数据包中的公钥，对数据包中密文（数字签名）进行解密，获得到数据包内容的摘要信息，再使用相同哈希函数对数据包的内容进行哈希运算，得到哈希码，通过比对数字签名的摘要哈希码和本地运算计算出的哈希码来确定数据包真实性和完整性。[/scode]CA 签发证书通过非对加密的私钥和哈希函数对数据进行数字签名，可以解决数据文件归属和署名的问题，接受者可以通过数字签名来确定发送者是谁，但随着而来的一个另外问题是如何获取对应的非对称加密公钥，在验证数字签名过程中需要获取公钥。上面的步骤中将公钥和数字签名、数据文件，打包一起发送给接受者，一个明显的问题是公钥匙明文存储的，且也没有被加密，容易导致数据包中间人截获之后调包更换了公钥。[scode type="yellow"]解决公钥获取最简单一个方式是使用数字签名同样对数据包中的公钥进行签名，公钥使用者在使用公钥之前进行签名验证来确保公钥的安全性。通过对公钥也进行数字签名来解决身份问题，但这样做问题又回到信任问题上，因为整个数据包都是在互联网上传输，也有可能遭受到中间人调包到风险。那如何解决这个问题呢？在现在互联网基础设施中引入了一个公共的公钥签发中心，它可以来管理所有人的公钥，以及确认公钥持有者的身份信息，确保持有者是合法合规的。CA 证书验证中心类似于现在社会中的政府设置安全部门，例如大家信任银行的安全性，都把钱存储在银行里，银行保管着用户存储的钱；通过信任 CA 用户可以信任与其相关的数字证书和公钥，从而建立起安全的通信链路。[/scode]上图为通过权威的 CA 认证证书签发中心颁发任何人的公钥的工作流程。用户首先将自己的非对称密钥的公钥和自己身份信息提交给 CA 中心，CA 中心会对公钥所有者身份进行校验，以确保公钥确实是属于所有者。在 CA 确认了公钥所有者的合法身份之后，会使用自己的私钥对提交者的公钥进行数字签名，并且打包为数字证书文件。当有人需要和公钥所有者通信的时候，只需要通过 CA 来获取被通信者的证书，可以通过 CA 颁发的证书来鉴别公钥，再使用 CA 的公钥对数字证书的内容进行数字签名合法校验，通过验证确认了身份，即可使用非对称加密进行网络通信。通过 CA 认证的证书签发中心就可以完全避免通信双方密钥的配送遭到中间人劫持的问题，一个完整的 CA 签发证书上面包含了 CA 信任链以及使用的签名算法，证书所签发的日前和终止日期，证书所在地球的国家和组织名称。如果当发现私钥泄露的问题了，可以考虑通过 CA 认证中心链来吊销相关的证书[scode type="yellow"]CA 签发的证书只能解决通信双方的身份认证问题，不能保证整个通信信道的安全性，解决这个通信信道安全性问题就要回到上面公钥和对称加密的混合密码体系了。[/scode]通过整合多种密码学技术（如混合加密、数字签名、单向散列和 HMAC）来实现 TLS 网络协议，确保了网络通信的机密性、完整性和身份验证能力。TLS 协议保证从通信双方的端系统从连接建立到通信会话结束整个过程中的安全性，即浏览器和服务器通信所有的数据包在不可信通信链路是经过加密处理的。使用了 TLS 协议进行加密通信，并不意味着就能掉以轻心，这个取决于使用者的习惯和服务器端的维护者如何管理这些客户端发送过来的数据；用户可能使用被二次编译修改过源代码的浏览器，这些浏览器可能就被黑客植入了一些木马后门程序；另外就是当这些数据在服务器端如何保存，没有妥善的保管这些数据，黑客也可能从服务器端漏洞进行窃取这些数据，从而导致数据外泄，这样即使用了 TLS 协议也无法保证敏感性数据不外泄的风险。
开发基于网络的程序时，有许多因素需要考虑，以确保你的应用程序是高效、安全、可伸缩和用户友好的。但是今天这篇文章更加关注的是 OSI 模型中的应用层协议和运输层的协议，基于这些层的程序开发更加关注什么？如果作为一名应用开发者我们最多接触到的网络层面上的应用协议和运输协议；应用的概念更加面向用户，强调的是提供某种服务或功能，可以是桌面应用、移动应用、Web 应用等，这里围绕的是应用开发如何对网络协议选型，因为应用开发是在操作系统用户态上的开发，对于内核的可编程操作是不允许的，所以本篇文章更加关注的是基于 TCP/UDP 之上的协议选型时要考虑的问题。TCP 和 UDPTCP 和 UDP 都是运输层提供两套协议，提供系统内核的 API 可以使用它进行运输数据包达到另外一台主机上，市面上很多的远程调用框架例如 gRPC 和 Thrift 在底层运输数据包都是采用的内核提供的 API 进行发包的。这些 RPC 框架在应用层之上提供了一种结构化的、高效的远程调用机制，使开发者能够更轻松地构建分布式系统。虽然它们通常在应用层协议的基础上构建，但它们自身更类似于框架或库，提供了更高级别的抽象，用于处理远程服务调用的复杂性。gRPC 使用 Protocol Buffers（ProtoBuf）作为接口定义语言（IDL），这允许开发者定义服务接口、消息格式等，而不用关心底层通信细节。这里围绕的 gRPC 来讨论这个问题，了解 gRPC 协议都知道该协议是基于 HTTP/2 协议，利用了 HTTP/2 的多路复用、头部压缩、二进制帧提供了更高效的网络传输，相比 HTTP/1.1 使用多个 TCP 连接，每个连接只能传输一个请求和响应，这可能导致 TCP 连接的浪费和性能瓶颈，而 HTTP/2 引入了多路复用的特性，允许多个请求和响应在同一 TCP 连接上并行传输，这样也减少了 TCP 连接的数量提高了性能。[scode type="yellow"]通过这么一解释就很容易明白 gRPC 为什么要使用 HTTP/2 协议作为基础的应用数据传输协议了，HTTP/2 协议属于是应用层的协议，它依赖运输层协议是 TCP 协议，确实符合 gRPC 协议的应用场景，针对在分布式系统上对数据的可靠性和高效的数据传输要求。也从侧面角度反映出了为什么 Spring Cloud 没有直接使用 gRPC 这种协议进行数据通信，而是直接使用 HTTP 协议，随着 HTTP 协议版本改进采用更高效的二进制数据传输，性能和 gRPC 也不相上下。Spring Cloud 并没有直接使用 HTTP/2.0 进行通信，而是建立在 HTTP 协议之上。默认情况下 Spring Cloud 使用 HTTP/1.1 作为通信协议，但是可以通过一些配置和定制来启用 HTTP/2.0 ，相比 gRPC 特殊的数据传输格式，HTTP/2.0 反而没有那么多数据格式要求可以自定义是 JSON 还是 XML 这种，最终都是会被编码为二进制数据帧。[/scode]对于需要可靠型网络数据传输的场景，使用 TCP 是一个最佳的选择，但是也并不意味着盲目的去使用它传输数据。 TCP 传输数据之前每次都是需要进行 3 次握手，这 3 个握手数据包是主要的耗时来源，延迟就此产生了，如果在一次性很短暂数据运输需求下，这就显得不是那么适用了，快速打开一个连接使用完成之后快速释放掉，很多应用层的网络程序都是跑在 TCP 协议之上，必须经过这个慢启动的过程。HTTP/1.0 版本主要就是针对此种突发性连接的场景，获取到某个 HTML 内容立马断开 TCP 连接，完全没有利用好 TCP 协议，每次发送一个 HTTP 请求都需要经过一次 TCP 慢启动过程。
众所周知计算机软件的架构目前最为常见的是 B/S 和 C/S 架构，B/S 和 C/S 架构的软件都采用了分层结构，将系统划分为不同的模块层次，每个模块层负责相应功能，这种架构便于管理和维护，同时提高了系统的可扩展性和灵活性。笔者感觉最主要的特点是在使用软件时都会产生数据，而这些数据如果存储在本地时很不安全的，并且用户产生的数据才是开发对应软件公司想用的东西，数据才是一个软件科技公司的财产。两种架构都将用户界面和数据处理分离，有助于系统的灵活性和维护性，用户界面只需要负责展示信息，而数据处理由服务器端或客户端中的逻辑处理模块完成；无论是 B/S 还是 C/S 架构，都涉及客户端和服务器之间的通信，客户端向服务器发送请求，并接收服务器的响应，这种通信机制实现了应用程序的交互和数据交换，这篇博文将探讨一些在 B/S 架构下的网络通信协议技术。Web 浏览器和客户端[scode type="yellow"]笔者将其 B/S 架构归纳为 C/S 架构中，其 C/S 架构的中的 C （client）一般是指的安装在用户电脑上的软件，那么其浏览器也是归于客户端软件的，浏览器本体也是一款软件，只不过它可能比其他客户端的软件实现了多种的应用层的网络通信协议。B/S 架构中，客户端通常是 Web 浏览器，用户通过浏览器访问应用程序，这种架构具有跨平台、易部署和易维护的优势。B/S 和 C/S 架构都可以支持多个用户同时访问系统，多用户的需求可以通过在服务器端处理实现，架构图：[/scode]当浏览器发生数据请求就会采用 HTTP 协议和应用服务器交换数据，而应用服务器和数据库服务器交互则采用传输层的 TCP 协议。HTTP ProtocolHTTP 协议是 HyperText Transfer Protocol 的缩写 (即超文本传输协议），是由 w3c（万维网联盟）制定的一种应用层协议，用来定义浏览器与 Web 服务器之间如何通信以及通信的数据格式。因为 B/S 架构中的通信模块就是以 HTTP 这个协议作为标准协议的，所以对该协议有所了解可以更好的编写程序。 HTTP 协议的发展历史悠久，已经不断的迭代多个版本，不同时期的 HTTP 版本特性，整理表格：时间段HTTP版本主要特点1989-1995HTTP/0.9- 最初版本，仅支持 GET 请求。- 无请求头或状态码，只有响应主体。- 用于传输 HTML 文件。1996-1999HTTP/1.0- 引入多种 HTTP 方法（GET、POST、HEAD 等）。- 引入状态码、请求头、响应头等概念。- 服务器在每次请求后关闭连接。1999-2015HTTP/1.1- 引入持久连接，减少连接建立的开销。- 引入管道化（Pipelining），实现多请求并发。- 引入 Host 头字段，允许多个域名共享 IP 地址。- 引入分块传输编码和压缩，提高效率。2015-至今HTTP/2.0- 完全重新设计，旨在提高性能和效率。- 改进多路复用，支持并行请求和响应。- 引入头部压缩，减少数据传输大小。- 允许服务器推送资源，提高效率。- 强制使用加密（TLS），提高安全性。2020-至今HTTP/3.0- 基于 QUIC 协议，进一步提高性能和安全性。- 改进连接建立速度和稳定性，减少握手延迟。- 实现数据包级别的错误纠正和拥塞控制。- 采用基于 UDP 的传输协议。[scode type="yellow"]HTTP 协议一次请求对应一次连接，当浏览器再次发请求给服务器时，Web 服务器并不知道这就是上次发请求的客户端，这也是 HTTP 协议的一个特点：无状态协议。这种需要时建立连接，使用结束后立即断开连接的方式使得 Web 服务器可以利用有限的连接为尽可能多的客户提供服务，并且能够处理分散的网络请求，处理来自不同地区浏览器请求，并且服务器也可以多台分散在不同地区的，正因为是一种应用层协议还可以使用多层代理架构，例如使用 Nginx 做代理网关，正是具备了这样的特点，才使得 B/S 架构能够承载企业级应用的大量访问。[/scode]如果要测试 HTTP 协议的一些特性这里推荐使用 telnet 命令行工具，可以在命令行进行 HTTP 数据包封装，Telnet 使用了 TCP/IP 协议进行通讯，Telnet 协议默认是 23 端口。它允许用户通过网络连接到远程主机，并在远程系统上执行命令，就像在本地终端上输入命令一样。完全可以使用 telent 命令行工具，来模拟整个 HTTP 协议连接建立的过程，HTTP 请求和响应的过程如下：[scode type="green"]浏览器根据 IP 地址和端口号与服务器建立连接向 Web 服务器发送请求数据包Web 服务器接收请求数据包后，发送相应的响应数据包浏览器接收响应数据后关闭连接[/scode]在 Windows 中的 telent 默认会在高级功能设置中，而 MacOS 和 Linux 用户则需要手动进行安装对应的 telent 命令，下图是一次使用 telent 进行 HTTP/1.1 版本请求响应的例子日志细节。$: telnet www.ibyte.me 80 Trying 172.67.151.15... Connected to www.ibyte.me. Escape character is '^]'. GET / HTTP/1.1 HTTP/1.1 400 Bad Request Server: cloudflare Date: Mon, 09 Oct 2023 11:55:59 GMT Content-Type: text/html Content-Length: 155 Connection: close CF-RAY: - 400 Bad Request 400 Bad Request cloudflare Connection closed by foreign host.[scode type="yellow"]在这个例子使用的 HTTP/1.1 版本，这个版本最大特性是多了 Host 头字段，允许多个域名共享 IP 地址，可以让一台 Web 服务器主机占用 80 端口来服务多个子网站网站，Web 服务器可以通过 Host 头字段区分具体要返回域名的内容，Host 字段是区别于 DNS 的，DNS 只是帮助浏览器通过域名查询到 IP 建立 TCP/IP 的连接再进行 HTTP 协议通讯，请求流程如下图：[/scode][scode type="yellow"]HTTP/1.0 相比前两个版本做了很多改进，Host 字段只是其中一个改进之一，另外为提高性能引入了 HTTP Keep-Alive 机制，老版本在发送 HTTP 数据包之前，每次都需要建立新的 TCP/IP 连接通信，如果读者了解过 TCP/IP 协议如何建立连接的，会明白这个过程很费时，所以 HTTP/1.1 版本引入了持久连接，以减少在每次请求时建立新连接所带来的开销，提高网络通信的效率。Keep-Alive 默认超时机制是 60 秒，也就是说如果连接在连接池中的空闲时间超过 60 秒，则该连接会关闭。[/scode]另外 Keep-Alive 超时间隔时间在各个 Web 服务器配置可以设置，Keep-Alive 机制对应的请求头中的是 Connection 字段，服务器会根据此字断来做相应的处理。字段作用Connection: keep-aliveTCP 持久连接Connection: close要求服务器关闭 TCP 连接[scode type="blue"]HTTP/1.1 为提升连接性能，旧版本的 HTTP/1.0 每发送一次 HTTP 请求都会重新建立一次 TCP 的连接才能正常发送正常的请求数据包，导致的问题如果客户端请求到一个页面里面包含了 5 个图片文件，那么浏览器会继续创建 5 次的 TCP 连接来完成这些 HTTP 对象资源的请求；而另外则为上面提到的 Keep-Alive 机制，持久化连接，多个 HTTP 请求共享一个 TCP 连接。还加一个 Pipelining 机制，允许在发送数据包多个请求一起发送，类似于 Redis 命令中的 Pipelining 机制，批量处理数据包请求，不使用 Pipelining 时，每个请求都需要等待前一个请求的响应返回才能发送下一个请求，这样会导致网络的低效利用。[/scode]下面是笔者一个使用 Telnet 例子日志信息：$: telnet www.ibyte.me 80 Trying 172.67.151.15... Connected to www.ibyte.me. Escape character is '^]'. GET /path1 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive GET /path2 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive GET /path3 HTTP/1.1 Host: www.ibyte.me Connection: keep-alive [scode type="blue"]在 HTTP/2.0 又引入了 Multiplexing 技术来提升性能，因为 Pipelining 存在 Head-of-Line Blocking 问题，这是什么问题？ Head-of-Line Blocking 问题根本原因是假设客户端发送了 3 个请求 A 、B 和 C 到服务器，请求的顺序是 A 、B 、C 。如果服务器处理请求 A 的时间较长，那么请求 B 和 C 就必须等待 A 完成后才能开始处理，响应也必须按照 A 、B 、C 的顺序返回给客户端，即使后续的请求处理时间很短，也需要等待前面的请求完成。[/scode]而 HTTP/2.0 引入了多路复用 Multiplexing 机制，允许客户端同时发送多个 HTTP 请求并在同一个 TCP 连接上进行传输，同时服务器也可以将多个响应并行地返回给客户端，不同请求的数据可以交错传输，而不必等待前面的请求响应完成。这样可以充分利用连接，减少了等待时间，提高了效率，总体来看不同性能提升方案差异对照图。[scode type="yellow"]HTTP/2.0 将每个请求或回应的所有数据包，称为一个数据流（stream），每个数据流都有一个独一无二的编号，数据包发送的时候，都必须标记数据流 ID，用来区分它属于哪个数据流；数据流的编号是递增的，客户端和服务器都可以创建和使用数据流，数据流的编号从 1 开始，分别由客户端和服务器独立维护，客户端发起的数据流使用奇数编号，服务器发起的数据流使用偶数编号。[/scode][scode type="yellow"]此外 HTTP/2.0 为提升整个传输效率问题，针对每次需要重新传递的 HTTP Handler 请求协议头，大部分情况下请求协议头都是固定，除了极少的字断经常变动之外；HTTP/1.1 版本每次传输会增加 500-800 字节的开销，如果使用 Cookie 来存储数据会增加千字节 KB 。而在新的版本 HTTP/2.0 则使用 HPACK 压缩格式压缩请求和响应标头元数据，HPACK 核心就是采用 Static Table Definition 结构来服用存储请求头元数据信息，和 Huffman Coding 编码对数据进行压缩传输。[/scode][scode type="yellow"]HTTP/2.0 协议中的 HPACK 复用机制，协议服务端可以维护一个动态缓存表，在发送新请求时协议会和之前请求做对比，之前传输的值的索引列表让我们可以通过传输索引值对重复值进行编码，索引值可用于高效查找和重建完整的标头键和值，如下图：[/scode]至于对于具体值和索引，建议查阅 IETF RFC7541 规范文档，如果不是自己去开发实现 HTTP/2.0 协议的话。另外仿照 TCP 的流量控制策略，HTTP/2.0 也设计了流量控制策略算法，在同一个 TCP 连接上的多个数据流是共享当前运输层的 TCP 连接的，基于 SETTING 和 WINDOW_UPDATE 帧进行数据窗口的调节，具体如何调节看客户端和服务器端的场景。所谓窗口调节也就是在请求头上双方协商调整滑动窗口的大小，这是一段 Java 代码的例子：import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; public class Http2FlowControlExample { public static void main(String[] args) throws IOException { // 创建 HTTP/2 服务器 HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); // 创建上下文处理器 server.createContext("/", new MyHandler()); // 启动服务器 server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { // 发送流控制窗口大小为 4096 的响应头 exchange.getResponseHeaders().add("Content-Type", "text/plain"); exchange.getResponseHeaders().add("Window-Size", "4096"); // 获取输出流 OutputStream os = exchange.getResponseBody(); // 模拟发送数据，每次发送 1024 字节 byte[] data = new byte[1024]; for (int i = 0; i < 10; i++) { // 发送数据并检查流控制窗口 os.write(data); os.flush(); // 模拟接收方处理数据 handleData(data); // 发送 WINDOW_UPDATE 帧，增加流控制窗口大小 exchange.getHttpContext().getServer().getExecutor().execute(() -> { exchange.getResponseHeaders().add("Window-Update", "4096"); }); // 暂停 1 秒以模拟处理延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } // 关闭输出流 os.close(); } private void handleData(byte[] data) { // 模拟接收方处理数据 System.out.println("Received data: " + new String(data)); } } }正常情况下，流量控制的处理应该由 HTTP/2 协议自动进行，而不需要手动添加 Window-Update 这样的字段，而发送 WINDOW_UPDATE 帧是由 HTTP/2 实现自动处理的。而不是通过添加响应头的方式，HTTP/2 协议会自动进行流量控制，确保在不超过对端流控制窗口大小的情况下发送数据。针对 HTTP/1.1 主动请求资源的方式，HTTP/2.0 升级了主动推送服务，当服务器决定要推送一些资源给客户端时，如资源的 URL 、头部信息等。客户端可以选择接受或拒绝这个推送，如果客户端接受了推送，那么服务器就会在接下来发送相应的数据帧，而不需要等待客户端发起请求，在经典的 Web 服务器 Nginx 中可以使用 http2_push 指令来实现这类的推送功能：server { # 启用 HTTP/2 协议版本 listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/your/certificate.crt; ssl_certificate_key /path/to/your/private_key.key; # 启用 HTTP/2 服务器推送 http2_push_preload on; location / { # 设置推送的资源 http2_push /styles.css; http2_push /script.js; # 处理正常的请求 try_files $uri $uri/ =404; } } [scode type="green"]整个的 HTTP/2.0 协议的设计可以认为借鉴 TCP 协议的设计，相比 HTTP/1.1 协议采用基于 TCP 多连接的进行数据传输，而 HTTP/2.0 总体将协议分为了 3 个部分：流 、消息 、帧 部分，采用 TCP 单连接多数据流的方式进行交叉传输，流可以认为是一种针对单个 TCP 连接的做平分的方式，多个流在一个 TCP 连接中传输形成一个虚拟信道。消息则是在流中传输的请求和响应数据包，帧则是消息中的数据包实体，多个帧可以组成一个消息数据包，帧是最小的通信单位，为了提升性能又添加了服务器端推送事件，而针对数据共享 TCP 连接又添加了流量控制窗口，针对反复传输的 HTTP 请求头 Handler 做了首部压缩，客户端和服务器端各自维护来一张动态缓存表 “首部表” 来跟踪已经被存储的请求头字断，不再通过每次请求和响应发送。[/scode]Ajax 异步 HTTP通过上面篇幅已经知道 HTTP 只能适用于从客户端主动发起，而服务器只能被动提供服务，对请求做出对应的请求数据响应；当然这里在 HTTP/2.0 允许服务器推送资源，提高效率，HTTP/2.0 会根据客户端请求主动推送对应的数据包到客户端中。在这种架构下客户端和客户端之间不是直接 P2P 方式进行通信的，而是通过服务器进行通信交互数据。典型的全双工通信例子有股票市场的 K 图，或者一些在线的 IM 即时通讯应用，目前 Web 实现这类应用的方案有通过 HTTP 协议进行请求轮训的方式，或者 WebSocket 的方式，这也是本篇技术博客要详细讲解的技术。[scode type="blue"]Ajax 轮询方式，所谓的轮询请求数据也就是异步的向服务器端定时发送 HTTP 请求来获取最新的数据，但是缺点也是明显的这种方式数据不能及时显示到客户端，轮询的频率只能由客户端来控制。因为 HTTP 协议书是基于 DNS 和 TCP 协议来传输数据，所以如果每次使用 HTTP 进行轮询请求，那么网络延迟会很大，由于发送 HTTP 请求要先查询 DNS 和建立一次 TCP 连接，建立 TCP 连接需要交换控制信息和三次握手。如果基于 Ajax 轮询方式采用的是 HTTP/1.0 版本性能更慢，因为每次轮询一次需要建立一次 TCP 连接，导致会出现 TCP 连接慢启动和 RTT 延迟。[/scode]在使用基于 HTTP/1.x 版本进行 Ajax 轮询的时候会出现一个问题，每次轮询都需要建立一次 TCP 连接，了解 TCP 协议的都明白每次建立一次 TCP 协议连接代价很大，都需要进行三次握手。为了解决这个问题开发者又提出了长轮询的概念 （Long Polling），普通轮询在服务器接收到请求后，开始处理如果有新的数据可用，服务器立即将数据返回给客户端，没有也会立即断开连接；而长轮询的做法，如果服务器暂时没有新的数据可用，服务器和客户端会保持连接打开，等待直到有新的数据为止，或者加一些策略在一定时间后关闭连接做超时控制，下面是一段 Java 实现的代码：import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @WebServlet("/polling") public class PollingServlet extends HttpServlet { private static BlockingQueue<String> dataQueue = new LinkedBlockingQueue<>(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // 设置响应类型为纯文本 response.setContentType("text/plain;charset=UTF-8"); // 设置响应缓冲区不缓存数据 response.setBufferSize(0); // 设置响应字符编码 response.setCharacterEncoding("UTF-8"); // 从队列中取出数据，如果队列为空，则等待一定时间 String data = dataQueue.poll(10, TimeUnit.SECONDS); // 没有新数据，返回适当的响应 response.getWriter().write(data != null ? data : "No new data"); } catch (InterruptedException e) { e.printStackTrace(); // 处理异常，例如返回错误信息 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); } } // 此方法可以在其他地方先调用，例如其他的 Servlet 方法 private static void startDataProducer() { // 启动数据产生者线程 Thread producerThread = new Thread(() -> { try { int count = 1; while (true) { String data = "Data-" + count++; dataQueue.put(data); // 模拟数据每两秒产生一次 Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); } }); producerThread.start(); } }对于客户端只需要在收到消息之后继续发送一个 XHR 请求到服务器，JavaScript 代码：function longPolling() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'https://example.com/longpoll', true); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { var responseData = JSON.parse(xhr.responseText); // 处理从服务器返回的数据 longPolling(); // 再次发起长轮询请求 } else { // 处理错误，重新发起长轮询请求 longPolling(); } } }; xhr.send(); } longPolling(); // 初始调用 [scode type="yellow"]对于采用轮询的方式采用那个版本的 HTTP 协议进行数据交换需要考虑。另外一个缺点也是很明显的，如何对轮询的请求间隔时间做选择？因为是采用的定时轮询的方式导致客户端和服务器端存在一定同步延迟，如果碰巧服务器消息在客户端轮询请求之前产生，那么客户端和服务器消息同步延迟时间是最短的，只存在请求网络的延迟。当如果客户端请求刚刚返回，服务器就产生了消息，那么此时两端同步的延迟就是最大，长轮询的缺点也是很明显的如果服务器产生的新数据频率很快，对于客户端来说长轮询能及时获取到最新数据，但是意义不大不如设置一定间隔时间去获取这些数据，采用常规轮询的方式进行。[/scode]SSE 事件推送HTTP 和采用 HTTP 轮询的方式都是为了解决，服务器和客户端之间的数据交换，轮询又是基于 HTTP 协议基础之上一种技术应用的实现，目的是让及时获取到服务器产生的数据变更，客户端能获取得到。这两种方式的都是采用客户端主动发送 HTTP 协议来获取数据，遇到的问题也很明显存在消息同步延迟，不能由服务器在产生新的数据同时就将消息同步到客户端中。针对这个轮询延迟交付问题，多浏览器厂商设计一款新的 API 来帮助解决服务器推送消息的问题，采用 Event Source 和 Event Stream 进行服务器推送通知，一个 Go 的实现自定义事件类型和数据推送功能：package main import ( "encoding/json" "fmt" "net/http" "time" ) type CustomEvent struct { Type string `json:"type"` Message string `json:"message"` Timestamp string `json:"timestamp"` } func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // 设置响应头，指定内容类型为 text/event-stream w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // 设置初始数据 initialData := CustomEvent{ Type: "message", Message: "Initial data", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, initialData) // 模拟定时向客户端推送不同类型的事件 ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: // 向客户端发送 "message" 事件 messageEvent := CustomEvent{ Type: "message", Message: "Server time", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, messageEvent) // 向客户端发送 "alert" 事件 alertEvent := CustomEvent{ Type: "alert", Message: "Important message", Timestamp: time.Now().Format(time.RFC3339), } sendEvent(w, alertEvent) case <-r.Context().Done(): // 客户端断开连接时关闭循环 fmt.Println("Client disconnected") return } } }) // 启动服务器 fmt.Println("Server listening on :8080") http.ListenAndServe(":8080", nil) } func sendEvent(w http.ResponseWriter, event CustomEvent) { // 序列化事件为 JSON 格式 jsonData, err := json.Marshal(event) if err != nil { fmt.Println("Error encoding JSON:", err) return } // 向客户端发送 JSON 格式的事件 fmt.Fprintf(w, "event: %s\n", event.Type) fmt.Fprintf(w, "data: %s\n\n", string(jsonData)) w.(http.Flusher).Flush() // 强制将数据推送到客户端 } [scode type="yellow"]Server-Sent Events (SSE) 代码实现起来比传统 Ajax 轮询要简单，只需要知道服务器的 SSE 协议地址，就可以发送 SSE 连接，服务器可以发送简单的文本消息到客户端，客户端通过事件监听器捕获这些消息，设置对应的事件处理逻辑即可。 SSE 是基于一个长连接的，相比轮询来推送消息，消息延迟是相对比较低的，但是只需要服务器推送消息到客户端，如果客户端要发送新消息则新建一个 HTTP 连接到服务器进行处理。在正常的 Event Source 使用中，服务器不需要重新建立连接来发送自定义事件，如果服务器和客户端遇到一些特殊情况断开连接了，服务器和客户端双方要考虑维护连接断开之后恢复，此外消息是文本格式同样可以使用 gzip 进行压缩。[/scode]WebSocket 全双工通信与传统的 Web 通信方式不同，WebSocket 允许客户端和服务器之间进行双向通信，双方都可以独立地发送消息。WebSocket 在客户端和服务器之间建立了一个持久连接，这个连接保持打开状态，允许低延迟通信。这样设计的好处允许客户端和服务器之间能交叉发送数据，客户端和服务器双方都可以随时发送消息到给对方。WebSocket 从一个 HTTP 握手开始，然后 HTTP 协议升级到 WebSocket 协议，然后再通过 WebSocket 连接发送的数据被组织成帧，帧可以携带文本或二进制数据，帧的机制支持高效的通信并支持不同的数据类型。因为和上述的通信方式一样，没有固定的消息格式，通信采用二进制数据流的方式，如果需要交互数据，可以在应用和服务器端设计子协议，子协议可以定义应用层的通信规范，规定在 WebSocket 连接上双方如何交换数据、消息和命令。这有助于确保在 WebSocket 连接上进行的通信是按照特定协议的规范进行的。子协议存在的目的统一客户端和服务器端都知道消息首部，各方面采用子协议就能正常编解码消息体，如果双方在握手时都同意使用特定的子协议，那么连接将在该子协议下进行通信，否则连接将使用默认的 WebSocket 协议。[scode type="blue"]网络延迟是必然存在的，数据包要从一台电脑到另外一台电脑必须经过 5 层网络模型，在这些协议栈上封包发包都需要时间，链路上传输取决于传输介质，传播延迟 + 传输延迟 + 处理延迟 + 排队延迟 = 整体网络延迟。这些延迟属于 ISO 网络层面的延迟，没有算上应用程序处理的延迟，和使用场景问题。何时使用 XHR ？如果你请求有突发性和短暂性这时应该使用 XHR 进行请求；如果服务器和客户端需要一些消息通知场景不涉及到大文件传输，可以考虑使用 SSE 进行；如果应用和服务器需要实时同步建议使用时久化连接的通信，WebSocket 可以提供此种方案，只需要首次建立连接握手，传输而使用二进制流方式进行高效的传输，并且可以使用子协议对通信协议进行扩展。[/scode]其他资料计算机网络：自顶向下方法（原书第8版)HTTP 资源与规范反模式：停用 HTTP 持久性HTTP keep-alive 二三事High Performance Browser NetworkinGoogle Web.dev HTTP/2 简介
前面的技术博文解释过通过 nmcil 来配置网络参数，而这篇文章要介绍的是 DHCP 协议程序，每当我们电脑接入网络时都会自动分配 IP 和 DNS 、子网掩码信息，并没有自己来配置网络连接信息，而是 IP 自动分配，能做到这一点就得益于 DHCP 协议，能帮助我们的设备自动配置网络信息。[scode type="blue"]现在建设你作为学校机房的管理员或者是网吧的管理员，每次都有不同的网络设备要接入到就局域网中，这就会带来大量配置网络信息工作，而且每次上完课部分设备又离开了教室，分配出去的 IP 如果不能回收，会导致 IP 地址被占用，不能被其他人再次使用，导致 IP 不够用浪费 IP 情况，也会带来手动管理 IP 麻烦，DHCP 要解决的问题就是针对这些场景的，自动化配置和自动化管理接入设备的 IP 地址。[/scode]DHCP 协议目前大部分网络设备和操作系统都默认支持 DHCP 协议，如果系统没有支持可以通过软件包的方式安装 DHCP 软件来支持 DHCP 协议，DHCP 分为两个部分一个服务器另外一个是客户端，服务器为提供网络接入服务的设备，而客户端则是要接入网络的设备。[scode type="yellow"]配置 DHCP 服务器：首先，您需要在网络上设置一个 DHCP 服务器，并为 DHCP 服务器分配 IP 地址和其他必要的网络参数，您可以使用现有的服务器，如路由器或专用 DHC P服务器，也可以使用开源 DHCP 服务器软件，如 ISC DHCP 或 dnsmasq ，在 Linux 下安装 dhcp 服务器可以使用下面命令：[/scode]# 安装 dhcp 服务器 dnf install -y dhcp-server 配置文件位如 /etc/dhcp/dhcpd.conf 文件中，DHCP 选项可以用来设置其他网络参数，如 DNS 服务器地址、NTP 服务器地址和 SMTP 服务器地址等，可以在 DHCP 服务器上配置这些选项，并在 DHCP 客户端上使用它们，有客户端接入到网络中 DHCP 会按照这些参数来配置自己的网络参数，官方提供参数参考文件，目录位于 /usr/share/doc/dhcp-server/ ，目前支持的网络参数列表：配置项配置值option domain-nameexample.comoption domain-name-serversns1.example.com, ns2.example.comsubnet192.168.1.0netmask255.255.255.0range192.168.1.100 - 192.168.1.200option routers192.168.1.1option subnet-mask255.255.255.0option broadcast-address192.168.1.255default-lease-time600max-lease-time7200hostclient1hardware ethernet00:11:22:33:44:55fixed-address192.168.1.10class"phone"matchsubstring(hardware, 1, 3) = 00:11:22option domain-name-serversns3.example.comshared-network"office"subnet192.168.2.0netmask255.255.255.0range192.168.2.100 - 192.168.2.200option routers192.168.2[scode type="yellow"]最常用参数也就那么几个，下面整理参数对应的作用说明：[/scode]配置参数说明option domain-name指定 DHCP 服务器分配的主机名所属的域名。option domain-name-servers指定 DHCP 客户端应该使用的 DNS 服务器列表。subnet定义 DHCP 服务器分配 IP 地址的子网。netmask定义 DHCP 子网的子网掩码。range定义 DHCP 服务器可以分配的 IP 地址范围。option routers指定 DHCP 客户端应该使用的网关地址。option subnet-mask指定 DHCP 客户端应该使用的子网掩码。option broadcast-address指定 DHCP 客户端应该使用的广播地址。default-lease-time定义 DHCP 服务器分配的 IP 地址的默认租约时间（以秒为单位）。max-lease-time定义 DHCP 服务器分配的 IP 地址的最大租约时间（以秒为单位）。host指定 DHCP 客户端的主机名。hardware ethernet指定 DHCP 客户端的 MAC[scode type="yellow"]下面是一个简单的示例代码，配置 DHCP 服务器能分配的 IP 地址池数量就 50 个，并且前 24 位为网段，192.168.31.1 为默认网关地址，最后默认 IP 生命周期时间和最大续租时间，单位为秒：[/scode]# 设置 dns 服务不能自动动态更新 ddns-update-style none; # 忽略客户端更新 DNS 记录 igrone client-update; # 网段作用域 subnet 192.168.31.0 netmask 255.255.255.0 { range 192.168.31.50 192.168.31.150; option subnet-mask 255.255.255.0; option routers 192.168.31.1; option domain-name "ibyte.me"; option domain-name-servers 1.1.1.1; defualt-lease-time 21600; max-lease-time 43200; }[scode type="blue"]DHCP 自动分配在某些场景下也有一些特殊的问题，例如 DHCP 自动续约服务，自动回收 IP 地址服务，导致某些 IP 不能和某个特定设备绑定。为了解决这些问题 DHCP 新版本有一个类似于绑定特定设备的服务，会把某个特定的 IP 绑定到某个特定的设备至上，要做到这一点就得知道特定设备的 mac 地址，大部分情况下可以在网络设置中查询到 mac 地址，写入 DHCP 到服务端配置文件中，这样 DHCP 会预留某个 IP ，当特定的设备 mac 地址和配置文件中一致即分配给这个设备特定的 IP ，想要达到这一点只需要在 DHCP 服务器端配置文件中添加一段代码：[/scode]host www.ibyte.me { # 指定网卡 mac 地址 hardware ethernet 00:0A:02:0B:03:0C; # 指定固定 IP 分配 fixed-address 192.168.31.88; }通过此种方式配置的 IP 和 Mac 地址会相互绑定在一起，特定设备分配特定的 IP 地址。其他资料DHCP 与 ARP 又是什么？
有互联网才有很多网络技术应用，让不同设备接入到网络从而达到互联网目的，就和现实世界中物流系统一样，互联网的前身美苏冷战时期的 ARPANET 最初在 1980 年代作为区域学术和军事网路连接的骨干网络，最后苏联解体了结束了冷战，最后这些已经曾在的网络基础设施，逐渐开始转向为民用网络系统了。[scode type="blue"]这篇文章要讲解的是 World Wide Web 这是存在于因特网之上的互联网应用服务，我日常见过很多人和同行认为万维网就是互联网，这是一个错误的认知，他们是有着本质上的区别的，万维网更多是指我们能通过浏览器或者支持通过 HTTP 协议能访问到的网络资源，例如文档、图片、视频、存在其他计算机所提供的资源集合并且通过网络进行开放访问。日常生活中使用的最多是浏览器访问 www 网站服务，而目前程序员做的事情通过不同编程语言和编程技术实现能动态服务的网站。[/scode]DNS 服务器每当我们访问一个已知的网址都是在浏览器的地址栏中输入一串字符串，这个字符串就时域名，而域名是 IP 地址的一串别名，浏览器底层发送一个网络请求时数据包时提供 TCP 协议栈发送出去的，我们也可以通过 IP 地址进行访问万维网服务，但是很少有人这么做，因为 IP 地址时一串由 4 个字节组成的 32 Bit 位数字串，往往一般人很难记住。这和日常生活中的电话号码一样，我们很少记住别人的电话号码的数字，而是通过手机的通讯录保存一个联系人名字，拨打电话时提供联系人姓名来打电话呼叫别人。[scode type="yellow"]同理在计算机中也是这样设计的只不过名称叫 DNS 服务，通过把目标 IP 地址绑定到一串特定便于记忆的字符串域名上，我们通过域名来服务网站，这比通过记住一串数字要容易的多，DNS 服务就是便于使用者查询对应域名所绑定的 IP 信息的，从而把组装数据包通过 TCP 协议栈发送出去。[/scode]DNS 服务是由全球 13 组根服务器所提供的，10 台在美国，另外 3 台分别在欧洲的挪威、伦敦和东亚的日本，就这 13 组根服务器为我们提供了 DNS 服务，不过目前世界上有很多 DNS 从服务器和缓存服务器方便我们快速查找和及时响应查询请求所设置，缓存服务器大部分是缓存已经查询过的域名信息，目录前世界上流行的公共 DNS 服务器 IP 地址有下列表：IP 地址供应商8.8.8.8Google Public DNS1.1.1.1Cloudflare Public DNS[scode type="blue"]DNS 服务器会根据客户的请求的内容，返回对于的信息，这些信息被注册到 DNS 服务器至上，分为不同类型的 DNS 信息，DNS 在设计的时候就考虑到了不是单单为网站域名服务，还有一些其他的类型服务，例如邮箱服务器地址查询，也是由 DNS 服务器负责的，在 DNS 服务器信息有一个 Class 字段，这个字段就是用来区分不同网络服务类型的，目前大部分类型为 IN，一个简单例子如下：[/scode]NameClassTypeDatawww.ibyte.meINA66.888.88.8ibyte.meINMXmxbiz1.qq.com.imgINAAAA2001:4860:4860::8888blogINCNAMEwww.ibyte.meptrINPTR192.168.0.1sevINSEV127.0.0.1整个 DNS 服务可以看作为一个交通指挥中心，存储着互联网上的交通路由表。Linux 的 DNS 服务在 Linux 中默认的 DNS 相关的配置有很多文件来制定，/etc/resolv.conf 为 DNS 解析服务器的配置文件，这样每当我们查询域名信息时，会从配置文件中找到 DNS 服务器的 IP 发送 DNS 查询请求，/etc/hosts.conf 为本机自定义 DNS 域名绑定映射文件，而另外一个至关重要的是 /etc/nsswitch.conf 文件，可以设置每次查询 DNS 服务的步骤。[scode type="yellow"]在 Linux 中如果默认使用 wget 命令和 curl 命令完全根据本地 DNS 配置方案进行查询步骤的，而 Linux 内置的 host 和 dig 命令是不会按照 hosts.conf 文件和 nsswitch.conf 的规则的，而是按照 resolv.conf 文件配置的远程 DNS 根服务器进行的查询。[/scode][root@cs9 ~]# dig www.ibyte.me ; <<>> DiG 9.16.23-RH <<>> www.ibyte.me ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61241 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;www.ibyte.me. IN A ;; ANSWER SECTION: www.ibyte.me. 300 IN A 104.21.48.119 www.ibyte.me. 300 IN A 172.67.151.15 ;; Query time: 129 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Sun Mar 26 17:58:41 CST 2023 ;; MSG SIZE rcvd: 73 [root@cs9 ~]# host www.ibyte.me. www.ibyte.me has address 104.21.48.119 www.ibyte.me has address 172.67.151.15 www.ibyte.me has IPv6 address 2606:4700:3037::6815:3077 www.ibyte.me has IPv6 address 2606:4700:3030::ac43:970f [root@cs9 ~]#dig 命令是目前最为常用的命令：# 对 IP 地址逆向解析 dig -x 166.111.4.100 # 查询域名邮箱服务器信息 XM 记录 dig -t XM ibyte.me # 查询 A 记录 dig -t A ibyte.me # 跟踪整个域名的解析过程 dig +trace ibyte.meBIND 9 服务目前 Linux 常用的 DNS 解析服务软件有 BIND ，全名为 Berkeley Internet Name Domain 是目前全球范围内使用的最多的 DNS 域名解析服务软件，可以运行在很多 Linux 发行版本至上，目前最新的版本是 BIND 9 ，下面介绍如何安装该版本到 Linux 上并且配置一些本地 DNS 解析服务：# 安装 DNS 解析服务 yum -y install bind-chroot[scode type="yellow"]核心的几个配置文件为，主配置文件 /etc/named.conf 、区域配置文件 /etc/named.rfc1912.zones 和数据配置目录 /etc/named，修改主配置文件 /etc/named.conf 让本机能提供 DNS 服务响应其他程序发送的 DNS 解析查询请求，将 listen-on port 和 allow-query 字段设置为 any ，表示接受任何 IP 的请求。[/scode][scode type="yellow"]光修改了主配置文件是没有任何解析作用的，因为没有添加 DNS 映射记录，改修 /etc/named.rfc1912.zones 文件将要添加的域名解析记录添加到其中，一个简单正向解析模版：[/scode]zone "ibyte.me" IN { type master; file "ibyte.me.zone"; allow-update { none;}; };其中 type 字段为类型，目前支持 3 中类型，也对于的域名级别来区分：类型作用说明master主区域hint根区域slave辅助区域[scode type="yellow"]其他可能的区域类型包括 type slave、type stub 和 type forward ，这些类型的作用如下：[/scode]type slave: 从服务器，它会从 Master Server 中复制数据，并将这些数据提供给其他请求者。Slave Server 可以提高服务器的可靠性和性能，因为它可以处理大量的查询请求。type stub: 存根服务器，它只保存指向特定域名的 NS 记录（权威 DNS 服务器）。当 BIND 9 接收到查询请求时，它将使用这些 NS 记录来查找适当的权威 DNS 服务器，并请求该服务器提供所需的解析数据。type forward: 转发服务器，它会将所有接收到的查询请求转发到其他 DNS 服务器进行处理。转发服务器可以在本地缓存中存储解析数据，以便更快地响应查询请求，并减少对其他 DNS 服务器的查询量。[scode type="yellow"]最后要创建一个域名解析对应的数据文件，默认在 /var/named/ 文件夹中有默认的数据文件模版，照着改就行了，比如下面这种格式的： [/scode]$TTL 1D @ IN SOA ns1.example.com. hostmaster.example.com. ( 2019032601 ; Serial 8H ; Refresh 2H ; Retry 1W ; Expire 1D ) ; Minimum TTL IN NS ns1.example.com. ns1 IN A 192.168.0.1 www IN A 192.168.0.2 修改配置文件的参数解析如下表格：参数解释$TTL该参数表示该区域文件中所有记录的默认 TTL 时间（生存时间）。这里的 1D 表示 1 天。如果某个记录没有指定 TTL 时间，就会使用该默认值。@表示当前域名，这里就是 example.com。IN表示这是一个 Internet 类型的域名。SOA表示 Start of Authority 记录，它包含了一些关于该域名的基本信息。ns1.example.com.表示该域名的主 DNS 服务器是 ns1.example.com。hostmaster.example.com.表示该域名的管理员邮箱地址是 hostmaster.example.com。2019032601表示该域名的版本号，一般是一个日期和一个序号的组合。8H表示该域名的刷新时间（Refresh），也就是从主 DNS 服务器更新该域名信息的间隔时间。2H表示该域名的重试时间（Retry），也就是从主 DNS 服务器更新该域名信息失败后重试的间隔时间。1W表示该域名的过期时间（Expire），也就是该域名信息在本地 DNS 服务器上保存的最长时间。1D表示该域名记录的最短 TTL 时间，也就是在该时间内，本地 DNS 服务器不会再次向主 DNS 服务器查询该域名的信息。NS表示 Name Server 记录，用于指定该域名的 DNS 服务器。A表示 Address 记录，用于指定域名的 IP 地址。支持的解析类型还有很多没有列出，可以查看这片博文DNS 原理入门，当前为正向解析，DNS 还可以通过 IP 进行逆向解析和 DNS 缓存服务器，这些后面有时间再介绍。其他资料A safer and more private browsing experience with Secure DNS
本篇博客将记录一下 Linux 系统下网络配置，会对 Linux 网络配置和基于 Linux 网络的负载均衡的配置介绍，如何在 Linux 系统下配置网络设置是一个极客程序员必须掌握的，如何配置 sshd 服务程序的知识和免密登录、创建虚拟网卡提高系统可靠性。Network ManagerNetworkManager 软件工具的目标是降低在 Linux 与其他类 Unix 操作系统上的计算机网络使用难度，RedHat 在 2004 年发起了 NetworkManager 项目，其目标是让 Linux 用户能够更容易的处理现代的网络需求，特别是无线网络。# 安装 NetWorkManager 软件包 dnf install NetworkManager -y # 查看网卡信息 ifconfig # 查看网卡状态 mii-tools ens160 # 查看网卡详细状态信息 ethtool ens160 # 给某块网卡启一个别名 ip addr 192.168.31.200/24 dev eth0 label eth0:1 # 命令行图像配置 nmtui比较老的版本可以在 /etc/sysconfig/network-scripts/ 目录中，编写对应的网卡脚本文件，Linux 会根据文件内容来设置对应的网卡信息，一个简单模版：# vim ifcfg-ens160 固定前缀 ifcfg- 后面为网卡名称 TYPE=Ethernet # 网络类型 BOOTPROTO=static # 静态分配 NAME=eno16777736 # 网卡名称 ONBOOT=yes # 是否自动启用该网络接口 IPADDR=192.168.10.10 # 分配的 IP NETMASK=255.255.255.0 # 子网掩码 GATEWAY=192.168.10.1 # 网关地址 DNS1=8.8.8.8 # DNS 地址 # 重启网络模块 systemctl restart network以上为老版本系统所使用的方式，不再推荐使用！ RHEL9 和 CentOS 9 Stream 默认使用了 NetworkManager 来管理网络，因此 systemctl restart network 命令已经不再被使用了，目前可以使用 nmcli 命令来管理网络连接，以下是一些常用的 nmcli 命令：nmcli connection show： # 显示所有的网络连接。 nmcli connection up # [连接名称]：启用某个网络连接。 nmcli connection down # [连接名称]：禁用某个网络连接。 nmcli device status # 显示所有网络设备的状态。 nmcli device wifi list # 列出所有可用的 Wi-Fi 热点。 nmcli connection add help # 列出所有适用于 add 的参数信息如果您需要修改网络连接的配置，可以使用以下命令：nmcli connection modify [连接名称] [选项] [值]：修改连接的配置，例如 IP 地址、网关、DNS 等。注意对于任何更改都要谨慎，以免影响系统的网络连接，最可怕是禁止使用网卡导致远程 SSH 连接断开。以下是一些常用的 nmcli connection modify 命令选项：参数名称参数作用ipv4.method设置 IPv4 的获取方式，可选值为 auto、manual、dhcp 或 shared。ipv4.addresses设置 IPv4 地址，格式为 IP 地址/掩码，多个地址之间用逗号分隔。ipv4.gateway设置 IPv4 网关地址。ipv4.dns设置 IPv4 DNS 服务器地址，多个地址之间用逗号分隔。ipv6.method设置 IPv6 的获取方式，可选值为 auto、manual 或 ignore。ipv6.addresses设置 IPv6 地址，格式为 IP 地址/前缀长度，多个地址之间用逗号分隔。ipv6.gateway设置 IPv6 网关地址。ipv6.dns设置 IPv6 DNS 服务器地址，多个地址之间用逗号分隔。例如要修改名为 eth0 的有线网络连接的 IPv4 地址为 192.168.1.2/24、网关为 192.168.1.1，DNS 服务器为 8.8.8.8 和 8.8.4.4，可以执行以下命令：nmcli connection modify eth0 ipv4.addresses 192.168.1.2/24 ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8,8.8.4.4"nmctl 不仅可以配置网络参数，还可以基于现有多网卡设备创建新的网络会话连接，基于一张网卡创建一个新的网络环境，例如在公共场所使用一个网络会话，在家里用另外一个网络会话，配置参数如下：参数说明add添加网络会话type网络类型ifname网卡名称con-name会话名称例如使用 eth0 网卡创建一个名字为 house 网络会话：sudo nmcli connection add type ethernet ifname eth0 con-name house最后使用 nmcli 配置这个网络会话参数信息：nmcli connection modify house ip.addresses 192.168.31.1/24 ip.dns "8.8.8.8"如果需要给网卡进行起别名，假设想为 eth0 的网卡设置别名为 my-eth0 ，则可以按照以下步骤操作：# 列出可用设备 nmcli device # 给你网卡起别名 nmcli connection modfiy eth0 connection.id my-eth0 # 删除指定的网卡 nmcli connection delete my-eth0以上是基本网络设备管理和网络设备配置基础，针对环境是物理网线环境，而某些设备采用的是 Wi-Fi 连接的网络的模式，上面的方式将不太适用了，使用下面命令进行配置扫描和连接配置：# 扫描附近的 Wi-Fi nmcli device wifi rescan # 查看可用的 Wi-Fi 网络 nmcli device wifi list # 连接到 Wi-Fi 网络 nmcli device wifi connect SSID password PASSWORD # 端断开 Wi-Fi 网络 nmcli device disconnect wlan0其他 DNS 和 IP 、网络 IP 获取模式可以通过和普通网桥连接到方式一致。虚拟网卡在生产环境大部分服务器都配置多张网卡，我们可以将多张网卡绑定成为一个张虚拟网卡，从而到达提高网络利用率和网络传输速度，更重要的功能是当其中一张物理网卡不能正常工作时，系统会自动切换到另外一张网卡进行工作，把多张网卡绑定成为一张虚拟网卡共用一个固定的 IP 地址。例如创建名字为 bond0 的虚拟网卡，要求这张虚拟网卡必须由物理网卡 ens160 和 ens161 组合而成的，并且指定数据负载模式为轮询：# 先创建虚拟网卡 nmcli connection add type bond ifname bond0 con-name bond0 bond.options "mode=0" # 往虚拟网卡添加物理网卡 nmcli connection add type ethernet slave-type bond con-name b1 ifname ens160 master bond0 nmcli connection add type ethernet slave-type bond con-name b2 ifname ens161 master bond0上面命令中的 connection 参数可简写为 con ，type 类型为 bond 为绑定模式，mode 模式也可以简写成为数字，其他参数在上面基础中已经说明了，mode 模式数字表示对应表为下：数字说明0balance-rr 默认轮询负载1active-backup 故障自动转移2balance-xor 基于MAC地址的负载均衡3broadcast 数据包同时发送到所有从属接口上4802.3ad 这个模式需要交换机支持802.3ad协议5balance-tlb 发送数据包时进行负载均衡，接收数据包时不进行负载均衡6balance-alb 与 5 类似同时也在接收数据包时进行负载均衡配置完成之后，可以用两台在同一网段的电脑测试一下效果：# 一直 ping 查看回包 ping xxx.xxx.xxx.xxx # 关闭其中一个网卡 nmcli connection down b2如果其中一张网卡关闭了，ping 还能正常响应说明配置正常。SSH 连接Secure Shell 是 SSH 的全称，这是一种安全的网络协议可以帮助远程登录管理 Linux 系统，传统方式采用的 FTP 上传文件不安全，后面出现了 SFTP 和 scp 命令进行传输文件；而早期的 Linux 管理采用的是 Telnet 协议进行连接，密码和账号都是明文存储的，从而出现了 SSH 协议和相关实现了 SSH 协议的软件，一般服务器端 Linux 默认都已经安装了 sshd 程序，如果没有安装可以执行下面命令：# 安装 sshd 软件 sudo yum install openssh-server # 设置开机自启动 systemctl enable sshd最后如果是服务端需要配置一些 sshd 软件运行时的规则，默认的配置文件路径位 /etc/ssh/sshd_config 目录中，sshd 支持加密通信的协议，所以可以配置非对称加密使用的密钥对，配置模版如下：# 端口号，如果不指定默认为 22 # Port 22 # 监听地址，如果不指定默认为所有接口 # ListenAddress 0.0.0.0 # 协议版本，可以指定多个，使用逗号分隔，如 "2,1" Protocol 2 # 是否启用公钥验证 PubkeyAuthentication yes # 是否允许密码验证 PasswordAuthentication no # 是否允许 root 用户登录 PermitRootLogin yes # 是否启用 ChallengeResponseAuthentication，即是否启用 PAM 验证 ChallengeResponseAuthentication yes # 是否启用 GSSAPI 验证 GSSAPIAuthentication no # 是否启用 X11 转发 将远程 GUI 程序显示到本地 # 登录时使用参数 -X ssh -X username@remote_host X11Forwarding on # 是否启用 TCP 端口转发 TCPKeepAlive yes # 登录时最大重试次数 MaxAuthTries 3 # 最大同时连接数 MaxSessions 10 # 公钥路径 AuthorizedKeysFile .ssh/authorized_keys # 客户端连接时 banner 的显示内容 # 超级酷可以自定义 ssh 协议登录时的提示消息 # Banner /etc/issue.net # 启用 Syslog 日志 SyslogFacility AUTH LogLevel INFO对应一般需求开启 root 登录工具将可以满足日常开发需求，如果想酷炫一点可以修改客户端连接时 banner 的显示内容，下面这样的：# vim /etc/issue.net ██ ██ ██ ░██░██ ██ ██ ░██ ░██░██ ░░██ ██ ██████ █████ ░██░██████ ░░███ ░░░██░ ██░░░██ ░██░██░░░██ ░██ ░██ ░███████ ░██░██ ░██ ██ ░██ ░██░░░░ ░██░██████ ██ ░░██ ░░██████ ░░ ░░░░░ ░░ ░░ ░░░░░░ welcome to www.ibyte.me ![scode type="yellow"]部分系统在 sshd 中设置 banner 可能不起作用，需要在 \etc\motd 文件中设置，例如：[/scode] ,%%%%%%%%, ,%%/\%%%%/\%% ,%%%\c "" J/%%% %. %%%%/ o o \%%% `%%. %%%% _ |%%% `%% `%%%%(__Y__)%%' // ;%%%%`\-/%%%' (( / `%%%%%%%' \\ .' | \\ / \ | | \\/ ) | | \ /_ | |__ (___________))))))) 一键3连！！更多技术干货 👉 https://blog.leonding.me这样配置之后会在下次 SSH 登录系统时提示保存的内容。如果开启 X11 转发功能，默认可以将远程的 GUI 程序在本地计算机上显示会占用一些网络带宽，但是本地计算机需要安装一些支持 X11 的软件，Windows 上有 WinSCP 和 Xming，MacOS 有 XQuartz，不推荐使用这些还是推荐基于 VNC 协议的软件，Geek 应该直接敲命令才对。基于 SSH 免密钥登录需要生产密钥对，最后将其公钥拷贝远程需要登录到服务器中实现免密登录，执行下面命令# 生成密钥 ssh-keygen -t rsa -b 4096 -C "ding@ibyte.me" # 拷贝密钥 ssh-copy-id xxx.xxx.xxx.xxx光通过 ssh 协议来管理远程服务器功能还是比较单一不能传送文件，目前可以基于 SFTP 一些客户端来实现文件传输，命令行版本可以使用 scp 命令进行传输文件，例如下：# 将某个目录传输到服务器 root 目录下 scp -r /xxx 192.168.31.10:/root # 下载某个目录文件 scp -r 192.168.31.10:/root/jdk /download默认情况下不借助其他第三方 ssh 连接管理软件，可以使用 ssh 默认目录下编写配置文件来管理多个 ssh 连接，配置文件如下：# 编辑 vim ~/.ssh/config Host cs9 HostName 172.16.19.128 User root #Port 22 #IdentityFile ~/.ssh/id_rsa_test默认这种 ssh 会话连接方式在实际操作过程中可能会出现断开情况，不能正常回复会话，在执行某个命令时断开连接就不能继续执行任务，或者执行任务整理一个窗口不能做其他事情，解决这个问题可以使用 terminal multiplever 软件来管理多个 ssh 会话连接，基本的常用命令：# 安装 terminal multiplexer dnf insatll -y tmux # 创建一个会话 tmux new -s backup # 临时离开隐藏会话 tmux detach # 查看当前 tmux 中会话 tmux ls # 恢复之前保存会话 tmux attach -t backup # 直接运行 shell 命令 tmux new "echo 'test text' > test.txt" # 连接到被分享服务器会话中 tmux attach-session -t share上面为基础会话管理命令，下面介绍如何在一个大屏幕上使用 tmux 开启多个小窗口执行更多任务，命令如下：# 分裂出一个新查看，左右分割，默认上上下分割 tmux split-widnow -h # 在多个窗口之间切换，向右切换 —R 向左切换 -L tmux select-pane -R # 上下切换窗口，向上切换 -U 向下切换 -D tmux selelct-pane -U这样就可以不用鼠标也能控制整个 tmux 窗口和管理 ssh 会话；但是在 MacOS 上有快捷键冲突问题，导致不能被正常使用这时得需要重新设置 tmux 的快捷键绑定方式，tmux 快捷键配置文件路径， ~/.tmux.conf ，使用默认 vim 命令编辑添加下面参数：# 将`select-pane`快捷键更改为指定的键，避免与Mac上的快捷键冲突 unbind-key Up unbind-key Down unbind-key Left unbind-key Right bind-key -n C-h select-pane -L bind-key -n C-j select-pane -D bind-key -n C-k select-pane -U bind-key -n C-l select-pane -R bind-key C-n windwo-split -h最后保存并关闭配置文件，然后重新加载配置：tmux source-file ~/.tmux.conf现在你可以使用新的快捷键来切换 tmux 窗口了。其他资料Open SUSE 基本联网知识What Is SSH
在传统单体项目中，一个项目的整体都部署在一台服务器之上，组成这个系统的模块可能包括了 Java 应用程序和 Oracle 数据库 ，可能一些项目会加快系统处理请求的性能还会引入缓存模块，在项目不停迭代的过程中可能会出现分布式架构，将数据库和应用程序模块分别部署到不同几台服务器上，这样单台机器的性能就会得到提升。应用程序也可能会分拆成为多个模块，部署到多台服务器之上，这时面临一个问题就是如何将外部的请求分发到不同模块服务器之上，这时就需要一种反向代理服务器将请求分发到不同应用服务器之上。 Nginx 就是这样一种能够具备反向代理负载均衡功能软件，它不仅可以作为 Web 服务器来使用，还能作为网关二次开发功能，也能作为静态资源的服务器并且提供一些静态文件缓存功能。负载均衡至于怎么安装 Nginx 不是本篇文章的重点，安装可以查看官方的文档 官方 Red Hat/CentOS 软件包，能做负载均衡的功能有很多种方式例如云计算厂商的 DNS 负载均衡服务，我经常使用的是 Ngnix 的负载均衡服务可以配置权重，权重越重的服务器处理的请求会相对比较多，默认是轮询的方式处理。首先先使用 server 关键字定义虚拟服务器，然后指定 listen 关键字需要监听的流量端口。接着使用 location 块匹配客户端请求的 URL，再用 proxy_set_header 关键字指定 Host 的 HTTP 请求头，最后用 proxy_pass 关键字将请求转发到 upstream 组，upstream 块定义了 NGINX 用来均衡流量的服务器，一个简单配置模板如下：http { upstream my_upstream { server tomcat1.example.com; server tomcat2.example.com; } server { listen 80; location / { proxy_set_header Host $host; proxy_pass http://my_upstream; } } } NGINX 还可以针对 TCP 应用程序（如 MySQL）和 UDP 应用程序，如 DNS 和 RADIUS 进行负载均衡，对于 TCP 应用程序，配置模板：stream { upstream my_upstream { server server1.example.com:1234; server server2.example.com:2345; } server { listen 1123 [udp]; proxy_pass my_upstream; } }目前 Nginx 支持几种负载均衡策略，为 HTTP、TCP 和 UDP 提供多种负载均衡方式，所有这些方式都能够选择分配到每个上游服务器的流量的权重：Hash： 根据特定的键值分发请求如客户端 IP 地址或请求 URL 上。最少连接： 向活跃连接数最少的服务器转发请求。最少时间： 基于响应时间和活跃连接数进行综合计算，将请求转发到负载最小的服务器。IP Hash： 根据客户端 IP 地址的前三个八位字节进行请求分配。Round Robin 轮询： 默认使用可按顺序在上游服务器之间分发请求。Random with Two Choices： 随机选择两台服务器并将请求转发到活动连接数较少的服务器。默认 Nginx 常用也就是加权重版本的负载均衡，默认有还有一些权重 Tag 的策略，配置如下：upstream tomcat_group { server 192.168.1.16:8080 down; server 192.168.1.18:8080 weight=2; server 192.168.1.13:8080; server 192.168.1.12:8080 backup; }down: 表示当前的 Web Server 暂时不参与负载。weight: 默认为1 Weight 越大，负载的权重就越大。backup: 其它所有的非 Backup Server Down 或者忙的时候，请求 Backup 机器，所以这台机器压力会最轻。静态缓存在计算机中有很多缓存的设计典型就是 CPU 和 硬盘之间的缓存设计，而对应业务开发人员来说大部分使用都是一些缓存中间件来提高系统性能，这里的缓存最大作用将已经计算过的对象数据通过缓存存储起来，下次有类似的需求的时候直接从缓存中返回而不是从数据库中读取，这样设计为通读缓存能降低数据源的计算压力。这里要介绍的是 Nginx 作为静态文件服务器时，可以做的静态文件缓存功能，因为很多静态文件是不变的一次写入很少修改了，所有如果有缓存功能支持，每次处理请求时就从缓存中返回，这种架构如下：每当浏览器访问 CSS 、HTML 、JS 静态文件时可以开启 Nginx 的缓存功能，使得这些文件能被缓存，开启这个功能只需要在对应的域名下配置文件加入一些配置项：nginx user nginx; pid /run/nginx.pid; worker_processes auto; worker_rlimit_nofile 100000; events { worker_connections 2048; multi_accept on; } http { sendfile on; access_log off; error_log /data/log/nginx-1.8/error.log error; # 缓存数据目录，Ngnix 不会主动创建需要手动创建 proxy_cache_path /data/nginx-1.8/cache levels=1:2 keys_zone=cache_zone:10m inactive=60m; server { listen 80; server_name localhost; root /usr/local/services/nginx-1.8/html/; location / { } # 缓存配置 location ~.*\.(gif|jpg|png|css|js)(.*) { # 设置是否开启对后端响应的缓存，如果开启的话，参数值就是zone的名称 proxy_cache cache_zone; # 响应状态码为200 302时，10分钟有效 proxy_cache_valid 200 302 10m; # 缓存过期时间 expires 1d; add_header X-Proxy-Cache $upstream_cache_status; } } }配置管理如果使用的 CentOS 系统默认的 YUM 包管理安装的 Nginx 软件，正常启动之后会有一个提示页面，此页面会帮助服务器管理员来管理 Nginx 软件，配置文件路径和静态文件目录都在页面有提示。如果无法查看到此页面，可能是默认的服务器防火墙软件阻止 Nginx HTTP 访问端口，使用以下命令来放行端口：sudo firewall-cmd --zone=public --permanent --add-service=http另外大部分情况 Linux 服务器从配置完成到服务运行都会产生关机状况，如果有特殊情况关机了，就需要手动启动这些 Nginx 服务，这里可以将 Nginx 设置为开机自启服务：# 设置开机自启 sudo systemctl enable nginx # 关闭开机自启 sudo systemctl disable nginx其他资料F5 NGINX 负载均衡器Installing NGINX PlusNginx的反向代理缓存A Guide to Caching with NGINX
本文要介绍的是UDP传输协议，相比前面一篇Blog介绍的TCP协议来说要简单的多。UDP不需要建立连接也不需要考虑对方是否收到数据包，也不用考虑丢包重传机制，所以对于UDP的设计要解决的问题来说要简单的多了，UDP更关注的是把用户的数据发送出去就即可完成任务。UDP适用于不需要或在程序中执行错误检查和纠正的应用，它避免了协议栈中此类处理的开销。对时间有较高要求的应用程序通常使用UDP，因为丢弃数据包比等待或重传导致延迟更可取。UDP短数据UDP协议更关注的是用户的数据和发送用户的数据达到互联网上，剩下的步骤就交给物理的网络链路设备了，至于对方能不能收到此数据包UDP协议是不用关心的；但程序员自己可以参考类似于TCP那样的协议在设置超时机制来判断对方是否收到，或者说通过对方回传UDP数据包的方式来保障，那这种UDP通信方式把数据包的传输可靠性交给了用户来控制。udp协议的包头部分相比tcp的包头字段要少得多了，发送端口、接收端口、数据长度、数据包完整性校验和组成的一个数据包，udp只需要关注的是一次性传输的数据。比如DNS的域名查询请求，用户态的程序只需要把数据放入缓冲区然后使用系统调用接口即可发送出去数据包。UDP设计原则第一发送数据之前不需要建立连接，传输数据因为每次适用于小数据包场景也不用考虑丢失重传机制，即使如果真的要重新传输用户态程序可以自定义重传规则，因为没有通信连接状态机所以断开连接也不需要进行复杂的状态机同步断开机制。udp协议可以发送的数据最大长度为IP包的最大长度减去IP包头和自己本身的UDP包头的长度，这里的长度和MTU、MSS不是一个层面的东西，MTU和MSS是基于以太网和通信链路上网络包的最大长度来计算的。从IP协议来看包最大长度为65535，这里减去前面所有包头部分剩下65507字节，当然这么长的长度肯定是超过了MTU允许的范围，所以大数据包传输的时候还是依赖于IP模块拆分包的，所以建议使用UDP协议传输数据的时候使用数据量不多的时候。udp协议常用场景就是直播视频和实时语音通话了，因为追求的是实时性不需要考虑丢包重新传输情况，所以udp非常适合；如果像tcp协议去做的话，数据包丢失了还要考虑重传，可能用户程序会出现卡顿重新加载情况，重新发送数据也会消耗时间，重新发送数据也没有用因为实时性很强的程序是追求的是实时性而不是消息可靠性。此外我们日常使用微信打视频或者语音的时候因为网络情况不好也会出现卡顿情况，这对用户来说是网络问题，而不是实时性问题，用户没有听到也可以要求对方人为重复一遍，哈哈哈哈。小 结由于UDP缺乏可靠性且属于无连接协议，所以应用程序通常必须容许一些丢失、错误或重复的数据包，某些应用程序如TFTP可能会根据需要在应用程序层中添加基本的可靠性机制，具体怎么运用UDP这得看开发者自己的需求了，如果开发者足够了解这些协议应该会做出明智的选择。其他链接TCP vs. UDPUDP in Depth用户数据报协议最大传输单元MTU网络知识十全大补丸
在计算机网络中的传输层TCP/UDP协议，都是操作系统网络协议栈提供好的接口供开发者使用，但是大家都会听一些有经验的程序员说TCP协议比UDP协议可靠，这是为什么呢？都说TCP协议提供可靠传送，面向连接性的，基于字节流的，数据包不丢包这是怎么做到的？本文将讲解一些关于TCP协议内部实现一些细节。计算机能实现网络通讯全部依靠着是互联网，互联网是有多个广域网组成的，通过世界各地网络运营商组成的通讯网络。但是两个计算机依靠着这些网络上的硬件设备进行通讯，这些硬件设备都不能保证网络数据能正常传到对方计算机，这是做不到的。网络环境是一个很复杂的环境，可能两台通讯的计算机一台东半球，另外一台在西半球，而网络数据包则需要通过遍布在地球表面上的物理网络设备来传输（当然这里也可以通过卫星上网，例如STARLINK，当然在某些所在的地区不提供服务...），路由器、集线器、网络运营商的网络传输节点都不能百分之百保证网络环境是正常的，所以在传输层上设计TCP协议，能解决这些问题。连接的状态对应开发者而言我们只需要使用各种编程语言提供好的Socket接口传入目标IP和端口即可得到相应的网络描述符，这是作为一个开发者而说只需要使用API即可完成数据收发工作，但是具体协议内部是如何工作的呢？这里我要剖析TCP协议工作原理，在TCP协议中通讯时分为3部分，不要看就3个状态而已，内部实现其实挺复杂：建立连接数据传送连接关闭相信很多开发者都听说过TCP的3次握手🤝和4次挥手👋🏻这些概念，但是大家有没有想过为什么要这么设计呢？而且这两个概念在TCP协议那个环节做的呢？为什么要这样做？通常建立连接通讯的时候，相应的计算机会在某进程中监听着某个端口，以便有其他客户端发送连接的请求，当有请求过来的时候会触发TCP建立连接的规则，这里的规则就是三次握手。在说连接建立之前的得先说明一下每个TCP数据包都要包含的TCP数据包头，TCP包头尺寸除非存在选项，否则其正常大小为20字节，如下图：我整理一个表格分别记录其对应的作用或者用途如下：字段名称作用说明源端口用于记录数据包发送者的端口目的端口用于记录数据包接收者的端口序列号TCP为TCP段中包含的每个数据字节分配一个唯一的序列号确认号下需要从对应值位置开始的序列号，它总是最后一个接收到的数据字节的序列号加1包头大小它包含TCP包头的长度，数据从哪里开始读数据窗口它通告发送者无需确认即可接收多少数据校验和发送方在发送数据之前将CRC校验和添加到校验和字段紧急指针添加到序列号的紧急指针指示紧急数据字节的结尾时间戳根据时间戳，接收方可以识别正确的段窗口尺寸扩展如果接收方希望接收更多数据，则可以使用此字段来通告其更大的窗口大小以上为部分TCP包头一些字段，还有一部分不是很重要的我没有列举出来，至于现在你只需要大致了解其作用，这篇文章后面部分我将在网络可靠性上引用这些标记位和要解决的问题，下面为最重要的部分标记位📌这部分主要做一些数据的特殊处理，表格如下：标记位作用说明URG位它指示接收器当前段中的一定数量的数据很紧急ACK位当ACK位设置为1时，表示TCP头中包含的确认号有效PSH位PSH位用于将整个缓冲区立即推送到接收应用程序RST位RST位用于重置TCP连接SYN位SYN位用于同步序列号FIN位FIN位用于终止TCP连接这些标志位在数据包通讯过程中起到很重要的作用，这些消息会数据包头部存储，客户端会服务器通讯的时候会携带着，以上的数据包头部为TCP控制信息，会在连接建立到结束的每次数据通讯都会有。连接的建立TCP在初始化连接建立时其实就为了交换两台计算机的控制信息，这里的控制信息一部分上面我已经讲过了。控制信息一般都是存储在操作系统提供的网络描述符中的，但是在首次建立连接的时候这些信息起到关键的作用，首先我们要了解一下一个标准的TCP数据包是什么样子的？我们主要关注的蓝色部分的控制信息的转换工作，蓝色部分为TCP数据包的包头信息也就是上面介绍的那些字段集合部分。初始化连接的时候就和我们日常生活中习以为常的打电话联系别人一样，在打电话的时候我们要知道被呼叫的那个人手机号，那我们要怎么做呢？日常生活中我们都线下见面留一个电话号然后保存起来，然后下次需要呼叫时候自己拨打即可。换到TCP协议建立连接时就是目标机器的IP和端口号信息，然后操作系统协议栈会帮助我们建立连接，这个过程非常复杂的。计算机网络环境非常复杂的，你不知道当前互联网设备是否存在网络延迟或者说某个光缆被施工队挖断掉了导致数据发送失败等等情况......下面为一个标准的TCP建立连接的过程，如下图：TCP建立连接为什么是3次数据包交换，而不是两次？这个跟互联网网络环境因素有很大关系；注意数据包中对应的标记位是什么意思这里我不过多叙述，不是我要说明的重点，如果读者你不知道这是你自己的问题自己解决！建设一个连接请求建立过程如下：为什么双方要发出第3个数据包才能确认连接建立呢？不要看就上面几条线而已，其TCP数据包的交互过程受到网络的因素非常大。看上图中的红线部分就为2次握手建立连接的方式，由于第一次发出去的数据包在网络传输的过程中出现了延迟，导致到达服务器端的时候数据包超过了TCP协议默认的超时限制，这个超时机制是什么？这里要说明的是TCP默认对发送出去的请求数据包设置有超时机制，会有一个时间窗口范围来控制数据时间。例如被呼叫端没有返回ACK包，则认为数据包超时并且会重新发送一次数据包，问题就在这由于要这些超时重发机制导致一些很多矛盾的问题要解决。假设上面的TCP对一个连接请求超时机制设置为150ms，上图中的红线部分，第一个请求发送出去了由于网络线路上出现抖动总耗时为300ms才达到的服务器端；与此同时客户端由于超时机制触发一次重发数据包对应的为蓝线部分，这次由于蓝线数据包没有遇到网络问题很顺利就达到服务器端耗时100ms，并且返回ACK包；那么问题来了在这个时候红线数据包到达了服务器端，服务器端也返回一个SYN包，此时客户端将认为哪个连接是正确的？或者说怎么防止重复建立连接？上图红线部分的请求报文在由于TCP发送方超时机制的影响已经被视为无效的报文请求了，但是请求报文还会在网络上传输导致服务器最终还是收到了此次请求并且认为建立连接；此时TCP为了解决这个问题在设计的时候，会让客户端在接受到服务器SYN包时还会向对方发送一次ACK包，如果第三个ACK被服务器端收到才会建立连接。这样即使第一次发出去的延迟请求包头到达了服务器端，服务器端再返回的SYN包，此时的客户端上已经有对应的网络描述符了，就不需要重新建立连接了，并且连接双方的网络描述符状态机已经发生了改变，不需要二次更改了。通过上面实际数据交换过程来证明了为什么TCP建立连接要3次握手的原因，但是这些问题还是TCP协议众多设计问题中的其中之一，下面我将开始讨论TCP协议如何保证数据可靠性传输，数据包存在乱序问题，怎么为何两台计算机的状态一致性问题...数据分包问题连接建立之后，两台计算机就开始通过互联网来保证数据交换，但是上面TCP建立连接都存在网络环境问题，那在连接建立成功之后该如何来保证每次数据包不丢失问题呢？并且可能两台计算机传输的数据文件很大，该如何传输这些保证数据文件能精准传输到对方计算机呢？这都是TCP协议要在设计的时候考虑到的问题。了解TCP数据包传送之前，我们的先了解一下TCP数据包是在整个网络协议栈中处于什么位置和什么样子的，TCP/IP模型中各层数据包结构的关系如下图：TCP完成3次握手控制信息和状态机转换后连接建立成功，两台计算机就形成一个网络通道，示意图：为什么不用一个数据包就把数据发生到对方计算机，然后就一次性发送完就结束通讯？首先TCP是面向连接的，也就是两台计算机双方可以主动发出数据包，任何一方都可以在连接没有关闭的情况下收到对方发了的数据包，例如下面两台计算机打开连接通讯，这里设计者就考虑数据是在连接没有关闭之后都是无限制的数据；对应开发者写数据就调用一个write接口写入字节流数据即可，但是底层怎么实现的？TCP为了解决这些问题会底层，这个又操作系统决定的，在操作系统内部分配一个缓冲区，根据我们每次写入的数据都被放入缓冲区，然后又协议栈来决定什么时候发送出去。但是带来一个问题就是有可能一次写入数据太多了，也可能一次数据太小，那么何时发送这些数据都带来很大的问题...这就你有一个手推车去取行李运到另外一个地方一样，如果一次性拉的太多了就会很重，换到TCP里面就是一次性运输数据量很大导致网络拥堵；如果每次都发送数据很小那又导致需要多次发送，这就要可能如何设计缓冲区发送频率和数据包大小了？一个数据包的长度取决于当前网络协议栈能容纳的数据长度，这个是怎么意思？这里拿PPPoE来说一个标准的MTU最大传输单元是1500字节，但要去除一些40字节的头部信息占用，剩下就1460字节可以用来存放数据了。回到上面的问题一个大于1460字节的数据就要拆分成多份数据包进行发送了，数据包大小和发送频率问题就出现这这里，如果要数据每次很小不能接近MTU大小就不进行发送就会出现低延迟，如果直接发送每个MTU数据区有得不到充分利用。上面所讨论这些都是矛盾因素TCP数据包传送要考虑的；如果按照长度优先那么网络的效率会提高，如果时间优先，网络链路上的数据包会变多，导致降低网络传送效率或者阻塞情况...这些问题在TCP滑动窗口算法来解决的问题，但是本文不会介绍这些因为TCP这个协议太复杂了；并且为了解决这些问题Google在2016年曾发布过他们研发的BBR加速算法，在本文末尾处有提供相关链接。数据可靠传输TCP协议对每个段字节序数据都会分配一个编号，这和开发中日常使用的数组下标一样一个意思，只不过是一个字节序列；协议栈会为每个已经存储在缓冲区里面的数据分配一个序号，如下图：发送数据过程其实很简单，这个数据交换过程是可以任意一方的，例如上图服务器发起一个数据请求，这是TCP包头上的那些字段位就派上用场了，可以在请求头序号填写好数据段从哪里开始，如果要数据段大小规定也可以在请求头中设置。如果没有则直接读取1460字节的数据，而接收方只需要减去包头大小就能获取全部数据，然后放到本地缓冲区拼接起来。TCP如何保障数据包不能丢的呢？发送方每次方式完的数据窗口里的数据都会被继续保存着，指定服务器端回复ACK编号即可，每次的ACK编号都是ACK=seq+size，这里的size就是每次接收到数据包大小，这样客户端也会根据ACK来确认数据完整性。上图为双方数据确认过程，如果接收方回复ACK=size+seq说明在这序号之前的数据都已经被接受或者确认完成，具体实现细节可以查看有关资料，TCP这个协议实现确实太复杂了，由于接受者和发送者双方出来的数据速度不一样并且还受到网络因素的影响比较大，还需要考虑动态调整发送频率算法，也就是TCP中的滑动窗口算法和納格算法，这些实现细节极其复杂，我不可能做的面面俱到。TCP状态机在两台独立计算机在通过TCP协议通道连接下，因为是全双工的通讯所以双方都发起连接关闭请求，这也对应着TCP状态机和状态机转换，那什么是TCP状态机呢？我个人有这么一句话定义TCP的状态机： 可以认为TCP状态机就是随着两台互相通讯的计算机随着连接的变化的过程，状态机的状态可以描述一个连接的变化情况； 下图就为TCP状态机的状态关系图：图中红实线表示客户端正常的状态变迁图中蓝实线表示服务端正常的状态变迁虚线用于不常见的序列，如复位、同时打开、同时关闭等等客户端特有的状态：SYN_SENT、FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSED；服务端特有的状态：LISTEN、SYN_RCVD、CLOSE_WAIT、LAST_ACK、CLOSED； 共有的状态：CLOSED、ESTABLISHED。TCP状态机其实就是TCP通信协议的生命周期，看了上面的图片是不是觉得这个状态机极其复杂，其实如果能够完全理解TCP连接过程和断开过程其实也不太复杂，并且状态要搞清楚之间的关系，下面是每个状态的说明：状态状态说明LISTEN等待来自远程TCP应用程序的请求SYN_SENT发送连接请求后等待来自远程端点的确认，TCP第一次握手后客户端所处的状态SYN-RECEIVED该端点已经接收到连接请求并发送确认，该端点正在等待最终确认，TCP第二次握手后服务端所处的状态。ESTABLISHED代表连接已经建立起来了，这是连接数据传输阶段的正常状态FIN_WAIT_1等待来自远程TCP的终止连接请求或终止请求的确认FIN_WAIT_2在此端点发送终止连接请求后，等待来自远程TCP的连接终止请求CLOSE_WAIT该端点已经收到来自远程端点的关闭请求，如果还有数据为发送继续发送，然后回复FIN确认同意关闭来连接LAST_ACK等待先前发送的FIN到远程TCP的连接终止请求的确认CLOSING等待来自远程TCP的连接终止请求确认TIME_WAIT等待足够的时间来确保远程TCP接收到其连接终止请求的确认连接建立主要会用到前3个状态，剩下的ESTABLISHED状态为正常通信状态，剩下都为连接断开所需要的状态，关于TCP状态转换的视频可以查看：TCP协议状态机转换过程。断开连接关闭连接的步骤比建立连接的步骤复杂的多，因为也要考虑到网络的因素比较大整个网络通信链路是不可靠的，断开连接就要更改双方的状态机；由于通信的双方是全双通信而工作的，断开连接可以由其中一台计算机发起，所以双方的TCP协议的状态变更都要保持同步，任何一个状态都需要2次确认才能进入下一个状态。上图是我画的TCP关闭连接的时候的状态图，记住一点：通信的双方的状态机的更新必须是收到对方的回应包才能更新，这里的回应包有ACK包和服务端发来的FIN包，只要没有收到回复的包，其状态一直保持当前状态并且触发数据包重发机制，没有等到回应就重发数据包。了解到上面我说的规则之后，再来看看为什么要4次挥手关闭连接。上图中的蓝色线部分表示网络链路没有出现阻塞或者上说抖动机制，数据包顺利到达对方；红色线部分表示此数据包可能在传输过程中超时或者某些原因对方不能收到，红色虚线部分表示因为没有收到ACK包，触发了数据包重发机制；绿色虚线部分表示正常的缓冲区数据的还没有传输完成再进行传输，等传输完成之后才能关闭连接。这里主要关注的是TIEM_WAIT状态： 服务器在发送缓冲区剩下的所有的数据之后会发一个FIN包表示自己也要进入关闭状态，此时客户端收到之后会回复一个ACK包，如此同时服务器的也在等待ACK的到来，只要最后一个ACK到达服务器端，服务器就关闭连接。但是问题就出现在这个ACK包上，如果最后一个ACK发出去了对方没有收到，对方肯定会重新发送一个FIN包表示重新同步状态，这个过程以此类推，这里的TIEM_WAIT作用就要有足够的时间让ACK传输并且看看有没有重试的FIN包，如果有客户端会重新刷新一下TIME_WAIT时间，这里Linux内核中的时间为60秒；也就是日常大家所说的“让子弹多飞一会的”装逼的话的意思差不多，至于子弹能不能命中目标那得看运气，如果没有命中就重新来一次，递归，直到命中为止。小 结两台计算机在通信的时候靠着的网络，但是网络是一个非常不可靠的因素，随时都会出现各种问题。TCP协议虽然说是面向连接的，但是在网络链路上哪里有真正的连接呢？都是依靠着软件的机制保障着可靠的连接状态，TCP之所以叫做TCP协议，都是双方约定好的规则来更新两台相互独立的状态机；网络连接的计算机就是一个分布式计算机系统，由于网络链路上各种不可靠因素所以出现了各种分布式系统问题，分布式脑裂、分布式CAP、分布式BASE、分布式Raft一致性算法，这些都是基于网络延伸出来的问题，换到程序身上就是要对可能发生的错误提前编写好错误处理程序，降低程序出错频率，设计一个基于网络同步状态的分布式系统是非常复杂的。其他资料UDP vs TCP Principles of Reliable Data TransferTransmission Control ProtocolKafka通讯协议指南Open System Interconnection ModelTransmission Control ProtocolWhen to Use Which Protocol《网络是怎样连接的》常见 TCP 拥塞控制算法TCP 的那些事儿（上）Google BBR论文：基于拥塞的拥塞控制Google BBR视频：拥塞控制算法原理BBR加速器的原理是什么？计算机网络 - TCP 协议原理总结TCP Buffer Ring什么是三次握手与四次挥手?TCP状态机变化过程Coping with the TCP TIME-WAIT state on busy Linux servers
在21世纪互联网几乎已经融入了每个人的生活了，有了互联网使得我们的生活变得更便利的同时，有没有想过互联网倒是是什么？互联网是通过怎么样的方式把每个人连接在一起的？互联网是如何传递我们产生的数据的？当我们在浏览器打开一个网页的时候这个网页是怎么显示出来的？我写这篇文章目的就是要介绍互联网是什么，并且我自己也是一个Z时代，我出身就和互联网融入在一起了。John Gage在1980年代提出了"THE NETWORK IS THE COMPUTER!"宣传口号，之后被Sun使用了数十年，直到 2010 年被Oracle收购。根据Sun前营销总监Ray Rothrock的解释，这个口号的意思是通过个人计算机你在网络中有了一扇窗，如果有合适软件的话你还可以使用其他人的计算机。早期的计算机程序都没有统一的系统，而是各自厂商开发的程序，并且都是运行本机上的。直到后面美国军方把ARPANET开始慢慢民用，才发展起来了互联网。之后每个人的个人计算机就对应上面那句话：通过个人计算机你在网络中有了一扇窗。现在互联网更应该是物联网，接入网络的设备不只是是传统的计算机和手机了，更多家用电器，工业互联网电冰箱、台灯、微波炉、智能汽车等，都接入到互联网中。WORLD WIDE WEB上面说计算机相当于一个连接世界的窗口，我个人认为这里更多的指的是Browser即浏览器，我们可以通过浏览器去访问一些在互联网中其他计算机所提供的服务，我们一般把这种访问其他计算机提供的资源的方式叫做Web网络服务。Web网络服务： 只是一种依赖于互联网计算机提供服务的一种方式，具体的通讯是依靠着HTTP协议进行的。当我们在浏览器输入www.google.com的时候，浏览器就会使用http协议请求到具体服务器上，这里的很多人把www认为是World Wide Web的简称当成了通讯协议，其实并非如此，而是初代网页内容编辑器和网页服务器的简称。World Wide Web只是一种通过互联网的基础之上做的一层网络计算机数据交换的方式；万维网并不等同互联网，万维网只是互联网所能提供的服务其中之一，是靠着互联网运行的一项服务。万维网发源地是欧洲核子研究中心，上面就为万维网的发明者蒂姆·伯纳斯-李，第一代浏览器和HTML编辑器就为他开发的。在万维网还没有普及的时候，大部分个人电脑都是靠着购买软件来使用，当时个人PC市场苹果、惠普，生产软件的公司是像微软这样的公司，靠着卖软件来挣钱的。计算机发展史就是软件行业发展史，最早计算机可能功能单一就是为了计算一些弹道和一些数学相关的，最后各大公司开始研发操作系统软件，在操作系统软件发展中，最后胜利的是微软公司，微软靠着自家的Windows系统占据个人PC市场。微软并没有把目光注重在计算机硬件上，而是更关注计算机软件，怎么充分发挥这些硬件优势。微软开发很多软件，靠着卖软件盈利，例如操作系统，在微软占据了操作系统市场之后几乎就是弄断了整个个人PC市场，除了Apple公司软件和硬件都做以外。在当时大部分软件都是靠着本机运行，直到后面万维网的出现，通过万维网去访问其他计算机上的信息，微软就意识到浏览器的重要性，不能完全依靠着操作系统去占据个人PC市场了，未来的市场应该是在万维网上，提供这一些网络服务的公司都是虚拟的，他们服务大部分运行在自己的万维网服务器上；这也就是后面Yahoo、Google等这些，网络服务公司出现，有浏览器就可以访问这些在线上提供服务的公司。WWW架构现在开发者开发的网络应用服务大部分都是在编写服务端应用或者是服务端的前端应用，都会对依赖于万维网才能正常工作的，所以每个开发者都应该了解万维网是如何工作的。网络应用服务不仅仅是我们日常编写的HTTP协议的应用，现在浏览器还支持更多不同协议例如FTP、mailto、file这些。这里我主要偏重的是HTTP协议进行网络通讯架构介绍：一般我们在浏览器输入一个网址的时候，浏览器会解析输入的网址然后生产请求数据包，但是HTTP是基于TCP协议的在发送数据之前要知道对方的网络服务器IP；这时就需要操作系统请求另外一个服务了DNS，DNS负责解析域名所绑定的IP地址，然后浏览器才能生产请求头和数据包然后交给操作系统发送出去，这就是一次完整浏览器HTTP请求过程。HTTP协议协议这个东西很重要的，做任何事情都需要协议才能达到默契配合，这不紧体现在生活中还体现在计算机的世界里，HTTP协议就是负责浏览器和WEB应用服务器进行数据交换的协议。HTTP是在应用层的协议所以依赖于TCP协议，最后请求数据包是通过tcp发送出去的。所以HTTP需要组织请求数据包，这里的数据包要要按照HTTP协议的规则进行的，例如下面就为一个标准的请求头数据包结构：数据包除了第一行是一些具体请求方式和请求资源路径和协议版本之外，其他一些参数信息是键值对的存在，最后一行为具体请求消息数据实体。在浏览器上打开开发者工具，也可以抓包到每次请求的数据，如下：当应用服务器收到了客户端请求，这时应用程序会根据HTTP协议来解析请求的数据包，这个过程就是拆解数据包，然后处理完对应的请求，响应给浏览器，从而到达一次请求和请求响应工作，下面是响应数据包内容：同样第一行HTTP协议版本和请求状态号码，剩下就是处理请求服务器相关信息键值对格式，这里的状态码和头字段所对应的参数非常多我不会一一列举我只是介绍工作原理。状态码表示的每次请求的状态信息，这是由HTTP协议制定的标准码，然后头字段这些表示客户端一些参数配置或者服务器端参数配置信息；最后是响应数据体；更多相关HTTP资料查看：什么是 HTTP？日常开发中我们使用的最多就是RESTful，这是一种设计提供万维网络服务的软件构建风格，允许客户端发出以统一资源标识符访问和操作网络资源的请求，而与预先定义好的无状态操作集一致化，这里就对应着HTTP协议不同的请求方法了；例如GET、POST、PUT、DELETE等来对URI进行各种操作。但是如果这个静态文件，页面中的HTML嵌入了其他资源链接，一个请求发送到web服务器，服务器返回了页面有其他资源，浏览器会继续请求页面中嵌入的其他资源，这些URI可能是其他服务器上的，和具体提供服务的服务器没有具体的关系，web服务器也不知道具体请求来自那个浏览器发出的，只需要回传的客户端IP和端口号就可以，然后通过HTTP协议响应数据包返回。DNS查询服务上面讲到了当我们在浏览器地址栏里输入一串域名地址，这时浏览器就要去生成一个请求数据包，请求数据包是要通过TCP协议发送出去的，也就是说目标服务器的IP地址我们得提前知道才能生成一个数据包发送出去。这里要说明的是访问WEB服务器也可以不需要通过域名，直接通过IP地址就可以，HTTP服务器默认端口就是80端口。有了域名是为了方便我们能快速找到WEB服务器所在的IP，这个就依赖于DNS服务器查询，域名相当于IP的一个别名；就好像每个人都是手机号，但是手机号是一串数字，我们可以给手机号添加备注名来方便快速查找。DNS服务基本工作原理就是，DNS解析器这里会在每个操作系统都有的服务，解析器依赖于遍布全球的DNS服务器，DNS服务器相当于一个分布式的数据库，DNS服务器上存储了全球的域名绑定的IP映射信息；查询DNS信息的时候对应开发者而言就是调用一个系统提供API查询，但是有一个前提就是操作系统要配置DNS服务器的IP，实际上有很多DNS服务器查询供应商，例如Google的8.8.8.8这个地址就为Google的DNS查询服务。在DNS服务架构是一个层次架构的也就是一个树型结构，域名一般以.做为分割线，由于互联网上的域名和web服务器太多了，一台DNS服务器是不够管理的，所以将域名划分了等级来存储在每个DNS节点服务器，每个节点服务器管理者都是对应着域名结尾的DNS信息，如上图所示；在DNS消息中每条对应的这记录也是有类型区分的如下图：当客户端发送域名记录查询消息的时候就要提供要查询的记录类型信息，这样DNS服务器在处理的时候就可以返回对应value给客户端使用，更多细节可以查看这篇文章：DNS 的工作方式。需要注意的是世界上只有13组根服务器，根域名服务器是什么？我们每次写域名的时候一遍都是些blog.ibyte.me，其实正确的地址是blog.ibyte.me.在域名末尾处还有一个.，这里就是根域，这里对应着DNS根服务器，根服务器会在互联网所有的DNS域名服务器上都有保存，并且所有的电脑中都有根DNS服务器地址信息，所有只要计算机联网就能查到到根域名服务器信息，根域名服务器会有互联网所有域名的所在的DNS服务器信息，这样就能快速越级查找。另外全球的DNS服务器节点都可以临时缓存一些域名信息，这也就是TTL过期时间，买过域名的朋友都知道域名绑定IP生效需要一点时间，有了缓存DNS信息支持，在查询域名信息的时候就不需要去逐步访问每个节点了。Socket通讯Socket就像一个电话插座，负责连通两端的电话，进行点对点通信，让电话可以进行通信，端口就像插座上的孔，端口不能同时被其他进程占用。这是操作系统为每台计算机提供一个特殊的网络操作库，中文叫“套接字”，当两台计算机要通讯的时候就可以使用套接字进行建立连接；创建一个套接字很简单只需要创建者提供被访问的计算机网络中的IP和所在进程的网络服务端口接口，例如在Java中创建一个网络通讯连接代码如下：import java.net.*; import java.io.*; /** * This program is a socket client application that connects to a time server * to get the current date time. * * @author www.codejava.net */ public class TimeClient { public static void main(String[] args) { String hostname = "time.nist.gov"; int port = 13; try (Socket socket = new Socket(hostname, port)) { InputStream input = socket.getInputStream(); InputStreamReader reader = new InputStreamReader(input); int character; StringBuilder data = new StringBuilder(); while ((character = reader.read()) != -1) { data.append((char) character); } System.out.println(data); } catch (UnknownHostException ex) { System.out.println("Server not found: " + ex.getMessage()); } catch (IOException ex) { System.out.println("I/O error: " + ex.getMessage()); } } }应用程序创建套接字需要借助操作系统通过的系统呼叫接口，我们只需要提供IP和端口号，操作系统就会返回一个文件描述符，这个描述符里面就保存了刚才传入的一些连接信息；这就和去超市购买商品时，有寄存东西的寄存柜一样会吐出来一个解锁码，通过解锁码上的标号就能找到对应位置，这里就是套接字的描述符，描述符相当于一个网络通讯入口标识，如下图：服务器端程序会主动监听某个端口，如果有连接请求会创建一个文件描述符保存起来，方便服务器主动发出数据使用；网络协议层次结构数据包最后还是通过操作系统的协议栈发出，这里的具体的层次关系是从传输层发出数据发生到网络IP层最后到链路层，如下图：在计算机网络中网络层次结构被分为了两种，第一种是7层网络模型，第二种是5层网络模型，我个人比较偏向喜欢的是5层模型。5层网络模型就如上图中所示，最顶层也就是我们日常使用的一些特殊的软件或者系统提供好的应用程序所封装的网络协议模型，例如浏览器的HTTP、邮箱的SMTP等，这些都属于应用层的协议，当然如果开发者自己能力很强可以使用传输层协议实现自己的网络协议，例如QQ有自己实现应用层协议。可以协议认为是多个计算机商量好一种处理数据的方式，这也就和平常打游戏时候，游戏得分评判标准一样，有了协议那么就好处理数据了；在计算机中网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。在计算机把这些规则分成多个可以相互协助工作的层次，来达到计算机网络通讯目的，这也就是为什么设计成上图的5层网络模型的原因。传输层TCP： TCP传输协议最大的特点就是可靠性，但是由于要保证通讯的可靠性，性能就会有所折耗；它能够保证数据包的可靠性交付，TCP 能够正确处理传输过程中的丢包、传输顺序错乱等异常情况；此外，TCP 还提供拥塞控制用于缓解网络拥堵。传输层UDP： UDP传输协议相比TCP协议实现要简单的多，效率也是最高的，只要有数据知道目标端口和IP即可把数据发送出去，不需要保证通讯的可靠性；它无法保证数据的可靠交付，相比 TCP ，UDP 不会检查数据包是否到达、网络是否阻塞等情况，特点是常用于分组数据较少或者广播、多播等视频通信和多媒体领域。网络层IP： IP协议主要工作在网络层，这里的网络层就是路由器、交换机、计算机子网等，主要工作目的是将数据包来确定IP所在的网络区域；互联网都是有不同子网组成，IP协议要做的设计就针对TCP和UDP网络路由确定工作，将其分发到具体那个子网络中，以便最终达到目标计算机的子网络；这和现实生活中的邮寄快递一样，要填写好收货人地址，快递公司在世界各地的分公司进行分发操练工作。目前为5层架构网络，网络层还一些其他辅助网络通讯协议，例如ARP，地址转换协议，这里主要用于的是子网内IP地址到MAC地址的翻译工作，例如一个数据包里面只有IP但是不知道MAC地址；这些就需要通过ARP协议，ARP协议就会广播到网络中计算机，如果计算机中的MAC地址匹配就会返回，以便查找到网络中的计算机。一个标准的数据包结构如下：最顶层的数据是属于传输层的TCP协议，至于这块详情文章可以查看我写的一篇文章：何时使用TCP协议；TCP模块主要包含着的是执行连接、收发数据、断开连接等操作，而IP网络层主要是来运输数据包达到目的地的，这和日常生活在快递包裹的运输单差不多。主要负责通过网络集线器和交换机、路由器来区分数据包具体传输到下一个目的是哪里。IP协议可以根据IP头部信息来查询目标地址和下一个路由器的位置，子网中以太网协议会将数据包继续送达到下一个设备，依次递归直到到达目的地为止，能配合完成此项各种的完全依赖于每个网络设备上的路由表。交换机的每个接口都接入一台计算机的物理网卡，交换机可以根据端口号对应着网卡，交换机路由表就可以查询到对应出口，从而把数据从对应的接口发出，交换机主会关注MAC地址，而MAC地址处于OSI层的数据链路层，所以交换机被称为二层设备，IP和以太网MAC的区别主要在于前者只能定位到设备所在的网络区域位置，而以太网MAC可以定位到具体的通信设备是谁。其他资料TCP/IP vs. OSIARPANET Wikipedia互联网发展史The Network is the ComputerCloudflare得到了Sun的“网络就是计算机”商标三十分钟看遍计算机历史博物馆偷来的地基，窃来的图纸微软是如何成为今天的商业巨头浏览器大战所有人都应该了解的浏览器发展史Web3.0 vs Web2.0 vs Web1.0The History Of The Web超文本传输协议RESTful API 是什么意思？什么是RESTful，What is RESTful？Writing Web Server in Python: sockets网络基础 LinuxCast视频教程什么是三次握手与四次挥手IP地址与mac地址是什么交换机和路由器有什么区别？