目录
再谈端口号
曾经的理解:端口号 (Port) 标识了一个主机上进行通信的不同的应用程序。
如今,TCP/IP协议,使用“源IP” ,"目的IP",“源端口号”,“目的端口号”,“协议号”这五个组来标记一个网络服务。
如下,就是体现在同台机器上,web浏览器打开多个页面,端口号不同;不同机器,源IP不同的例子:
端口号在网络中是用来标识一台机器中的一个特定通信的。问?一个进程是否能绑定多个端口号?
端口号的返回划分
0 - 1023: 知名端口号 , HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。因此我们需要 避开这些端口号
例如:
- ssh服务器, 使用22端口
- ftp服务器, 使用21端口
- telnet服务器, 使用23端口
- http服务器, 使用80端口
- https服务器, 使用443
详细请输入查看:
cat /etc/services
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的(我们网上买的服务器大多默认不开放端口号,需要我们手动打开)
netstat(重要)
是一个用来查看网络状态的重要工具 .
语法 : netstat [ 选项 ]
功能 :查看网络状态
常用选项 :
- n 拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服务状态
- p 显示建立相关链接的程序名
- t (tcp)仅显示tcp相关选项
- u (udp)仅显示udp相关选项
- a (all)显示所有选项,默认不显示LISTEN相关
sudo 提升权限后,我们可以详细看到服务的进程名。
pidof
在查看服务器的进程id时非常方便.
语法:pidof [进程名]
功能:通过进程名, 查看进程id
一,UDP协议
UDP协议格式
- 16位UDP长度, 表示整个数据报(UDP头部+UDP数据)的最大长度(报头固定长度,非常有利于分离报头);
- 如果校验和出错, 就会直接丢弃。
答:向下交付:我们只需上层协议发送的数据,字符串拼接形成报头。
向上交付:提取目的端口号,然后从8字节开始提取有效载荷,最后操作系统完成信息分配。
理解tcp/udp报文
本质上是一个结构体(位段) ,当需要获取报文里面的数据时,只需要进行数据强转为udp结构体类型,就能正确的读取每个数据。
UDP的特点
UDP传输的过程类似于寄信.
UDP的缓冲区
UDP没有真正意义上的 发送缓冲区。调用 sendto会直接交给内核, 由 内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区 不能保证收到的UDP报的顺序和发送UDP报的 顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;
UDP 的 socket 既能读 , 也能写 , 这个概念叫做 全双工 。
怎么理解全双工,半双工呢?
全双工,就是既能发送信息,同时又能接收信息,两者不冲突。半双工发送与接收信息两者互斥则不允许。
UDP 使用注意事项
我们注意到 , UDP 协议首部中有一个 16位的最大长度 . 也就是说一个 UDP 能传输的数据最大长度是 64K(包含UDP 首部)。然而64K 在当今的互联网环境下 , 是一个非常小的数字 .
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包 , 多次发送, 并在接收端手动拼装 ;
基于 UDP 的应用层协议
- NFS: 网络文件系统
- TFTP: 简单文件传输协议
- DHCP: 动态主机配置协议—(连wifi, 或者路由器时,在路由器中会启动这个程序,连接成功将会为我们自动分配一个IP,断开回收)
- BOOTP: 启动协议(用于无盘设备启动)
- DNS: 域名解析协议
当然 , 也包括你自己写 UDP 程序时自定义的应用层协议。
二,TCP协议(传输控制协议)
协议格式
tcp协议的实现向上,向下交付
a. 序号与确认序号
理解tcp协议的可靠性
我们知道UDP协议不关心,数据报文是否被收到,可靠性不高。反过来tcp协议具有可靠性,需要关心数据是否被收到,因此某端在成功接收到数据报后,需要通过一种方法返回已收到信息。
先由下图,逐步引出对tcp的理解:
序号与确认序号的5层理解:
1. 将请求与应答一一对应。
2. 确认序号的含义:表示前面的数据已经安全接收。如果C端接收到了3001的确认序号,则代表3000,2000,1000已经完整接收。
3. 允许部分确认丢失,或者不应答。假设C端收到3001,前面的完整接收,所以C端不会选择重发;如果C端传递时丢失2000,当S端收到3000,1000而没有2000时,3001将不会被设置,而1001将会被设置(只收到1001),这就是不应答。
4. 为什么要用两个序号,而不是一个数字?(小面试) 任何一方通信都是全双攻的,各自也需要保证各自的信息是否传达成功,接收方也会携带自身的数据。
5. 保证数据按序到达。乱序是一种不可靠,提取数据是按照顺序来的,而有了序号,OS就会对资源进行排序,保证数据按序到达。
两序号作用:保证了确认应答,和数据排序。
b. 16位窗口大小
本质上是一种流量控制,在上一小章中,我们知道UDP没有发送端缓冲区,接收端有缓冲区,那tcp是否有缓冲区呢?
答:有接收,发送缓冲区。数据在应用层,进行send,recv后数据将首先存放在系统中的发送,接收缓冲区。
那如何让发送方知道,接收方缓冲区满了?
c. 6个标记位
每个报文都有其自身所携带的信息,他们会被分成6类,我们可以将6个标记位理解为报文类型。
6 位标志位(报文类型) :
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段。
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段。
- ACK: 具有应答特征的报文。一般ACK都会被设置为1,除了第一次请求。
- URG: 设置为1时,表示紧急指针有效,后面讲紧急指针会详细讲。
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走(接收端缓冲区满时,发送端可能会发送PSH报文)。
- RST: 对方要求重新建立连接; 比如三次握手出现异常,就会向对方发送RST的重新建立连接。
关于6个标记位的认识,我们通过tcp连接管理机制来学习吧(三次握手,四次挥手)
连接管理机制
1.三次握手
首先我们需要理解:服务端管理相应的连接是需要资源的(内存 + cpu)
服务端对大量客户端连接的管理,必然是先描述,后管理。一个客户端的连接,服务器必然会为其创建合适的数据类型,然后再用数据结构进行管理。
示意图:
上图示意:第一次客户端向服务端发送SYN被设置的完整报头,服务端也会发送SYN+ACK被设置的报头,并且会进入SYN_RCVD的状态,最后等待客户端发送来的ACK报头。
理解:为什么是三次握手,而不是一次,2次或者是4次?
答:
2.四次挥手
为什么Time_wait状态设置为2MSL?
- MSL是TCP报文的最大生存时间(一般设置为60s),因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时,也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN,重发到接收一般会少于2MSL。 这时虽然客户端进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
有那些情况下,TIME_WIAT会导致bind失败?
而为了解决上面场景,编写套接字时使用setsockopt()设置属性 选项SO_REUSEADDR为1, 表示允许创建端口号相同,但IP地址不同的多个socket描述符。
d. 紧急指针
功能:URG被设置的报文,TCP会优先提取紧急指针(偏移量)所标示的数据范围,优先提交应用层。
e. 策略
1. 确认应答(ACK)机制
TCP协议中会对应用层载入的数据,进行一个字节一个字节的标记,说白就是char数组下标+1,这也是上面说过的序列号。
功能:每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
2. 超时重传机制
就是发送端从发送数据后开始计时,等到达一定时间后,未收到ACK则重新发送。
情况一:ACK丢失
主机B的ACK丢失,但主机B已经缓存了数据,主机A在下一个ACK接收时超过特定时间,主机A会触发超时重传,重新发送,
这时,主机B就有可能会收到大量的重复数据(不过主机B会去重),接收到后返回ACK。这就是一次超时重传。
网络情况比较复杂,在不同的网络环境中,超时时间的设置也不同,那如何动态设置超时时间?
答:Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍,第二次重发2 * 500ms,第三次重发3* 500ms,当积累到一定次数后,TCP会认为网络或者主机出现问题,强制关闭连接。
3. 滑动窗口
从上面的确认应答机制,我们可以发现一些问题,发送端发送一个报文,会等待ACK返回,然后再向接收端发送下一个内容。效率太低了,那有什么方法来提高效率?
如下,该窗口大小为4000字节
理解:
- 在窗口大小内,可以不接收ACK,连续发送多段数据报文,在确认窗口内的数据都到达后,向后移动,移动的大小为win_start + 对方的接收能力。
- 操作系统需要维护该滑动窗口,需要记录窗口中的数据是否收到应答,收到应答后才将发送数据删除。
- 窗口越大,网络吞吐量越大。
可能存在的异常情况:
情况一:数据包接收,ACK丢失
这种情况一般不会有影响,因为确认序号是当前序号的前面序号已经全部接收,因此下一个ACK将会确认。
情况二:数据包丢失
主机B在接收主机A连续发送的多个数据报中,首先将所收到的数据载入接收缓冲区中,排序检查发现缺失1001~2000,因此后面所返回的ACK的确认序号会全部设置为1001,当主机A收到连续3个同样的ACK确认序号为1001,这就会触发重发机制,滑动窗口会定位该数据段,重新发送。
这种机制被称为:“高速重发机制”(也叫快重传),但这要求主机A连续收到3次相同的ACK。
如何理解同时存在快重传与超时重传?
- 快重传比超时重传速度快,效率高,但有前提。
- 快重传与超时重传之间是相辅相成的,
比如下面情况:
4. 流量控制
接收端处理数据的速度是有限的。 如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引起丢包重传等等一系列连锁反应 .
因此 TCP 支持根据接收端的处理能力 , 来决定发送端的发送速度 .。 这个机制就叫做 流量控制 (Flow Control) ;
5. 网络拥塞控制
当我们发现少量丢包时,我们可能会触发超时重传(或者快重传);但如果遇到大量丢包呢?这就是网络拥塞了,TCP协议则会立即停止重传。
因为网络上有很多的计算机 , 可能当前的网络状态就已经比较拥堵。 在不清楚当前网络状态下 , 贸然发送大量的数据, 是很有可能引起雪上加霜的。
TCP 引入 慢启动 机制 , 先发少量的数据 , 探探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据 ;
意义:当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降; 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
6. 延迟应答
当接收方接收到发送的信息后,并不立即发送ACK,而是等待200s让应用层将数据取走,如果立即发送ACK下次窗口大小为500K, 而等待后下次窗口大小则为1mb,这样通过延迟应答机制,窗口变大,吞吐量增大,吞吐量增大,传输效率提高。
那么所有的包 都可以延迟应答 么 ? 肯定也不是
7. 捎带应答
在延迟应答的基础上 , 我们发现 , 很多情况下 , 客户端服务器在应用层也是 " 一发一收 " 的 . 意味着客户端给服务器说 了 "How are you", 服务器也会给客户端回一个 "Fine, thank you"; 那么这个时候ACK 就可以 搭顺风车 , 和服务器回应的 数据 一起回给客户端。
f. tcp机制小结
tcp传输控制协议之所以这么复杂,是既保证了可靠性,又尽可能的提高了效率。
保证可靠传输:
- 连接管理机制
- 数据序列(保证数据有序)
- 检验和
- 确认应答机制
- 超时重传
- 流量控制(保证数据不异常丢包)
- 网络拥塞控制
保证效率:
- 滑动窗口
- 快重传(不用等待发送方触发超时重传)
- 延迟应答
- 捎带应答
g.面向字节流
创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区
- 应用层通过write写入发送缓冲区;
- 接收方接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,然后通过read从缓冲区中提取数据。
由于缓冲区的存在 , TCP 程序的读和写不需要一一匹配(指的是发送方的发送次数,与提取次数不相同,区别与UDP)
h.黏包问题
我们知道TCP协议接收方从接收缓冲区中读取数据时,数据包之间是不像UDP那样一个一个地读取,而是一段一段的读取,有可能到的内容存在1.5个数据包。问题是我们怎么解决这个数据包之间的分界问题?
答:这个是应用层的事情,TCP只负责数据在将数据发出,收到的数据拷贝到接收缓冲区中,数据包分界tcp不关心。
应用层常见的区分法:
1. 定长数据包,由于TCP没有区分数据包,通过read()读取相应的数据包,在应用层提前声明数据包的长度。
2. 分割符,比如设置"\r\n****\r\n"来区分数据包。
3. 自定义描述 + 分割符, 比如我们曾经的“length\r\n*****\r\n”来动态分割数据包
I: TCP异常情况
- 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别。
- 机器重启: 和进程终止的情况相同。
- 电脑断电或者拔网线:接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放。
对于一些长连接的服务,例如QQ聊天,客户端如果检测到长时间未进行数据发送,服务端的资源任然占用着,则会在客户察觉不到的情况下关闭连接,在客户需要发送时,会再次连接。
k. listen()的第二个参数
backlog:指定在拒绝新连接之前,操作系统可以排队等待的最大已连接数量。
Linux 内核协议栈为一个 tcp 连接管理使用两个队列 :
- 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求信息,一段时间后会被释放)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
一旦新的连接请求时,由于已经超过了等待的最大连接数, 因此服务器,不会发出SYN + ACK(不同意连接),并将此次连接信息载入半连接队列(未连接成功的),在netstat上显示此次连接为 SYN_RECV 状态, 而不是 ESTABLISHED 状态。
而全连接队列的长度会受到 listen 第二个参数的影响,最大等待数量 = listen 的第二个参数 + 1。
结语
本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获,请动动你发财的小手点个免费的赞,你的点赞和关注永远是博主创作的动力源。