W55MH32使用UPnP协议访问路由器进行端口映射
更新于 2026年9月18日
本篇总结
W55MH32 实现 UPnP 协议,通过 SSDP(Simple Service Discovery Protocol)搜索 IGD 网关并以 SOAP 消息调用 AddPortMapping / DeletePortMapping 接口,实现路由器端口的动态映射与删除。
硬件平台:本例程基于 W55MH32L-EVB 开发板运行。
采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB
本示例演示如何在 W55MH32 上实现 UPnP 协议,并通过实战例程,为大家讲解如何使用 UPnP 协议来配置路由器的端口转发功能。该例程用到的其他网络协议,例如 DHCP,请参考相关章节。
前置工具准备
开始本例程前,请确认以下工具已安装:
UPnP 协议简介
UPnP(Universal Plug and Play)协议是一种支持设备在局域网中实现自动发现和通信的网络协议。其端口转发功能由 IGD Profile 提供,允许局域网设备动态请求路由器为其开放指定的端口,以实现外部设备访问内部服务。这种功能消除了手动配置端口转发的复杂性,特别适用于需要穿透 NAT(网络地址转换)环境的应用场景。
IGD(Internet Gateway Device,互联网网关设备)是 UPnP 协议的一部分,主要用于管理网络中的网关设备(如路由器)的服务和资源。IGD 扩展定义了一套标准接口,允许局域网设备与网关设备通信,动态配置网络设置,例如端口转发、带宽管理和连接状态查询等。
应用场景
通过 UPnP 端口转发功能,可以实现远程访问(将外部请求转发到局域网设备如 NAS、监控摄像头)和设备发现与即插即用(通过 UPnP 广播发现新设备,获取设备描述文件来获取服务和接口)。
硬件连接
请注意:
W55MH32 必须接入在支持 UPnP/IGD 端口转发的路由器下,且路由器的 UPnP 功能已开启。PC(运行 UPnP Wizard)和 W55MH32 需处于同一局域网。
- Micro USB 线连接 DEBUG USB 口到 PC(供电 + 调试串口)
- 网线连接开发板 RJ45 网口 到路由器 LAN 口
│ 路由器(IGD / UPnP 网关) │
│ 192.168.100.1 :1900 │
│ 外部IP: 192.168.1.135 │
└────┬───────────────────┬──────────┘
│ │
┌────┴──────┐ ┌────┴──────────────┐
│ W55MH32 │ │ PC │
│(UPnP CP) │ │ UPnP Wizard │
│ SSDP:1900 │ │ SocketTester │
└───────────┘ └───────────────────┘
↑
DEBUG USB ──→ WIZ UartTool
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\WZTOE\UPnP\,核心文件如下:
UPnP/
├── Project/
│ └── Project.uvprojx ← Keil 工程文件
└── USER/
├── main.c ← 硬件初始化 + 网络配置 + UPnP流程 + 主菜单
├── wiz_interface/
│ ├── wiz_interface.c ← PHY 检测 / 网络信息打印 / DHCP
│ └── wiz_interface.h
├── upnp/
│ ├── upnp.c ← UPnP 协议实现(SSDP/描述/订阅/SOAP)
│ ├── upnp.h
│ └── upnp_util.c/h ← HTTP 解析 / XML 解析辅助
├── ioLibrary/
│ └── Internet/HTTPServer/ ← HTTP 解析库
├── tim/
│ └── bsp_tim.c/h ← TIM3 定时器(DHCP 1s + 超时计时)
└── uart/
└── bsp_uart.c/h ← 调试串口 + printf 重定向
本例程的核心:通过 SSDP 组播搜索 IGD → HTTP GET 获取设备描述 → SUBSCRIBE 订阅事件 → SOAP POST 调用 AddPortMapping / DeletePortMapping。Socket 分配:socket0 处理 UPnP 协议,socket1 处理 TCP/UDP 回环,socket2 监听 IGD 事件。
报文讲解
通过 UPnP 给路由器添加端口转发规则的完整工作流程如下:
- 设备发现:W55MH32 通过 SSDP 向局域网发送组播请求(HTTP M-SEARCH),搜索支持 IGD 的网关设备。
- 获取服务描述:W55MH32 访问网关设备获取服务描述文件,了解支持的服务和接口。
- 订阅 IGD 事件:通过事件订阅,W55MH32 可在不主动轮询的情况下接收实时通知。
- 调用服务接口:使用 UPnP 的 SOAP 消息调用 IGD 提供的端口映射接口。
- 数据交互测试:外部通过访问映射的端口及路由器地址和局域网内部设备进行通信。
设备搜索(SSDP)
SSDP 是 UPnP 协议中的关键协议,用于设备发现和服务发布。它通过 HTTP over UDP 的形式在局域网内广播和接收报文,采用多播地址 239.255.255.250 和端口 1900。
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:3M-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:1HTTP/1.1 200 OK:表示响应成功。
CACHE-CONTROL:响应有效时间为 60 秒。
LOCATION:设备描述文件的 URL。
SERVER:设备的操作系统、UPnP 版本和设备名称。
USN:唯一设备标识符。
获取设备描述
这一步会通过 HTTP GET 方式去请求 xml 文件。
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响应示例(设备描述 XML):
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"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<presentationURL>http://192.168.100.1:80 </presentationURL>
<friendlyName>Wireless N Router TL-WR886N</friendlyName>
<manufacturer>TP-LINK</manufacturer>
<manufacturerURL>http://www.tp-link.com.cn</manufacturerURL>
<modelDescription>TL-WR886N 6.0</modelDescription>
<modelName>TL-WR886N</modelName>
<modelNumber>6.0</modelNumber>
<UDN>uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9</UDN>
<UPC>123456789001</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
<controlURL>/l3f</controlURL>
<eventSubURL>/l3f</eventSubURL>
<SCPDURL>/l3f.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WAN Device</friendlyName>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonInterfaceConfig</serviceId>
<controlURL>/ifc</controlURL>
<eventSubURL>/ifc</eventSubURL>
<SCPDURL>/ifc.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WAN Connection Device</friendlyName>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConnection</serviceId>
<controlURL>/ipc</controlURL>
<eventSubURL>/ipc</eventSubURL>
<SCPDURL>/ipc.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>
其中 controlURL(/ipc)用于发送 SOAP 控制请求,eventSubURL(/ipc)用于订阅事件。若设备描述中不支持 WANIPConnection 服务,则说明不支持端口映射。
订阅 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: <http://192.168.100.101:5002/>
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"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewRemoteHost xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">
</NewRemoteHost>
<NewExternalPort xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui2">
1000</NewExternalPort>
<NewProtocol xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">
TCP</NewProtocol>
<NewInternalPort xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui2">
8000</NewInternalPort>
<NewInternalClient xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">
192.168.100.101</NewInternalClient>
<NewEnabled xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="boolean">1</NewEnabled>
<NewPortMappingDescription xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">W5500_uPnPGetway</NewPortMappingDescription>
<NewLeaseDuration xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui4">0</NewLeaseDuration>
</m:AddPortMapping>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>m:AddPortMapping:添加端口映射
NewExternalPort:外部端口号
NewProtocol:协议类型(TCP/UDP)
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"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"></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"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body>
<m:DeletePortMapping xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewRemoteHost xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string"></NewRemoteHost>
<NewExternalPort xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="ui2">1000</NewExternalPort>
<NewProtocol xmlns:dt="urn:schemas-microsoft-com:datatypes" dt:dt="string">TCP</NewProtocol>
</m:DeletePortMapping>
</SOAP-ENV:Body></SOAP-ENV:Envelope>m:DeletePortMapping:删除端口映射
NewExternalPort:外部端口号
NewProtocol:协议类型
NewRemoteHost:外部访问来源,可以为空
实现过程
在这个例程中,我们实现了通过串口控制 LED 灯开关、获取和设置网络地址信息、TCP 和 UDP 回环数据测试以及 UPnP 添加映射端口和删除映射端口的功能。
步骤1:设置以太网缓存大小
文件:main.c
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.. \r\n");
} while (SSDPProcess(SOCKET_ID) != 0); /* SSDP Search discovery */
SSDP 头部定义及 SSDPProcess() 函数通过 UDP 发送 M-SEARCH 组播报文并接收响应:
/* SSDP Header */
unsigned char SSDP[] = "\
M-SEARCH * HTTP/1.1\r\n\
Host:239.255.255.250:1900\r\n\
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\
Man:\"ssdp:discover\"\r\n\
MX:3\r\n\
\r\n\
";
/**
* @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 recv_addr[4];
unsigned short recv_port;
/* UDP Socket Open */
close(sockfd);
socket(sockfd, Sn_MR_UDP, PORT_SSDP, 0);
while (getSn_SR(sockfd) != SOCK_UDP);
/* Send SSDP */
if (sendto(sockfd, SSDP, strlen((char *)SSDP), mcast_addr, 1900) <= 0)
printf("SSDP Send error!!!!!!!\r\n");
/* Receive Reply */
memset(recv_buffer, '\0', 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);
if (my_time >= endTime)
{
close(sockfd);
return -1; /* Timeout */
}
close(sockfd);
/* Parse SSDP Message */
if ((ret_value = parseSSDP(recv_buffer)) == 0)
UPnP_Step = 1;
return ret_value;
}
在这个函数中,主要是使用 SSDP 协议搜索 IGD 设备。成功解析后 UPnP_Step 置为 1。
步骤4:获取 IGD 设备描述
if (GetDescriptionProcess(SOCKET_ID) == 0) /* GET IGD description */
{
printf("GetDescription Success!!\r\n");
}
else
{
printf("GetDescription Fail!!\r\n");
}
GetDescriptionProcess() 建立 TCP 连接到 IGD,通过 MakeGETHeader() 组包发送 HTTP GET 请求,然后通过 parseDescription() 解析设备描述 XML,提取 controlURL 和 eventSubURL:
/**
* @brief This function makes the HTTP GET header.
*/
void MakeGETHeader(char *dest)
{
char local_port[6] = {'\0'};
strcat(dest, "GET ");
strcat(dest, descLOCATION);
strcat(dest, " HTTP/1.1\r\n");
strcat(dest, "Accept: text/xml, application/xml\r\n");
strcat(dest, "User-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1)\r\n");
strcat(dest, "Host: ");
strcat(dest, descIP);
sprintf(local_port, ":%s", descPORT);
strcat(dest, local_port);
strcat(dest, "\r\nConnection: Keep-Alive\r\nCache-Control: no-cache\r\nPragma: no-cache\r\n\r\n");
}
/**
* @brief This function parses the received description message.
* @return 0: success, 1: received xml parse error
*/
signed char parseDescription(const char *xml)
{
char *URL_start = 0, *URL_end = 0;
if (parseHTTP(xml) != 0) return 1;
/* Find Control URL for WANIPConnection */
if ((URL_start = strstr(xml, "urn:schemas-upnp-org:service:WANIPConnection:1")) == NULL) return 1;
if ((URL_start = strstr(URL_start, "<controlURL>")) == NULL) return 1;
if ((URL_end = strstr(URL_start, "</controlURL>")) == NULL) return 1;
strncpy(controlURL, URL_start + strlen("<controlURL>"),
URL_end - URL_start - strlen("<controlURL>"));
/* Find Eventing Subscription URL */
if ((URL_start = strstr(xml, "urn:schemas-upnp-org:service:WANIPConnection:1")) == NULL) return 1;
if ((URL_start = strstr(URL_start, "<eventSubURL>")) == NULL) return 1;
if ((URL_end = strstr(URL_start, "</eventSubURL>")) == NULL) return 1;
strncpy(eventSubURL, URL_start + strlen("<eventSubURL>"),
URL_end - URL_start - strlen("<eventSubURL>"));
return 0;
}
如果设备描述中不支持 WANIPConnection 服务,则说明不支持端口映射,返回错误。解析成功后 UPnP_Step 置为 2。
步骤5:订阅 IGD 事件
if (SetEventing(SOCKET_ID) == 0) /* Subscribes IGD event messages */
{
printf("SetEventing Success!!\r\n");
}
else
{
printf("SetEventing Fail!!\r\n");
}
请求报文通过 MakeSubscribe() 函数组包,最后通过 parseHTTP() 函数解析 HTTP 响应报文,判断是否订阅成功:
/**
* @brief This function makes the Subscription message.
*/
void MakeSubscribe(char *dest, const unsigned int listen_port)
{
char local_port[6] = {'\0'}, ipaddr[16] = {'\0'};
unsigned char ip[4];
strcat(dest, "SUBSCRIBE ");
strcat(dest, eventSubURL);
strcat(dest, " HTTP/1.1\r\n");
strcat(dest, "Host: ");
strcat(dest, descIP);
sprintf(local_port, ":%s", descPORT);
strcat(dest, local_port);
strcat(dest, "\r\nUSER-AGENT: Mozilla/4.0 (compatible; UPnP/1.1; Windows NT/5.1)\r\n");
strcat(dest, "CALLBACK: <http://");
getSIPR(ip);
sprintf(ipaddr, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
strcat(dest, ipaddr);
sprintf(local_port, ":%d/>", listen_port);
strcat(dest, local_port);
strcat(dest, "\r\nNT: upnp:event\r\nTIMEOUT: Second-1800\r\n\r\n");
}
/**
* @brief This function parses the HTTP header.
* @return 0: success, 1: received xml parse error
*/
signed char parseHTTP(const char *xml)
{
char *loc = 0;
if (strstr(xml, "200 OK") != NULL)
return 0;
else
{
loc = strstr(xml, "\r\n");
memset(content, '\0', CONT_BUFFER_SIZE);
strncpy(content, xml, loc - xml);
printf("\r\nHTTP Error:\r\n%s\r\n\r\n", content);
return 1;
}
}步骤6:执行 UPnP 主程序
Main_Menu(SOCKET_ID, SOCKET_ID + 1, SOCKET_ID + 2,
ethernet_buf, tcps_port, udps_port); /* Main menu */在这里会执行一个用户选项菜单,选项 1 和 2 控制 LED 开关,选项 3 和 4 打印和设置网络地址信息,选项 5 运行 TCP 回环测试程序,选项 6 运行 UDP 回环测试程序。选项 7 添加一个 UPnP 端口映射表,选项 8 删除一个 UPnP 端口映射表。这里我们主要讲解 UPnP 相关的选项 7 和选项 8。
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];
while (1)
{
printf("\r\n============== WIZnet Chip Control Point ==============\r\n");
printf(" 1 - Set LED on\r\n");
printf(" 2 - Set LED off\r\n");
printf(" 3 - Show network setting\r\n");
printf(" 4 - Set network setting\r\n");
printf(" 5 - Run TCP Loopback\r\n");
printf(" 6 - Run UDP Loopback\r\n");
printf(" 7 - UPnP PortForwarding: AddPort\r\n");
printf(" 8 - UPnP PortForwarding: DeletePort\r\n");
printf("Enter your choice : ");
memset(choice, 0, sizeof(choice));
scanf("%s", choice);
/* ... handle choice ... */
}
}步骤7:添加一个 UPnP 端口映射表
if (choice[0] == '7')
{
bTreat = SET;
printf("\r\nType a Protocol(TCP/UDP) : ");
scanf("%s", msg);
strncpy(protocol, msg, 3);
protocol[3] = '\0';
printf("\r\nType a External Port Number : ");
scanf("%s", msg);
external_port = ATOI(msg, 10);
printf("\r\nType a Internal Port Number : ");
scanf("%s", msg);
internal_port = ATOI(msg, 10);
if(strcmp(protocol,"tcp") || strcmp(protocol,"TCP"))
tcps_port = internal_port;
else
udps_port = internal_port;
close(sn2);
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!!\r\n");
else
printf("AddPort Error Code is %d\r\n", ret);
}
在这里需要外部输入端口映射的协议类型(TCP 或 UDP),以及外部端口号和内部端口号。输入完成后,通过 AddPortProcess() 函数执行添加端口映射处理。程序首先通过 MakeSOAPAddControl() 组装 SOAP XML,再通过 MakePOSTHeader() 拼接 HTTP 头部发送请求:
/* SOAP header & tail */
const char soap_start[] =
"<?xml version=\"1.0\"?>\r\n\
<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" \
SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><SOAP-ENV:Body>";
const char soap_end[] = "</SOAP-ENV:Body></SOAP-ENV:Envelope>\r\n";
/* Add Port Mapping */
const char AddPortMapping_[] = "<m:AddPortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">";
const char _AddPortMapping[] = "</m:AddPortMapping>";
/**
* @brief This function makes the Add Port Control message in SOAP.
*/
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] = {'\0'};
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);
}
/**
* @brief This function makes the HTTP POST Header.
*/
void MakePOSTHeader(char *dest, int content_length, int action)
{
char local_length[6] = {'\0'}, local_port[6] = {'\0'};
sprintf(local_length, "%d", content_length);
strcat(dest, "POST ");
strcat(dest, controlURL);
strcat(dest, " HTTP/1.1\r\n");
strcat(dest, "Content-Type: text/xml; charset=\"utf-8\"\r\n");
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, "\r\nUser-Agent: Mozilla/4.0 (compatible; UPnP/1.0; Windows NT/5.1)\r\n");
strcat(dest, "Host: ");
strcat(dest, descIP);
sprintf(local_port, ":%s", descPORT);
strcat(dest, local_port);
strcat(dest, "\r\nContent-Length: ");
strcat(dest, local_length);
strcat(dest, "\r\nConnection: Keep-Alive\r\nCache-Control: no-cache\r\nPragma: no-cache\r\n\r\n");
}
/**
* @brief Parse AddPort response
*/
signed short parseAddPort(const char *xml)
{
parseHTTP(xml);
if (strstr(xml, "u:AddPortMappingResponse xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"") == NULL)
return parseError(xml);
return 0;
}
发送请求后,通过 parseAddPort() 函数解析响应内容判断是否添加端口映射成功。
步骤8:删除一个 UPnP 端口映射表
if (choice[0] == '8')
{
bTreat = SET;
printf("\r\nType a Protocol(TCP/UDP) : ");
scanf("%s", msg);
strncpy(protocol, msg, 3);
protocol[3] = '\0';
printf("\r\nType a External Port Number : ");
scanf("%s", msg);
external_port = ATOI(msg, 10);
/* Try to Delete Port Action */
if ((ret = DeletePortProcess(sn, protocol, external_port)) == 0)
printf("DeletePort Success!!\r\n");
else
printf("DeletePort Error Code is %d\r\n", ret);
}
在这里需要外部输入删除端口映射的协议类型(TCP 或 UDP),以及外部端口号。输入完成后,通过 DeletePortProcess() 函数执行删除端口映射处理。程序首先通过 MakeSOAPDeleteControl() 组装 SOAP XML:
/* Delete Port Mapping */
const char DeletePortMapping_[] = "<m:DeletePortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">";
const char _DeletePortMapping[] = "</m:DeletePortMapping>";
/**
* @brief This function makes the Delete Port Control message in SOAP.
*/
void MakeSOAPDeleteControl(char *dest, const char *protocol,
const unsigned int extertnal_port)
{
char local_port[6] = {'\0'};
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);
}
/**
* @brief Parse DeletePort response
*/
signed short parseDeletePort(const char *xml)
{
parseHTTP(xml);
if (strstr(xml, "u:DeletePortMappingResponse xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"") == NULL)
return parseError(xml);
return 0;
}
发送请求后,通过 parseDeletePort() 函数解析响应内容判断是否删除端口映射成功。
运行验证
请注意
测试实例需要 W55MH32 接入在支持 UPnP 端口转发的路由器下。
烧录例程运行后,首先进行了 PHY 链路检测,然后是通过 DHCP 获取网络地址并打印网络地址信息。接下来是搜索 IGD 设备,搜索成功后会进行获取设备描述以及设置订阅 IGD 事件,全部成功后则进入主菜单。
接着,我们输入 7,添加一个 TCP 协议的端口映射,外部端口为 12345,内部端口为 8000。打开 UPnP Wizard 软件,点击刷新后可以看到我们添加的端口映射表。
然后我们输入 5,打开 TCP 回环测试程序。随后打开一个网络调试助手(如 SocketTester),选择 TCP Client 模式,服务器地址为外部 IP 地址(192.168.1.135),端口号为外部端口号 12345,点击"Connect"连接后,可以看到成功连接到内部的 W55MH32 上了。UDP 也是同样进行操作。
接着我们输入 Q 退出回环测试程序,然后输入 8,将之前添加的 TCP 协议的 12345 外部端口删除。在 UPnP Wizard 上点击刷新,可以看到已经成功删除,再次执行回环测试程序,已经无法连接上内部的 W55MH32 了。




常见问题
Q: 为什么搜索不到 IGD 设备
请确认路由器支持 UPnP/IGD 功能且已开启。SSDP 使用 UDP 多播地址 239.255.255.250:1900,确保 W55MH32 与路由器在同一局域网且 DHCP 已正确获取 IP。若路由器关闭了 UPnP 功能,M-SEARCH 请求将无法收到响应,函数会在 3 秒后超时返回 -1。
Q: 获取设备描述时提示不支持端口映射
parseDescription() 函数会在设备描述 XML 中查找 WANIPConnection 服务。如果路由器的设备描述中不包含该服务(仅支持 WANPPPConnection 或不提供端口映射功能),则说明该路由器不支持通过此方式进行端口映射,需要更换支持的路由器。
Q: AddPortMapping 返回错误码是什么意思
返回 0 表示成功;-2 表示 UPnP 步骤无效(未完成设备发现或描述获取);-1 表示响应超时;1 表示 XML 解析错误;其他值为 UPnP 错误码,如 718(ConflictInMappingEntry,端口已被映射)、500(ActionFailed)等。可通过 parseError() 函数获取详细错误描述。
Q: UPnP 端口映射的安全性如何
UPnP 端口映射会自动在路由器上开放端口,外部网络可直接访问内部设备,存在一定安全风险。建议仅在需要时添加映射,使用完毕后及时删除。NewRemoteHost 字段可限制来源 IP,NewLeaseDuration 可设置映射有效期(0 表示永久)。生产环境中应结合防火墙等安全措施使用。
Q: 如何测试端口映射是否生效
添加端口映射后,可使用 UPnP Wizard 软件查看路由器上的映射表确认。然后在外部网络使用网络调试助手(如 SocketTester)以 TCP/UDP Client 模式连接路由器外部 IP 和映射的外部端口,若能成功连接并通信到内部 W55MH32,则说明映射生效。删除映射后同样方法验证应无法连接。
总结
- 协议栈:UPnP 基于 SSDP(UDP 多播)发现设备、HTTP GET 获取描述、SUBSCRIBE 订阅事件、SOAP(HTTP POST)调用控制接口
- 工作流程:设备发现 → 获取描述 → 订阅事件 → 添加/删除端口映射,通过 UPnP_Step 变量控制流程状态
- 核心接口:AddPortMapping 添加映射(需指定协议、内外端口、内部 IP),DeletePortMapping 删除映射(需指定协议和外部端口)
- Socket 分配:socket0 处理 UPnP 协议,socket1 处理 TCP/UDP 回环,socket2 监听 IGD 事件
- 应用价值:消除手动配置端口转发的复杂性,实现 NAT 穿透,适用于远程访问 NAS、监控摄像头等场景
下一篇文章将聚焦 TFTP 协议,解析其核心原理及在文件传输中的应用,同时讲解如何在 W55MH32 上实现 TFTP 功能,敬请期待!