引入

章节重点

这一章节主要聚焦于套接字编程之上,HTTP这样的应用层协议。最后介绍P2P通信和其应用。

Web和HTTP是这一章节的重点,一些其他的应用层协议也会被简要介绍。

应用层

在应用程序中,同一主机内的进程可通过进程间通信(inter-process communication) 交互,进程间通信遵循操作系统规则。而不同主机的进程通过计算机网络交换消息来通信。

而支持这些应用在设备-设备间通信、设备-服务器间通信,就是网络的终极目标。

在开发应用程序时,我们需要让程序员只专注于应用的功能,而无需关注网络和通信方面的实现,这就是应用层需要干的事情:使用应用层协议,让程序员轻易地迭代升级他们需要的功能。

两种通信架构

客户端-服务端架构

如其名字,客户端-服务端架构下,服务器为用户客户端提供服务。

  • 服务器使用固定的IP地址,几乎始终在线。通过数据中心实现规模扩展(支持大量用户访问)
  • 客户端可能是动态IP,甚至IP经过多层NAT。客户端与服务器通信,客户端与客户端之间不直接通信。

在客户端-服务端架构下,如果多个设备间需要相互通信(例如多人游戏),他们的数据会先被汇总到服务器,再由服务器分发给各自终端。终端间不直接连接。

P2P架构

在P2P架构下,无持续在线的服务器,而是由任意终端系统(arbitrary end systems)直接通信。对等节点(peers)向其他节点请求服务,同时也向其他节点提供服务(即 “我为人人,人人为我”)

在P2P通信下,新节点加入时,既带来新的服务能力,也带来新的服务需求。因此说其自带拓展性。

而且,peers都是间接性联网的,IP地址也会变化,这会让P2P服务管理变得复杂。

P2P通信的主要挑战有如下2点:

  • 安全性(Security):由于 P2P 高度分布式且开放的特性,其应用易受攻击。
  • 激励机制(Incentives):成功的 P2P 应用需说服用户自愿贡献带宽、存储和计算资源。例如,用户的回报是什么?如何避免 “搭便车”(free-riders)现象?

应用层协议会负责那些东西?

应用层协议不再关心数据如何传输,如何保证通信可靠。它的重点在于“如何让应用之间读懂消息”。它涵盖:

  • 交换的消息类型:例如请求消息(request messages)、响应消息(response messages)。
  • 消息语法(message syntax):消息中的字段结构,以及字段的分隔方式。
  • 消息语义(message semantics):消息字段中信息的含义。
  • 消息发送与响应的规则:决定进程何时、如何发送消息以及如何响应消息的规则。、

应用层协议可分为两类:

  • 开放协议(open protocols):由 RFC(Request for Comments,请求评议)文档定义;支持不同系统 / 软件之间的互操作性;典型的有HTTP、SMTP(简单邮件传输协议,用于电子邮件发送)。
  • 专有协议(proprietary protocols):不在公共领域公开;典型的有Skype(其通信协议为私有,仅支持自身系统内的交互)。

Web和HTTP

介绍

万维网(World Wide Web)是客户端 - 服务器架构下极具影响力的互联网应用。而超文本传输协议(HTTP) 是 Web 的应用层协议。

HTTP 由运行在不同主机上的客户端程序和服务器程序实现,用于交换 HTTP 消息。HTTP 定义了 HTTP 消息的结构,以及客户端和服务器交换消息的方式。

网页的构成

网页由多个对象(objects)组成,对象可以是 HTML 文件、JPEG 图片、Java 小程序、音频文件等。

网页包含一个基础 HTML 文件,该文件引用了多个其他对象,一同构成完整的网页。在一个网页中,每个对象可通过统一资源定位符(uniform resource locator URL)寻址。

HTTP概述

http是web下的应用层协议,它的客户端和服务端负责:

  • 客户端(client):浏览器(如运行 Firefox 的 PC、运行 Safari 的 iPhone),负责发起请求、接收(通过 HTTP 协议)并展示 Web 对象;
  • 服务器(server):Web 服务器(如运行 Apache 的服务器),负责响应请求,通过 HTTP 协议发送对象。

HTTP是基于TCP的应用层协议,客户端向服务器的80 端口发起 TCP 连接(创建套接字)。

HTTP 是 “无状态(stateless)” 的:服务器不保存客户端过往请求的任何信息。

维护状态的协议通常是非常复杂的。其必须保存历史状态;若服务器 / 客户端崩溃,双方的状态视图可能不一致,需进行协调。

HTTP连接

HTTP 连接分为两类:

  • 非持久 HTTP(non-persistent HTTP):一条 TCP 连接最多传输一个对象。传输完成后 TCP 连接关闭。若要下载多个对象,需建立多个 TCP 连接。
  • 持久 HTTP(persistent HTTP):单个 TCP 连接可在客户端与服务器之间传输多个对象。

非持久连接

举个非持久连接的例子:假设用户输入 URL https://www.kaysonz.top/posts/773de39a.html(该网页包含文本和多个 JPEG 图片引用)

  1. HTTP 客户端向www.kaysonz.top的 80 端口发起 TCP 连接;
  2. 服务器在 80 端口接受连接,通知客户端。
  3. 发送 HTTP 请求:客户端将包含 URL 的 HTTP 请求消息发送到 TCP 连接套接字,请求www.kaysonz.top/posts/773de39a.html对象
  4. 服务器响应:服务器接收请求消息,生成包含请求对象的响应消息,并发送到套接字。
  5. 服务器关闭 TCP 连接。
  6. 客户端接收包含 HTML 文件的响应消息,展示 HTML;解析 HTML 时,发现多个引用的 JPEG 对象
  7. 对每个 JPEG 对象,重复步骤 1-5(即每个对象都要单独建立 TCP 连接、请求、响应、关闭连接)。

这样,加载每一个对象所花费的时间都是

image-20251118164536142

如上图所示,因此,非持久 HTTP 的响应时间 = 2RTT + 文件传输时间。

持久连接

相较于非持久连接,持久连接在现在更常用。采用持久连接时,服务器在发送响应后保持 TCP 连接打开,后续的请求和响应可通过同一连接传输;一个包含多个对象的完整网页可通过单个持久 TCP 连接传输;同一服务器上的多个网页也可通过单个持久 TCP 连接发送给同一客户端。

在持久连接下,可连续发送对象请求(无需等待服务器的响应),这种方式称为 “流水线(pipelining)”;服务器接收连续请求后,会连续返回对象。

若连接在一段可配置的超时时间内未被使用,HTTP 服务器会关闭连接。

为获取网页中的多个对象(如多个 JPEG 文件),客户端和服务器可采用两种方式:

  • 串行 TCP 连接(serial TCP connections);
  • 并行 TCP 连接(parallel TCP connections,现代浏览器可配置并行度,默认通常打开 5-10 个并行连接,每个连接处理一个请求 - 响应事务)。

并行连接有助于缩短响应时间,但在某些情况下可能引发前面介绍的公平性问题。然而,HTTP仅定义客户端 HTTP 程序与服务器 HTTP 程序之间的通信,不对如何获取文件 / 获取到文件后如何展示做任何定义。

举个例子来看持久连接:假设请求一个包含1 个文档和 5 张图片的网页,文档大小为1 kbyte,每张图片大小是50 kbytes。下载速率是1 Mbps,RTT为100ms。忽略其他延迟(如DNS解析),计算非持久HTTP和持久HTTP的连接总耗时

(1)非持久HTTP

对于文档请求,耗时为$2RTT+传输时间$,即:$200ms+\frac{8\times10^3}{10^6}=208ms$

对于单次图片请求,耗时为:$200ms+\frac{50\times8\times10^3}{10^6}=600ms$

总耗时是$208ms+5\times 600ms=3.208ms$

(2)持久HTPP

对于持久HTTP,仅需要建立一次连接,建立后的TCP第三次握手时会将所有资源都进行请求。因此它的时间就是$2RTT+总资源传输时间$

HTTP消息

HTTP 消息分为两类:请求(request) 和 响应(response)。且其消息采用消息采用ASCII 明文格式(人类可读)

HTTP请求

HTTP 请求消息的结构分为三部分:

  • 请求行(request line):由方法(method)、URL、版本(version)组成,以\r\n分隔。
  • 头部行(header lines):由多个 “头部字段名 - 值” 对组成,每个对以\r\n分隔;头部行整体以\r\n结束。
  • 实体体(entity body):可选部分,常用于POST等方法传递表单数据等内容。

每部分的详细内容如下图所示

image-20251118170233125

其中:

  • cr 是 carriage return(回车符)的缩写,对应 ASCII 字符\r;
  • lf 是 line feed(换行符)的缩写,对应 ASCII 字符\n;
  • sp 是 space(空格)的缩写,即普通的空格字符。
  • request line 负责指明http请求使用的方法,操作的对象,以及协议版本
  • Header lines(头部行)是 HTTP 请求消息的核心控制区域,用于传递元数据(metadata),定义了请求的各种属性和约束,主要作用包括
    • 说明请求的目标(如Host字段指定目标主机);
    • 描述客户端的能力(如Accept字段说明可接受的资源类型、User-Agent标识客户端类型);
    • 控制连接行为(如Keep-Alive指定持久连接的超时时间);
    • 传递认证、缓存、编码等辅助信息(如Authorization用于身份认证、Cache-Control控制缓存策略、Accept-Encoding说明支持的压缩格式)。
  • body(实体体)是 HTTP 请求的可选部分,主要作用是传递请求的实际数据。例如当使用POST方法提交表单时,实体体包含用户在表单中输入的内容(如向搜索引擎提交的搜索词、用户注册的信息)。或使用PUT方法上传文件时,实体体包含要上传的文件数据

HTTP不同请求方法的功能如下:

  • GET:用于请求一个对象;实体体为空。
  • POST:用于提交表单(如向搜索引擎提交搜索词);实体体包含用户在表单中输入的内容。
  • HEAD:功能与GET类似,但仅获取 HTTP 响应头(不包含请求的对象);常用于调试。
  • PUT:允许用户或应用向特定 Web 服务器的指定目录上传对象,用于网页发布。
  • DELETE:允许用户或应用删除 Web 服务器上的一个对象。

举个例子:

1
2
3
4
5
6
7
8
9
GET /index.html HTTP/1.1\r\n
Host: www-net.cs.umass.edu\r\n
User-Agent: Firefox/3.6.10\r\n
Accept: text/html,application/xhtml+xml\r\n
Accept-Language: en-us,en;q=0.5\r\n
Accept-Encoding: gzip,deflate\r\n
Accept-Charset: ISO-8859-1,utf-8;q=0.7\r\n
Keep-Alive: 115\r\n
\r\n

对于这一段代码,GET /index.html HTTP/1.1,说明这次HTTP连接要请求index.html,http协议版本为1.1。

剩下部分是头部行:

  • Host: www-net.cs.umass.edu\r\n:指定请求的目标主机域名。
  • User-Agent: Firefox/3.6.10\r\n:标识发起请求的客户端(这里是 Firefox 浏览器的 3.6.10 版本)。
  • Accept: text/html,application/xhtml+xml\r\n:客户端可接受的资源类型(如 HTML、XHTML)。
  • Accept-Language: en-us,en;q=0.5\r\n:客户端偏好的语言(英语,美国;通用英语的优先级为 0.5,q是相对质量因子,用于权重排序)。
  • Accept-Encoding: gzip,deflate\r\n:客户端可接受的内容编码方式(如 gzip 压缩、deflate 压缩)。
  • Accept-Charset: ISO-8859-1,utf-8;q=0.7\r\n:客户端可接受的字符集(ISO-8859-1、UTF-8,UTF-8 的优先级为 0.7)。
  • Keep-Alive: 115\r\n:表示希望保持持久连接,超时时间为 115 秒(即 115 秒内无新请求,连接才会关闭)。
  • 空的\r\n:标识头部行结束

由于是GET请求,无需使用Body,因此Body为空。

HTTP响应

HTTP响应也分为三部分:

  • 状态行(status line):包含协议版本、状态码、状态消息。三者用空格隔开,尾部有\r\n换行
  • 头部行(header lines):传递元数据,示例中列举了常见头部字段:
    • Connection:指示本次响应后是否关闭 TCP 连接,closed表示关闭,keep-alive表示持续。
    • Date:服务器发送响应的时间和日期。
    • Server:生成响应的 Web 服务器标识(如 Apache、Nginx)。
    • Last-Modified:对象创建或最后修改的时间和日期。
    • Content-Length:响应实体体的字节数。
    • Content-Type:实体体的类型(如text/html表示 HTML 文本)。
  • 实体体(entity body):包含请求的对象(如 HTML 文件、图片等)。

常见的状态码和状态消息有(数字表示状态码,文字表示状态消息):

  • 200 OK:请求成功,请求的对象会在后续消息中返回。
  • 301 Moved Permanently:请求的对象已永久移动,新位置会在消息的Location头部中指定。
  • 400 Bad Request:服务器无法理解请求消息。
  • 404 Not Found:请求的文档在服务器上不存在。
  • 505 HTTP Version Not Supported:服务器不支持请求的 HTTP 协议版本。

例如下面这个例子:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Connection: close
Date: Tue, 09 Aug 2011 15:44:04 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT
Content-Length: 6821
Content-Type: text/html
(data, data, data,… )
  • 状态行(status line):表明协议是http/1.1,请求状态码是200,含义为正确响应(OK)‘
  • 头部行(header lines):
    • Connection: close:指示响应后关闭连接。
    • Date: Tue, 09 Aug 2011 15:44:04 GMT:服务器发送响应的时间。
    • Server: Apache/2.2.3 (CentOS):标识服务器是 Apache 2.2.3 版本(运行在 CentOS 系统)。
    • Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT:对象最后修改时间。
    • Content-Length: 6821:实体体的字节数为 6821。
    • Content-Type: text/html:实体体是 HTML 文本。
  • 实体体(entity body):(data, data, data,… )表示包含实际的 HTML 数据的实体。

维护状态的小文件:Cookies

由于HTTP 服务器是无状态(stateless)的,但很多网站使用需要识别区分用户,根据用户身份限制权限或是提供特定服务。就像是,你明明刚刚输入账户密码登录了,但是下一秒服务器就把你忘记了,因为http请求没办法保存登录这个状态。

这就需要一个能记录状态的东西——Cookies,他是由服务器发送到用户浏览器并保存在用户计算机上的小型文本文件,每一次HTTP请求都会带上cookies,这样通过cookies就可以识别出你是谁了。

在HTTP响应报文头部行中,使用Set-Cookie: <cookie-name>=<cookie-value>字段可以让客户端把<cookie-name>=<cookie-value>保存在自己的本地。例如下面这个例子:

1
2
3
4
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

在后续的所有HTTP请求中,浏览器都会把保存的Cookie内容通过请求头部行的Cookie字段发送给服务器。例如:

1
2
3
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

这样,服务器就可以利用Cookie的内容来:鉴权(authorization)、存储个性化推荐(recommendations)、维护用户会话状态(如 Web 邮箱)等等

HTTP的Cookie机制让它每次都携带这些信息传输,来实现了状态维护。

Cookie也可以存储各种用户偏好,例如你在淘宝逛了哪些商品,服务器都可以用Cookie让你存下来,这样以后就可以对你精准化推送。

Web缓存机制(Cache)

是什么

Web缓存是指的代理服务器使用自己的存储,保存近期请求对象的副本。用户的 HTTP 请求先发送到缓存,如果缓存有对应资源,就直接发给用户;如果缓存没有,就去源服务器同步一个过来。

缓存会同时扮演客户端和服务器角色:对发起请求的客户端而言,它是服务器;对源服务器而言,它是客户端。

使用缓存可以:

  • 缩短客户端请求的响应时间;
  • 减少机构接入链路的流量;
  • 互联网中大量缓存的存在,让 “资源不足” 的内容提供者也能高效分发内容(类似 P2P 文件共享的资源复用逻辑)。

一个例子:缓存的必要性

假设平均每个网页对象大小是 100Kb,浏览器平均每秒向源服务器发 15 个请求,浏览器自己的下载速率是 1.50 Mbps;从机构路由器到任何源服务器的往返时间(RTT)是 2 秒;机构的接入链路速率是 1.54 Mbps。

image-20251118183030765

此时,浏览器跑满自己的下载速率1.50 Mbps,对于机构的LAN,它的利用率只有1.5%,而机构对外的接入链路的利用率高达 99%,外部接入带宽快被占满了。同时,延迟方面,整体的延迟是互联网延迟 + 接入链路延迟传输文件延迟 + 局域网延迟,也就是2 秒 + 好几分钟 + 数微秒。每次打开网页都要等好几分钟。

为了解决瓶颈接入链路的瓶颈,那把接入链路速率从 1.54 Mbps 提到 154 Mbps 呢?这次局域网利用率还是 1.5%;接入链路利用率直接降到 0.99%;总延迟也缩短了,变成 “2 秒 + 几毫秒 + 微秒” 。但是这带来了其他问题:成本极高,高速商用宽带非常贵。

那如果,使用机构LAN内的服务器进行Web缓存呢?假设缓存命中率是 0.4,意思是:40% 的请求,缓存里直接有,不用去源服务器请求,其他请求需要去源服务器。在上述假设情景下,只有60% 的请求需要走接入链路,即$0.6 × 1.50 Mbps = 0.9 Mbps$,接入链路利用率58%。再看延迟方面,60% 的请求,走源服务器,延迟是 “RTT(2 秒) + 其他延迟”,简化为 2.01 秒;40% 的请求,走缓存,延迟几乎可以忽略,算 0.01 秒。$总延迟 = 0.6×2.01 + 0.4×0.01 ≈ 1.2秒$,延迟也得到了提升。

条件 GET机制(Conditional GET)

源服务器可能有更新,一旦更新,缓存的东西就过期了。因此,缓存需要定时问问服务器,“我这的版本是不是最新的?”。这个操作通过条件GET实现。

条件GET在HTTP GET的头部行中包含If-modified-since:<date>字段:

  • 如果对象在该日期后没被修改,服务器返回 HTTP/1.0 304 Not Modified,无需更新
  • 如果对象在该日期后被修改了,服务器返回 HTTP/1.0 200 OK 并带上最新的对象数据

P2P通信

引入

P2P通信常用于文件分发(BitTorrent)、流媒体、VoIP等。

考虑下面这个场景,假设服务器要给N个用户分发大小为F的文件,需要多久?($u_s$:服务器的上传速率;$d_i$:第i个节点的下载速率;$u_i$:第i个节点的上传速率。)

image-20251118184916316

在客户端 - 服务器模式下:服务器必须依次发送N份文件副本,发 1 份的时间是$F/u_s$,发N份的时间是$N F/u_s$。客户端的最小下载速率是$d_{min}$,所以单个客户端的最小下载时间是$F/d_{min}$。总分发时间下限:$D_{c-s} ≥ max(N F/u_s, F/d_{min})$

在P2P 模式下:服务器至少发 1 份副本,时间是$F/u_s$。每个客户端仍需下载 1 份文件,最小时间是$F/d_{min}$。但 P2P 的优势在于所有客户端能一起上传:系统的最大上传速率是 服务器上传速率$u_s$ + 所有客户端上传速率之和$\sum u_i$。那么,总分发时间的下限就变成了:$D_{P2P} ≥ max(F/u_s,F/d_{min}, NF/(u_s+\sum u_i))$

例如,N=10,F=8000 Mb,$u_s=20 Mbps$,$d_i=10 Mbps$时:

  • $D_{\text{c-s}} \geq \max\left( \frac{10 \times 8000}{20}, \frac{8000}{10} \right) = \max(4000, 800) = 4000 \, \text{秒}$
  • (D_{\text{P2P}} \geq \max\left( \frac{8000}{20}, \frac{8000}{10}, \frac{10 \times 8000}{20 + 100} \right) = \max(400, 800, 66.67) = 800 \, \text{秒})

因此,使用P2P分发文件,可以极大地提升速度。

BitTorrent

P2P 文件分发的典型实现就是BitTorrent。BitTorrent 把文件分成256KB 大小的块(chunks),参与的节点(peers)通过互相发送、接收这些块来完成文件下载。

BT使用跟踪器(trackers)和种子(torrent)来维护节点(peers)信息:

  • 跟踪器是一台服务器,负责记录参与下载的所有节点;
  • “种子” 是指参与交换某一文件所有块的节点集合。

当某一用户加入时,会先向跟踪器注册,获取当前参与节点的列表,然后和其中一部分节点(邻居)开始交换文件块。刚加入时节点没有任何块,但会随着时间从其他节点那里 “积累” 块。节点在下载的同时,也会向其他节点上传块;可以随时更换交换块的节点,节点会频繁加入或离开。当节点下完整个文件后,可能 “自私地” 离开,也可能 “无私地” 留在网络中继续分享。

请求块策略-稀缺优先(rarest first):任何时刻,不同节点拥有的文件块都是不一样的。节点会定期向其他节点询问 “你有哪些块”,然后优先请求最稀缺的块(rarest first)—— 这样能让整个网络的块分布更均匀,下载效率更高。

上传块策略-以牙还牙(tit-for-tat):节点会把块发分享给当前给自己发送块速度最快的 4 个节点,称这 4 个节点是 “未阻塞(unchoked)” 的,其他节点则是 “被阻塞(choked)” 的,被阻塞的节点无法收到分享。前4个每10秒会进行一次重新评估。每30秒,会随机选择一个peer开始发送块,这被称为“乐观解除( optimistically unchoke)”,当节点1对节点2乐观解除时,节点2发现节点1给我分享的速率挺快地,我也给它分享。

BT是一个相当复杂的协议,这里只是简要介绍