通過UDP廣播實現Android局域網Peer Discovering

本文是對個人筆記中內容的整理,部分代碼及圖片來自互聯網,由于不好找到原始出處,所以未加注明。
如有痛感,聯系刪除。

本文將介紹以下知識點:

  1. TCP與UDP的區別;
  2. 單播、多播、廣播;
  3. Java中實現UDP的重要的類;
  4. Peer Discovering方案

一、TCP vs UDP

TCP:Transmission Control Protocol(傳輸控制協議)
TCP是一種面向連接(連接導向)的、可靠的、基于字節流的運輸層(Transport layer)通信協議,由IETF的RFC 793說明(specified)。TCP建立連接之后,通信雙方都同時可以進行數據的傳輸,是全雙工的。

  • 在保證可靠性上,采用超時重傳和捎帶確認機制;
  • 在流量控制上,采用滑動窗口協議,協議中規定,對于窗口內未經確認的分組需要重傳;
  • 在擁塞控制上,采用慢啟動算法。

TCP傳輸過程示意圖:


TCP(圖片來自互聯網)

Client和Server建立連接之后,服務器處于監聽狀態,即:服務器端Socket并不定位具體的客戶端套接字,而是處于等待連接的狀態,實時監控網絡狀態,等待客戶端的連接請求。

客戶端Socket提出連接請求,要連接的目標是服務器端Socket。為此,客戶端Socket必須首先描述它要連接的服務器Socket,指出服務端Socket的地址和端口號,然后就向服務器端Socket提出連接請求。

當服務器端Socket監聽到或者說接收到客戶端Socket的連接請求時,就響應客戶端Socket的請求,建立一個新的線程,把服務器端Socket的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式通信。

而服務端Socket繼續處于監聽狀態,繼續接收其他客戶端Socket的連接請求。

TCP服務器端代碼:

try {  
    Boolean endFlag = false;  
    ServerSocket ss = new ServerSocket(12345);  
    while (!endFlag) {  
        // 等待客戶端連接  
        Socket s = ss.accept();  
        BufferedReader input = new BufferedReader(newInputStreamReader(s.getInputStream()));  
        //注意第二個參數據為true將會自動flush,否則需要需要手動操作output.flush()  
        PrintWriter output = newPrintWriter(s.getOutputStream(),true);  
        String message = input.readLine();  
        Log.d("Tcp Demo", "message from Client:"+message);  
        output.println("message received!");  
        //output.flush();  
        if("shutDown".equals(message)){  
            endFlag=true;  
        }  
        s.close();  
    }  
    ss.close();  
} catch (UnknownHostException e) {  
    e.printStackTrace();  
} catch (IOException e) {  
    e.printStackTrace();  
} 

TCP客戶端代碼:

try {  
    Socket s = new Socket("localhost", 12345);  
    // outgoing stream redirect to socket  
    OutputStream out = s.getOutputStream();  
    // 注意第二個參數據為true將會自動flush,否則需要需要手動操作out.flush()  
    PrintWriter output = new PrintWriter(out, true);  
    output.println("Hello World!");  
    BufferedReader input = new BufferedReader(newInputStreamReader(s.getInputStream()));  
    // read line(s)  
    String message = input.readLine();  
    Log.d("Tcp Demo", "message From Server:" + message);  
    s.close();  
} catch (UnknownHostException e) {  
    e.printStackTrace();  
} catch (IOException e) {  
    e.printStackTrace();  
} 

UDP:User Datagram Protocol(用戶數據包協議)
UDP是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。它是IETF RFC 768是UDP的正式規范。

  • UDP協議的主要作用是將網絡數據流量壓縮成數據報的形式。
  • 一個典型的數據報就是一個二進制數據的傳輸單位。
  • 每一個數據報的前8個字節用來包含報頭信息,剩余字節則用來包含具體的傳輸數據。
UDP

相比于TCP,UDP在通信之前并不建立連接,UDP服務端Socket監聽某個端口的流量,客戶端Socket發送報文給服務端Socket指定端口,服務端Socket處理完信息之后也并不反饋信息給客戶端Socket。
即:客戶端Socket發送報文后,不關心服務端是否收到報文;服務端Socket若收到報文,也并不告知客戶端Socket。

UDP服務器端代碼:

// UDP服務器監聽的端口  
Integer port = 12345;  
// 接收的字節大小,客戶端發送的數據不能超過這個大小  
byte[] message = new byte[1024];  
try {  
    // 建立Socket連接  
    DatagramSocket datagramSocket = new DatagramSocket(port);  
    DatagramPacket datagramPacket = new DatagramPacket(message, message.length);
    try {  
        while (true) {  
            // 準備接收數據  
            datagramSocket.receive(datagramPacket);  
            Log.d("UDP Demo", datagramPacket.getAddress()  
                    .getHostAddress().toString()  
                    + ":" + new String(datagramPacket.getData()));  
        }  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
} catch (SocketException e) {  
    e.printStackTrace();  
} 

UDP客戶端代碼:

public static void send(String message) {  
    message = (message == null ? "Hello IdeasAndroid!" : message);  
    int server_port = 12345;  
    DatagramSocket s = null;  
    try {  
        s = new DatagramSocket();  
    } catch (SocketException e) {  
        e.printStackTrace();  
    }  
    InetAddress local = null;  
    try {  
        // 換成服務器端IP  
        local = InetAddress.getByName("localhost");  
    } catch (UnknownHostException e) {  
        e.printStackTrace();  
    }  
    int msg_length = message.length();  
    byte[] messagemessageByte = message.getBytes();  
    DatagramPacket p = new DatagramPacket(messageByte, msg_length, local,  
            server_port);  
    try {  
        s.send(p);  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
} 

總結下TCP和UDP的主要區別:

TCP UDP
是否連接 面向連接 面向非連接
傳輸是否可靠 可靠 不可靠
速度
應用場景 要求準確性數據(例如金融、庫存) 不求準確,但求實時、快(語音、圖像數據)

二、單播、多播、廣播

  • 單播(unicast): 是指封包在計算機網絡的傳輸中,目的地址為單一目標的一種傳輸方式。它是現今網絡應用最為廣泛,通常所使用的網絡協議或服務大多采用單播傳輸,例如一切基于TCP的協議。
    單播(unicast)

    每次只有兩個實體相互通信,發送端和接收端都是唯一確定的。在IPv4網絡中,0.0.0.0到223.255.255.255屬于單播地址。

你對小月月喊“小月月”,那么只有小月月回過頭來答應你。

  • 組播(multicast): 也叫多播, 多點廣播或群播。 指把信息同時傳遞給一組目的地址。它使用策略是最高效的,因為消息在每條網絡鏈路上只需傳遞一次,而且只有在鏈路分叉的時候,消息才會被復制。
    組播(multicast)

    “組播”這個詞通常用來指代IP組播。IP組播是一種通過使用一個組播地址將數據在同一時間以高效的方式發往處于TCP/IP網絡上的多個接收者的協議。此外,它還常用來與RTP等音視頻協議相結合。互聯網架構師戴夫·克拉克是這樣描述IP組播的:“你把數據包從一頭放進去,網絡就會試圖將它們傳遞到想要得到它們的人那里。”組播報文的目的地址使用D類IP地址, D類地址不能出現在IP報文的源IP地址字段。在IPv4網絡中,224.0.0.0到239.255.255.255屬于多播地址。

你在大街上大喊一聲“美女”, 會有一群女性回頭看你。

  • 廣播(broadcast):是指封包在計算機網絡中傳輸時,目的地址為網絡中所有設備的一種傳輸方式。實際上,這里所說的“所有設備”也是限定在一個范圍之中,稱為“廣播域”。
    廣播(broadcast)

    并非所有的計算機網絡都支持廣播,例如X.25網絡和幀中繼都不支持廣播,而且也沒有在“整個互聯網范圍中”的廣播。IPv6亦不支持廣播,廣播相應的功能由任播(anycast)代替。通常,廣播都是限制在局域網中的,比如以太網或令牌環網絡。因為廣播在局域網中造成的影響遠比在廣域網中小得多。
    以太網和IPv4網都用全1的地址表示廣播,分別是ff:ff:ff:ff:ff:ff和255.255.255.255
    令牌環網絡使用IEEE 802.2控制域中的一個特殊值來表示廣播。

你在公司大喊一聲“放假了”, 全部同事都會響應,大叫爽死了。

  • 任播(anycast):是一種網絡尋址和路由的策略,使得資料可以根據路由拓樸來決定送到“最近”或“最好”的目的地。
    任播(anycast)

    任播是與單播、廣播和組播不同的方式。
    在單播中,在網絡位址和網絡節點之間存在一一對應的關系。
    在廣播和組播中,在網絡位址和網絡節點之間存在一對多的關系:每一個目的位址對應一群接收可以復制資訊的節點。
    在任播中,在網絡位址和網絡節點之間存在一對多的關系:每一個位址對應一群接收節點,但在任何給定時間,只有其中之一可以接收到傳送端來的資訊。在互聯網中,通常使用邊界網關協議來實現任播。

作為老板,你在公司大喊一聲“開發組的過來一個人”, 總會有一個人灰溜溜去響應, 挨批還是發錢啊?

以上內容部分出自單播,組播(多播),廣播以及任播

三、Java中實現UDP的重要的類

幾個關鍵的類:

  • DatagramSocket
  • DatagramPacket
  • NetworkInterface

1、DatagramPacket類:數據報文

如果把DatagramSocket比作創建的港口碼頭,那么DatagramPacket就是發送和接收數據的集裝箱。

  1. 接收構造函數
public DatagramPacket(byte[] buf,int length) //接收數據

比如,要接收數據長度為1024的字節,構建字節緩存區byte buf[] = new byte[1024],創建DatagramPacket只需傳入buf[]和長度,實現接收長度為length的包。

while (true) {
    byte buf[] = new byte[1024];
    // 接收數據
    DatagramPacket packet = new DatagramPacket(buf, buf.length);
    datagramSocket.receive(packet);
    String content = new String(packet.getData()).trim();
    // ……
}
  1. 發送構造函數
public DatagramPacket(byte[] buf,int length,InetAddress address,int port)

比如,要發送數據為byte[] data,構造函數需要字節數組,數組長度,接收端地址(IP)和端口(Port),構造數據報文包用來把長度為length 的包傳送到指定宿主的指定的端口號。

byte[] data = paramVarArgs[0].getBytes();
DatagramPacket dataPacket = new DatagramPacket(data,
        data.length, inetAddress, BROADCAST_PORT);
try {
    datagramSocket.send(dataPacket);
} catch (IOException e) {
    e.printStackTrace();
    return App.getInstance().getResources().getString(R.string.send_failed);
}
return App.getInstance().getResources().getString(R.string.send_success);
  1. 主要方法
  • getAddress()返回接收或發送此數據報文的機器的 IP 地址。
  • getData()返回接收的數據或發送出的數據。
  • getLength()返回發送出的或接收到的數據的長度。
  • getPort()返回接收或發送該數據報文的遠程主機端口號。

2、DatagramSocket類:數據報套接字

此類表示用來發送和接收數據報包的套接字。數據報套接字是包投遞服務的發送或接收點。

  1. 不綁定地址及端口構造函數:DatagramSocket()創建數據報套接字。
try {
    datagramSocket = new DatagramSocket();
    datagramSocket.setBroadcast(true);
} catch (Exception e) {
    e.printStackTrace();
}

用于發送報文的套接字,一般不指定特定端口及地址。

  1. 綁定端口構造函數:DatagramSocket(int port)創建數據報套接字并將其綁定到本地主機上的指定端口。

  2. 綁定地址與端口構造函數:DatagramSocket(int port, InetAddress laddr)創建數據報套接字,將其綁定到指定的本地地址。

// 保持一個套接字打開,監聽該端口上所有UDP流量(0.0.0.0表示所有未處理的流量)
datagramSocket = new DatagramSocket(BROADCAST_PORT, InetAddress.getByName("0.0.0.0"));
datagramSocket.setBroadcast(true);

關于0.0.0.0的意義,可參考:全零網絡IP地址0.0.0.0表示意義詳談

  1. 主要方法
  • receive(DatagramPacket p)從此套接字接收數據報包。
  • void send(DatagramPacket p)從此套接字發送數據報包。
  • bind(SocketAddress addr)將此 DatagramSocket 綁定到特定的地址和端口。
  • void close()關閉此數據報套接字。
  • void connect(InetAddress address, int port)將套接字連接到此套接字的遠程地址。
  • void connect(SocketAddress addr)將此套接字連接到遠程套接字地址(IP 地址 + 端口號)。
  • void disconnect()斷開套接字的連接。
  • getInetAddress()返回此套接字連接的地址。
  • InetAddress getLocalAddress()獲取套接字綁定的本地地址。

3、NetworkInterface類:網絡接口

NetworkInterface是JDK1.4中添加的一個獲取網絡接口的類,該網絡接口既可以是物理的網絡接口,也可以是虛擬的網絡接口,而一個網絡接口通常由一個 IP 地址來表示。

既然 NetworkInterface 用來表示一個網絡接口,那么如果可以獲得當前機器所有的網絡接口(包括物理的和虛擬的),然后篩選出表示局域網的那個網絡接口,那就可以得到機器在局域網內的 IP 地址。

NetworkInterface常用到的方法有兩個:

  • getNetworkInterfaces()用于獲取當前機器上所有的網絡接口;
  • getInetAddresses()用于獲取綁定到該網卡的所有的 IP 地址。

來看下這段代碼,實現的功能是遍歷所有本地網絡接口,獲取廣播地址,并向它們發送廣播報文。

// 獲取本地所有網絡接口
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
    NetworkInterface networkInterface = interfaces.nextElement();
    if (networkInterface.isLoopback() || !networkInterface.isUp()) {
        continue;
    }
    // getInterfaceAddresses()方法返回綁定到該網絡接口的所有 IP 的集合
    for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
        InetAddress broadcast = interfaceAddress.getBroadcast();
        // 不廣播回環網絡接口
        if (broadcast  == null) {
            continue;
        }
        // 發送廣播報文
        try {
            DatagramPacket sendPacket = new DatagramPacket(data,
                    data.length, broadcast, BROADCAST_PORT);
            datagramSocket.send(sendPacket);
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.d("發送請求", getClass().getName() + ">>> Request packet sent to: " +
                broadcast.getHostAddress() + "; Interface: " + networkInterface.getDisplayName());
    }
}

getInterfaceAddresses方法返回的是一個綁定到該網絡接口的所有 InterfaceAddress 的集合。InterfaceAddress 是 JDK1.6 之后添加的類,包含 IP 地址(InetAddress),以及該地址對應的廣播地址和掩碼長度。

以上內容部分出自使用 NetworkInterface 獲得本機在局域網內的 IP 地址

四、Peer Discovering方案

在局域網內通過UDP廣播實現Peer Discovering的方法非常簡單:

  • 新加入局域網的設備發送廣播消息“我來了”;
  • 其它已存在的設備回復“知道了”。

整個流程如下圖所示:

Peer Discovering方案
  1. 因此,在初始化階段,首先要啟動一個廣播接收線程,用于接收指定端口的所有廣播流量:
try {
    handler = new ReceiveMsgHandler(this);
    new ServerSocket(handler).start();
} catch (IOException e) {
    e.printStackTrace();
}

在ServerSocket的構造函數中實例化DatagramSocket,指定端口,IP設置為0.0.0.0。

public ServerSocket(Handler handler) throws IOException {
    // Keep a socket open to listen to all the UDP trafic that is destined for this port
    datagramSocket = new DatagramSocket(BROADCAST_PORT, InetAddress.getByName("0.0.0.0"));
    datagramSocket.setBroadcast(true);
    // handler
    this.handler = handler;
}

在接收線程的run()方法中,接收所有廣播消息:

while (true) {
    byte buf[] = new byte[1024];
    // 接收數據
    DatagramPacket packet = new DatagramPacket(buf, buf.length);
    datagramSocket.receive(packet);
    String content = new String(packet.getData()).trim();
    if (content.equals("DISCOVER_REQUEST") &&
            !packet.getAddress().toString().equals("/" + IPUtil.getLocalIPAddress())) {
        byte[] feedback = "DISCOVER_RESPONSE".getBytes();
        // 發送數據
        DatagramPacket sendPacket = new DatagramPacket(feedback, feedback.length,
                packet.getAddress(), BROADCAST_PORT);
        datagramSocket.send(sendPacket);
        // 發送Handler消息
        sendHandlerMessage(packet.getAddress().toString());
    } else if (content.equals("DISCOVER_RESPONSE") &&
            !packet.getAddress().toString().equals("/" + IPUtil.getLocalIPAddress())) {
        // 發送Handler消息
        sendHandlerMessage(packet.getAddress().toString());
    }
}

如上圖所示,接收線程需要接收兩種廣播消息:“我來了”(DISCOVER_REQUEST)和“知道了”(DISCOVER_RESPONSE)。接收到DISCOVER_REQUEST后,發送DISCOVER_RESPONSE。需要注意的是:

// 發送數據
DatagramPacket sendPacket = new DatagramPacket(feedback, feedback.length, packet.getAddress(), BROADCAST_PORT);

這里需要指定端口為BROADCAST_PORT,因為DISCOVER_REQUEST報文的的端口是隨機的。不然無法在BROADCAST_PORT端口接收到DISCOVER_RESPONSE報文,新加入局域網的設備就無法感知其他設備的存在。

  1. 廣播發送線程在類的構造函數中初始化DatagramSocket
private ClientSocket() {
    try {
        inetAddress = InetAddress.getByName(BROADCAST_IP);
        datagramSocket = new DatagramSocket();
        datagramSocket.setBroadcast(true);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在本demo中,發送通過AsyncTask進行實現,在background中發送消息,發送完成后通過Handler在界面Toast提示。

new AsyncTask<String, Integer, String>() {

    @Override
    protected String doInBackground(String... paramVarArgs) {
        byte[] data = paramVarArgs[0].getBytes();
        DatagramPacket dataPacket = new DatagramPacket(data,
                data.length, inetAddress, BROADCAST_PORT);
        try {
            datagramSocket.send(dataPacket);
        } catch (IOException e) {
            e.printStackTrace();
            return App.getInstance().getResources().getString(R.string.send_failed);
        }
        return App.getInstance().getResources().getString(R.string.send_success);
    }

    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        Message msg = new Message();
        msg.what = SendMsgHandler.STATUS;
        msg.obj = result;
        handler.sendMessage(msg);
    }
}.execute(content);

代碼已上傳github:yhthu / intercom,如有興趣,可移步參考代碼Demo。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • 1.這篇文章不是本人原創的,只是個人為了對這部分知識做一個整理和系統的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,110評論 6 174
  • 網絡模型 物理層 物理層表示的是比特流傳輸,通常包括串口/COM口、并行/LPT口、USB、網線接口、電話線接口;...
    秋風弄影閱讀 731評論 0 2
  • 本篇結構: ICMP IGMP 附 反思 接著上一篇TCP/IP--劃分子網和構造超網,本章接著分享IP協議的兩個...
    w1992wishes閱讀 10,996評論 0 4
  • 個人認為,Goodboy1881先生的TCP /IP 協議詳解學習博客系列博客是一部非常精彩的學習筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,083評論 0 8
  • 又有一個同事走咯,這成為最近的感慨語。對于一個世界500強,人才流動是常態,只是今年流動比較多,調回國的,換地區部...
    安小姐2020閱讀 228評論 0 1