(一)原理要点
- 数据类型:
DatagramSocket
与DatagramPacket
- 每个socket在创建时都会绑定一个端口:
- 使用构造方法
public DatagramSocket(int port)
会绑定指定端口; - 使用构造方法
public DatagramSocket()
会随机绑定可用的端口。
- 使用构造方法
- 发送数据报时需要指定目的地址和目的端口:
DatagramPacket packet = new DatagramPacket(data, data.length, dstAddress, dstPort); // data是需要发送的数据(类型:byte[]) socket.send(packet);
- 接收数据报时直接从socket绑定的端口处接收数据,该方法调用时会阻塞线程直至收到数据。
DatagramPacket packet = new DatagramPacket(data, data.length); // data是接收数据的缓冲区(类型:byte[]),接收数据时要注意先清空缓冲区 socket.receive(packet);
- 通信内容的协议可以是自定义的类型,因为需要转换成字节数组进行收发,所以该类型以及其成员变量的类型必须实现
Serializable
接口。相互转换的方法如下所示:public static byte[] encode(Protocol message) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream sOut; sOut = new ObjectOutputStream(out); sOut.writeObject(message); sOut.flush(); return out.toByteArray(); } public static Protocol decode(byte[] data) throws ClassNotFoundException, IOException{ ByteArrayInputStream in = new ByteArrayInputStream(data); ObjectInputStream sIn = new ObjectInputStream(in); return (Protocol)sIn.readObject(); }
(二)开发阶段
- 服务器使用PC,客户端可选用Android Studio的AVD模拟器,所以此时的客户端和服务器端同属一个子网。
- Android模拟器使用固定的本地IP地址10.0.2.2,可以轻松实现从模拟器到本机的通信。但是反过来,主机并不会给模拟器提供特定的IP地址,这就需要通过telnet进入模拟器的Android Console,通过端口重定向(端口映射)来模拟主机服务器到Android模拟器的通信。具体过程如下所示(留意auth token的位置):
- 所以建议服务器与客户端的socket使用不同的端口,这样在服务器和客户端都位于本地PC时不会造成端口冲突的问题。
(三)部署阶段
- 云服务器位于外网,移动客户端处于内网,两者进行通信时必须进行UDP穿透。即客户端先给服务器发送数据报,这样客户端的公网会记录下服务器的IP和端口;当该服务器给客户端发送数据报时,公网会进行IP和端口的映射,从而使客户端接收到服务器的报文。
- 客户端发送报文:
DatagramSocket socket = new DatagramSocket(cPort); // cPort为客户端端口 DatagramPacket packet = new DatagramPacket(data, data.length, sAddress, sPort); // sAddress和sPort为服务器的地址和端口 socket.send(packet); // 客户端发送报文
- 服务器接收报文,并进行响应:
DatagramSocket socket = new DatagramSocket(sPort); // sPort为服务器端口 DatagramPacket packet = new DatagramPacket(data, data.length); socket.receive(packet); // 服务器接收报文 InetAddress cAddress = packet.getAddress(); // 客户端地址 int cPort = packet.getPort(); // 客户端端口,因为进行了映射,所以会与建立客户端socket的端口不同 DatagramPacket packet2 = new DatagramPacket(data2, data2.length, cAddress, cPort); socket.send(packet2); // 服务器发送报文
- 完成了以上两步,客户端才可以接收到服务器的数据报:
DatagramPacket packet2 = new DatagramPacket(data2, data2.length); socket.receive(packet2); // 服务器接收报文
- 注意:客户端和服务器使用不同的端口并不代表发送和接收要使用不同的socket,相反,无论是服务器还是客户端在发送和接收时都需要使用相同的socket(不同的socket不可以绑定同一个端口)。因为在UDP穿透时,客户端所在的公网会记录下服务器的地址和端口以及客户端的地址和端口,当接收到来自该地址和端口的数据报时会将该数据报转发给映射好的地址和端口的客户端。如果服务器在发送时使用了不同的socket,那么客户端所在公网会无法识别该端口;又或者客户端在接收时使用了不同的socket,则无法接收到公网转发过来的数据报。
- 为了避免上一次的socket没有及时关闭而造成端口重复使用的异常,一般不会直接使用端口创建socket,而是使用以下方式:
if(socket == null) { socket = new DatagramSocket(null); socket.setReuseAddress(true); socket.bind(new InetSocketAddress(recvPort)); }