在我们刚学习网络时,我们知道网络是层状的结构,前面所有的内容都是围绕着应用层展开的。
接着我们来学习操作系统内部最顶层协议,叫做传输层协议。
应用层用应用的功能都是传输层提供的接口。
例如,用http
通信,http
协议底层用的是tcp
协议。
https
在密钥协商之前,首先要将客户端和服务端的连接建立好,所以在应用层的理解之下,还要搞一下传输层协议。
传输层最典型协议有两种,一个udp,以个tcp,对应的就是曾经学到的udp套接字和tcp套接字。
网络层状结构复习
端口号(Port)标识了一个主机上进行通信的不同的应用程序。
在写套接字的时候,无论udp
还是tcp
,在服务器启动时都必须bind
,得明确服务端的端口号,一旦保存端口号,底层在收到报文时,操作系统在收到数据,会根据对应的报文来进行数据处理,根据端口号将数据推送到特定的服务中。
这四个概念非常重要:在TCP / IP
协议中,用 “源IP”,“源端口号”,“目的IP”,“目的端口号”。
端口号的位数是16位端口号:
tcp
或udp
它的报头当中,端口号的字段就是16位的。端口号和服务是一一对应的。
有些服务器是非常常用的,为了使用方便,约定一些常用的服务器,都是用以下这些固定的端口号:
查看知名端口号:
cat /etc/services
所以在自己写一个程序使用端口号时,要避开这些知名端口号,避免不必要的麻烦。
这些端口是被特定的服务所采用的:
是因为有这个后端的守护进程sshd
,一直在后端运行,所以在本地打开X-shelI
时,连接的就是这个服务,由这个服务来帮助我们登录Linux。
端口号是用来标定进程唯一性的。
一个进程是否可以bind
多个端口号?一个端口号是否可以被多个进程bind
?
一个进程可以同时监听和处理多个端口号上的网络数据。
netstat是一个用来查看网络状态的重要工具。
netstat
是一个用于显示网络连接、路由表和网络接口等信息的常用命令。它可以在命令行界面下运行,并提供了多种选项来获取特定类型的网络统计数据。
查看网络状态:
netstat [选项]
常用选项:
在查看服务器的进程id时非常方便。
pidof
是一个用于查找指定进程名称所对应的进程ID的命令。在 Linux系统中,可以使用这个命令来获取特定进程的PID(进程 ID),以便进行后续处理。
通过进程名,查看进程id:
pidof [进程名]
常用选项:
源端口和目的端口组合在一起形成一个套接字(Socket),在网络中充当数据交换的门户。通过源端口和目的端口的配对,可以确保数据包被正确地发送和接收,并且能够正确地路由到目的地。
在传输层协议中,如TCP(传输控制协议)和UDP(用户数据报协议),源端口和目的端口信息包含在数据包的首部中,用于标识通信双方的应用程序或进程。通过源端口和目的端口的匹配,网络设备可以将接收到的数据包正确地路由和交付给相应的应用程序或进程。
我们之前用UDP协议写过一个简单的聊天程序: UDP的服务端 + 客户端。
未来学习的所有协议必须考虑两个共性问题:
在我们之前网络基础中讲到过封装解包和分用
的过程, 复习传送门。
我们以UDP为例:
UDP的特点:
UDP传输的过程类似于寄信。
udp一定不会是为了可靠性而做过多的设计!
udp不可靠,报文丢了就是真的丢了。丟包了不重要,因为允许丢包。
面向数据报:
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。
假设用UDP传输100个字节的数据:
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节,而不能循环调用10次recvfrom,每次接收10个字节。
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):
UDP叫做面向数据报,体现在它不是流式的,报文和报文有明显边界。
16位UDP检验和:
之前我们学习过操作系统的文件操作的write/read
接口,我们在使用者的身份来看,只看到了最后的结果,但是详细的过程我们真的清楚吗?下面我们看看下面过程。
write
函数不能叫做将数据写入到磁盘,而应该叫做拷贝函数。IO
,更新到磁盘上。write
和sendto
这样的系统接口,并没有把数据发送到网络里,而是把数据交给了操作系统:tcp
或udp
所对应的缓冲区中!!数据什么时候发,怎么发这个工作就由操作系统来决定了。
发送的时候直接交给操作系统,操作系统直接向上交付,速度很快不需要缓冲区。所以就不需要真正的发送缓冲区。
UDP 在发送端不一定需要显式的发送缓冲区,但通常会由操作系统提供一个临时的缓冲区来保存待发送的数据。
recvfrom
接口是阻塞的。recvfrom
接口才会返回,并将数据拷贝到提前设置到recvfrom
的缓冲区里面。如果udp收到了数据,操作系统压力很大,收到了数据还没来得及调度这个进程(应用层进程来不及接收数据),如果内核不存在接收缓冲区,一定会出现数据的丢失问题。
udp是不保证可靠性,但是并不代表它可以在可靠性这件事上肆意妄为:(重点)
然尽管UDP协议本身没有应用层缓冲区,但在实际应用中,很多基于UDP的应用程序会在应用层上实现自己的缓冲机制。这是为了解决数据传输中可能出现的丢包、乱序等问题。应用层缓冲区可以用来存储接收到的数据包,以便应用程序按照自己的需求进行处理、重组或重发等操作。
需要注意的是,应用层缓冲区是由应用程序自己实现和管理的,不同的应用程序可能有不同的缓冲策略和机制。使用UDP协议时,开发者需要在应用程序中自行管理缓冲区,确保数据的完整性和正确性。
UDP报文乱序到达:
UDP协议本身并没有提供应用层缓冲区。UDP是一种无连接的、不可靠的传输协议,它将数据封装成数据包,通过网络进行传输,不需要建立像TCP那样的连接。
在UDP通信中,数据包从发送方直接发送到目的地,如果发生丢包或乱序等情况,UDP协议本身不提供任何机制来处理这些问题。因此,UDP不具备对数据包的重组、重发和流量控制等功能。
UDP的socket既能读也能写,UDP报文出的路径和报文进的路径是两条路径,互相不干扰,所以是全双工。
UDP协议首部是一个16位的最大长度(单位Byte),也就是说一个UDP能传输的数据最大长度是64K
(包含UDP首部2^16 个 Byte + 8 Byte
)。
然而64K
在当今的互联网环境下,是一个非常小的数字。如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装 (在应用层),没办法发送大数据量。
udp存在的意义: