UDP协议

传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol),想要掌握这两种协议,则需要阅读协议的标准文件,UDP标准内容如下:
image
UDP协议提供面向事务的简单不可靠信息传送服务,因为无法保证数据包的交付以及当数据包出现丢包之后不具备对丢失的数据包重传的功能。
UDP协议提供的是面向无连接的服务,也就是说双方在通信之前不需要建立连接,接收方收到数据之后也无需进行回复确认送达,所以UDP协议无法知道报文发送之后是否安全完整到达,体现出“不可靠”的特性。
UDP协议也不保证数据包可以有序到达,以及不提供数据包进行分组和组装的服务。

UDP协议的报首
UDP协议的数据包体积较小,因为UDP协议的数据包报首较小。
image

(1)源端口号
image
源端口号指的是发送数据的主机进程对应的端口号,可以看到是16bit,也就是2个字节,该成员是可选的,如果不指定端口,则会填充为0。

(2)目标端口
image
目标端口指的是接收数据的主机的进程对应的端口号,可以看到是16bit,也就是2个字节,这里注意:并不是每台主机的进程的端口号都相同,也就是端口号是本地有效的,换个说法,两台主机中相同的端口号不一定指的是相同进程!

(3)包总长度
image
包长度指的是UDP报首长度 + 数据内容长度之和,而包长度占16bit,也就是包长度的最大值是65535字节,可以看到UDP数据报报首的长度是8字节,而UDP协议接收到的网络层转发过来的数据包中还存在IP协议的报首,而IP协议的报首固定为20字节(不使用可选选项的情况下),所以UDP携带的数据内容的最大值是65507字节。

(4)校验和
image
校验和是用于检测UDP用户数据包传输过程中是否出错,如果出错,则直接丢弃,如果源主机不想生成校验和,可以填充把校验和的16bit设置为0。

UDP的接口说明
在Linux系统中提供了UDP协议的描述和UDP协议相关的接口说明,建议通过man手册的第7章了解UDP协议的使用规则:
image
想要指定UDP协议进行通信,需要先调用socket函数创建UDP套接字,如果只打算向对方主机发送数据包,则可以调用sendto()函数或者sendmsg()函数,注意要把对方主机的有效IP地址作为参数。
如果打算接收对方主机发送的数据包,则需要提前调用bind()函数把UDP套接字和本地主机地址进行绑定,然后调用recvfrom()函数来接收数据包。

(1)socket()函数
Linux系统的思想是“一切皆文件”,所以Linux系统把文件分为7类、其中有一种文件是为了实现在不同主机的进程间进行通信而设计出来的,也被称为套接字文件(标识符是s,指的是socket的缩写)。
如果双方主机打算通信,则双发都需要在本地创建套接字文件,Linux系统中提供了一个名称叫做socket()的函数接口,使用规则如下:
image

1)函数参数
第一个参数:domain指的是要使用的协议族,因为协议族决定了通信地址类型,常用的是AF_INET协议族,这样就采用IPV4地址进行通信。
第二个参数:type指的是套接字类型,一般常用的类型有SOCK_STREAM 和 SOCK_DGRAM。
SOCK_STREAM类型 :提供有序、可靠、双向、基于连接的字节流,也就是基于TCP协议。
SOCK_DGRAM类型 :提供不可靠、无连接、固定长度数据的数据报,是基于UDP协议。
第三个参数:protocol指的是和套接字类型相关的协议,一般设置为0即可,这样系统会自动根据套接字类型选择合适协议。

2)返回结果
socket函数创建套接字成功,则会返回对应的文件描述符,如果套接字创建失败,则返回-1。

(2)bind()函数
双方主机通信时一般采用双向通信,也就是主机在发送数据的同时也可能等待接收对方的数据,如果打算接收对方主机的数据,则需要把创建的套接字和本地地址以及端口进行绑定,Linux系统提供了一个名称叫做bind()的函数接口,该接口的使用规则如下:
image

  1. 函数参数
    第一个参数:sockfd指的是创建成功的套接字返回的文件描述符,就是socket()函数返回值。
    第二个参数:addr指的是要绑定的本地地址的结构体指针,但是实际addr的结构体类型是和协议族有关的,比如采用AF_INET协议族,则需要阅读man手册的第7章了解地址结构。
    image
    可以看到,采用IPV4协议需要指定IP地址和端口号,对应的结构体类型是struct sockaddr_in,该结构体有3个成员:
    成员sin_family :指的是协议族,需要设置为AF_INET
    成员sin_port :指的是端口号,必须转换为网络字节序,需调用htons()函数实现转换
    成员sin_addr :指的是IP地址,必须转换为网络字节序,需调用inet_addr()实现转换
    Linux系统提供很多函数实现把主机字节序转换为网络字节序也提供了函数接口实现把网络字节序转换为主机字节序,比如htons()、htonl()、ntohs()、ntohl()、inet_addr()等等。
    image
    image

2)返回结果
bind()函数调用成功则返回0,bind()函数调用失败则返回-1,建议绑定地址后进行错误处理。

(3)sendto()函数
如果打算向对方主机发送数据,则双方主机都需要创建UDP套接字,发送端可以调用sendto()函数发送数据,函数使用规则如下:
image

1)函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是要发送的消息对应的缓冲区的地址,const void * 表示地址类型任意。
第三个参数:len指的是要发送的消息的大小,以字节为单位,不要超过UDP的数据包大小!
第四个参数:flags指的是发送消息的标志,一般设置为0,和系统调用write()函数作用类似。
第五个参数:dest_addr指的是目标主机的IP地址,因为UDP是无连接的,需要指定接收端。
第六个参数:addrlen指的是目标主机的IP地址的大小,一般通过sizeof()进行计算即可得到。

2)返回结果
sendto()函数发送消息成功,则返回发送的消息的字节个数,sendto()函数调用失败则返回-1。

(4)recvfrom()函数
接收方主机想要得到发送过来的数据包,则需要调用recvfrom()函数实现,函数的使用规则如下:
image

1)函数参数
第一个参数:sockfd指的是创建的套接字对应的文件描述符,其实是socket()函数的返回值。
第二个参数:buf指的是接收的消息要存储的缓冲区的地址,void * 表示地址类型可以任意。
第三个参数:len指的是要接收的消息的大小,以字节为单位,大小可以结合实际情况设置。
第四个参数:flags指的是接收消息的标志,一般设置为0,和系统调用read()函数作用类似。
第五个参数:src_addr指的是源主机的IP地址,如果对发送方不感兴趣,可以设置为NULL。
第六个参数:addrlen指的是源主机的IP地址的大小,注意是一个指针变量,需要传地址!!!

2)返回结果
recvfrom()函数接收消息成功返回接收的消息的字节个数,recvfrom()函数调用失败则返回-1。

客户端:

点击查看代码

/**************************************************************************************************************************************
*
*   字节序:数据以字节流的方式进行传输,底层都是采用二进制,字节流的顺序是由架构决定的,现在假设使用X86架构,是采用小端存储
*	网络字节序:
*	本地字节序:
*
*	大端存储:低地址存储高字节   当数据超过1个字节的时候才需要区分大端还是小端  假设int型 0x12345678   0x12  | 0x34  | 0x56 |  0x78
*	小端存储:低地址存储低字节   当数据超过1个字节的时候才需要区分大端还是小端  假设int型 0x12345678   0x78  | 0x56  | 0x34 |  0x12
*
*	设备A采用X86架构,所以设备A采用小端存储  待发送的数据 :0x12345678
*   设备B采用ARM架构,所以设备B采用大端存储  待接收的数据 :?
*
*   为了统一发送数据的格式,所以互联网传输的数据统一采用大端方式,为了方便开发,linux系统提供了转换的接口:htonl、htons、ntohl、ntohs
*
*   h :host  主机/本地
*   to:      转换
*   n :net   网络
*   l :long  长整型
*   s :short 短整型
*
*   htons:把本地字节序的一个短整型转换为网络字节序
*   htonl:把本地字节序的一个长整型转换为网络字节序
*   ntohs:把网络字节序的一个短整型转换为本地字节序
*   ntohl:把网络字节序的一个长整型转换为本地字节序
* ***********************************************************************************************************************************/
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>

// 运行可执行文件  ./xxx  端口  目标地址

int main(int argc,char *argv[])
{
	//检查参数有效性
	if(argc != 3){
		fprintf(stderr, "argc error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}

	//UDP协议使用下面的数据类型来存储目标IP和目标端口
	struct sockaddr_in  dest_addr;

	//1.创建UDP套接字             
	int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (udp_socket == -1)
	{
		fprintf(stderr, "udp socket error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}

	//2.向目标主机发送消息,需要设置目标端口和目标地址
	char buf[128] = {0};

	dest_addr.sin_family 		= AF_INET; 						//协议族,是固定的
	dest_addr.sin_port   		= htons(atoi(argv[1]));			//目标端口,必须转换为网络字节序
	dest_addr.sin_addr.s_addr = inet_addr(argv[2]);			//目标地址 "192.168.64.xxx"  已经转换为网络字节序

	while(1)
	{
		scanf("%s",buf);
		sendto(udp_socket,buf,strlen(buf),0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
		bzero(buf,sizeof(buf));
	}
	

	return 0;

}



服务器:

点击查看代码
/**************************************************************************************************************************************
*
*   字节序:数据以字节流的方式进行传输,底层都是采用二进制,字节流的顺序是由架构决定的,现在假设使用X86架构,是采用小端存储
*	网络字节序:
*	本地字节序:
*
*	大端存储:低地址存储高字节   当数据超过1个字节的时候才需要区分大端还是小端  假设int型 0x12345678   0x12  | 0x34  | 0x56 |  0x78
*	小端存储:低地址存储低字节   当数据超过1个字节的时候才需要区分大端还是小端  假设int型 0x12345678   0x78  | 0x56  | 0x34 |  0x12
*
*	设备A采用X86架构,所以设备A采用小端存储  待发送的数据 :0x12345678
*   设备B采用ARM架构,所以设备B采用大端存储  待接收的数据 :?
*
*   为了统一发送数据的格式,所以互联网传输的数据统一采用大端方式,为了方便开发,linux系统提供了转换的接口:htonl、htons、ntohl、ntohs
*
*   h :host  主机/本地
*   to:      转换
*   n :net   网络
*   l :long  长整型
*   s :short 短整型
*
*   htons:把本地字节序的一个短整型转换为网络字节序
*   htonl:把本地字节序的一个长整型转换为网络字节序
*   ntohs:把网络字节序的一个短整型转换为本地字节序
*   ntohl:把网络字节序的一个长整型转换为本地字节序
* ***********************************************************************************************************************************/
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char *argv[])
{
	//检查参数有效性
	if(argc != 3){
		fprintf(stderr, "argc error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}
	

	//1.创建UDP套接字             
	int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (udp_socket == -1)
	{
		fprintf(stderr, "udp socket error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}

	//2.需要先绑定主机的端口和地址
	struct sockaddr_in  host_addr;

	host_addr.sin_family 		= AF_INET; 						//协议族,是固定的
	host_addr.sin_port   		= htons(atoi(argv[1]));			//目标端口,必须转换为网络字节序
	host_addr.sin_addr.s_addr = inet_addr(argv[2]);				//目标地址 "192.168.64.xxx"  已经转换为网络字节序

	bind(udp_socket,(struct sockaddr *)&host_addr, sizeof(host_addr));

	//3.循环调用recvfrom准备接收数据
	char buf[128] = {0};
	while(1)
	{
		recvfrom(udp_socket,buf,sizeof(buf), 0 ,NULL,NULL);
		printf("recv data is [%s]\n",buf);
		bzero(buf,sizeof(buf));
	}
	
	return 0;

}

posted @ 2025-08-17 20:49  Lè_Sage  阅读(212)  评论(0)    收藏  举报