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的相关介绍:
UPnP协议编程实践(1)
UPnP协议编程实践(2)
正文
在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
");
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
");
DPRINTF(E_WARN, L_GENERAL, "HTTP listening on port %d
", 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
");
进入一个标准的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
", 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
", strerror(errno));
DPRINTF(E_FATAL, L_GENERAL, "Failed to select open sockets. EXITING
");
}
upnpevents_processfds(&readset, &writeset);
/* process SSDP packets */
if (sudp >= 0 && FD_ISSET(sudp, &readset))
{
/*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet
");*/
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
", strerror(errno));
}
else
{
struct upnphttp * tmp = 0;
DPRINTF(E_DEBUG, L_GENERAL, "HTTP connection from %s:%d
",
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
");
}*/
/* 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
");
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
");
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
");*/
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
", 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>
"
"<NumberReturned>%u</NumberReturned>
"
"<TotalMatches>%u</TotalMatches>
"
"<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 parentID="64" restricted="1" ><dc:title>android-14</dc:title><upnp:class>object.container.storageFolder</upnp:class></container>< container parentID="64" restricted="1" ><dc:title>armeabi-v7a</dc:title><upnp:class>object.container.storageFolder</upnp:class></container>< container parentID="64" restricted="1" ><dc:title>libwnck-2.22.0</dc:title><upnp:class>object.container.storageFolder</upnp:class></container>< container 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服务。