基于UPnP协议_cling开源库实现分析_扫描

631 阅读4分钟

扫描

扫描,其实就是对发现的可见和可用的UPnP设备进行访问,扫描对应的UPnP设备描述和UPnP设备中包含的services描述文档;

这个过程不是client或者是server手动触发的,而是在一定情况下触发的,比如在发现可见和可用的UPnP设备时主动请求设备描述文档和服务描述文档,使用的request.method为GET;

其实在发现阶段执行最后的时候,执行了一个Runnable,这一步其实就是扫描阶段的开始,发现和扫描阶段的执行是绑定的,不需要任何外部条件触发,例如人为的点击事件等:

getUpnpService().getConfiguration().getAsyncProtocolExecutor().execute(
    new RetrieveRemoteDescriptors(getUpnpService(), rd)
);
RetrieveRemoteDescriptors

RetrieveRemoteDescriptors为Runnable,我们看一下其中的run()方法:

public void run() {
​
    URL deviceURL = rd.getIdentity().getDescriptorURL();
​
    // Performance optimization, try to avoid concurrent GET requests for device descriptor,
    // if we retrieve it once, we have the hydrated device. There is no different outcome
    // processing this several times concurrently.
​
    if (activeRetrievals.contains(deviceURL)) {
        log.finer("Exiting early, active retrieval for URL already in progress: " + deviceURL);
        return;
    }
​
    // Exit if it has been discovered already, could be we have been waiting in the executor queue too long
    if (getUpnpService().getRegistry().getRemoteDevice(rd.getIdentity().getUdn(), true) != null) {
        log.finer("Exiting early, already discovered: " + deviceURL);
        return;
    }
​
    try {
        activeRetrievals.add(deviceURL);
        describe();
    } catch (RouterException ex) {
        log.log(Level.WARNING,
            "Descriptor retrieval failed: " + deviceURL,
            ex
        );
    } finally {
        activeRetrievals.remove(deviceURL);
    }
}

在该方法中首先先获取了deviceURL,这是Device的描述文档的资源连接,但是仅此而已,并没有实际的获取到,需要再次通过发送request获取到详细的device描述文档;

扫描Device Describe

方法中调用的describe()方法:

protected void describe() throws RouterException {
​
    // All of the following is a very expensive and time consuming procedure, thanks to the
    // braindead design of UPnP. Several GET requests, several descriptors, several XML parsing
    // steps - all of this could be done with one and it wouldn't make a difference. So every
    // call of this method has to be really necessary and rare.
​
   if(getUpnpService().getRouter() == null) {
      log.warning("Router not yet initialized");
      return ;
   }
​
   StreamRequestMessage deviceDescRetrievalMsg;
   StreamResponseMessage deviceDescMsg;
​
   try {
​
      deviceDescRetrievalMsg =
            new StreamRequestMessage(UpnpRequest.Method.GET, rd.getIdentity().getDescriptorURL());
​
        // Extra headers
        UpnpHeaders headers =
            getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(rd.getIdentity());
        if (headers != null)
            deviceDescRetrievalMsg.getHeaders().putAll(headers);
​
      log.fine("Sending device descriptor retrieval message: " + deviceDescRetrievalMsg);
        deviceDescMsg = getUpnpService().getRouter().send(deviceDescRetrievalMsg);
​
   } catch(IllegalArgumentException ex) {
      // UpnpRequest constructor can throw IllegalArgumentException on invalid URI
      // IllegalArgumentException can also be thrown by Apache HttpClient on blank URI in send()
        log.warning(
            "Device descriptor retrieval failed: "
            + rd.getIdentity().getDescriptorURL()
            + ", possibly invalid URL: " + ex);
        return ;
    }
​
    if (deviceDescMsg == null) {
        log.warning(
            "Device descriptor retrieval failed, no response: " + rd.getIdentity().getDescriptorURL()
        );
        return;
    }
​
    if (deviceDescMsg.getOperation().isFailed()) {
        log.warning(
                "Device descriptor retrieval failed: "
                        + rd.getIdentity().getDescriptorURL() +
                        ", "
                        + deviceDescMsg.getOperation().getResponseDetails()
        );
        return;
    }
​
    if (!deviceDescMsg.isContentTypeTextUDA()) {
        log.fine(
            "Received device descriptor without or with invalid Content-Type: "
                + rd.getIdentity().getDescriptorURL());
        // We continue despite the invalid UPnP message because we can still hope to convert the content
    }
​
    String descriptorContent = deviceDescMsg.getBodyString();
    if (descriptorContent == null || descriptorContent.length() == 0) {
        log.warning("Received empty device descriptor:" + rd.getIdentity().getDescriptorURL());
        return;
    }
​
    log.fine("Received root device descriptor: " + deviceDescMsg);
    describe(descriptorContent);
}

在该方法中创建了StreamRequestMessage对象,其中封装了UpnpRequest.Method.GET,即请求类型为GET,RemoteDevice的instanceId以及device描述文档的URL;

调用了getUpnpService().getRouter().send(deviceDescRetrievalMsg),用于获取device描述文档,返回值为ResponseMessage,然后调用了describe(String descriptorXML)方法:

protected void describe(String descriptorXML) throws RouterException {
​
    boolean notifiedStart = false;
    RemoteDevice describedDevice = null;
    try {
​
        DeviceDescriptorBinder deviceDescriptorBinder =
                getUpnpService().getConfiguration().getDeviceDescriptorBinderUDA10();
​
        describedDevice = deviceDescriptorBinder.describe(
                rd,
                descriptorXML
        );
​
        log.fine("Remote device described (without services) notifying listeners: " + describedDevice);
        notifiedStart = getUpnpService().getRegistry().notifyDiscoveryStart(describedDevice);
​
        log.fine("Hydrating described device's services: " + describedDevice);
        RemoteDevice hydratedDevice = describeServices(describedDevice);
        if (hydratedDevice == null) {
           if(!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) {
              errorsAlreadyLogged.add(rd.getIdentity().getUdn());
              log.warning("Device service description failed: " + rd);
           }
            if (notifiedStart)
                getUpnpService().getRegistry().notifyDiscoveryFailure(
                        describedDevice,
                        new DescriptorBindingException("Device service description failed: " + rd)
                );
            return;
        } else {
            log.fine("Adding fully hydrated remote device to registry: " + hydratedDevice);
            // The registry will do the right thing: A new root device is going to be added, if it's
            // already present or we just received the descriptor again (because we got an embedded
            // devices' notification), it will simply update the expiration timestamp of the root
            // device.
            getUpnpService().getRegistry().addDevice(hydratedDevice);
        }
​
    } catch (ValidationException ex) {
        ……………………
    }
}

在该方法中解析device Descriptor,获取到device中的services list,其中就包括了ConnectionManagerService、AVTransportService、RendingControlService这3个服务,调用了describeServices(RemoteDevice currentDevice)来获取当前device中的service描述文档;

扫描Service Describe
protected RemoteDevice describeServices(RemoteDevice currentDevice)
        throws RouterException, DescriptorBindingException, ValidationException {
​
    List<RemoteService> describedServices = new ArrayList<>();
    if (currentDevice.hasServices()) {
        List<RemoteService> filteredServices = filterExclusiveServices(currentDevice.getServices());
        for (RemoteService service : filteredServices) {
            RemoteService svc = describeService(service);
             // Skip invalid services (yes, we can continue with only some services available)
            if (svc != null)
                describedServices.add(svc);
            else
                log.warning("Skipping invalid service '" + service + "' of: " + currentDevice);
        }
    }
​
    List<RemoteDevice> describedEmbeddedDevices = new ArrayList<>();
    if (currentDevice.hasEmbeddedDevices()) {
        for (RemoteDevice embeddedDevice : currentDevice.getEmbeddedDevices()) {
             // Skip invalid embedded device
            if (embeddedDevice == null)
                continue;
            RemoteDevice describedEmbeddedDevice = describeServices(embeddedDevice);
             // Skip invalid embedded services
            if (describedEmbeddedDevice != null)
                describedEmbeddedDevices.add(describedEmbeddedDevice);
        }
    }
​
    Icon[] iconDupes = new Icon[currentDevice.getIcons().length];
    for (int i = 0; i < currentDevice.getIcons().length; i++) {
        Icon icon = currentDevice.getIcons()[i];
        iconDupes[i] = icon.deepCopy();
    }
​
    // Yes, we create a completely new immutable graph here
    return currentDevice.newInstance(
            currentDevice.getIdentity().getUdn(),
            currentDevice.getVersion(),
            currentDevice.getType(),
            currentDevice.getDetails(),
            iconDupes,
            currentDevice.toServiceArray(describedServices),
            describedEmbeddedDevices
    );
}

在describeServices()方法中for循环调用了describeService(RemoteService service)方法,用于遍历扫描RemoteService描述文档:

protected RemoteService describeService(RemoteService service)
        throws RouterException, DescriptorBindingException, ValidationException {
​
   URL descriptorURL;
   try {
      descriptorURL = service.getDevice().normalizeURI(service.getDescriptorURI());
   }  catch(IllegalArgumentException e) {
      log.warning("Could not normalize service descriptor URL: " + service.getDescriptorURI());
      return null;
   }
​
    StreamRequestMessage serviceDescRetrievalMsg = new StreamRequestMessage(UpnpRequest.Method.GET, descriptorURL);
​
    // Extra headers
    UpnpHeaders headers =
        getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(service.getDevice().getIdentity());
    if (headers != null)
        serviceDescRetrievalMsg.getHeaders().putAll(headers);
​
    log.fine("Sending service descriptor retrieval message: " + serviceDescRetrievalMsg);
    StreamResponseMessage serviceDescMsg = getUpnpService().getRouter().send(serviceDescRetrievalMsg);
​
    if (serviceDescMsg == null) {
        log.warning("Could not retrieve service descriptor, no response: " + service);
        return null;
    }
​
    if (serviceDescMsg.getOperation().isFailed()) {
        log.warning("Service descriptor retrieval failed: "
                            + descriptorURL
                            + ", "
                            + serviceDescMsg.getOperation().getResponseDetails());
        return null;
    }
​
    if (!serviceDescMsg.isContentTypeTextUDA()) {
        log.fine("Received service descriptor without or with invalid Content-Type: " + descriptorURL);
        // We continue despite the invalid UPnP message because we can still hope to convert the content
    }
​
    String descriptorContent = serviceDescMsg.getBodyString();
    if (descriptorContent == null || descriptorContent.length() == 0) {
        log.warning("Received empty service descriptor:" + descriptorURL);
        return null;
    }
​
    log.fine("Received service descriptor, hydrating service model: " + serviceDescMsg);
    ServiceDescriptorBinder serviceDescriptorBinder =
            getUpnpService().getConfiguration().getServiceDescriptorBinderUDA10();
​
    return serviceDescriptorBinder.describe(service, descriptorContent);
}

而在该方法中,也是通过StreamRequestMessage和StreamResponseMessage的形式,请求获取到service的描述文档;

至此,扫描阶段的逻辑分析就结束了;