“If you want to live your whole life free from pain, you must become either a god or else a corpse. Consider other men’s troubles and that will comfort yours.”
1. 网络基础知识总结
1.1 OSI参考模型
OSI参考模型将实际的分组通信协议整理并分成了易于理解的7个分层。


| 分层名称 | 功能 | 详细概述 |
|---|---|---|
| 应用层 | 针对特定应用的协议 | 为应用程序提供服务并规定应用中通信相关的细节。其包括文件传输、电子邮件、远程登录等协议 |
| 表示层 | 设备固有数据格式和网络标准数据格式的转换 | 将应用处理的信息转换为适合网络传输的格式,或将来自下一层的数据转换成上一层能够处理的数据格式,负责数据格式的转换 |
| 会话层 | 通信管理,负责建立和断开通信连接(数据流动的逻辑通路) | 负责建立和断开通信连接,决定采用何种连接方式,以及数据的分割等数据传输相关的管理 |
| 传输层 | 管理两个节点之间的数据传输。负责可靠传输(确保数据被可靠地传输到目标地址) | 保证数据的可靠传输,只在通信双方节点上进行处理,而无需再路由器上处理 |
| 网络层 | 地址管理与路由选择 | 将数据传输到目标地址。目标地址可以是多个网络通过路由器连接而成的某一个地址。因此这一层主要负责寻址和路由选择 |
| 数据链路层 | 互连设备之间传送和识别数据帧 | 负责物理层面上互连的、节点之间的通信传输 |
| 物理层 | 以 0,1 代表电压的高低、灯光的闪灭 | 负责0,1,比特流与电压的高低、光的闪灭之间的互换 |
1. 物理层
定义了为建立、维护和拆除物理链路所需的机械的、电气的、功能的和规程的特性,其作用是使原始的数据比特流能在物理媒体上传输。具体涉及接插件的规格、“0”、“1”信号的电平表示、收发双方的协调等内容。
2. 数据链路层
比特流被组织成数据链路协议数据单元(通常称为帧),并以其为单位进行传输,帧中包含地址、控制、数据及校验码等信息。数据链路层的主要作用是通过校验、确认和反馈重发等手段,将不可靠的物理链路改造成对网络层来说无差错的数据链路。数据链路层的作用还要协调收发双方的数据传输,即进行流量控制,以防止接收方因来不及处理发送方来的高速数据而导致缓冲器溢出以及线路阻塞。
物理层负责0、1比特流与物理设备电压高低、光的闪灭之间的互换。 数据链路层负责将0、1序列划分为数据帧从一个节点传输到临近的另一个节点,这些节点是通过MAC来唯一标识的(MAC,物理地址,一个主机会有一个MAC地址)。

- 封装成帧:把网络层数据报加头和尾,封装成帧,帧头中包括源MAC地址和目的MAC地址。
- 透明传输:零比特填充、转义字符。
- 可靠传输:在出错率很低的链路上很少用,但是无线链路WLAN会保证可靠传输。
- 差错检测(CRC): 接收者检测错误,如果发现差错,丢弃该帧。
3. 网络层
数据以网络协议数据单元(分组)为单位进行传输。网络层关心的是通信子网的运行控制,主要解决如何使数据分组跨越通信子网从源传送到目的地的问题。这就需要在通信子网中进行路由选择。另外,为了避免通信子网中出现过多的分组而造成网络阻塞,需要对流入的分组数量进行控制。当分组要跨越多个通信子网才能到达目的地时,还要解决网际互连的问题。
4. 传输层
是第一个端到端,也就是主机到主机的层次。传输层提供的端到端的透明数据运输服务,使得高层用户不必关心通信子网的存在,由此用统一的运输原语书写的高层软件便可以运行于任何通信子网上。传输层还要处理端到端的 差错控制和流量控制问题。
5. 会话层
是进程到进程的层次,主要功能是组织和同步不同的主机上各种进程之间的通信(也称为对话)。会话层负责在两个会话层实体之间进行对话连接的建立和拆除。在半双工情况下,会话层提供了一种数据权标来控制某一方何时有权发送数据。会话层还提供在数据流中插入同步点的机制,使得数据传输因为网络故障而中断后,可以不必从头开始而仅仅重传最近一个同步点以后的数据。
6. 表示层
为上层用户提供共同的数据或信息的语法表示变换。为了让采用不同编码方法的计算机在通信中能相互理解数据的内容,可以采用抽象的标准方法来定义数据结构,并采用标准的编码表示形式。表示层管理这些抽象的数据结构,并将计算机内部的表现形式转换成网络通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的表示变换功能。
7. 应用层
应用层是开放式系统互连环境的最高层。不同的应用层特定类型的网络应用提供访问OSI环境的手段。网络环境下不同主机间的文件传送和管理(FTAM)、传送标准电子邮件的文电处理系统(MHS)、使不同类型的终端和主机通过网络交互访问的虚拟终端(VT)协议都属于应用层的范畴。
1.2 网络的构成要素
构建一套网络环境需要涉及到各种各样的电缆和网络设备。搭建网络的基本设备及主要作用如下所示:
| 设备 | 作用 |
|---|---|
| 通信媒介与数据链路 | 网络设备之间的连接——电缆 |
| 网卡 | 使计算机连网的设备 |
| 中继器 | 从物理层面上延长网络的设备 |
| 网桥/2层交换机 | 从数据链路层延长网络的设备 |
| 路由器/3层交换机 | 通过网络层转发分组数据的设备 |
| 4~7层交换机 | 处理传输层以上各层网络传输的设备 |
| 网关 | 转换协议的设备 |
1.3 TCP/IP模型
TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。
基于TCP/IP的参考模型将协议分成四个层次,它们分别是链路层、网络层、传输层和应用层。下图表示TCP/IP模型与OSI模型各层的对照关系。

TCP/IP协议族按照层次由上到下,层层包装。最上面的是应用层,这里面有http,ftp,等等我们熟悉的协议。而第二层则是传输层,著名的TCP和UDP协议就在这个层次。第三层是网络层,IP协议就在这里,它负责对数据加上IP地址和其他的数据以确定传输的目标。第四层是数据链路层,这个层次为待传送的数据加入一个以太网协议头,并进行CRC编码,为最后的数据传输做准备。

上图清楚地表示了TCP/IP协议中每个层的作用,而TCP/IP协议通信的过程其实就对应着数据入栈与出栈的过程。入栈的过程,数据发送方每层不断地封装首部与尾部,添加一些传输的信息,确保能传输到目的地。出栈的过程,数据接收方每层不断地拆除首部与尾部,得到最终传输的数据。
给出一个Http协议的具体案例:

2. 计算机网络基础
1. OSI与TCP/IP各层的结构和功能,都有哪些协议,协议所占端口号?
学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。

下面简要地阐述一下各层的作用:
1.1 应用层
应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等等。我们把应用层交互的数据单元称为报文。
1.2 传输层
传输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个传输层服务。由于一台主机可以同时运行多个进程,因此传输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。
传输层主要使用以下两种协议:
- 传输控制协议TCP(Transmission Control Protocol):提供面向连接的、可靠的数据传输服务。
- 用户数据报协议UDP(User Datagram Protocol): 提供无连接的、尽最大努力的数据传输服务(不保证数据传输的可靠性)。
1.3 网络层
在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的作用就是选择合适的网间路由和交换结点,确保数据及时传送。在发送数据时,网络层把传输层产生的报文段或用户数据包封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。
这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.
互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。
1.4 数据链路层
数据链路层(data link layer)通常称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。在两个相邻的节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。
在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提取出数据部分,上交给网络层。控制信息还使得接收端能够检测到所收到的帧中是否有错误。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在数据链路层传输时出现的差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。
1.5 物理层
在物理层上所传输的数据单位是比特。物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。
给出互联网上一张非常经典的计算机网络七层体系结构图:

每层的协议主要如下所示:

- 物理层:RJ45、CLOCK、IEEE802.3
- 数据链路:PPP、FR、HDLC、VLAN、MAC
- 网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGMP
- 传输层:TCP、UDP、SPX
- 会话层:RPC、SQL、NETBIOS、NFS
- 表示层:JPEG、MPEG、ASCII、MIDI
- 应用层:RIP、BGP、FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS
协议主要占用的端口号:
- http 80
- ftp 20,21
- telnet 23
- SMTP 25
- DNS 53
2. 网络层
2.1 IP协议
IP协议是TCP/IP协议的核心,所有的TCP.UDP,IMCP的数据都以IP数据格式传输。要注意的是,IP是不可靠的协议,这是说,IP协议没有提供一种数据未传达以后的处理机制,这被认为是上层协议:TCP或者UDP要做的事情。
1. IP地址
在数据链路层我们一般通过MAC地址来识别不同的节点,而在IP层我们也要一个类似的地址标识,这就是IP地址。
32位的IP地址分为网络位和地址位,这样做可以减少路由器中路由表记录的数目,有了网络地址,就可以限定拥有相同网络地址的终端都在同一个范围内,那么路由表只需要维护一条这个网络地址的方向,就可以找到相应的这些终端了。
绝大部分IP地址属于以下几类:
- A类地址:IP地址的前8位代表网络ID,后24位代表主机ID
- B类地址:IP地址的前16位代表网络ID,后16位代表主机ID
- C类地址:IP地址的前24位代表网络ID,后8位代表主机ID
这里很明显可以得知A类地址能够提供出的网络ID较少,但是每个网络可以拥有非常多的主机。但是我们怎么才能看出一个 IP 地址到底是哪类地址呢?
- 如果 32 位的 IP 地址以 0 开头,那么它就是一个 A 类地址。
- 如果 32 位的 IP 地址以 10 开头,那么它就是一个 B 类地址。
- 如果 32 位的 IP 地址以 110 开头,那么它就是一个 C 类地址。
那么转化为十进制(四段)的话,我们就能以第一段中的十进制数来区分 IP 地址到底是哪类地址了。

Attention:
- 十进制第一段大于223的属于D类和E类地址,这两类比较特殊也不常见,不做介绍
- 每一类都有一些排除地址,这些地址并不属于该类,他们是在一些特殊情况使用地址
- 除了这样的方式来划分网络,我们还可以把每个网络划分为更小的网络块,称之为子网
全是0的主机ID代表网络本身,比如说IP地址位130.100.0.0指的是网络ID位130.100的B类地址。
全是1的主机ID代表广播,是用于向该网络中的全部主机发送消息的。IP 地址为 130.100.255.255 就是网络 ID 为 130.100 网络的广播地址(二进制 IP 地址中全是 1 ,转换为十进制就是 255 )
以十进制 127 开头的地址都是环回地址。目的地址是环回地址的消息,其实是由本地发送和接收的。主要是用于测试 TCP/IP 软件是否正常工作。我们用 ping 功能的时候,一般用的环回地址是 127.0.0.1。
2. IP协议报文结构

主要介绍其中几个重要的字段如下:
- 版本字段: 占用4比特。用来表明IP协议实现的版本号,当前版本一般为IPV4, 即0100
- 标识字段:占用16比特。用来唯一地标识主机发送的每一份数据报,通常每发送一份报文,它的值会加1
- 标志位字段:占用3比特。标志一份数据报是否要求分段。
- 段偏移字段:占用13比特。如果一份数据报要求分段的话,此字段表明该段偏移量距原始数据报开始的位置。
- 源IP地址、目标IP地址字段:各占32比特。用来标明发送IP数据报文的源主机地址和接收IP报文的目标主机地址。
最后,介绍一下8位的TTL字段(8位生存时间)。这个字段规定该数据包在穿过多少个路由之后才会被抛弃。某个IP数据包每穿过一个路由器,该数据包的TTL数值就会减少1,当该数据包的TTL为零时,它就会被自动抛弃。这个字段的最大值也就是255,也就是说一个协议包也就在路由器里面穿行255次就会被抛弃了,根据系统的不同,这个数字也不一样,一般是32或者是64。
3. 子网
前面提到了 IP 地址的分类,但是对于 A 类和 B 类地址来说,每个网络下的主机数量太多了,那么网络的传输会变得很低效,并且很不灵活。比如说 IP地址为 100.0.0.0 的 A 类地址,这个网络下的主机数量超过了 1600 万台。所以子网掩码的出现就是为了解决这样的问题。
我们先回顾一下之前如何区分主机IP和网络IP的。以A类地址 99.10.10.10 为例,前8位是网络IP ,后24位是主机IP 。(如下图)

子网掩码也是一个32位的二进制数,也可以用4个十进制数来分段。他的每一位对应着IP地址的对应位置,数值为1时代表的是非主机位,数值为0时代表的是主机位。

由表格可以很清晰的看出,网络IP仍是由之前的分类来决定到底是多少位,主机IP则是由子网掩码值为 0 的位数来决定,剩下的则是子网IP。
2.2 ARP及RARP协议
ARP是根据IP地址获取MAC地址的一种协议。
ARP(地址解析协议)协议是一种解析协议,本来主机是完全不知道这个IP对应的是哪个主机的哪个接口,当主机要发送一个IP包的时候,它会首先查一下自己的ARP高速缓存(就是一个IP-MAC地址对应表缓存)。如果查询的IP-MAC值对不存在,那么主机就向网络发送一个ARP协议广播包,这个广播包里面就有待查询的IP地址,而直接收到这份广播的包的所有主机都会查询自己的IP地址,如果收到广播包的某一个主机发现自己符合条件,那么久准备好一个包含自己的MAC地址的ARP包传送给ARP广播的主机。
而广播主机拿到ARP包之后会更新自己的ARP缓存(就是存放IP-MAC对应表的地方)。发送广播的主机就会用新的ARP缓存数据准备好数据链路层的数据包发送工作。
RARP的过程与ARP的过程相反,不具体阐述。
2.3 ICMP协议
IP协议并不是一个可靠的协议,它不保证数据被送达,那么,自然的,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。ICMP不是高层协议,而是IP层的协议。
当传送IP数据包发生错误,比如主机不可达,路由不可达等等,ICMP协议将会将错误信息封装成包,然后传送回给主机。给主机一个处理错误的机会,这也是说为什么建立在IP层以上的协议是可能做到安全的原因。
具体应用1:ping
ping可以说是ICMP的最著名的应用,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否连通,可以很好地帮助我们分析和判定网络故障。
例如,当我们某一个网站上不去的时候。通常会ping一下这个网站。ping会回显出一些有用的信息。一般的信息如下:

ping这个单词源自声纳定位,而这个程序的作用也确实如此,它利用ICMP协议包来侦测另一个主机是否可达。原理是利用类型码为0的ICMP发请求,收到请求的主机则用类型码为8的ICMP回应。
ping程序来计算间隔时间,并计算有多少个包被送达。用户就可以判断网络大致的情况。我们可以看到, ping给出来了传送的时间和TTL的数据。
具体应用2: Traceroute
Traceroute是用来侦测主机到目的主机之间所经路由情况的重要工具,也是最便利的工具。
它的原理很有意思,它收到目的主机的IP后,首先给目标主机发送一个TTL=1的UDP数据包,而经过的第一个路由收到这个数据包以后就自动把TTL减1,而TTL变为0以后,路由器就把这个包给抛弃了,并同时产生一个产生 一个主机不可达的ICMP数据报给主机。主机收到这个数据报以后再发一个TTL=2的UDP数据报给目的主机,然后刺激第二个路由器给主机发ICMP数据 报。如此往复直到到达目的主机。这样,traceroute就拿到了所有的路由器IP。

3. TCP/UDP
3.1 TCP,UDP协议的区别
TCP/UDP都是传输层协议,但是两者具有不同的特性,同时也具有不同的应用场景。下面以图表的方式进行对比分析:
| 指标 | TCP | UDP |
|---|---|---|
| 可靠性 | 可靠 | 不可靠 |
| 连接性 | 面向连接 | 无连接 |
| 报文 | 面向字节流 | 面向报文(保留报文的边界) |
| 效率 | 传输效率低 | 传输效率高 |
| 双工性 | 全双工 | 一对一、一对多、多对一、多对多 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢开始、拥塞避免、快重传、快恢复 | 无 |
| 传输速度 | 慢 | 快 |
| 应用场景 | 对效率要求低,对准确性要求高或者要求有连接的场景 | 对效率要求高,对准确性要求低 |

UDP 在传送数据之前不需要先建立连接,远程主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠传输,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等。
TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

1.面向报文
面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这也就是说,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。
2.面向字节流
面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。
3.面向连接与无连接的区别
这里,我们再来详细地解释一下面向连接和面向无连接的区别:
- 面向连接举例:两个人之间通过电话进行通信;
- 面向无连接举例:邮政服务,用户把信函放在邮件中期待邮政处理流程来传递邮政包裹。显然,不可达代表不可靠。
从程序实现的角度来看:

从上图我们可以看出,TCP通信需要服务器端侦听listen、接收客户端连接请求accept,等待客户端connect建立连接后才能进行数据包的收发(recv/send)工作。而UDP则服务器和客户端的概念不明显,服务器端即接收端需要绑定端口,等待客户端的数据的到来。后续便可以进行数据的收发(recvfrom/sendto)工作。
4.报文边界
在默认的阻塞模式下,TCP无边界,UDP有边界。
- 对于TCP协议,客户端连续发送数据,只要服务端的这个函数的缓冲区足够大,会一次性接收过来,即客户端是分好几次发过来,是有边界的,而服务端却一次性接收过来,所以证明是无边界的。
- 而对于UDP协议,客户端连续发送数据,即使服务端的这个函数的缓冲区足够大,也只会一次一次的接收,发送多少次接收多少次,即客户端分几次发送过来,服务端就必须按几次接收,从而证明,这种UDP的通讯模式是有边界的。
TCP无边界,造成对采用TCP协议发送的数据进行接收比较麻烦,在接收的时候易出现粘包,即发送方发送的若干包数据到接收方接收时粘成一包。由于TCP是流协议,对于一个socket的包,如发送 10AAAAABBBBB两次,由于网络原因第一次又分成两次发送, 10AAAAAB和BBBB,如果接包的时候先读取10(包长度)再读入后续数据,当接收得快,发送的慢时,就会出现先接收了 10AAAAAB,会解释错误 ,再接到BBBB10AAAAABBBBB,也解释错误的情况。这就是TCP的粘包。
为了避免粘包,可以采取以下几种措施:
- 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
- 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象。
- 由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
3.2 TCP与UDP的报文结构
我们先来看一下TCP的数据报文的格式:


TCP协议通过使用端口来标识源端口和目标端口的应用进程。我们来分析一下几个重要的字段:
- 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
- 确认序号:Ack(Acknowledge Number)序号占32位,客户端和服务器端都可以发送,Ack = Seq + 1.
- 标志位:每个标志位占用1Bit,共有6个标志位(U,A,P,R,S,F),分别是URF、ACK、PSH、RST、SYN、FIN,其具体含义如下所示“
- URG(urgent):紧急指针(urgent pointer)有效.
- ACK(acknowledgement): 确认序号有效。
- PSH(push): 传送,接收方应该尽快将这个报文段交给应用层。
- RST(reset): 重置,重建连接。
- SYN(synchronous): 建立联机,发起一个连接。
- FIN(finish):释放一个连接。
下面来看一下UDP的数据报文结构:

3.3 TCP三次握手
TCP是面向连接的,无论哪一方向向另一方向发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换TCP窗口大小信息。

第一次握手:建立连接。主机A发送标志位SYN = 1,随机产生序号seq = x的数据包到服务器,主机B由SYN=1知道,A要求建立连接;此时主机A状态位SYN_SENT,B为LISTEN。
第二次握手:主机B收到请求后要求确认连接信息,向A发送确认序号ack=(主机A的seq+1),标志位SYN=1,ACK=1,随机产生序号seq = y的包,此时状态A为ESTABLISHED, B的状态位SYN_RCVD。
第三次握手:主机A收到后检查ack是否正确(即第一次发送的序号seq + 1),以及标志位ACK是否为1,若正确,主机A会再发送确认序号ack = (主机B的seq +1),标志位ACK=1包,主机B收到后确认seq值与ACK=1,则连接建立成功。此时A,B状态都变为ESTABLISHED.完成TCP三次握手。
还有一种比较容易理解的三次握手如下所示:

- 为了方便描述我们将主动发起请求的172.16.50.72:65076 主机称为客户端,将返回数据的主机172.16.17.94:8080称为服务器。
- 第一次握手: 建立连接。客户端发送连接请求,发送SYN报文,将seq设置为0。然后,客户端进入SYN_SEND状态,等待服务器的确认。
- 第二次握手: 服务器收到客户端的SYN报文段。需要对这个SYN报文段进行确认,发送ACK报文,将ack设置为1。同时,自己还要发送SYN请求信息,将seq为0。服务器端将上述所有信息一并发送给客户端,此时服务器进入SYN_RECV状态。
- 第三次握手: 客户端收到服务器的ACK和SYN报文后,进行确认,然后将ack设置为1,seq设置为1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
3次握手中过程状态:
- LISTEN: 表示服务器端的某个 SOCKET 处于监听状态,可以接受连接了。
- SYN_SENT: 当客户端 SOCKET 执行 CONNECT 连接时,它首先发送 SYN 报文,因 此也随即它会进入到了 SYN_SENT 状态,并等待服务端发送三次握手中的第 2 个报文。 SYN_SENT 状态表示客户端已发送 SYN 报文。
- SYN_RECV: 这个状态表示接收到了 SYN 报文,在正常情况下,这个状态是服务器端 的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上 用 netstat 你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个 ACK 报文不予发送。因此这种状态时,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。(服务器端)
- ESTABLISHED:表示连接已经建立了。
为什么需要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
- 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。
- 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常。
- 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常。
其实,更具体的原因是因为防止已经失效的连接请求报文段突然又传送到了服务器端,因而产生错误。给出一个具体的例子如下:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
3.4 数据传输
建立连接1后,两台主机就可以相互传输数据了。如下图所示:

上图给出了主机A分2次(分2个数据包)向主机B传递200字节的过程:
- 首先,主机A通过1个数据包发送100个字节的数据,数据包的Seq号设置为1200.
- 主机B为了确认这一点,向主机A发送ACK包,并将Ack号设置为1300.
为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)后必须立即回传ACK包,这样发送方才能确认数据传输成功。
此时 Ack 号为 1300 而不是 1200,原因在于 Ack 号的增量为传输的数据字节数。
下面分析传输过程中数据包丢失的情况,如下图所示:

上图表示通过 Seq 1300 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1300 的ACK确认,因此尝试重传数据。为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。
3.5 TCP四次握手
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接收到一个FIN后仍能发送数据。首先进行关闭的一方将主动执行关闭,而另一方执行被动关闭。

第一次挥手:A的应用程序先向其TCP发出连接释放报文段(FIN=1, 序号seq = u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT_1(终止等待1)状态,等待B的确认;
第二次挥手:B收到连接释放报文段后立即发出确认报文段,(ACK = 1,确认序号ack = u + 1,序号seq = v),B进入CLOSE_WAIT状态,此时TCP处于半连接状态,A到B的连接释放。A收到B的确认后,进入FIN_WAIT_2状态,等待B发出的连接释放报文段。
第三次挥手:B没有向A发出的数据时,B发出连接释放报文段(FIN = 1,ACK = 1, 序号seq = w, 确认号ack = u + 1),B进入LAST_ACK(最后确认状态),等待A的确认。
第四次挥手: A收到B的连接释放报文段后,对此发出确认报文段(ACK = 1, seq = u+1, ack = w + 1),A进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSED状态。
还有一种比较容易理解的四次挥手如下所示:

- 第一次挥手:客户端向服务器发送一个FIN报文段,将设置seq为160和ack为112,;此时,客户端进入 FIN_WAIT_1状态,这表示客户端没有数据要发送服务器了,请求关闭连接;
- 第二次挥手:服务器收到了客户端发送的FIN报文段,向客户端回一个ACK报文段,ack设置为1,seq设置为112;服务器进入了CLOSE_WAIT状态,客户端收到服务器返回的ACK报文后,进入FIN_WAIT_2状态;
- 第三次挥手:服务器会观察自己是否还有数据没有发送给客户端,如果有,先把数据发送给客户端,再发送FIN报文;如果没有,那么服务器直接发送FIN报文给客户端。请求关闭连接,同时服务器进入LAST_ACK状态;
- 第四次挥手:客户端收到服务器发送的FIN报文段,向服务器发送ACK报文段,将seq设置为161,将ack设置为113,然后客户端进入TIME_WAIT状态;服务器收到客户端的ACK报文段以后,就关闭连接;此时,客户端等待2MSL后依然没有收到回复,则证明Server端已正常关闭,客户端也可以关闭连接了。
四次挥手中的过程状态:
- FIN_WAIT_1: 这个状态要好好解释一下,其实 FIN_WAIT_1 和 FIN_WAIT_2 状态的真正含义都是表示等待对方的 FIN 报文。而这两种状态的区别是:FIN_WAIT_1 状态实际上 是当 SOCKET 在 ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文, 此时该 SOCKET 即进入到 FIN_WAIT_1 状态。而当对方回应 ACK 报文后,则进入到FIN_WAIT_2状态。当然在实际的正常情况下,无论对方何种情况下,都应该马上回应 ACK 报文,所以 FIN_WAIT_1 状态一般是比较难见到的,而 FIN_WAIT_2 状态还有时常常可以 用 netstat 看到。(主动方持有的状态)
- FIN_WAIT_2: 实际上 FIN_WAIT_2 状态下的 SOCKET, 表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传 送给你(ACK 信息),稍后再关闭连接。(主动方的状态)
- TIME_WAIT: 表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 后即可 回到 CLOSED 可用状态了。 如果 FIN_WAIT_1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。(主动方的状态)。
- CLOSING(比较少见):表示双方同时关闭连接。如果双方几乎同时调用 close 函数, 那么会出现双方同时发送 FIN 报文的情况,就会出现 CLOSING 状态,表示双方都在关闭 连接。这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正 常情况下,当你发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报 文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对 方的 ACK 报文,反而却收到了对方的 FIN 报文。什么情况下会出现此种情况呢?其实细 想一下,也不难得出结论:那就是如果双方几乎在同时 close 一个 SOCKET 的话,那么 就出现了双方同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关 闭 SOCKET 连接。
- CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。当对方 close 一个 SOCKET 后发送 FIN 报文给自己,你系统毫无疑问地会回应一个 ACK 报文给对方,此时则进入到 CLOSE_WAIT 状态。接下来,实际上你真正需要考虑的事情是察看你是否还有数据发送 给对方,如果没有的话,那么你也就可以 close 这个 SOCKET,发送 FIN 报文给对方, 也即关闭连接。所以你在 CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。 (被动方的状态)
- LAST_CHECK: 它是被动关闭一方在发送 FIN 报文后, 最后等待对方的 ACK 报文。当收到 ACK 报文后,也即可以进入到 CLOSED 可用状态了。 (被动方的状态)。
- CLOSED: 表示连接中断。
为什么建立连接协议是三次握手,而关闭连接需要四次挥手?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建立请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
什么是2MSL?MSL即Maximum Segment Lifetime,也就是报文最大生存时间,引用《TCP/IP详解》中的话:“它 (MSL)是任何报文段被丢弃前在网络内的最长时间,那么,2MSL也就是这个时间的2倍,当TCP连接完成四个报文段的交换时,主动关闭的一方将继续等待一定时间(2-4分钟),即使两端的应用程序结束。
等待2MSL的原因主要有两点:
- 保证TCP协议的全双工连接能够可靠关闭。
- 保证这次连接的重复数据段从网络中消失。
第一点:保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二点:防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中,即在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身 (incarnation),那么有可能出现这种情况(假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达主机,由于新连接和老连接的端口号是一样的,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了)。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
如果网络连接中出现大量 TIME_WAIT 状态所带来的危害?
如果系统中有很多 socket 处于 TIME_WAIT 状态,当需要创建新的 socket 连接的时 候可能会受到影响,这也会影响到系统的扩展性。之所以 TIME_WAIT 能够影响系统的扩展性是因为在一个 TCP 连接中,一个 Socket 如果 关闭的话,它将保持 TIME_WAIT 状态大约 1-4 分钟 。如果很多连接快速的打开和关闭的 话,系统中处于 TIME_WAIT 状态的 socket 将会积累很多,由于本地端口数量的限制,同一 时间只有有限数量的 socket 连接可以建立,如果太多的 socket 处于 TIME_WAIT 状态,你 会发现,由于用于新建连接的本地端口太缺乏,将会很难再建立新的对外连接。
如何消除大量 TCP 短连接引发的 TIME_WAIT?
- 可以改为长连接,但代价较大,长连接太多会导致服务器性能问题,而且 PHP 等脚本语言,需要通过 proxy 之类的软件才能实现长连接
- 修改 ipv4.ip_local_port_range,增大可用端口范围,但只能缓解问题,不能从根本上解决问题;
- 客户端中设置socket的SO_REUSEADDR选项,意味着你可以重用一个地址。
- 客户端机器打开 tcp_tw_recycle 和 tcp_timestamps 选项;
- 客户端机器打开 tcp_tw_reuse 和 tcp_timestamps 选项;
- 客户端机器设置 tcp_max_tw_buckets 为一个很小的值。
3.6 TCP状态转换图

3.7 TCP协议如何保证可靠传输
- 确认和重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。
- 数据校验(CRC,循环冗余校验码,保证数据传输的正确性和完整性)
- 数据合理分片和排序:
- UDP: IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片(fragmentation).把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个UDP数据报.
- TCP: 会按照MTU合理分片,接收方会缓存未按序到达的数据,重新排序后再交给应用层。
- 流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
- 拥塞控制: 当网络阻塞时,减少数据的发送。
- ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发送下一个分组。
3.7.1 ARQ协议
自动重传请求(Automatic Repeat-reQuest, ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ和连续ARQ协议。
停止等待ARQ协议
- 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;
- 在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;
优点是简单,缺点是信道利用率低,等待时间长。
连续ARQ协议
连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
优点:信道利用率高,容易实现,即使确认丢失,也不必重传。
缺点:不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。
3.7.2 滑动窗口与流量控制
TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
TCP通过让发送方维护一个称为接收窗口(receivewindow)的变量(TCP报文段首部的接收窗口字段)来提供流量控制。通俗的讲,接收窗口用于给发送方一个指示--该接收方还有多少可用的缓存空间。因为TCP是全双工通信,在连接两端的发送方都各自维护了一个接收窗口。
考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。解决这个问题,TCP为每一个连接设置一个 持续计时器(persistencetimer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小。
设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。大写ACK表示首部中的确认位ACK,小写ack表示确认字段的值ack。

从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在ACK=1时确认号字段才有意义。
3.7.3 拥塞控制
为什么我们需要进行拥塞控制呢?要回答这个问题,首先必须知道什么时候TCP会出现拥塞。TCP作为一个端到端的传输层协议,它并不关心连接双方在物理链路上会经过多少路由器交换机以及报文传输的路径和下一条,这是IP层该考虑的事。然而,在现实网络应用中,TCP连接的两端可能相隔千山万水,报文也需要由多个路由器交换机进行转发。交换设备的性能不是无限的!, 当多个入接口的报文都要从相同的出接口转发时,如果出接口转发速率达到极限,报文就会开始在交换设备的入接口缓存队列堆积。但这个队列长度也是有限的,当队列塞满后,后续输入的报文就只能被丢弃掉了。对于TCP的发送端来说,看到的就是发送超时丢包了。
计算机网络中的带宽、交换结点中的缓存和处理机等,都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏。这种情况就叫做拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素,其在感知到传输发生拥塞时,需要降低自己的发送速率,等待拥塞解除。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
为了进行拥塞控制,TCP发送方要维持一个拥塞窗口的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。
拥塞窗口(cwnd)定义:首先明确的是,TCP是在发送端进行阻塞控制的。TCP为每条连接准备了一个记录拥塞窗口大小的变量cwnd,它限制了本端TCP可以发送到网络中的最大报文数量。显然,这个值越大,连接的吞吐量越高,但也更容易导致网络阻塞。所以,TCP的拥塞控制本质上就是根据丢包情况调整cwnd,使得传输的吞吐率尽可能地大!而不同的拥塞控制算法就是调整cwnd的方式不同!
补充:rwnd与cwnd的区别
rwnd(Receiver Windows,接收者窗口):是用于流量控制的窗口大小,即上述流量控制中的Advertised Window,主要取决于接收方的处理速度,由接收方通知发送方被动调整。
cwnd(Congestion Windows,拥塞窗口):是用于拥塞处理的窗口大小,取决于网络状况,由发送方探查网络主动调整。
介绍流量控制时,我们没有考虑cwnd,认为发送方的滑动窗口最大即为rwnd。实际上,需要同时考虑流量控制与拥塞处理,则发送方窗口的大小不超过min{rwnd, cwnd}。下述4种拥塞控制算法只涉及对cwnd的调整,同介绍流量控制时一样,暂且不考虑rwnd,假定滑动窗口最大为cwnd;但读者应明确rwnd、cwnd与发送方窗口大小的关系。
TCP的拥塞控制采用了四种算法,即慢启动、拥塞避免、快速重传和快速恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。
1. 慢启动算法
当主机开始发送数据时,如果立即将大量数据字节注入到网络,那么就有可能引起网络阻塞,因为现在并不清楚网络的负荷情况。因此,较好的方式是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。
通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。

每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。不过“传输轮次”更加强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。
另外,慢开始的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大cwnd。为了防止拥塞窗口cwnd增长过大引起网络阻塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限ssthresh(通常ssthresh = 65535)的用法如下:
- 当 cwnd < ssthresh 时,使用上述的慢开始算法。
- 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
- 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
2. 拥塞避免算法
让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。
这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。如下图,用具体数值说明了上述拥塞控制的过程。现在发送窗口的大小和拥塞窗口一样大。

慢启动算法主要呈指数增长,粗犷型,速度快(“慢”是相对于一步到位而言的);而拥塞避免算法主要呈线性增长,精细型,速度慢,但更容易在不导致拥塞的情况下,找到网络环境的cwnd最优值。
3. 快速重传算法
由于TCP采用的是累计确认机制,即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。

接收方收到了M1和M2后都分别发出了确认。现在假定接收方没有收到M3但接着收到了M4。显然,接收方不能确认M4,因为M4是收到的失序报文段。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认。
但按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让 发送方及早知道报文段M3没有到达接收方。发送方接着发送了M5和M6。接收方收到这两个报文后,也还要再次发出对M2的重复确认。这样,发送方共收到了接收方的四个对M2的确认,其中后三个都是重复确认。
快重传算法还规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段M3,而不必继续等待M3设置的重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。
4.快速恢复算法
如果触发了快速重传,即发送方收到至少3次相同的Ack,那么TCP认为网络情况不那么糟,也就没必要提心吊胆的,可以适当大胆的恢复。为此设计快速恢复算法(Fast Recovery),其过程有以下两个要点:
- 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。
- 与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为 慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

3.7.4 TCP Nagle算法
百度百科:TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。(减少大量小包的发送)
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):
- 如果包长度达到MSS,则允许发送;
- 如果该包含有FIN,则允许发送;
- 设置了TCP_NODELAY选项,则允许发送;
- 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议(停止等待ARQ协议),只不过它是基于包停-等的,而不是基于字节停-等的。Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低.
参考文章:
3.8 TCP连接建立之后怎样检测连接没断?
有两种技术可以运用。一种是由TCP协议层实现的Keepalive机制,另一种是由应用层实现的HeartBeat心跳包。
- 在TCP中有一个 Keep-alive 的机制可以检测死连接。当连接闲置一定 的时间(参数值可以设置,默认是 2 小时)之后,TCP 协议会向对方发一个 keepalive 探 针包(包内没有数据),对方在收到包以后,如果连接一切正常,应该回复一个 ACK;如 果连接出现错误了(例如对方重启了,连接状态丢失),则应当回复一个 RST;如果对方 没有回复,那么,服务器每隔一定的时间(参数值可以设置)再发送 keepalive 探针包, 如果连续多个包(参数值可以设置)都被无视了,说明连接被断开了。
- 心跳包之所以叫心跳包是因为它像心跳一样每隔固定时间发一次,以此来告诉服务器这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么 特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。由应用程序自己发送 心跳包来检测连接的健康性。客户端可以在一个 Timer 中或低级别的线程中定时向服务器 发送一个短小精悍的包,并等待服务器的回应。客户端程序在一定时间内没有收到服务器 回应即认为连接不可用,同样,服务器在一定时间内没有收到客户端的心跳包则认为客户端已经掉线。
3.9 TCP三次握手有哪些漏洞?
3.9.1 SYN Flood攻击
SYN Flood 是 DDoS 攻击的方式之一,这是一种利用 TCP 协议缺陷,发送大量伪造 的 TCP 连接请求,从而使得被攻击方资源耗尽(CPU 满负荷或内存不足)的攻击方式。 要明白这种攻击的基本原理,还是要从 TCP 连接建立的过程开始说起: 首先,请求端(客户端)发送一个包含 SYN 标志的 TCP 报文,SYN 即同步 (Synchronize),同步报文会指明客户端使用的端口以及 TCP 连接的初始序号; 第二步,服务器在收到客户端的 SYN 报文后,将返回一个 SYN+ACK 的报文,表示客户端的请求被接受,同时 TCP 序号被加一,ACK 即确认(Acknowledgment)。 第三步,客户端也返回一个确认报文 ACK 给服务器端,同样TCP序列号被加一,到 此一个 TCP 连接完成。
以上的连接过程在 TCP 协议中被称为三次握手。 问题就出在 TCP 连接的三次握手中,假设一个用户向服务器发送了 SYN 报文后突然 死机或掉线,那么服务器在发出 SYN+ACK 应答报文后是无法收到客户端的 ACK 报文的 (第三次握手无法完成),这种情况下服务器端一般会不停地重试(再次发送 SYN+ACK 给客 户端)并等待一段时间后丢弃这个未完成的连接,这段时间的长度我们称为 SYN Timeout (大约为 30 秒-2 分钟);一个用户出现异常导致服务器的一个线程等待 1 分钟并不是什 么很大的问题,但如果有一个恶意的攻击者发送大量伪造原 IP 地址的攻击报文,发送到服务端,服务器端将为了维护一个非常大的半连接队列而消耗非常多的 CPU 时间和内存。服务器端也将忙于处理攻击者伪造的 TCP 连接请求而无暇理睬客户的正常请求(毕竟客户端 的正常请求比率非常之小),此时从正常客户的角度看来,服务器失去响应,这种情况我们称作:服务器端受到了 SYN Flood 攻击(SYN 洪水攻击)。
原理:攻击者首先伪造地址对服务器发起 SYN 请求,服务器回应(SYN+ACK)包,而 真实的 IP 会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务 器不知 道(SYN+ACK)是否发送成功,默认情况下会重试 5 次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者如果处于公网,可以伪造 IP 的话, 对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。
解决方案主要有:
- 缩短 SYN Timeout 时间。由于 SYN Flood 攻击的效果取决于服务器上保持的 SYN 半连接数,这个值=SYN 攻击 的频度 x SYN Timeout,所以通过缩短从接收到 SYN 报文到确定这个报文无效并丢弃该连 接的时间,例如设置为 20 秒以下(过低的 SYN Timeout 设置可能会影响客户的正常访问), 可以成倍的降低服务器的负荷。
- 设置SYN Cookie。就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个 IP 地址来的包会被丢弃。 可是上述的两种方法只能对付比较原始的 SYN Flood 攻击,缩短 SYN Timeout 时间仅 在对方攻击频度不高的情况下生效,SYN Cookie 更依赖于对方使用真实的 IP 地址,如果 攻击者以数万/秒的速度发送 SYN 报文,同时利用随机改写 IP 报文中的源地址,以上的方 法将毫无用武之地。例如 SOCK_RAW 返回的套接字通过适当的设置可以自己完全控制 IP 头的内容从而实现 IP 欺骗。
- SYN Cache技术。这种技术在收到 SYN 时不急着去分配系统资源,而是先回应一个ACK报文,并在一个专用的 HASH 表中(Cache)中保存这种半开连接,直到收到正确的ACK报文再去分配系统资源。
- 使用硬件防火墙。SYN FLOOD 攻击很容易就能被防火墙拦截。
本文扩展:ddos的攻击原理以及如何防止ddos攻击?
DDOS 是英文 Distributed Denial of Service 的缩写,意即“分布式拒绝服务”。

当前主要有2种流行的DDOS攻击:
- SYN Flood 攻击:这种攻击方法是经典最有效的 DDOS 方法。
- TCP全连接攻击。
TCP全连接攻击: 这种攻击是为了绕过常规防火墙的检查而设计的,一般情况下,常规防火墙大多具备 过滤 Land 等 DOS 攻击的能力,但对于正常的 TCP 连接是放过的,很多网络服务程序(如: IIS、Apache 等 Web 服务器)能接受的 TCP 连接数是有限的,一旦有大量的 TCP 连接, 则会导致网站访问非常缓慢甚至无法访问。
TCP 全连接攻击就是通过许多僵尸主机不断地与受害服务器建立大量的 TCP 连接, 直到服务器的内存等资源被耗尽而被拖跨,从而造成拒绝服务。这种攻击的特点是可绕过一般防火墙的防护而达到攻击目的。缺点是需要找很多僵尸主机,并且由于僵尸主机的 IP 是暴露的,因此容易被追踪。
解决方案可以有:
- 限制SYN流量。用户在路由器上配置 SYN 的最大流量来限制 SYN 封包所能占有的最高频宽,这样, 当出现大量的超过所限定的 SYN 流量时,说明不是正常的网络访问,而是有黑客入侵。
- 定期扫描。定期扫描现有的网络主节点,清查可能存在的安全漏洞,对新出现的漏洞及时进行清 理
- 在骨干节点配置防火墙。防火墙本身能抵御 Ddos 攻击和其他一些攻击。在发现受到攻击的时候,可以将攻击 导向一些牺牲主机,这样可以保护真正的主机不被攻击。当然导向的这些牺牲主机可以选 择不重要的,或者是 linux 以及 unix 等漏洞少和天生防范攻击优秀的系统。
- 用足够的机器承受黑客攻击。这是一种较为理想的应对策略。如果用户拥有足够的容量和足够的资源给黑客攻击, 在它不断访问用户、夺取用户资源之时,自己的能量也在逐渐耗失,或许未等用户被攻死, 黑客已无力支招儿了。不过此方法需要投入的资金比较多,平时大多数设备处于空闲状态, 和目前中小企业网络实际运行情况不相符。
- 过滤不必要的服务和端口。可以使用 Inexpress、Express、Forwarding 等工具来过滤不必要的服务和端口,即在 路由器上过滤假 IP。
3.9.2 Land攻击
LAND 攻击利用了 TCP 连接建立的三次握手过程,通过向一个目标主机发送一个用于 建立请求连接的 TCP SYN 报文而实现对目标主机的攻击。与正常的 TCP SYN 报文不同的 是:LAND 攻击报文的源 IP 地址和目的 IP 地址是相同的,都是目标主机的 IP 地址。这样 目标主机接在收到这个 SYN 报文后,就会向该报文的源地址发送一个 ACK 报文,并建立 一个 TCP 连接控制结构,而该报文的源地址就是自己。由于目的 IP 地址和源 IP 地址是相 同的,都是目标主机的 IP 地址,因此这个 ACK 报文就发给了目标主机本身。这样如果攻 击者发送了足够多的 SYN 报文,则目标计算机的 TCB 可能会耗尽,最终不能正常服务。
3.10 在三次握手和四次挥手协议中,客户端和服务器端各用到什么函数?

下面给出Socket三次握手连接图:

从图中可以看出,当客户端调用 connect()函数时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态(先调用 connect()函数,然后发送 SYN 包); 服务器监听到连接请求,即收到 SYN J 包,调用 accept()函数接收请求(先收到 SYN 包, 然后调用 accept()函数),向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态; 客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认; 服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。 总结:客户端的 connect()函数在三次握手的第二次之后返回,而服务器端的 accept ()在三次握手的第三次之后返回。
Socket四次分手连接图:

socket的主要函数包括:
- socket():创建套接字,它会创建一个结构体及收发缓冲区。此时并不指定该套接字在哪个IP和PORT端口上。
- bind():用于将套接字绑定在特定的 IP 和 PORT 上。
- listen(SOCKET s, int backlog):用于为侦听端口创建两个队列用于接收客户端的 SYN 请求,侦听客户端的 Socket 连接请求。backlog 指的就是已经完成握手了的队列的大小。
- accept():将侦听端口中的 ESTABLISHED 队列中取出那些连接。 accept 函数返回的是已建立连接的套接字描述符,包括客户端的 ip 和 port 信息,服务器的 ip 和 port 信息。
- connect():客户端连接请求。
- read(): 负责从 fd 中读取内容。当读成功时,read 返回实际所读的字节数,如果返回的 值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。
- write(): 将 buf 中的 nbytes 字节内容写入文件描述符 fd。成功时返回写的字节数。
listen 的函数为侦听端口创建两个队列:未完成队列(SYN_RCV 状态)和已完成队列。 如果不调用 listen,则客户端过来的 SYN 请求无法入队接受进一步的处理。因此,listen 是服务器的必须过程。(如下图三次握手的队列所示)

4. 在浏览器中输入URL地址发生了什么?
4.1 从协议角度层面回答

整体而言可以分为以下几个过程:
- DNS解析
- TCP连接
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
从上图可以看出用到的协议主要包括:
- DNS: 获取域名对应IP(查找过程:浏览器缓存、路由器缓存、DNS缓存)
- TCP: 与服务器建立TCP连接。
- IP: 建立TCP连接时,需要发送数据,发送数据在网络层使用IP协议。
- OPSF: IP数据包在路由器之间,路由选择使用OPSF协议。
- ARP:路由器在与服务器通信时,需要将IP转换为MAC地址,需要使用ARP协议
- HTTP: 在TCP建立完成后,使用HTTP协议访问网页。
《图解HTTP》中一幅插画生动形象地阐述了这个过程:

4.2 前端的角度
整体过程也如前面所说的那样,分为6个步骤。下面具体来分析各个步骤是干了什么。
DNS解析
DNS解析的过程就是寻找哪台机器上有你需要资源的过程。当你在浏览器中输入一个地址时,例如www.baidu.com,其实不是百度网站真正意义上的地址。互联网上每一台计算机的唯一标识是它的IP地址,但是IP地址并不方便记忆。用户更喜欢用方便记忆的网址去寻找互联网上的其它计算机,也就是上面提到的百度的网址。所以互联网设计者需要在用户的方便性与可用性方面做一个权衡,这个权衡就是一个网址到IP地址的转换,这个过程就是DNS解析。它实际上充当了一个翻译的角色,实现了网址到IP地址的转换。网址到IP地址转换的过程是如何进行的?
解析过程
DNS解析是一个递归查询的过程。

上述图片是查找www.google.com的IP地址过程。首先在本地域名服务器中查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。从上述过程中,可以看出网址的解析是一个从右向左的过程: com -> google.com -> www.google.com。但是你是否发现少了点什么,根域名服务器的解析过程呢?事实上,真正的网址是www.google.com.,并不是我多打了一个.,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上,所有网址真正的解析过程为: . -> .com -> google.com. -> www.google.com.。
我们来总结一下查找过程(查找www.baidu.com的IP):
- 浏览器搜索自己的 DNS 缓存(维护一张域名与 IP 地址的对应表);
- 搜索操作系统中的 DNS 缓存(维护一张域名与 IP 地址的对应表);
- 搜索操作系统的 hosts 文件( Windows 环境下,维护一张域名与 IP 地址的对应表);
- 操作系统将域名发送至 LDNS(本地区域名服务器,如果你在学校接入互联网,则 LDNS 服务器就在学校,如果通过电信接入互联网,则 LDNS 服务器就在你当地的电信那里。)LDNS 查询 自己的 DNS 缓存(一般查找成功率在 80% 左右),查找成功则返回结果,失败则发起一个迭代 DNS 解析请求;
- LDNS 向 Root Name Server (根域名服务器,其虽然没有每个域名的的具体信息,但存储了负责每个域,如 com、net、org等的解析的顶级域名服务器的地址)发起请求,此处,Root Name Server 返回 com 域的顶级域名服务器的地址;
- LDNS 向 com 域的顶级域名服务器发起请求,返回 baidu.com 域名服务器地址;
- LDNS 向 baidu.com 域名服务器发起请求,得到 www.baidu.com 的 IP 地址;
- LDNS 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来;
- 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来;
- 至此,浏览器已经得到了域名对应的 IP 地址。
DNS优化过程
了解了DNS的过程,可以为我们带来哪些?上文中请求到google的IP地址时,经历了8个步骤,这个过程中存在多个请求(同时存在UDP和TCP请求,为什么有两种请求方式,请自行查找)。如果每次都经过这么多步骤,是否太耗时间?如何减少该过程的步骤呢?那就是DNS缓存。
- DNS缓存
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
- 在你的chrome浏览器中输入:chrome://dns/,你可以看到chrome浏览器的DNS缓存。
- 系统缓存主要存在/etc/hosts(Linux系统)中。
- 等等
- DNS负载均衡
不知道大家有没有思考过一个问题: DNS返回的IP地址是否每次都一样?如果每次都一样是否说明你请求的资源都位于同一台机器上面,那么这台机器需要多高的性能和储存才能满足亿万请求呢?其实真实的互联网世界背后存在成千上百台服务器,大型的网站甚至更多。但是在用户的眼中,它需要的只是处理他的请求,哪台机器处理请求并不重要。DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡,又叫做DNS重定向。大家耳熟能详的CDN(Content Delivery Network)就是利用DNS的重定向技术,DNS服务器会返回一个跟用户最接近的点的IP地址给用户,CDN节点的服务器负责响应用户的请求,提供所需的内容。
TCP连接
知道了服务的IP地址,下面就开始与服务器建立连接了。就是TCP的三次握手的过程。通俗点来讲,通信连接的建立需要经历以下三个过程:
- 主机向服务器发送一个建立连接的请求(您好,我想认识您);
- 服务器接到请求后发送同意连接的信号(好的,很高兴认识您);
- 主机接到同意连接的信号后,再次向服务器发送了确认信号(我也很高兴认识您),自此,主机与服务器两者建立了连接。
发送HTTP请求
其实这部分又可以称为前端工程师眼中的HTTP,它主要发生在客户端。发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。
请求行
1
2
3
4
格式如下:
Method Request-URL HTTP-Version CRLF
eg: GET index.html HTTP/1.1
常用的方法有:GET,POST,PUT,DELETE,OPTIONS,HEAD
请求报头
请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。(客户端不一定特指浏览器,有时候也可使用Linux下的CURL命令以及HTTP客户端测试工具等。)
一般有四种首部,分别是:通用首部、请求首部、响应首部和实体首部。
常见的请求报头有:Accept,Accept-Charset,Accept-Encoding,Accept-Language,Content-Type,Authorization,Cookie,User-Agent等。

上图是使用Chrome开发者工具截取的对百度的HTTP请求以及响应报文,从图中可以看出,请求报头中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用于指定客户端用于接受哪些类型的信息,Accept-Encoding与Accept类似,它用于指定接受的编码方式。Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。
请求正文
当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求包头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。
服务器处理请求并返回HTTP报文
服务器上安装了处理http请求的应用 —— web server,常见的web server产品有apache、nginx、IIS、Lighttpd等。
当web server接收到一个HTTP请求(request),会结合配置文件,把不同请求委托给服务器上处理对应请求的程序进行处理(例如CGI脚本、JSP脚本、servlets、ASP脚本、服务器端JavaScript、或者一些其它的服务器端技术等)。不管是哪种脚本,这些服务器端(server-side)程序都会产生一个http响应(response),例如送回一个HTML页面,来让浏览器可以展现。
完成处理过程的代码框架,大部分是按照MVC设计模式搭建的,实际处理过程如下图:

MVC的处理过程是这样的:每个用户输入的请求,首先被控制器(C)接收,控制器决定用哪个模型(M)来处理,然后模型用业务逻辑来处理用户的请求,再然后控制器决定用哪个视图模型(V)来接收模型处理后的数据,最后由该视图模型对应的视图格式化模型来返回HTML字符串给浏览器。
HTTP响应报文也是由三部分组成:状态码,响应报头和响应报文。
状态码
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
- 1xx:指示信息–表示请求已接收,继续处理。
- 2xx:成功–表示请求已被成功接收、理解、接受。
- 3xx:重定向–要完成请求必须进行更进一步的操作。
- 4xx:客户端错误–请求有语法错误或请求无法实现。
- 5xx:服务器端错误–服务器未能实现合法的请求。
常见的状态码:
1
2
3
4
5
6
7
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
响应报头
常见的响应报头有Connection,Content-Disposition,Server等等。
响应报文
服务器返回给浏览器的文本信息,通常HTML, CSS, JS, 图片等文件就放在这一部分。
浏览器解析渲染页面
浏览器在收到HTML,CSS,JS文件后,它是如何把页面呈现到屏幕上的?下图对应的就是WebKit渲染的过程。

浏览器是一个边解析边渲染的过程。首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为reflow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。

JS的解析是由浏览器中的JS解析引擎完成的。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如IO读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。JS的执行机制就可以看做是一个主线程加上一个任务队列(task queue)。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈;异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。
浏览器在解析过程中,如果遇到请求外部资源时,如图像,iconfont,JS等。浏览器将重复1-6过程下载该资源。请求过程是异步的,并不会影响HTML文档进行加载,但是当文档加载过程中遇到JS文件,HTML文档会挂起渲染过程,不仅要等到文档中JS文件加载完毕还要等待解析执行完毕,才会继续HTML的渲染过程。原因是因为JS有可能修改DOM结构,这就意味着JS执行完成前,后续所有资源的下载是没有必要的,这就是JS阻塞后续资源下载的根本原因。CSS文件的加载不影响JS文件的加载,但是却影响JS文件的执行。JS代码执行前浏览器必须保证CSS文件已经下载并加载完毕。
参考文章
5. HTTP协议
Http协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写。HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务器即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。
5.1 Http的请求报文格式和响应报文格式
HTTP请求报文主要由请求行、请求头、空行、请求正文(Get请求没有请求正文)4部分组成。

请求行:由三部分组成,分别为请求方法、URL以及协议版本,之间由空格分隔;请求方法包括GET,HEAD,PUT,POST,TRACE,OPTIONS,DELETE以及扩展方法,当然并不是所有的服务器都实现了所有的方法,部分方法即便支持,出于安全考虑也是不可用的。协议版本的格式为:HTTP/主版本号.次版本号,常用的有HTTP/1.0和HTTP/1.1。请求头: 请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。其常见的请求头如下图所示:

给出了比较全面的请求头如下表:
| 协议头 | 说明 | 示例 | 状态 |
|---|---|---|---|
| Accept | 可接受的响应内容类型(Content-Types)。 | Accept: text/plain | 固定 |
| Accept-Charset | 可接受的字符集 | Accept-Charset: utf-8 | 固定 |
| Accept-Encoding | 可接受的响应内容的编码方式。 | Accept-Encoding: gzip, deflate | 固定 |
| Accept-Language | 可接受的响应内容语言列表 | Accept-Language: en-US | 固定 |
| Accept-DateTime | 可接受的按照时间来表示的响应内容版本 | Accept-Datetime: Sat, 26 Dec 2015 17:30:00 GMT | 临时 |
| Authorization | 用于表示HTTP协议中需要认证资源的认证信息 | Authorization: Basic OSdjJGRpbjpvcGVuIANlc2SdDE== | 固定 |
| Cache-Control | 用来指定当前的请求/回复中是否使用缓存机制 | Cache-Control: no-cache | 固定 |
| Connection | 客户端(浏览器)想要优先使用的连接类型 | Connection: keep-alive 或者 Connection: Upgrade | 固定 |
| Cookie | 由之前服务器通过Set-Cookie设置的一个HTTP协议Cookie | Cookie: $Version=1; Skin=new; | 固定 : 标准 |
| Content-Length | 以8进制表示的请求体的长度 | Content-Length: 348 | 固定 |
| Content-MD5 | 请求体的内容的二进制 MD5 散列值(数字签名),以 Base64 编码的结果 | Content-MD5: oD8dH2sgSW50ZWdyaIEd9D== | 废弃 |
| Content-Type | 请求体的MIME类型 (用于POST和PUT请求中) | Content-Type: application/x-www-form-urlencoded | 固定 |
| Date | 发送该消息的日期和时间(以RFC 7231中定义的”HTTP日期”格式来发送) | Date: Dec, 26 Dec 2015 17:30:00 GMT | 固定 |
| Expect | 表示客户端要求服务器做出特定的行为 | Expect: 100-continue | 固定 |
| From | 发起此请求用户的邮件地址 | From: user@itbilu.com | 固定 |
| Host | 表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略。 | Host: www.itbilu.com:80 | 固定 |
| If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要用于像 PUT 这样的方法中,仅当从用户上次更新某个资源后,该资源未被修改的情况下,才更新该资源。 | If-Match: “9jd00cdj34pss9ejqiw39d82f20d0ikd” | 固定 |
| If-Modified-Since | 允许在对应的资源未被修改的情况下返回304未修改 | If-Modified-Since: Dec, 26 Dec 2015 17:30:00 GMT | 固定 |
| If-None-Match | 允许在对应的内容未被修改的情况下返回304未修改( 304 Not Modified ),参考 超文本传输协议 的实体标记 | If-None-Match: “9jd00cdj34pss9ejqiw39d82f20d0ikd” | 固定 |
| If-Range | 如果该实体未被修改过,则向返回所缺少的那一个或多个部分。否则,返回整个新的实体 | If-Range: “9jd00cdj34pss9ejqiw39d82f20d0ikd” | 固定 |
| If-Unmodified-Since | 仅当该实体自某个特定时间以来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Dec, 26 Dec 2015 17:30:00 GMT | 固定 |
| Max-Forwards | 限制该消息可被代理及网关转发的次数 | Max-Forwards: 10 | 固定 |
| Origin | 发起一个针对跨域资源共享的请求(该请求要求服务器在响应中加入一个Access-Control-Allow-Origin的消息头,表示访问控制所允许的来源)。 | Origin:http://www.itbilu.com | 固定 : 标准 |
| Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生。 | Pragma: no-cache | 固定 |
| Proxy-Authorization | 用于向代理进行认证的认证信息。 | Proxy-Authorization: Basic IOoDZRgDOi0vcGVuIHNlNidJi2== | 固定 |
| Range | 表示请求某个实体的一部分,字节偏移以0开始。 | Range: bytes=500-999 | 固定 |
| Referer | 表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer其实是Referrer这个单词,但RFC制作标准时给拼错了,后来也就将错就错使用Referer了。 | Referer: http://itbilu.com/nodejs | 固定 |
| TE | 浏览器预期接受的传输时的编码方式:可使用回应协议头Transfer-Encoding中的值(还可以使用”trailers”表示数据传输时的分块方式)用来表示浏览器希望在最后一个大小为0的块之后还接收到一些额外的字段。 | TE: trailers,deflate | 固定 |
| User-Agent | 浏览器的身份标识字符串 | User-Agent: Mozilla/…… | 固定 |
| Upgrade | 要求服务器升级到一个高版本协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | 固定 |
| Via | 告诉服务器,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 itbilu.com.com (Apache/1.1) | 固定 |
| Warning | 一个一般性的警告,表示在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | 固定 |
- 空行:请求头的最后会有一个空行,表示请求头部结束,接下来为请求正文,这一行非常重要,必不可少。
- 请求正文:可选部分,比如GET请求就没有请求正文。
HTTP响应报文主要由状态行、响应头、空行、响应正文4部分组成。

- 状态行:由 3 部分组成,分别为:协议版本,状态码,状态码描述,之间由空格分隔;
- 响应头:与请求头类似,为响应报文添加了一些附加信息。常见响应头如下图所示:

给出了比较全面的响应头如下表:
| 响应头 | 说明 | 示例 | 状态 |
|---|---|---|---|
| Access-Control-Allow-Origin | 指定哪些网站可以跨域资源共享 | Access-Control-Allow-Origin: * | 临时 |
| Accept-Patch | 指定服务器所支持的文档补丁格式 | Accept-Patch: text/example;charset=utf-8 | 固定 |
| Accept-Ranges | 服务器所支持的内容范围 | Accept-Ranges: bytes | 固定 |
| Age | 响应对象在代理缓存中存在的时间,以秒为单位 | Age: 12 | 固定 |
| Allow | 对于特定资源的有效动作; | Allow: GET, HEAD | 固定 |
| Cache-Control | 通知从服务器到客户端内的所有缓存机制,表示它们是否可以缓存这个对象及缓存有效时间。其单位为秒 | Cache-Control: max-age=3600 | 固定 |
| Connection | 针对该连接所预期的选项 | Connection: close | 固定 |
| Content-Disposition | 对已知MIME类型资源的描述,浏览器可以根据这个响应头决定是对返回资源的动作,如:将其下载或是打开。 | Content-Disposition: attachment; filename=”fname.ext” | 固定 |
| Content-Encoding | 响应资源所使用的编码类型。 | Content-Encoding: gzip | 固定 |
| Content-Language | 响应内容所使用的语言 | Content-Language: zh-cn | 固定 |
| Content-Length | 响应消息体的长度,用8进制字节表示 | Content-Length: 348 | 固定 |
| Content-Location | 所返回的数据的一个候选位置 | Content-Location: /index.htm | 固定 |
| Content-MD5 | 响应内容的二进制 MD5 散列值,以 Base64 方式编码 | Content-MD5: IDK0iSsgSW50ZWd0DiJUi== | 已淘汰 |
| Content-Range | 如果是响应部分消息,表示属于完整消息的哪个部分 | Content-Range: bytes 21010-47021/47022 | 固定 |
| Content-Type | 当前内容的MIME类型 | Content-Type: text/html; charset=utf-8 | 固定 |
| Date | 此条消息被发送时的日期和时间(以RFC 7231中定义的”HTTP日期”格式来表示) | Date: Tue, 15 Nov 1994 08:12:31 GMT | 固定 |
| ETag | 对于某个资源的某个特定版本的一个标识符,通常是一个 消息散列 | ETag: “737060cd8c284d8af7ad3082f209582d” | 固定 |
| Expires | 指定一个日期/时间,超过该时间则认为此回应已经过期 | Expires: Thu, 01 Dec 1994 16:00:00 GMT | 固定 : 标准 |
| Last-Modified | 所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示) | Last-Modified: Dec, 26 Dec 2015 17:30:00 GMT | 固定 |
| Link | 用来表示与另一个资源之间的类型关系,此类型关系是在RFC 5988中定义 | Link: ; rel=”alternate” | 固定 |
| Location | 用于在进行重定向,或在创建了某个新资源时使用。 | Location: http://www.itbilu.com/nodejs | 固定 |
| P3P | P3P策略相关设置 | P3P: CP=”This is not a P3P policy! | 固定 |
| Pragma | 与具体的实现相关,这些响应头可能在请求/回应链中的不同时候产生不同的效果 | Pragma: no-cache | 固定 |
| Proxy-Authenticate | 要求在访问代理时提供身份认证信息。 | Proxy-Authenticate: Basic | 固定 |
| Public-Key-Pins | 用于防止中间攻击,声明网站认证中传输层安全协议的证书散列值 | Public-Key-Pins: max-age=2592000; pin-sha256=”……”; | 固定 |
| Refresh | 用于重定向,或者当一个新的资源被创建时。默认会在5秒后刷新重定向。 | Refresh: 5; url=http://itbilu.com | 固定 |
| Retry-After | 如果某个实体临时不可用,那么此协议头用于告知客户端稍后重试。其值可以是一个特定的时间段(以秒为单位)或一个超文本传输协议日期。 | 示例1:Retry-After: 120 示例2: Retry-After: Dec, 26 Dec 2015 17:30:00 GMT | 固定 |
| Server | 服务器的名称 | Server: nginx/1.6.3 | 固定 |
| Set-Cookie | 设置HTTP cookie | Set-Cookie: UserID=itbilu; Max-Age=3600; Version=1 | 固定:标准 |
| Status | 通用网关接口的响应头字段,用来说明当前HTTP连接的响应状态。 | Status: 200 OK | |
| Trailer | Trailer用户说明传输中分块编码的编码信息 | Trailer: Max-Forwards | 固定 |
| Transfer-Encoding | 用表示实体传输给用户的编码形式。包括:chunked、compress、 deflate、gzip、identity。 | Transfer-Encoding: chunked | 固定 |
| Upgrade | 要求客户端升级到另一个高版本协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | 固定 |
| Vary | 告知下游的代理服务器,应当如何对以后的请求协议头进行匹配,以决定是否可使用已缓存的响应内容而不是重新从原服务器请求新的内容。 | Vary: * | 固定 |
| Via | 告知代理服务器的客户端,当前响应是通过什么途径发送的。 | Via: 1.0 fred, 1.1 itbilu.com (nginx/1.6.3) | 固定 |
| Warning | 一般性警告,告知在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | 固定 |
| WWW-Authenticate | 表示在请求获取这个实体时应当使用的认证模式。 | WWW-Authenticate: Basic | 固定 |
- 空行
- 响应正文

5.2 Http状态码
- 1xx:指示信息–表示请求已接收,继续处理
- 2xx:成功–表示请求已被成功接收、理解、接受
- 3xx:重定向–要完成请求必须进行更进一步的操作
- 4xx:客户端错误–请求有语法错误或请求无法实现
- 5xx:服务器端错误–服务器未能实现合法的请求
常见的状态码含义:
- 200 OK 表示服务器已成功处理了请求并提供了请求的网页
- 202 Accepted 已接收请求,但处理尚未完成
- 204 No Content 没有新文档,浏览器应该继续显示原来的文档
- 206 Partial Content 客户端进行了范围请求。响应报文中由 Content-Range 指定实体 内容的范围。实现断点续传。
- 301 Moved Permanently 永久性重定向。请求的网页已永久移动到新位置。
- 302 Moved Temporatily 临时性重定向。请求的网页临时移动到新位置。
- 304 Not Modified 未修改。自从上次请求后,请求的内容未修改过。
- 401 Unauthorized 客户试图未经授权访问受密码保护的页面。应答中会包含一个 WWW-Authenticate 头, 浏览器据此显示用户名字/密码对话框, 然后在填写合适的 Authorization 头后再次发出请求。
- 403 Forbidden 服务器拒绝请求。
- 404 Not Found 服务器上不存在客户机所请求的资源。
- 500 Internal Server Error 服务器遇到一个错误,使其无法为请求提供服务。
5.3 HTTP 1.0和HTTP 1.1 的主要区别是什么?
HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在:
- 缓存处理。在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
- 带宽优化及网络连接的使用。HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- 错误通知的管理。在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
- Host头处理。在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
- 长连接。HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
HTTP2.0和HTTP1.X相比的新特性如下:
- 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
- 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
- header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。
5.4 HTTP与HTTPS的区别?
- 端口:HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。
- 安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。
- 对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等;
- 非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。
5.5 常用的HTTP方法有哪些?

注意:只有 POST 和 PUT 方法才有请求内容。
1
2
3
4
5
6
GET: 用于请求访问已经被 URI(统一资源标识符)识别的资源,可以通过 URL 传参 给服务器。
POST: 用于传输信息给服务器,主要功能与 GET 方法类似,但一般推荐使用 POST 方式.
PUT: 传输文件,报文主体中包含文件内容,保存到对应 URI 位置。
HEAD: 获得报文首部,与 GET 方法类似,只是不返回报文主体。
DELETE:删除文件,与 PUT 方法相反,删除对应 URI 位置的文件。
OPTIONS:查询相应 URL 支持的 HTTP 方法。
并非所有的服务器都实现了这几个方法。有的服务器还实现了自己特有的HTTP方法,称为扩展方法。
- GET:GET 可以说是最常见的了,它本质就是发送一个请求来取得服务器上的某一资源。资源通过一组 HTTP 头和呈现数据(如 HTML 文本,或者图片或者视频等)返回 给客户端。GET 请求中,永远不会包含呈现数据。
- POST: 向服务器提交数据。这个方法用途广泛,几乎目前所有的提交操作都是靠这个完成。
- PUT: 这个方法比较少见。HTML 表单也不支持这个。本质上来讲, PUT 和 POST 极 为相似,都是向服务器发送数据,但它们之间有一个重要区别,PUT 通常指定了资源 的存放位置,而 POST 则没有,POST 的数据存放位置由服务器自己决定。举个例子: 如一个用于提交博文的 URL,/addBlog。如果用 PUT,则提交的 URL 会是像这样 的”/addBlog/abc123”,其中 abc123 就是这个博文的地址。而如果用 POST,则这 个地址会在提交后由服务器告知客户端。目前大部分博客都是这样的。显然,PUT 和 POST 用途是不一样的。具体用哪个还取决于当前的业务场景。
- DELETE:删除某一个资源。基本上这个也很少见,不过还是有一些地方比如 amazon 的 S3 云服务里面就用的这个方法来删除资源。
- HEAD:HEAD 和 GET 本质是一样的,区别在于 HEAD 不含有呈现数据,而仅仅是 HTTP 头信息。有的人可能觉得这个方法没什么用,其实不是这样的。想象一个业务情 景:欲判断某个资源是否存在,我们通常使用 GET,但这里用 HEAD 则意义更加明确。
- OPTIONS:这个方法很有趣,但极少使用。它用于获取当前 URL 所支持的方法。若 请求成功,则它会在 HTTP 头中包含一个名为“Allow”的头,值是所支持的方法, 如“GET, POST”。
- TRACE:请求服务器回送收到的请求信息,主要用于测试和诊断,所以是安全的。
5.6 HTTP的特点
- 支持客户端/服务器端通信模式。
- 简单方便快速:当客户端向服务器端发送请求时,只是简单的填写请求路径和请求 方法即可,然后就可以通过浏览器或其他方式将该请求发送就行了。比较常用的请求方法 有三种,分别是:GET、HEAD、POST。不同的请求方法使得客户端和服务器端联系的方式 各不相同。因为 HTTP 协议比较简单,所以 HTTP 服务器的程序规模相对比较小,从而使得 通信的速度非常快。
- 灵活:Http 协议允许客户端和服务器端传输任意类型任意格式的数据对象。这些不 同的类型由 Content-Type 标记。
- 无连接:无连接的含义是每次建立的连接只处理一个客户端请求。当服务器处理完客户端的请求之后,并且收到客户的反馈应答后,服务器端立即断开连接。采用这种通信方式可以大大的节省传输时间。
- 无状态:Http 是无状态的协议。所谓的无状态是指协议对于请求的处理没有记忆功 能。无状态意味着如果要再次处理先前的信息,则这些先前的信息必须要重传,这就导致了数据量传输的增加。但是从另一方面来说,当先前的信息服务器不在使用的时候,则服 务器的响应将会非常的快。
5.7 HTTPS的安全性是怎样实现的?

- 客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
- Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
- 客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
- 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
- Web服务器利用自己的私钥解密出会话密钥。
- Web服务器利用会话密钥加密与客户端之间的通信。

5.8 Session原理
session可以放在文件、内存或数据库都可以,是以键值对的形式存储的。Session也是一种key-value的属性对。当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否包含了一个session标识——称为session id。如果已包含一个 session id 则说明以前已经为此客户端创建过 session,服务器就按照 session id 把这个 session 检索 出来使用(如果检索不到,可能会新建一个,根据 getSession()方法的参数)。如果客户端请求不包含session id,那么则为此客户端创建一个session,并且生成一个与此session相关联的session id,这个session id将在本次响应中返回给客户端保存。
Session的客户端保存方法一般有以下三种:
- 使用Cookie来保存,这是最常见的方法,“记住我的登录状态”功能的实现正是基于这种方式的。服务器通过设置 Cookie的方式将 Session ID 发送到浏览器。如果我们不 设置过期时间,那么这个 Cookie 将不存放在硬盘上,当浏览器关闭的时候,Cookie 就消失了,这个 Session ID 就丢失了。如果我们设置这个时间,那么这个 Cookie 会保存在客户端硬盘中,即使浏览器关闭,这个值仍然存在,下次访问相应网站时,同 样会发送到服务器上。
- URL重写。就是把 session id 直接附加在 URL 路径的后面,也就是像我们经常看到 JSP 网站会有 aaa.jsp?JSESSIONID=*一样的。
- 在页面表单里面增加隐藏域,这种方式实际上和第二种方式一样,只不过前者通过 GET 方式发送数据,后者使用 POST 方式发送数据。但是明显后者比较麻烦。
session什么时候被创建?
一个常见的错误是以为 session 在有客户端访问时就被创建,然而事实是直到某 server 端程序(如 Servlet)调用 HttpServletRequest.getSession(true)这样的语句时才会被创建。注意如果JSP没有显示的使用<%@page session="false"%>关闭session,则JSP文件在编译成Servlet的时候将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。
由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。
session何时被删除
- 程序调用 HttpSession.invalidate()
- 距离上一次收到客户端发送的 session id 时间间隔超过了 session 的最大有效时间
- 服务器进程被停止。
注意:关闭浏览器只会使存储在客户端浏览器内存中的 session cookie 失效,不会使服务器端的 session 对象失效。
5.9 Cookie的机制
5.9.1 Cookie的种类
- 以文件形式存在硬盘空间上的永久性的 cookie。持久 cookie 是指存放于客户端硬盘 中的 cookie 信息(设置了一定的有效期限),当用户访问某网站时,浏览器就会在本地 硬盘上查找与该网站相关联的 cookie。如果该 cookie 存在,浏览器就将它与页面请求一 起通过 HTTP 报头信息发送到您的站点,然后在系统会比对 cookie 中各属性和值是否与 存放在服务器端的信息一致,并根据比对结果确定用户为“初访者”或者“老客户”。
- 停留在浏览器所占内存中的临时性的 cookie,关闭 Internet Explorer 时即从计算机 上删除。
5.9.2 Cookie的有效期
Cookie 的 maxAge 决定着 Cookie 的有效期,单位为秒。如果 maxAge 属性为正数,则表示该 Cookie 会在 maxAge 秒之后自动失效。浏览器会将 maxAge 为正数的 Cookie 持久化,即写到对应的 Cookie 文件中。无论客户关闭了浏览器 还是电脑,只要还在 maxAge 秒之前,登录网站时该 Cookie 仍然有效。下面代码中的 Cookie 信息将永远有效。
1
2
3
Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建 Cookie
cookie.setMaxAge(Integer.MAX_VALUE); // 设置生命周期为 MAX_VALUE
response.addCookie(cookie); // 输出到客户端
如果 maxAge 为负数,则表示该 Cookie 仅在本浏览器窗口以及本窗口打开的子窗口内有 效,关闭窗口后该 Cookie 即失效。 maxAge 为负数的 Cookie,为临时性 Cookie, Cookie 信息保存在浏览器内存中,因此关闭浏览器该 Cookie 就消失了。Cookie 默认的 maxAge 值为–1。
如果 maxAge 为 0,则表示删除该 Cookie。Cookie 机制没有提供删除 Cookie 的方法, 因此通过设置该 Cookie 即时失效实现删除 Cookie 的效果。失效的 Cookie 会被浏览器从 Cookie 文件或者内存中删除.
1
2
3
Cookie cookie = new Cookie("username","helloweenvsfei");// 新建 Cookie
cookie.setMaxAge(0); // 设置生命周期为 0,表示删除该 cookie
response.addCookie(cookie); // 必须执行这一句
5.9.3 Cookie的组成部分
Cookie 在 HTTP 的头部信息中。其标准格式为:
1
2
3
标准格式 : Set - Cookie: NAME=VALUE ; Expires=DATE ; Path=PATH ; Domain=DOMAIN_NAME;SECURE;
举例说明: Set-Cookie: JSESSIONID=mysession; Expires=Thu, 05-Jun-2 008 05:02:50 GMT; Path=/web;
Cookie的内容主要包含名字、值、过期时间、域和路径。Cookie 的 Expires 属性标识了 Cookie 的有效时间,当 Cookie 的有效时间过了之后, 这些数据就被自动删除了。若不设置过期时间,则表示这个 cookie 的生命期为浏览器会话 期间,关闭浏览器窗口,cookie 就消失。这种生命期为浏览器会话期的 cookie 被称为会 话 cookie(临时性 cookie),会话 cookie 保存在内存里。若设置了过期时间,浏览器 就会把 cookie 保存到硬盘上,关闭后再次打开浏览器,这些 cookie 仍然有效直到超过设 定的过期时间。存储在硬盘上的 cookie 可以在不同的浏览器进程间共享,比如两个 IE 窗口。
Cookie 的域和路径属性一起构成 cookie 的作用范围。domain 属性可以使多个 web 服务器共享 cookie。path 指定与 cookie 关联在一起的网页。
5.10 Cookie 和 Session 原理解析
客户第一次发送请求给服务器,此时服务器产生一个唯一的 sessionID,并返回给客户 端(通过 cookie),此时的 cookie 并没有 setMaxAge();只是保存于客户端的内存中,并与一 个浏览器窗口对应着,由于 HTTP 协议的特性,这一次连接就断开了。以后此客户端再发送 请求给服务器的时候,就会在请求 request 中携带 cookie,由于 cookie 中有 sessionID,所以 服务器就知道这是刚才那个客户,从而区分不同的人,购物车就是这样实现的:

两者的区别:
- cookie 数据存放在客户端,用来记录用户信息的,session 数据放在服务器上。
- 正是由于 Cookie 存储在客户端中,对客户端是可见的,客户端的一些程序可能会窥探、 复制甚至修改 Cookie 中的内容。而 Session 存储在服务器上,对客户端是透明的,不存在 敏感信息泄露的危险。(如果选用 Cookie,比较好的办法是,敏感的信息如账号密码等尽量不要写到 Cookie 中。最好是像 Google、Baidu 那样将 Cookie 信息加密,提交到服务器后再进行解密,保 证 Cookie 中的信息只有自己能读得懂。而如果选择 Session 就省事多了,反正是放在服务 器上,Session 里任何隐私都可以。)
- Session 是保存在服务器端的,每个用户都会产生一个 Session。如果并发访问的用户 非常多,会产生非常多的 Session,消耗大量的服务器内存。因此像 Google、Baidu、Sina 这样并发访问量极高的网站,是不太可能使用 Session 来追踪客户会话的。(而 Cookie 保存在客户端,不占用服务器资源。如果并发浏览的用户非常多,Cookie 是很好的选择。对于 Google、Baidu、Sina 来说,Cookie 也许是唯一的选择。)
- cookie 的容量和个数都有限制。单个 cookie 的容量不能超过 4KB,很多浏览器都限制 一个站点最多保存 20 个 cookie,而 session 没有此问题。
为此,将登录信息等重要信息存放到 SESSION 中,其他信息如果需要保留,可以放在 COOKIE 中。