文章目录
- 传输层TCP/UDP
- 预备知识:
- 端口号的理解
- 端口号的范围
- 关于端口号的相关问题
- netstat命令
- pidof命令
- UDP协议
- 所处的位置
- UDP协议格式
- UDP的特点
- UDP的缓冲区
- 基于UDP的应用层协议
传输层TCP/UDP
回忆数据发送到网络的过程
之前在学习HTTP等应用层协议时为了方便理解:我们简单的认为HTTP协议是将请求和响应直接发送到了网络当中
实际上: 应用层需要先将数据交给传输层,由传输层对数据做进一步处理后再将数据继续向下进行交付,该过程贯穿整个网络协议栈,最终才能将数据发送到网络当中
传输层负责可靠性传输,确保数据能够可靠地传送到目标地址, 为了方便理解,在学习传输层协议时也可以简单的认为传输层协议是将数据直接发送到了网络当中
预备知识:
端口号的理解
- 端口号(Port)标识一个主机上进行网络通信的不同的应用程序
主机从网络中获取到数据后,需要自底向上进行数据的交付,而这个数据最终应该交给上层的哪个应用处理程序,就是由该数据当中的目的端口号来决定的
- 从网络中获取的数据在进行向上交付时,在传输层就会提取出该数据对应的目的端口号,进而确定该数据应该交付给当前主机上的哪一个服务进程
- 端口号是属于传输层的概念的,在传输层协议的报头当中就会包含与端口相关的字段
在TCP/IP协议中用下面的五元组来标识一个唯一的通信:
组成 | 解释 |
---|---|
源IP+源端口号 | 标识数据来源,来自互联网中的那台主机的那个进程 |
目的IP+目的端口号 | 标识数据目的地,去往互联网中的那台主机的那个进程 |
协议号 | 通信使用的是哪一个协议,向上交付给哪一个协议 |
具体步骤:
1)先提取出数据当中的目的IP地址和目的端口号,确定该数据是发送给哪个服务进程的
2)然后提取出数据当中的协议号,为该数据提供对应类型的服务
3)最后提取出数据当中的源IP地址和源端口号,将其作为响应数据的目的IP地址和目的端口号,将响应结果发送给对应的客户端进程
端口号的范围
-
端口号的长度是16位,因此端口号的范围是0 ~ 65535
-
一些知名的服务绑定的端口一般是固定不可变的
- 如:0 ~ 1023:知名端口号.比如HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的
- ssh服务器,使用22端口 ftp服务器,使用21端口 telnet服务器,使用23端口 http服务器,使用80端口 https服务器,使用443端口
- 如:0 ~ 1023:知名端口号.比如HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的
-
1024 ~ 65535:操作系统动态分配的端口号.客户端程序的端口号就是由操作系统从这个范围分配的
- 我们部署自身服务时,绑定1024-65536的端口即可
关于端口号的相关问题
协议号 VS 端口号
-
协议号是存在于IP报头当中的,其长度是8位
- 协议号指明了数据报所携带的数据是使用的何种协议,以便让目的主机的IP层知道应该将该数据交付给传输层的哪个协议进行处理.
-
端口号是存在于UDP和TCP报头当中的,其长度是16位
- 端口号的作用是唯一标识一台主机上的某个进程.
区别: 协议号是作用于传输层和网络层之间的,而端口号是作用于应用层于传输层之间的.
一个端口号是否可以被多个进程绑定 (一个端口号是否可以绑定多个进程)
不能!因为端口号的作用就是唯一标识一个进程,如果绑定一个已经被绑定的端口号,就会出现绑定失败的问题.
- 我们之前写http代码的时候,就出现过
bind error
的错误,本质就是该端口号已经被绑定了
一个进程是否可以绑定多个端口号
可以! 一个进程是可以绑定多个端口号的,这与“端口号必须唯一标识一个进程”是不冲突的,只不过现在这多个端口唯一标识的是同一个进程罢了
可以类比一颗多叉树:
我们可以通过vim /etc/services
查看知名端口号:
netstat命令
netstat
是一个用来查看网络状态的重要工具,通过netstat
命令可以查看到上面的五元组信息
常见选项
- n:拒绝显示别名,能显示数字的全部转换成数字
- l:仅列出处于LISTEN(监听)状态的服务
- p:显示建立相关链接的程序名
- t(tcp):仅显示tcp相关的选项
- u(udp):仅显示udp相关的选项
- a(all):显示所有的选项,默认不显示LISTEN相关
查看TCP相关的网络信息时,一般选择使用nltp
组合选项, 查看UDP相关的网络信息时,一般选择使用nlup
组合选项
Local Address
表示的就是源IP地址和源端口号 Foreign Address
表示的就是目的IP地址和目的端口号
Proto
表示的就是协议类型,因为我们带了-t选项,所以显示的都是TCP协议通信的进程
pidof命令
pidof
命令可以通过进程名,查看进程id.
使用方法:pidof 进程名称
小例子:我们用pidof
命令查看我们自己编写的一个死循环进程,然后用kill名称杀死这个进程
UDP协议
所处的位置
传输层协议在系统内核中之上是系统调用,再之上是具体的应用层协议.而UDP/TCP协议是在应用层(内核)中,所以UDP和TCP是对上提供接口的协议.
- 之前的网络套接字编程时用到的各种接口,是位于应用层和传输层之间的一层系统调用接口,这些接口是系统提供的,我们可以通过这些接口搭建上层应用,比如HTTP
- 说HTTP是基于TCP的,实际就是因为HTTP在TCP套接字编程上搭建的
传输层实际就是由操作系统管理的,因此UDP是属于内核当中的,是操作系统本身协议栈自带的,其代码不是由上层用户编写的,UDP的所有功能都是由操作系统完成,因此网络也是操作系统的一部分
不管是哪一层协议都需要思考两个问题:
1.如何进行封装和解包
- 封装:就是添加上8字节的定长报头
- 解包:就是将报头和有效载荷分离
2.如何分用,向上交付
所以我们来一下UDP协议的段格式:
UDP协议格式
UDP协议格式如下
- 16位源端口号:表示数据是从哪里来的
- 16位目的端口号:表示数据要到哪里去,代表的是当这个报文被目标主机收到的时候,它会根据16位目的端口号交付给应用层的哪一个进程,其中会把数据交互给上层程序
- 16位UDP长度:表示整个数据报(即报文 = UDP首部(报头)+UDP数据(有效载荷) )的长度
- 16位UDP检验和:如果UDP报文的检验和出错,就会直接将报文丢弃
理解之前我们写http套接字的时候,为什么需要绑定端口号:
因为当底层收到了对应的UDP报文,他会根据报文的目的端口号把数据转给特定的绑定这个端口的进程
为什么端口号是16位
我们之前定义端口号都是这样定义的: uint16_t port = atoi(argv[1])
,其中uint16_t代表的就是16位整形
我们在应用层看到的端口号大部分都是16位的,其根本原因就是因为传输层协议UDP报头当中的端口号就是16位的,这属于协议规定!
UDP如何将报头与有效载荷进行分离
1)UDP的报头当中只包含四个字段,每个字段的长度都是16位,总共8字节.因此UDP采用的实际上是一种定长报头
2)UDP在读取报文时读取完前8个字节后剩下的就都是有效载荷了
UDP如何做到向上交付, 即: UDP如何决定将有效载荷交付给上层的哪一个协议
1)UDP上层也有很多应用层协议,因此UDP必须想办法将有效载荷交给对应的上层协议,也就是交给应用层对应的进程
2)应用层的每一个网络进程都会绑定一个端口号,服务端进程必须显示绑定一个端口号,客户端进程则是由系统动态绑定的一个端口号
3)UDP就是通过报头当中的目的端口号交付有效载荷给应用层的对应进程
注意:
内核中用哈希的方式维护了端口号与进程ID之间的映射关系,因此传输层可以通过目的端口号得到对应的进程ID,进而找到对应的应用层进程
Linux kernel 是用C语言写的,如何看待UDP报头(报文)
注意: 报文 = 报头 + 有效载荷
操作系统是C语言写的,而UDP协议又是属于内核协议栈的,因此UDP协议也一定是用C语言编写的,UDP报头实际就是一个位段类型
struct udp_hdr
{
uint32_t src_port: 16; //源端口号-16位
uint32_t dst_port: 16; //目的端口号-16位
uint32_t total: 16; //UDP长度-16位
uint32_t check: 16; //UDP检验和-16位
};
UDP数据封装:
当应用层将数据交给传输层后:
- 在传输层就会创建一个UDP报头类型的变量,然后填充报头当中的各个字段,此时就得到了一个UDP报头
- 操作系统再在内核当中开辟一块空间,将UDP报头和有效载荷拷贝到一起,此时就形成了UDP报文
UDP数据分用:
当传输层从下层获取到一个报文后:
- 读取该报文的前8个字节,提取出对应的目的端口号
- 通过目的端口号找到对应的上层应用层进程,然后将剩下的有效载荷向上交付给该应用层进程
UDP的特点
udp的通信过程类似于寄信.
-
无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接.
-
不可靠:没用确认和重传机制
- 如果因为网络故障或者其它问题导致报文无法发到对方,UDP协议不会返给上层任何数据丢失或接收成功的信息.
-
面向数据报:不能灵活地控制写入获取报文中数据的次数和数量
什么是面向数据报?
应用层交给UDP多长的报文,UDP就原样发送,既不会拆分,也不会合并,这就叫做面向数据报.
-
即: 接收数据时只能按报文的大小一次性将报文全部收取上来
例子:发送端假如一次调用sendto发送了100个字节,接收端也必须一次性recvfrom将100个字节全部提取出来,所以在网络看来,在双端主机看来, 我们拿到的都是一个一个独立的报文
-
UDP使用注意事项
UDP报头中用16位UDP长度来表示整个报头的长度
- 所以UDP最大只有65536个字节(2^16), 因此一个UDP报文的最大长度是64K(UDP报头+有效载荷)
- 如果非要使用UDP发送超过64KB长度的数据包,只能拆包发送,并且对端按序号再将数据包整合到一起
注意: 报文在网络中进行路由转发时,并不是每一个报文选择的路由路径都是一样的,因此报文发送的顺序和接收的顺序可能是不同的
UDP的缓冲区
首先,我们要知道: 网络套接字收发接口read/write/recv/send
,虽名为读取,发送函数,但实质我们更愿意称之为拷贝函数, 因为这些接口并不是将数据直接发送到网络中,而是将数据从应用层拷贝到传输层的收发缓冲区中
拷贝完成之后,这些网络接口的工作就完成了.具体该数据什么时候发,一次发多少,发送失败怎么办,完全由OS内核中的传输层控制.这就是传输层的任务:提供传输数据的策略
例如:
1)当调用读取函数的时候,并不是把数据直接从网络里读上来,而是把数据从传输层的TCP的接收缓冲区/UDP的接收缓冲区里拷贝到用户空间
- UDP只有接收缓冲区,没有发送缓冲区 TCP有接收缓冲区&&发送缓冲区
2)当我们写入函数的时候,并不是把数据直接发到网络中,而是把数据拷贝到TCP的发送缓冲区当中,然后再往后处理
- UDP没有真正意义上的发送缓冲区.调用
sendto函数
会直接把数据交给内核,由内核将数据传给网络层协议进行后续的传输动作 - UDP具有接收缓冲区.接收到的数据存放在接收缓冲区中,以待上层调用接收函数, 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致
- 如果UDP的接收缓冲区满了,再到达的UDP数据就会被丢弃
- UDP是全双工的
- 全双工就是双方的收发互不影响,可以同时进行收发,同时调用收发函数
- 半双工就是双方不能同时进行收发,一方在发对方就不能发只能收,反之同理
问题:为什么UDP要有接收缓冲区
1)如果UDP没有接收缓冲区,那么就要求上层及时将UDP获取到的报文读取上去
- 如果一个报文在UDP没有被读取(上层没有调用读取函数),此时UDP从底层获取上来的报文数据就会被迫丢弃
2)一个报文从一台主机传输到另一台主机,在传输过程中会消耗主机资源和网络资源.如果UDP收到一个报文后仅仅因为上次收到的报文没有被上层读取,而被迫丢弃一个可能并没有错误的报文,这就是在浪费主机资源和网络资源.
3)因此UDP本身是会维护一个接收缓冲区的,
- 当有新的UDP报文到来时就会把这个报文放到接收缓冲区当中,此时上层在读数据的时就直接从这个接收缓冲区当中进行读取就行了
- 如果UDP接收缓冲区当中没有数据那上层在读取时就会被阻塞.因此UDP的接收缓冲区的作用就是,将接收到的报文暂时的保存起来,供上层读取.
缓冲区存在的价值:
一方面:让传输层定制传输数据的策略,另一方面,它将应用层协议和下层的通信细节,进行了解耦!
1)上层只要把数据拷贝到缓冲区,因为传输层属于协议栈,在OS启动之后就在内核,也就是在内存里面,所以说白了就是把数据从内存拷贝到内存,效率很高,
2)接下来数据怎么发送,要经过网络,比较费时间,所以只要能正常拷贝,拷贝之后,上层立马返回,可以直接进行后序处理,而发送的细节就由OS帮我们进行
基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
当然,我们自己写UDP程序时自定义的应用层协议也属于这个范畴