先对上一遍的工具类,补充两点:
1、Client关闭异常
如果没有连接host就调用close()的话,会导致NullPointException,因为mInputStream为null。虽然socket关闭后,输入输出流也会随之关闭,但为了加快回收速度,建议把流也关闭。
public void close() {
if (mSocket != null) {
try {
mInputStream.close();
mOutputStream.close();
mSocket.close();
mInputStream = null;
mOutputStream = null;
mSocket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
修改为:
public void close() {
if (mInputStream != null) {
try {
mInputStream.close();
// mInputStream输入流不置为null,因为子线程中要用,防止空指针异常
} catch (IOException e) {
e.printStackTrace();
}
}
if (mOutputStream != null) {
try {
mOutputStream.close();
mOutputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
if (mSocket != null) {
try {
mSocket.close();
mSocket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、使用available()来监测输入流
用设置读取流超时,然后处理异常的方法,会在日志一直打印信息:
强迫症,没有办法,总想要解决它。
就想到用available()取代之:
// 读取流
byte[] data = new byte[0];
try {
while (mInputStream.available() > 0) {
byte[] buf = new byte[1024];
int len = mInputStream.read(buf);
byte[] temp = new byte[data.length + len];
System.arraycopy(data, 0, temp, 0, data.length);
System.arraycopy(buf, 0, temp, data.length, len);
data = temp;
}
} catch (IOException e) {
}
这样日志也会一直打印信息,这里没定时,所以频率更高:
想到前一篇说的,在查看前,先等待一会。拿来先试试再说:
// 读取流
byte[] data = new byte[0];
try {
Thread.sleep(100);
while (mInputStream.available() > 0) {
byte[] buf = new byte[1024];
int len = mInputStream.read(buf);
byte[] temp = new byte[data.length + len];
System.arraycopy(data, 0, temp, 0, data.length);
System.arraycopy(buf, 0, temp, data.length, len);
data = temp;
}
} catch (IOException | InterruptedException e) {
}
OK,日志很干净了。这才爽。。。(虽然没理解why)
接着上篇继续来,目录也连续着。
三、UDP
UDP发送数据,不管对方有没收到,也就不需要把两主机先连接好再通信。所以,UDP一般不用做自由通信用。下面是最简单的demo,服务器只负责接收数据,而客户端只负责发送数据。
关于UDP网络编程,主要区分TCP,注意以下几点:
- 连接网络属于耗时,必须在子线程中执行。网络的连接主要在socket的send()与receive();
- 服务器与客户端的套接字都是DatagramSocket;
- 接收时监听的端口与DatagramSocket直接绑定,此绑定的端口也可直接用于发送数据;
- 目标主机及端口信息都是封装在数据报DatagramPacket中。本机的发送端口若未绑定,则是由系统分配;
- 是数据报模式(TCP是流模式),数据发送与接收都是使用数据报。一次性发送完毕,接收也是一次性必须接收完毕,所以数据缓冲区要足够大,否则会导致数据丢失;
- 能在局域网内组播与广播。
3.1 UDP服务器
主要API:
- DatagramSocket:
- new DatagramSocket(int port) —— 创建监听端口为port的套接字
- setSoTimeout(int timeout) —— 设置接收信息的超时时间。不设置,则一直阻塞
- receive(DatagramPacket packet) —— 用数据报packet接收数据,阻塞式。未设置超时时间,一直阻塞,设置了没接收到数据会抛SocketTimeoutException
- close() —— 关闭
- DatagramPacket:
- new DatagramPacket(byte[] data, int length) —— 创建一个data为数据缓冲区,数据最大长度(≤data.length)为length的数据报。有效数据缓冲区应该足够大来装下对方发送过来的全部数据,否则超过缓冲区的数据将丢失。
- getLength() —— 获取接收到数据的有效长度
- getData() —— 获取数据报中的数据,就是上面的data
- getAddress().getHostAddress() —— 获取数据报中的主机IP地址。发送和接收获取的,都是对方IP
- getPort() —— 获取数据报中的端口。发送和接收获取的,都是对方IP
private boolean mIsServerOn;
private void turnOnUdpServer() {
final int port = 8000;
new Thread(){
@Override
public void run() {
super.run();
DatagramSocket socket = null;
try {
// 1、创建套接字
socket = new DatagramSocket(port);
// 2、创建数据报
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
// 3、一直监听端口,接收数据包
mIsServerOn = true;
while (mIsServerOn) {
socket.receive(packet);
String rece = new String(data, 0, packet.getLength(), Charset.forName("UTF-8"));
pushMsgToMain(rece); // 推送信息到主线程
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != socket) {
socket.close();
socket = null;
}
}
}
}.start();
}
3.2 UDP客户端
主要API(与服务器一样的,就不介绍了):
- DatagramSocket:
- new DatagramSocket() —— 创建套接字,端口为系统给定
- getLocalPort() —— 获取套接字绑定在本机的端口
- getLocalAddress().getHostAddress() —— 获取本机IP地址。需要connect()连接成功后才能获取到
- bind(SocketAddress addr) —— 将套接字连接到远程套接字地址(IP地址+端口号)
- connect(SocketAddress addr) —— 将套接字连接到远程套接字地址(IP地址+端口号)。连接后,在数据报中可以不指定目标主机IP地址和端口了,如果要指定,必须与connect中的一样
- isConnected() —— 用connect()连接成功后,返回true
- DatagramPacket:
- new DatagramPacket(byte[] data, int length, SocketAddress sockAddr) —— 创建数据报,并指定目标主机的套接字地址
- new DatagramPacket(byte[] data, int length, InetAddress host, int port) —— 创建数据报,并制定目标主机的网络地址与端口号
- setData() —— 设置数据报的缓冲区数据
- InetAddress:
- InetAddress.getByName(String host) —— 创建IP地址为host的网络地址对象,封装IP地址。
- SocketAddress:
- new InetSocketAddress(String host, int port) —— 创建IP地址为host,端口号位port的套接字地址对象。封装了IP地址和端口号。
private void turnOnUdpClient() {
final String hostIP = "192.168.1.145";
final int port = 8000;
new Thread(new Runnable() {
@Override
public void run() {
DatagramSocket socket = null;
try {
// 1、创建套接字
socket = new DatagramSocket(8888);
// 2、创建host的地址包装实例
SocketAddress socketAddr = new InetSocketAddress(hostIP, port);
// 3、创建数据报。包含要发送的数据、与目标主机地址
byte[] data = "Hello, I am Client".getBytes(Charset.forName("UTF-8"));
DatagramPacket packet = new DatagramPacket(data, data.length, socketAddr);
// 4、发送数据
socket.send(packet);
// 再次发送数据
packet.setData("Second information from client".getBytes(Charset.forName("UTF-8")));
socket.send(packet);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != socket) {
socket.close();
}
}
}
}).start();
}
3.3 UDP广播
广播就是发送信息给网络中内所有的计算机设备。
广播的实现方法:在发送消息时,把目标主机IP地址修改为广播地址即可。
广播地址,一般有两种:
- UDP有固定的广播地址:255.255.255.255
- 另外,使用TCP/IP协议的网络,主机标识段host ID全为1的IP地址也为广播地址。如:我的局域网网段为192.168.1.0(255.255.255.0),广播地址为:192.168.1.255。
3.4 UDP组播(多播)
组播,是让同一组的计算机设备都接收到信息。让具有相同需求功能的计算机设备,加入到同一组中,然后任一计算机发送组播信息,其他成员都能接收到。
发送和接收信息,都必须使用组播地址(224.0.0.0~239.255.255.255)。计算机要加入该组,就必须加入该多播组地址。
具有以下特点:
- 它与广播都是UDP独有的;
- 只有相同组的计算机设备才能接收到信息;
- 发送和接收的套接字都是MulticastSocket。
主要API(基本使用方法与DatagramSocket是一样的,就多了几个方法):
- MulticastSocket:
- new MulticastSocket() —— 创建多播套接字,端口是系统给定的
- new MulticastSocket(int port) —— 创建绑定端口号到port的多播套接字
- new MulticastSocket(SocketAddress localAddr) —— 创建绑定到套接字地址localAddr的多播套接字
- setTimeToLive(int ttl) —— 设置time to live为ttl,默认为1。time to live可简单理解为可到达路由器的个数(详见下面总结)
- joinGroup(InetAddress groupAddr) —— 加入到组播地址groupAddr
- leaveGroup(InetAddress groupAddr) —— 离开组播地址groupAddr
- setSoTimeout(int timeout) —— 设置接收信息的超时时间
- send(DatagramPacket pack) —— 发送数据报
- receive(DatagramPacket pack) —— 接收数据报
下面是发送和接收的demo代码。
发送:
private void sendUdpMulticast() {
final String groupIP = "224.1.1.1";
final int port = 8000;
new Thread(new Runnable() {
@Override
public void run() {
MulticastSocket mcSocket = null;
try {
// 1、创建组播套接字
mcSocket = new MulticastSocket();
// 设置TTL为1,套接字发送的范围为本地网络。默认也为1
mcSocket.setTimeToLive(1);
// 2、创建组播网络地址,并判断
InetAddress groupAddr = InetAddress.getByName(groupIP);
if (!groupAddr.isMulticastAddress()) {
pushMsgToMain(UDP_HANDLER_MESSAGE_TOAST, "IP地址不是组播地址(224.0.0.0~239.255.255.255)");
return;
}
// 3、让套接字加入到组播中
mcSocket.joinGroup(groupAddr);
// 4、创建数据报
byte[] data = ("Hi, I am Multicast of UDP".getBytes(Charset.forName("UTF-8")));
DatagramPacket pack = new DatagramPacket(data, data.length, groupAddr, port);
// 5、发送信息
mcSocket.send(pack);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != mcSocket) {
mcSocket.close();
}
}
}
}).start();
}
接收:
private boolean mIsUdpMulticastOn;
private void receiveUdpMulticast() {
final String groupIP = "224.1.1.1";
final int port = 8000;
new Thread(){
@Override
public void run() {
MulticastSocket mcSocket = null;
try {
// 1、创建多播套接字
mcSocket = new MulticastSocket(port);
// 2、创建多播组地址,并校验
InetAddress groupAddr = InetAddress.getByName(groupIP);
if (!groupAddr.isMulticastAddress()) {
pushMsgToMain(UDP_HANDLER_MESSAGE_TOAST, "IP地址不是组播地址(224.0.0.0~239.255.255.255)");
return;
}
// 3、把套接字加入到多播组中
mcSocket.joinGroup(groupAddr);
// 4、创建数据报
byte[] data = new byte[1024];
DatagramPacket pack = new DatagramPacket(data, data.length);
// 5、接收信息。循环接收信息,并把接收到的数据交给主线程处理
mIsUdpMulticastOn = true;
while (mIsUdpMulticastOn) {
mcSocket.receive(pack);
String rece = new String(data, pack.getOffset(), pack.getLength());
pushMsgToMain(UDP_HANDLER_MESSAGE_DATA, rece);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != mcSocket) {
mcSocket.close();
}
}
}
}.start();
}
3.5 UDP总结
3.5.1 UDP的数据data最大是多少
经过测试,DatagramPacket中的数据data最大是65507,超过则会在发送的时候报错:
Exception:sendto failed: EMSGSIZE (Message too long)
接收的data大小,可以超65536(2^16),但一般也没必要超过发送的最大值65507,最多65536。
发送的测试,自己设计了一个数据填充小算法。使用时,在发送的时候修改data的大小即可。代码如下:
byte[] data = new byte[65507];
byte[] temp = "abcdefghijklmnopABCDEFGHIJKLMNOP".getBytes(); // 固定为32个
for (int i = 0; i < data.length >> 5; i++) {
System.arraycopy(temp, 0, data, i<<5, temp.length);
}
System.arraycopy(temp, 0, data, data.length - data.length % temp.length, data.length % temp.length);
大小分析:
数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节。不过,一些实际应用往往会限制数据报的大小,有时会降低到8192字节。(摘自 百度百科UDP)
而报头又包括IP包头(20字节)和UDP报文头(8字节)。
所以,UDP数据的最大值 = 65535 - 20 - 8 = 65507
虽然我测试那么大数据时OK的,但不是越大越好,建议小于1472。(原因详见:UDP中一个包的大小最大能多大)
3.5.2 bind 与connect 的区别
1、bind(SocketAddress addr)
将套接字绑定到特定的地址和端口,本地的绑定。
使用示例:
DatagramSocket s = new DatagramSocket(null);
SocketAddress local = new InetSocketAddress(8888);
s.bind(local);
与此句代码等效:
DatagramSocket s = new DatagramSocket(8888);
使用说明:
- DatagramSocket如果绑定了端口,则不能再绑定,否则抛异常。如:
DatagramSocket s = new DatagramSocket(8000); s.bind(local);
- 一般情况下,去绑定地址(就算与本机地址一样)也将报错。如:
SocketAddress local = new InetSocketAddress("192.168.1.222", 8888); s.bind(local);
2、connect(SocketAddress addr)
将套接字连接到远程套接字地址(IP地址+端口号),连接对方。
使用示例:
socket = new DatagramSocket(8888);
SocketAddress local = new InetSocketAddress("192.168.1.145", 8000);
socket.connect(local);
byte[] data = "Hello, I am Client".getBytes(Charset.forName("UTF-8"));
DatagramPacket packet = new DatagramPacket(data, data.length);
socket.send(packet);
与此代码等效:
socket = new DatagramSocket(8888);
SocketAddress socketAddr = new InetSocketAddress(hostIP, port);
byte[] data = "Hello, I am Client".getBytes(Charset.forName("UTF-8"));
DatagramPacket packet = new DatagramPacket(data, data.length, socketAddr);
socket.send(packet);
3.5.3 巧记组播地址
组播地址为224.0.0.0~239.255.255.255。怎么记?
查isMulticastAddress()的源码:
public boolean isMulticastAddress() {
return ((holder().getAddress() & 0xf0000000) == 0xe0000000);
}
也就是说,只要第一段的高四位为E的IP地址,就是组播地址。
而第一段的最小值E0 = 256 - 32(后五位) = 224
最大值EF = 224 + 15(F) = 239
3.5.4 简单理解TTL
TTL(Time To Live)的作用是限制IP数据包在计算机网络中的存在的时间。TTL的最大值是255,TTL的一个推荐值是64。
虽然TTL从字面上翻译,是可以存活的时间,但实际上TTL是IP数据包在计算机网络中可以转发的最大跳数。TTL字段由IP数据包的发送者设置,在IP数据包从源到目的的整个转发路径上,每经过一个路由器,路由器都会修改这个TTL字段值,具体的做法是把该TTL的值减1,然后再将IP包转发出去。如果在IP包到达目的IP之前,TTL减少为0,路由器将会丢弃收到的TTL=0的IP包并向IP包的发送者发送 ICMP time exceeded消息。
(摘自百度百科TTL)
所以,TTL可以简单的理解为能达到路由器的个数。
剩下的是一个UDP实例与常见问题。由于实例的代码太多,还是另外写一篇吧。。。>>>