前言
minidlna是一种优秀的DLNA解决方案。本文将涉及minidlna的upnp以及目录管理的代码。minidlna的下载链接如下:
wget http://netcologne.dl.sourceforge.net/project/minidlna/minidlna/1.1.0/minidlna-1.1.0.tar.gz
控制点使用VLC Media Player,下载链接如下:
http://www.videolan.org/vlc/index.zh.html#download
关于minidlna的配置,网上已有很多介绍,在这里就不复述了。
本文中一些关于UPNP的理论问题参考了IBM的相关介绍:
正文
在minidlna,本文描述的主要内容分布在minidlna.c(主程序),inotify.c(目录管理),upnphttp.c(upnp通信),minissdp.c(ssdp设备发现相关),upnpsoap.c(soap设备控制相关)等。
照例从main函数进入,这个在~/minidlna.c下。程序首先执行了init,open_db等方法:
-
ret = init(argc, argv);
//这里主要分析配置文件以及命令中的选项
-
//......
-
LIST_INIT(&upnphttphead);
//初始化upnphttphead
-
ret = open_db(
NULL);
//新建sqlite3 db
-
//......
-
check_db(db, ret, &scanner_pid);
新建连接用socket:
-
sudp = OpenAndConfSSDPReceiveSocket();
//新建一个socket,执行setsockopt并且bind之, sudp就是返回的socket , 端口号SSDP_PORT(1900), 用于接受控制点信息
-
if (sudp <
0)
-
{
-
DPRINTF(E_INFO, L_GENERAL,
"Failed to open socket for receiving SSDP. Trying to use MiniSSDPd\n");
-
if (SubmitServicesToMiniSSDPD(lan_addr[
0].str, runtime_vars.port) <
0)
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to connect to MiniSSDPd. EXITING");
-
}
-
/* open socket for HTTP connections. Listen on the 1st LAN address */
-
shttpl = OpenAndConfHTTPSocket(runtime_vars.port);
//新建一个socket,执行setsockopt并且bind之, shttpl就是返回的socket , 端口号runtime_vars.port = 8200 , 它来自minidlna.conf
-
if (shttpl <
0)
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to open socket for HTTP. EXITING\n");
-
DPRINTF(E_WARN, L_GENERAL,
"HTTP listening on port %d\n", runtime_vars.port);
-
/* open socket for sending notifications */
-
if (OpenAndConfSSDPNotifySockets(snotify) <
0)
//初始化n_lan_addr个广播用socket
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to open sockets for sending SSDP notify "
-
"messages. EXITING\n");
进入一个标准的select模型:
-
while (!quitting)
//init quitting = 0
-
{
-
/* Check if we need to send SSDP NOTIFY messages and do it if
-
* needed */
-
if (gettimeofday(&timeofday,
0) <
0)
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"gettimeofday(): %s\n", strerror(errno));
-
timeout.tv_sec = runtime_vars.notify_interval;
-
timeout.tv_usec =
0;
-
}
-
else
-
{
-
/* the comparison is not very precise but who cares ? */
-
if (timeofday.tv_sec >= (lastnotifytime.tv_sec + runtime_vars.notify_interval))
//如果超时
-
{
-
SendSSDPNotifies2(snotify,
-
(
unsigned
short)runtime_vars.port,
-
(runtime_vars.notify_interval <<
1)+
10);
//心跳广播ssdp:alive消息,通知其他接入点自己就绪
-
memcpy(&lastnotifytime, &timeofday,
sizeof(struct timeval));
-
timeout.tv_sec = runtime_vars.notify_interval;
-
timeout.tv_usec =
0;
-
}
-
else
-
{
-
timeout.tv_sec = lastnotifytime.tv_sec + runtime_vars.notify_interval
-
- timeofday.tv_sec;
-
if (timeofday.tv_usec > lastnotifytime.tv_usec)
-
{
-
timeout.tv_usec =
1000000 + lastnotifytime.tv_usec
-
- timeofday.tv_usec;
-
timeout.tv_sec--;
-
}
-
else
-
timeout.tv_usec = lastnotifytime.tv_usec - timeofday.tv_usec;
-
-
//..............
-
-
FD_ZERO(&readset);
-
-
if (sudp >=
0)
-
{
-
FD_SET(sudp, &readset);
//将sudp加入readset
-
max_fd = MAX(max_fd, sudp);
-
}
-
-
if (shttpl >=
0)
-
{
-
FD_SET(shttpl, &readset);
//将shttpl加入readset
-
max_fd = MAX(max_fd, shttpl);
-
}
-
-
//......
-
i =
0;
/* active HTTP connections count */
-
// struct upnphttp *e
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2))
-
{
-
FD_SET(e->socket, &readset);
//添加记录的socket进入readset
-
max_fd = MAX(max_fd, e->socket);
-
i++;
-
}
-
}
-
-
//.......
-
-
FD_ZERO(&writeset);
-
upnpevents_selectfds(&readset, &writeset, &max_fd);
-
ret = select(max_fd+
1, &readset, &writeset,
0, &timeout);
-
if (ret <
0)
-
{
-
if(quitting)
goto shutdown;
-
if(errno == EINTR)
continue;
-
DPRINTF(E_ERROR, L_GENERAL,
"select(all): %s\n", strerror(errno));
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to select open sockets. EXITING\n");
-
}
-
upnpevents_processfds(&readset, &writeset);
-
-
/* process SSDP packets */
-
if (sudp >=
0 && FD_ISSET(sudp, &readset))
-
{
-
/*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/
-
ProcessSSDPRequest(sudp, (
unsigned
short)runtime_vars.port);
//接受控制点传来的ssdp信息,并回传给控制点设备描述信息
-
}
-
-
//......
-
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2) && (FD_ISSET(e->socket, &readset)))
-
{
-
Process_upnphttp(e);
//这里会回送消息给控制点( 设备信息xml或远程目录信息等), port:8200
-
}
-
}
-
/* process incoming HTTP connections */
-
if (shttpl >=
0 && FD_ISSET(shttpl, &readset))
-
{
-
int shttp;
-
socklen_t clientnamelen;
-
struct sockaddr_in clientname;
-
clientnamelen =
sizeof(struct sockaddr_in);
-
shttp = accept(shttpl, (struct sockaddr *)&clientname, &clientnamelen);
//获取远程socket shttp
-
if (shttp<
0)
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"accept(http): %s\n", strerror(errno));
-
}
-
else
-
{
-
struct upnphttp * tmp =
0;
-
DPRINTF(E_DEBUG, L_GENERAL,
"HTTP connection from %s:%d\n",
-
inet_ntoa(clientname.sin_addr),
-
ntohs(clientname.sin_port) );
-
/*if (fcntl(shttp, F_SETFL, O_NONBLOCK) < 0) {
-
DPRINTF(E_ERROR, L_GENERAL, "fcntl F_SETFL, O_NONBLOCK\n");
-
}*/
-
/* Create a new upnphttp object and add it to
-
* the active upnphttp object list */
-
tmp = New_upnphttp(shttp);
//初始化 struct upnphttp ,并且将shttp赋予其socket字段
-
if (tmp)
-
{
-
tmp->clientaddr = clientname.sin_addr;
-
LIST_INSERT_HEAD(&upnphttphead, tmp, entries);
//将tmp插入链表upnphttphead中
-
}
-
else
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"New_upnphttp() failed\n");
-
close(shttp);
-
}
-
}
-
}
-
-
//......
-
-
}
设备发现是UPnP网络实现的第一步。在这里,minidlna启动后,本机作为一个设备加入到网络中,设备发现过程允许设备向网络上的控制点告知它提供的服务(ssdp:alive)。当一个控制点加入到网络中时,设备发现过程允许控制点寻找网络上感兴趣的设备(ssdp:discover)。在这两种情况下,基本的交换信息就是发现消息。发现消息包括设备的一些特定信息或者某项服务的信息,例如它的类型、标识符、和指向XML设备描述文档的指针。简单发现协议(SSDP)定义了在网络中发现网络服务,控制点定位网络上相关资源和设备在网络上声明其可用性的方法。在上面的select模型中,程序通过定时执行SendSSDPNotifies2方法,广播设备就绪消息(心跳包),它的实现如下:
-
void
-
SendSSDPNotifies2
(int *sockets,
-
unsigned
short port,
-
unsigned
int lifetime)
-
{
-
int i;
-
DPRINTF(E_DEBUG, L_SSDP,
"Sending SSDP notifies\n");
-
for (i =
0; i < n_lan_addr; i++)
//向本地的网络接口循环发送ssdp:alive消息
-
{
-
SendSSDPNotifies(sockets[i], lan_addr[i].str, port, lifetime);
//发送ssdp:alive
-
}
-
}
发送的ssdp:alive消息格式如下:
NOTIFY * HTTP/1.1
HOST:239.255.255.250:1900 #协议保留多播地址和端口,必须是239.255.255.250:1900
CACHE-CONTROL:max-age=1810 #max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
LOCATION:http://192.168.1.20:8200/rootDesc.xml #包含根设备描述得URL地址
SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
NT:upnp:rootdevice #在此消息中,NT头必须为服务的服务类型
USN:uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力
NTS:ssdp:alive #表示通知消息的子类型,必须为ssdp:alive
UPnP网络结构的第二步是设备描述。在控制点发现了一个设备之后,控制点仍然对设备知之甚少,控制点可能仅仅知道设备或服务的UPnP类型,设备的UUID和设备描述的URL地址。为了让控制点更多的了解设备和它的功能或者与设备交互,控制点必须从发现消息中得到设备描述的URL,通过URL取回设备描述。
在程序中,我们发送完ssdp:alive广播后,网络上的控制点就会发送相应的消息到程序,在上边的select模型中,我们会通过以下程序接收控制点传来的ssdp消息:
-
if (sudp >=
0 && FD_ISSET(sudp, &readset))
-
{
-
/*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/
-
ProcessSSDPRequest(sudp, (
unsigned
short)runtime_vars.port);
//接受控制点传来的ssdp信息,并回传给控制点设备描述信息
-
}
在ProcessSSDPRequest中实现了接收控制点传来的消息,以及回传给控制点的信息(设备描述URL),接收的控制点消息格式如下(ssdp:discover):
M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900 #设置为协议保留多播地址和端口,必须是239.255.255.250:1900。
Man: "ssdp:discover" #设置协议查询的类型,必须是"ssdp:discover"。
MX: 5 #设置设备响应最长等待时间,设备响应在0和这个值之间随机选择响应延迟的值。这样可以为控制点响应平衡网络负载。
ST: upnp:rootdevice #设置服务查询的目标
回传给控制点的消息格式如下:
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1810 #max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
DATE: Tue, 11 Feb 2014 08:16:14 GMT #指定响应生成的时间
ST: upnp:rootdevice #内容和意义与查询请求的相应字段相同
USN: uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力。
EXT: #向控制点确认MAN头域已经被设备理解
SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
LOCATION: http://192.168.1.20:8200/rootDesc.xml #包含根设备描述得URL地址
Content-Length: 0
设备控制是UPnP网络的第三步。在接收设备和服务描述之后,控制点可以向这些服务发出动作,同时控制点也可以轮询服务的状态变量值。发出动作实质上是一种远程过程调用,控制点将动作送到设备服务,在动作完成之后,服务返回相应的结果。在这里,我们利用minidlna的基本功能——远程目录浏览,来说明。当我们在控制点VLC Media Player中点击“通用即插即播”,它会自动完成前面描述的设备发现和设备描述,显示可用的设备信息列表(在这里,可用设备就是minidlna服务)
点击这里的Jane,就会显示minidlna设备指定的目录下的目录信息。当我们做这些操作的时候,控制点正在向minidlna设备发送请求消息。这个请求的格式如下:
POST /ctl/ContentDir HTTP/1.1
HOST: 192.168.1.20:8200
CONTENT-LENGTH: 488
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
USER-AGENT: 6.1.7600 2/, UPnP/1.0, Portable SDK for UPnP devices/1.6.18
<s:Envelope xmlns:s=“http://schemas.xmlsoap.org/soap/envelope/” s:encodingStyle=“http://schemas.xmlsoap.org/soap/encoding/”>
<s:Body><u:Browse xmlns:u=“urn:schemas-upnp-org:service:ContentDirectory:1”>
<ObjectID>64$4</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>id,dc:title,res,sec:CaptionInfo,sec:CaptionInfoEx</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>0</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>注意这里SOAPACTION: “urn:schemas-upnp-org:service:ContentDirectory:1#Browse”,Browse将决定我们远程执行何种方法(有点类似信令)。在上边的select模型中,我们收到该请求:
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2) && (FD_ISSET(e->socket, &readset)))
-
{
-
Process_upnphttp(e);
//这里会回送消息给控制点( 设备信息xml或远程目录信息等), port:8200
-
}
-
}
Process_upnphttp会在底层调用upnpsoap.c中的ExecuteSoapAction方法,在upnpsoap.c定义了相关信令和它们对应的方法,如下:
-
static
const
struct
-
{
-
const
char * methodName;
-
void (*methodImpl)(struct upnphttp *,
const
char *);
-
}
-
soapMethods[] =
-
{
-
{
“QueryStateVariable”, QueryStateVariable},
-
{
“Browse”, BrowseContentDirectory},
-
{
“Search”, SearchContentDirectory},
-
{
“GetSearchCapabilities”, GetSearchCapabilities},
-
{
“GetSortCapabilities”, GetSortCapabilities},
-
{
“GetSystemUpdateID”, GetSystemUpdateID},
-
{
“GetProtocolInfo”, GetProtocolInfo},
-
{
“GetCurrentConnectionIDs”, GetCurrentConnectionIDs},
-
{
“GetCurrentConnectionInfo”, GetCurrentConnectionInfo},
-
{
“IsAuthorized”, IsAuthorizedValidated},
-
{
“IsValidated”, IsAuthorizedValidated},
-
{
“X_GetFeatureList”, SamsungGetFeatureList},
-
{
“X_SetBookmark”, SamsungSetBookmark},
-
{
0,
0 }
-
};
更具对应关系,ExecuteSoapAction会再调用BrowseContentDirectory方法。BrowseContentDirectory中会搜索sqlite中的目录信息,将信息拼接出xml字符串,代码如下:
-
static void
-
BrowseContentDirectory
(struct upnphttp * h, const char * action)
-
{
-
static
const
char resp0[] =
-
"<u:BrowseResponse "
-
"xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
-
"<Result>"
-
"<DIDL-Lite"
-
-
//......
-
-
sql = sqlite3_mprintf( SELECT_COLUMNS
-
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
-
" where PARENT_ID = '%q' %s limit %d, %d;",
-
ObjectID, orderBy, StartingIndex, RequestedCount);
-
DPRINTF(E_DEBUG, L_HTTP,
"Browse SQL: %s\n", sql);
-
/*
-
* SELECT o.OBJECT_ID, o.PARENT_ID, o.REF_ID, o.DETAIL_ID, o.CLASS, d.SIZE, d.TITLE, d.DURATION,
-
* d.BITRATE, d.SAMPLERATE, d.ARTIST, d.ALBUM, d.GENRE, d.COMMENT, d.CHANNELS, d.TRACK, d.DATE,
-
* d.RESOLUTION, d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.DISC from OBJECTS o
-
* left join DETAILS d on (d.ID = o.DETAIL_ID) where PARENT_ID = '0' limit 0, -1;
-
*/
-
ret = sqlite3_exec(db, sql, callback, (
void *) &args, &zErrMsg);
//查询目录信息
-
// ......
-
-
ret = strcatf(&str,
"</DIDL-Lite></Result>\n"
-
"<NumberReturned>%u</NumberReturned>\n"
-
"<TotalMatches>%u</TotalMatches>\n"
-
"<UpdateID>%u</UpdateID>"
-
"</u:BrowseResponse>",
-
args.returned, totalMatches, updateID);
//拼接xml字符串
-
-
BuildSendAndCloseSoapResp(h, str.data, str.off);
//回送给控制点xml字符串消息
-
-
//......
-
}
通过BuildSendAndCloseSoapResp回传给控制点,这个xml字符串格式如下:
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result><DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><
container id="64$0" parentID="64" restricted="1" ><dc:title>android-14</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$1" parentID="64" restricted="1" ><dc:title>armeabi-v7a</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$2" parentID="64" restricted="1" ><dc:title>libwnck-2.22.0</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$3" parentID="64" restricted="1" ><dc:title>voice-client-example</dc:title><upnp:class>object.container.storageFolder</upnp:class></container></DIDL-Lite>
</Result>
<NumberReturned>6</NumberReturned>
<TotalMatches>6</TotalMatches>
<UpdateID>10</UpdateID></u:BrowseResponse>
这个xml字符串,说明minidlna指定的目录下有android-14,armeabi-v7a,libwnck-2.22.0和voice-client-example等4个目录。控制点通过这一信息获取minidlna服务。
minidlna源码初探(一)
前言
minidlna是一种优秀的DLNA解决方案。本文将涉及minidlna的upnp以及目录管理的代码。minidlna的下载链接如下:
wget http://netcologne.dl.sourceforge.net/project/minidlna/minidlna/1.1.0/minidlna-1.1.0.tar.gz
控制点使用VLC Media Player,下载链接如下:
http://www.videolan.org/vlc/index.zh.html#download
关于minidlna的配置,网上已有很多介绍,在这里就不复述了。
本文中一些关于UPNP的理论问题参考了IBM的相关介绍:
正文
在minidlna,本文描述的主要内容分布在minidlna.c(主程序),inotify.c(目录管理),upnphttp.c(upnp通信),minissdp.c(ssdp设备发现相关),upnpsoap.c(soap设备控制相关)等。
照例从main函数进入,这个在~/minidlna.c下。程序首先执行了init,open_db等方法:
-
ret = init(argc, argv);
//这里主要分析配置文件以及命令中的选项
-
//......
-
LIST_INIT(&upnphttphead);
//初始化upnphttphead
-
ret = open_db(
NULL);
//新建sqlite3 db
-
//......
-
check_db(db, ret, &scanner_pid);
新建连接用socket:
-
sudp = OpenAndConfSSDPReceiveSocket();
//新建一个socket,执行setsockopt并且bind之, sudp就是返回的socket , 端口号SSDP_PORT(1900), 用于接受控制点信息
-
if (sudp <
0)
-
{
-
DPRINTF(E_INFO, L_GENERAL,
"Failed to open socket for receiving SSDP. Trying to use MiniSSDPd\n");
-
if (SubmitServicesToMiniSSDPD(lan_addr[
0].str, runtime_vars.port) <
0)
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to connect to MiniSSDPd. EXITING");
-
}
-
/* open socket for HTTP connections. Listen on the 1st LAN address */
-
shttpl = OpenAndConfHTTPSocket(runtime_vars.port);
//新建一个socket,执行setsockopt并且bind之, shttpl就是返回的socket , 端口号runtime_vars.port = 8200 , 它来自minidlna.conf
-
if (shttpl <
0)
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to open socket for HTTP. EXITING\n");
-
DPRINTF(E_WARN, L_GENERAL,
"HTTP listening on port %d\n", runtime_vars.port);
-
/* open socket for sending notifications */
-
if (OpenAndConfSSDPNotifySockets(snotify) <
0)
//初始化n_lan_addr个广播用socket
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to open sockets for sending SSDP notify "
-
"messages. EXITING\n");
进入一个标准的select模型:
-
while (!quitting)
//init quitting = 0
-
{
-
/* Check if we need to send SSDP NOTIFY messages and do it if
-
* needed */
-
if (gettimeofday(&timeofday,
0) <
0)
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"gettimeofday(): %s\n", strerror(errno));
-
timeout.tv_sec = runtime_vars.notify_interval;
-
timeout.tv_usec =
0;
-
}
-
else
-
{
-
/* the comparison is not very precise but who cares ? */
-
if (timeofday.tv_sec >= (lastnotifytime.tv_sec + runtime_vars.notify_interval))
//如果超时
-
{
-
SendSSDPNotifies2(snotify,
-
(
unsigned
short)runtime_vars.port,
-
(runtime_vars.notify_interval <<
1)+
10);
//心跳广播ssdp:alive消息,通知其他接入点自己就绪
-
memcpy(&lastnotifytime, &timeofday,
sizeof(struct timeval));
-
timeout.tv_sec = runtime_vars.notify_interval;
-
timeout.tv_usec =
0;
-
}
-
else
-
{
-
timeout.tv_sec = lastnotifytime.tv_sec + runtime_vars.notify_interval
-
- timeofday.tv_sec;
-
if (timeofday.tv_usec > lastnotifytime.tv_usec)
-
{
-
timeout.tv_usec =
1000000 + lastnotifytime.tv_usec
-
- timeofday.tv_usec;
-
timeout.tv_sec--;
-
}
-
else
-
timeout.tv_usec = lastnotifytime.tv_usec - timeofday.tv_usec;
-
-
//..............
-
-
FD_ZERO(&readset);
-
-
if (sudp >=
0)
-
{
-
FD_SET(sudp, &readset);
//将sudp加入readset
-
max_fd = MAX(max_fd, sudp);
-
}
-
-
if (shttpl >=
0)
-
{
-
FD_SET(shttpl, &readset);
//将shttpl加入readset
-
max_fd = MAX(max_fd, shttpl);
-
}
-
-
//......
-
i =
0;
/* active HTTP connections count */
-
// struct upnphttp *e
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2))
-
{
-
FD_SET(e->socket, &readset);
//添加记录的socket进入readset
-
max_fd = MAX(max_fd, e->socket);
-
i++;
-
}
-
}
-
-
//.......
-
-
FD_ZERO(&writeset);
-
upnpevents_selectfds(&readset, &writeset, &max_fd);
-
ret = select(max_fd+
1, &readset, &writeset,
0, &timeout);
-
if (ret <
0)
-
{
-
if(quitting)
goto shutdown;
-
if(errno == EINTR)
continue;
-
DPRINTF(E_ERROR, L_GENERAL,
"select(all): %s\n", strerror(errno));
-
DPRINTF(E_FATAL, L_GENERAL,
"Failed to select open sockets. EXITING\n");
-
}
-
upnpevents_processfds(&readset, &writeset);
-
-
/* process SSDP packets */
-
if (sudp >=
0 && FD_ISSET(sudp, &readset))
-
{
-
/*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/
-
ProcessSSDPRequest(sudp, (
unsigned
short)runtime_vars.port);
//接受控制点传来的ssdp信息,并回传给控制点设备描述信息
-
}
-
-
//......
-
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2) && (FD_ISSET(e->socket, &readset)))
-
{
-
Process_upnphttp(e);
//这里会回送消息给控制点( 设备信息xml或远程目录信息等), port:8200
-
}
-
}
-
/* process incoming HTTP connections */
-
if (shttpl >=
0 && FD_ISSET(shttpl, &readset))
-
{
-
int shttp;
-
socklen_t clientnamelen;
-
struct sockaddr_in clientname;
-
clientnamelen =
sizeof(struct sockaddr_in);
-
shttp = accept(shttpl, (struct sockaddr *)&clientname, &clientnamelen);
//获取远程socket shttp
-
if (shttp<
0)
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"accept(http): %s\n", strerror(errno));
-
}
-
else
-
{
-
struct upnphttp * tmp =
0;
-
DPRINTF(E_DEBUG, L_GENERAL,
"HTTP connection from %s:%d\n",
-
inet_ntoa(clientname.sin_addr),
-
ntohs(clientname.sin_port) );
-
/*if (fcntl(shttp, F_SETFL, O_NONBLOCK) < 0) {
-
DPRINTF(E_ERROR, L_GENERAL, "fcntl F_SETFL, O_NONBLOCK\n");
-
}*/
-
/* Create a new upnphttp object and add it to
-
* the active upnphttp object list */
-
tmp = New_upnphttp(shttp);
//初始化 struct upnphttp ,并且将shttp赋予其socket字段
-
if (tmp)
-
{
-
tmp->clientaddr = clientname.sin_addr;
-
LIST_INSERT_HEAD(&upnphttphead, tmp, entries);
//将tmp插入链表upnphttphead中
-
}
-
else
-
{
-
DPRINTF(E_ERROR, L_GENERAL,
"New_upnphttp() failed\n");
-
close(shttp);
-
}
-
}
-
}
-
-
//......
-
-
}
设备发现是UPnP网络实现的第一步。在这里,minidlna启动后,本机作为一个设备加入到网络中,设备发现过程允许设备向网络上的控制点告知它提供的服务(ssdp:alive)。当一个控制点加入到网络中时,设备发现过程允许控制点寻找网络上感兴趣的设备(ssdp:discover)。在这两种情况下,基本的交换信息就是发现消息。发现消息包括设备的一些特定信息或者某项服务的信息,例如它的类型、标识符、和指向XML设备描述文档的指针。简单发现协议(SSDP)定义了在网络中发现网络服务,控制点定位网络上相关资源和设备在网络上声明其可用性的方法。在上面的select模型中,程序通过定时执行SendSSDPNotifies2方法,广播设备就绪消息(心跳包),它的实现如下:
-
void
-
SendSSDPNotifies2
(int *sockets,
-
unsigned
short port,
-
unsigned
int lifetime)
-
{
-
int i;
-
DPRINTF(E_DEBUG, L_SSDP,
"Sending SSDP notifies\n");
-
for (i =
0; i < n_lan_addr; i++)
//向本地的网络接口循环发送ssdp:alive消息
-
{
-
SendSSDPNotifies(sockets[i], lan_addr[i].str, port, lifetime);
//发送ssdp:alive
-
}
-
}
发送的ssdp:alive消息格式如下:
NOTIFY * HTTP/1.1
HOST:239.255.255.250:1900 #协议保留多播地址和端口,必须是239.255.255.250:1900
CACHE-CONTROL:max-age=1810 #max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
LOCATION:http://192.168.1.20:8200/rootDesc.xml #包含根设备描述得URL地址
SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
NT:upnp:rootdevice #在此消息中,NT头必须为服务的服务类型
USN:uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力
NTS:ssdp:alive #表示通知消息的子类型,必须为ssdp:alive
UPnP网络结构的第二步是设备描述。在控制点发现了一个设备之后,控制点仍然对设备知之甚少,控制点可能仅仅知道设备或服务的UPnP类型,设备的UUID和设备描述的URL地址。为了让控制点更多的了解设备和它的功能或者与设备交互,控制点必须从发现消息中得到设备描述的URL,通过URL取回设备描述。
在程序中,我们发送完ssdp:alive广播后,网络上的控制点就会发送相应的消息到程序,在上边的select模型中,我们会通过以下程序接收控制点传来的ssdp消息:
-
if (sudp >=
0 && FD_ISSET(sudp, &readset))
-
{
-
/*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/
-
ProcessSSDPRequest(sudp, (
unsigned
short)runtime_vars.port);
//接受控制点传来的ssdp信息,并回传给控制点设备描述信息
-
}
在ProcessSSDPRequest中实现了接收控制点传来的消息,以及回传给控制点的信息(设备描述URL),接收的控制点消息格式如下(ssdp:discover):
M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900 #设置为协议保留多播地址和端口,必须是239.255.255.250:1900。
Man: "ssdp:discover" #设置协议查询的类型,必须是"ssdp:discover"。
MX: 5 #设置设备响应最长等待时间,设备响应在0和这个值之间随机选择响应延迟的值。这样可以为控制点响应平衡网络负载。
ST: upnp:rootdevice #设置服务查询的目标
回传给控制点的消息格式如下:
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1810 #max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在
DATE: Tue, 11 Feb 2014 08:16:14 GMT #指定响应生成的时间
ST: upnp:rootdevice #内容和意义与查询请求的相应字段相同
USN: uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力。
EXT: #向控制点确认MAN头域已经被设备理解
SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
LOCATION: http://192.168.1.20:8200/rootDesc.xml #包含根设备描述得URL地址
Content-Length: 0
设备控制是UPnP网络的第三步。在接收设备和服务描述之后,控制点可以向这些服务发出动作,同时控制点也可以轮询服务的状态变量值。发出动作实质上是一种远程过程调用,控制点将动作送到设备服务,在动作完成之后,服务返回相应的结果。在这里,我们利用minidlna的基本功能——远程目录浏览,来说明。当我们在控制点VLC Media Player中点击“通用即插即播”,它会自动完成前面描述的设备发现和设备描述,显示可用的设备信息列表(在这里,可用设备就是minidlna服务)
点击这里的Jane,就会显示minidlna设备指定的目录下的目录信息。当我们做这些操作的时候,控制点正在向minidlna设备发送请求消息。这个请求的格式如下:
POST /ctl/ContentDir HTTP/1.1
HOST: 192.168.1.20:8200
CONTENT-LENGTH: 488
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
USER-AGENT: 6.1.7600 2/, UPnP/1.0, Portable SDK for UPnP devices/1.6.18
<s:Envelope xmlns:s=“http://schemas.xmlsoap.org/soap/envelope/” s:encodingStyle=“http://schemas.xmlsoap.org/soap/encoding/”>
<s:Body><u:Browse xmlns:u=“urn:schemas-upnp-org:service:ContentDirectory:1”>
<ObjectID>64$4</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>id,dc:title,res,sec:CaptionInfo,sec:CaptionInfoEx</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>0</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>注意这里SOAPACTION: “urn:schemas-upnp-org:service:ContentDirectory:1#Browse”,Browse将决定我们远程执行何种方法(有点类似信令)。在上边的select模型中,我们收到该请求:
-
for (e = upnphttphead.lh_first; e !=
NULL; e = e->entries.le_next)
-
{
-
if ((e->socket >=
0) && (e->state <=
2) && (FD_ISSET(e->socket, &readset)))
-
{
-
Process_upnphttp(e);
//这里会回送消息给控制点( 设备信息xml或远程目录信息等), port:8200
-
}
-
}
Process_upnphttp会在底层调用upnpsoap.c中的ExecuteSoapAction方法,在upnpsoap.c定义了相关信令和它们对应的方法,如下:
-
static
const
struct
-
{
-
const
char * methodName;
-
void (*methodImpl)(struct upnphttp *,
const
char *);
-
}
-
soapMethods[] =
-
{
-
{
“QueryStateVariable”, QueryStateVariable},
-
{
“Browse”, BrowseContentDirectory},
-
{
“Search”, SearchContentDirectory},
-
{
“GetSearchCapabilities”, GetSearchCapabilities},
-
{
“GetSortCapabilities”, GetSortCapabilities},
-
{
“GetSystemUpdateID”, GetSystemUpdateID},
-
{
“GetProtocolInfo”, GetProtocolInfo},
-
{
“GetCurrentConnectionIDs”, GetCurrentConnectionIDs},
-
{
“GetCurrentConnectionInfo”, GetCurrentConnectionInfo},
-
{
“IsAuthorized”, IsAuthorizedValidated},
-
{
“IsValidated”, IsAuthorizedValidated},
-
{
“X_GetFeatureList”, SamsungGetFeatureList},
-
{
“X_SetBookmark”, SamsungSetBookmark},
-
{
0,
0 }
-
};
更具对应关系,ExecuteSoapAction会再调用BrowseContentDirectory方法。BrowseContentDirectory中会搜索sqlite中的目录信息,将信息拼接出xml字符串,代码如下:
-
static void
-
BrowseContentDirectory
(struct upnphttp * h, const char * action)
-
{
-
static
const
char resp0[] =
-
"<u:BrowseResponse "
-
"xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">"
-
"<Result>"
-
"<DIDL-Lite"
-
-
//......
-
-
sql = sqlite3_mprintf( SELECT_COLUMNS
-
"from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)"
-
" where PARENT_ID = '%q' %s limit %d, %d;",
-
ObjectID, orderBy, StartingIndex, RequestedCount);
-
DPRINTF(E_DEBUG, L_HTTP,
"Browse SQL: %s\n", sql);
-
/*
-
* SELECT o.OBJECT_ID, o.PARENT_ID, o.REF_ID, o.DETAIL_ID, o.CLASS, d.SIZE, d.TITLE, d.DURATION,
-
* d.BITRATE, d.SAMPLERATE, d.ARTIST, d.ALBUM, d.GENRE, d.COMMENT, d.CHANNELS, d.TRACK, d.DATE,
-
* d.RESOLUTION, d.THUMBNAIL, d.CREATOR, d.DLNA_PN, d.MIME, d.ALBUM_ART, d.DISC from OBJECTS o
-
* left join DETAILS d on (d.ID = o.DETAIL_ID) where PARENT_ID = '0' limit 0, -1;
-
*/
-
ret = sqlite3_exec(db, sql, callback, (
void *) &args, &zErrMsg);
//查询目录信息
-
// ......
-
-
ret = strcatf(&str,
"</DIDL-Lite></Result>\n"
-
"<NumberReturned>%u</NumberReturned>\n"
-
"<TotalMatches>%u</TotalMatches>\n"
-
"<UpdateID>%u</UpdateID>"
-
"</u:BrowseResponse>",
-
args.returned, totalMatches, updateID);
//拼接xml字符串
-
-
BuildSendAndCloseSoapResp(h, str.data, str.off);
//回送给控制点xml字符串消息
-
-
//......
-
}
通过BuildSendAndCloseSoapResp回传给控制点,这个xml字符串格式如下:
<u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result><DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><
container id="64$0" parentID="64" restricted="1" ><dc:title>android-14</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$1" parentID="64" restricted="1" ><dc:title>armeabi-v7a</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$2" parentID="64" restricted="1" ><dc:title>libwnck-2.22.0</dc:title><upnp:class>object.container.storageFolder</upnp:class></container><
container id="64$3" parentID="64" restricted="1" ><dc:title>voice-client-example</dc:title><upnp:class>object.container.storageFolder</upnp:class></container></DIDL-Lite>
</Result>
<NumberReturned>6</NumberReturned>
<TotalMatches>6</TotalMatches>
<UpdateID>10</UpdateID></u:BrowseResponse>
这个xml字符串,说明minidlna指定的目录下有android-14,armeabi-v7a,libwnck-2.22.0和voice-client-example等4个目录。控制点通过这一信息获取minidlna服务。