一 协议分层
一个网络请求需要经过多层,如底层物理层、上链路层、网络层、传输层和应用层。当然,我们的一般工作是针对应用层的,但我们也需要对传输层有深刻的了解,个人感觉也是最复杂的。下图为TCP/IP协议分层图。请注意,虽然ARP和RARP协议分为链路层,但实际上IP、ARP和RARP数据报都需要以太网驱动程序封装成帧;同样,虽然ICMP和IGMP协议分为网络层,但实际上都需要IP协议封装成数据报告。建议先看这篇文章再看《TCPIP协议之初识别。
图1 协议分层 协议分层参考图
图1中未绘制的物理层是指电信号的传输方式,如以太网通用的网线(双绞线)、以太网早期使用的同轴电缆(现在主要用于有线电视)、光纤属于物理层的概念。物理层的能力决定了最大的传输速率、传输距离、抗干扰性等。简单地说,物理层是硬件部分。
链路层有以太网、令牌环网等标准。链路层负责网卡设备的驱动、帧同步、冲突检测和数据错误验证。现在我们通常接触以太网。交换机是一种在链路层工作的网络设备。它可以在不同的链路层网络之间转发数据帧(如十兆以太网和百兆以太网、以太网和令牌环网)。由于不同链路层的帧格式可能不同,交换机应在转发前拆除链路层的第一个数据包并重新包装。
网络层主要是经常提到的IP协议。IP协议不保证数据传输的可靠性。数据包可能在传输过程中丢失,可靠性可以在上层协议或应用程序中提供支持。路由器是一种工作在第三层的网络设备,具有开关功能,可以在不同的链路层接口之间转发数据包。因此,路由器需要拆除网络层和链路层的第一个数据包并重新包装。
传输层是TCP和UDP协议。TCP协议确保数据收发的可靠性,丢失的数据包自动重新发送,上层应用程序总是收到可靠的数据流。UDP协议不面向连接或可靠性。
应用层是我们自己的应用程序。在应用程序中发送的数据不仅是网络上的数据本身,也是每个协议的头部。数据包的包装过程如图2所示。下一步将通过一个例子分析各层协议的头部和内容。
图2 TCP/IP数据报封装
二 基于TCP协议的编程1 以太网帧与ARP协议从图2可以看出,数据最终包装成以太网帧,并在网络中传输。以太网帧的格式如图3所示:
图3 以太网帧格式
在TCP编程中,在两端通信之前,指定IP机器的物理地址需要通过ARP(地址分析协议)协议确定,即其网卡的硬件地址(MAC),MAC地址长度为48位,网卡出厂时固化在网卡内,可通过命令ifconfig
查看网卡地址。ARP协议的数据报告格式如下:
图4 ARP数据报格式
例如,当我们运行命令时ping 192.168.1.100
,然后需要先获得arp协议192.168.1.100
该IP对应的MAC地址,以下是ARP协议的请求和响应包。通常操作系统会有ARP缓存,所以只要缓存没有过期,下次就可以从缓存中获取MAC地址,而不需要发送ARP请求来获取目的IP地址。
请求包
图5 ARP请求包
响应包
图6 ARP响应包
与之前发布的以太网帧格式相比,很容易分析这两个数据包。例如,以太网帧的头包含14个字节,即目的地址、源地址和协议类型。起初,目的地址填写了目的地址,因为它不知道目的IP地址的mac地址ff:ff:ff:ff:ff:ff
广播,协议类型为0x0806。ARP协议的内容可以参考上图,分布为硬件类型(以太网,标志1),IPV4(0x0800),硬件地址长度(6字节),协议地址长度(IPV4,4字节),操作码(ARP请求类型为1,ARP响应类型为2),发送方MAC地址(本机MAC地址)。送方ip地址(请求包192.168.1.106响应包为192.168.1.100),目标MAC地址(请求时不知道目标MAC地址,填写0,响应包填写目标MAC地址),目标IP地址(请求包为192.168.1.100,响应包是192.168.1.106)。
接下来要开始socket编程,先写一个简单的客户端-服务端。
#服务端:server.pyimport socketdef start_server(ip, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((ip, port)) sock.listen(1) while True: conn, cliaddr = sock.accept() print 'server connect from: ', cliaddr while True: data = conn.recv(1024) if not data: print 'client closed:', cliaddr break conn.send(data.upper()) conn.close() except Exception, ex: print 'exception occured:', ex finally: sock.close()if __name__ == "__main__": start_server('127.0.0.1', 7777)#客户端:client.pyfrom socket import *import sysdef start_client(ip, port): try: sock = socket(AF_INET, SOCK_STREAM, 0) sock.connect((ip, port)) print 'connected' while True: data = sys.stdin.readline().strip() print 'input data:', data if not data: break sock.send(data) result = sock.recv(1024) if not result: print 'other side has closed' else: print 'response from server:%s' % result sock.close() except Exception, ex: print exif __name__ == "__main__": start_client('127.0.0.1', 7777)
TCP的通信流程如下图所示,前面有三个握手,后面有四个握手。注意图为C语言函数,与python相对应的send函数发送,读取为recv函数。需要注意的是,TCP连接的套接字是一个四元组,包括源IP、源端口、目的IP和目的端口。如果客户端的ACK发送失败,客户端的连接可能是ESTABLISHED,而服务端对应的连接仍然是SYN_RCVD。
图5 TCP协议通信流程
SYN(synchronous建立在线)是TCP的状态 ACK(acknowledgement 确认) PSH(push传输) FIN(finish结束) RST(reset重置) URG(urgent紧急),但前五个字段对日常分析有用:
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有 DATA数据传输,
RST表示连接重置。
其中,ACK可以与SYN和FIN同时使用。例如,SYN和ACK可以同时使用1。它表示建立连接后的响应。如果只是一个SYN,它只表示建立连接。TCP的几次握手都是通过这样的ACK表现出来的。但SYN和FIN不会同时为1,因为前者表示建立连接,后者表示断开连接。RST通常在FIN之后出现为1,表示连接重置。一般来说,当FIN包或RST包出现时,我们认为客户端与服务器端断开连接;当SYN和SYN出现时+在ACK包中,我们认为客户端与服务器建立了连接。PSH为1的情况一般只出现在 在DATA内容不为0的包中,也就是说,PSH为1表示有真正的TCP数据包内容被传递。通过请求建立和关闭TCP的连接-完成了响应模式。
三 TCP通信流程实例分析接下来要做的就是通过wireshark观察这个过程。首先在终端上运行python server.py
,然后在第二个终端运行python client.py
,这时,我们看到了第二个终端输出connected
,表示连接。wireshark输出如下:
图6 TCP建立连接三次握手的连接
从图中可以看到三次握手的过程。在这里,我们拿出第一个SYN包来分析数据包的格式。我们已经提到了以太网帧的格式。前12个字节是目的地址和源地址。因为是本地址,都是00,然后两个字节帧类型是0800,即IP协议。以下是IP协议栈和TCP协议栈的内容。
首先,IP协议堆栈的格式如图7所示。数据包如图8所示。在第一个字节0x45中,前四个是IPV4版本,然后四个是第一个长度,代表4*5=20字节,指的是整个IP数据包的长度。第二字节0x00是服务型TOS,有三个位置用于指定IP数据报告的优先级,现在几乎不需要了。然后16位0x003c是IP包的总长度60(第一个20字节+数据40字节)。从IP数据包的第一个字节45到最后,总共有60个字节。然后是16位0xe963作为标识。如果IP包的大小超过MTU,则需要拆分。该标识字段用于识别哪些包在拆分前是相同的组。接下来的16位0x4000,前三位是标志位,其中最高位为0,第二位为DF(don't fragment)1,即不分片,第三名为MF(more fragments,更多的分片)位于0,因为这里没有分片; 接下来的13位是片的偏移,这里没有分片,所以是0。接下来的8位0x40是TTL,值为64(TTL在Traceroute上非常有用,TTL是这样使用的:源主机为数据包设定一个生存时间,比如64,每个路由器都会减少1。如果减少到0,说明路由太长,找不到目的主机网络,就丢弃了包。所以这个生存时间的单位不是秒,而是跳。(hop)),另外八个是协议字段,表示上层协议是TCP、UDP、ICMP、IGMP等。这是TCP,所以值0x06。我们是TCP,所以值0x06。然后16位0x5356是第一次验证,只验证IP,数据验证由更高层协议负责。然后32位7f00001是源IP地址127.0.0.1,然后32位是目的IP地址127.0.0.1,选项是空的,然后是TCP协议栈的内容。
1 三次握手数据包分析图8 IP数据包实例
如图9、10所示,TCP段格式和数据段实例。前16位0xdbb8为源端口56280,后16位0x1e61为目的端口777。然后是32位序号0x2d4a6c26,即759852070。请注意,为了在wireshark中显示友好性,显示值为0,这是一个相对序号(可以在右键Protocol中使用 在Preference中取消相对序号选项可以看到绝对序号)。然后是32位确认序号0x000000。在接下来的16位中,前4位是第一个长度0xa,即4*10=40
个字节。这里的TCP段只有40个字节,也就是说没有数据部分,只有第一个字节。接下来的6位是保留位,这16位的最后6位是6位标志位0x002,分布在URG,ACK,PSH,RST,SYN,FIN,URG首先,ACK是确认标志,PSH是将数据推送到接收过程标志,RST是复位连接标志,SYN是同步序号标志,FIN是完成数据发送标志。这里只看到SYN标志位置为1,表示是同步序号。然后16位0xaaaa,说窗口大小43690。然后是16位校准和0xfe30,然后是16位紧急指针0x000,然后是选项字段。
TCP选项字段格式分为三部分,kind为选项类型,length为选项总长度(包括kind和length),info为选项值,下表为常见选项值和含义,更多选项值见参考资料2。
kind (1字节)
length (1字节)
info (n字节)
含义
0
空
空
表示选项表结束
1
空
空
空操作nop通常用于填充TCP选项的总长度为4倍
2
4
MSS
最大段长度
3
3
window scale
滑动窗扩大因子
4
2
SACK
选择性确认
8
10
timestamp
时间戳值4字节+时间戳回答4字节
首先可以看到选项字段 0x02 04 ff d7表示为MSS,长度为4字节,值为65495。MSS通常等于MTU-20-20,而MTU通常设置为1500,所以MSS通常是1460。当然,考虑到TCP的选项值可能占据最多20个字节,MSS也可能是1460-12-8=1440。通常MTU是1500,lo特别,65536,所以这里的MSS不是1440,而是0xfd7=65495。服务器可以配置MTU,客户端和服务器端会在TCP通信过程中协商最小的MSS作为最终值。有了MSS限制,TCP可以保证IP层不需要分片。然后0x0402是选择性确认选项,类别为4,总长度为2字节,这里说没有info。接着的0x08 0a ff ff 85 45 00 00 00 00是时间戳,类别为8,总长度为10,内容为0xfff8545=4294935877,时间戳回显值为0x000000=0。然后是1字节0x01,类别为1,表示空操作,用于填充。接着是0x03 03 07。类别为3,长度为3,值为7,表示窗口扩大因子为7。
图9 TCP段格式
图10 TCP数据段实例
第二阶段SYN+ACK与第三阶段ACK的数据包相似,如下所示,SYN+ACK中的标记是SYN和ACK位置,然后时间戳回显示为当前时间;ACK数据包的第三阶段是ACK标记位置和时间戳回显示。
图11 SYN+ACK数据包第二阶段
图12 ACK数据包第三阶段ACK数据包
2 发送数据的数据包分析让我们来看看发送数据的包。我们在client.Py终端输入 haha,您可以看到四个数据包被捕获。第一个包是从客户端发送到服务端的PSH和ACK标志位置。同时,数据是haha;第二个包是服务端发送到客户端,ACK标志位置;第三个包是服务端发送到客户端,PSH和ACK标志位置,这是服务器发送的HAHA内容;第四包是客户端发送到服务端,ACK标志位置,确认收到数据。需要注意的是,由于TCP是全双工通信,客户端和服务端各自维护了一个序号。需要注意的是,客户端和服务端维护了一个序号,因为TCP是全双工通信。如果一个面向连接的协议是半双工的,通信过程只能通过一个问题和一个答案来传输。收发两个方向不能同时传输。如果同时只允许一个方向的数据传输,只需要一套序号,双方不需要维护一套序号。ACK从服务端发送到客户端是5,因为收到的数据长度是4,所以新的请求序列号是5(注意相对序列号)。同样,客户端发送数据的序列号是5,序列号与数据长度有关。
图13 发送数据
3 关闭连接四次握手数据包分析关闭TCP连接时,会有四次握手,主动关闭的一方会在TIME_WAIT状态一段时间后完全关闭。TIME_WAIT的时间是系统配置参数TCP_fin_timeout设置,Linux通常是60秒,当然,我们也可以缩短它,关于为什么Time_WAIT状态,有两个主要原因:一是可靠地实现TCP全双工连接的终止,二是允许旧的重复分段在网络中消失。首先,假设最终客户端发送的ACK服务端未收到,服务端将重新发送FIN。此时,处于TIME_WAIT状态的客户端连接也可以重新发送ACK。否则,如果连接已经关闭,客户端会发送RST的分段,这样服务端就会被分析为错误,这不是我们想要的。第二点,如果没有TIME_WAIT,然后,在连接关闭后,可以建立新的连接连接、IP地址和端口与以前一样,TCP必须防止旧的重复分组或延迟分组在连接终止后再现,否则无法区分旧连接或新连接,如下图所示。当然,这种情况很难发生,因为新的和旧的必须有相同的IP地址和端口,旧的分组ISN号也应该有效。通常,新的连接会使用一个随机的端口号,很难像以前一样,而旧的分组序列号ISN几乎不可能有效。因此,TCP禁止在TIME_WAIT状态的端口发起新的连接。TIME_WAIT时间后,建立新的连接基本上可以保证连接前旧化身的重复分组已经消失。
图14 TIME_WAIT存在的原因
在第二个终端,CTRL+C关闭client.py,您可以看到wireshark捕获的数据包如下。您可以看到服务器的FIN和ACK合并在一个数据包中。此外,可以看到客户端连接处于TIME_WAIT状态,一段时间后关闭。
图15 关闭连接四次握手
图16 TIME_WAIT状态查看
四 总结这是TCP/IP协议的第一篇文章,总结了每个协议的格式和数据包的分析,第二篇文章将专注于分析,如SO_REUSEADDR, backlog
等待参数的意义,尤其是backlog参数,只有查了很多资料才能理解一两个。
参考文献:
http://www.jianshu.com/p/cf5948ad3b40(推荐)
Unix网络编程
TCP头部选项
time-wait-and-its-design-implications-for-protocols-and-scalable-servers