前言
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\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服务。