среда, 6 ноября 2013 г.

UDP в Android: как приложения ищут друг друга в сети?

В большинстве случаев говря о передаче данных по сети мы имеем в виду TCP. Для большинства сетевых задач этот протокол лучший, но он вообще-то совсем не единственный. И есть вещи, которые с его помощью делать не удобно. Например: мы знаем порт, но не знаем IP-адреса получателя и нам нужно его найти. Опрашивать все адреса локальной сети по очереди? Жуть... Тут надо бы послать широковещательное сообщение, а для этого мы уже используем UDP. Этот протокол достаточно интересен. Например, мы можем слать сообщения и слушать на одном и том же проту одновременно. UDP при отправке широковещательных сообщений не создаёт соединения в привычном нам смысле. Мы не знаем, доставлено ли получателю наше UDP сообщение. Впрочем нам это и не нужно. Передавать данные мы будем уже по TCP. Итак как же экземплярам нашего приложения искать друг друга?



Кричим и слушаем одновременно

import android.content.Context;
import android.net.DhcpInfo;
import android.net.wifi.WifiManager;
 
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
 
public class UDPHelper extends Thread {
 
    private BroadcastListener listener;
    private Context ctx;
    private DatagramSocket socket;
    private static final int PORT = 5050;
 
    public UDPHelper(Context ctx, BroadcastListener listener) throws IOException {
        this.listener = listener;
        this.ctx = ctx;
    }
 
    public void send(String msg) throws IOException {
        DatagramSocket clientSocket = new DatagramSocket();
        clientSocket.setBroadcast(true);
        byte[] sendData = msg.getBytes();
        DatagramPacket sendPacket = new DatagramPacket(
                sendData, sendData.length, getBroadcastAddress(), PORT);
        clientSocket.send(sendPacket);
    }
 
    @Override
    public void run() {
        try {
            socket = new DatagramSocket(PORT);
        } catch (SocketException e) {
            e.printStackTrace();
        }
        while (!socket.isClosed()) {
            try {
                byte[] buf = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buf, buf.length);
                socket.receive(packet);
                listener.onReceive(
                        new String(packet.getData(), 0, packet.getLength()), 
                        packet.getAddress().getHostAddress());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        socket.close();
    }
 
    public void end() {
        socket.close();
    }
 
    public interface BroadcastListener {
        public void onReceive(String msg, String ip);
    }
 
    InetAddress getBroadcastAddress() throws IOException {
        WifiManager wifi = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE);
        DhcpInfo dhcp = wifi.getDhcpInfo();
        if(dhcp == null)
            return InetAddress.getByName("255.255.255.255");
        int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask;
        byte[] quads = new byte[4];
        for (int k = 0; k < 4; k++)
            quads[k] = (byte) ((broadcast >> k * 8) & 0xFF);
        return InetAddress.getByAddress(quads);
    }
}

Этот класс - обычный поток, который открывает DatagramSocket и слушает его методом socket.receive(). При получении сообщения мы достаём текст и IP-адрес отправителя и передаём это методу onReceive() экземпляра класса BroadcastListener, который получили в конструкторе. Чтобы отправлять широковещательные сообщения используем метод send(). Заметьте, что сокет для отправки broadcast-ов мы открыли на том же порту, что и сокет для приёма сообщений. Никаких проблем с этим не возникает.

Как это использовать?

Очень просто:

    List<String> ips = Collections.synchronizedList(new LinkedList<String>());
 
    private class Pinger extends Thread {
        private boolean running;
 
        @Override
        public void run() {
            try {
                udp = new UDPHelper(getApplicationContext(), new UDPHelper.BroadcastListener() {
                    @Override
                    public void onReceive(String msg, String ip) {
                        Log.v(TAG, "receive message "+msg+" from "+ip);
                        if (!ips.contains(ip)) ips.add(ip);
                    }
                });
                udp.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
            running = true;
            while (running) {
                try {
                    udp.send("!PING!");
                    Log.v(TAG, "ping sended ....");
                    sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
 
        public void end() {
            running = false;
            udp.end();
        }
    }

Делаем наш Pinger вложенным классом какого-нибудь Service, стартуем как обычный Thread и через очень короткое время имеем в переменной ips список IP-адресов всех кто шлёт сейчас запросы в нашей сети. Себя тоже мы там найдём, так что не забываем фильтровать свой IP-адрес. Скорость такого "поиска родственных душ" зависит только от задержки между пингами, которую (если не жалко трафика) можно сделать минимальной. Чтобы сохранять список IP-адресов актуальным, сохраняем время последнего пинга от данного клиента и вычищаем слишком "старые" адреса.

4 комментария: