本帖最后由 WildboarG 于 2024-9-13 17:09 编辑
本帖最后由 WildboarG 于 2024-9-13 17:09 编辑
本帖最后由 WildboarG 于 2024-9-13 16:36 编辑
本帖最后由 WildboarG 于 2024-9-13 16:34 编辑
本帖最后由 WildboarG 于 2024-9-13 16:10 编辑
本帖最后由 WildboarG 于 2024-9-13 14:19 编辑
一、UDP的概述(User Datagram Protocol,用户数据报协议)
UDP是传输层的协议,功能即为在IP的数据报服务之上增加了最基本的服务(加入了端口的概念):复用和分用以及差错检测。
UDP提供不可靠服务,具有TCP所没有的优势:
-
UDP无连接,时间上不存在建立连接需要的时延。空间上,TCP需要在端系统中维护连接状态,需要一定的开销。此连接装入包括接收和发送缓存,拥塞控制参数和序号与确认号的参数。UCP不维护连接状态,也不跟踪这些参数,开销小。空间和时间上都具有优势。举个例子:
DNS如果运行在TCP之上而不是UDP,那么DNS的速度将会慢很多。HTTP使用TCP而不是UDP,是因为对于基于文本数据的Web网页来说,可靠性很重要。同一种专用应用服务器在支持UDP时,一定能支持更多的活动客户机。
-
分组首部开销小,TCP首部20字节,UDP首部只有8字节。
-
UDP没有拥塞控制,应用层能够更好的控制要发送的数据和发送时间,网络中的拥塞控制也不会影响主机的发送速率。某些实时应用要求以稳定的速度发送,能容 忍一些数据的丢失,但是不能允许有较大的时延(比如实时视频,直播等)
-
UDP提供尽最大努力的交付,不保证可靠交付。所有维护传输可靠性的工作需要用户在应用层来完成。没有TCP的确认机制、重传机制。如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息
-
UDP是面向报文的,对应用层交下来的报文,添加首部后直接乡下交付为IP层,既不合并,也不拆分,保留这些报文的边界。对IP层交上来UDP用户数据报,在去除首部后就原封不动地交付给上层应用进程,报文不可分割,是UDP数据报处理的最小单位。正是因为这样,UDP显得不够灵活,不能控制读写数据的次数和数量。比如我们要发送100个字节的报文,我们调用一次sendto函数就会发送100字节,对端也需要用recvfrom函数一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。
-
UDP常用一次性传输比较少量数据的网络应用,如DNS,SNMP等,因为对于这些应用,若是采用TCP,为连接的创建,维护和拆除带来不小的开销。UDP也常用于多媒体应用(如IP电话,实时视频会议,流媒体等)数据的可靠传输对他们而言并不重要,TCP的拥塞控制会使他们有较大的延迟,也是不可容忍的
二、UDP的首部格式
UDP数据报分为首部和用户数据部分,整个UDP数据报作为IP数据报的数据部分封装在IP数据报中,UDP数据报文结构如图所示:
UDP首部有8个字节,由4个字段构成,每个字段都是两个字节,
1.源端口: 源端口号,(需要对方回信时选用,不需要时全部置0)。
2.目的端口:目的端口号,发给谁(在终点交付报文的时候需要用到)。
3.长度:UDP的数据报的长度(包括首部和数据)其最小值为8(只有首部)
4.校验和:检测UDP数据报在传输中是否有错,有错则丢弃。
该字段是可选的,当源主机不想计算校验和,则直接令该字段全为0.
当传输层从IP层收到UDP数据报时,就根据首部中的目的端口,把UDP数据报通过相应的端口,上交给应用进程。
如果接收方UDP发现收到的报文中的目的端口号不正确(不存在对应端口号的应用进程0,),就丢弃该报文,并由ICMP发送“端口不可达”差错报文给对方。
三、UDP的校验
在计算校验和的时候,需要在UDP数据报之前增加12字节的伪首部,伪首部并不是UDP真正的首部。只是在计算校验和,临时添加在UDP数据报的前面,发送时候并没有伪首部,得到一个临时的UDP数据报。校验和就是按照这个临时的UDP数据报计算的。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验。
UDP校验和的计算方法和IP数据报首部校验和的计算方法相似,都使用二进制反码运算求和再取反,但不同的是:IP数据报的校验和之检验IP数据报和首部,但UDP的校验和是把首部和数据部分一起校验。
发送方,
- 首先是把全零放入校验和字段并且添加伪首部,
- 然后把UDP数据报看成是由许多16位的子串连接起来,若UDP数据报的数据部分不是偶数个字节,则要在数据部分末尾增加一个全零字节(此字节不发送)
- 接下来就按照二进制反码计算出这些16位字的和。将此和的二进制反码写入校验和字段。在接收方,把收到得UDP数据报加上伪首部(如果不为偶数个字节,还需要补上全零字节)后,按二进制反码计算出这些16位字的和。当无差错时其结果全为1,。否则就表明有差错出现,接收方应该丢弃这个UDP数据报。
- 值得注意的是伪首部种的内容不计入UDP长度。以下图为例
UDP首部 8个字节 DDP数据7个字节,共15字节(补零不计入UDP数据长度)
注意:
1.校验时,若UDP数据报部分的长度不是偶数个字节,则需要填入一个全0字节,但是次字节和伪首部一样,是不发送的。
2.如果UDP校验和校验出UDP数据报是错误的,可以丢弃,也可以交付上层,但是要附上错误报告,告诉上层这是错误的数据报。
3.通过伪首部,不仅可以检查源端口号,目的端口号和UDP用户数据报的数据部分,还可以检查IP数据报的源IP地址和目的地址。
这种差错检验的检错能力不强,不但简单,速度快。
UDP与TCP一样也是传输层协议,在网络数据交换中,不论是以太网还是wifi,依旧是一帧完整包中的一部分,数据进入到传输层会被添加或拆除相应的头部。
基于udp的应用层协议
DNS(域名服务):DNS协议用于将域名转换为IP地址。它使用UDP端口53,因为DNS查询通常很短,而UDP是轻量级的,可以快速处理这些查询。
TFTP(简单文件传输协议):TFTP是一个简单的文件传输协议,它使用UDP端口69。尽管TFTP不如FTP功能强大,但由于其简单性和易于实现,它在某些需要快速和简单文件传输的场合仍然很有用。
SNMP(简单网络管理协议):SNMP用于网络设备的监控和管理。它通常使用UDP端口161来接收请求,而trap信息则使用UDP端口162。
NTP(网络时间协议):NTP用于同步计算机时钟到某个参考时间源,通常是一个可靠的时间服务器。它使用UDP端口123。
VoIP(语音传输协议):一些VoIP(如RTP)使用UDP进行音频数据的实时传输。UDP的无连接和快速传输特性使其适用于音频和视频流等实时通信。
DHCP(动态主机配置协议):DHCP在客户端和DHCP服务器之间使用UDP进行通信,以自动分配IP地址和其他网络设置。
QUIC(快速UDP互联网连接):虽然QUIC是基于UDP的,但它本身更像是一个传输层协议,为应用层提供了类似TCP的可靠传输服务,同时保持了UDP的低延迟特性。
这些协议之所以选择UDP而不是TCP,通常是因为它们需要低延迟、简单性、或对数据传输的实时性要求较高,而可以容忍一定程度的数据丢失或不按顺序到达。需要注意的是,尽管UDP为这些应用提供了所需的特性,但在实际应用中,它们可能还需要在应用层实现额外的机制来确保数据的可靠性或完整性
与TCP相同,在WB2的sdk中都是使用socket接口来控制UDPPCB,只需在创建套接字时候指定流为 SOCK_DGRAM
当然UDP也需要初始化,连接,发送,接受,和关闭
官方也给了相应的例子程序
- 作为客户端连接别的UDP服务(单播)
- 开启udp服务,等待客户端连接
- 广播
- 组播
再补充以下广播与组播的概念:
广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。
广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。
其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。
多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。
在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通 信。
相对于传统的一对一的单播,多播具有如下的优点:
1、具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。
2、服务器的总带宽不受客户端带宽的限制。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。
3、与单播一样,多播是允许在广域网即Internet上进行传输的,而广播仅仅在同一局域网上才能进行。
组播的缺点:
1、多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。
2、多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。
3、多播的应用主要有网上视频、网上会议等。
2、广域网的多播
多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即224.0.0.0至239.255.255.255之间的IP地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:
1、局部多播地址:在224.0.0.0~224.0.0.255之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。
2、预留多播地址:在224.0.1.0~238.255.255.255之间,可用于全球范围(如Internet)或网络协议。
3、管理权限多播地址:在239.0.0.0~239.255.255.255之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。
多播的程序设计使用setsockopt()函数和getsockopt()函数来实现
作为客户端连接UDP服务:
用串口助手或者脚本创建一个udp服务端,烧录官方给的clinet例程。
验证udp客户端 效果图见评论区
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <string.h>
#include <blog.h>
#include <aos/yloop.h>
#include <aos/kernel.h>
#include <lwip/sockets.h>
#include <lwip/tcpip.h>
#include <wifi_mgmr_ext.h>
#include <cli.h>
#include <hal_wifi.h>
#include <lwip/init.h>
#include "udp_client.h"
#define ROUTER_SSID "CU_e6f6"
#define ROUTER_PWD "c9g3geyu"
#define UDP_SERVER_IP "192.168.1.8"
#define UDP_SERVER_PORT 12345
static wifi_conf_t conf = {
.country_code = "CN",
};
/**
* @brief wifi_sta_connect
* wifi station mode connect start
* @param ssid
* @param password
*/
static void wifi_sta_connect(char* ssid, char* password)
{
wifi_interface_t wifi_interface;
wifi_interface = wifi_mgmr_sta_enable();
wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0);
}
/**
* @brief udp_client_task
*
* @param arg
*/
static void udp_client_task(void* arg)
{
blog_info("udp client task run\r\n");
int socketfd;
int ret = 0;
char* tcp_buff = pvPortMalloc(512);
memset(tcp_buff, 0, 512);
socketfd = udp_client_init(UDP_SERVER_IP, UDP_SERVER_PORT);
if (!udp_client_connect(socketfd)) {
blog_info("%s:udp client connect OK\r\n", __func__);
}
else goto __exit;
if (udp_client_send(socketfd, "hell udp server")<0) {
printf("udp client send fail\r\n");
goto __exit;
}
else
blog_info("udp client send OK\r\n");
while (1) {
ret = udp_client_receive(socketfd, tcp_buff);
if (ret>0) {
blog_info("%s:udp receive data:%s \r\n", __func__, tcp_buff);
if (strstr(tcp_buff, "close")) goto __exit;
memset(tcp_buff, 0, 512);
}
vTaskDelay(100/portTICK_PERIOD_MS);
}
__exit:
vPortFree(tcp_buff);
udp_client_send(socketfd, "client close");
udp_client_deinit(socketfd);
vTaskDelete(NULL);
}
/**
* @brief event_cb_wifi_event
* wifi connet ap event Callback function
* @param event
* @param private_data
*/
static void event_cb_wifi_event(input_event_t* event, void* private_data)
{
static char* ssid;
static char* password;
switch (event->code)
{
case CODE_WIFI_ON_INIT_DONE:
{
printf("[APP] [EVT] INIT DONE %lld\r\n", aos_now_ms());
wifi_mgmr_start_background(&conf);
}
break;
case CODE_WIFI_ON_MGMR_DONE:
{
printf("[APP] [EVT] MGMR DONE %lld\r\n", aos_now_ms());
//_connect_wifi();
wifi_sta_connect(ROUTER_SSID, ROUTER_PWD);
}
break;
case CODE_WIFI_ON_SCAN_DONE:
{
printf("[APP] [EVT] SCAN Done %lld\r\n", aos_now_ms());
// wifi_mgmr_cli_scanlist();
}
break;
case CODE_WIFI_ON_DISCONNECT:
{
printf("[APP] [EVT] disconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTING:
{
printf("[APP] [EVT] Connecting %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_CMD_RECONNECT:
{
printf("[APP] [EVT] Reconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTED:
{
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_PRE_GOT_IP:
{
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_GOT_IP:
{
printf("[APP] [EVT] GOT IP %lld\r\n", aos_now_ms());
printf("[SYS] Memory left is %d Bytes\r\n", xPortGetFreeHeapSize());
//WiFi connection succeeded, create UDP client task
xTaskCreate(udp_client_task, (char*)"udp_client_task", 1024*2, NULL, 16, NULL);
}
break;
case CODE_WIFI_ON_PROV_SSID:
{
printf("[APP] [EVT] [PROV] [SSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (ssid)
{
vPortFree(ssid);
ssid = NULL;
}
ssid = (char*)event->value;
}
break;
case CODE_WIFI_ON_PROV_BSSID:
{
printf("[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (event->value)
{
vPortFree((void*)event->value);
}
}
break;
case CODE_WIFI_ON_PROV_PASSWD:
{
printf("[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n", aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (password)
{
vPortFree(password);
password = NULL;
}
password = (char*)event->value;
}
break;
case CODE_WIFI_ON_PROV_CONNECT:
{
printf("[APP] [EVT] [PROV] [CONNECT] %lld\r\n", aos_now_ms());
printf("connecting to %s:%s...\r\n", ssid, password);
wifi_sta_connect(ssid, password);
}
break;
case CODE_WIFI_ON_PROV_DISCONNECT:
{
printf("[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n", aos_now_ms());
}
break;
default:
{
printf("[APP] [EVT] Unknown code %u, %lld\r\n", event->code, aos_now_ms());
/*nothing*/
}
}
}
static void proc_main_entry(void* pvParameters)
{
aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL);
hal_wifi_start_firmware_task();
aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
vTaskDelete(NULL);
}
void main()
{
puts("[OS] Starting TCP/IP Stack...\r\n");
tcpip_init(NULL, NULL);
puts("[OS] proc_main_entry task...\r\n");
xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL);
}
用wb2启动udp服务器,用脚本作为客户端连接(效果图见评论区)
/**
* @file main.c
* @author your name ([email]you@domain.com[/email])
* @brief
* @version 0.1
* @date 2022-10-13
*
* @copyright Copyright (c) 2022
*
*/
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <string.h>
#include <blog.h>
#include <aos/yloop.h>
#include <aos/kernel.h>
#include <lwip/sockets.h>
#include <lwip/tcpip.h>
#include <wifi_mgmr_ext.h>
#include <cli.h>
#include <hal_wifi.h>
#include <lwip/init.h>
#include "udp_server.h"
#define ROUTER_SSID "CU_e6f6"
#define ROUTER_PWD "c9g3geyu"
#define UDP_SERVER_PORT 7878
static wifi_conf_t conf = {
.country_code = "CN",
};
/**
* @brief wifi_sta_connect
* wifi station mode connect start
* @param ssid
* @param password
*/
static void wifi_sta_connect(char* ssid, char* password)
{
wifi_interface_t wifi_interface;
wifi_interface = wifi_mgmr_sta_enable();
wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0);
}
/**
* @brief udp_server_task
*
* @param arg
*/
static void udp_server_task(void* arg)
{
int socket_fd;
int ret = 0;
socket_fd = udp_server_init(NULL, UDP_SERVER_PORT);
struct sockaddr_in u_sockaddr;
int socke_len = sizeof(struct sockaddr_in);
char* udp_buf = pvPortMalloc(512);
while (1) {
memset(udp_buf, 0, 512);
ret = recvfrom(socket_fd, udp_buf, 512, MSG_DONTWAIT, (struct sockaddr*)&u_sockaddr, (socklen_t*)&socke_len);
if (ret>0) {
blog_info("%s:%s\r\n", inet_ntoa(u_sockaddr.sin_addr.s_addr), udp_buf);
ret = sendto(socket_fd, udp_buf, strlen(udp_buf), 0, (struct sockaddr*)&u_sockaddr, socke_len);
if (strstr(udp_buf, "close")!=NULL) {
ret = sendto(socket_fd, "server close connect", strlen("server close connect"), 0, (struct sockaddr*)&u_sockaddr, socke_len);
goto __exit;
}
}
vTaskDelay(50/portTICK_PERIOD_MS);
}
__exit:
vPortFree(udp_buf);
udp_server_deinit();
vTaskDelete(NULL);
}
/**
* @brief event_cb_wifi_event
* wifi connet ap event Callback function
* @param event
* @param private_data
*/
static void event_cb_wifi_event(input_event_t* event, void* private_data)
{
static char* ssid;
static char* password;
switch (event->code)
{
case CODE_WIFI_ON_INIT_DONE:
{
printf("[APP] [EVT] INIT DONE %lld\r\n", aos_now_ms());
wifi_mgmr_start_background(&conf);
}
break;
case CODE_WIFI_ON_MGMR_DONE:
{
printf("[APP] [EVT] MGMR DONE %lld\r\n", aos_now_ms());
//_connect_wifi();
wifi_sta_connect(ROUTER_SSID, ROUTER_PWD);
}
break;
case CODE_WIFI_ON_SCAN_DONE:
{
printf("[APP] [EVT] SCAN Done %lld\r\n", aos_now_ms());
// wifi_mgmr_cli_scanlist();
}
break;
case CODE_WIFI_ON_DISCONNECT:
{
printf("[APP] [EVT] disconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTING:
{
printf("[APP] [EVT] Connecting %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_CMD_RECONNECT:
{
printf("[APP] [EVT] Reconnect %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_CONNECTED:
{
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_PRE_GOT_IP:
{
printf("[APP] [EVT] connected %lld\r\n", aos_now_ms());
}
break;
case CODE_WIFI_ON_GOT_IP:
{
printf("[APP] [EVT] GOT IP %lld\r\n", aos_now_ms());
printf("[SYS] Memory left is %d Bytes\r\n", xPortGetFreeHeapSize());
// wifi connection succeeded, create udp server task
xTaskCreate(udp_server_task, "udp_server_task", 2048, NULL, 16, NULL);
}
break;
case CODE_WIFI_ON_PROV_SSID:
{
printf("[APP] [EVT] [PROV] [SSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (ssid)
{
vPortFree(ssid);
ssid = NULL;
}
ssid = (char*)event->value;
}
break;
case CODE_WIFI_ON_PROV_BSSID:
{
printf("[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n",
aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (event->value)
{
vPortFree((void*)event->value);
}
}
break;
case CODE_WIFI_ON_PROV_PASSWD:
{
printf("[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n", aos_now_ms(),
event->value ? (const char*)event->value : "UNKNOWN");
if (password)
{
vPortFree(password);
password = NULL;
}
password = (char*)event->value;
}
break;
case CODE_WIFI_ON_PROV_CONNECT:
{
printf("[APP] [EVT] [PROV] [CONNECT] %lld\r\n", aos_now_ms());
printf("connecting to %s:%s...\r\n", ssid, password);
wifi_sta_connect(ssid, password);
}
break;
case CODE_WIFI_ON_PROV_DISCONNECT:
{
printf("[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n", aos_now_ms());
}
break;
default:
{
printf("[APP] [EVT] Unknown code %u, %lld\r\n", event->code, aos_now_ms());
/*nothing*/
}
}
}
static void proc_main_entry(void* pvParameters)
{
aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL);
hal_wifi_start_firmware_task();
aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0);
vTaskDelete(NULL);
}
void main()
{
puts("[OS] Starting TCP/IP Stack...\r\n");
tcpip_init(NULL, NULL);
puts("[OS] proc_main_entry task...\r\n");
xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL);
}
广播,在广播例程中由于广播地址设置的本机IP(也就是wb2获取的ip),所以只能自己接受,在尝试把广播地址设为192.168.1.255 或255.255.255.255 时,发现并没有用,有人知道怎么个事吗?一开始怀疑路由器拒绝广播,后来写了个测试脚本发现,测试例程序中广播接受仍有效。搞不懂,坐等评论区大佬解惑。