第二天笔记
- 传输层协议与应用
- UDP协议的特点
处于不同网络的两台主机上的应用进程如果想要进行通信,则需要通过物理层、数据链路层、网络层这三层进行数据包转发,再通过传输层把数据包发送给主机中的指定进程,所以网络模型中的传输层至关重要。
传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol),想要掌握这两种协议,则需要阅读协议的标准文件,UDP标准内容如下:
通过标准可以知道,UDP协议提供面向事务的简单不可靠信息传送服务,因为无法保证数据包的交付以及当数据包出现丢包之后不具备对丢失的数据包重传的功能。如果用户打算有序可靠的传输数据流给某个应用程序则推荐使用TCP协议。
对于UDP协议而言,还存在其他特性,比如UDP协议提供的是面向无连接的服务,也就是说双方在通信之前不需要建立连接,接收方收到数据之后也无需进行回复确认送达,所以UDP协议无法知道报文发送之后是否安全完整到达,体现出“不可靠”的特性。
注意:UDP协议也不保证数据包可以有序到达,以及不提供数据包进行分组和组装的服务。
- UDP协议的报首
当然,通过标准可以知道,UDP协议的数据包体积较小,因为UDP协议的数据包报首较小。
- 源端口号
源端口号指的是发送数据的主机进程对应的端口号,可以看到是16bit,也就是2个字节,该成员是可选的,如果不指定端口,则会填充为0。
- 目标端口
目标端口指的是接收数据的主机的进程对应的端口号,可以看到是16bit,也就是2个字节,这里注意:并不是每台主机的进程的端口号都相同,也就是端口号是本地有效的,换个说法,两台主机中相同的端口号不一定指的是相同进程!
- 包总长度
包长度指的是UDP报首长度 + 数据内容长度之和,而包长度占16bit,也就是包长度的最大值是65535字节,可以看到UDP数据报报首的长度是8字节,而UDP协议接收到的网络层转发过来的数据包中还存在IP协议的报首,而IP协议的报首固定为20字节(不使用可选选项的情况下),所以UDP携带的数据内容的最大值是65507字节。
- 校验和
校验和是用于检测UDP用户数据包传输过程中是否出错,如果出错,则直接丢弃,如果源主机不想生成校验和,可以填充把校验和的16bit设置为0。
- UDP的接口说明
在Linux系统中提供了UDP协议的描述和UDP协议相关的接口说明,建议通过man手册的第7章了解UDP协议的使用规则:
可以看到,UDP协议提供的是一种无连接、不可靠的服务,并且可以通过RFC 768标准了解UDP协议。
另外,通过手册发现想要指定UDP协议进行通信,需要先调用socket函数创建UDP套接字,如果只打算向对方主机发送数据包,则可以调用sendto()函数或者sendmsg()函数,注意要把对方主机的有效IP地址作为参数。
如果打算接收对方主机发送的数据包,则需要提前调用bind()函数把UDP套接字和本地主机地址进行绑定,然后调用recvfrom()函数来接收数据包。
- 接口说明
- socket()函数
Linux系统的思想是“一切皆文件”,所以Linux系统把文件分为7类、其中有一种文件是为了实现在不同主机的进程间进行通信而设计出来的,也被称为套接字文件(标识符是s,指的是socket的缩写)。
如果双方主机打算通信,则双发都需要在本地创建套接字文件,Linux系统中提供了一个名称叫做socket()的函数接口,使用规则如下:
- 函数参数
第一个参数:domain指的是要使用的协议族,因为协议族决定了通信地址类型,常用的是AF_INET协议族,这样就采用IPV4地址进行通信。
第二个参数:type指的是套接字类型,一般常用的类型有SOCK_STREAM 和 SOCK_DGRAM。
SOCK_STREAM类型 :提供有序、可靠、双向、基于连接的字节流,也就是基于TCP协议。
SOCK_DGRAM类型 :提供不可靠、无连接、固定长度数据的数据报,是基于UDP协议。
第三个参数:protocol指的是和套接字类型相关的协议,一般设置为0即可,这样系统会自动根据套接字类型选择合适协议。
- 返回结果
socket函数创建套接字成功,则会返回对应的文件描述符,如果套接字创建失败,则返回-1。
举例:如果想要使用IPV4协议族,并通过UDP协议进行通信,可以参考man手册的案例!
- bind()函数
双方主机通信时一般采用双向通信,也就是主机在发送数据的同时也可能等待接收对方的数据,如果打算接收对方主机的数据,则需要把创建的套接字和本地地址以及端口进行绑定,Linux系统提供了一个名称叫做bind()的函数接口,该接口的使用规则如下:
- 函数参数
第一个参数:sockfd指的是创建成功的套接字返回的文件描述符,就是socket()函数返回值。
第二个参数:addr指的是要绑定的本地地址的结构体指针,但是实际addr的结构体类型是和协议族有关的,比如采用AF_INET协议族,则需要阅读man手册的第7章了解地址结构。
可以看到,采用IPV4协议需要指定IP地址和端口号,对应的结构体类型是struct sockaddr_in,该结构体有3个成员:
成员sin_family :指的是协议族,需要设置为AF_INET
成员sin_port :指的是端口号,必须转换为网络字节序,需调用htons()函数实现转换
成员sin_addr :指的是IP地址,必须转换为网络字节序,需调用inet_addr()实现转换
思考:请问什么是网络字节序,为什么一定要把端口号和IP地址都转换为网络字节序才行?
回答:对于不同网络中的主机而言可能采用的平台都各不相同,而不同平台的主机在存储数据的方式也不同,一般分为两种方案:大端存储(Big_Endian)or 小端存储(Little_Endian)。
大端存储:数据的高字节数据存储在内存的低地址,如ARM平台就采用大端方式存储数据
小端存储:数据的低字节数据存储在内存的低地址,比如X86平台就采用小端方式存储数据
由于不同平台存储数据的方式不同,所以把数据封包发送出去,对方主机接收到数据包进行解包之后得到的原始数据的值可能含义完全不同,导致数据异常,所以为了统一标准,就设计出网络字节序,网络字节序统一采用大端方式传输数据。
当然,Linux系统提供很多函数实现把主机字节序转换为网络字节序也提供了函数接口实现把网络字节序转换为主机字节序,比如htons()、htonl()、ntohs()、ntohl()、inet_addr()等等。
- 返回结果
bind()函数调用成功则返回0,bind()函数调用失败则返回-1,建议绑定地址后进行错误处理。
- sendto()函数
如果打算向对方主机发送数据,则双方主机都需要创建UDP套接字,发送端可以调用sendto()函数发送数据,函数使用规则如下:
- 函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是要发送的消息对应的缓冲区的地址,const void * 表示地址类型任意。
第三个参数:len指的是要发送的消息的大小,以字节为单位,不要超过UDP的数据包大小!
第四个参数:flags指的是发送消息的标志,一般设置为0,和系统调用write()函数作用类似。
第五个参数:dest_addr指的是目标主机的IP地址,因为UDP是无连接的,需要指定接收端。
第六个参数:addrlen指的是目标主机的IP地址的大小,一般通过sizeof()进行计算即可得到。
- 返回结果
sendto()函数发送消息成功,则返回发送的消息的字节个数,sendto()函数调用失败则返回-1。
思考:如果待发送的数据包的大小超过UDP规定的数据包大小,请问可能会出现什么现象???
回答:UDP是不可靠的传输协议,为了减少UDP包丢失的风险,我们最好能控制UDP包在IP层的传输过程中不要被切割,在下层数据链路层MTU是1500字节的情况下要想IP层不分包,那么UDP数据包的最大大小应该是1500字节- IP头(20字节) - UDP头(8字节) = 1472字节。
这也就是说IP数据报大于1500字节,大于MTU,这个时候发送方IP层就需要分片。把数据报分成若干片,使每一片都小于MTU,而接收方IP层则需要进行数据报的重组。但是由于UDP的特性,当某一片数据传送中丢失时,接收方无法重组数据报,将导致丢弃整个UDP数据报,所以通常建议UDP的数据包不要超过MTU的大小。
- recvfrom()函数
接收方主机想要得到发送过来的数据包,则需要调用recvfrom()函数实现,函数的使用规则如下:
- 函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是接收的消息要存储的缓冲区的地址,void * 表示地址类型可以任意。
第三个参数:len指的是要接收的消息的大小,以字节为单位,大小可以结合实际情况设置。
第四个参数:flags指的是接收消息的标志,一般设置为0,和系统调用read()函数作用类似。
第五个参数:src_addr指的是源主机的IP地址,如果对发送方不感兴趣,可以设置为NULL。
第六个参数:addrlen指的是源主机的IP地址的大小,注意是一个指针变量,需要传地址!!!
- 返回结果
recvfrom()函数接收消息成功返回接收的消息的字节个数,recvfrom()函数调用失败则返回-1。
作业:设计两个程序A和B,程序A作为客户端,程序B作为服务器,客户端启动之后把上线时间作为消息发送给服务器。服务器启动之后等待客户端消息,如果有客户端上线,则把客户端的IP地址和上线时间输出到终端显示。 要求在该练习的基础上实现客户端和服务器端的双向通信(提示:多线程实现,另外C/S都需要调用bind函数)
- UDP的广播功能
由于UDP协议是面向无连接的,所以客户端和服务器之间不需要建立连接,所以利用UDP协议除了可以实现单播通信(一对一),还可以实现广播通信(一对多)和组播通信(一对多)。
广播通信指的是可以给处于局域网中的所有主机发送消息,只不过需要使用特定的广播地址,局域网大多数情况使用的是C类地址,对于某个C类网络而言,本地地址占8bit,如果本地地址8bit全部为1,则表示所有主机,举例:192.168.60.255就是粤嵌从化校区8栋407课室的广播地址。
思考:如果知道了某个局域网的广播地址,是否某台主机直接向该地址发送数据包就可以让处于该局域网的所有主机都收到该数据包?
回答:不可以,因为IP地址只能标识局域网中的主机,而每台主机都可能运行了多个网络进程,所以主机收到的数据包到底交给哪个网络进程是由系统中网络进程的端口决定的。
所以处于局域网的每台主机必须提供一个相同的端口号,这样才可以把接收的数据包保留,如果某个主机没有提供这个端口号,则收到的数据包会被丢弃。
由于广播功能属于Linux系统套接字文件的属性选项之一,所以想要启动广播功能,则需要设置套接字的属性选项,Linux系统中提供了两个函数接口来获取和设置套接字的属性选项,分别是getsockopt()和setsockopt(),使用规则如下所示:
- 函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:level指的是选项对应的协议级别,一般建议把该参数设置为SOL_SOCKET即可。
第三个参数:optname指的是选项的名称,比如广播选项的名称是SO_BROADCAST,如下:
注意:关于套接字选项的描述可以通过man手册的第7章来了解,输入 man 7 socket 即可。
第四个参数:optval指的是要设置的选项值,比如要启用广播选项,则设置optval为非0值。
第五个参数:optlen指的是选项值的长度,一般可以通过sizeof计算对应选项值的长度大小。
- 返回结果
setsockopt()函数和getsockopt()函数调用成功则返回0,如果调用失败,则返回-1和错误码。
练习:设计程序,要求把教师机作为服务器,教师机向课室局域网的广播地址发送数据包,课室其他电脑作为客户端,客户端把服务器广播的消息输出到自己电脑的终端,并思考教师机是否会收到这个数据包?
- UDP的组播功能
思考:通过刚才的学习,可以知道UDP套接字支持广播,也就是向处于局域网中的所有主机发送数据包,但是有时可能只打算向局域网中的部分主机发送数据包,但是又不想一对一进行通信,请问有什么方案可以实现?
回答:可以利用UDP的组播功能实现(组播也被称为多播),其实组播功能还是依赖于UDP的广播属性,只不过不再使用局域网的广播地址,而是采用组播地址实现,只有加入了该组的主机才可以收到数据包,这样可以降低负载。
IP协议中规定了的IP地址的分类,其中包含A类地址、B类地址和C类地址,但是这三类地址并没有耗尽所有的IP地址,所以剩余的IP地址又分为D类地址和E类地址。
只不过这两类地址不用于标识主机,因为这两类地址没有分配本地地址,也就是这两类地址的32bit全部分配给网络编号。
用户只需要从D类地址中选择一个IP地址作为组播地址,并把主机IP地址加入该组中即可,这样发送方只需要把数据包发给组播地址,处于该组的主机都可以接收到数据包。
思考:发送方只需要通过sendto()函数就可以向组播地址发送数据包,但是请问应该如何把主机加入到组内呢?
回答:组播也属于套接字的属性选项,所以还是调用setsockopt()函数进行设置即可,只不过函数参数需要修改,具体可参考man手册的第7章关于ip协议的描述:man 7 ip 如下:
可以看到,如果想要把主机IP加入到多播组中,需要使用一个名称叫做struct ip_mreqn的结构体,该结构体有3个成员,分别是多播组的IP地址、主机的IP地址、多播组接口索引。
注意:一般接收端才需要加入多播组,所以接收端设置即可,发送端只需要知道多播组的IP地址和端口号即可,另外,接收端的端口号需要和发送端一致,否则无法收到数据包!!!!!
拓展练习:设计两个程序,程序A作为服务器,程序B作为客户端,程序B加入到一个多播组中并等待服务器发送数据包,如果收到数据包则把消息内容输出到终端,程序A每隔一段时间向多播组发送一条消息即可。 提示:最好发送消息的主机启动广播功能
- UDP协议的应用
- 音视频流
UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序。当强调传输性能而不是传输的完整性时UDP是最好的选择,比如音视频和多媒体应用。
在网络质量令人十分不满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如聊天用的QQ就是使用的UDP协议。
- 域名解析
此外,UDP协议也常用于域名解析服务。比如当计算机向DNS服务器查询域名对应的IP地址时通常使用UDP协议进行通信。由于DNS查询通常是简短的请求和响应,所以UDP协议适合这种快速而简单的通信。
比如主机打算响应某个网站的网络请求,但是只知道网站域名是无法通信的,需要对域名进行地址解析,得到网站的公有IP地址,所以Linux系统提供了名称叫做gethostbyname()的函数接口实现该功能,使用规则如下:
可以看到,该函数的返回值是一个指向hostent结构体类型的指针,该结构体中有一个名称叫做h_assr_list的成员,该成员是一个记录了主机域名对应IP地址的数组(一个主机域名可能会注册多个IP地址),数组中记录的IP地址是以网络字节序存储的。
为了直观的输出主机域名的IP地址(采用点分十进制最直观),所以需要把网络字节序的IP地址转换为字符串形式的点分十进制IP,Linux系统提供了一个名称叫做inet_ntoa()的函数实现,使用规则如下:
练习:设计程序实现解析www.baidu.com的域名,把获取到的百度的IP地址全部输出到终端并验证是否正确。