4.2 实现数据通信的解决方案
在穿戴设备的网络传输应用中,通常可以通过TCP、IP或UDP这三种协议实现数据传输。在传输数据的过程中,需要通过一个双向的通信连接实现数据的交互。在这个传输过程中,通常将这个双向链路的一端称为Socket,一个Socket通常由一个IP地址和一个端口号来确定。由此可见,在整个数据传输过程中,Socket的作用是十分巨大的。在Java编程应用中,Socket是Java网络编程的核心。因为Java是Android应用开发的主流语言,所以在本节的内容中将详细讲解在Android系统中实现数据通信的基本知识,为读者步入本书后面知识的学习打下基础。
4.2.1 使用ServletSocket传递数据
在Java程序中,使用ServerSocket类接受其他通信实体的连接请求。对象ServerSocket的功能是监听来自客户端的Socket连接,如果没有连接则会一直处于等待状态。在类ServerSocket中包含了如下监听客户端连接请求的方法。
● Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket,否则该方法将一直处于等待状态,线程也被阻塞。
为了创建ServerSocket对象,ServerSocket类为我们提供了如下构造器。
● ServerSocket(int port):用指定的端口port创建一个ServerSocket,该端口应该是有一个有效的端口整数值:0~65535。
● ServerSocket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog。
● ServerSocket(int port,int backlog,InetAddress localAddr):在机器存在多个IP地址的情况下,允许通过localAddr这个参数来指定将ServerSocket绑定到指定的IP地址。
当使用ServerSocket后,需要使用ServerSocket中的方法close()关闭该ServerSocket。在通常情况下,因为服务器不会只接受一个客户端请求,而是会不断地接受来自客户端的所有请求,所以可以通过循环来不断地调用ServerSocket中的方法accept()。
4.2.2 使用Socket传递数据
在客户端可以使用Socket的构造器实现和指定服务器的连接,在Socket中可以使用如下两个构造器。
● Socket(InetAddress/String remoteAddress,int port):创建连接到指定远程主机、远程端口的Socket,该构造器没有指定本地地址、本地端口,默认使用本地主机的默认IP地址,默认使用系统动态指定的IP地址。
● Socket(InetAddress/String remoteAddress,int port,InetAddress localAddr,int localPort):创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口号,适用于本地主机有多个IP地址的情形。
在使用上述构造器指定远程主机时,既可使用InetAddress来指定,也可以使用String对象指定,在Java中通常使用String对象指定远程IP,例如192.168.2.23。当本地主机只有一个IP地址时,建议使用第一种方法,因为这样更简单。
4.2.3 实现非阻塞Socket通信
在Java应用程序中,可以使用NIO API来开发高性能网络服务器。当程序执行输入、输出操作后,在这些操作返回之前会一直阻塞该线程,服务器必须为每个客户端都提供一条独立线程进行处理。这说明前面的程序是基于阻塞式API的,当服务器需要同时处理大量客户端时,这种做法会降低性能。
在Java应用程序中可以用NIO API让服务器使用一个或有限几个线程来同时处理连接到服务器上的所有客户端。在Java的NIO中,为非阻塞式的Socket通信提供了以下的特殊类。
● Selector:是SelectableChannel对象的多路复用器,所有希望采用非阻塞方式进行通信的Channel都应该注册到Selector对象。可通过调用此类的静态open()方法来创建Selector实例,该方法将使用系统默认的Selector来返回新的Selector。Selector可以同时监控多个SelectableChannel的IO状况,是非阻塞IO的核心。一个Selector实例有如下三个SelectionKey的集合。
● 所有SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
● 被选择的SelectionKey集合:代表了所有可以通过select()方法监测到、需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
● 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问该集合。
除此之外,Selector还提供了如下和select()相关的方法。
● int select():监控所有注册的Channel,当它们中间有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。
● int select(long timeout):可以设置超时时长的select()操作。
● int selectNow():执行一个立即返回的select()操作,相对于无参数的select()方法而言,该方法不会阻塞线程。
● Selector wakeup():使一个还未返回的select()方法立刻返回。
● SelectableChannel:它代表可以支持非阻塞IO操作的Channel对象,可以将其注册到Selector上,这种注册的关系由SelectionKey实例表示。在Selector对象中,可以使用select()方法设置允许应用程序同时监控多个IO Channel。Java程序可调用SelectableChannel中的register()方法将其注册到指定Selector上,当该Selector上某些SelectableChannel上有需要处理的IO操作时,程序可以调用Selector实例的select()方法获取它们的数量,并通过selectedKeys()方法返回它们对应的SelectKey集合。这个集合的作用十分巨大,因为通过该集合就可以获取所有需要处理IO操作的SelectableChannel集。
对象SelectableChannel支持阻塞和非阻塞两种模式,其中所有Channel默认都是阻塞模式,必须使用非阻塞式模式才可以利用非阻塞IO操作。
在SelectableChannel中提供了如下两个方法来设置和返回该Channel的模式状态。
● SelectableChannel configureBlocking(boolean block):设置是否采用阻塞模式。
● boolean isBlocking():返回该Channel是否是阻塞模式。
不同的SelectableChannel所支持的操作不一样,例如ServerSocketChannel代表一个Server-Socket,它就只支持OP_ACCEPT操作。在SelectableChannel中提供了如下方法来返回它支持的所有操作。
● int validOps() :返回一个bit mask,表示这个Channel上支持的IO操作。
除此之外,SelectableChannel还提供了如下方法获取它的注册状态。
● boolean isRegistered():返回该Channel是否已注册在一个或多个Selector上。
● SelectionKey keyFor(Selector sel):返回该Channel和Selector之间的注册关系,如果不存在注册关系,则返回null。
● SelectionKey:该对象代表SelectableChannel和Selector之间的注册关系。
● ServerSocketChannel:支持非阻塞操作,对应于java.net.ServerSocket这个类,提供了TCP协议IO接口,只支持OP_ACCEPT操作。该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。
● SocketChannel:支持非阻塞操作,对应于java.net.Socket这个类,提供了TCP协议IO接口,支持OP_CONNECT,OP_READ和OP_WRITE操作。这个类还实现了Byte-Channel接口、ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通过SocketChannel来读写ByteBuffer对象。
服务器上所有Channel都需要向Selector注册,包括ServerSocketChannel和SocketChannel。该Selector则负责监视这些Socket的IO状态,当其中任意一个或多个Channel具有可用的IO操作时,该Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具有可用的IO操作,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合。正是通过Selector才使得服务器端只需要不断地调用Selector实例的select()方法,这样就可以知道当前所有Channel是否有需要处理的IO操作。当Selector上注册的所有Channel都没有需要处理的IO操作时,将会阻塞select()方法,此时调用该方法的线程被阻塞。
4.2.4 使用DatagramSocket实现数据交互
DatagramSocket本身只是码头,不维护状态,不能产生IO流,其唯一的功能是接收和发送数据报。Java语言使用DatagramPacket代表数据报,DatagramSocket的接收和发送数据功能都是通过DatagramPacket对象实现的。
在DatagramSocket中有如下三个构造器。
● DatagramSocket():负责创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、本机所有可用端口中随机选择的某个端口。
● DatagramSocket(int prot):负责创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、指定端口。
● DatagramSocket(int port,InetAddress laddr):负责创建一个DatagramSocket实例,并将该对象绑定到指定IP地址、指定端口。
在Java程序中,通过上述任意一个构造器即可创建一个DatagramSocket实例。在创建服务器时必须创建指定端口的DatagramSocket实例,目的是保证其他客户端可以将数据发送到该服务器。一旦得到了DatagramSocket实例,即可通过下面的两个方法接收和发送数据。
● receive(DatagramPacket p):从该DatagramSocket中接收数据报。
● send(DatagramPacket p):以该DatagramSocket对象向外发送数据报。
在使用DatagramSocket发送数据报时,DatagramSocket并不知道将该数据报发送到哪里,而是由DatagramPacket自身决定数据报的目的。就像码头并不知道每个集装箱的目的地,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。
4.2.5 发送求救信号的实现技巧
通过本章前面内容的学习,已经了解了Java应用中Socket网络编程的基本知识。在Android平台中可以使用相同的方法用Socket实现数据传输功能。在本节的内容中将通过一个具体实例的实现过程,来讲解在Android穿戴设备中使用Socket发送求救信号的基本方法。
本实例的具体实现流程如下所示。
(1)首先实现服务器端,使用Eclipse新建一个名为“android_server”的Java工程,然后编写服务器端的实现文件AndroidServer.java,功能是创建Socket对象client以接受客户端请求,并创建BufferedReader对象in向服务器发送消息。文件AndroidServer.java的具体实现代码如下所示。
(2)开始实现客户端的测试程序,使用Eclipse新建一个名为“testSocket”的Android工程,编写布局文件main.xml,在主界面中插入一个信息输入文本框和一个“发送”按钮。
(3)编写测试文件TestSocket.java,功能是获取输入框的文本信息,并将信息发送到“192.168.2.113”。
(4)在文件AndroidManifest.xml中添加访问网络的权限,具体代码如下所示。
到此为止,整个实例已介绍完毕,执行后的效果如图4-3所示。
图4-3 执行效果