Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android網絡編程TCP、UDP(二)

Android網絡編程TCP、UDP(二)

編輯:關於Android編程

先對上一遍的工具類,補充兩點:
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) {
}

這樣日志也會一直打印信息,這裡沒定時,所以頻率更高:
這裡寫圖片描述
想到前一篇說的,在查看前,先等待一會。拿來先試試再說:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> // 讀取流 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。

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可以簡單的理解為能達到路由器的個數。


剩下的是一個UDP實例與常見問題。由於實例的代碼太多,還是另外寫一篇吧。。。>>>

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved