一、再谈端口号
(一)端口号定义
端口号(Port) 标识了一个主机上进行通信的不同的应用程序 ;
在 TCP/IP 协议中 , 用 " 源 IP", " 源端口号 “, " 目的 IP”, " 目的端口号 ", " 协议号 " 这样一个五元组来标识一个通信 ( 可以通过 netstat -n 查看 );
(二)端口号范围划分
1.一共有 2^16 个端口
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的,端口号和进程是一一对应的。
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号,例如8080, 就是由操作系统从这个范围分配的。
2.认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便 , 人们约定一些常用的服务器 , 都是用以下这些固定的端口号 :
—— ssh 服务器 , 使用 22 端口
—— ftp服务器 , 使用 21 端口
—— elnet服务器 , 使用 23 端口
—— http服务器 , 使用 80 端口
—— https服务器 , 使用 443
执行此命令 , 可以看到知名端口号。
cat /etc/services
我们自己写一个程序使用端口号时, 要避开这些知名端口号。
3.端口号和进程就是K-V关系
- 一个进程是否可以 bind 多个端口号? ——可以。端口号:进程=K:V
- 一个端口号是否可以被多个进程bind? ——不可以
4.netstat
netstat是一个用来查看网络状态的重要工具!
语法 : netstat [ 选项 ]
功能 :查看网络状态
常用选项 :
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服务状态,-ntp就是只看established状态
p 显示建立相关链接的PID和程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
通常使用:netstat -nltp
(1)示例1: n 拒绝显示别名,能显示数字的全部转化成数字。
(2)示例2: l 仅列出有在 Listen (监听) 的服务状态。
(3)示例3: p 显示建立相关链接的程序名。
5.pidof
在查看服务器的进程 id 时非常方便 .
语法 : pidof [ 进程名 ]
**功能 :**通过进程名 , 查看进程 id
UDP 协议
UDP 协议端格式
二、UDP协议
(一)UDP协议端格式
1.UDP协议结构
前8个字节是 UDP报头;剩下是报文!
16位源端口号:自己的进程的端口号
16位目的端口号:要访问的目标进程的端口号
16位UDP长度:表示整个数据报(UDP首部+UDP数据)的最大长度;就是为了让UDP成为数据报式,而不是流式的。
数据报:报文和报文之间有明显边界,因为有16位UDP长度。
把一个一个的报文通过UDP长度分开一次接收一个。tcp字节流就需要一直等到读取长度到达encode报头长度才能进行。
16位UDP检验和:如果校验和出错,说明数据有问题,就会直接丢弃,暂时不考虑这个。
2.报头就是带有位段的结构体
3.添加报头的本质,其实就是拷贝对象
把对象的成员变量填好,通过sizeof(udp_header)的大小拷贝进二进制,再强转成udp_header(udp_hdr)类型。
(二)UDP的特点
UDP传输的过程类似于寄信.
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
不可靠: (不可靠是特点不是缺点,中性词)没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;(不可靠可理解为:传输过程UDP报文丢失就真的丢失了。优点是:不需要为可靠性做更多的工作——>代码简单,维护简单,效率高等)
面向数据报: 不能够灵活的控制读写数据的次数和数量;
经典UDP的使用场景:直播
(三)面向数据报
数据报:报文和报文之间有明显边界,因为有16位UDP长度。
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
举例:用UDP传输100个字节的数据:
如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;
(四)UDP的缓冲区
UDP没有真正意义上的发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致(即:UDP报文大概率乱序,因为UDP不保证可靠性); 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
UDP的socket既能读,也能写,因为UDP写和读数据的路径是两个,这个概念叫做全双工。
(五)UDP使用注意事项
我们注意到, UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).
然而64K在当今的互联网环境下, 是一个非常小的数字.
如果我们需要传输的数据超过64K,就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;这是没办法的。
(六)基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议 ( 用于无盘设备启动 )
DNS: 域名解析协议
当然 , 也包括你自己写 UDP 程序时自定义的应用层协议
三、TCP协议
(一)TCP定义
TCP全称为“传输控制协议 (Transmission Control Protocol)”,人如其名,要对数据的传输进行一个详细的控制。
(二)TCP协议段格式
1.如何封装和解包:
封装:封装和UDP一样,定义一个 TCP_header 的对象,把报头对象的各个值填上,再把这个对象拷贝到报文的前面即可;
解包:用先读取前20字节,读取其中的 4位首部长度, 4位首部长度*4 - 20 = 选项的大小
2.如何分用
通过“16位目的端口号” 实现向上应用层交付实现分用。
3.详细介绍TCP协议段格式
TCP报文包含报头和有效载荷。TCP报头包括前2字节和选项。报头是变长的:前20字节是标准长度,选项是变长的。
源/目的端口号: 表示数据是从哪个进程来 , 到哪个进程去 ;
32位序号/32位确认号 :序号标定一个报文的编号;确认号标定 该确定号之前的报文全部收到 ,保证双向的全双工的确认应答机制。
4位TCP报头长度(4位首部长度 ): 表示该 TCP头部有多少个32位bit(单位是4字节);此值是4bit位,则取值范围是0000-1111,即0~15,因为单位是4字节,所以TCP头部最大长度是15 * 4 = 60;
因为标准长度有20,所以4位首部长度最少是 20 / 4字节 = 5=0101,即4位首部长度范围是0101-1111,20-60字节,则选项的大小就是0-40字节。
6位标志位。
4.报文也是有类别的
16 位窗口大小: 应答本质就是要包含TCP报头,tcp报头可以有保存server接受能力的属性字段,叫做 16位窗口大小 详解见下面 5.—>(2)
16位校验和 : 发送端填充 , CRC 校验 . 接收端校验不通过 , 则认为数据有问题 . 此处的检验和不光包含 TCP 首部 , 也包含TCP 数据部分.(16位检验和是为了验证数据在传输过程有没有问题,有问题就丢掉。暂时不考虑这个属性)
16 位紧急指针 : 标识哪部分数据是紧急数据 ;
40 字节头部选项 : 暂时忽略 ;
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从 TCP缓冲区把数据读走
RST: 对方要求重新建立连接 ; 我们把携带 RST 标识的称为 复位报文段
SYN: 请求建立连接 ; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方 , 本端要关闭了 , 我们称携带 FIN 标识的为 结束报文段
SYN:同步标记位,只要报文是建立链接的请求,SYN需要被设置为1,证明是链接请求的报文(sync的前三个字母)
FIN:该报文是一个断开链接的请求报文(finish)
ACK:确认标记位,设置为1表示该报文不仅仅是对历史报文的确认,同时也可以携带要发送的数据,一般在大部分正式通信的情况下ACK都是1。
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
解释:之前我们是通过阻塞式调用read函数来读取数据,现在是通知以后再调用read,后者效率更高。
示例说明:假设接收缓冲区大小是100字节,超过了20字节会向上应用层通知来read,20叫接收数据的低水位线。当接收缓冲区内数据超过了20字节,PSH会让服务器操作系统通知上层已有数据,快来read。
URG:紧急指针标记位。作用:该报文忽略序号,被上层直接读取处理。(该报文叫紧急指针报文)详情见三—>2—>(3)
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。
若最后的ACK丢失,则客户端不知道仍认为建立好连接了,则会继续发消息;服务器在未收到ACK时也意识到了这种问题,为了避免这种情况,服务器在未收到ACK时会连接重置,重新发送SYN+ACK应答,同时也把RST置为1!
5.32位序号保证按序到达——URG
(1)32位序号保证按序到达到达:
报文在发送的时候,是可能乱序到达的。这是不可靠的一种。
需要让我们的报文进行按序到达,如何做到?
——32位序号保证按序到达。为报文标序号,接收缓冲区收到后排序即可。
(2)URG紧急指针标记位与紧急指针:
如果数据是必须在TCP中进行按序到达的话,也就是说如果有一些数据优先级更高,但是序号较晚,无法做到数据被有限紧急处理!
解决方案:使用 URG紧急指针标记位, URG标记为1,将优先级更高的报文忽略序号,被上层直接读取处理。(该报文叫紧急指针报文)
16位紧急指针:指向紧急指针报文的偏移量。紧急指针报文只能有一个字节!
(3)URG应用场景——用来 获取服务器/主机状态
客户端向服务器发数据,服务器可能因某种原因(例如内存不足)而无法接收消息或接收消息缓慢,这种情况下客户端想获知服务器的状态,就发送报文 URG紧急指针标记位 标为1,此时server在非常困难的情况下依旧优先接收了指向的一字节,在服务器内部通过已设置的某种逻辑得出一个状态码20(假设),通过 URG紧急指针标记位 把指向的一字节返回给客户端,客户端收到20状态码得知服务器无法正常工作原因是“内存不足”,再通知工作人员即可。
6.可靠性问题
(1)什么是不可靠:丢包,乱序,校验失败。
(2)怎么确认一个报文是丢了还是没丢? ?(确认应答机制)
答:我们如果收到了应答,我们确认是没丢,否则就是不确定。可靠性是指我们发出去的报文得到了对方应答是可靠的,没被应答的不能保证可靠。
详细解释:我们发出去的消息,我们如何得知对方是否收到?——只要得到对方的应答就意味着我刚刚发的消息对方100%收到了!
在长距离交互的时候,永远有一条最新的数据是没有应答的,所以没有百分之百可靠的协议!!但是可以做到局部百分之百可靠。
但是:只要发送的消息有对应的应答,我们就认为我们发送的消息,对方是收到的! !
确认应答机制保证了数据能被对方收到的问题。
7.确认应答(ACK)机制
(1)32位序号/32位确认号
建立一个共识: tcp进行通信的时候,发送出去的报文一定会携带tcp报头!
解释下图:客户端发送一个有效载荷时一定是携带tcp报头的,并且数据都已经填好。假设随机生成的起始序号是1, 客户端发给服务器的整个报头+报文大小1000字节,则他占了1~1000序号,则32位序号就是1。服务器收到后为了应答客户端,则他会回复32位确认号是1001,它表示接收方已经收到了字节序号为 [1, 1000] 的数据,现在期望你发送字节序号为 1001 以及以后的数据。
32位序号/32位确认号:序号标定一个报文的编号;确认号标定 该确定号之前的报文全部收到,之后的报文从该确定好下一个序号开始发 ,保证双向的全双工的确认应答机制。
例:服务器收到6个报文,32位序号分别是1,2,3,5,6,7,响应时返回给客户端的确认序号应该填4。解释:确认号是为了告诉客户端特定序号之前的报文全部收到,因为这里没收到4号报文,所以填4是告诉客户端4之前的报文全部收到,4没收到,客户端要从4号开始重新发。
(2)为什么TCP报头有32位序号和32位确认号两组序号?常考
因为TCP协议是全双工的,我在给你发消息的同时,我也可以收消息。
解释:如果服务器想给你应答(填充确认序号),并且同时给你发消息(携带自己的序号)呢?——客户端给服务器发送“你吃饭了吗?”(填充32位序号),服务器给客户端发送应答“我吃饭了”(填充32位确认序号),同时发出“那你吃饭了吗?”(填充自己的32位序号)
客户端用序号和对方的确认序号保证了从左向右的可靠性,服务器用自己的序号和对方的确认序号保证了从右向左的可靠性。这样保证了双向的全双工的确认应答机制。
(3)TCP 将每个字节的数据都进行了编号,即为序列号。
每一个 ACK 都带有对应的确认序列号 , 意思是告诉发送者 , 我已经收到了哪些数据 ; 下一次你从哪里开始发 .
8.tcp的发送和接收缓冲区
(1)tcp有发送和接收缓冲区
IO类函数.本质其实都是拷贝函数
我们用write/send函数把数据拷贝到内核缓冲区后,tcp会在合适时候进行发送。内核缓冲区的数据什么时候发,发多少,出错了怎么办,要不要添加提高效率的策略——是由OS内的TCP自主决定的,所以TCP叫做传输控制协议!
上图可知,读写缓冲区用一个文件描述符业互不影响,可以说明TCP通信是全双工的!
(2)16位窗口大小——流量控制策略
在客户端向服务器发送数据包时,如果发送的太快了,导致server来不及接收怎么办?
——需要让client知道server的接受能力!
1.哪一个指标表示 server的接受能力呢? ——接收缓冲区剩余空间的大小!
2. client怎么知道? ? ——发送都会有应答!应答本质就是要包含TCP报头,tcp报头可以有保存server接收能力的属性字段,叫做16位窗口大小。
3.server给client发报文,填充的窗口大小,填充的是自己的接收缓冲区剩余空间大小。
4. client——>server, server——> client,两个方向都是一样的
总结:双方的发送缓冲区基于得知对方接收缓冲区接收能力的条件下进行数据通信,根据对方每次应答中不断更新的窗口大小定期向对方发送合适的数据大小,这种策略就叫做流量控制策略。流量控制策略是双向的!
5.这个流量控制策略通过16位窗口大小保存接收能力的属性字段,那第一次客户端向服务器发数据时怎么得知服务器的接收能力呢?
三次握手期间不光建立链接,还有就是交换信息 信息就包含了向对端告知自己的接收能力。
(3)tcp正文长度由应用层决定
tcp流式服务,所以没必要考虑正文长度,正文长度是需要应用层自己定协议的,比如我们写的encode和decode对整个序列化之后的字符串 9\r\n100 + 200\r\n 进行提取长度!
四、连接管理机制
(一)四次挥手
在正常情况下,TCP 要经过三次握手建立连接,四次挥手断开连接。
accept和三次握手没关系,只是把已经完成的三次握手提上来。
四次挥手:一方向另一方发送FIN断开连接请求,对方ACK应答,对方同理,一个close对应一对挥手,共4次挥手。(特殊情况三次挥手:客户端向服务器发送FIN断开连接请求,对方ACK应答并且正好FIN置1也想断开,客户端在发送第三次ACK,这就是三次)。
(二)四次挥手状态(包括握手)
1.服务端状态转化:
[CLOSED -> LISTEN] 服务器端调用 listen 后进入 LISTEN 状态 , 等待客户端连接 ;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求 ( 同步报文段 ), 就将该连接放入内核等待队列中 , 并向客户端发送SYN 确认报文 .
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文 , 就进入 ESTABLISHED 状态 , 可以进行读写数据了.
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接 (调用close), 服务器会收到结束报文段, 服务器返回确认报文段ACK并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接。
2.客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用 connect, 发送同步报文段 ;
[SYN_SENT -> ESTABLISHED] connect 调用成功 , 则进入 ESTABLISHED 状态 , 开始读写数据 ;
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入
FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态。
(1)CLOSE_WAIT 半关闭状态
无论是否accept,客户端只要close关闭,但服务器未close关闭,服务器一方就一直是 CLOSE_WAIT 状态(一方close,另一方未close,即半关闭状态):
(2)TIME _WAIT
连接保持的状态下直接关闭服务器,服务器就成了主动关闭的一方,主动关闭的一方最终就会进入 TIME _WAIT(4次挥手已完成),设置一段时间,过完这一段时间连接就会自动关闭。
设置TIME _WAIT一段时间的意义:
①保证最后一个ACK尽可能传输成功:若在这个设置合理的时间段内,客户端没有收到服务器的重传FIN,则认为服务器收到了自己的ACK应答;若在这个设置合理的时间段内,客户端收到服务器的重传FIN,则认为刚刚传出的ACK确认应答丢失了,就需要重传ACK;
②这一段时间还可以保证让历史发送的数据在网络中消散,即尽可能被对方收到。
MSL:最大传输时间,来回就是2个MSL
TCP 协议规定 , 主动关闭连接的一方要处于 TIME_ WAIT 状态 , 等待两个 MSL(maximum segment lifetime) 的时间后才能回到CLOSED 状态 .
我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方 , 在 TIME_WAIT 期间仍然不能再次监听同样的server 端口 ;
MSL 在 RFC1122 中规定为两分钟 , 但是各操作系统的实现不同 , 在 Centos7 上默认配置的值是 60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值 ;
为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
解决TIME_WAIT状态引起的bind失败的方法(作业)
平时我们8080断开后bind不成功就换8081,原因就是:客户端主动断开连接后进入TIME_WAIT状态,TIME_WAIT状态期间仍会保持一个名存实亡的不会再被使用的连接,但这个连接依旧持有客户端的IP和端口,其他进程想绑定就无法绑定,这是操作系统的默认行为
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的:
假设:淘宝狂欢节,服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
这个时候如果由服务器端崩溃主动关闭连接, 就会产生大量TIME_WAIT连接.
由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议)。其中服务器的ip和端口和协议是固定的. 如果新来的客户端请求的ip和端口号就是已经TIME_WAIT服务器ip和端口号,就会失败.
函数 setsockopt 崩溃时可以重启
用 setsockopt() 设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但IP地址不同的多个 socket描述符。
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
sockfd:被设置的套接字文件描述符。level:设置 SOL_SOCKET 层。optname:设置的名字,一般用SO_REUSEADDR。optval:值 optlen:长度
(三)listen的第2个参数——backlog
list的第二个参数叫backlog,叫做底层的全连接队列的长度,算法是: n+1, (n是用户传入的值)表示在不accept的情况你最多能够维护多少个链接。例如传2,就表示在不accept的情况最多能够维护3个链接,剩下的客户端请求的连接以半连接保持,当有全连接退出时,这个半连接才会变成全连接。
全连接维护的意义!
1.为什么需要排队?
可以让我们的服务器在有闲置的情况下,通过accept从底层拿去链接,进行链接处理!相当于是池化技术,形成了一个“链接池(缓冲池)”。文章来源:https://www.toymoban.com/news/detail-542173.html
2.队列为什么不能太长?
①太长影响客户体验
②太长过于占用系统资源,反而可能导致服务器效率低下。不如把维护长链文章来源地址https://www.toymoban.com/news/detail-542173.html
到了这里,关于二十五、传输层协议(上)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!