从零开始理解UDP协议
之前我们讲过服务器上的端口号和服务器的进程是绑定的!客户端的进程与客户端的端口号也是绑定的!再通过IP地址,就可以快速找到网络中需要进行通信的进程!
用 “源 IP”, “源端口号”, “目的 IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信,可以明确目标进程和来源进程以及通信协议
端口号范围划分 • 0 - 1023:知名端口号, HTTP,FTP,SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的。 • 1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号, 就是由操作系统从这个范围分配的。
我们之前测试的时候都是绑定的8888端口,如果今天绑定0 - 1023
的端口,就会绑定失败:
只有我们使用超级用户的权限,我们才可以绑定0-1023
端口号!普通用户是不能随便绑定知名端口号的!知名端口号都是与特定的服务联系在一起的!也就是说未来客户端可以在不知道端口号的情况下链接特定服务时直接使用知名端口号!
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号: • ssh 服务器, 使用 22 端口 • ftp 服务器, 使用 21 端口 • telnet 服务器, 使用 23 端口 • http 服务器, 使用 80 端口 • https 服务器, 使用 443
现在有两个问题:
理解端口号和进程的关系:
在操作系统内部有这样一个描述进程的结构体task_struct
!操作系统还有这样一个哈希表,K为端口号,V为进程的结构体task_struct
!进绑定时就是将端口号与进程结构体建立哈希关系!这样传输层从网络层解析出来端口号时,你可以找到对应的进程,进入应用层!这样也就理解了一个端口号不能被多个进程bind,不然就产生哈希冲突了!
协议是一种约定,是双方都认识的结构化数据!在学习应用层时,我们自己设计了自己的结构体作为协议!那么UDP也就是一种结构体!
我们来看源代码中的UDP报头结构:
这个结构体十分的简单奥!其中的UDP校验和是用来检验数据是否正常的,如果数据校验出错,就会直接丢弃数据!所以说UDP协议是不可靠的!
作为一种协议,那么是不是就要进行序列化与反序列化?那为什么没有看到UDP的序列化和反序列化?其实报头就是一个结构体变量,直接加到报文前,读取是直接进行二进制读取获取到结构体变量!**但是应用层是不能这样写的!应用层涉及不同语言,大小端机器等很多问题!!!**而内核没有业务!报文该是多少就是多少,不会增添新的内容!并且双方操作系统都是C语言写的!都要先转大端序列!那么内核就能直接读取结构体变量,并且不会出现问题了!!!
UDP 传输的过程类似于寄信:
UDP 没有真正意义上的发送缓冲区。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。应用层直接就通过内核发送!因为UDP没有重传机制,发送只会进行一次,并且UDP的报头结构很简单,应用层的报文直接加上8字节即可。那么就不需要同一个缓冲区来进行管理了!
但UDP 具有接收缓冲区。UDP的接收缓冲区可以提高效率,执行任务时依旧可以读取数据!但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致。如果缓冲区满了,再到达的 UDP 数据就会被丢弃!
UDP的发送与接收是独立的,那么自然就支持全双工通信了!
在网络通信过程中,操作系统会不断的接收报文,应用层产生报文。所以OS中可能同时存在大量的报文,这些报文可能正在被向上交付,也可能被向下交付!所以操作系统就要对报文进行管理!一个完整的报文不仅仅是报头和有效载荷,还需要一个管理报文的结构 !
管理报文的结构化字段struct_sk_buff
内部一个指针指向下一个报文。是通过链式结构(Linux系统是双链表)的增删查改进行管理的!
struct_sk_buff
内部有指向数据的指针。当报文向下传输时,会先将报文内部的数据写到下一层的一个缓冲区中,注意是写到缓冲区的中间位置。然后将head指针向前移动相应报头大小,之后就可以在head这片空间内写入新的报头了!向上传输就是将head指针向后移动除去报头即可!
我们注意到,UDP 协议首部中有一个 16 位的最大长度。 也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)。单个报文的长度不能超过64K!
然而 64K 在当今的互联网环境下,是一个非常小的数字。
如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包,多次发送, 并在接收端手动拼装。多次发送,接收端手动拼装!