Android网络编程TCP、UDP(二)

先对上一遍的工具类,补充两点:
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,注意以下几点:

  1. 连接网络属于耗时,必须在子线程中执行。网络的连接主要在socket的send()与receive();
  2. 服务器与客户端的套接字都是DatagramSocket;
  3. 接收时监听的端口与DatagramSocket直接绑定,此绑定的端口也可直接用于发送数据;
  4. 目标主机及端口信息都是封装在数据报DatagramPacket中。本机的发送端口若未绑定,则是由系统分配;
  5. 是数据报模式(TCP是流模式),数据发送与接收都是使用数据报。一次性发送完毕,接收也是一次性必须接收完毕,所以数据缓冲区要足够大,否则会导致数据丢失;
  6. 能在局域网内组播与广播。

3.1 UDP服务器

主要API:

  1. DatagramSocket:
    • new DatagramSocket(int port) —— 创建监听端口为port的套接字
    • setSoTimeout(int timeout) —— 设置接收信息的超时时间。不设置,则一直阻塞
    • receive(DatagramPacket packet) —— 用数据报packet接收数据,阻塞式。未设置超时时间,一直阻塞,设置了没接收到数据会抛SocketTimeoutException
    • close() —— 关闭
  2. 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(与服务器一样的,就不介绍了):

  1. DatagramSocket:
    • new DatagramSocket() —— 创建套接字,端口为系统给定
    • getLocalPort() —— 获取套接字绑定在本机的端口
    • getLocalAddress().getHostAddress() —— 获取本机IP地址。需要connect()连接成功后才能获取到
    • bind(SocketAddress addr) —— 将套接字连接到远程套接字地址(IP地址+端口号)
    • connect(SocketAddress addr) —— 将套接字连接到远程套接字地址(IP地址+端口号)。连接后,在数据报中可以不指定目标主机IP地址和端口了,如果要指定,必须与connect中的一样
    • isConnected() —— 用connect()连接成功后,返回true
  2. DatagramPacket:
    • new DatagramPacket(byte[] data, int length, SocketAddress sockAddr) —— 创建数据报,并指定目标主机的套接字地址
    • new DatagramPacket(byte[] data, int length, InetAddress host, int port) —— 创建数据报,并制定目标主机的网络地址与端口号
    • setData() —— 设置数据报的缓冲区数据
  3. InetAddress:
    • InetAddress.getByName(String host) —— 创建IP地址为host的网络地址对象,封装IP地址。
  4. 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地址修改为广播地址即可。

扫描二维码关注公众号,回复: 2532341 查看本文章

广播地址,一般有两种:

  1. UDP有固定的广播地址:255.255.255.255
  2. 另外,使用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)。计算机要加入该组,就必须加入该多播组地址。

具有以下特点:

  1. 它与广播都是UDP独有的;
  2. 只有相同组的计算机设备才能接收到信息;
  3. 发送和接收的套接字都是MulticastSocket。

主要API(基本使用方法与DatagramSocket是一样的,就多了几个方法):

  1. 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(); // 固定为32for (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实例与常见问题。由于实例的代码太多,还是另外写一篇吧。。。>>>

猜你喜欢

转载自blog.csdn.net/a10615/article/details/52395592