前言:
前一篇文章minidlna源码初探(二)—— SSDP设备发现的大致流程介绍了SSDP设备发现的大致流程。本文将根据这一流程使用ACE库大致实现该流程。在VLC中模拟出一个伪服务端(设备),为了方便,我们省略了一些验证的内容,对一些XML消息也采取写死的方式。
正文:
首先,我们需要一个消息循环,如下:
//创建组播SOCKET ACE_SOCK_Dgram_Mcast udp; ACE_INET_Addr mcast_inet("239.255.255.250:1900"); udp.join(mcast_inet); udp.set_option(IP_MULTICAST_TTL, MAX_MULTICAST_IP_TTL); //创建单址SOCKET ACE_HANDLE peer_handle = GetPeerListenSocket(8200); thread_pool thrd_pool; //新建线程池 thrd_pool.run(); //启动线程池 while(1) { if ((GetCurrentSecond() - time_record) > 20) { handle_alive(udp); time_record = GetCurrentSecond(); //定时组播ssdp:alive消息 } active_handle_set.set_bit(udp_handle); active_handle_set.set_bit(peer_handle); width = (int)active_handle_set.max_set() + 1; if (ACE::select(width , &active_handle_set , 0 , 0 , &atv) < 1) //监听组播和单址是否有请求 { continue; } if (active_handle_set.num_set() <= 0) { continue; } ACE_Handle_Set_Iterator iterator(active_handle_set); ACE_HANDLE handle = iterator(); for (; handle != ACE_INVALID_HANDLE; handle = iterator()) { if (handle == udp_handle) //处理组播请求 { { boost::mutex::scoped_lock lock(transform_mutex); res = udp.recv(buf , 512 , remote_inet_addr); if(res <= 0) continue; } std::map<ACE_HANDLE , std::string> m; m.insert(std::pair<ACE_HANDLE , std::string>(udp_handle , std::string(buf))); thrd_pool.push_task(m); //向线程池push新的消息 } if (handle == peer_handle) //处理单地请求 { std::map<ACE_HANDLE , std::string> m; m.insert(std::pair<ACE_HANDLE , std::string>(peer_handle , std::string("HANDLE_PEER_ACCEPTOR"))); thrd_pool.push_task(m); //向线程池push新的消息 } } }
消息循环定时组播ssdp:alive消息,并获取组播或者单址传递来的消息。然后将SOCKET handle连同消息存入一个std::map<ACE_HANDLE , std::string>容器,随后将这个map压入线程池中的队列。线程池代码如下:
class thread_pool : public boost::noncopyable { private: boost::thread_group thrds; boost::mutex io_mutex; std::map<std::string , std::string>::iterator iter; static bool finish; int thread_num; typedef std::map<ACE_HANDLE , std::string> MAPHDLSTR; typedef std::queue<MAPHDLSTR> QUEMAPHDLSTR; QUEMAPHDLSTR vecque; public: thread_pool(){ thread_num = 8;} thread_pool(int num):thread_num(num){} ~thread_pool() { //...... //处理队列中剩余的任务 } void proxy() { while(!finish) { MAPHDLSTR maphdlstr; { boost::mutex::scoped_lock lock(io_mutex); if (!vecque.empty()) { maphdlstr = vecque.front(); //获取队列的map讯息 vecque.pop(); } } if (!maphdlstr.empty()) { handle_data(maphdlstr); //处理map } } } void push_task(MAPHDLSTR m) { vecque.push(m); //push新的map讯息 } void run() { for(size_t i = 0; i < thread_num ; ++i) { thrds.create_thread(boost::bind(&thread_pool::proxy,this)); //启动线程 } } void join(){thrds.join_all();} int get_static(){return finish;} int handle_data(MAPHDLSTR maphdlstr); };
int thread_pool::handle_data(MAPHDLSTR maphdlstr) { ACE_HANDLE sock = maphdlstr.begin()->first; std::string data = maphdlstr.begin()->second; if (0 == data.find("M-SEARCH * HTTP/1.1")) //处理ssdp:discover { //data中的格式如下 /* 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 #设置服务查询的目标 */ SendSSDPResponse(sock); return 0; } if (0 == data.find("NOTIFY * HTTP/1.1")) //ssdp:alive { //data中的格式如下 /* 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 */ return 0; } if (0 == data.find("POST /ctl/ContentDir HTTP/1.1")) { //处理设备发现后的后续请求,在这里暂缺,data中的格式如下 /* 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> */ } if (0 == data.find("HANDLE_PEER_ACCEPTOR")) { handle_peer(sock); //处理单址请求 } return 0; }当客户端(控制点)连入服务,发送ssdp:discover过来,我们需要回复其消息,这个在SendSSDPResponse中完成,其代码如下:
void SendSSDPResponse(ACE_HANDLE handle) { //回复客户端的消息格式 /* HTTP/1.1 200 OK CACHE-CONTROL: max-age=1810 DATE: Wed, 21 May 2014 03:54:53 GMT ST: urn:schemas-upnp-org:device:MediaServer:1 USN: uuid:4d696e69-444c-164e-9d41-001ec92f0378::urn:schemas-upnp-org:device:MediaServer:1 EXT: SERVER: 3.2.0-61-generic DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.2 LOCATION: http://192.168.1.20:8200/rootDesc.xml Content-Length: 0 */ std::cout<<"SendSSDPResponse handle = "<<handle<<std::endl; char buf[512]; char tmstr[30]; time_t tm = time(NULL); strftime(tmstr, sizeof(tmstr), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&tm)); snprintf(buf, sizeof(buf), "HTTP/1.1 200 OK\r\n" "CACHE-CONTROL: max-age=%u\r\n" "DATE: %s\r\n" "ST: %s%s\r\n" "USN: %s%s%s%s\r\n" "EXT:\r\n" "SERVER: " MINIDLNA_SERVER_STRING "\r\n" "LOCATION: http://%s:%u" ROOTDESC_PATH "\r\n" "Content-Length: 0\r\n" "\r\n", (895<<1)+20, tmstr, "urn:schemas-upnp-org:device:MediaServer:", (3>1?"1":""), "uuid:4d696e69-444c-164e-9d41-001ec92f0378", (3 > 0 ? "::" : ""), (3 > 0 ? "urn:schemas-upnp-org:device:MediaServer:" : ""), (3 > 1 ? "1" : ""), "192.168.1.20", (unsigned int)8200); { boost::mutex::scoped_lock lock(transform_mutex); ACE_SOCK_Dgram_Mcast udp_t; udp_t.set_handle(handle); udp_t.send(buf , strlen(buf)); } }以上操作完成后,客户端会在8200端口向设备(服务端)发送单址请求,在线程池中,我们交由handle_peer完成,其代码如下:
int handle_peer(ACE_HANDLE handle) { socklen_t clientnamelen; struct sockaddr_in clientname; clientnamelen = sizeof(struct sockaddr_in); char buf[2048] = {0}; int shttp = accept(handle, (struct sockaddr *)&clientname, &clientnamelen); //获取远程客户端单址SOCKET if (shttp < 0) { std::cout<<GREEN<<"accept(http): "<<strerror(errno)<<NONE<<std::endl; return -1; } recv(shttp, buf, 2048, 0); if (std::string(buf).find("GET /rootDesc.xml HTTP/1.1") == 0) { //如果是请求信息,则会送客户端根目录信息,请求信息格式如下 /* GET /rootDesc.xml HTTP/1.1 HOST: 192.168.1.20:8200 DATE: Fri, 30 May 2014 05:37:15 GMT CONNECTION: close USER-AGENT: 6.1.7600 2/, UPnP/1.0, Portable SDK for UPnP devices/1.6.14 */ SendRootContainer(shttp); //会送给客户端根目录讯息 } close(shttp); return 0; }
SendRootContainer将会送给客户端根目录讯息,其代码如下:
void SendRootContainer(ACE_HANDLE handle) { const char *friendly_name = "Jane"; const char *resp = "HTTP/1.1 200 OK\r\n" "Content-Type: text/xml; charset=\"utf-8\"\r\n" "Connection: close\r\n" "Content-Length: 2189\r\n" "Server: 3.2.0-61-generic DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.1.2\r\n" "Date: Thu, 22 May 2014 05:29:30 GMT\r\n" "EXT:\r\n" "\r\n" "<?xml version=\"1.0\"?>\r\n" "<root xmlns=\"urn:schemas-upnp-org:device-1-0\"><specVersion><major>1</major><minor>0</minor></specVersion><device>" "<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType><friendlyName>Jane" "</friendlyName><manufacturer>Justin Maggard</manufacturer><manufacturerURL>http://www.netgear.com/</manufacturerURL>" "<modelDescription>MiniDLNA on Linux</modelDescription><modelName>Windows Media Connect compatible (MiniDLNA)</modelName>" "<modelNumber>1</modelNumber><modelURL>http://www.netgear.com</modelURL><serialNumber>12345678</serialNumber>" "<UDN>uuid:4d696e69-444c-164e-9d41-001ec92f0378</UDN><dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>" "<presentationURL>/</presentationURL><iconList><icon><mimetype>image/png</mimetype><width>48</width><height>48</height>" "<depth>24</depth><url>/icons/sm.png</url></icon><icon><mimetype>image/png</mimetype><width>120</width><height>120</height>" "<depth>24</depth><url>/icons/lrg.png</url></icon><icon><mimetype>image/jpeg</mimetype><width>48</width><height>48</height>" "<depth>24</depth><url>/icons/sm.jpg</url></icon><icon><mimetype>image/jpeg</mimetype><width>120</width><height>120</height>" "<depth>24</depth><url>/icons/lrg.jpg</url></icon></iconList><serviceList><service><serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>" "<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId><controlURL>/ctl/ContentDir</controlURL><eventSubURL>/evt/ContentDir</eventSubURL>" "<SCPDURL>/ContentDir.xml</SCPDURL></service><service><serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>" "<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId><controlURL>/ctl/ConnectionMgr</controlURL><eventSubURL>/evt/ConnectionMgr</eventSubURL>" "<SCPDURL>/ConnectionMgr.xml</SCPDURL></service><service><serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>" "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId><controlURL>/ctl/X_MS_MediaReceiverRegistrar</controlURL>" "<eventSubURL>/evt/X_MS_MediaReceiverRegistrar</eventSubURL><SCPDURL>/X_MS_MediaReceiverRegistrar.xml</SCPDURL></service></serviceList></device></root>"; ACE_SOCK_Stream stream; stream.set_handle(handle); int res = 0; int len = strlen(resp); { boost::mutex::scoped_lock lock(peer_mutex); if( (res = stream.send_n(resp, len)) <= 0 ) { std::cout<<RED<<"send(http): "<< strerror(errno)<<std::endl; } } stream.close(); }
源码大致讲述到此,看看效果,在VlC的UPNP中:
完整代码下载地址https://github.com/iwojima/ACE_SSDP_DEMO.git