1.传输层协议
首先我们要明白,进入传输层,也就进入了操作系统内核。
因为应用层下面这几个就是操作系统内部
我们学传输层及其以下几层,也就是在学操作系统内核的网络模块。
1.1.传输层协议
传输层能够实现端到端的连接。比如说我们用QQ与别人发信息,网络层能够将信息发送到对方的主机上,主机上使用什么协议来接受这个信息就由传输层来完成,所以传输层实现的是进程到进程间的连接。
传输层提供的是应用程序间的逻辑通信,也就是说它向高层(应用层)屏蔽了下面网络层的细节,使应用程序看起来好像是在传输层之间沿着水平方向传输数据,但事实上两者之间并没有这样一条实际的物理连接。
传输层的作用:
- 为上面的应用层提供通信服务,提供应用进程间的逻辑通信
- 在 OSI 七层参考模型中,传输层是 面向通信部分的最高层,用户功能中的最底层
- 两大重要的功能:1)复用,2)分用。复用是指,在发送端,多个应用进程共用一个传输层。分用是指,在接收端,传输层会根据端口号将数据分派给不同的应用进程
- 传输层和网络层的区别:1)网络层为不同主机提供通信服务,而传输层为不同主机的不同应用提供通信服务;2)网络层只对报文头部进行差错检测,而传输层对整个报文进行差错检测
常见的传输层协议有以下两种
- TCP协议:面向连接的可靠传输协议。利用TCP进行通信时,首先要通过三步握手,以建立通信双方的连接。TCP提供了数据的确认和数据重传的机制,保证发送的数据一定能到达通信的对方。
- UDP协议:是无连接的,不可靠的传输协议。采用UDP进行通信时不用建立连接,可以直接向一个IP地址发送数据,但是不能保证对方是否能收到。
2.再谈端口号
端口号(port)标识了一个主机上进行通信的不同的应用程序。
如图所示,在一个机器上运行着许多进程,每个进程使用的协议都不一样,比如FTP,SSH,SMTP,HTTP,FTP等。
正是因为每一个进程都有自己的端口号(如图中TCP21,TCP22这些,后面的数字就是端口号)当发来的数据从网络中传输到应用层后,在传输层就会提取出该数据对应的目的端口号,进而确定该数据应该交付给当前主机上的哪一个服务进程。
2.1.五元组
TCP/IP协议是传输数据常用的协议,它会用 “源IP”、“源端口号”、“目的IP”、“目的端口号”、“协议号” 这样一个五元组来标识一个通信。
- 协议号是什么
IP是网络层协议,IP头中的协议号用来说明IP报文中承载的是哪种协议(一般是传输层协议,比如6 TCP,17 UDP;但也可能是网络层协议,比如1 ICMP;也可能是应用层协议,比如89 OSPF)。
TCP/UDP是传输层协议,TCP/UDP的端口号用来说明是哪种上层应用,比如TCP 80代表WWW,TCP 23代表Telnet,UDP 69代表TFTP。
目的主机收到IP包后,根据IP协议号确定送给哪个模块(TCP/UDP/ICMP...)处(数据由一台主机传输至另一台主机时,根据IP头上的协议号将数据送到相应的传输层解析(IP头被过滤掉,即此时到达传输层的数据已经没有IP头));
送给TCP/UDP模块的报文根据端口号确定送给哪个应用程序处理。【根据端口号确定数据送达主机中的应用程序,当数据送至应用程序(可以理解为主机中的某个进程)时,TCP/UDP头被过滤掉】
- 协议号和端口号的区别
- 协议号是存在于IP报头当中的,其长度是8位。协议号指明了数据报所携带的数据是使用的何种协议,以便让目的主机的IP层知道应该将该数据交付给传输层的哪个协议进行处理。
- 端口号是存在于UDP和TCP报头当中的,其长度是16位。端口号的作用是唯一标识一台主机上的某个进程
- 协议号是作用于传输层和网络层之间的,而端口号是作用于应用层于传输层之间的。
如上图所示,客户端A打开了两个浏览器页面(可认为是两个进程)向服务器分别发送数据1和数据2。所以在这两份数据的五元组中,源IP地址都是客户端A的IP地址,目标IP地址都是服务器的IP地址,源端口号分别是2001和2002,它们能标识这两个进程,目标端口号都是服务端的HTTP进程,HTTP使用的端口号是80。
客户端B也打开一个浏览器页面(可认为是一个进程)向服务器发送的数据3。所以在这份数据的五元组中,源IP地址就是客户端B的IP地址,目标IP是服务端的IP地址,源端口号是这个页面进程的端口号,目标端口号是服务器HTTP进程的端口号80。
协议号是通信协议的编号,指定的协议号可以标识一种协议,比如6号就标识TCP协议。
IP地址属于IP协议的内容,IP协议属于网络层,在传输层处不做讨论。
2.2.端口号的划分
端口号的长度是16位,因此端口号的范围是0 ~ 65535:
- 0 ~ 1023:知名端口号。比如HTTP,FTP,SSH等这些广为使用的应用层协议,它们的端口号都是固定的。
- 1024 ~ 65535:操作系统动态分配的端口号。客户端程序的端口号就是由操作系统从这个范围分配的。
2.2.1.认识知名端口号
0~1023范围内的端口号叫做知名端口号,这些端口号常被固定的服务绑定,如HTTP、FTP、SSH等这些广为使用的应用层协议,他们的端口号都是固定的。由于应用层协议本质上是进程在使用,所以在这里应用层协议、进程可以看作是等价的。
可以这样理解,110对应的是报警电话,120对应的是急救电话,119是火警电话,这些号码已经跟这些公共服务强绑定了。这些放在端口号上也一样,0~1023这些端口号都有对应的协议,不能随便更改,就像报警电话不会随便更改一样。所以,我们自己写程序时,要避开0~1023这些知名端口号。
有些服务器是非常常用的,为了使用方便,人们约定了一些常用的服务器,都是用以下这些固定的端口号:
- ssh服务器,使用22端口
- ftp服务器,使用21端口
- telnet服务器,使用23端口
- http服务器,使用80端口。
- https服务器,使用443端口
在Linux机器中,有一个文件/etc/services,它存储了所有具体端口号对应的服务协议。
我截取了该文件的一部分,可以清楚地看到http的端口号是80。
我们也可以看到,这个80端口支持tcp,udp,sctp三种协议!!!
接下来来问两个问题
- 1.一个端口号是否可以被多个进程绑定?
一个端口号绝对不能被多个进程绑定,因为端口号的作用就是唯一标识一个进程,如果绑定一个已经被绑定的端口号,就会出现绑定失败的问题。
- 2.一个进程是否可以绑定多个端口号?
一个进程是可以绑定多个端口号的,这与“端口号必须唯一标识一个进程”是不冲突的,只不过现在这多个端口唯一标识的是同一个进程罢了。
我们限制的是从端口号到进程的唯一性,而没有要求从进程到端口号也必须满足唯一性,因此一个进程是可以绑定多个端口号的。
2.3.netstat命令
netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]
功能:可用于查看当前机器的网络状态。
- n 拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有在 Listen ( 监听 ) 的服务状态
- p 显示建立相关链接的程序名
- t (tcp) 仅显示 tcp 相关选项
- u (udp) 仅显示 udp 相关选项
- a (all) 显示所有选项,默认不显示 LISTEN 相关
我们演示一下
这个最常用的就是下面这两个
查看监听的TCP端口
netstat -ntlp
查看监听的UDP端口
netstat -nulp
就不演示了
netstat命令是一个用于显示网络连接、路由表、接口统计、伪装连接以及多播成员等信息的工具。当使用netstat命令时,其输出结果包含多个列,每列都代表了不同的信息。以下是对netstat输出中常见列的含义的解释:
- 1. Proto:含义:显示连接使用的协议,如TCP或UDP。
- 2. Local Address含义:表示本地计算机的IP地址和端口号。这个地址是服务正在监听的地址。对于监听所有IP地址的服务,该列会显示为0.0.0.0:<端口号>。对于只监听特定IP地址的服务,将显示该IP地址和端口号。
- 3. Foreign Address含义:表示远程计算机的IP地址和端口号。这个地址是与本地计算机建立连接的远程计算机的地址。如果远程地址未知或未指定,则可能显示为0.0.0.0:*或*:*。
- 4. State:含义:表示TCP连接的状态。TCP连接状态包括多种,如LISTEN(监听状态,等待进入连接队列的连接请求)、ESTABLISHED(已建立连接,表示双方已经开始数据传输)、TIME_WAIT(等待足够的时间以确保远程TCP接收到连接中断请求的确认)等。UDP协议是无连接的,因此UDP连接不会显示状态。
- 5. Recv-Q:含义:表示接收队列中的连接个数。这是指已经到达本地计算机但尚未被应用程序读取的数据量。对于TCP连接,这个值通常应该很小或为零,因为TCP会尽量保持接收队列为空。
- 6. Send-Q:含义:表示发送队列中的字节数。这是指已经由本地计算机发送但尚未被远程计算机确认的数据量。对于TCP连接,这个值也应该很小或为零,除非正在传输大量数据。
- 7. PID/Program name(可选):含义:在某些netstat命令的输出中,还会显示创建该连接的进程的PID(进程ID)和程序名称。这有助于识别哪个程序正在使用特定的端口或IP地址。
以下是一个简化的netstat命令输出示例,用于说明上述列的含义:
Proto Local Address Foreign Address State Recv-Q Send-Q PID/Program name TCP 0.0.0.0:22 0.0.0.0:* LISTEN 0 128 - TCP 192.168.1.100:80 192.168.1.20:51234 ESTABLISHED 0 0 1234/nginx UDP 0.0.0.0:68 *:* 0 0 -
在这个示例中:
- 第一行表示本地计算机上的SSH服务(端口22)正在监听所有IP地址。
- 第二行表示一个TCP连接,本地计算机的IP地址是192.168.1.100,端口是80,与远程计算机192.168.1.20的端口51234建立了连接,状态是ESTABLISHED。
- 第三行表示一个UDP连接,本地计算机正在监听所有IP地址的68端口,但没有远程地址信息,因为UDP是无连接的。
2.4.pidof命令
pidof是一个用于查找指定名称的进程的进程ID的工具。
语法为:pidof [进程名]。
pidof命令可以通过查询运行中的进程信息来找到特定进程的进程ID。它可以返回一个或多个进程ID,这些ID可以用于进一步确认进程的运行状态、杀掉进程或者发送一个信号给它。
选项和参数:
- -s:仅返回一个进程号。
- -c:仅显示具有相同根目录的进程。
- -x:显示由脚本开启的进程。
- -o:指定不显示的进程ID。
使用pidof命令查找特定进程的ID时,只需要将进程名作为参数传递给pidof命令即可。
例如,要查找名为nginx的进程的ID,可以运行命令pidof nginx。
常见搭配kill命令使用,例如 kill -9 $(pidof nginx)
3.UDP协议报头
现在开始我们就正式进入传输层的学习。
从现在开始,进入操作系统内核部分,我们在学习传输层以及他下面每一层协议的时候,都要搞清楚几件事。
- 报头和有效载荷如何分离,有效载荷应该上交给哪一个上层协议(对应的协议字段,方案)——这个是最重要的
- 认识报头
- 学习该协议周边的问题
3.1.UDP协议格式
下图就是UDP协议的格式,前八个字节属于协议报头,之后的是有效载荷。
UDP协议的前八个字节作为报头,64个比特位。其中这八个字节的前16位0-15存放的是源端口号,第16~31位存放目的端口号,32~47位存放的是UDP的长度,48~64位存放的是UDP校验和。
- 16位源端口号:表示发送端(客户端)的端口号。
- 16位目的端口号:表示接收端(服务端)的端口号。
- 16位UDP长度:整个UDP数据段(报文)的长度,这个长度是包括报头和有效载荷的长度,UDP数据段最大64KB。
- 16位校验和:由操作系统进行校验,如果发送方和接收方的校验和不一致,说明出错,就会丢弃整个UDP数据段。(不可靠的部分)
我们注意到, UDP协议首部中有一个16位的最大长度. 说的是一个UDP能传输的最大报文长度是2^16 --> 2^10 * 2^6=64K(包括UDP首部)
然而64K在当今的互联网环境下, 是一个非常小的数字.
UDP数据段最长不能超过64KB,所以如果传输的数据超过了64KB,就需要用户在应用层手动将数据分成多个大小小于等于64KB的数据包,并进行多次发送,在接收端用户也需要手动将各分包重新拼装。
每层协议的学习,我们都应该掌握这两个问题:
- 协议报头和有效载荷如何分离?
- 有效载荷如何向上交付?
- 第一个问题,协议报头和有效载荷如何分离。
我们看到,UDP首部有一个16位的UDP长度。16位UDP长度:整个UDP数据段(报文)的长度,这个长度是包括报头和有效载荷的长度,UDP数据段最大64KB。
通过首部长度,我们就可以将TCP首部和有效载荷分离。
- 第二个问题,有效载荷如何向上交付。
UDP是传输层的,上层是应用层。而应用层程序会绑定端口号,UDP首部中有16位目的端口号,根据端口号做到向上交付。
3.2.UDP在Linux的表现形式
UDP协议是传输层协议,由操作系统维护,而Linux操作系统又是由C语言写的,所以UDP的报头一定会在操作系统中有自己的结构化数据,我们如果自己写一个结构体描述差不多就是这样:
struct udp_hdr
{
uint16_t src_port;//16位源端口号
uint16_t dest_port;//16位目的端口号
uint16_t length;//16位UDP长度
uint16_t check;//16位UDP校验和
};
当然将各变量改为unsigned int src_port:16;这样的结构也可以。
上面就是一个简易的UDP报文。
由于可能会有很多进程都在使用UDP向对方发送消息,由于一定会存在大量的UDP报文,操作系统一定要把它管理起来,怎么管理?
先描述,在组织。操作系统会有一个struct sk_buff来管理这些UDP报文。
struct sk_buff { char* start; char* end; char* pos; int type; //..... 省略 struct sk_buff* _Next; // 指向下一个,像链表一样串起来 };
- 理解报头与有效数据的分离
以C语言的知识理解报头与有效数据的分离,我们可以认为当计算机接收UDP数据时,该数据会先放在操作系统的已创建的内核缓冲区内(最大为64KB)。
然后创建一个hdr指针指向UDP报头起始位置,再再偏移报头字节数(sizeof(udp_hdr))构建一个start指针指向内核中有效载荷的起始位置。
最后根据报头的结构化数据将数据将各个数据(包括正文内容)拷贝到内存,这样就能实现了分离。
我们说填充报头,就是下面这个伪代码这样子。
char* hdr = malloc(XXX);//操作系统创建内核缓冲区 char* start = hdr + sizeof(struct udr_hdr);//指向有效载荷 strcpy(start,buffer,len);//将用户缓冲区中数据复制到内核缓冲区有效载荷处。 (struct udp_hdr*)hdr->src_port = xxx;//赋值源端口 (struct udp_hdr*)hdr->dest_port = xxx;//赋值源端口 (struct udp_hdr*)hdr->length = xxx;//赋值源端口 (struct udp_hdr*)hdr->check = xxx;//赋值源端口
- 分用
分用时,操作系统中已经维护了一个哈希表,使用绑定的端口号作为key值,就能直接找到这个进程,将有效载荷交给进程即可。所以,进程只要使用网络就一定要绑定端口号,不管是否使用bind显式绑定,这个绑定的过程本质上就是将数据插入哈希表。
3.3.UDP是面向数据报的
- 什么是面向数据报的?
面向数据报可以理解成面向快递,你的朋友给你寄了一个、两个、三个快递,未来你在收的一定是一个、两个、三个快递。你朋友发了三个快递你一定是收三个快递,不会收半个、一个半、两个等,他发几个你就收几个。
客户端曾经发了一个报文,你在调用recvfrom成功的时候,这个函数必定把一个完整的报文全部读上来。这叫做UDP数据报。
- 其一,在写udp的代码时明显可以感觉到不像写tcp网页版计数器哪里首先必须要先读到一个完整的报文,在udp哪里从来没有说过这样的话,因为用udp直接可以保证读到一个完整报文。
- 其二,对方调sendto发送10次报文,对方必须调用recvfrom接收10次报文,次数是1:1的。
这就是面向数据报,使用UDP协议我们不用考虑在应用层enlength增加报头,delength删除报头,用来区分数据,只用考虑序列化和反序列化就可以了。
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并;
用UDP传输100个字节的数据:
- 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节
- 2.为什么 UDP 是面向报文的协议?
当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息,也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。
- 你可能会问,如果收到了两个 UDP 报文,操作系统是怎么区分开的?
操作系统在收到 UDP 报文后,会将其插入到队列里,队列里的每一个元素就是一个 UDP 报文,这样当用户调用 recvfrom() 系统调用读数据的时候,就会从队列里取出一个数据,然后从内核里拷贝给用户缓冲区。
3.4.TCP的缓冲区和UDP的缓冲区
- TCP的缓冲区
还记得之前用过的send/sendto、recv/recvfrom这样的的接口吗?这些系统调用直接就能把数据发出去了吗?
结论当然是否定的。系统调用的工作在操作系统和用户应用的界限之间,而在网络的分层模型中,数据会一层一层向下传递并在物理层发出,所以这些系统调用的任务一定是从应用层将数据向操作系统传递。
在TCP协议中,操作系统会为通信双方各自维护一个发送缓冲区,一个接收缓冲区。用户层在调用send/sendto函数之后,操作系统会自动对需要发送的内容拼接TCP报头(TCP报头的具体内容后面会讲)形成数据段,然后拷贝到到发送缓冲区中。
同样,本主机从对端主机收到数据段后,也会先将其放入接收缓冲区中。当用户层读使用recv/recvfrom这样的系统调用时,系统调用就会将接收缓冲区内的数据拷贝到应用层中,具体点说就是拷贝到接收数据的字符串中。
所以,send/sendto、recv/recvfrom本质上是一个拷贝接口,它们前者负责将应用层的数据拷贝到发送缓冲区,后者负责将数据从接收缓冲区拷贝到应用层。
在两个缓冲区中的数据由TCP的内部代码进行发送和接收,用户只负责将数据放入或拿出这两个缓冲区。正是因为数据的传输无需用户参与,所以TCP的全名叫做传输控制协议。
而且这两个缓冲区使得通信双方进程在同一时刻,既可以发送数据,也可以接收数据,且互不影响,我们称之为全双工。
- 2.UDP的缓冲区
UDP协议与TCP相似,只是少了一个发送缓冲区。当客户端使用UDP协议将用户层的数据使用sendto发送的时候,数据会被直接拷贝到内核中,经过一定处理后立即发出。
在使用UDP协议接收数据时,操作系统将发送来的数据存放在接收缓冲区中,在适当的时候,操作系统会将数据交给用户层。
UDP的信息传输方式也实现了发送和接收数据同时进行和互不干扰,也叫做全双工。
3.5.基于UDP的应用层协议
我们不会单独说UDP协议有啥用,因为他是操作系统内部的东西,类似于底层的东西,存在的意义就是为了给上层提供服务。UDP的存在,可以让上层的应用层有各式各样的协议。
缩写 | 全名 |
NFS | 网络文件系统 |
TFTP | 简单文件传输协议 |
DHCP | 动态主机配置协议 |
BOOT | 启动协议(用于无盘设备启动) |
DNS | 域名解析协议 |
上面这些协议在传输层都是使用的UDP协议,其中很多我们天天都在用。
UDP我们就简单的讲一下就好了,这个不是我们的重点,我们的重点是TCP协议