通过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阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 13,037评论 6 174
  • 网络模型 物理层 物理层表示的是比特流传输,通常包括串口/COM口、并行/LPT口、USB、网线接口、电话线接口;...
    秋风弄影阅读 705评论 0 2
  • 本篇结构: ICMP IGMP 附 反思 接着上一篇TCP/IP--划分子网和构造超网,本章接着分享IP协议的两个...
    w1992wishes阅读 10,762评论 0 4
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,051评论 0 8
  • 又有一个同事走咯,这成为最近的感慨语。对于一个世界500强,人才流动是常态,只是今年流动比较多,调回国的,换地区部...
    安小姐2020阅读 211评论 0 1