DLNA投屏

Cling核心教程(译)

2019-06-14  本文已影响0人  云中的醉

1. 开始

如下是一个使用Cling的示例:

package ...;

import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.model.message.header.STAllHeader;
import org.fourthline.cling.model.meta.LocalDevice;
import org.fourthline.cling.model.meta.RemoteDevice;
import org.fourthline.cling.registry.Registry;
import org.fourthline.cling.registry.RegistryListener;

public class SimpleUPnP {

    public static void main(String[] args) throws Exception{

        // UPnP discovery is asynchronous, we need a callback
        RegistryListener listener = new RegistryListener() {
            @Override
            public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoteDevice) {
                System.out.println("Discovery started: " + remoteDevice.getDisplayString());
            }

            @Override
            public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remoteDevice, Exception e) {
                System.out.println("Discovery failed: " + remoteDevice.getDisplayString() + " => " + e);
            }

            @Override
            public void remoteDeviceAdded(Registry registry, RemoteDevice remoteDevice) {
                System.out.println("Remote device available: " + remoteDevice.getDisplayString());
            }

            @Override
            public void remoteDeviceUpdated(Registry registry, RemoteDevice remoteDevice) {
                System.out.println("Remote device updated: " + remoteDevice.getDisplayString());
            }

            @Override
            public void remoteDeviceRemoved(Registry registry, RemoteDevice remoteDevice) {
                System.out.println("Remote device removed: " + remoteDevice.getDisplayString());
            }

            @Override
            public void localDeviceAdded(Registry registry, LocalDevice localDevice) {
                System.out.println("Local device removed: " + localDevice.getDisplayString());
            }

            @Override
            public void localDeviceRemoved(Registry registry, LocalDevice localDevice) {
                System.out.println("Local device removed: " + localDevice.getDisplayString());
            }

            @Override
            public void beforeShutdown(Registry registry) {
                System.out.println("Before shutdown, the registry has devices: " + registry.getDevices().size());
            }

            @Override
            public void afterShutdown() {
                System.out.println("Shutdown of registry complete");
            }
        };

        // This will create necessary network resources for UPnP right away
        System.out.println("Starting Cling...");

        UpnpService upnpService = new UpnpServiceImpl(listener);

        // Send a search message to all devices and service, they should respond soon
        upnpService.getControlPoint().search(new STAllHeader());

        // Let's wait 10 seconds for them to responds
        System.out.println("Waiting 10 seconds before shutting down...");
        Thread.sleep(10000);

        // Release all resources and advertise BYEBYE to other UPnP devices
        System.out.println("Stopping Cling");
        upnpService.shutdown();
    }
}

在运行这段代码之前,你需要将cling-core.jar以及他的依赖(seamless-*.jar)放到你的环境变量中。

2. 第一个UPnP服务和控制点

你可以把最基础的UPnP服务想象为一个发光二极管。每个设备有一个开关服务,用于把二极管打开或关闭。

如下我们将使用Cling核心库来实现UPnP服务和设备控制台应用。

2.1 SwitchPower服务实现

如下是SwitchPower服务实现的源代码(尽管源代码中有许多的注解,但是对于Cling并不存在运行时的依赖)

package example.binarylight;

import org.fourthline.cling.binding.annotations.*;

@UpnpService(
        serviceId = @UpnpServiceId("SwitchPower"),
        serviceType = @UpnpServiceType(value = "SwitchPower", version = 1)
)
public class SwitchPower {

    @UpnpStateVariable(defaultValue = "0", sendEvents = false)
    private boolean target = false;

    @UpnpStateVariable(defaultValue = "0")
    private boolean status = false;

    @UpnpAction
    public void setTarget(@UpnpInputArgument(name = "NewTargetValue")
                          boolean newTargetValue) {
        target = newTargetValue;
        status = newTargetValue;
        System.out.println("Switch is: " + status);
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
    public boolean getTarget() {
        return target;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
    public boolean getStatus() {
        // If you want to pass extra UPnP information on error:
        // throw new ActionException(ErrorCode.ACTION_NOT_AUTHORIZED);
        return status;
    }
}

为了能够成功的编译这份代码,你需要将Cling核心库添加到你的环境变量中。但是,只要这个类被编译完成了,它就可以在任何环境下运行,而且并不需要依赖什么框架或者库。

Cling使用注解来获取元数据,这些元数据包含了服务的描述,UPnP声明的变量,访问方式以及可作为UPnP操作公开的方法。当然,你也可以选择使用XML或者Java代码编程的方式来提供元数据(在接下来会说明),但是通常情况下,注解是最好的选择。

你可能认为代码其实可以比这更简单,毕竟发光二极管只有一个布尔状态,要么开,要么关。但是这个服务的设计者认为,这与实际的发光二极管还是有一点差异的。我们可以想象一下,如果发光二极管坏掉了,那么你把它设置为开的时候,你看到了依然是关的状态,因为你设置的状态无法切换(它坏掉了)。但是,对于我们这个演示不会有问题,因为她的状态都是通过控制台输出的。

2.2 绑定UPnP设备

通过Cling,我们使用简单的Java编程来实例化一个带图标的设备(嵌入式设备)。如下为创建带图标设备以及如何绑定到上一节中所述服务的方法:

LocalDevice createDevice()
        throws ValidationException, LocalServiceBindingException, IOException {

    DeviceIdentity identity =
            new DeviceIdentity(
                    UDN.uniqueSystemIdentifier("Demo Binary Light")
            );

    DeviceType type =
            new UDADeviceType("BinaryLight", 1);

    DeviceDetails details =
            new DeviceDetails(
                    "Friendly Binary Light",
                    new ManufacturerDetails("ACME"),
                    new ModelDetails(
                            "BinLight2000",
                            "A demo light with on/off switch.",
                            "v1"
                    )
            );

    Icon icon =
            new Icon(
                    "image/png", 48, 48, 8,
                    getClass().getResource("icon.png")
            );

    LocalService<SwitchPower> switchPowerService =
            new AnnotationLocalServiceBinder().read(SwitchPower.class);

    switchPowerService.setManager(
            new DefaultServiceManager(switchPowerService, SwitchPower.class)
    );

    return new LocalDevice(identity, type, details, icon, switchPowerService);

    /* Several services can be bound to the same device:
    return new LocalDevice(
            identity, type, details, icon,
            new LocalService[] {switchPowerService, myOtherService}
    );
    */
   
}

让我们来分析一下这段代码。正如你看见的,这里的所有元数据都是通过构造函数提供的,因为这些元数据都是不可变和线程安全的。

DeviceIdentity

不管这个设备是根设备还是根设备中的嵌入式设备,这个设备都需要有一个特殊的设备名称(UDN - Unique Device Name)。这个UDN应该是稳定的,不应该在设备重启后就发生变化。比如说,你把一个UPnP设备从网络上拔下来(或者是把设备关闭,待机),等你再次打开它的时候,它需要向其他设备显示同样的UDN,以便其他设备知道这是它们之前处理过的设备。

你每次调用UDN.uniqueSystemIdentifier()方法时,它返回的都是同一个唯一标识符。这个标识符是通过设备的网卡以及其他一些参数计算出的Hash值,来确保这个标识符的唯一性和稳定性。

DeviceType

设备类型还包括一个使用简单整数的来标志的版本号。在这里的情况下,发光二极管是一个遵循UDA(UPnP Device Architecture)规范的标准化模板设备。

DeviceDetails

设备详细信息中的“友好名称”是必须要提供的,这个“友好名称”是UPnP向其他设备主要显示的内容,而型号和设备制造商信息是可选的。

Icon

每个设备都可以有一个与其”友好名称“相关的图标,这个图标将在合适的机会向其他设备展示。当然,你也可以使用不带图标参数的LocalDevice来进行构造设备。

Service

最后,设备最重要的还是它的服务部分。每个设备实例都封装了特定服务的元数据来,这些元数据标志了它的操作选项以及声明的变量,如何被调用。在这里我们使用Cling注解的方式来绑定到服务实例并且获取它的元数据信息。

因为Service仅仅是作为一个描述服务的元数据实例,所以你需要使用一个ServiceManager来完成实际的工作。ServiceManager就是作为元数据之间的连接以及你一个服务的具体实现,就像马路上粘合的橡胶一样。当需要对服务执行操作时(操作发送的很慢),DefaultServiceManager将会实例化给定的SwitchPower类。这个ServiceManager将会持有这个实例并且在注册了UPnP之后会尽可能的进行重用它。换句话说,ServiceManager就是作为一个实例化你UPnP服务具体实现的工厂。

需要注意的是,LocalDevice是表示UPnP设备的接口,它是主机上作为一个运行在UPnP协议栈上的设备。任何通过网络设备上发现的都是具有远程服务的远程设备,通常情况下不应该直接实例化这些设备。

当你实例化设备的图标是无效的时候,程序将会抛出ValidationException异常,你可以通过调用getErrors()方法来找出那个属性值未通过完整性规则。如果你服务中注解的元数据有错,那么在你绑定服务是将会抛出LocalServiceBindingException异常。另外,IOException只会在调用有图标的构造函数时候才会抛出,这应该是它在读取资源文件的时候出错了。

2.3 运行服务器

Cling核心库的主要API入口是线程安全的,因为通常情况下它是单个UPnPService实例:

package example.binarylight;

import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.binding.*;
import org.fourthline.cling.binding.annotations.*;
import org.fourthline.cling.model.*;
import org.fourthline.cling.model.meta.*;
import org.fourthline.cling.model.types.*;

import java.io.IOException;

public class BinaryLightServer implements Runnable {

    public static void main(String[] args) throws Exception {
        // Start a user thread that runs the UPnP stack
        Thread serverThread = new Thread(new BinaryLightServer());
        serverThread.setDaemon(false);
        serverThread.start();
    }

    public void run() {
        try {

            final UpnpService upnpService = new UpnpServiceImpl();

            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    upnpService.shutdown();
                }
            });

            // Add the bound local device to the registry
            upnpService.getRegistry().addDevice(
                    createDevice()
            );

        } catch (Exception ex) {
            System.err.println("Exception occured: " + ex);
            ex.printStackTrace(System.err);
            System.exit(1);
        }
    }

}

(之前的createDevice方法应该添加到这个类中)

一旦UPnPServiceImpl被创建,它的堆栈就会启动并开始运行。不管你编写的是客户端还是服务端,都需要创建UPnPService实例。UPnPService维护着所有在网络上被发现的远程设备以及本地设备的注册表,它在后台进行管理用于发现和事件处理的广播。

在你应用程序退出的时候,你需要关闭UPnP服务,以便于网络上的所有其他UPnP设备都会收到你退出的通知,即该应用程序绑定的设备将不再可用。如果你在应用程序退出的时候没有关闭UPnP服务,那么网络上其他设备的UPnP对你设备的控制点仍将是可使用的,但是事实上你的设备已经退出了。

一旦本地UPnP服务堆栈的Registry是可获得的,那么应尽快在这个类中调用之前章节中的createDevice()方法。

现在你应该编译并运行了你的服务器,它应该在控制台打印出了一些信息,并且它将会开始等待UPnP控制点的连接。

2.4 创建控制点

客户端程序同样有类型服务端那样的轮子可以用,也是作为一个UPnPService的单例来使用:

package example.binarylight;

import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.controlpoint.*;
import org.fourthline.cling.model.action.*;
import org.fourthline.cling.model.message.*;
import org.fourthline.cling.model.message.header.*;
import org.fourthline.cling.model.meta.*;
import org.fourthline.cling.model.types.*;
import org.fourthline.cling.registry.*;

public class BinaryLightClient implements Runnable {

    public static void main(String[] args) throws Exception {
        // Start a user thread that runs the UPnP stack
        Thread clientThread = new Thread(new BinaryLightClient());
        clientThread.setDaemon(false);
        clientThread.start();

    }

    public void run() {
        try {

            UpnpService upnpService = new UpnpServiceImpl();

            // Add a listener for device registration events
            upnpService.getRegistry().addListener(
                    createRegistryListener(upnpService)
            );

            // Broadcast a search message for all devices
            upnpService.getControlPoint().search(
                    new STAllHeader()
            );

        } catch (Exception ex) {
            System.err.println("Exception occured: " + ex);
            System.exit(1);
        }
    }

}

通常情况下,在网络上找到特定服务类型的设备之前,控制点都将会保持休眠状态(不可用)。当远程设备被发现时,或者它进入网络广播自己时,Cling会调用RegistryListener方法。如果你不想等待周期性的通知结果,你可以调用控制点对所有设备(或者指定服务,UDN)执行搜索方法,如果发现了与之匹配的设备将会触发设备发现通知给你。

之前你已经看到了ControlPointAPI中的search()方法,这个方法是你使用Cling编写UPnP客户端时需要调用的重要方法之一。

如果你已经完成了这份客户端以及之前章节中服务端的代码,你将会发现我们并没有在程序退出时调用UPnPService的关闭方法。这还不是我们目前需要讨论的问题,因为这个程序目前还没有任何本地设备和服务时间监听(注册监听也一样)没绑定和注册。因此,我们还不需要在程序退出时进行调用,仅仅把它当做一个简单的示例代码就好。

下面我们来专注在注册监听的实现和网络上UPnP设备被发现时发生的事件。

2.5 执行操作

我们在这里穿件的控制点仅专注于实现了SwitchPower的服务。在设备被发现时,我们可以根据在SwitchPower模板中定义的服务,来判断这个服务是否提供对应的服务:

RegistryListener createRegistryListener(final UpnpService upnpService) {
    return new DefaultRegistryListener() {

        ServiceId serviceId = new UDAServiceId("SwitchPower");

        @Override
        public void remoteDeviceAdded(Registry registry, RemoteDevice device) {

            Service switchPower;
            if ((switchPower = device.findService(serviceId)) != null) {

                System.out.println("Service discovered: " + switchPower);
                executeAction(upnpService, switchPower);

            }

        }

        @Override
        public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
            Service switchPower;
            if ((switchPower = device.findService(serviceId)) != null) {
                System.out.println("Service disappeared: " + switchPower);
            }
        }

    };
}

如果获得了一个服务,我们可以马上在这个服务上执行操作。当SwitchPower设备从网络上消失的时候将会有一段log信息打印出来。这是一个非常简单的控制点,当服务可获得的时候,它将执行一个调用后就不管了的操作:

void executeAction(UpnpService upnpService, Service switchPowerService) {

        ActionInvocation setTargetInvocation =
                new SetTargetActionInvocation(switchPowerService);

        // Executes asynchronous in the background
        upnpService.getControlPoint().execute(
                new ActionCallback(setTargetInvocation) {

                    @Override
                    public void success(ActionInvocation invocation) {
                        assert invocation.getOutput().length == 0;
                        System.out.println("Successfully called action!");
                    }

                    @Override
                    public void failure(ActionInvocation invocation,
                                        UpnpResponse operation,
                                        String defaultMsg) {
                        System.err.println(defaultMsg);
                    }
                }
        );

}

class SetTargetActionInvocation extends ActionInvocation {

    SetTargetActionInvocation(Service service) {
        super(service.getAction("SetTarget"));
        try {

            // Throws InvalidValueException if the value is of wrong type
            setInput("NewTargetValue", true);

        } catch (InvalidValueException ex) {
            System.err.println(ex.getMessage());
            System.exit(1);
        }
    }
}

Action(元数据)和ActionInvocation(实际调用数据)API可以执行一个非常精细的控制,比如如何准备调用,如何设置输入值,如何执行动作以及结果如何输出如何处理。如注册监听者一样,UPnP本质上也是异步的,执行的操作结果是作为回调显示。

我们推荐将特定的操作封装到ActionInvocation子类中,这样可以更进一步抽象调用的输入和输出值。但ActionInvocation是线程不安全的,所以不能够被两个线程同时执行。

ActionCallback有两个必须要实现的方法,一个会在执行成功的时候调用,另一个会在执行失败的时候调用。有很多原因都可能导致操作执行失败,具体原因你可以通过查询API文档或者查阅错误日志信息。

2.6 运行应用

首先编译发光二极管的演示应用:

$ javac -cp /path/to/semaless-jar-files:/path/to/cling-core.jar \
            -d classes/ \
            src/example/binarylight/BinaryLiaghtServer.java \
            src/example/binarylight/BinaryLiaghtClient.java \
            src/example/binarylight/SwitchPower.java

另外也不要忘记将你的icon.png文件放到classes的输出目录,在对应的包中该文件将会作为资源文件被加载(如果你是都是按照之前的步骤进行操作的,那么包名就是example.binarylight)

无论你先启动服务器还是客户端,都没有关系,他们都会自动的发现对方:

$ java -cp /path/to/seamless-jar-files:/path/to/cling-core.jar:classes/ \
           example.binaryLight.BinaryLightServer

$ java -cp /path/to/seamless-jar-files:/path/to/cling-core.jar:classes/ \
           example.binaryLight.BinaryLightClient

运行成功后,你将会在对应控制台看到操作执行的信息。你也可以单独的停止或重启对应应用(在控制台使用Ctrl+C)

2.7 调试日志

尽管这个发光二极管应用是一个非常简单的例子,但是你仍然可能会运行出错。Cling核心库提供了大量的日志信息,可以帮助你解决大部分的问题。在Cling核心库内部使用Java的JDK日志系统,即大家所知道的JUL(java.util.logging)。并没有做任何类似日志框架,日志服务或者其他相关依赖的封装。

默认情况下,在SUN的JDK中对JUL的实现只会显示INFO,WARNING,SEVERE级别的输出信息,并且每个消息都会打印成两行。但这是非常不方便和难以阅读的,所以你刚开始可能需要为每条消息配置成一行,这就需要你自己定义日志处理程序了。

接下来你就需要为不同的日志级别进行配置。Cling核心库只会在刚开始运行和关闭是输出一些INFO级别的日志信息,在其运行期间并不会打印相关信息(除非程序运行出错)。

在调试过程中,通常需要打印出非常详细的日志信息。在Cling核心库中日志通常是按照包名进行分类。下表是相关日志类别和推荐的调试级别:

Network/Transport
org.fourthline.cling.transport.spi.DatagramIO (FINE) org.fourthline.cling.transport.spi.MulticastReceiver (FINE) UDP communication
org.fourthline.cling.transport.spi.DatagramProcessor (FINER) UDP datagram processing and content
org.fourthline.cling.transport.spi.UpnpStream (FINER) org.fourthline.cling.transport.spi.StreamServer (FINE) org.fourthline.cling.transport.spi.StreamClient (FINE) TCP communication
org.fourthline.cling.transport.spi.SOAPActionProcessor (FINER) SOAP action message processing and content
org.fourthline.cling.transport.spi.GENAEventProcessor (FINER) GENA event message processing and content
org.fourthline.cling.transport.impl.HttpHeaderConverter (FINER) HTTP header processing
UPnP Protocol
org.fourthline.cling.protocol.ProtocolFactory (FINER) org.fourthline.cling.protocol.async (FINER) Discovery (Notification & Search)
org.fourthline.cling.protocol.ProtocolFactory (FINER) org.fourthline.cling.protocol.RetrieveRemoteDescriptors (FINE) org.fourthline.cling.protocol.sync.ReceivingRetrieval (FINE) org.fourthline.cling.binding.xml.DeviceDescriptorBinder (FINE) org.fourthline.cling.binding.xml.ServiceDescriptorBinder (FINE) Description
org.fourthline.cling.protocol.ProtocolFactory (FINER) org.fourthline.cling.protocol.sync.ReceivingAction (FINER) org.fourthline.cling.protocol.sync.SendingAction (FINER) Control
org.fourthline.cling.model.gena (FINER) org.fourthline.cling.protocol.ProtocolFactory (FINER) org.fourthline.cling.protocol.sync.ReceivingEvent (FINER) org.fourthline.cling.protocol.sync.ReceivingSubscribe (FINER) org.fourthline.cling.protocol.sync.ReceivingUnsubscribe (FINER) org.fourthline.cling.protocol.sync.SendingEvent (FINER) org.fourthline.cling.protocol.sync.SendingSubscribe (FINER) org.fourthline.cling.protocol.sync.SendingUnsubscribe (FINER) org.fourthline.cling.protocol.sync.SendingRenewal (FINER) GENA
Core
org.fourthline.cling.transport.Router (FINER) Message Router
org.fourthline.cling.registry.Registry (FINER) org.fourthline.cling.registry.LocalItems (FINER) org.fourthline.cling.registry.RemoteItems (FINER) Registry
org.fourthline.cling.binding.annotations (FINER) org.fourthline.cling.model.meta.LocalService (FINER) org.fourthline.cling.model.action (FINER) org.fourthline.cling.model.state (FINER) org.fourthline.cling.model.DefaultServiceManager (FINER) Local service binding & invocation
org.fourthline.cling.controlpoint (FINER) Control Pointinteraction

我们可以使用配置文件的方式来配置JUL,例如,创建如下mylogging.properties文件:

# Enables a one-message-per-line handler(shipping in seamless-util.jar)
handler=org.seamless.util.logging.SystemOutLoggingHandler

# The default (root) log level
.level=INFO

# Extra setting for various categories

org.fourthline.cling.level=INFO

org.fourthline.cling.protocol.level=FINEST

org.fourthline.cling.registry.Registry.level=FINER
org.fourthline.cling.registry.LocalItems.level=FINER
org.fourthline.cling.registry.RemoteItems.level=FINER

现在你就可以启动带有配置了日志信息的应用了,然后你就可以看到你期望的日志信息了:

$ java -cp /path/to/seamless-jar-files:/path/to/cling-core.jar:classes/ \
           -d java.util.logging.config.file=/path/to/mylogging.properties \
           example.binaryLight.BinaryLightServer

Cling核心库PAI

从根本上来说,使用Cling进行编程的客户端和服务端是相同的。任何程序的单一入口都是UPnPService实例,通过该API可以访问到本地UPnP协议栈,可以作为客户端(控制点)执行操作,或者为本地货远程注册的客户端提供服务。

下图显示了Cling核心库中最重要的接口信息:

Cling核心库API概览.png

当然你也可以通过调用如下接口使UPnP设备与UPnP服务进行交互,Cling提供了一些友好的模型来表示这些元素:

Cling元素模型概览.png

在这一章节中,我们将从UPnPService开始,进行更详细的介绍Cling的API和元素模型。

3.1 使用UPnPService

如下为UPnPService接口:

public interface UPnPService {

    public UPnPServiceConfiguration getConfiguration();
    public ProtocolFactory getProtocolFactory();
    public Router getRouter();

    public ControlPoint getControlPoint();
    public Registry getRegistry();

    public void shutdown();
}

一个UPnPService实例表示一个正在运行的UPnP协议栈,包括所有的网络监听器,后台维护线程等。Cling核心库包含了一个默认的实现,你可以按照如下方式进行实例化:

UPnPService upnpService = new UPnPServiceImpl();

有了这个实现,当本地UPnP协议栈准备就绪时,就可以在网络上监听UPnP相关的信息。当不需要这个UPnP协议栈时,你可以调用shutdown()方法来告知网络上所有其他UPnP设备你的服务不可用。如果你退出的时候没有调用shutdown()方法,那么其他程序会认为你的服务仍是可用的,除非你之前宣告其不可用。

绑定这个实现由两种构造方式进行,第一种为:

UpnpService upnpService = 
        new UpnpServiceImpl(RegistryListener... registryListeners);

这个构造函数可以接收你自定义的RegistryListener实例,这个实例甚至会在UPnP协议栈监听到任何网络信息之前被激活。也就意味着,一旦网络堆栈信息就绪,你就可以收到所有接入设备和服务注册的通知。但需要注意的是这很少情况下回起作用,所以通常情况下你需要在堆栈启动之后自己调用搜索请求。

第二种构造函数接收UPNP协议栈配置的自定义:

UpnpService upnpService = 
       new UpnpServiceImpl(new DefaultUpnpServiceConfiguration(8081));

这个示例是将UPnP的堆栈端口设置为8081,默认情况下是选择系统空闲的临时端口。UpnpServiceConfiguration也是一个借口,在上面的示例中,我们也看到了实现它的方式

下面的章节中将更详细的接收UpnpService接口以及其返回的参数信息。

3.1.1 自定义配置

这是Cling核心库中默认的UPnP协议栈配置接口,也是你创建UpnpServiceImpl实例时必须要提供的:

    public interface UpnpServiceConfiguration {
        // NETWORK 
        public NetworkAddressFactory createNetworkAddressFactory();
        
        public StreamClient createStreamClient();
        public StreamServer createStreamServer(NetworkAddressFactory naf);
        
        public MulticastReceiver createMulticastReceiver(NetworkAddressFactory naf);
        public DatagramIO createDatagramIO(NetworkAddressFactory naf);
        
        // PROCESSORS
        public DatagramProcessor getDatagramProcessor();
        public SOAPActionProcessor getSoapActionProcessor();
        public GENAEventProcessor getGenaEventProcessor();
        
        // DESCRIPTORS
        public DeviceDescriptorBinder getDeviceDescriptorBinderUDA10();
        public ServiceDescriptorBinder getServiceDescriptorBinderUDA10();
        
        // EXECUTORS
        public Executor getMulticastReceiverExecutor();
        public Executor getDatagramIOExecutor();
        public Executor getStreamServerExecutor();
        public Executor getAsyncProtocolExecutor();
        public Executor getSyncProtocolExecutor();
        public Executor getRegistryMaintainerExecutor();
        public Executor getRegistryListenerExecutor();
        
        // REGISTRY
        public Namespace getNamespace();
        public int getRegistryMaintenanceIntervalMillis();
        ...
    }

这是一个相当宽泛的SPI,但是通常情况下你并不会从头开始实现它。并且在大多数情况下,重写和自定义绑定的DefaultUpnpServiceConfiguration就已经足够使用了。

同时这些配置也反映了Cling核心库的内部结构信息。

Network

NetworkAddressFactory提供了UPnP协议栈使用到的网络接口,端口和多播设置。在写入时,默认配置将忽略一下接口和IP地址:所有IPv6接口和地址,名称为”vmnet“,”vnic“,”vboxnet“,”virtual“,或者”ppp*“以及本地回环。
否则,就需要绑定所有接口和TCP/IP地址。

你可以通过设置org.fourthline.cling.network.useInterfaces属性来列出你想要独占的绑定的网络接口列表,列表以逗号进行分割。另外你也可以通过设置org.fourthline.cling.network.useAddresses属性来限制需要绑定的实际TCP/IP地址列表,列表以逗号分割。

此外,可以配置生成网络级消息接收器和发送器,即网络路由器使用的实现。

消息流是使用TCP/HTTP的请求和响应,默认使用Sun JDK 6.0的web server来监听HTTP请求,并使用标准的JDK HttpURLConnection来发送请求。这就意味着Cling核心库不依赖于任何的HTTP服务器库。但是,如果你想要在一些运行时容器(如Tomcat,JBossAS等)中使用Cling核心库,你可能在开始运行时就会报错。这个错误告诉你Cling不能使用HttpURLConnection连接HTTP客户端操作。这个JDK中的一个陈旧设计,特别的糟糕:整个JVM中只有一个应用程序可以配置URL连接。如果容器已经在使用HttpURLConnection,则必须切换到另一个HTTP客户机。有关其他可用选项以及如何更改各种网络相关设置,请参阅配置网络传输。

UDP的单播和多播的发送,接收和解析是与Cling核心库绑定在一起的,所以并不需要实现任何特定的Sun JDK类,因此它可以在所有平台和任何环境中工作。

Processors

SSDP数据报由默认的Processor来处理,通常情况下你不需要去自定义。SOAP操作和GENA事件由可配置的processors来处理,如果你需要使用其他实现,请参考切换XML processor。为了实现与其他(损坏的)UPNP协议栈最佳互操作性,请考虑从默认严格符合规范的SOAP和GENA processor切换到要求更宽松的替代Processors。

Descriptors

读写UPNP XML设备和服务描述符是由专用绑定器处理的,请参阅交换描述符XML绑定器。为了实现与其他(损坏的)UPNP协议栈最佳互操作性,请考虑从默认严格符合规范的绑定器切换到更宽松的替代绑定器上去。

Executors

Cling堆栈是多线程的,线程的创建和执行都是通过java.util.concurrent执行器来处理的。默认配置使用的线程池的最大大小为64个并发运行的线程,即使是非常大的设备也是足够的。可以为网络消息处理、实际UPnP协议执行(处理发现、控制和事件过程)、本地注册表维护和侦听器回调执行配置细粒度的执行器。更多情况下,你不必自定义任何这些设置。

Registry

本地设备和服务XML描述符和图标可以与给定的Namespace一起使用,定义如何构造本地资源的URL路径。你还可以配置Cling检查其注册表中过期设备和过期GENA订阅的频率。

还有许多其他很少需要的配置选项,可用于定制Cling的行为,请参见UpnpConfiguration的Javadoc。

3.1.2 协议工厂

Cling核心库内部是模块化的,UPNP协议的任何方面都由一个实现(类)来处理,该实现可以在不影响任何其他方面的情况下进行替换。ProtocolFactory提供了这些实现,当必须处理到达网络的消息或传出消息时,它始终是UPnP协议栈的第一个访问点:

public interface ProtocolFactory {
        public ReceivingAsync createReceivingAsync(IncomingDatagramMessage message) throws ProtocolCreationException;
        
        public ReceivingSync createReceivingSync(StreamRequestMessage requestMessage) throws ProtocolCreationException;
        
        public SendingNotificationAlive createSendingNotificationAlive(LocalDevice ld);
        public SendingNotificationByebye createSendingNotificationByeBye(LocalDevice ld);
        public SendingSearch createSendingSearch(UpnpHeader searchTarget);
        public SendingAction createSendingAction(ActionInvocation invocation, URL url);
        public SendingSubscribe createSendingSubscribe(RemoteGENASubscription s);
        public SendingRenewal createSendingRenewal(RemoteGENASubscription s);
        public SendingUnsubscribe createSendingUnsubscribe(RemoteGENASubscription s);
        public SendingEvent createSendingEvent(LocalGENASubscription s);
    }

这个API是一个低级接口,你可以通过此接口访问UPnP协议栈的内部,但只在极少数情况下,你可能手动触发该特定的过程。

当消息到达时,无论是多播或单播UDP数据报,或TCP(HTTP)流请求,前两个方法都会被调用。默认协议工厂实现将选择适当的接收协议实现来处理传入消息。

注册在UPNP协议栈的本地服务也会自动的发送消息,例如ALIVE 和BYEBYE 通知。此外,如果编写UPnP控制点,则本地UPnP协议栈将发送各种搜索、控制和事件消息。协议工厂会将消息发送方(注册表、控制点)与消息的实际创建、准备和传输分离开来。

在最底层传输和接收消息是网络路由器(Router)的工作。

3.1.3 访问低层次的网络服务

消息的接收和发送,即所有消息传输,是通过Router接口进行封装:

public interface Router {
        
        public void received(IncomingDatagramMessage msg);
        public void received(UpnpStream stream);
        
        public void send(OutgoingDatagramMessage msg);
        public StreamResponseMessage send(StreamRequestMessage msg);
        
        public void broadcast(byte[] bytes);
}

UPNP可以处理两种类型的消息:多播和单播UDP数据报(通常是异步处理的),以及带有HTTP数据的请求/响应TCP消息。Cling核心库绑定的RouterImpl 将为传入消息实例化和维护监听器,并传输所有传出消息。

我们前面介绍的UpnpServiceConfiguration提供了在网络或消息发送器上侦听的消息接收器的实际实现。如果必须在UPnP协议栈的网络层上执行低级操作,则可以直接访问Router 。

然而,大多数时候你会使用ControlPoint 和Registry 接口与UPNP协议栈交互。

3.2 客户端使用控制点进行操作

编写客户端时主要使用的就是UPnP的ControlPoint,你可以通过UpnpService的getControlPoint方法来获取这个实例:

public interface ControlPoint {
    
    public void search(UpnpHeader searchType);
    public void execute(ActionCallback callback);
    public void execute(SubscriptionCallback callback);

}

一个UPnP客户端应用代表性特征是:

3.2.1 搜索网络

当你的控制点加入网络时,它可能不知道任何可用的UPnP设备和服务。要了解当前的设备,它可以发送广播——实际上是用UDP多播数据报——每个设备都会收到一条搜索消息。然后,每个接收器检查搜索消息,并决定是否应直接(使用通知UDP数据报)答复发送控制点。

搜索消息带有搜索类型的头,接收者在评估潜在响应时会分析此头。在创建传出搜索消息时,Cling的ControlPoint API接受UpnpHeader 参数。

在大多数情况下,如果你希望所有设备响应你的搜索,你可以使用STAllHeader 进行搜索:

upnpService.getControlPoint().search(
        new STAllHeader()
);

你的控制点将会收到通知消息,并且你也可以监听Registry 并检查找到的设备及其服务。(另外,如果不带任何参数调用search()也是一样的。)

另一方面,如果你已经知道正在搜索的设备的唯一设备名(UDN)时-可能是因为控制点在关闭时记住了它-你可以发送一条消息,该消息只会触发特定设备的响应:

upnpService.getControlPoint().search(
        new UDNHeader(udn)
);

当数十个设备都可能响应搜索请求时,这对于避免网络拥塞非常有用。但是,Registry 侦听器代码仍然必须检查每个新找到的设备,因为注册可能独立于搜索发生。

你还可以按设备或服务类型进行搜索。此搜索请求将触发来自“urn:schemas-upnp-org:device:BinaryLight”类型的所有设备的响应:

UDADeviceType udaType = new UDADeviceType("BinaryLight");
upnpService.getControlPoint().search(
        new UDADeviceTypeHeader(udaType)
);

如果所需的设备类型是自定义命名空间,请使用以下方式:

DeviceType type = new DeviceType("org-mydomain", "MyDeviceType", 1);
upnpService.getControlPoint().search(
        new DeviceTypeHeader(type)
);

或者,你可以搜索实现特定服务类型的所有设备:

UDAServiceType udaType = new UDAServiceType("SwitchPower");
upnpService.getControlPoint().search(
        new UDAServiceTypeHeader(udaType)
);
ServiceType type = new ServiceType("org-mydomain", "MyServiceType", 1);
upnpService.getControlPoint().search(
        new ServiceTypeHeader(type)
);
3.2.2 执行操作

UPnP服务公开了状态变量和操作。虽然状态变量表示服务的当前状态,但操作是用于查询或重新连接服务状态的操作。你必须从一个Device 获得一个Service 的实例,以获得所有的Action。目标设备可以本地设定为与您控制点相同的上堆,或者在网络上的任何地方都可以远程设定。我们将在本章晚些时候讨论如何通过本地堆栈Registry进入设备。

一旦拥有了设备,就可以通过元数据模型访问服务,例如:

Service service = device.findService(new UDAServiceId("SwitchPower"));
Action getStatusAction = service.getAction("GetStatus");

此方法将在设备及其所有嵌入设备中搜索具有给定标识符的服务,并返回找到的Service 或null。Cling的元数据模型是线程安全的,因此你可以获取Service或Action的实例并同时访问它。

ActionInvocation实例的工作是执行一个操作,需要注意的是,此实例不是线程安全的,所以执行操作的每个线程都必须从Action 元数据模型自己调用:

ActionInvocation getStatusInvocation = new ActionInvocation(getStatusAction);

ActionCallback getStatusCallback = new ActionCallback(getStatusInvocation) {

    @Override
    public void success(ActionInvocation invocation) {
        ActionArgumentValue status  = invocation.getOutput("ResultStatus");

        assert status != null;

        assertEquals(status.getArgument().getName(), "ResultStatus");

        assertEquals(status.getDatatype().getClass(), BooleanDatatype.class);
        assertEquals(status.getDatatype().getBuiltin(), Datatype.Builtin.BOOLEAN);

        assertEquals((Boolean) status.getValue(), Boolean.valueOf(false));
        assertEquals(status.toString(), "0"); // '0' is 'false' in UPnP
    }

    @Override
    public void failure(ActionInvocation invocation,
                        UpnpResponse operation,
                        String defaultMsg) {
        System.err.println(defaultMsg);
    }
};

upnpService.getControlPoint().execute(getStatusCallback);

执行是异步的,当执行完成时,UPNP协议栈将调用ActionCallback有两个方法。如果操作成功,则可以从调用实例中获取任何输出参数值,这些值可以方便地传递到success()方法中。可以检查已命名的输出参数值及其数据类型以继续处理结果。

操作执行不必异步处理,毕竟底层HTTP/SOAP协议是一个等待响应的请求。然而,回调编程模型非常适合于典型的UPnP客户端,该客户端还必须异步处理事件通知和设备注册。如果要直接在当前线程内执行ActionInvocation ,请使用空的ActionCallback.Default实现:

new ActionCallback.Default(getStatusInvocation, upnpService.getControlPoint()).run();

当调用失败时,你可以通过invocation.getfailure()获取详细的失败信息,或者使用所示的方便方法创建简单的错误消息。有关详细信息,请参阅ActionCallback的JavaDoc。

当操作需要输入参数值时,你必须提供它们。与输出参数一样,操作的任何输入参数也会被命名,因此你可以通过调用setInput("MyArgumentName", value)来设置它们:

Action action = service.getAction("SetTarget");

ActionInvocation setTargetInvocation = new ActionInvocation(action);

setTargetInvocation.setInput("NewTargetValue", true); // Can throw InvalidValueException

// Alternative:
//
// setTargetInvocation.setInput(
//         new ActionArgumentValue(
//                 action.getInputArgument("NewTargetValue"),
//                 true
//         )
// );

ActionCallback setTargetCallback = new ActionCallback(setTargetInvocation) {

    @Override
    public void success(ActionInvocation invocation) {
        ActionArgumentValue[] output = invocation.getOutput();
        assertEquals(output.length, 0);
    }

    @Override
    public void failure(ActionInvocation invocation,
                        UpnpResponse operation,
                        String defaultMsg) {
        System.err.println(defaultMsg);
    }
};

upnpService.getControlPoint().execute(setTargetCallback);

此操作有一个UPnP 类型的输入参数“boolean”。您可以设置一个Java boolean 或Boolean 实例,并且它将被自动转换。如果为特定参数(如类型错误的实例)设置了无效值,则将立即引发InvalidValueException 异常。

3.2.3 接收服务端事件

UPNP规范定义了一个基于发布/订阅范式的通用事件通知体系结构(GENA)。您的控制点订阅服务以接收事件。当服务状态更改时,将通过回调的方式向控制点调发送一条事件消息。订阅将定期刷新,直到您取消订阅该服务。如果不取消订阅,并且刷新订阅失败,可能是因为控制点在没有正确关闭的情况下被关闭,订阅将在发布服务端超时后关闭。

这是一个服务的订阅示例,该服务为名为Status 的状态变量(例如前面显示的switchpower服务)发送事件。订阅的刷新和超时时间为600秒:

SubscriptionCallback callback = new SubscriptionCallback(service, 600) {

    @Override
    public void established(GENASubscription sub) {
        System.out.println("Established: " + sub.getSubscriptionId());
    }

    @Override
    protected void failed(GENASubscription subscription,
                          UpnpResponse responseStatus,
                          Exception exception,
                          String defaultMsg) {
        System.err.println(defaultMsg);
    }

    @Override
    public void ended(GENASubscription sub,
                      CancelReason reason,
                      UpnpResponse response) {
        assertNull(reason);
    }

    @Override
    public void eventReceived(GENASubscription sub) {

        System.out.println("Event: " + sub.getCurrentSequence().getValue());

        Map<String, StateVariableValue> values = sub.getCurrentValues();
        StateVariableValue status = values.get("Status");

        assertEquals(status.getDatatype().getClass(), BooleanDatatype.class);
        assertEquals(status.getDatatype().getBuiltin(), Datatype.Builtin.BOOLEAN);

        System.out.println("Status is: " + status.toString());

    }

    @Override
    public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
        System.out.println("Missed events: " + numberOfMissedEvents);
    }

    @Override
    protected void invalidMessage(RemoteGENASubscription sub,
                                  UnsupportedDataException ex) {
        // Log/send an error report?
    }
};

upnpService.getControlPoint().execute(callback);

SubscriptionCallback 提供了在订阅的生命周期中调用的 failed(), established()和ended() 方法。当订阅结束时,如果订阅终止不正常,您将收到取消原因通知。有关更多详细信息,请参见这些方法的javadoc。

来自服务的每个事件消息都将传递给 eventReceived()方法,并且每个消息都将带有序列号。您可以在此方法中访问已更改的状态变量值,请注意,只有已更改的状态变量才会包含在事件消息中。当您订阅时,服务将发送一次名为“初始事件”的特殊事件消息。此消息包含服务的所有事件状态变量的值;您将在订阅时收到服务状态的初始快照。

每当接收UPnP 协议栈检测到一条不按顺序发送的事件消息时,例如,由于某些消息在传输过程中丢失,在接收该事件之前,将调用eventsMissed()方法。然后决定丢失的事件对于应用程序的正确行为是否重要,或者您是否可以静默地忽略它并继续处理具有非连续序列号的事件。

如果订阅使用的是远程服务,则可以选择重写invalidMessage()方法并对消息分析错误做出响应。在这里,您所能做的大部分时间都是记录或向开发人员报告一个错误,这样他们就可以处理中断的远程服务(UPnP 互操作性通常非常差)。

通过调用callback.end()定期结束订阅,这将取消订阅服务中的控制点。

3.3 注册表

在UpnpService上使用 getRegistry()获取的Registry是紧贴核心的UPnP 协议栈的核心。注册中心负责:

3.3.1 浏览注册表

虽然您通常会创建一个RegistryListener 来通知网络上发现和消失的UPnP 设备,但有时您必须手动浏览Registry 。

以下调用将返回具有给定唯一设备名称的设备,但只返回根设备,而不返回任何嵌入设备。如果要查找的设备可能是嵌入式设备,请将registry.getDevice()的第二个参数设置为false。

Registry registry = upnpService.getRegistry();
Device foundDevice = registry.getDevice(udn, true);

assertEquals(foundDevice.getIdentity().getUdn(), udn);

如果您知道您需要的设备是LocalDevice 或RemoteDevice 您可以使用以下操作:

LocalDevice localDevice = registry.getLocalDevice(udn, true);

大多数情况下,您需要一个特定类型或实现特定服务类型的设备,因为这是您的控制点可以处理的:

DeviceType deviceType = new UDADeviceType("MY-DEVICE-TYPE", 1);
Collection<Device> devices = registry.getDevices(deviceType);
ServiceType serviceType = new UDAServiceType("MY-SERVICE-TYPE-ONE", 1);
Collection<Device> devices = registry.getDevices(serviceType);

3.3.2 监听注册表变化

当使用控制点发现设备和服务时,RegistryListener 是您的主要API。UPnP 操作是异步的,因此设备的广告(alive 或byebye)可以随时发生。对网络搜索消息的响应也是异步的。

这是接口:

public interface RegistryListener {

    public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device);

    public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex);

    public void remoteDeviceAdded(Registry registry, RemoteDevice device);

    public void remoteDeviceUpdated(Registry registry, RemoteDevice device);

    public void remoteDeviceRemoved(Registry registry, RemoteDevice device);

    public void localDeviceAdded(Registry registry, LocalDevice device);

    public void localDeviceRemoved(Registry registry, LocalDevice device);

}

通常,您不希望实现所有这些方法。有些只有在编写服务或通用控制点时才有用。大多数情况下,当网络上出现具有特定服务的特定设备时,您希望收到通知。因此,扩展DefaultRegistryListener要容易得多,它对接口的所有方法都有空的实现,并且只重写您需要的方法。

remoteDeviceDiscoveryStarted()和remoteDeviceDiscoveryFailed()方法是完全可选的,但在慢的设备(如Android手机)上很有用。Cling 将检索并初始化每个UPnP 设备的所有设备元数据,然后在Registry中注册。UPnP 元数据分为几个XML描述符,因此通过HTTP检索这些描述符,解析和验证复杂UPnP 设备和服务模型的所有元数据可能需要几秒钟。这两种方法允许您在检索和分析第一个描述符之后尽快访问设备。但此时服务元数据还不可用:

public class QuickstartRegistryListener extends DefaultRegistryListener {

    @Override
    public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {

        // You can already use the device here and you can see which services it will have
        assertEquals(device.findServices().length, 3);

        // But you can't use the services
        for (RemoteService service : device.findServices()) {
            assertEquals(service.getActions().length, 0);
            assertEquals(service.getStateVariables().length, 0);
        }
    }

    @Override
    public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {
        // You might want to drop the device, its services couldn't be hydrated
    }
}

以下是注册和激活侦听器的方法:

QuickstartRegistryListener listener = new QuickstartRegistryListener();
upnpService.getRegistry().addListener(listener);

大多数情况下,在任何比手机更快的设备上,您的监听器都会像这样:

public class MyListener extends DefaultRegistryListener {

    @Override
    public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
        Service myService = device.findService(new UDAServiceId("MY-SERVICE-123"));
        if (myService != null) {
            // Do something with the discovered service
        }
    }

    @Override
    public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
        // Stop using the service if this is the same device, it's gone now
    }
}

remoteDeviceAdded()参数的设备元数据是完全相融合,其所有服务、操作和状态变量都可用。您可以继续使用此元数据,编写操作调用和事件监视回调。当设备从网络中消失时,您也可能希望做出相应的反应。

4.创建并绑定服务

任何Java类都可以是UPnP服务。让我们回到第一章中UPNP服务的第一个例子,SwitchPower服务实现,在这里重复一遍:

package example.binarylight;

import org.fourthline.cling.binding.annotations.*;

@UpnpService(
        serviceId = @UpnpServiceId("SwitchPower"),
        serviceType = @UpnpServiceType(value = "SwitchPower", version = 1)
)
public class SwitchPower {

    @UpnpStateVariable(defaultValue = "0", sendEvents = false)
    private boolean target = false;

    @UpnpStateVariable(defaultValue = "0")
    private boolean status = false;

    @UpnpAction
    public void setTarget(@UpnpInputArgument(name = "NewTargetValue")
                          boolean newTargetValue) {
        target = newTargetValue;
        status = newTargetValue;
        System.out.println("Switch is: " + status);
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
    public boolean getTarget() {
        return target;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
    public boolean getStatus() {
        // If you want to pass extra UPnP information on error:
        // throw new ActionException(ErrorCode.ACTION_NOT_AUTHORIZED);
        return status;
    }

}

这个类在编译时依赖于org.fourthline.cling.annotation包。这些源注解中编码的元数据保存在字节码中,当您绑定服务时,Cling 将在运行时读取它(“binding”只是一个用于读取和写入元数据的花哨词)。您可以在任何环境中加载和执行这个类,而不需要访问注解,也不需要在类路径上使用Cling 库。这只是编译时依赖项。
Cling 注解为设计服务实现类提供了很大的灵活性,如下面的示例所示。

4.1 注释服务实现

前面显示的服务类在类本身上有一些注释,声明了服务的名称和版本。然后,使用字段上的注释来声明服务的状态变量,并使用方法上的注释来声明可调用操作。
您的服务实现可能没有直接映射到UPNP状态变量的字段。

4.1.1 映射状态变量

下面的示例只有一个名为power的字段,但是UPnP 服务需要两个状态变量。在这种情况下,您将用类上的注释声明UPnP 状态变量:

@UpnpService(
        serviceId = @UpnpServiceId("SwitchPower"),
        serviceType = @UpnpServiceType(value = "SwitchPower", version = 1)

)
@UpnpStateVariables(
        {
                @UpnpStateVariable(
                        name = "Target",
                        defaultValue = "0",
                        sendEvents = false
                ),
                @UpnpStateVariable(
                        name = "Status",
                        defaultValue = "0"
                )
        }
)
public class SwitchPowerAnnotatedClass {

    private boolean power;

    @UpnpAction
    public void setTarget(@UpnpInputArgument(name = "NewTargetValue")
                          boolean newTargetValue) {
        power = newTargetValue;
        System.out.println("Switch is: " + power);
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
    public boolean getTarget() {
        return power;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
    public boolean getStatus() {
        return power;
    }
}

power 字段没有映射到状态变量,您可以自由地根据自己的喜好设计服务内部。您是否注意到您从未声明过状态变量的数据类型?另外,当收到一个“查询状态变量”动作时,cling如何读取gena订户的服务的“当前状态”?两个问题的答案相同。

让我们先考虑一下GENA 事件。这个例子有一个名为Status的事件状态变量,如果一个控制点订阅了要通知更改的服务,那么cling将如何获得当前状态?如果您在字段中使用了 @UpnpStateVariable,则Cling 将通过Java反射直接访问字段值。另一方面,如果不是在字段上而是在服务类上声明状态变量,那么cling将在绑定期间检测与状态变量的派生属性名匹配的任何JavaBean样式getter方法。

换句话说,Cling 会发现您的类有一个 getStatus()方法。不管该方法是否也是一个动作映射方法,重要的是它与JavaBean属性命名约定相匹配。Status UPnP状态变量映射到Status 属性,该属性应具有 getStatus()访问器方法。Cling 将使用此方法读取GENA 订阅服务器的当前服务状态以及手动查询状态变量时的状态。

如果在@upnpstatevariable注释中不提供UPnP数据类型名,Cling 将使用带注释字段的类型或发现的JavaBeangetter方法来确定类型。Java类型和UPnP数据类型之间支持的默认映射显示在下表中:

Java Type UPnP Datatype
java.lang.Boolean boolean
boolean boolean
java.lang.Short i2
short i2
java.lang.Integer i4
int i4
org.fourthline.cling.model.types.UnsignedIntegerOneByte ui1
org.fourthline.cling.model.types.UnsignedIntegerTwoBytes ui2
org.fourthline.cling.model.types.UnsignedIntegerFourBytes ui4
java.lang.Float r4
float r4
java.lang.Double float
double float
java.lang.Character char
char char
java.lang.String string
java.util.Calendar datetime
byte[] bin.base64
java.net.URI uri

Cling希望提供智能默认值。例如,前面显示的服务类没有按照UPnP的要求命名操作输出参数的相关状态变量。Cling将自动检测到getStatus()方法是JavaBean getter方法(其名称以get或is开头),并使用JavaBean属性名查找相关的状态变量。在这种情况下,javaBean属性Status和getStatus()也足够聪明,可以知道您真正需要名为Status的大写UPnP状态变量。

4.1.2 显式命名相关状态变量

如果映射的操作方法与映射状态变量的名称不匹配,则必须提供(任何)参数的相关状态变量的名称:

@UpnpAction(
        name = "GetStatus",
        out = @UpnpOutputArgument(
                name = "ResultStatus",
                stateVariable = "Status"
        )
)
public boolean retrieveStatus() {
    return status;
}

在这里,该方法具有名称retrieveStatus,如果希望将其称为GetStatus UPnP 操作,还必须重写它。因为它不再是状态的JavaBean访问器,所以它必须显式地与相关的状态变量Status链接。如果操作有多个输出参数,则必须始终提供相关的状态变量名。

然而,Cling中的“相关状态变量”检测算法还有一个技巧。UPnP 规范规定,仅用于描述输入或输出参数类型的状态变量应以前缀A_ARG_TYPE_.命名。因此,如果不命名操作参数的相关状态变量,Cling还将查找名为“arg”的状态变量type[参数名称]。因此,在上面的示例中,Cling还搜索(未成功)一个名为A_ARG_TYPE_ResultStatus的状态变量。(考虑到在UDA1.0中已经不赞成直接查询状态变量,因此除了操作输入/输出参数的类型声明之外,没有其他状态变量。这是一个很好的例子,为什么UPnP 是如此讨厌的规范。)

对于下一个示例,假设您有一个已经编写的类,不一定是作为UPnP的服务后端,而是用于其他目的。在不中断所有现有代码的情况下,无法重新设计和重写类。Cling在映射操作方法方面提供了一些灵活性,特别是如何获得操作调用的输出。

4.1.3 从其他方法获取输出值

在下面的示例中,UPnP 操作有一个输出参数,但映射方法是空的,不返回任何值:

public boolean getStatus() {
    return status;
}

@UpnpAction(
        name = "GetStatus",
        out = @UpnpOutputArgument(
                name = "ResultStatus",
                getterName = "getStatus"
        )
)
public void retrieveStatus() {
    // NOOP in this example
}

通过在注释中提供getterName ,可以指示Cling在action方法完成时调用此getter方法,并将getter方法的返回值作为输出参数值。如果有几个输出参数,可以将每个参数映射到不同的getter方法。

或者,特别是如果一个操作有多个输出参数,您可以从您的action方法返回用JavaBean包装的多个值。

4.1.4 从JavaBean获取输出值

这里action方法不直接返回输出参数值,而是返回一个JavaBean实例,该实例提供一个getter方法来获取输出参数值:

@UpnpAction(
        name = "GetStatus",
        out = @UpnpOutputArgument(
                name = "ResultStatus",
                getterName = "getWrapped"
        )
)
public StatusHolder getStatus() {
    return new StatusHolder(status);
}

public class StatusHolder {
    boolean wrapped;

    public StatusHolder(boolean wrapped) {
        this.wrapped = wrapped;
    }

    public boolean getWrapped() {
        return wrapped;
    }
}

Cling将检测到您在输出参数中映射了getter名称,并且action方法不是void。现在,它期望在返回的JavaBean上找到getter方法。如果有几个输出参数,那么所有这些参数都必须映射到返回的JavaBean上的getter方法。

开关电源中缺少一个重要部分实现:当SwitchPower的状态更改时,它不会触发任何事件。这实际上是定义SwitchPower服务的规范所要求的。以下部分介绍如何将UPNP服务中的状态更改传播到本地和远程订阅服务器。

4.2 提供有关服务状态更改的事件

JDK中事件处理的标准机制是PropertyChangeListener对PropertyChangeEvent进行响应。Cling 将此API用于服务事件,从而避免了服务代码和专有API之间的依赖关系。

考虑对原SwitchPower进行以下修改实现:

package example.localservice;

import org.fourthline.cling.binding.annotations.*;
import java.beans.PropertyChangeSupport;

@UpnpService(
        serviceId = @UpnpServiceId("SwitchPower"),
        serviceType = @UpnpServiceType(value = "SwitchPower", version = 1)
)
public class SwitchPowerWithPropertyChangeSupport {

    private final PropertyChangeSupport propertyChangeSupport;

    public SwitchPowerWithPropertyChangeSupport() {
        this.propertyChangeSupport = new PropertyChangeSupport(this);
    }

    public PropertyChangeSupport getPropertyChangeSupport() {
        return propertyChangeSupport;
    }

    @UpnpStateVariable(defaultValue = "0", sendEvents = false)
    private boolean target = false;

    @UpnpStateVariable(defaultValue = "0")
    private boolean status = false;

    @UpnpAction
    public void setTarget(@UpnpInputArgument(name = "NewTargetValue") boolean newTargetValue) {

        boolean targetOldValue = target;
        target = newTargetValue;
        boolean statusOldValue = status;
        status = newTargetValue;

        // These have no effect on the UPnP monitoring but it's JavaBean compliant
        getPropertyChangeSupport().firePropertyChange("target", targetOldValue, target);
        getPropertyChangeSupport().firePropertyChange("status", statusOldValue, status);

        // This will send a UPnP event, it's the name of a state variable that triggers events
        getPropertyChangeSupport().firePropertyChange("Status", statusOldValue, status);
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
    public boolean getTarget() {
        return target;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
    public boolean getStatus() {
        return status;
    }

}

唯一的附加依赖项是java.beans.propertyChangeSupport。Cling检测到服务类的getPropertyChangeSupport()方法,并自动在该方法上绑定服务管理。你将不得不有这种方法,以使事件工作与坚持。您可以在服务的构造函数或任何其他方式中创建PropertyChangeSupport实例,Cling唯一感兴趣的是具有UPNP状态变量“property”名称的属性更改事件。

因此,firefropertychange(“nameofastervariable”)就是告诉clang状态变量值已经更改的方式。即使您调用FirePropertyChange(“状态”,空,空)或FirePropertyChange(“状态”,旧值,新值),也没有关系。cling只关心状态变量的名称;然后它将检查状态变量是否发生,并通过访问适当的字段或getter将数据从服务实现实例中拉出。忽略您传递的任何“旧”或“新”值。
另外请注意,FirePropertyChange(“Target”,空,空)将不起作用,因为目标是用sendEvents=“false”映射的。

大多数情况下,JavaBean属性名与UPnP状态变量名不同。例如,javaBean状态属性名是小写的,而upnp状态变量名是大写状态。Cling 事件系统忽略任何不确切命名服务状态变量的属性更改事件。这允许您独立于UPnP事件独立地使用JavaBean事件,例如用于GUI绑定(Swing组件也使用JavaBean事件系统)。

为了下一个例子,我们假设目标实际上也是事件,比如状态。如果您的服务中有多个事件状态变量发生更改,但您不想为每个变量触发单独的更改事件,则可以在单个事件中将它们组合为状态变量名称的逗号分隔列表:

@UpnpAction
public void setTarget(@UpnpInputArgument(name = "NewTargetValue") boolean newTargetValue) {

    target = newTargetValue;
    status = newTargetValue;

    // If several evented variables changed, bundle them in one event separated with commas:
    getPropertyChangeSupport().firePropertyChange(
        "Target, Status", null, null
    );

    // Or if you don't like string manipulation:
    // getPropertyChangeSupport().firePropertyChange(
    //    ModelUtil.toCommaSeparatedList(new String[]{"Target", "Status"}), null, null
    //);
}

更高级的映射是可能的并且经常需要,如下面的例子所示。我们现在离开了开关电源服务落后了,因为它不再复杂了。

4.3 转换字符串动作参数值

UPnP规范定义了自定义数据类型的框架。可预见的结果是,服务设计者和销售商用他们认为需要的任何语义来重载字符串。例如,UPnP A/V规范通常需要值列表(如字符串列表或数字列表),然后将它们作为单个字符串在服务和控制点之间传输——单个值在逗号分隔的字符串中表示。

CLAIN支持这些转换,并且试图尽可能透明。

4.3.1 串值转换器

考虑以下所有状态变量的服务类stringUPnP数据类型——但具有更具体的Java类型:

import org.fourthline.cling.model.types.csv.CSV;
import org.fourthline.cling.model.types.csv.CSVInteger;

@UpnpService(
        serviceId = @UpnpServiceId("MyService"),
        serviceType = @UpnpServiceType(namespace = "mydomain", value = "MyService"),
        stringConvertibleTypes = MyStringConvertible.class
)
public class MyServiceWithStringConvertibles {

    @UpnpStateVariable
    private URL myURL;

    @UpnpStateVariable
    private URI myURI;

    @UpnpStateVariable(datatype = "string")
    private List<Integer> myNumbers;

    @UpnpStateVariable
    private MyStringConvertible myStringConvertible;

    @UpnpAction(out = @UpnpOutputArgument(name = "Out"))
    public URL getMyURL() {
        return myURL;
    }

    @UpnpAction
    public void setMyURL(@UpnpInputArgument(name = "In") URL myURL) {
        this.myURL = myURL;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "Out"))
    public URI getMyURI() {
        return myURI;
    }

    @UpnpAction
    public void setMyURI(@UpnpInputArgument(name = "In") URI myURI) {
        this.myURI = myURI;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "Out"))
    public CSV<Integer> getMyNumbers() {
        CSVInteger wrapper = new CSVInteger();
        if (myNumbers != null)
            wrapper.addAll(myNumbers);
        return wrapper;
    }

    @UpnpAction
    public void setMyNumbers(
            @UpnpInputArgument(name = "In")
            CSVInteger myNumbers
    ) {
        this.myNumbers = myNumbers;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "Out"))
    public MyStringConvertible getMyStringConvertible() {
        return myStringConvertible;
    }

    @UpnpAction
    public void setMyStringConvertible(
            @UpnpInputArgument(name = "In")
            MyStringConvertible myStringConvertible
    ) {
        this.myStringConvertible = myStringConvertible;
    }
}

状态变量都是UPnP数据类型。string因为CLing知道注释字段的Java类型是“字符串可转换”。这种情况总是如此。Java.NET.URI和java.net.URL

您希望在自动字符串转换中使用的任何其他Java类型都必须在@UpnpService类别注释,如迈斯林敞篷车. 请注意,这些类型必须具有适当的toString()方法和接受参数的单个参数构造函数。Java.Lang.Stand“从字符串转换”)

这个List<Integer>是在服务实现中使用的集合来对多个数字进行分组。假设UPnP通信需要一个字符串中单个值的逗号分隔表示,这是许多UPnP A/V规范所要求的。首先,请注意,状态变量实际上是一个字符串数据类型,它不能从字段类型推断出。然后,如果一个操作具有这个输出参数,则不是手动创建逗号分隔的字符串,而是从类中选择适当的转换器。第四种模式并从你的动作方法中返回。这些其实是java.util.List实现,因此您可以使用它们。相反属于java.util.List如果你不在乎依赖关系。任何动作输入参数值也可以从逗号分隔的字符串表示形式自动转换为列表-您所要做的就是使用CSV转换器类作为输入参数类型。

4.3.2 使用枚举

不幸的是,Celing可以将您的枚举值转换为UPnP消息中传输的字符串,但是您必须手动从字符串中转换它。如下服务示例所示:

@UpnpService(
        serviceId = @UpnpServiceId("MyService"),
        serviceType = @UpnpServiceType(namespace = "mydomain", value = "MyService"),
        stringConvertibleTypes = MyStringConvertible.class
)
public class MyServiceWithEnum {

    public enum Color {
        Red,
        Green,
        Blue
    }

    @UpnpStateVariable
    private Color color;

    @UpnpAction(out = @UpnpOutputArgument(name = "Out"))
    public Color getColor() {
        return color;
    }

    @UpnpAction
    public void setColor(@UpnpInputArgument(name = "In") String color) {
        this.color = Color.valueOf(color);
    }

}

如果字段(或吸收器)或GETTER Java类型是枚举,则CLAN将自动假定数据类型是UPnP字符串。此外,将在服务描述符XML中创建一个<allowedValueList>,因此控制点知道该状态变量实际上具有一组已定义的可能值。

4.4 限制允许的状态变量值

UPNP规范定义了一组规则,用于限制状态变量的合法值及其类型。对于字符串类型的状态变量,可以提供独占允许字符串的列表。对于数值状态变量,可以提供具有最小值、最大值和允许“步进”(间隔)的值范围。

4.4.1 字符串值的独占列表

如果有合法字符串值的静态列表,请直接在状态变量字段的注释上进行设置:

@UpnpStateVariable(
    allowedValues = {"Foo", "Bar", "Baz"}
)
private String restricted;

或者,如果在绑定服务时必须动态确定允许的值,则可以使用org.4thline.cling.binding.allowedValueProvider接口实现类:

public static class MyAllowedValueProvider implements AllowedValueProvider {
    @Override
    public String[] getValues() {
        return new String[] {"Foo", "Bar", "Baz"};
    }
}

然后,不要在状态变量声明中指定字符串值的静态列表,而是将提供程序类命名为:

@UpnpStateVariable(
    allowedValueProvider= MyAllowedValueProvider.class
)
private String restricted;

请注意,只有在处理注释时才会查询此提供程序,当服务绑定到cling时才会查询一次。

4.4.2 限制数值范围

对于数字状态变量,在声明状态变量时,可以在一个范围内限制一组合法值:

@UpnpStateVariable(
    allowedValueMinimum = 10,
    allowedValueMaximum = 100,
    allowedValueStep = 5
)
private int restricted;

或者,如果在绑定服务时必须动态确定允许的范围,则可以使用org.4thline.cling.binding.allowedValueRangeProvider接口实现类:

public static class MyAllowedValueProvider implements AllowedValueRangeProvider {
    @Override
    public long getMinimum() {
        return 10;
    }

    @Override
    public long getMaximum() {
        return 100;
    }

    @Override
    public long getStep() {
        return 5;
    }
}

然后,不要在状态变量声明中指定字符串值的静态列表,而是将提供程序类命名为:

@UpnpStateVariable(
    allowedValueRangeProvider = MyAllowedValueProvider.class
)
private int restricted;

请注意,只有在处理注释时才会查询此提供程序,当服务绑定到cling时才会查询一次。

5. 在Android上使用Cling

CLING CORE为Android应用程序提供了一个UPNP堆栈。通常,你会编写控制点应用程序,因为现在大多数Android系统都是小型手持设备。但是,您也可以在Android上编写UPNP服务器应用程序,支持CLING CORE的所有功能。
cling 2.x需要Android平台级别15(4.0),使用cling 1.x支持旧版Android。

下面的示例基于cling发行版中提供的cling demo android应用程序、cling demo android浏览器和cling demo android light。

5.1 配置应用服务

您可以像往常一样在Android应用程序的主要活动中实例化cling-upnpservice。另一方面,如果应用程序中的几个活动需要访问UPNP堆栈,则更好的设计将使用背景android.app.service。任何想要访问UPNP堆栈的活动都可以根据需要从该服务绑定和解除绑定。
此类服务组件的接口在cling中提供,如org.fourthline.cling.android.androidupnpservice:

public interface AndroidUpnpService {

    /**
     * @return The actual main instance and interface of the UPnP service.
     */
    public UpnpService get();

    /**
     * @return The configuration of the UPnP service.
     */
    public UpnpServiceConfiguration getConfiguration();

    /**
     * @return The registry of the UPnP service.
     */
    public Registry getRegistry();

    /**
     * @return The client API of the UPnP service.
     */
    public ControlPoint getControlPoint();

}

活动通常访问已知UPNP设备的注册表,或使用控制点搜索和控制UPNP设备。
您必须在androidmanifest.xml中配置此服务组件的内置实现,以及各种必需的权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.fourthline.cling.demo.android.browser">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>

    <uses-sdk
            android:targetSdkVersion="22"
            android:minSdkVersion="15"/>

    <application
            android:icon="@drawable/appicon"
            android:label="@string/appName"
            android:allowBackup="false">

        <activity android:name=".BrowserActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service android:name="org.fourthline.cling.android.AndroidUpnpServiceImpl"/>

        <!-- Or a custom service configuration, also use this class in bindService()!
        <service android:name=".BrowserUpnpService"/>
        -->

    </application>

</manifest>

如果存在WiFi接口,则CLING需要访问该接口。CLING将自动检测网络接口何时打开和关闭,并优雅地处理这种情况:当没有网络可用时,任何客户端操作都将导致“服务器无响应”状态。无论如何,您的客户机代码必须处理这种情况。
Cling在Android上使用了一个自定义配置,即AndroidupNpserviceConfiguration,它利用了Jetty传输和恢复中的XML解析器和处理器。有关更多信息,请参见类的javadoc。
Jetty 8库需要在Android上使用cling,请参阅Maven依赖项的演示应用程序!
例如,在Maven POM中,通常需要这些依赖项,才能在Android上工作:

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-server</artifactId>
        <version>${jetty.version}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-servlet</artifactId>
        <version>${jetty.version}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-client</artifactId>
        <version>${jetty.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-jdk14</artifactId>
        <version>${slf4j.version}</version>
    </dependency>

服务组件在创建和销毁服务组件时启动和停止UPNP系统。这取决于您如何从活动中访问服务组件。

5.2 从Activity中获取服务

Android中服务组件的生命周期定义良好。绑定到服务的第一个活动将在服务尚未运行时启动该服务。当不再有活动绑定到服务时,操作系统将销毁该服务。
让我们编写一个简单的UPNP浏览活动。它在列表中显示网络上的所有设备,并且它有一个菜单选项,可以触发搜索操作。活动连接到UPNP服务,然后监听注册表中的任何设备添加或删除操作,因此显示的设备列表保持最新:

public class BrowserActivity extends ListActivity {

    private ArrayAdapter<DeviceDisplay> listAdapter;

    private BrowseRegistryListener registryListener = new BrowseRegistryListener();

    private AndroidUpnpService upnpService;

    private ServiceConnection serviceConnection = new ServiceConnection() {

        public void onServiceConnected(ComponentName className, IBinder service) {
            upnpService = (AndroidUpnpService) service;

            // Clear the list
            listAdapter.clear();

            // Get ready for future device advertisements
            upnpService.getRegistry().addListener(registryListener);

            // Now add all devices to the list we already know about
            for (Device device : upnpService.getRegistry().getDevices()) {
                registryListener.deviceAdded(device);
            }

            // Search asynchronously for all devices, they will respond soon
            upnpService.getControlPoint().search();
        }

        public void onServiceDisconnected(ComponentName className) {
            upnpService = null;
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Fix the logging integration between java.util.logging and Android internal logging
        org.seamless.util.logging.LoggingUtil.resetRootHandler(
            new FixedAndroidLogHandler()
        );
        // Now you can enable logging as needed for various categories of Cling:
        // Logger.getLogger("org.fourthline.cling").setLevel(Level.FINEST);

        listAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        setListAdapter(listAdapter);

        // This will start the UPnP service if it wasn't already started
        getApplicationContext().bindService(
            new Intent(this, AndroidUpnpServiceImpl.class),
            serviceConnection,
            Context.BIND_AUTO_CREATE
        );
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (upnpService != null) {
            upnpService.getRegistry().removeListener(registryListener);
        }
        // This will stop the UPnP service if nobody else is bound to it
        getApplicationContext().unbindService(serviceConnection);
    }
    // ...
}

我们使用android运行时和listactivity超类提供的默认布局。请注意,此活动可以是应用程序的主活动,也可以是任务堆栈中更高的活动。listadapter是cling注册表上的设备添加和删除与用户界面中显示的项目列表之间的粘合剂。

当没有后端服务绑定到此活动时,upnpservice变量为空。绑定和取消绑定发生在onCreate()和onDestroy()回调中,因此只要活动还活着,就将其绑定到服务。
绑定和取消绑定服务是通过serviceconnection处理的:在connect上,首先将一个侦听器添加到upnp服务的注册表中。此侦听器将处理在网络上发现的设备的添加和删除,并更新用户界面列表中显示的项目。当活动被破坏时,将删除browseregistryListener。
然后将任何已经发现的设备手动添加到用户界面,并通过侦听器传递它们。(如果UPNP服务刚刚启动,到目前为止还没有设备宣布它的存在,那么可能就没有了。)最后,您通过向所有UPNP设备发送搜索消息来启动异步发现,因此它们将自己宣布。每次连接到服务时不需要此搜索消息。当(主)活动和应用程序启动时,只需要用所有已知设备填充注册表一次。
这是browseregistryListener,它的唯一任务是更新显示的列表项:

protected class BrowseRegistryListener extends DefaultRegistryListener {

    /* Discovery performance optimization for very slow Android devices! */
    @Override
    public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
        deviceAdded(device);
    }

    @Override
    public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(
                    BrowserActivity.this,
                    "Discovery failed of '" + device.getDisplayString() + "': "
                        + (ex != null ? ex.toString() : "Couldn't retrieve device/service descriptors"),
                    Toast.LENGTH_LONG
                ).show();
            }
        });
        deviceRemoved(device);
    }
    /* End of optimization, you can remove the whole block if your Android handset is fast (>= 600 Mhz) */

    @Override
    public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
        deviceAdded(device);
    }

    @Override
    public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
        deviceRemoved(device);
    }

    @Override
    public void localDeviceAdded(Registry registry, LocalDevice device) {
        deviceAdded(device);
    }

    @Override
    public void localDeviceRemoved(Registry registry, LocalDevice device) {
        deviceRemoved(device);
    }

    public void deviceAdded(final Device device) {
        runOnUiThread(new Runnable() {
            public void run() {
                DeviceDisplay d = new DeviceDisplay(device);
                int position = listAdapter.getPosition(d);
                if (position >= 0) {
                    // Device already in the list, re-set new value at same position
                    listAdapter.remove(d);
                    listAdapter.insert(d, position);
                } else {
                    listAdapter.add(d);
                }
            }
        });
    }

    public void deviceRemoved(final Device device) {
        runOnUiThread(new Runnable() {
            public void run() {
                listAdapter.remove(new DeviceDisplay(device));
            }
        });
    }
}

出于性能方面的考虑,当发现新设备时,我们不会等到完全水合(检索并验证所有服务)的设备元数据模型可用。我们尽可能快地做出反应,不要等到调用remotedeviceaded()方法。即使发现仍在运行,我们也会显示任何设备。在台式计算机上通常不关心这个问题,但是,Android手持设备速度很慢,UPNP使用几个膨胀的XML描述符来交换设备和服务的元数据。有时需要几秒钟,设备及其服务才能完全可用。在发现过程中,将尽快调用RemoteDeviceDiscoveryStarted()和RemoteDeviceDiscoveryFailed()方法。在现代快速安卓手机上,除非你必须在局域网上处理数十个UPNP设备,否则你不需要这种优化。
顺便说一下,设备是相等的(a.equals(b))如果它们有相同的UDN,它们可能不相同(a==b)。
注册表将在单独的线程中调用侦听器方法。您必须更新用户界面线程中显示的列表数据。
活动上的以下方法将添加一个带有搜索操作的菜单,以便用户可以手动刷新列表:

public class BrowserActivity extends ListActivity {

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add(0, 0, 0, R.string.searchLAN).setIcon(android.R.drawable.ic_menu_search);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case 0:
                if (upnpService == null)
                    break;
                Toast.makeText(this, R.string.searchingLAN, Toast.LENGTH_SHORT).show();
                upnpService.getRegistry().removeAllRemoteDevices();
                upnpService.getControlPoint().search();
                break;
        }
        return false;
    }
    // ...
}

最后,deviceDisplay类是一个非常简单的JavaBean,它只提供用于呈现列表的toString()方法。通过更改此方法,您可以显示有关UPNP设备的任何信息:

protected class DeviceDisplay {

    Device device;

    public DeviceDisplay(Device device) {
        this.device = device;
    }

    public Device getDevice() {
        return device;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DeviceDisplay that = (DeviceDisplay) o;
        return device.equals(that.device);
    }

    @Override
    public int hashCode() {
        return device.hashCode();
    }

    @Override
    public String toString() {
        String name =
            getDevice().getDetails() != null && getDevice().getDetails().getFriendlyName() != null
                ? getDevice().getDetails().getFriendlyName()
                : getDevice().getDisplayString();
        // Display a little star while the device is being loaded (see performance optimization earlier)
        return device.isFullyHydrated() ? name : name + " *";
    }
}

我们还必须重写相等操作,因此我们可以使用devicedisplay实例手动从列表中删除和添加设备,作为一个方便的句柄。
到目前为止,我们已经实现了一个UPNP控制点,接下来我们创建一个具有服务的UPNP设备。

5.3 创建UPnP设备

以下活动提供UPnP服务,即众所周知的switchPower:1和BinaryLight:1设备:

public class LightActivity extends Activity implements PropertyChangeListener {

    private AndroidUpnpService upnpService;

    private UDN udn = new UDN(UUID.randomUUID()); // TODO: Not stable!

    private ServiceConnection serviceConnection = new ServiceConnection() {

        public void onServiceConnected(ComponentName className, IBinder service) {
            upnpService = (AndroidUpnpService) service;

            LocalService<SwitchPower> switchPowerService = getSwitchPowerService();

            // Register the device when this activity binds to the service for the first time
            if (switchPowerService == null) {
                try {
                    LocalDevice binaryLightDevice = createDevice();

                    Toast.makeText(LightActivity.this, R.string.registeringDevice, Toast.LENGTH_SHORT).show();
                    upnpService.getRegistry().addDevice(binaryLightDevice);

                    switchPowerService = getSwitchPowerService();

                } catch (Exception ex) {
                    log.log(Level.SEVERE, "Creating BinaryLight device failed", ex);
                    Toast.makeText(LightActivity.this, R.string.createDeviceFailed, Toast.LENGTH_SHORT).show();
                    return;
                }
            }

            // Obtain the state of the power switch and update the UI
            setLightbulb(switchPowerService.getManager().getImplementation().getStatus());

            // Start monitoring the power switch
            switchPowerService.getManager().getImplementation().getPropertyChangeSupport()
                    .addPropertyChangeListener(LightActivity.this);

        }

        public void onServiceDisconnected(ComponentName className) {
            upnpService = null;
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.light);

        getApplicationContext().bindService(
                new Intent(this, AndroidUpnpServiceImpl.class),
                serviceConnection,
                Context.BIND_AUTO_CREATE
        );
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // Stop monitoring the power switch
        LocalService<SwitchPower> switchPowerService = getSwitchPowerService();
        if (switchPowerService != null)
            switchPowerService.getManager().getImplementation().getPropertyChangeSupport()
                    .removePropertyChangeListener(this);

        getApplicationContext().unbindService(serviceConnection);
    }

    protected LocalService<SwitchPower> getSwitchPowerService() {
        if (upnpService == null)
            return null;

        LocalDevice binaryLightDevice;
        if ((binaryLightDevice = upnpService.getRegistry().getLocalDevice(udn, true)) == null)
            return null;

        return (LocalService<SwitchPower>)
                binaryLightDevice.findService(new UDAServiceType("SwitchPower", 1));
    }
    // ...
}

当upnp服务被绑定时,我们第一次通过查询注册表来检查设备是否已经被创建。如果没有,我们创建设备并将其添加到注册表。

该活动也是在switchpower服务中注册的JavaBean PropertyChangeListener。注意,这是JavaBean事件,与UPNP Gena事件无关!我们监控服务状态并相应地切换UI,打开和关闭灯:

public class LightActivity extends Activity implements PropertyChangeListener {

    public void propertyChange(PropertyChangeEvent event) {
        // This is regular JavaBean eventing, not UPnP eventing!
        if (event.getPropertyName().equals("status")) {
            log.info("Turning light: " + event.getNewValue());
            setLightbulb((Boolean) event.getNewValue());
        }
    }

    protected void setLightbulb(final boolean on) {
        runOnUiThread(new Runnable() {
            public void run() {
                ImageView imageView = (ImageView) findViewById(R.id.light_imageview);
                imageView.setImageResource(on ? R.drawable.light_on : R.drawable.light_off);
                // You can NOT externalize this color into /res/values/colors.xml. Go on, try it!
                imageView.setBackgroundColor(on ? Color.parseColor("#9EC942") : Color.WHITE);
            }
        });
    }
    // ...
}

createDevice()方法只是实例化一个新的本地设备:

public class LightActivity extends Activity implements PropertyChangeListener {

    protected LocalDevice createDevice()
            throws ValidationException, LocalServiceBindingException {

        DeviceType type =
                new UDADeviceType("BinaryLight", 1);

        DeviceDetails details =
                new DeviceDetails(
                        "Friendly Binary Light",
                        new ManufacturerDetails("ACME"),
                        new ModelDetails("AndroidLight", "A light with on/off switch.", "v1")
                );

        LocalService service =
                new AnnotationLocalServiceBinder().read(SwitchPower.class);

        service.setManager(
                new DefaultServiceManager<>(service, SwitchPower.class)
        );

        return new LocalDevice(
                new DeviceIdentity(udn),
                type,
                details,
                createDefaultDeviceIcon(),
                service
        );
    }
    // ...
}

对于switchpower类,再次注意javaBeans和upnp的双重事件:

@UpnpService(
        serviceId = @UpnpServiceId("SwitchPower"),
        serviceType = @UpnpServiceType(value = "SwitchPower", version = 1)
)
public class SwitchPower {

    private final PropertyChangeSupport propertyChangeSupport;

    public SwitchPower() {
        this.propertyChangeSupport = new PropertyChangeSupport(this);
    }

    public PropertyChangeSupport getPropertyChangeSupport() {
        return propertyChangeSupport;
    }

    @UpnpStateVariable(defaultValue = "0", sendEvents = false)
    private boolean target = false;

    @UpnpStateVariable(defaultValue = "0")
    private boolean status = false;

    @UpnpAction
    public void setTarget(@UpnpInputArgument(name = "NewTargetValue") boolean newTargetValue) {
        boolean targetOldValue = target;
        target = newTargetValue;
        boolean statusOldValue = status;
        status = newTargetValue;

        // These have no effect on the UPnP monitoring but it's JavaBean compliant
        getPropertyChangeSupport().firePropertyChange("target", targetOldValue, target);
        getPropertyChangeSupport().firePropertyChange("status", statusOldValue, status);

        // This will send a UPnP event, it's the name of a state variable that sends events
        getPropertyChangeSupport().firePropertyChange("Status", statusOldValue, status);
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
    public boolean getTarget() {
        return target;
    }

    @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
    public boolean getStatus() {
        return status;
    }
}

5.4 自定义服务行为

UPNP服务在运行时消耗内存和CPU时间。虽然这在普通机器上通常不是问题,但在Android手机上可能是问题。如果您禁用了CLING UPNP服务的某些功能,或者您甚至在适当的时候暂停并恢复它,则可以保留内存和手机电池的电源。
此外,一些Android手机不支持多播网络(例如HTC手机),因此您必须在此类设备上相应地配置CLING,并禁用大多数UPNP发现协议。

5.4.1 优化注册表维护

当服务运行时,后台会发生一些事情。首先,存在服务及其维护线程的注册表。如果您正在编写控制点,此后台注册表维护程序将定期使用远程服务续订出站Gena订阅。它还将过期,并在不说再见就退出网络时删除任何发现的远程设备。如果您提供本地服务,您的设备公告将由注册表维护人员刷新,如果未及时更新,则将删除入站Gena订阅。有效地,注册表维护人员可以防止UPNP网络上的过时状态,因此所有参与者都具有所有其他参与者的最新视图,等等。
默认情况下,注册表维护人员将每秒钟运行一次,并检查是否有什么事情要做(当然,大多数时候没有什么事情要做)。但是,默认的Android配置的默认睡眠间隔为3秒,因此它已经占用了较少的后台CPU时间,而您的应用程序可能会暴露在一些过时的信息中。您可以通过重写upnpserviceConfiguration中的getRegistryMaintenanceIntervalMillis()进一步优化此设置。在Android上,您必须对服务实现进行子类化,以提供新的配置:

public class BrowserUpnpService extends AndroidUpnpServiceImpl {

    @Override
    protected UpnpServiceConfiguration createConfiguration() {
        return new AndroidUpnpServiceConfiguration() {

            @Override
            public int getRegistryMaintenanceIntervalMillis() {
                return 7000;
            }

        };
    }
}

别忘了现在在androidmanifest.xml中配置browserupnpservice,而不是原来的实现。当绑定到活动中的服务时,您还必须使用这个类,而不是AndroidupNPServiceImpl。

5.4.2 暂停和恢复注册表维护

另一个更有效但也更复杂的优化是,当您的活动不再需要UPNP服务时,暂停并恢复注册表。通常情况下,活动不再位于前台(暂停),甚至不再可见(停止)。默认情况下,任何活动状态更改都不会影响UPNP服务的状态,除非您在活动生命周期回调中绑定和取消绑定来自或指向该服务。
除了从服务绑定和解除绑定之外,还可以在调用活动的onPause()或onStop()方法时调用registry pause()来暂停其注册表。然后可以使用registry resume()恢复后台服务维护(线程),或者使用registry ispaused()检查状态。
请阅读这些方法的javadoc了解更多详细信息,以及暂停注册表维护对设备、服务和gena订阅有什么影响。根据您的应用程序所做的,这种非常小的优化可能不值得处理这些影响。另一方面,您的应用程序应该已经能够处理失败的gena订阅续订,或消失的远程设备!

5.4 配置发现服务

最有效的优化是选择性地发现UPNP设备。尽管UPNP服务的网络传输层将在后台继续运行(线程正在等待,套接字已绑定),但此功能允许您有选择地快速删除发现消息。
例如,如果您正在编写一个控制点,那么如果接收到的任何发现消息没有公布您想要控制的服务,那么您可以删除它—您对任何其他设备都不感兴趣。另一方面,如果您只提供设备和服务,则可能会删除所有发现消息(搜索服务消息除外),您对任何远程设备及其服务都不感兴趣。
当UDP数据报内容可用时,CLING会选择并可能删除发现消息,因此不需要进一步的分析和处理,并且在Android手机上保持UPNP服务在后台运行时,CPU时间/内存消耗会显著减少。
要配置控制点应用程序支持哪些服务,请重写配置并提供一组ServiceType实例:

public class BrowserUpnpService extends AndroidUpnpServiceImpl {

    @Override
    protected UpnpServiceConfiguration createConfiguration() {
        return new AndroidUpnpServiceConfiguration() {

            @Override
            public ServiceType[] getExclusiveServiceTypes() {
                return new ServiceType[]{
                    new UDAServiceType("SwitchPower")
                };
            }
        };
    }
}

此配置将忽略来自任何设备的任何广告,这些设备也不广告模式upnp org:switchpower:1服务。这是我们的控制点可以处理的,所以我们不需要其他任何东西。如果您返回一个空数组(默认行为),那么将发现所有服务和设备,并且不会删除任何广告。
如果不是编写控制点而是服务器应用程序,则可以在getExclusiveServiceTypes()方法中返回空值。这将完全禁用发现,现在所有设备和服务广告一收到即被丢弃。

上一篇下一篇

猜你喜欢

热点阅读