前言
minidlna是一種優(yōu)秀的DLNA解決方案。本文將涉及minidlna的upnp以及目錄管理的代碼。minidlna的下載鏈接如下:
- wget http://netcologne.dl./project/minidlna/minidlna/1.1.0/minidlna-1.1.0.tar.gz
控制點(diǎn)使用VLC Media Player,下載鏈接如下:
- http://www./vlc/index.zh.html#download
關(guān)于minidlna的配置,網(wǎng)上已有很多介紹,在這里就不復(fù)述了。
本文中一些關(guān)于UPNP的理論問題參考了IBM的相關(guān)介紹:
UPnP協(xié)議編程實(shí)踐(1)
UPnP協(xié)議編程實(shí)踐(2)
正文
在minidlna,本文描述的主要內(nèi)容分布在minidlna.c(主程序),inotify.c(目錄管理),upnphttp.c(upnp通信),minissdp.c(ssdp設(shè)備發(fā)現(xiàn)相關(guān)),upnpsoap.c(soap設(shè)備控制相關(guān))等。
照例從main函數(shù)進(jìn)入,這個在~/minidlna.c下。程序首先執(zhí)行了init,open_db等方法:
- ret = init(argc, argv); //這里主要分析配置文件以及命令中的選項(xiàng)
- //......
- LIST_INIT(&upnphttphead); //初始化upnphttphead
- ret = open_db(NULL); //新建sqlite3 db
- //......
- check_db(db, ret, &scanner_pid);
新建連接用socket:
- sudp = OpenAndConfSSDPReceiveSocket(); //新建一個socket,執(zhí)行setsockopt并且bind之, sudp就是返回的socket , 端口號SSDP_PORT(1900), 用于接受控制點(diǎn)信息
- 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,執(zhí)行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");
進(jìn)入一個標(biāo)準(zhǔ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消息,通知其他接入點(diǎn)自己就緒
- 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進(jìn)入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); //接受控制點(diǎn)傳來的ssdp信息,并回傳給控制點(diǎn)設(shè)備描述信息
- }
-
- //......
-
- 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); //這里會回送消息給控制點(diǎn)( 設(shè)備信息xml或遠(yuǎn)程目錄信息等), 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); //獲取遠(yuǎn)程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);
- }
- }
- }
-
- //......
-
- }
設(shè)備發(fā)現(xiàn)是UPnP網(wǎng)絡(luò)實(shí)現(xiàn)的第一步。在這里,minidlna啟動后,本機(jī)作為一個設(shè)備加入到網(wǎng)絡(luò)中,設(shè)備發(fā)現(xiàn)過程允許設(shè)備向網(wǎng)絡(luò)上的控制點(diǎn)告知它提供的服務(wù)(ssdp:alive)。當(dāng)一個控制點(diǎn)加入到網(wǎng)絡(luò)中時,設(shè)備發(fā)現(xiàn)過程允許控制點(diǎn)尋找網(wǎng)絡(luò)上感興趣的設(shè)備(ssdp:discover)。在這兩種情況下,基本的交換信息就是發(fā)現(xiàn)消息。發(fā)現(xiàn)消息包括設(shè)備的一些特定信息或者某項(xiàng)服務(wù)的信息,例如它的類型、標(biāo)識符、和指向XML設(shè)備描述文檔的指針。簡單發(fā)現(xiàn)協(xié)議(SSDP)定義了在網(wǎng)絡(luò)中發(fā)現(xiàn)網(wǎng)絡(luò)服務(wù),控制點(diǎn)定位網(wǎng)絡(luò)上相關(guān)資源和設(shè)備在網(wǎng)絡(luò)上聲明其可用性的方法。
在上面的select模型中,程序通過定時執(zhí)行SendSSDPNotifies2方法,廣播設(shè)備就緒消息(心跳包),它的實(shí)現(xiàn)如下:
- 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++) //向本地的網(wǎng)絡(luò)接口循環(huán)發(fā)送ssdp:alive消息
- {
- SendSSDPNotifies(sockets[i], lan_addr[i].str, port, lifetime); //發(fā)送ssdp:alive
- }
- }
發(fā)送的ssdp:alive消息格式如下:- NOTIFY * HTTP/1.1
- HOST:239.255.255.250:1900 #協(xié)議保留多播地址和端口,必須是239.255.255.250:1900
- CACHE-CONTROL:max-age=1810 #max-age指定通知消息存活時間,如果超過此時間間隔,控制點(diǎn)可以認(rèn)為設(shè)備不存在
- LOCATION:http://192.168.1.20:8200/rootDesc.xml #包含根設(shè)備描述得URL地址
- SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
- NT:upnp:rootdevice #在此消息中,NT頭必須為服務(wù)的服務(wù)類型
- USN:uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服務(wù)的統(tǒng)一服務(wù)名,它提供了一種標(biāo)識出相同類型服務(wù)的能力
- NTS:ssdp:alive #表示通知消息的子類型,必須為ssdp:alive
UPnP網(wǎng)絡(luò)結(jié)構(gòu)的第二步是設(shè)備描述。在控制點(diǎn)發(fā)現(xiàn)了一個設(shè)備之后,控制點(diǎn)仍然對設(shè)備知之甚少,控制點(diǎn)可能僅僅知道設(shè)備或服務(wù)的UPnP類型,設(shè)備的UUID和設(shè)備描述的URL地址。為了讓控制點(diǎn)更多的了解設(shè)備和它的功能或者與設(shè)備交互,控制點(diǎn)必須從發(fā)現(xiàn)消息中得到設(shè)備描述的URL,通過URL取回設(shè)備描述。
在程序中,我們發(fā)送完ssdp:alive廣播后,網(wǎng)絡(luò)上的控制點(diǎn)就會發(fā)送相應(yīng)的消息到程序,在上邊的select模型中,我們會通過以下程序接收控制點(diǎn)傳來的ssdp消息:
- if (sudp >= 0 && FD_ISSET(sudp, &readset))
- {
- /*DPRINTF(E_DEBUG, L_GENERAL, "Received UDP Packet\n");*/
- ProcessSSDPRequest(sudp, (unsigned short)runtime_vars.port); //接受控制點(diǎn)傳來的ssdp信息,并回傳給控制點(diǎn)設(shè)備描述信息
- }
在ProcessSSDPRequest中實(shí)現(xiàn)了接收控制點(diǎn)傳來的消息,以及回傳給控制點(diǎn)的信息(設(shè)備描述URL),接收的控制點(diǎn)消息格式如下(ssdp:discover):- M-SEARCH * HTTP/1.1
- Host: 239.255.255.250:1900 #設(shè)置為協(xié)議保留多播地址和端口,必須是239.255.255.250:1900。
- Man: "ssdp:discover" #設(shè)置協(xié)議查詢的類型,必須是"ssdp:discover"。
- MX: 5 #設(shè)置設(shè)備響應(yīng)最長等待時間,設(shè)備響應(yīng)在0和這個值之間隨機(jī)選擇響應(yīng)延遲的值。這樣可以為控制點(diǎn)響應(yīng)平衡網(wǎng)絡(luò)負(fù)載。
- ST: upnp:rootdevice #設(shè)置服務(wù)查詢的目標(biāo)
回傳給控制點(diǎn)的消息格式如下:- HTTP/1.1 200 OK
- CACHE-CONTROL: max-age=1810 #max-age指定通知消息存活時間,如果超過此時間間隔,控制點(diǎn)可以認(rèn)為設(shè)備不存在
- DATE: Tue, 11 Feb 2014 08:16:14 GMT #指定響應(yīng)生成的時間
- ST: upnp:rootdevice #內(nèi)容和意義與查詢請求的相應(yīng)字段相同
- USN: uuid:4d696e69-444c-164e-9d41-001ec92f0378::upnp:rootdevice #表示不同服務(wù)的統(tǒng)一服務(wù)名,它提供了一種標(biāo)識出相同類型服務(wù)的能力。
- EXT: #向控制點(diǎn)確認(rèn)MAN頭域已經(jīng)被設(shè)備理解
- SERVER: 3.4.72-rt89 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.0
- LOCATION: http://192.168.1.20:8200/rootDesc.xml #包含根設(shè)備描述得URL地址
- Content-Length: 0
設(shè)備控制是UPnP網(wǎng)絡(luò)的第三步。在接收設(shè)備和服務(wù)描述之后,控制點(diǎn)可以向這些服務(wù)發(fā)出動作,同時控制點(diǎn)也可以輪詢服務(wù)的狀態(tài)變量值。發(fā)出動作實(shí)質(zhì)上是一種遠(yuǎn)程過程調(diào)用,控制點(diǎn)將動作送到設(shè)備服務(wù),在動作完成之后,服務(wù)返回相應(yīng)的結(jié)果。在這里,我們利用minidlna的基本功能——遠(yuǎn)程目錄瀏覽,來說明。當(dāng)我們在控制點(diǎn)VLC Media Player中點(diǎn)擊“通用即插即播”,它會自動完成前面描述的設(shè)備發(fā)現(xiàn)和設(shè)備描述,顯示可用的設(shè)備信息列表(在這里,可用設(shè)備就是minidlna服務(wù))
點(diǎn)擊這里的Jane,就會顯示minidlna設(shè)備指定的目錄下的目錄信息。當(dāng)我們做這些操作的時候,控制點(diǎn)正在向minidlna設(shè)備發(fā)送請求消息。這個請求的格式如下:
- 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./soap/envelope/" s:encodingStyle="http://schemas./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將決定我們遠(yuǎn)程執(zhí)行何種方法(有點(diǎn)類似信令)。在上邊的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); //這里會回送消息給控制點(diǎn)( 設(shè)備信息xml或遠(yuǎn)程目錄信息等), port:8200
- }
- }
Process_upnphttp會在底層調(diào)用upnpsoap.c中的ExecuteSoapAction方法,在upnpsoap.c定義了相關(guān)信令和它們對應(yīng)的方法,如下:- 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 }
- };
更具對應(yīng)關(guān)系,ExecuteSoapAction會再調(diào)用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); //回送給控制點(diǎn)xml字符串消息
-
- //......
- }
通過BuildSendAndCloseSoapResp回傳給控制點(diǎn),這個xml字符串格式如下:
- <u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
- <Result><DIDL-Lite xmlns:dc="http:///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個目錄??刂泣c(diǎn)通過這一信息獲取minidlna服務(wù)。
|