单芯片解决方案,开启全新体验——W55MH32 高性能以太网单片机
W55MH32是WIZnet重磅推出的高性能以太网单片机,它为用户带来前所未有的集成化体验。这颗芯片将强大的组件集于一身,具体来说,一颗W55MH32内置高性能Arm? Cortex-M3核心,其主频最高可达216MHz;配备1024KB FLASH与96KB SRAM,满足存储与数据处理需求;集成TOE引擎,包含WIZnet全硬件TCP/IP协议栈、内置MAC以及PHY,拥有独立的32KB以太网收发缓存,可供8个独立硬件socket使用。如此配置,真正实现了All-in-One解决方案,为开发者提供极大便利。
在封装规格上,W55MH32提供了两种选择:QFN100和QFN68。
W55MH32L采用QFN100封装版本,尺寸为12x12mm,其资源丰富,专为各种复杂工控场景设计。它拥有66个GPIO、3个ADC、12通道DMA、17个定时器、2个I2C、5个串口、2个SPI接口(其中1个带I2S接口复用)、1个CAN、1个USB2.0以及1个SDIO接口。如此丰富的外设资源,能够轻松应对工业控制中多样化的连接需求,无论是与各类传感器、执行器的通信,还是对复杂工业协议的支持,都能游刃有余,成为复杂工控领域的理想选择。同系列还有QFN68封装的W55MH32Q版本,该版本体积更小,仅为8x8mm,成本低,适合集成度高的网关模组等场景,软件使用方法一致。更多信息和资料请进入http://www.w5500.com/网站或者私信获取。
此外,本W55MH32支持硬件加密算法单元,WIZnet还推出TOE+SSL应用,涵盖TCP SSL、HTTP SSL以及 MQTT SSL等,为网络通信安全再添保障。
为助力开发者快速上手与深入开发,基于W55MH32L这颗芯片,WIZnet精心打造了配套开发板。开发板集成WIZ-Link芯片,借助一根USB C口数据线,就能轻松实现调试、下载以及串口打印日志等功能。开发板将所有外设全部引出,拓展功能也大幅提升,便于开发者全面评估芯片性能。
若您想获取芯片和开发板的更多详细信息,包括产品特性、技术参数以及价格等,欢迎访问官方网页:http://www.w5500.com/,我们期待与您共同探索W55MH32的无限可能。
第十三章 W55MH32 UPnP端口转发示例
本篇文章,我们将详细介绍如何在W55MH32芯片上面实现UPnP协议。使用W55MH32的TOE引擎,我们只需进行简单的socket编程及寄存器读写,便可轻松实现以太网应用。接下来我们通过实战例程,为大家讲解如何使用TOE引擎实现UPnP协议的端口转发功能。
该例程用到的其他网络协议,例如DHCP,请参考相关章节。有关W55MH32的初始化过程,请参考Network Install章节,这里将不再赘述。
1 UPnP协议简介
UPnP(Universal Plug and Play)协议是一种支持设备在局域网中实现自动发现和通信的网络协议。其端口转发功能由IGD Profile提供,允许局域网设备动态请求路由器为其开放指定的端口,以实现外部设备访问内部服务。这种功能消除了手动配置端口转发的复杂性,特别适用于需要穿透NAT(网络地址转换)环境的应用场景。
IGD(Internet Gateway Device,互联网网关设备)是UPnP(Universal Plug and Play)协议的一部分,主要用于管理网络中的网关设备(如路由器)的服务和资源。IGD扩展定义了一套标准接口,允许局域网设备与网关设备通信,动态配置网络设置,例如端口转发、带宽管理和连接状态查询等。
2 UPnP协议特点
自动化配置:无需用户手动设置,减少了配置错误的风险。
动态灵活:端口映射规则可以根据需求动态添加或删除。
设备友好:支持即插即用,简化了设备的联网和部署过程。
跨设备兼容:UPnP基于标准化协议,广泛支持各种设备和平台。
3 UPnP应用场景
通过UPnP端口转发功能,我们可以使用W55MH32实现以下功能:
远程访问:将外部请求转发到局域网设备(如NAS、监控摄像头),实现外部远程访问内部设备。
远程控制:外部设备通过UPnP转换的端口,可以实现远程控制局域网内部设备(智能门锁、灯光控制器)。
4 UPnP设置端口转发的工作流程
设备发现:W55MH32通过SSDP(Simple Service Discovery Protocol)向局域网中发送组播请求(HTTP M-SEARCH报文),搜索支持IGD的网关设备。
获取服务描述:W55MH32访问网关设备(路由器)获取服务描述文件,了解支持的服务和接口。
订阅IGD事件:通过事件订阅,W55MH32可以在不主动轮询的情况下,接收实时通知。
调用服务接口:使用UPnP的SOAT消息调用IGD提供的端口映射接口。
数据交互测试:外部通过访问映射的端口及路由器地址和局域网内部设备进行通信。
5报文讲解
设备搜索
上文我们提到,设备搜索时使用SSDP协议,SSDP(Simple Service Discovery Protocol)是 UPnP协议中的关键协议,用于设备发现和服务发布。它通过HTTP over UDP的形式在局域网内广播和接收报文,采用多播地址 239.255.255.250和端口 1900。
SSDP报文主要分为以下几类:
NOTIFY消息(设备主动广播通知):用于设备向网络通告自己的存在或离线状态。
M-SEARCH消息(客户端主动搜索):客户端发送搜索请求以发现设备或服务。
HTTP/1.1响应消息(设备对 M-SEARCH的响应):设备对搜索请求的响应,提供设备描述文件的位置及服务信息。
SSDP报文基于HTTP协议,有固定的格式,主要包括以下字段:
HOST:目标地址和端口,固定为 239.255.255.250:1900。
MAN:用于标识搜索消息,固定为 "ssdp:discover"(仅在 M-SEARCH中使用)。
MX:最大响应时间,指定设备在多长时间内响应(单位:秒)。
ST:搜索目标,标识要查找的设备类型或服务类型。
NT:通知类型,表示设备或服务的类型(在 NOTIFY消息中使用)。
USN:唯一服务名称,设备或服务的唯一标识符。
LOCATION:设备描述文件的 URL,包含设备的详细信息。
CACHE-CONTROL:设备信息的缓存时间,表示在多长时间内有效。
M-SEARCH请求报文实例:
M-SEARCH * HTTP/1.1 Host:239.255.255.250:1900 ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1 Man:"ssdp:discover" MX:3
字段解析:
M-SEARCH * HTTP/1.1:表明是一个搜索请求。
Host:多播地址和端口。
ST:搜索目标类型,这里是IGD设备。
MX:最大响应事件,设备需要在3秒内返回响应。
Man:搜索请求类型,固定。
M-SEARCH响应报文实例:
HTTP/1.1 200 OK CACHE-CONTROL: max-age=60 DATE: Tue, 07 Jan 2025 06:43:49 GMT EXT: LOCATION: http://192.168.100.1:1900/igd.xml SERVER: vxWorks/5.5 UPnP/1.0 TL-WR886N/6.0 ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1 USN: uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9::urn:schemas-upnp-org:device:InternetGatewayDevice:1
HTTP/1.1 200 OK:表示响应成功。
CACHE-CONTROL:响应有效时间为60秒。
DATE:响应的时间戳。
EXT:保留字段,目前为空。
LOCATION:设备描述文件的URL。
SERVER:设备的操作系统,UPnP版本和设备名称。
ST:搜索目标类型,和请求中的ST字段一致。
USN:唯一设备标识符。
获取设备标识符
这一步会通过HTTP GET方式去请求xml文件,有关HTTP GET报文以及HTTP响应报文这里不过多讲解,有兴趣的可以参考 HTTP Client章节。
请求示例:
GET /igd.xml HTTP/1.1 Accept: text/xml, application/xml User-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) Host: 192.168.100.1:1900 Connection: Keep-Alive Cache-Control: no-cache Pragma: no-cache
响应示例:
HTTP/1.1 200 OK Content-Type: text/xml;charset=UTF-8 Content-Length: 2580 Connection: close Cache-control: no-cache ?xml version="1.0"??> 1/major?> 0/minor?> /specVersion?> urn:schemas-upnp-org:device:InternetGatewayDevice:1/deviceType?> http://192.168.100.1:80 /presentationURL?> Wireless N Router TL-WR886N/friendlyName?> TP-LINK/manufacturer?> http://www.tp-link.com.cn/manufacturerURL?> TL-WR886N 6.0/modelDescription?> TL-WR886N/modelName?> 6.0/modelNumber?> uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9/UDN?> 123456789001/UPC?> urn:schemas-upnp-org:service:Layer3Forwarding:1/serviceType?> urn:upnp-org:serviceId:L3Forwarding1/serviceId?> /l3f/controlURL?> /l3f/eventSubURL?> /l3f.xml/SCPDURL?> /service?> /serviceList?> urn:schemas-upnp-org:device:WANDevice:1/deviceType?> WAN Device/friendlyName?> TP-LINK/manufacturer?> http://www.tp-link.com.cn/manufacturerURL?> WAN Device/modelDescription?> WAN Device/modelName?> 1.0/modelNumber?> /modelURL?> 12345678900001/serialNumber?> uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9/UDN?> 123456789001/UPC?> urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1/serviceType?> urn:upnp-org:serviceId:WANCommonInterfaceConfig/serviceId?> /ifc/controlURL?> /ifc/eventSubURL?> /ifc.xml/SCPDURL?> /service?> /serviceList?> urn:schemas-upnp-org:device:WANConnectionDevice:1/deviceType?> WAN Connection Device/friendlyName?> TP-LINK/manufacturer?> http://www.tp-link.com.cn/manufacturerURL?> WAN Connection Device/modelDescription?> WAN Connection Device/modelName?> 1.0/modelNumber?> /modelURL?> 12345678900001/serialNumber?> uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9/UDN?> 123456789001/UPC?> urn:schemas-upnp-org:service:WANIPConnection:1/serviceType?> urn:upnp-org:serviceId:WANIPConnection/serviceId?> /ipc/controlURL?> /ipc/eventSubURL?> /ipc.xml/SCPDURL?> /service?> /serviceList?> /device?> /deviceList?> /device?> /deviceList?> /device?> /root?>
订阅IGD事件
通过HTTP SUBSCRIBE订阅IGD事件,示例:
SUBSCRIBE /ipc HTTP/1.1 Host: 192.168.100.1:1900 USER-AGENT: Mozilla/4.0 (compatible; UPnP/1.1; Windows NT/5.1) CALLBACK: NT: upnp:event TIMEOUT: Second-1800
响应示例:
HTTP/1.1 200 OK Content-Type: text/xml;charset=UTF-8 Content-Length: 0 Connection: close Cache-control: no-cache Server: vxWorks/5.5 UPnP/1.0 TL-WR886N/6.0 Timeout: Second-1800 SID: uuid:82-2150160019
添加映射端口报文
例如,我们想映射TCP协议的内部端口8000到外部端口1000上,可以按照以下示例进行HTTP请求:
POST /ipc HTTP/1.1 Content-Type: text/xml; charset="utf-8" SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping" User-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) Host: 192.168.100.1:1900 Content-Length: 1131 Connection: Keep-Alive Cache-Control: no-cache Pragma: no-cache ?xml version="1.0"??> /NewRemoteHost?> 1000/NewExternalPort?> TCP/NewProtocol?> 8000/NewInternalPort?> 192.168.100.101/NewInternalClient?> 1/NewEnabled?> W5500_uPnPGetway/NewPortMappingDescription?> 0/NewLeaseDuration?> /m:AddPortMapping?> /SOAP-ENV:Body?> /SOAP-ENV:Envelope?>
主要字段的描述如下:
m:AddPortMapping:添加端口映射
NewExternalPort:外部端口号
NewProtocol:协议类型
NewInternalPort:内部端口号
NewInternalClient:内部地址
响应内容:
HTTP/1.1 200 OK Content-Type: text/xml;charset=UTF-8 Content-Length: 289 Connection: close Cache-control: no-cache Server: vxWorks/5.5 UPnP/1.0 TL-WR886N/6.0 ?xml version="1.0"??> /u:AddPortMappingResponse?>/s:Body?>/s:Envelope?>
删除端口映射报文
例如,我们想删除上面映射的1000端口,可以按照以下示例进行HTTP请求:
POST /ipc HTTP/1.1 Content-Type: text/xml; charset="utf-8" SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping" User-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1) Host: 192.168.100.1:1900 Content-Length: 604 Connection: Keep-Alive Cache-Control: no-cache Pragma: no-cache ?xml version="1.0"??> /NewRemoteHost?> 1000/NewExternalPort?> TCP/NewProtocol?> /m:DeletePortMapping?> /SOAP-ENV:Body?>/SOAP-ENV:Envelope?>
主要字段的描述如下:
m:DeletePortMapping:删除端口映射
NewExternalPort:外部端口号
NewProtocol:协议类型
NewRemoteHost:外部访问来源,可以为空
6实现过程
在这个例程中,我们实现了通过串口控制LED灯开关、获取和设置网络地址信息、TCP和UDP回环数据测试以及UPnP添加映射端口和删除映射端口的功能。
注意:测试实例需要W55MH32接入在支持UPnP端口转发的路由器下。
步骤1:设置以太网缓存大小
static uint8_t tx_size[_WIZCHIP_SOCK_NUM_] = {4, 4, 2, 1, 1, 1, 1, 2}; static uint8_t rx_size[_WIZCHIP_SOCK_NUM_] = {4, 4, 2, 1, 1, 1, 1, 2}; /* socket rx and tx buff init */ wizchip_init(tx_size, rx_size);
在这里我们给socket0-7的收发缓存分别设置为4KB,4KB,2KB,1KB,1KB,1KB,1KB,2KB。
其中socket0用于UPnP协议处理,socket1用于TCP和UDP回环处理,socket2用于监听IGD事件。
步骤2:LED控制函数注册
UserLED_Control_Init(set_user_led_status);
set_user_led_status()函数为控制LED的函数,具体内容如下:
/*void set_user_led_status(uint8_t val) { if (val) { GPIO_SetBits(GPIOD, GPIO_Pin_14); } else { GPIO_ResetBits(GPIOD, GPIO_Pin_14); } }
步骤3:搜索UPnP设备
do { printf("Send SSDP.. rn"); } while (SSDPProcess(SOCKET_ID) != 0); // SSDP Search discovery
/**< SSDP Header */ unsigned char SSDP[] = " M-SEARCH * HTTP/1.1rn Host:239.255.255.250:1900rn ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1rn Man:"ssdp:discover"rn MX:3rn rn "; /** * @brief This function processes the SSDP message. * @return 0: success, -1: reply packet timeout, 1: received SSDP parse error */ signed char SSDPProcess(SOCKET sockfd) { char ret_value = 0; long endTime = 0; unsigned char mcast_addr[4] = {239, 255, 255, 250}; // unsigned char_t_t mcast_mac[6] = {0x28, 0x2C, 0xB2, 0xE9, 0x42, 0xD6}; unsigned char recv_addr[4]; unsigned short recv_port; // UDP Socket Open close(sockfd); socket(sockfd, Sn_MR_UDP, PORT_SSDP, 0); /*Initialize socket for socket 0*/ while (getSn_SR(sockfd) != SOCK_UDP); #ifdef UPNP_DEBUG printf("%srn", SSDP); #endif // Send SSDP if (sendto(sockfd, SSDP, strlen((char *)SSDP), mcast_addr, 1900) <= 0) printf("SSDP Send error!!!!!!!rn"); // Receive Reply memset(recv_buffer, '', RECV_BUFFER_SIZE); endTime = my_time + 3; while (recvfrom(sockfd, (unsigned char *)recv_buffer, RECV_BUFFER_SIZE, recv_addr, &recv_port) <= 0 && my_time < endTime); // Check Receive Buffer if (my_time >= endTime) { // Check Timeout close(sockfd); return -1; } // UDP Socket Close close(sockfd); #ifdef UPNP_DEBUG printf("rnReceiveDatarn%srn", recv_buffer); #endif // Parse SSDP Message if ((ret_value = parseSSDP(recv_buffer)) == 0) UPnP_Step = 1; return ret_value; }
在这个函数中,主要是使用SSDP协议搜索IGD设备,发送报文和前面我们介绍的一致。
步骤4:获取IGD设备描述
if (GetDescriptionProcess(SOCKET_ID) == 0) // GET IGD description { printf("GetDescription Success!!rn"); } else { printf("GetDescription Fail!!rn"); }
/** * @brief This function gets the description message from IGD(Internet Gateway Device). * @return 0: success, -2: Invalid UPnP Step, -1: reply packet timeout, 1: received xml parse error */ signed char GetDescriptionProcess( SOCKET sockfd /**< a socket number. */ ) { char ret_value = 0; long endTime = 0; unsigned long ipaddr; unsigned short port; // Check UPnP Step if (UPnP_Step < 1) return -2; // Make HTTP GET Header memset(send_buffer, '', SEND_BUFFER_SIZE); MakeGETHeader(send_buffer); #ifdef UPNP_DEBUG printf("%srn", send_buffer); #endif ipaddr = inet_addr((unsigned char *)descIP); ipaddr = swapl(ipaddr); port = ATOI(descPORT, 10); // Connect to IGD(Internet Gateway Device) close(sockfd); socket(sockfd, Sn_MR_TCP, PORT_UPNP, Sn_MR_ND); /*Open a port of the socket*/ while (getSn_SR(sockfd) != SOCK_INIT) { delay_ms(100); } if (connect(sockfd, (unsigned char *)&ipaddr, port) == 0) printf("TCP Socket Error!!rn"); // Send Get Discription Message while ((getSn_SR(sockfd) != SOCK_ESTABLISHED)); send(sockfd, (void *)send_buffer, strlen(send_buffer)); // Receive Reply memset(recv_buffer, '', RECV_BUFFER_SIZE); delay_ms(500); endTime = my_time + 3; while (recv(sockfd, (void *)recv_buffer, RECV_BUFFER_SIZE) <= 0 && my_time < endTime); // Check Receive Buffer if (my_time >= endTime) { // Check Timeout close(sockfd); return -1; } // TCP Socket Close close(sockfd); #ifdef UPNP_DEBUG printf("rnReceiveDatarn%srn", recv_buffer); #endif // Parse Discription Message if ((ret_value = parseDescription(recv_buffer)) == 0) UPnP_Step = 2; return ret_value; }
请求报文通过MakeGETHeader()函数进行组包,具体报文如下:
/** * @brief This function makes the HTTP GET header. * @param dest:Target string pointer * @return none */ void MakeGETHeader(char *dest) { char local_port[6] = {''}; strcat(dest, "GET "); strcat(dest, descLOCATION); strcat(dest, " HTTP/1.1rn"); strcat(dest, "Accept: text/xml, application/xmlrn"); strcat(dest, "User-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1)rn"); strcat(dest, "Host: "); strcat(dest, descIP); sprintf(local_port, ":%s", descPORT); strcat(dest, local_port); strcat(dest, "rnConnection: Keep-AlivernCache-Control: no-cachernPragma: no-cachernrn"); }
然后将接收到的内容,通过parseDescription()函数进行解析,如果设备描述中不支持WANIPConnection服务,则说明不支持端口映射,返回错误。
parseDescription()函数内容如下:
/** * @brief This function parses the received description message from IGD(Internet Gateway Dev * @return 0: success, 1: received xml parse error */ signed char parseDescription( const char *xml /**< string for parse */ ) { const char controlURL_[] = ""; const char eventSubURL_[] = ""; char *URL_start = 0, *URL_end = 0; if (parseHTTP(xml) != 0) return 1; //printf("rn%srn", xml); // Find Control URL("/etc/linuxigd/gateconnSCPD.ctl") if ((URL_start = strstr(xml, "urn:schemas-upnp-org:service:WANIPConnection:1")) == NULL) retur if ((URL_start = strstr(URL_start, controlURL_)) == NULL) return 1; if ((URL_end = strstr(URL_start, "")) == NULL) return 1; strncpy(controlURL, URL_start + strlen(controlURL_), URL_end - URL_start - strlen(controlURL_) // Find Eventing Subscription URL("/etc/linuxigd/gateconnSCPD.evt") if ((URL_start = strstr(xml, "urn:schemas-upnp-org:service:WANIPConnection:1")) == NULL) retur if ((URL_start = strstr(URL_start, eventSubURL_)) == NULL) return 1; if ((URL_end = strstr(URL_start, "")) == NULL) return 1; strncpy(eventSubURL, URL_start + strlen(eventSubURL_), URL_end - URL_start - strlen(eventSubUR return 0; }
步骤5:订阅IGD事件
if (SetEventing(SOCKET_ID) == 0) // Subscribes IGD event messages { printf("SetEventing Success!!rn"); } else { printf("SetEventing Fail!!rn"); }
SetEventing()函数内容如下:
/** * @brief This function subscribes to the eventing message from IGD(Internet Gateway Device). * @return 0: success, -2: Invalid UPnP Step, -1: reply packet timeout */ signed char SetEventing( SOCKET sockfd /**< a socket number. */ ) { long endTime = 0; unsigned long ipaddr; unsigned short port; // Check UPnP Step if (UPnP_Step < 2) return -2; // Make Subscription message memset(send_buffer, '', SEND_BUFFER_SIZE); MakeSubscribe(send_buffer, PORT_UPNP_EVENTING); #ifdef UPNP_DEBUG printf("%srn", send_buffer); #endif ipaddr = inet_addr((unsigned char *)descIP); ipaddr = swapl(ipaddr); port = ATOI(descPORT, 10); // Connect to IGD(Internet Gateway Device) close(sockfd); socket(sockfd, Sn_MR_TCP, PORT_UPNP, Sn_MR_ND); /*Open a port of the socket*/ while (getSn_SR(sockfd) != SOCK_INIT) { delay_ms(100); } if (connect(sockfd, (unsigned char *)&ipaddr, port) == 0) printf("TCP Socket Error!!rn"); // Send Get Discription Message while ((getSn_SR(sockfd) != SOCK_ESTABLISHED)); send(sockfd, (void *)send_buffer, strlen(send_buffer)); // Receive Reply memset(recv_buffer, '', RECV_BUFFER_SIZE); delay_ms(500); endTime = my_time + 3; while (recv(sockfd, (void *)recv_buffer, RECV_BUFFER_SIZE) <= 0 && my_time < endTime); // Check Receive Buffer if (my_time >= endTime) { // Check Timeout close(sockfd); return -1; } // TCP Socket Close close(sockfd); #ifdef UPNP_DEBUG printf("rnReceiveDatarn%srn", recv_buffer); #endif return parseHTTP(recv_buffer); }
请求报文通过MakeSubscribe()函数进行组包,具体报文如下:
/** * @brief This function makes the Subscription message. * @param dest:Target string pointer * @param listen_port:Listen port * @return none */ void MakeSubscribe(char *dest, const unsigned int listen_port) { char local_port[6] = {''}, ipaddr[16] = {''}; unsigned char ip[4]; strcat(dest, "SUBSCRIBE "); strcat(dest, eventSubURL); strcat(dest, " HTTP/1.1rn"); strcat(dest, "Host: "); strcat(dest, descIP); sprintf(local_port, ":%s", descPORT); strcat(dest, local_port); strcat(dest, "rnUSER-AGENT: Mozilla/4.0 (compatible; UPnP/1.1; Windows NT/5.1)rn"); strcat(dest, "CALLBACK: ", listen_port); strcat(dest, local_port); strcat(dest, "rnNT: upnp:eventrnTIMEOUT: Second-1800rnrn"); }
最后,通过parseHTTP()函数解析HTTP响应报文,判断是否订阅成功。
parseHTTP()函数如下:
/*-----String Parse Functions-----*/ /** * @brief This function parses the HTTP header. * @return 0: success, 1: received xml parse error */ signed char parseHTTP( const char *xml /**< string for parse */ ) { char *loc = 0; if (strstr(xml, "200 OK") != NULL) return 0; else { loc = strstr(xml, "rn"); memset(content, '', CONT_BUFFER_SIZE); strncpy(content, xml, loc - xml); printf("rnHTTP Error:rn%srnrn", content); return 1; } }
步骤6:执行UPnP主程序
Main_Menu(SOCKET_ID, SOCKET_ID + 1, SOCKET_ID + 2, ethernet_buf, tcps_port, udps_port); // Main menu
/** * @brief Display/Manage a Menu on HyperTerminal Window * @param sn: use for SSDP; sn2: use for run tcp/udp loopback; sn3: use for listenes IGD event message * @param buf: use for tcp/udp loopback rx/tx buff; tcps_port: use for tcp loopback listen; udps_port: use for udp loopback receive * @return none */ void Main_Menu(uint8_t sn, uint8_t sn2, uint8_t sn3, uint8_t *buf, uint16_t tcps_port, uint16_t udps_port) { static char choice[3]; static char msg[256], ipaddr[12], protocol[4]; static unsigned short ret, external_port, internal_port; static uint8_t bTreat; static uint8_t Sip[4]; while (1) { /* Display Menu on HyperTerminal Window */ bTreat = RESET; printf("rn====================== WIZnet Chip Control Point ===================rn"); printf("This Application is basic example of UART interface withrn"); printf("Windows Hyper Terminal. rn"); printf("rn==========================================================rn"); printf(" APPLICATION MENU :rn"); printf("rn==========================================================rnn"); printf(" 1 - Set LED on rn"); printf(" 2 - Set LED off rn"); printf(" 3 - Show network settingrn"); printf(" 4 - Set network settingrn"); printf(" 5 - Run TCP Loopbackrn"); printf(" 6 - Run UDP Loopbackrn"); printf(" 7 - UPnP PortForwarding: AddPortrn"); printf(" 8 - UPnP PortForwarding: DeletePortrn"); printf("Enter your choice : "); memset(choice, 0, sizeof(choice)); scanf("%s", choice); printf("%crn", choice[0]);
在这里会执行一个用户选项菜单,选项1和2控制LED开关,选项3和4打印和设置网络地址信息,选项5运行一个TCP回环测试程序(回环测试程序可参考TCP Server章节),选项6运行一个UDP回环测试程序(回环测试程序可参考UDP章节)。选项7添加一个UPnP端口映射表,选项8删除一个UPnP端口映射表。这里我们主要讲解UPnP相关的选项7和选项8。
步骤7:添加一个UPnP端口映射表
代码如下:
if (choice[0] == '7') { bTreat = SET; printf("rnType a Protocol(TCP/UDP) : "); memset(msg, 0, sizeof(msg)); scanf("%s", msg); printf("%srn", msg); strncpy(protocol, msg, 3); protocol[3] = ''; printf("rnType a External Port Number : "); memset(msg, 0, sizeof(msg)); scanf("%s", msg); printf("%srn", msg); external_port = ATOI(msg, 10); printf("rnType a Internal Port Number : "); memset(msg, 0, sizeof(msg)); scanf("%s", msg); printf("%srn", msg); internal_port = ATOI(msg, 10); if(strcmp(protocol,"tcp") || strcmp(protocol,"TCP")) tcps_port = internal_port; else udps_port = internal_port; close(sn2); // Try to Add Port Action getSIPR(Sip); sprintf(ipaddr, "%d.%d.%d.%d", Sip[0], Sip[1], Sip[2], Sip[3]); if ((ret = AddPortProcess(sn, protocol, external_port, ipaddr, internal_port, "W5500_uPnPGetway")) == 0) printf("AddPort Success!!rn"); else printf("AddPort Error Code is %drn", ret); }
在这里,我们需要外部输入端口映射的协议类型(TCP或UDP),以及外部端口号和内部端口号。输入完成后,选项5或选项6的端口号会替换为输入的内部端口号,然后通过AddPortProcess()函数执行添加端口映射处理。AddPortProcess()函数内容如下:
/** * @brief This function processes the add port to IGD(Internet Gateway Device). * @return 0: success, -2: Invalid UPnP Step, -1: reply packet timeout, 1: received xml parse error, other: UPnP error code */ signed short AddPortProcess( SOCKET sockfd, /**< a socket number. */ const char *protocol, /**< a procotol name. "TCP" or "UDP" */ const unsigned int extertnal_port, /**< an external port number. */ const char *internal_ip, /**< an internal ip address. */ const unsigned int internal_port, /**< an internal port number. */ const char *description /**< a description of this portforward. */ ) { short len = 0; long endTime = 0; unsigned long ipaddr; unsigned short port; // Check UPnP Step if (UPnP_Step < 2) return -2; // Make "Add Port" XML(SOAP) memset(content, '', CONT_BUFFER_SIZE); MakeSOAPAddControl(content, protocol, extertnal_port, internal_ip, internal_port, description); // Make HTTP POST Header memset(send_buffer, '', SEND_BUFFER_SIZE); len = strlen(content); MakePOSTHeader(send_buffer, len, ADD_PORT); strcat(send_buffer, content); //#ifdef UPNP_DEBUG printf("%srn", send_buffer); //#endif ipaddr = inet_addr((unsigned char *)descIP); ipaddr = swapl(ipaddr); port = ATOI(descPORT, 10); // Connect to IGD(Internet Gateway Device) socket(sockfd, Sn_MR_TCP, PORT_UPNP, Sn_MR_ND); /*Open a port of the socket*/ while (getSn_SR(sockfd) != SOCK_INIT); if (connect(sockfd, (unsigned char *)&ipaddr, port) == 0) printf("TCP Socket Error!!rn"); // Send "Delete Port" Message while (getSn_SR(sockfd) != SOCK_ESTABLISHED); send(sockfd, (void *)send_buffer, strlen(send_buffer)); // Receive Reply memset(recv_buffer, '', RECV_BUFFER_SIZE); delay_ms(500); endTime = my_time + 3; while (recv(sockfd, (void *)recv_buffer, RECV_BUFFER_SIZE) <= 0 && my_time < endTime); // Check Receive Buffer if (my_time >= endTime) { // Check Timeout close(sockfd); return -1; } // TCP Socket Close close(sockfd); //#ifdef UPNP_DEBUG printf("rnReceiveDatarn%srn", recv_buffer); //#endif // Parse Replied Message return parseAddPort(recv_buffer); }
程序首先会通过MakeSOAPAddControl()函数组装请求报文中的XML部分,具体内容如下:
/**< SOAP header & tail */ const char soap_start[] = " ?xml version="1.0"??>rn "; const char soap_end[] = " /SOAP-ENV:Body?>/SOAP-ENV:Envelope?>rn "; /**< Delete Port Mapping */ const char DeletePortMapping_[] = ""; const char _DeletePortMapping[] = "/m:DeletePortMapping?>"; /**< New Remote Host */ const char NewRemoteHost_[] = ""; const char _NewRemoteHost[] = "/NewRemoteHost?>"; /**< New External Port */ const char NewExternalPort_[] = ""; const char _NewExternalPort[] = "/NewExternalPort?>"; /**< New Protocol */ const char NewProtocol_[] = ""; const char _NewProtocol[] = "/NewProtocol?>"; /**< Add Port Mapping */ const char AddPortMapping_[] = ""; const char _AddPortMapping[] = "/m:AddPortMapping?>"; /**< New Internal Port */ const char NewInternalPort_[] = ""; const char _NewInternalPort[] = "/NewInternalPort?>"; /**< New Internal Client */ const char NewInternalClient_[] = ""; const char _NewInternalClient[] = "/NewInternalClient?>"; /**< New Enabled */ const char NewEnabled[] = "1/NewEnabled?>"; const char NewEnabled_[] = ""; const char _NewEnabled[] = "/NewEnabled?>"; /**< New Port Mapping Description */ const char NewPortMappingDescription_[] = ""; const char _NewPortMappingDescription[] = "/NewPortMappingDescription?>"; /**< New Lease Duration */ const char NewLeaseDuration[] = "0/NewLeaseDuration?>"; const char NewLeaseDuration_[] = ""; const char _NewLeaseDuration[] = "/NewLeaseDuration?>"; /** * @brief This function makes the Add Port Control message in SOAP. * @param dest:Target string pointer * @param protocol:Protocol type * @param extertnal_port:External port * @param internal_ip:Internal IP address * @param internal_port:Internal port * @param description:Description * @return none */ void MakeSOAPAddControl(char *dest, const char *protocol, const unsigned int extertnal_port, const char * internal_ip, const unsigned int internal_port, const char *description) { char local_port[6] = {''}; strcat(dest, soap_start); strcat(dest, AddPortMapping_); strcat(dest, NewRemoteHost_); strcat(dest, _NewRemoteHost); strcat(dest, NewExternalPort_); sprintf(local_port, "%d", extertnal_port); strcat(dest, local_port); strcat(dest, _NewExternalPort); strcat(dest, NewProtocol_); strcat(dest, protocol); strcat(dest, _NewProtocol); strcat(dest, NewInternalPort_); sprintf(local_port, "%d", internal_port); strcat(dest, local_port); strcat(dest, _NewInternalPort); strcat(dest, NewInternalClient_); strcat(dest, internal_ip); strcat(dest, _NewInternalClient); strcat(dest, NewEnabled); strcat(dest, NewPortMappingDescription_); strcat(dest, description); strcat(dest, _NewPortMappingDescription); strcat(dest, NewLeaseDuration); strcat(dest, _AddPortMapping); strcat(dest, soap_end); }
然后通过MakePOSTHeader()函数制作HTTP头部内容,具体内容如下:
/** * @brief This function makes the HTTP POST Header. * @param dest:Target string pointer * @param content_length: content length * @param action: action type * @return none */ void MakePOSTHeader(char *dest, int content_length, int action) { char local_length[6] = {''}, local_port[6] = {''}; sprintf(local_length, "%d", content_length); strcat(dest, "POST "); strcat(dest, controlURL); strcat(dest, " HTTP/1.1rn"); strcat(dest, "Content-Type: text/xml; charset="utf-8"rn"); strcat(dest, "SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#"); switch (action) { case DELETE_PORT: strcat(dest, "DeletePortMapping""); break; case ADD_PORT: strcat(dest, "AddPortMapping""); break; } strcat(dest, "rnUser-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1)rn"); strcat(dest, "Host: "); strcat(dest, descIP); sprintf(local_port, ":%s", descPORT); strcat(dest, local_port); strcat(dest, "rnContent-Length: "); strcat(dest, local_length); strcat(dest, "rnConnection: Keep-AlivernCache-Control: no-cachernPragma: no-cachernrn"); }
最后则是发送请求,然后通过parseAddPort()函数解析响应内容判断是否添加端口映射成功。
signed short parseAddPort( const char *xml /**< string for parse */ ) { parseHTTP(xml); if (strstr(xml, "u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"") == NULL) { return parseError(xml); } return 0; }
步骤8:删除一个UPnP端口映射表
if (choice[0] == '8') { bTreat = SET; printf("rnType a Protocol(TCP/UDP) : "); memset(msg, 0, sizeof(msg)); scanf("%s", msg); printf("%srn", msg); //GetInputString(msg); strncpy(protocol, msg, 3); protocol[3] = ''; printf("rnType a External Port Number : "); // TCP_LISTEN_PORT=num; // UDP_LISTEN_PORT=num; // printf("%drn",TCP_LISTEN_PORT); memset(msg, 0, sizeof(msg)); scanf("%s", msg); printf("%srn", msg); external_port = ATOI(msg, 10); // Try to Delete Port Action if ((ret = DeletePortProcess(sn, protocol, external_port)) == 0) printf("DeletePort Success!!rn"); else printf("DeletePort Error Code is %drn", ret); } /* OTHERS CHOICE*/ if (bTreat == RESET) { printf(" wrong choice rn"); }
在这里,我们需要外部输入删除端口映射的协议类型(TCP或UDP),以及外部端口号。输入完成后,通过DeletePortProcess()函数执行添加端口映射处理。DeletePortProcess()函数内容如下:
/** * @brief This function processes the delete port to IGD(Internet Gateway Device). * @return 0: success, -2: Invalid UPnP Step, -1: reply packet timeout, 1: received xml parse error, other: UPnP error code */ signed short DeletePortProcess( SOCKET sockfd, /**< a socket number. */ const char *protocol, /**< a procotol name. "TCP" or "UDP" */ const unsigned int extertnal_port /**< an external port number. */ ) { short len = 0; long endTime = 0; unsigned long ipaddr; unsigned short port; // Check UPnP Step if (UPnP_Step < 2) return -2; // Make "Delete Port" XML(SOAP) memset(content, '', CONT_BUFFER_SIZE); MakeSOAPDeleteControl(content, protocol, extertnal_port); // Make HTTP POST Header memset(send_buffer, '', SEND_BUFFER_SIZE); len = strlen(content); MakePOSTHeader(send_buffer, len, DELETE_PORT); strcat(send_buffer, content); //#ifdef UPNP_DEBUG printf("%srn", send_buffer); //#endif ipaddr = inet_addr((unsigned char *)descIP); ipaddr = swapl(ipaddr); port = ATOI(descPORT, 10); // Connect to IGD(Internet Gateway Device) close(sockfd); socket(sockfd, Sn_MR_TCP, PORT_UPNP, Sn_MR_ND); /*Open a port of the socket*/ while (getSn_SR(sockfd) != SOCK_INIT); if (connect(sockfd, (unsigned char *)&ipaddr, port) == 0) printf("TCP Socket Error!!rn"); // Send "Delete Port" Message while (getSn_SR(sockfd) != SOCK_ESTABLISHED); send(sockfd, (void *)send_buffer, strlen(send_buffer)); // Receive Reply memset(recv_buffer, '', RECV_BUFFER_SIZE); delay_ms(500); endTime = my_time + 3; while (recv(sockfd, (void *)recv_buffer, RECV_BUFFER_SIZE) <= 0 && my_time < endTime); // Check Receive Buffer if (my_time >= endTime) { // Check Timeout close(sockfd); return -1; } // TCP Socket Close close(sockfd); //#ifdef UPNP_DEBUG printf("rnReceiveDatarn%srn", recv_buffer); //#endif // Parse Replied Message return parseDeletePort(recv_buffer); }
首先会通过MakeSOAPDeleteControl()函数组装请求报文中的XML部分,具体内容如下:
/** * @brief This function makes the Delete Port Control message in SOAP. * @param dest:Target string pointer * @param protocol:Protocol type * @param extertnal_port:External port * @return none */ void MakeSOAPDeleteControl(char *dest, const char *protocol, const unsigned int extertnal_port) { char local_port[6] = {''}; strcat(dest, soap_start); strcat(dest, DeletePortMapping_); strcat(dest, NewRemoteHost_); strcat(dest, _NewRemoteHost); strcat(dest, NewExternalPort_); sprintf(local_port, "%d", extertnal_port); strcat(dest, local_port); strcat(dest, _NewExternalPort); strcat(dest, NewProtocol_); strcat(dest, protocol); strcat(dest, _NewProtocol); strcat(dest, _DeletePortMapping); strcat(dest, soap_end); }
然后通过MakePOSTHeader()函数制作HTTP头部内容,具体内容如下:
/** * @brief This function makes the HTTP POST Header. * @param dest:Target string pointer * @param content_length: content length * @param action: action type * @return none */ void MakePOSTHeader(char *dest, int content_length, int action) { char local_length[6] = {''}, local_port[6] = {''}; sprintf(local_length, "%d", content_length); strcat(dest, "POST "); strcat(dest, controlURL); strcat(dest, " HTTP/1.1rn"); strcat(dest, "Content-Type: text/xml; charset="utf-8"rn"); strcat(dest, "SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#"); switch (action) { case DELETE_PORT: strcat(dest, "DeletePortMapping""); break; case ADD_PORT: strcat(dest, "AddPortMapping""); break; } strcat(dest, "rnUser-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1)rn"); strcat(dest, "Host: "); strcat(dest, descIP); sprintf(local_port, ":%s", descPORT); strcat(dest, local_port); strcat(dest, "rnContent-Length: "); strcat(dest, local_length); strcat(dest, "rnConnection: Keep-AlivernCache-Control: no-cachernPragma: no-cachernrn"); }
最后则是发送请求,然后通过parseDeletePort()函数解析响应内容判断是否添加端口映射成功。
signed short parseDeletePort( const char *xml /**< string for parse */ ) { parseHTTP(xml); if (strstr(xml, "u:DeletePortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"") == NULL) { return parseError(xml); } return 0; }
7运行结果
烧录例程运行后,首先进行了PHY链路检测,然后是通过DHCP获取网络地址并打印网络地址信息:
接下来是搜索IGD设备,搜索成功后会进行获取设备描述以及设置订阅IGD事件,全部成功后则进入主菜单。
接着,我们输入7,添加一个TCP协议的端口映射,外部端口为12345,内部端口为8000。
打开UPnP Wizard软件,点击刷新后可以看到我们添加的端口映射表。(UPnP Wizard下载链接:https://upnp-wizard.en.softonic.com/)
然后我们输入5,打开TCP回环测试程序。
随后,我们打开一个网络调试助手,例如SocketTester,选择为TCP Client模式,服务器地址为外部IP地址也就是192.168.1.135,端口号为外部端口号12345,点击”Connect”连接后,可以看到成功连接到内部的W55MH32上了。UDP也是同样进行操作,这里不再演示。
接着我们输入Q退出回环测试程序,然后输入8,将之前添加的TCP协议的12345外部端口删除。在UPnP Wizard上点击刷新,可以看到已经成功删除,再次执行回环测试程序,已经无法连接上内部的W55MH32上。
8总结
本文讲解了如何在 W55MH32芯片上实现 UPnP协议的端口转发功能,通过实战例程详细展示了从设备搜索、获取设备描述、订阅事件到添加和删除端口映射的完整流程,包括各步骤涉及的协议报文、函数实现和具体操作。文章还对 UPnP协议的简介、特点、应用场景进行了分析,帮助读者理解其在网络设备互联互通中的实际应用价值。
下一篇文章将聚焦 TFTP协议,解析其核心原理及在文件传输中的应用,同时讲解如何在W55MH32上实现 TFTP功能,敬请期待!
WIZnet是一家无晶圆厂半导体公司,成立于 1998年。产品包括互联网处理器 iMCU?,它采用 TOE(TCP/IP卸载引擎)技术,基于独特的专利全硬连线 TCP/IP。iMCU?面向各种应用中的嵌入式互联网设备。
WIZnet在全球拥有 70多家分销商,在香港、韩国、美国设有办事处,提供技术支持和产品营销。
香港办事处管理的区域包括:澳大利亚、印度、土耳其、亚洲(韩国和日本除外)。
审核编辑 黄宇
-
以太网
+关注
关注
41文章
5706浏览量
176471 -
端口
+关注
关注
4文章
1048浏览量
33029 -
UPnP
+关注
关注
0文章
7浏览量
8517
发布评论请先 登录
第二十六章 W55MH32?上位机搜索和配置示例

第二十三章 W55MH32 MQTT_OneNET示例

第十八章 W55MH32 FTP_Server示例

第十七章 W55MH32 ARP示例

第十六章 W55MH32 PING示例

第十五章 W55MH32 SNMP示例

第十四章 W55MH32 TFTP示例

第十二章 W55MH32 NetBIOS示例

第十一章 W55MH32 SMTP示例

第十章 W55MH32 SNTP示例

第九章 W55MH32 HTTP Server示例

第五章 W55MH32 UDP示例

第三章 W55MH32 TCP Client示例

第二章 W55MH32 DHCP示例

WIZnet W55MH32以太网单片机开发教程 第十一章 通用定时器(上篇)

评论