网络协议
在计算机诞生以来,从最原始的单机模式到现在多台计算机协同工作,形成计算机网络,从前很难想象的信息共享、多机合作、大规模计算在今天也早已成了现实。在早期,计算机网络需要解决的痛点,就是怎样才能无障碍地发送和接受数据。而发送和接受数据的过程中,需要相关的协议来支撑,下面介绍下平时接触最多的协议TCP/IP
协议。
TCP和IP
TCP
(Transmission Control Protocol)的中文名称为传输控制协议,IP
(Internet Protocol)的中文名称为互联网互联协议,除此之外,大家常见的还有HTTP
、HTTPS
、FTP
、SMTP
、UDP
等等。因为TCP/IP
是当前最为流行的网络传输协议框架,所以我们也将TCP/IP
称为协议族。
TCP
TCP
的分层框架图如下图所示,为了表示网络拓补图在连接层面上的机器对等理念,采用了A机器和B机器的说法。
下面对图中所涉及到层进行简单的说明
- 链路层:单个0、1是没有意义的,链路层是以字节为单位把0与1进行分组,定义数据帧,写入源和目标机器的物理地址、数据、校验位来传输数据。链表层报文结构如下:
MAC地址长6个字节共48位,通常使用十六进制数表示,我们可以在命令行窗口中输入ifconfig -a
指令即可看到MAC地址。
网络层:根据
IP
定义网络地址,区分网段。子网内根据地址解析协议(ARP)进行MAC寻址,子网外进行路由转发数据包。这个数据包也就是IP数据包
。传输层:数据包通过网络层发送到目标计算机后,应用程序在传输层定义逻辑端口,确认身份后,将数据包交给应用程序,实现端口到端口间通信。最典型的传输层协议是
UDP
和TCP
。UDP
只是在IP
数据包上增加端口等部分信息,是面向无连接的,是属于不可靠的传输协议,常用于视频通信、电话会议等等,因为这些应用场景就算少/丢一两帧数据影响也不会很大。与之相反,TCP
是面向连接
的,所谓的面向连接,就是一种端到端之间通过不断地失败重试机制建立的可靠数据传输方式
,如同一条固定的信息通道承载着数据的可靠传输。应用层:传输层的数据到达应用程序时,以某种统一规定的协议格式解读数据。比如,E-mail在每个公司的程序界面、操作、管理方式都不一样,但是都能读取到邮件的信息,这是因为邮件遵守了SMTP协议,该协议如同传统的书信格式,按规定填写邮政编码以及收件人信息。
小结
程序在发送消息时,应用层按既定的协议打包数据,随后由传输层加上双方的端口号,由网络层加上双方的IP
地址,再由链路层加上双方的MAC地址,并将数据拆分成数据帧,经过多个路由器和网关后,到达目标机器。简而言之,就是按 “ 端口->IP地址->MAC地址 ”这样的路径进行数据的封装和发送,解包的时候反过来操作即可。
IP
IP
是面向无连接、无状态的,没有额外的机制保证发送的包是否有序到达。IP
的地址格式,保证了每个计算机分配的详细地址唯一。那么,既然链路层可以通过唯一的MAC
地址找到机器,为什么还需要通过唯一的IP
地址来标识呢?这是因为,在互联网区域内,我们很难通过广播的形式,从上千万的计算机当中找到目标MAC
地址的计算机而不超时,在数据投递时就需要对地址进行分层管理。
IP
地址属于网络层,主要功能在WLAN
内进行路由寻址,从而选择最佳路由。IP
报文格式如下图所示,共32位4个字节,通常用十进制数来表示。IP
地址的掩码0xffffff00
表示255.255.255.0,掩码相同,则在同一子网内。IP
协议在IP
报头中记录源IP
和目标IP
地址。
图中标橙色的部分就是数据包的生存时间,即TTL
(Time To Live),该字段表示IP
报文被路由器丢弃之前可经过的最多路由总数。TTL
初始值由源主机设置后,数据包在传输过程中每经过一个路由器TTL
值则减1,当该字段为0时,数据包就会被丢弃,并发送ICMP
报文通知源主机,以防止源主机无休止地发送报文。扩展下,ICMP
它是检测传输网络是否顺畅、主机是否可达、路由是否可用等网络运行状态的协议。我们经常使用的ping
,tracert
命令就是基于ICMP
来检测网络健康状态的。图中,TTL
右侧是挂载协议标识,表示的是IP
数据包中放置的子数据包协议类型。6代表TCP
,17代表UDP
等。
IP
报文在互联网上传输时,会经历多个物理网络,最终才能从源主机到达目标主机。举个栗子,我们在手机上给某个PC客户端发送一条消息,首先要经过无线网的IEEE 802.1x
认证,转到光纤通信上,然后进入内部企业网802.3
,并最终到达目标PC。由于不同硬件之间,对于数据帧的最大长度都有着不同的限制,这个最大长度被称为最大传输单元
,即MTU
。在不同的物理网之间,可能会对IP
报文进行分片,这个工作往往是由路由器去完成。
TCP建立连接
传输控制协议(Transmission Control Protocol,TCP),是一种面向连接
、确保数据在端到端间可靠传输
的协议。面向连接
是指在发送数据前,需要先建立一条虚拟的链路,然后让数据在这条链路上“流动”完成传输。为了确保数据的可靠传输,不仅需要对发出的每一个字节进行编号确认,校验每一个数据包的有效性,在出现超时
情况时进行重传
,还需要通过实现滑动窗口
和堵塞控制
等机制,避免网络状况恶化而最终影响数据传输的极端情形。每个TCP
数据包是封装IP
包中,每一个IP
头的后面紧接着的是TCP
头,TCP报文格式如下图所示
协议第一行的两个端口号各站两个字节,分别表示了源机器和目标机器的端口号。这两个端口号与IP
报头中的源IP
地址和目标IP
地址所组成的四元组可唯一标识一条TCP
连接。由于TCP
是面向连接的,因此有服务端和客户端之分。需要服务端先在相应的端口上进行监听,准备好接受客户端发起的建立连接请求。当客户端发起第一次请求连接的TCP
包时,目标机器端口就是服务端所监听的端口号。常见的端口号如HTTP
服务的80
端口、HTTPS
服务的443
端口、SSH
服务的22
端口等。可通过输入netstat
命令,可以得到当前机器活跃的连接信息。
协议第二行和第三行是序列号,各占4个字节。前者
是指所发送数据包
中数据部分第一字节的序号,后者
是指期望收到来自对方的下一个数据包
中数据部分第一个字节的序号。
由于TCP
报头中存在一些扩展字段,所以需要通过长度为4个bit的头部长度字段表示TCP
报头的大小,这样接收方才能准确地计算出包中数据部分的开始位置。
TCP
的FLAG由6个bit组成,分别有SYN
、ACK
、FIN
、URG、PSH、RST,都以置1表示有效。我们主要介绍下SYN
、ACK
、FIN
。
- SYN:全称Synchronize Sequence Number,用作建立连接时的同步信号
- ACK:全称Acknowledgement,用于对收到的数据进行确认,所确认的数据由确认序列号表示
- FIN:全称Finish,表示后面没有数据需要发送,通常意味着所建立的连接需要关闭了
接下来重点介绍TCP中连接建立的原理。通过图文的形式展示正常网络通信中,通过三次握手建立连接的过程。本次依然还是以A机器
和B机器
举例,因为发起请求的一端不一定是客户端
,也有可能是服务端
向另外一台服务器发送TCP
请求。前者需要在后者发起连接建立请求时先打开某个端口等待数据传输,否则将无法正常建立连接。
三次握手指的是建立连接的三个步骤:
- A机器发出一个数据包并将
SYN
置1,表示希望建立连接。图中我们假设A机器发送的数据包(seq)的序列号为x
- B机器收到A机器发过来的数据包后,通过
SYN
得知这是一个建立连接的请求,于是发送一个响应包并将SYN
和ACK
标志都置1。图中我们假设B机器发送的数据包(seq)的序列号为y
,而确认序列号必须是x+1
,表示收到了A机器发过来的SYN
。在TCP
中,SYN
被当作数据部分的一个字节。 - A机器收到B机器的响应包后需进行确认,确认包中将
ACK
置1并将确认序列号设置为y+1
,表示收到了来自B机器的SYN
。
图中展示就是TCP连接时候的三次握手,三次握手两个主要目的:信息对等
,防止超时
。从信息对等角度来看,如下表所示,双方只有确定4类信息,才能建立连接。在第2次握手后,从B机器视角看还有两个红色的NO
信息无法确认。在第3次握手后,B机器才能确认自己的发报能力和对方的收报能力是正常的。
连接三次握手也是防止出现请求超时导致脏连接
。TTL
(Time To Live)网络报文的生存时间往往都会超过TCP请求超时时间,如果两次握手就可以创建连接,传输完数据并释放连接后,第一个超时的连接请求才到达B机器,B机器会认为是A机器创建新连接的请求,然后确认同意创建连接。因为A机器的状态不是SYN_SENT
,所以直接丢弃了B机器的确认数据,以至于最后只是B机器单方面创建连接完毕,具体的流程可参考下图。
但如果是三次握手,则B机器收到连接请求后,也会向A机器确认同意后才会创建连接,但因为A机器不是SYN_SENT
状态,所以会直接丢弃,B机器由于长时间没有收到确认信息,最终超时导致连接创建失败,因此不会出现脏连接。
扩展
:从编程的角度,TCP连接的建立是通过文件描述符(File Descriptor,fd)完成的。通过创建套接字获得一个fd,然后服务端和客户端需要基于所获得的fd调用不同的函数分别进入监听状态和发起连接请求。由于fd的数量将决定服务端进程所能建立连接的数量,对于大规模分布式服务来说,当fd不足时将会出现“open too many files”错误而导致无法建立更多的连接。为此,需要注意调整服务端进程和操作系统所支持的最大文件句柄数。通过使用ulimit -n
命令来查看单个进程可以打开文件句柄的数量。如果想要查看当前系统各个进程产生了多少个句柄,可输入以下命令:
lsof -n | awk '{print $2}' | sort|uniq -c |sort -nr|more
TCP断开连接
TCP是全双工通信,双方都能作为数据的发送方和接受方,但TCP连接也会有断开的时候。建立连接只有三次,而断开连接需要四次。接下来,还是通过以下图文的方式展示连接断开的步骤。如图所示,主要分为四个步骤:
- A机器想要关闭连接,则待本方数据发送完毕后,传递
FIN
信号给B机器。 - B机器应答给
ACK
信号,告诉A机器可以断开,但是需要等B机器处理完数据,再主动给A机器发送FIN
信号。这时,A机器处于半关闭状态(FIN_WAIT_2),无法再发送新的数据。 - B机器做好连接关闭前的准备工作后,发送
FIN
给A机器,此时B机器也进入半关闭状态(CLOSE_WAIT)。 - A机器发送针对B机器FIN的
ACK
后,进入TIME_WAIT
状态,经过2MSL
(Maximum Segment Lifetime)后,没有收到B机器传来的报文,则确定B机器已经收到A机器最后发送的ACK
指令,此时TCP连接正式释放。一般来说,MSL
大于TTL
衰减至0的时间。在RFC793中规定MSL
为2分钟。
图中的红色字体TIME_WAIT
,CLOSE_WAIT
分别表示主动关闭和被动关闭产生的阶段性状态,如果在线上服务器大量出现这两种状态,就会加重机器负载,也会影响有效连接的创建,因此需要进行有针对性的调优处理。
TIME_WAIT
:主动要求关闭的A机器表示收到了对方的FIN
报文,并发送出了ACK
报文,进入TIME_WAIT
状态,等2MSL
后即可进入到CLOSED
状态。如果FIN_WAIT_1
状态下,同时收到带FIN
标志和ACK
标志的报文时,可以直接进入TIME_WAIT
,无须经过FIN_WAIT_2
状态。CLOSE_WAIT
:被动要求关闭的机器收到对方请求关闭连接的FIN
报文,在第一次ACK
应答后,马上进入CLOSE_WAIT
状态。这种状态其实表示在等待关闭,并且通知应用程序发送剩余数据,处理现场信息,关闭相关资源。
在TIME_WAIT
等待的2MSL
是报文在网络上生存的最长时间,超过阈值报文则被丢弃。但是在当前的高速网络中,2分钟的等待时间会导致网络资源的极大浪费,在高并发服务器上通常会使用更小的值。那既然TIME_WAIT
貌似是百害无一利的,为何不直接关闭,进入CLOSED
状态呢?原因如下两点:
- 确认被动关闭方能够顺利进入
CLOSED
状态。如上图所示,假如最后一个ACK
由于网络原因导致无法到达B机器,处于LAST_ACK
的B机器通常“自信”地以为对方没有收到自己的FIN
+ACK
报文,所以会重发。A机器收到第二次的FIN
+ACK
报文,会重发一次ACK
,并且重新计时。如果按上面问题所描述那样,A机器收到B机器的FIN+ACK
报文后,发送一个ACK
给B机器,就“自私”地立马进入CLOSED
状态,最终可能会导致B机器无法确保收到最后的ACK
指令,也无法进入CLOSED状态。 - 防止失效请求。这样做是为了防止已失效连接的请求数据包与正常连接的请求数据包混淆而发生异常。
扩展
:TIME_WAIT是四次挥手断开连接的尾声,如果此状态连接过多,则可以通过优化服务器参数得到解决。如果不是对方连接的异常,一般不会出现连接无法关闭的情况。但是CLOSED_WAIT过多很可能是程序自身的问题,比如在对方关闭连接后,程序没有检测到,或者忘记自己关闭连接。