文章目录
📖 前言
在我们刚学习网络时,我们知道网络是层状的结构,前面所有的内容都是围绕着应用层展开的。
接着我们来学习操作系统内部最顶层协议,叫做传输层协议。
应用层用应用的功能都是传输层提供的接口。
例如,用http
通信,http
协议底层用的是tcp
协议。
https
在密钥协商之前,首先要将客户端和服务端的连接建立好,所以在应用层的理解之下,还要搞一下传输层协议。
传输层最典型协议有两种,一个udp,以个tcp,对应的就是曾经学到的udp套接字和tcp套接字。
👉 网络层状结构复习
1. 再谈端口号
端口号(Port)标识了一个主机上进行通信的不同的应用程序。
在写套接字的时候,无论udp
还是tcp
,在服务器启动时都必须bind
,得明确服务端的端口号,一旦保存端口号,底层在收到报文时,操作系统在收到数据,会根据对应的报文来进行数据处理,根据端口号将数据推送到特定的服务中。
这四个概念非常重要:在TCP / IP
协议中,用 “源IP”,“源端口号”,“目的IP”,“目的端口号”。
端口号的位数是16位端口号:
- 为什么在应用层写套接字时,对应的端口号是16位呢?
-
- 因为
tcp
或udp
它的报头当中,端口号的字段就是16位的。
- 因为
-
- 也就是说在内核里面端口号就是16位。
- 所以在应用层使用系统接口时,传入的端口号也只需要16位就够了。
1.1 端口号划分范围:
端口号和服务是一一对应的。
- 0 ~ 1023: 知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的。
- 1024 ~ 65535: 操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的。
有些服务器是非常常用的,为了使用方便,约定一些常用的服务器,都是用以下这些固定的端口号:
- ssh 服务器,使用22端口。
- ftp 服务器,使用21端口。
- telnet 服务器,使用23端口。
- http 服务器,使用80端口。
- https 服务器,使用443。
查看知名端口号:
cat /etc/services
所以在自己写一个程序使用端口号时,要避开这些知名端口号,避免不必要的麻烦。
这些端口是被特定的服务所采用的:
是因为有这个后端的守护进程sshd
,一直在后端运行,所以在本地打开X-shelI
时,连接的就是这个服务,由这个服务来帮助我们登录Linux。
1.2 端口和进程的关系:
端口号是用来标定进程唯一性的。
一个进程是否可以bind
多个端口号?一个端口号是否可以被多个进程bind
?
- 这是一个经典的问题,因为端口号是用来标定进程唯一性的。
- 所以一个端口号只能对应一个进程,而一个进程是可以对应多个端口号的!
一个进程可以同时监听和处理多个端口号上的网络数据。
1.2 - 1 netstat
netstat是一个用来查看网络状态的重要工具。
netstat
是一个用于显示网络连接、路由表和网络接口等信息的常用命令。它可以在命令行界面下运行,并提供了多种选项来获取特定类型的网络统计数据。
查看网络状态:
netstat [选项]
常用选项:
- -n:拒绝显示别名,能显示数字的全部转化成数字
- -l:仅列出有在 Listen (监听) 的服务状态
- -p:显示建立相关链接的程序名
- -t:(tcp)仅显示tcp相关选项
- -u:(udp)仅显示udp相关选项
- -a:(all)显示所有选项,默认不显示LISTEN相关
1.2 - 2 pidof
在查看服务器的进程id时非常方便。
pidof
是一个用于查找指定进程名称所对应的进程ID的命令。在 Linux系统中,可以使用这个命令来获取特定进程的PID(进程 ID),以便进行后续处理。
通过进程名,查看进程id:
pidof [进程名]
常用选项:
- -s:只返回一个进程 ID(如果有多个匹配结果)。
- -x:同时匹配进程名和命令行参数,而不仅仅是进程名。
- -o:仅返回指定用户运行的进程 ID。
- -c:只返回匹配的进程数量,而不返回进程 ID。
1.3 源端口和目的端口:
- 源端口(Source Port)是指发送数据包的应用程序或进程所使用的端口号:
-
- 在计算机通信中,数据从源端发送到目的端时,需要通过网络传输,源端口用于标识发送方应用程序或进程。
- 目的端口(Destination Port)是指接收数据包的应用程序或进程所使用的端口号:
-
- 当数据包到达目的地后,通过目的端口将数据包传递给相应的应用程序或进程。
源端口和目的端口组合在一起形成一个套接字(Socket),在网络中充当数据交换的门户。通过源端口和目的端口的配对,可以确保数据包被正确地发送和接收,并且能够正确地路由到目的地。
在传输层协议中,如TCP(传输控制协议)和UDP(用户数据报协议),源端口和目的端口信息包含在数据包的首部中,用于标识通信双方的应用程序或进程。通过源端口和目的端口的匹配,网络设备可以将接收到的数据包正确地路由和交付给相应的应用程序或进程。
2. UDP协议
我们之前用UDP协议写过一个简单的聊天程序:👉 UDP的服务端 + 客户端。
- 我们再来回顾一下UDP的一组概念:
未来学习的所有协议必须考虑两个共性问题:
- 如何封装和解包?
- 如何分用?
在我们之前网络基础中讲到过封装解包和分用
的过程,👉 复习传送门。
我们以UDP为例:
- 封装:
-
- udp当上层应用调用sendto,要发送信息。
-
- 在传输层添加udp报头时,就是给原始数据前面再写上8字节数据就可以了(报头一加)。
-
- udp采用的是定长报文。
- 解包:
-
- 收到消息之后,只需要提取前8个字书就是报头。
-
- 剩下的就是报文的有效载荷了。
- 分用:
-
- udp上面还有应用服务,那么udp收到报文之后,它如何知道要将报文转发给上层的哪一个应用呢?
-
- 源端口和目的端口!!
UDP的特点:
UDP传输的过程类似于寄信。
- 无连接: 知道对放的ip地址和端口号就能直接进行传输,不需要建立连接。
- 不可靠: 没有确认机制,没有重传机制,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
- 面向数据报: 不能够灵活的控制读写数据的次数和数量。
udp一定不会是为了可靠性而做过多的设计!
udp不可靠,报文丢了就是真的丢了。丟包了不重要,因为允许丢包。
面向数据报:
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。
- udp要么不收报文,要么收到的就是一个完整的报文。
- 因为有udp长度和校验和保证。
- 报文有自己的报头长度和总长度。
- 当收到了一批二进制,可以正常的去提取一个一个的完整的udp报文。
- 面相数据报的通信方式有点像寄信,寄了十封信,就能够收到十封信,信和信用信封分开来。
假设用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节,而不能循环调用10次recvfrom,每次接收10个字节。
2.1 UDP协议格式:
UDP(User Datagram Protocol)的报头长度固定为8个字节。
网络协议栈的tcp/ip
协议,是内核中实现的,内核是用C语言实现的,报头本质上也是一个对象。
假设报头代码是如下(并非源码实现):
struct udp_hdr
{
unsigned int src_port : 16;
unsigned int dst_port : 16;
unsigned int udp_len : 16;
unsigned int udp_check : 16;
};
udp报头类型C语言中称之为位段。
添加报头的本质,其实就是拷贝对象!
这些字段全都是位段,给数据添加报头就是把对应的位段的对象一定义,属性一填,然后把对象拷贝到属性的前面,最后就形成一个报文。
16位UDP长度是UDP报文整体的长度(单位Byte):
- 16位UDP长度,表示整个数据报(UDP首部 + UDP数据)的最大长度。
- 16位UDP检验和,主要是为了防止报文当中有些字段出现一些偏差,需要进行校验。
- 如果校验和出错,就会直接丢弃!!
UDP叫做面向数据报,体现在它不是流式的,报文和报文有明显边界。
16位UDP检验和:
- 如果UDP校验和失败了,说明数据是有问题的。
- 如果校验和出错,就会直接丢弃报文数据。
3. 再谈write/read
之前我们学习过操作系统的文件操作的write/read
接口,我们在使用者的身份来看,只看到了最后的结果,但是详细的过程我们真的清楚吗?下面我们看看下面过程。
- 文件中,我们把数据通过文件描述符写到文件当中:
- 其实并不是把数据写到了磁盘当中的特定文件。
- 而是把数据写给了操作系统的缓冲区里,然后操作系统再根据它自己的策略将数据刷新到外设,比如磁盘上。
- 所以之前学习文件时的
write
函数不能叫做将数据写入到磁盘,而应该叫做拷贝函数。 - 先将应用层数据拷贝到内核,再由操作系统定向的根据自己的策略进行
IO
,更新到磁盘上。
- 调用
write
和sendto
这样的系统接口,并没有把数据发送到网络里,而是把数据交给了操作系统:
- 真正发送数据不是我们发的,而是我们将数据交给操作系统,让操作系统帮我们发的。
- 所以我们之前写的所有的网络或者文件的接口,根本就不能叫做发送和写入接口,而应该叫做拷贝函数。
- 所以我们在学习网络中,大部分用套接字发送的数据,其实根本就没有发送数据,而只是将数据拷贝到了一个叫
tcp
或udp
所对应的缓冲区中!!
数据什么时候发,怎么发这个工作就由操作系统来决定了。
4. UDP需要接收/发送缓冲区吗
- UDP没有也不需要发送缓冲区:
- 因为数据太简单了,上层拷贝的数据到了内核。
- 内核添加八个字节的报头,三四个字段一填。
- 填完之后,操作系统直接将报文交给了网络协议栈。
- 到了网络层,后续就是ip的任务了。
- 不需要维护可靠性,不需要将数据暂存起来。
- 只需要将数据交给内核这个工作就完了。
发送的时候直接交给操作系统,操作系统直接向上交付,速度很快不需要缓冲区。所以就不需要真正的发送缓冲区。
UDP 在发送端不一定需要显式的发送缓冲区,但通常会由操作系统提供一个临时的缓冲区来保存待发送的数据。
- UDP需要接收缓冲区吗?需要!!
- 作为udp在进行数据接收的时候,当底层数据没就绪时,在应用层的表现就是
recvfrom
接口是阻塞的。 - 一直卡在那里当底层数据就绪时,
recvfrom
接口才会返回,并将数据拷贝到提前设置到recvfrom
的缓冲区里面。
如果udp收到了数据,操作系统压力很大,收到了数据还没来得及调度这个进程(应用层进程来不及接收数据),如果内核不存在接收缓冲区,一定会出现数据的丢失问题。
udp是不保证可靠性,但是并不代表它可以在可靠性这件事上肆意妄为:(重点)
- 如果没有接收缓冲区,一旦应用层进程来不及接收数据,这个数据直接就丢了!
- 所以udp是具备接收缓冲区的,数据收到之后,如果上层来不及处理。
- 会暂时保存在缓冲区里,等到应用层来得及读取了,再把数据从缓冲区里拷过去。
然尽管UDP协议本身没有应用层缓冲区,但在实际应用中,很多基于UDP的应用程序会在应用层上实现自己的缓冲机制。这是为了解决数据传输中可能出现的丢包、乱序等问题。应用层缓冲区可以用来存储接收到的数据包,以便应用程序按照自己的需求进行处理、重组或重发等操作。
需要注意的是,应用层缓冲区是由应用程序自己实现和管理的,不同的应用程序可能有不同的缓冲策略和机制。使用UDP协议时,开发者需要在应用程序中自行管理缓冲区,确保数据的完整性和正确性。
UDP报文乱序到达:
- udp不保证可靠性,就导致了数据不能够按序到达。
- 虽然有接收缓冲区,但是接收缓冲区里数据的顺序性,upd和操作系统不关心。
- 应用层用户若是关心接收数据的顺序,就要采取响应措施,如应用层用户对数据报文编号,重排序。
- 如果接收缓冲区满了的话,再来数据,再到达的udp数据就会被丢弃了。
UDP协议本身并没有提供应用层缓冲区。UDP是一种无连接的、不可靠的传输协议,它将数据封装成数据包,通过网络进行传输,不需要建立像TCP那样的连接。
在UDP通信中,数据包从发送方直接发送到目的地,如果发生丢包或乱序等情况,UDP协议本身不提供任何机制来处理这些问题。因此,UDP不具备对数据包的重组、重发和流量控制等功能。
UDP的socket既能读也能写,UDP报文出的路径和报文进的路径是两条路径,互相不干扰,所以是全双工。
5. UDP使用注意事项
UDP协议首部是一个16位的最大长度(单位Byte),也就是说一个UDP能传输的数据最大长度是64K
(包含UDP首部2^16 个 Byte + 8 Byte
)。
然而64K
在当今的互联网环境下,是一个非常小的数字。如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装 (在应用层),没办法发送大数据量。
udp存在的意义:
- 现在的网络环境一般不丢包,如果丢包丢掉一些某些场景不影响。
- 因为udp很简单,不需要维护复杂连接,所以也是需要在各种不同的场景中被采用的。
- 其中最经典的场景就是直播。
-
- 观看直播时,模糊卡顿掉帧等情况是允许的。
-
- 因为观看直播的人太多了,每个人都要维护连接,服务器扛不住。
-
- 如果推送的是udp,不用建立连接,谁需要就给谁。