EqLlyHJ5w6,694hQRgLWm

Android网络编程TCP、UDP

来源:济南磐龙笔记本交换机维修作者:济南磐龙防火墙维修网址:http://www.pldtwx.com

Android网络编程TCPUDP



先对上一遍的工具类,补充两点:
1Client关闭异常
如果没有连接host就调用close()的话,会导致NullPointException,因为mInputStreamnull。虽然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();

       }

   }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

修改为:

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();

       }

   }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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) {

}

1

2

3

4

5

6

7

8

9

10

11

12

13

这样日志也会一直打印信息,这里没定时,所以频率更高:
想到前一篇说的,在查看前,先等待一会。拿来先试试再说:

// 读取流

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) {

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

OK,日志很干净了。这才爽。。。(虽然没理解why

接着上篇继续来,目录也连续着。

三、UDP

UDP发送数据,不管对方有没收到,也就不需要把两主机先连接好再通信。所以,UDP一般不用做自由通信用。下面是最简单的demo,服务器只负责接收数据,而客户端只负责发送数据。

关于UDP网络编程,主要区分TCP,注意以下几点:

连接网络属于耗时,必须在子线程中执行。网络的连接主要在socketsend()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();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

3.3 UDP广播

广播就是发送信息给网络中内所有的计算机设备。
广播的实现方法:在发送消息时,把目标主机IP地址修改为广播地址即可。

广播地址,一般有两种:

UDP有固定的广播地址:255.255.255.255

另外,使用TCP/IP协议的网络,主机标识段host ID全为1IP地址也为广播地址。如:我的局域网网段为192.168.1.0255.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 livettl,默认为1time 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();

               // 设置TTL1,套接字发送的范围为本地网络。默认也为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();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

接收:

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();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

3.5 UDP总结

3.5.1 UDP的数据data最大是多少

经过测试,DatagramPacket中的数据data最大是65507,超过则会在发送的时候报错:
Exception:sendto failed: EMSGSIZE (Message too long)

接收的data大小,可以超655362^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);

1

2

3

4

5

6

大小分析:

数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节。不过,一些实际应用往往会限制数据报的大小,有时会降低到8192字节。(摘自
百度百科UDP

而报头又包括IP包头(20字节)和UDP报文头(8字节)。

所以,UDP数据的最大值 = 65535 - 20 - 8 = 65507

虽然我测试那么大数据时OK的,但不是越大越好,建议小于1472。(原因详见:UDP中一个包的大小最大能多大

3.5.2 bind connect 的区别

1bind(SocketAddress addr)
将套接字绑定到特定的地址和端口,本地的绑定。
使用示例:

DatagramSocket s = new DatagramSocket(null);

SocketAddress local = new InetSocketAddress(8888);

s.bind(local);

1

2

3

与此句代码等效:
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);

2connect(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);

1

2

3

4

5

6

与此代码等效:

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);

1

2

3

4

5

3.5.3 巧记组播地址

组播地址为224.0.0.0~239.255.255.255。怎么记?
isMulticastAddress()的源码:

public boolean isMulticastAddress() {

   return ((holder().getAddress() & 0xf0000000) == 0xe0000000);

}

1

2

3

也就是说,只要第一段的高四位为EIP地址,就是组播地址。
而第一段的最小值E0 = 256 - 32(后五位) = 224
最大值EF = 224 + 15F = 239

3.5.4 简单理解TTL

TTLTime To Live)的作用是限制IP数据包在计算机网络中的存在的时间。TTL的最大值是255TTL的一个推荐值是64

虽然TTL从字面上翻译,是可以存活的时间,但实际上TTLIP数据包在计算机网络中可以转发的最大跳数。TTL字段由IP数据包的发送者设置,在IP数据包从源到目的的整个转发路径上,每经过一个路由器,路由器都会修改这个TTL字段值,具体的做法是把该TTL的值减1,然后再将IP包转发出去。如果在IP包到达目的IP之前,TTL减少为0,路由器将会丢弃收到的TTL=0IP包并向IP包的发送者发送 ICMP time exceeded消息。


济南磐龙笔记本交换机维修,专业芯片级维修服务商  www.pldtwx.com


EqLlyHJ5w6