UPnP端口转发示例
本篇文章,我们将详细介绍如何在W5500芯片上面实现UPnP协议,并通过实战例程,为大家讲解如何使用UPnP协议来配置路由器的端口转发功能。
该例程用到的其他网络协议,例如DHCP, 请参考相关章节。有关 W5500 的初始化过程,请参考Network install,这里将不再赘述。
UPnP协议简介
UPnP(Universal Plug and Play)协议是一种支持设备在局域网中实现自动发现和通信的网络协议。其端口转发功能由IGD Profile提供,允许局域网设备动态请求路由器为其开放指定的端口,以实现外部设备访问内部服务。 这种功能消除了手动配置端口转发的复杂性,特别适用于需要穿透NAT(网络地址转换)环境的应用场景。 IGD(Internet Gateway Device,互联网网关设备)是UPnP(Universal Plug and Play)协议的一部分, 主要用于管理网络中的网关设备(如路由器)的服务和资源。IGD扩展定义了一套标准接口,允许局域网设备与网关设备通信,动态配置网络设置,例如端口转发、带宽管理和连接状态查询等。
UPnP协议特点
- 自动化配置:无需用户手动设置,减少了配置错误的风险。
- 动态灵活:端口映射规则可以根据需求动态添加或删除。
- 设备友好:支持即插即用,简化了设备的联网和部署过程。
- 跨设备兼容:UPnP基于标准化协议,广泛支持各种设备和平台。
UPnP应用场景
通过UPnP端口转发功能,我们可以使用W5500实现以下功能:
- 远程访问:将外部请求转发到局域网设备(如NAS、监控摄像头),实现外部远程访问内部设备。
- 设备发现和即插即用:通过UPnP广播发现新设备,再通过获取设备描述文件来获取服务和接口,做到即插即用。
通过UPnP给路由器添加端口转发规则的工作流程
- 设备发现:W5500通过SSDP(Simple Service Discovery Protocol)向局域网中发送组播请求(HTTP M-SEARCH报文),搜索支持IGD的网关设备。
- 获取服务描述:W5500访问网关设备(路由器)获取服务描述文件,了解支持的服务和接口。
- 订阅IGD事件:通过事件订阅,W5500可以在不主动轮询的情况下,接收实时通知。
- 调用服务接口:使用UPnP的SOAT消息调用IGD提供的端口映射接口。
- 数据交互测试外部通过访问映射的端口及路由器地址和局域网内部设备进行通信。
报文讲解
设备搜索
上文我们提到,设备搜索时使用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"?>
<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>
<manufacturer>TP-LINK</manufacturer>
<manufacturerURL>http://www.tp-link.com.cn</manufacturerURL>
<modelDescription>WAN Device</modelDescription>
<modelName>WAN Device</modelName>
<modelNumber>1.0</modelNumber>
<modelURL></modelURL>
<serialNumber>12345678900001</serialNumber>
<UDN>uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9</UDN>
<UPC>123456789001</UPC>
<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>
<manufacturer>TP-LINK</manufacturer>
<manufacturerURL>http://www.tp-link.com.cn</manufacturerURL>
<modelDescription>WAN Connection Device</modelDescription>
<modelName>WAN Connection Device</modelName>
<modelNumber>1.0</modelNumber>
<modelURL></modelURL>
<serialNumber>12345678900001</serialNumber>
<UDN>uuid:8c15e41f-3d83-41c1-b35d-5D2A64377DE9</UDN>
<UPC>123456789001</UPC>
<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>
订阅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:协议类型
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:设置以太网缓存大小
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 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_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("%s\r\n", SSDP);
#endif
// 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); // Check Receive Buffer
if (my_time >= endTime)
{ // Check Timeout
close(sockfd);
return -1;
}
// UDP Socket Close
close(sockfd);
#ifdef UPNP_DEBUG
printf("\r\nReceiveData\r\n%s\r\n", 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!!\r\n");
}
else
{
printf("GetDescription Fail!!\r\n");
}
/**
* @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, '\0', SEND_BUFFER_SIZE);
MakeGETHeader(send_buffer);
#ifdef UPNP_DEBUG
printf("%s\r\n", 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!!\r\n");
// Send Get Discription Message
while ((getSn_SR(sockfd) != SOCK_ESTABLISHED));
send(sockfd, (void *)send_buffer, strlen(send_buffer));
// Receive Reply
memset(recv_buffer, '\0', 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("\r\nReceiveData\r\n%s\r\n", 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] = {'\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");
}
然后将接收到的内容,通过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("\r\n%s\r\n", 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!!\r\n");
}
else
{
printf("SetEventing Fail!!\r\n");
}
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, '\0', SEND_BUFFER_SIZE);
MakeSubscribe(send_buffer, PORT_UPNP_EVENTING);
#ifdef UPNP_DEBUG
printf("%s\r\n", 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!!\r\n");
// Send Get Discription Message
while ((getSn_SR(sockfd) != SOCK_ESTABLISHED));
send(sockfd, (void *)send_buffer, strlen(send_buffer));
// Receive Reply
memset(recv_buffer, '\0', 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("\r\nReceiveData\r\n%s\r\n", 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] = {'\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");
}
最后,通过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, "\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
/**
* @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("\r\n====================== WIZnet Chip Control Point ===================\r\n");
printf("This Application is basic example of UART interface with\r\n");
printf("Windows Hyper Terminal. \r\n");
printf("\r\n==========================================================\r\n");
printf(" APPLICATION MENU :\r\n");
printf("\r\n==========================================================\r\n\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);
printf("%c\r\n", 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("\r\nType a Protocol(TCP/UDP) : ");
memset(msg, 0, sizeof(msg));
scanf("%s", msg);
printf("%s\r\n", msg);
strncpy(protocol, msg, 3);
protocol[3] = '\0';
printf("\r\nType a External Port Number : ");
memset(msg, 0, sizeof(msg));
scanf("%s", msg);
printf("%s\r\n", msg);
external_port = ATOI(msg, 10);
printf("\r\nType a Internal Port Number : ");
memset(msg, 0, sizeof(msg));
scanf("%s", msg);
printf("%s\r\n", 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!!\r\n");
else
printf("AddPort Error Code is %d\r\n", 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, '\0', CONT_BUFFER_SIZE);
MakeSOAPAddControl(content, protocol, extertnal_port, internal_ip, internal_port, description);
// Make HTTP POST Header
memset(send_buffer, '\0', SEND_BUFFER_SIZE);
len = strlen(content);
MakePOSTHeader(send_buffer, len, ADD_PORT);
strcat(send_buffer, content);
//#ifdef UPNP_DEBUG
printf("%s\r\n", 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!!\r\n");
// Send "Delete Port" Message
while (getSn_SR(sockfd) != SOCK_ESTABLISHED);
send(sockfd, (void *)send_buffer, strlen(send_buffer));
// Receive Reply
memset(recv_buffer, '\0', 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("\r\nReceiveData\r\n%s\r\n", recv_buffer);
//#endif
// Parse Replied Message
return parseAddPort(recv_buffer);
}
程序首先会通过MakeSOAPAddControl()函数组装请求报文中的XML部分,具体内容如下:
/**< 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\
";
/**< Delete Port Mapping */
const char DeletePortMapping_[] = "<m:DeletePortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">";
const char _DeletePortMapping[] = "</m:DeletePortMapping>";
/**< New Remote Host */
const char NewRemoteHost_[] = "<NewRemoteHost xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"string\">";
const char _NewRemoteHost[] = "</NewRemoteHost>";
/**< New External Port */
const char NewExternalPort_[] = "<NewExternalPort xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui2\">";
const char _NewExternalPort[] = "</NewExternalPort>";
/**< New Protocol */
const char NewProtocol_[] = "<NewProtocol xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"string\">";
const char _NewProtocol[] = "</NewProtocol>";
/**< Add Port Mapping */
const char AddPortMapping_[] = "<m:AddPortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">";
const char _AddPortMapping[] = "</m:AddPortMapping>";
/**< New Internal Port */
const char NewInternalPort_[] = "<NewInternalPort xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui2\">";
const char _NewInternalPort[] = "</NewInternalPort>";
/**< New Internal Client */
const char NewInternalClient_[] = "<NewInternalClient xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"string\">";
const char _NewInternalClient[] = "</NewInternalClient>";
/**< New Enabled */
const char NewEnabled[] = "<NewEnabled xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"boolean\">1</NewEnabled>";
const char NewEnabled_[] = "<NewEnabled xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"boolean\">";
const char _NewEnabled[] = "</NewEnabled>";
/**< New Port Mapping Description */
const char NewPortMappingDescription_[] = "<NewPortMappingDescription xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"string\">";
const char _NewPortMappingDescription[] = "</NewPortMappingDescription>";
/**< New Lease Duration */
const char NewLeaseDuration[] = "<NewLeaseDuration xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui4\">0</NewLeaseDuration>";
const char NewLeaseDuration_[] = "<NewLeaseDuration xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui4\">";
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] = {'\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);
}
通过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] = {'\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");
}
然后通过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] = {'\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");
}
最后则是发送请求,然后通过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("\r\nType a Protocol(TCP/UDP) : ");
memset(msg, 0, sizeof(msg));
scanf("%s", msg);
printf("%s\r\n", msg);
//GetInputString(msg);
strncpy(protocol, msg, 3);
protocol[3] = '\0';
printf("\r\nType a External Port Number : ");
// TCP_LISTEN_PORT=num;
// UDP_LISTEN_PORT=num;
// printf("%d\r\n",TCP_LISTEN_PORT);
memset(msg, 0, sizeof(msg));
scanf("%s", msg);
printf("%s\r\n", 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);
}
/* OTHERS CHOICE*/
if (bTreat == RESET)
{
printf(" wrong choice \r\n");
}
在这里,我们需要外部输入删除端口映射的协议类型(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, '\0', CONT_BUFFER_SIZE);
MakeSOAPDeleteControl(content, protocol, extertnal_port);
// Make HTTP POST Header
memset(send_buffer, '\0', SEND_BUFFER_SIZE);
len = strlen(content);
MakePOSTHeader(send_buffer, len, DELETE_PORT);
strcat(send_buffer, content);
//#ifdef UPNP_DEBUG
printf("%s\r\n", 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!!\r\n");
// Send "Delete Port" Message
while (getSn_SR(sockfd) != SOCK_ESTABLISHED);
send(sockfd, (void *)send_buffer, strlen(send_buffer));
// Receive Reply
memset(recv_buffer, '\0', 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("\r\nReceiveData\r\n%s\r\n", 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] = {'\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);
}
然后通过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] = {'\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");
}
最后则是发送请求,然后通过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;
}
运行结果
请注意:
测试实例需要W5500接入在支持UPnP端口转发的路由器下。
烧录例程运行后,首先进行了PHY链路检测,然后是通过DHCP获取网络地址并打印网络地址信息:

接下来是搜索IGD设备,搜索成功后会进行获取设备描述以及设置订阅IGD事件,全部成功后则进入主菜单。

接着,我们输入7,添加一个TCP协议的端口映射,外部端口为12345,内部端口为8000。


打开UPnP Wizard软件,点击刷新后可以看到我们添加的端口映射表。

然后我们输入5,打开TCP回环测试程序。

随后,我们打开一个网络调试助手,例如SocketTester,选择为TCP Client模式,服务器地址为外部IP地址也就是100.64.223.173,端口号为外部端口号12345, 点击”Connect”连接后,可以看到成功连接到内部的W5500上了。UDP也是同样进行操作,这里不再演示。

接着我们输入Q退出回环测试程序,然后输入8,将之前添加的TCP协议的12345外部端口删除。
在UPnP Wizard上点击刷新,可以看到已经成功删除,再次执行回环测试程序,已经无法连接上内部的W5500上。
总结
本文讲解了如何在 W5500 芯片上实现 UPnP 协议的端口转发功能,通过实战例程详细展示了从设备搜索、获取设备描述、 订阅事件到添加和删除端口映射的完整流程,包括各步骤涉及的协议报文、函数实现和具体操作。 文章还对 UPnP 协议的简介、特点、应用场景进行了分析,帮助读者理解其在网络设备互联互通中的实际应用价值。 下一篇文章将聚焦 TFTP 协议,解析其核心原理及在文件传输中的应用,同时讲解如何在W5500上实现 TFTP 功能,敬请期待!