1. 网络基础篇
1.1 网络的概述
互联网
MAC地址数据链路层,交换机通过
IP协议网络层,路由器通过IP
Ping使用ICMP,Ping应用层,ICMP是网络层
速率 带宽 吞吐量 时延
socket编程
socket编程 服务器 socket socket bind bind() listen connect() accept read read() write write() 使用现成的协议栈进行网络通信 一定用到TCP和IP accept()返回表明服务器已经完成三次握手 connect()返回表明
mac地址只能上局域网,上不了公网
3. TCP
3.1 TCP基本认识
TCP头格式有哪些?
TCP头部如图:
主要介绍以下几个部分:
-
序列号:
- 在建立连接时,由计算机生成的随机数作为其初始值
- 通过 ==SYN== 包传给接收端主机
- 每发送一次数据,就「累加」一次该「数据字节数」的大小
- 即新seq等于原seq + 报文字节数
- 序列号的作用:
- 解决网络包乱序问题
-
确认应答号:
- 指下一次「期望」收到的数据的序列号
- 发送端收到这个确认应答以后,可以认为在这个序号以前的数据都已经被正常接收
- 确认应答号作用:
- 解决丢包的问题
-
控制位:
- ACK:
- 该位为 1 时,「确认应答」的字段变为有效
- TCP 规定除了最初建立连接时的 ==SYN== 包之外,该位必须设置为 1
- RST:
- 该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接
- SYN:
- 该位为 1 时,表示希望建立连接
- 并在其「序列号」的字段进行序列号初始值的设定
- FIN:
- 该位为 1 时,表示今后不会再有数据发送,希望断开连接
- 当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段
- ACK:
为什么需要 TCP 协议? TCP 工作在哪一层?
需要TCP协议的原因:
- IP 层是「不可靠」的
- 为什么说它不可靠:
- 它不保证网络包的交付
- 不保证网络包的按序交付
- 不保证网络包中的数据的完整性
如果需要保障网络数据包的可靠性,需要:
- 由上层(传输层)的 TCP 协议来负责
TCP/IP分层模型与OSI七层模型如图:
为什么要TCP协议负责:
- TCP 是一个工作在==传输层==的==可靠数据传输==的服务
- 它能确保接收端接收的网络包是==无损坏==、==无间隔==、==非冗余==、==按序==的
什么是 TCP ?
TCP的本质:
- TCP 是==面向连接==的、==可靠==的、==基于字节流==的==传输层==通信协议
对这三个概念的理解:
- 面向连接:
- 一定是「一对一」才能连接
- 不能像 UDP 协议可以一个主机同时向多个主机发送消息
- 也就是一对多是无法做到的
- 可靠:
- 无论网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端
- 即不保证发的这个你能收到,但是丢了我会再发,直到你收到了为止
- 无论网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端
- 基于字节流:
- 用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文
- 如果接收方的程序不知道「消息的边界」,是无法读出一个有效的用户消息的
- 并且 TCP 报文是「有序的」
- 当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给==应用层==去处理
- 同时对「重复」的 TCP 报文会自动丢弃
什么是 TCP 连接?
首先我们要知道连接的定义:
- 用于保证==可靠性==和==流量控制维护==的某些==状态信息==
- 这些信息的==组合==,包括 ==Socket==、==序列号==和==窗口大小==称为连接
所以建立一个 TCP 连接需要:
- 客户端与服务器达成上述三个信息的共识
- 即:
- Socket:由 ==IP地址==和==端口号==组成
- 序列号:用来解决==乱序==问题等
- 窗口大小:用来做==流量控制==
如何唯一确定一个 TCP 连接呢?
TCP ==四元组==可以唯一的确定一个连接,四元组包括:
- 源地址
- 源端口
- 目的地址
- 目的端口
四元组字段存在哪里,作用:
- 源地址和目的地址的字段(32 位)是在 IP 头部中
- 作用是:
- 通过 ==IP协议==发送报文给对方主机
- 源端口和目的端口的字段(16 位)是在 TCP 头部中
- 作用是:
- 告诉 ==TCP协议==应该把报文发给哪个==进程==
有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?
服务端通常是:
- 固定在某个本地端口上监听,等待客户端的连接请求
由于客户端 IP 和端口是可变的,那么它能建立的TCP最大连接数就取决于:
- 客户端的==IP地址==和==端口号==的数量
其理论值计算公式为:
- ==最大TCP连接数 = 客户端的IP数 X 客户端的端口数==
- 例如对 IPv4:
- 客户端的 IP 数最多为 2 的 32 次方
- 客户端的端口数最多为 2 的 16 次方
- 也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
- 文件描述符限制:
- 每个 TCP 连接都是一个文件
- 如果文件描述符被占满了,会发生
Too many open files
- Linux 对可打开的文件描述符的数量有三个方面的限制:
- 系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;
- 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;
- 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
- 内存限制:
- 每个 TCP 连接都要占用一定内存
- 操作系统的内存是有限的
- 如果内存资源被占满后,会发生 OOM错误(“Out of Memory” 内存耗尽)。
UDP 和 TCP 有什么区别呢?分别的应用场景是?
我们先了解一下UDP:
- UDP 不提供复杂的==控制机制==
- 利用 IP 提供面向「无连接」的通信服务
- UDP 协议真的非常简单,头部只有 8 个字节(64 位)
- UDP 的头部格式如下:
- 我们来介绍一下UDP协议的头部:
- 源端口和目的端口:
- 告诉 UDP 协议应该把报文发给哪个进程
- 包长度:
- 该字段保存了 UDP ==首部==的长度跟==数据==的长度之==和==
- 校验和:
- 为了提供可靠的 UDP 首部和数据而设计
- 防止收到在网络传输中==不完整==的 UDP 包
- 源端口和目的端口:
了解了UDP后,就可以看看TCP和UDP有什么区别了:
- 连接:
- TCP:
- 面向连接的传输层协议,传输数据前先要建立连接
- UDP:
- 不需要连接,即刻传输数据
- TCP:
- 服务对象:
- TCP:
- 只能一对一
- UDP:
- 既能一对一、又能一对多、还能多对多
- TCP:
- 可靠性:
- TCP:
- 可靠交付数据的
- 什么是可靠交付数据呢:
- 数据可以==无差错==、==不丢失==、==不重复==、==按序到达==
- 什么是可靠交付数据呢:
- 可靠交付数据的
- UDP:
- 是尽最大努力交付,不保证可靠交付数据
- 如果想要可靠传输呢:
- 可以基于 UDP 传输协议实现一个可靠的传输协议,比如 ==QUIC== 协议
- TCP:
- 拥塞控制、流量控制:
- TCP:
- 有==拥塞==控制和==流量==控制机制
- 保证数据传输的==安全==性
- UDP:
- 没有这两个机制
- 即使网络非常拥堵了,也不影响 UDP 的发送速率
- TCP:
- 首部开销:
- TCP:
- 首部长度较长,会有一定的开销
- 首部在没有使用「选项」字段时是 ==20== 个字节
- 如果使用了「选项」字段还会变长
- UDP:
- 首部只有 8 个字节,并且是固定不变的
- 开销较小
- TCP:
- 传输方式
- TCP:
- 是==流式传输==,没有边界
- 但保证==顺序==和==可靠==
- UDP:
- 是==一个包一个包==的发送,是有边界的
- 但可能会==丢包==和==乱序==
- TCP:
- 分片不同
- TCP:
- 数据大小如果大于 MSS 大小,会在==传输层==进行分片
- 目标主机收到后,也同样在传输层组装 TCP 数据包
- 如果中途丢失了一个分片,只需要重传丢失的这个分片
- UDP:
- 数据大小如果大于 MTU 大小,则会在 ==IP层==进行分片
- 目标主机收到后,在 IP 层组装完数据,==再传给传输层==
- TCP:
TCP和UDP既然存在区别,那么就有会不同的应用场景:
- 由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
- FTP 文件传输
- HTTP / HTTPS
- 由于 UDP 面向无连接,它可以==随时==发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:
- 包总量较少的通信
- 如 DNS 、SNMP 等
- 视频、音频等多媒体通信
- 广播通信
- 包总量较少的通信
扩展:
- 首部长度:
为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?
- TCP 有可变长的「选项」字段
- 而 UDP 头部长度则是不会变化的
- 所以不需要多一个字段去记录 UDP 的首部长度
- 包长度:
为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?
- 先说说 TCP 是如何计算负载数据长度:
- 其中 IP 总长度 和 IP 首部长度,在 IP 首部格式是已知的
- TCP 首部长度,则是在 TCP 首部格式已知的
- 所以就可以求得 TCP 数据的长度
- 大家这时就奇怪了问:
- “UDP 也是基于 IP 层的呀,那 UDP 的数据长度也可以通过这个公式计算呀? 为何还要有「包长度」呢?”
- 这么一问,确实感觉 UDP 的「包长度」是冗余的
- 我查阅了很多资料,我觉得有两个比较靠谱的说法:
- 第一种说法:
- 因为为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍
- 如果去掉 UDP 的「包长度」字段,那 UDP 首部长度就不是 4 字节的整数倍了
- 所以我觉得这可能是为了补全 UDP 首部长度是 4 字节的整数倍,才补充了「包长度」字段
- 第二种说法:
- 如今的 UDP 协议是基于 IP 协议发展的
- 而当年可能并非如此,依赖的可能是别的不提供自身报文长度或首部长度的网络层协议
- 因此 UDP 报文首部需要有长度字段以供计算
上面的TCP数据长度不懂???
TCP 和 UDP 可以使用同一个端口吗?
答案:
- 可以
在不同层中有不同的寻址方法:
- 在数据链路层中:
- 通过 MAC 地址来寻找局域网中的==主机==
- 在网络层中:
- 通过 IP 地址来寻找网络中互连的==主机==或==路由器==
- 在传输层中:
- 需要通过端口进行寻址,来识别同一计算机中同时通信的不同==应用程序==
传输层的「端口号」的作用:
- 是为了区分同一个主机上不同应用==程序==的数据包
而一个程序可以使用两种协议:
- TCP 和 UDP
- 这两种协议在内核中是两个完全独立的软件模块
- 当主机收到数据包后
- 可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP
- 所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理
- 送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理
- 因此,TCP/UDP 各自的端口号也相互独立
- 如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突
如何理解是 TCP 面向字节流协议?
先了解一下字节流与什么有关:
- 之所以会说 TCP 是面向字节流的协议,UDP 是面向报文的协议
- 是因为操作系统对 TCP 和 UDP 协议的发送方的机制不同
- 也就是问题原因在发送方
那么分别看一下TCP和UDP,先来说说为什么 UDP 是面向报文的协议?
- 当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分
- 在组装好 UDP 头部后就交给网络层来处理
- 所以发出去的 UDP 报文中的数据部分就是完整的用户消息
- 也就是每个 UDP 报文就是一个用户消息的边界
- 这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息
- 那么如果收到了两个 UDP 报文,操作系统是怎么区分开的?
- 操作系统在收到 UDP 报文后,会将其插入到队列里
- 队列里的每一个元素就是一个 UDP 报文
- 这样当用户调用 recvfrom() 系统调用读数据的时候
- 就会从队列里取出一个数据
- 然后从内核里拷贝给用户缓冲区
- 如图所示:
再来说说为什么 TCP 是面向字节流的协议?
- 当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文
- 也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输
- 这时,接收方的程序如果不知道发送方发送的消息的长度
- 也就是不知道消息的边界时
- 是无法读出一个有效的用户消息的
- 因为用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息
举个实际的例子来说明:
- 发送方准备发送 「Hi.」和「I am Xiaolin」这两个消息
- 在发送端:
- 当我们调用 send 函数完成数据“发送”以后
- 数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中
- 至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件
- 也就是说,我们不能认为每次 send 调用发送的数据,都会作为一个整体完整地消息被发送出去
- 在发送端:
- 如果我们考虑实际网络传输过程中的各种影响:
- 假设发送端陆续调用 send 函数先后发送 「Hi.」和「I am Xiaolin」 报文
- 那么实际的发送很有可能是这几种情况:
- 第一种情况,这两个消息被分到同一个 TCP 报文,像这样:
- 第二种情况,「I am Xiaolin」的部分随 「Hi」 在一个 TCP 报文中发送出去,像这样:
- 第三种情况,「Hi.」 的一部分随 TCP 报文被发送出去,另一部分和 「I am Xiaolin」 一起随另一个 TCP 报文发送出去,像这样:
- 类似的情况还能举例很多种
- 这里主要是想说明,我们不知道 「Hi.」和 「I am Xiaolin」 这两个用户消息是如何进行 TCP 分组传输的
- 因此,一个用户消息与一个 TCP 报文不是对应的
- 正因为这样,所以 TCP 是面向字节流的协议
- 当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP ==粘包==问题
- 这时接收方不知道消息的边界的话,无法读出有效的消息
- 要解决这个问题,要交给==应用程序==
如何解决粘包?
粘包是指:
- 两个消息的部分内容被分到同一个 TCP 报文
发生粘包的原因是:
- 不知道一个用户消息的边界在哪
- 如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息
要解决粘包问题,一般有三种方式分包的方式:
- 固定长度的消息
- 特殊字符作边界
- 自定义消息结构
下面来分别解释一下这三个分包方式:
-
固定长度的消息:
- 概念:
- 每个用户消息都是固定长度
- 比如规定一个消息的长度是 64 个字节
- 当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息
- 优点:
- 这种是最简单方法
- 缺点:
- 灵活性不高,实际中很少用
- 概念:
-
特殊字符作边界:
- 概念:
- 在两个用户消息之间插入一个特殊的字符串
- 这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息
- HTTP 是一个非常好的例子
- HTTP请求报文如图:
- HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界
- 注意:
- 作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义
- 避免被接收方当作消息的边界点而解析到无效的数据
- 概念:
-
自定义消息结构:
-
自定义一个消息结构
-
由包头和数据组成
-
其中包头是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大
-
比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面
-
结构体代码如下:
struct {u_int32_t message_length;char message_data[];} message; -
当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容
-
就可以知道数据的长度
-
再继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了
-