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\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服务)

minidlna源码初探(一)_第1张图片

点击这里的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服务。







你可能感兴趣的:(源码,unix,网络编程)