数据包封装
不同的协议层对数据包有不同的称谓。在传输层叫做段,在网络层叫做数据报,在链路层叫做帧。数据封装成帧后发送到传输介质上,到达目的的主机后每层协议再剥掉相应的首部,最后将应用层数据交给程序应用。
协议
网络协议简称协议,是通信计算机双方必须共同遵循的一组约定。包括如何建立连接、怎样互相识别等。只要遵守这个约定,计算机之间才能互相通信交流。
三要素:语法、语义、时序
为了使数据再网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议,它最终体现在网络上传输的数据包的格式
协议往往分成几个层次进行定义,分层定义是为了使某一层协议的改变不影响其他层次的协议。
常见的协议:
应用层:FTP(文件传输协议)、HTTP(超文本传输协议)、NFS(网络文件系统)
传输层:TCP(传输控制协议)、UDP(用户数据报协议)
网络层:IP(因特网互联协议)、ICMP(因特网控制协议)、IGMP(因特网组管理协议)
网络接口层:ARP(地址解析协议)、RARP(反向地址解析协议)
以太网帧格式
其中目的地址和源地址是指网卡的硬件地址(MAC地址),长度48位,是在出厂时固化的。可以通过ifconfig命令查看。协议字段有三种值,分别对应IP、ARP、RARP。帧尾是CRC校验码。
以太网帧中的数据长度规定最小46字节,最大1500字节,ARP和RARP数据包的长度不够46字节,要在后面进行补充。最大值1500称为以太网的最大传输单元(MTU),不同的网络类型有不同的MTU,如果一个数据包从以太网路由器到拨号链路上,数据包长度大于拨号链路MTU,则需要对数据包进行分片。【ifcong中可以看到】MTU这个概念指数据帧有效载荷的最大长度,不包括帧头长度。
补充:TTL 表示最长生命周期 ,每跳一个单位(每经过一个路由器TTL-1)
提问:为什么不把以太网帧协议载荷变大(变得超级大)?
因为如果发送的数据接受端没有接收到,要重发。显然重发1500比60000方便(快)。
ARP数据报格式
在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号(提前告知的,在程序中包含的),却不知道目的主机的硬件地址,而数据包首先被网卡接受到再去处理上层协议,如果接受到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。
每台主机的内核中都维护着一个ARP缓存表,源主机首先在主机的ARP表中查找目的主机的硬件地址,如果找到了就将数据包发送给对应的目的主机。如果没有,则源主机会广播一个ARP请求到本地网段(以太网帧首部的硬件地址填FF:FF:FF:FF:FF:FF表示广播)。目的主机接受到广播的ARP请求,发现其中的IP地址与本机的相符,则发送一个ARP应答数据包给源主机,并更新自己的ARP缓存表,将自己的硬件地址填写在应答包中。
每台主机都维护着一个ARP缓存表,可以用arp -a命令查看。缓存表中的表项过期时间一般为20分钟。
硬件类型:1表示MAC地址
协议类型:0x800表示IP地址
硬件地址长度:6 (6*8 = 48)
协议地址长度:4 (4*8 = 32)
操作:1表示ARP请求 2表示ARP应答 3表示RARP请求 4表示RARP应答
当是广播时:目的端以太网地址 FF:FF:FF:FF:FF:FF
IP数据报格式
IP数据报的首部长度和数据长度是可变长的,但总是4字节的整数倍。对于IPV4,4位版本字段是4。4位头部长度的数值是以4字节位单位的,最小值为5 ,也就是说首部长度最小值为4*5=20,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃),还要4个位表示可选的服务类型(最小延迟、最大吞吐、最大可靠性、最小成本),还有一个位总是0。
总长度是整个数据报的字节数(包括头部和数据)
每传一个IP数据报,16位的标识加1,可用于分片和重新组装数据报。
3位标志和13位偏移量用于分片。
TTL:源主机为数据报设定的一个生存时间,每经过一个路由器就减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间是跳(hop)。协议字段指示上层协议是TCP、UDP、ICMP、IGMP。然后是校验和,只校验IP首部,数据的校验由更高层协议负责。
UDP协议格式
源端口号:发送方端口号
目的端口号:接收方端口号
长度:UDP用户数据报的长度,最小为8(仅含头部)
校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃
TCP协议格式
1.源端口号:发送方端口号
2.目的端口号:接收方端口号
3.序列号:本报文段的数据的第一个字节的序号
4.确认序号:期望收到对方下一个报文段的第一个数据字节的序号
5.首部长度(数据偏移):TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。单位:32位
6.保留:占6位,保留今后使用,目前应置为0
7.紧急URG:此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
8.确认ACK:仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文都必须把ACK置1
9.推送PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望子啊键入一个命令后立即能够收到对方响应。在这种情况下,TCP就可以使用推送操作,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快交付给接收应用进程,而不再等到整个缓冲都填满后再向上交付
10.复位PST:用于复位相应的TCP连接
11.同步SYN:仅在三次握手建立TCP连接时才有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1。SYN置1就表示这是一个连接请求或连接接受报文
封装
应用程序数据在发送到物理网络之前,将沿着协议栈从上往下依次传递,每层协议都将在上层数据的基础上加上自己的头部信息(包括以太网帧的尾部CRC),以实现该层的功能,这个过程就称为封装
分用
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中的本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用。分用时依靠头部信息中的类型字段实现的。
TCP详解
TCP与UDP
TCP和UDP都是传输层的协议。
UDP:用户数据报协议,面向无连接,可以单播、多播、广播,面向数据报,不可靠交付
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
UDP | TCP | |
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠交付(丢包不重传) | 可靠交付(丢包重传) |
连接对象的个数 | 一对一、一对多、多对多 | 一对一 |
传输方式 | 面向数据报 | 面向字节流 |
首部开销 | 8个字节 | 最少20个字节 |
适用场景 | 实时应用(视频会议、直播) | 可靠性高的应用(文件传输) |
TCP通信流程
服务器端:
1.创建一个用于监听的套接字
-监听:监听有客户端的连接
-套接字:这个套接字其实就是一个文件描述符
2.将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
-客户端连接服务器的时候使用的就是这个IP和端口
3.设置监听,监听的fd开始工作
4.设置阻塞,当有客户端发起连接,解除阻塞。接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5.通信
-接受数据
-发送数据
6.通信结束,断开连接
客户端:
1.创建一个用于通信的套接字(fd)
2.连接服务器,需要指定连接的服务器的 IP 和 端口
3.连接成功,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4.通信结束,断开连接
TCP 三次握手
TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓“连接”,其实是客户端和服务器端内存里保持的一份关于对方的信息(IP地址、端口号)
TCP可以看作是一种字节流,他会处理IP层或以下的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放到TCP头部
TCP提供一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接、四次挥手来关闭一个连接。
三次握手的过程:
1.客户端发送一个带SYN标志的TCP报文到服务器。这个三次握手过程中的段1
客户端发出段1,SYN位表示连请求。序号是0,这个序号在通信中用作临时地址,每发送一个书记字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包是情况。另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由发了SYN位,因此下次再发送应该用序号1001。mms表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过发来的段不要超过这个长度。
2.服务器端回应一个带ACK和SYN标志的响应报文。它表示对刚才客户端SYN的回应,同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。这是三次握手中的第2个报文段。
服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1,表示“我接受到序号为0及以前所有的段,请你下次发送序号为1的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024
3.客户必须再次回应服务器端一个ACK报文。这个报文段2
客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器端分别给对方发了连接请求,有应答了对方的连接请求。在建立连接的同时,双方协商了一些信息。例如双方发送序号的初始值、最大段尺寸
为什么是3次握手呢?
通过3次握手才能够确定客户端的发送数据、数据数据的功能正常。服务器端的接收数据和发送数据正常。2次握手显然无法证明,比如客户端端发送SYN连接请求,服务器端回应ACK报文并发送,那么只能证明客户端发送数据正常,接收数据的能力不能确定。4次握手也可以达到,但是3次就能搞定,多花费一次没有必要。并且在三次握手的过程中,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸。
TCP 滑动窗口(TCP流量控制)
滑动窗口是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络拥挤情况直接发送数据。由于大家不知道网络拥挤状况,同时发送数据,导致中间节点阻塞丢包,谁也发送不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(窗口尺寸)。
还有一种情况,当发送端发送的速度较快,接收端收到数据后处理速度较慢,而接收缓冲区的大小是固定的(一般采用循环队列),就会丢失数据。TCP协议通过“滑动窗口”机制解决着一问题。
看上面的通讯过程:
1.发送端(客户端)发起连接,声明最大尺寸是1460。初始序号是0,窗口大小4K,表示“我的接收缓冲区还有4K字节空闲区,你发送的数据不要超过4K”。接受端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三次握手结束。
2.发送端发出段4-9,每一个段带1K的数据,发送端根据窗口大小知道接受端的缓冲区满了,因此停止发送数据。(6*1024=6K)
3.接收端的应用程序处理了2K数据,接受缓冲区有2K空闲,接收发出段10,再应答已收到6K数据同时声明窗口大小为2K,并且包含了下一次发送的序号
4.接收端的应用程序处理了2K数据,接收缓冲区有4K空闲,接收发出段11。重新声明窗口大小为4K.
5.发送端发出段12-13,每一个段带1K数据,段13同时包含FIN位(FIN表示断开连接)
6.接收端应答接收到2K数据(6145-8192),再加上FIN位占用一个序号8193,因此告知发送端下次发送从序号8194开始,连接处于半关闭状态,接收段同时声明窗口大小为2K
7.接收端的应用程序处理了2K数据,接收端重新声明窗口大小为4K
8.接收端的应用程序处理了2K数据,接收端重新声明窗口大小为6K
9.接收端的应用程序处理全部数据后,决定关闭连接,发出段17,包含FIN位,发送端应答,连接完全关闭
随着应用程序提走数据,虚线框不断向右滑动,因此称为滑动窗口
我们还可以分析出一个现象:
应用程序所看到的数据是一个整体或者说是一个流,在底层通讯中这些数据可能被拆成数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向字节流的协议,而UDP是面向消息的协议,每一个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的
TCP 四次挥手
由于TCP是半双工的,因此每一个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向上的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将主动关闭,而另一方执行被动关闭。(发送数据跟发送请求应答不一样,一个定义在TCP头部(FIN、ACK、SYN),一个属于数据部分)
1.客户端发送FIN位表示关闭连接的请求
2.服务器应答客户端的关闭连接请求
3.服务器处理完缓冲区中的数据后(做出了相应的应答),向客户端发送FIN表示关闭连接
4.客户端应答服务器的关闭连接请求
建立连接的过程是三次握手,而关闭连接通常需要4次挥手,服务器的应答和关闭连接请求通常不合并在一个段,因为有半关闭状态的情况(如上图,再者说A将需要的发送的数据发送完之后就调用close()关闭,B的缓冲区中还有数据没有处理完,还需要区应答A发送的数据,所以B的写端暂时不能关闭)。这种情况下客户端关闭连接之后就不能在发送数据给服务器端了,但是服务器端还可以发送数据给客户端,直到服务器也关闭连接为止。
TCP 状态转换
红线:客户端 绿线(虚线):服务器端 (两条线同步分析)
CLOSED:标识处于初始状态
LISTEN: 该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接
SYN_SENT:这个状态与STN_RCVD相呼应,当客户端SOCKET执行连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务器发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN请求
SYN_RCVD:该状态表示收到SYN报文,在正常情况下,这个状态时服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLSHED状态(客户端和服务器端都是在接收到ACK进入ESTABLSHED状态)
ESTABLSHED:表示连接已经建立
FIN_WAIT_1:表示当处于ESTABLSHED状态时,想主动关闭连接的一方向对方发送FIN报文,此时该soceket进入FIN_WAIT_1状态(一般很快)
FIN_WAIT_2:主动关闭的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的soceket只能接受数据,不能发
TIME_WAIT:主动关闭的一方收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。为什么要设置2MSL? 确保最后一次发送的ACK对方能够接收到。如果丢失,则重新发送ACK
CLOSING:这种状态较特殊,属于一种比较罕见的状态。正常情况下,当你发送FIN报文后,按理先收到对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却收到了对方的FIN报文。(如果双方几乎同时close的话,那么就出现双方同时发送FIN的情况,也就进入CLOSING状态)
CLOSE_WAIT:此状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来察看是否还有数据需要发送给对方,如果没有则可以close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK:该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可进入到CLOSED可用状态
补充说一下:socket本质是伪文件
半关闭
当TCP连接中主动发送FIN请求关闭的一端收到对方发送的ACK回应,并且对方没有发送FIN之前,主动关闭的一方进入FIN_WAIT_2状态(半关闭状态),主动关闭的一端只能读不能写。
使用close():说是半关闭,但不能读也不能写。这种状态不是正真的半关闭状态,所以使用shutdown()
使用close终止一个连接,但它只是减少了描述符的引用计数,并不是直接关闭连接,只有当描述符的引用计数为0时才关闭连接。shutdown不考虑描述符的应用计数,直接关闭描述符。也可以选择中止一个方向的连接,只中止读或只中止写。
从程序的角度,可以使用API来控制实现半连接状态(主动实现半关闭状态)
#include <sys/socket.h>
int shutdown(int sockfd,int how);
socket:需要关闭的socket的描述符
how:
SHUT_ED(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作
该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉
SHUT_WR(1): 关闭sockfd上的写功能,此选项将不允许socked进行写操作。
进程不能对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能
如果有多个进程共享一个套接字,close每被调用一次,计数就减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放
在多进程中如果一个进程调用了shutdown(sockfd,SHUT_RDWR),其他进程将无法进行通信,但如果一个进程调用了close(sockfd)将不会影响到其他进程
2MSL
TIME_WAIT状态的存在理由:
1.让4次握手关闭流程更加可靠,4次握手的最后一个ACK是由主动关闭方发送出去的,若这个ACK丢失,被动方会再次发送一个FIN过来,若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。
2.防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由路由器产生故障,路径无法收敛,导致一个package在路由器A,B,C之间做类似死循环的跳转。(TCP超时重传、TCP是流式的,所有包到达的顺序是不一致的,TCP通过序号来拼接,通过2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误)
为什么设计在主动关闭的一方:
1.发最后ACK的是主动关闭的一方
2.只要一方保持TIME_WAIT状态,就能避免incarnation connection在2MSL内重新建立,不需要两方都有
RFC 793中规定MSL为2分钟,实际应用中常是30秒
程序设计中的问题
做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。
现在用Ctrl-C把client也终止掉,再观察现象:
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口
端口复用
端口复用最常见的用途:
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
#include<sys/types.h>
#include<sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
点到点,端到端
路由器和交换机的区别
路由器和交换机的区别:交换机主要是实现大家通过一根网线上网,但是大家上网是分别拨号的,各自使用自己的宽带,大家各自上网没有影响,哪怕其他人在下载,对自己上网也没有影响,并且所有使用同一条交换机的电脑都是在同一个局域网内。路由器比交换机多了一个虚拟拨号功能,通过同一台路由器上网的电脑是共用一个宽带账号,大家之间上网是相互影响的,比如一台电脑在下载,那么同一个路由器上的其他电脑会很明显的感觉到网速很慢。同一台路由器上的电脑也是在一个局域网内的。
交换机工作在中继层,交换机根据MAC地址寻址,路由器工作在网络层,根据IP地址寻址,路由器可以处理TCP/IP协议,而交换机不可以。