Wednesday, June 29, 2011

AirFileExchange. Реализиация.

Приветствую.

Ранее я показывал ролик того, что хочу сделать. Как оказалось уже существует AirShare trademark, поэтому теперь обзывается эта пакость AirFileExchange.

Более детально о том, о сем читаем далее.

Рассуждения
На самом деле Air тут для красоты, все будет (должно) работать в локальной сети на ура. К примеру, у меня дома есть один роутер, к нему подключен большой ПК через Ethernet + 2 ноутбука через WiFi. Все друг друга видят и передают/принимают файлы.

Основная цель приложения - быстрая, легкая и интуитивная передачи и прием файлов в локальной сети. Что может быть проще, чем просто набор изображений профилей пользователей, имен пользователей и компьютеров? Пусть будет так, считаю что достаточная информация чтобы понять кому шлем файлы.

Как будем искать доступных пользователей? Как будем передавать файлы? А что остается? Будем использовать UDP - для процесса обнаружения и TCP - для обмена файлами, и для облегчения жизни, все файлы будем кидать на рабочий стол в папку с названием приложения. А сообщения будут простыми XML пакетами.

Больше ничего и не требуется, вот такая вот маленькая программулина.

+ Новое видео работы данной утилиты (все как обычно, по делитански):



Open Source
Решил сделать свой первый Open Source проект, разместился я на github'е. Коды смотреть тут, а дальше будем продолжать рассуждать о логике приложения.

Обнаружение
Логику построил следующим образом:
  1. Broadcast рассылка по UDP с сообщением о присутствии со статусом 'ask', что значит, просто запрос - если кто-то есть, ответьте
  2. Прослушивание порта (3000) по протоколу UDP в ожидании прихода сообщения о присутствии
  3. При получении сообщения о присутствии, чтение статуса и выполнение соответствующих действий
private void DiscoveringUsers()
{
    Random random = new Random();

    while (this.discoveringThread.IsAlive)
    {
        try
        {
            FilterTimeoutUsers();
            try
            {
                Udp.SendBroadcast(Helper.XmlSerialize(new RequestPresence()
                {
                    Status = "ask",
                    UserAddress = new UserAddress()
                    {
                        Address = Helper.LocalIPAddress(),
                        Port = Udp.DefaultPort
                    },
                    UserInfo = null
                }));

                Log.WriteLn("Send: ask - broadcast");
            }
            catch
            {
            }
            Thread.Sleep(random.Next(8000, 14000));
        }
        catch
        {
        }
    }
}
Статусы сообщения о присутствии
Для реализации общения и обнаружения, потребуется 3 статуса:
  1. 'presence' - сообщение о присутствии содержит полную информацию о пользователе (имя пользователя, имя компьютера, изображение профайла)
  2. 'ask' - запрос на ответ о присутствии со статусом 'presence'
  3. 'left' - сообщение о том, что указанный пользователь в сообщении покинул сеть (выключил сервер, закрыл приложение)
Есть некоторые моменты, например, если по какой то причине (мы ведь UDP используем), мы получили 'ask' от пользователя, который еще не присылал нам 'presence', т.е. не добавлен в наш список, то требуется ему лично отправить 'ask', чтобы гарантировано получить личный 'presence' и добавить в список. Основной проблемой является то, что начальный (циклический) опрос 'ask' broadcast UDP может не доходить до всех, поэтому требуется создать вот такую подстраховку.
При выходе, выключении сервера, требуется разослать всем 'left', я отказался от broadcast, а просто в цикле пробегаемся по уже добавленным пользователям и каждому, лично, рассылаем 'left'.
private void IdentificationMe()
{
    using (UdpClient udpClient = new UdpClient())
    {
        udpClient.EnableBroadcast = true;

        IPEndPoint udpBroadcastPoint = new IPEndPoint(IPAddress.Any, Udp.DefaultPort);
        udpClient.Client.Bind(udpBroadcastPoint);

        while (this.identificationThread.IsAlive)
        {
            try
            {
                TimeSpan timeToWait = TimeSpan.FromMilliseconds(50);
                IAsyncResult result = udpClient.BeginReceive(null, null);
                result.AsyncWaitHandle.WaitOne(timeToWait);
                try
                {
                    IPEndPoint remotePoint = null;
                    byte[] buffer = udpClient.EndReceive(result, ref remotePoint);

                    try
                    {
                        RequestPresence requestPresence = Helper.XmlDeserialize(Encoding.UTF8.GetString(buffer));

                        Log.WriteLn("Recv: {0} - {1}", requestPresence.Status, remotePoint.Address.ToString());

                        if ("presence".Equals(requestPresence.Status))
                        {
                            IPEndPointHolder ipEndPointHolder = listOfAvailablePCs.Find(
                                item => item.IpEndPoint.Address.Equals(IPAddress.Parse(requestPresence.UserAddress.Address)));
                            if (ipEndPointHolder == null)
                            {
                                ipEndPointHolder = new IPEndPointHolder();
                                ipEndPointHolder.IpEndPoint = new IPEndPoint(IPAddress.Parse(requestPresence.UserAddress.Address),
                                    requestPresence.UserAddress.Port);
                                ipEndPointHolder.IsAvailable = true;

                                listOfAvailablePCs.Add(ipEndPointHolder);
                                if (UserPresenceReceivedRequest != null)
                                {
                                    UserPresenceReceivedRequest(requestPresence, ipEndPointHolder);
                                }
                            }
                            else
                            {
                                ipEndPointHolder.IsAvailable = true;
                            }
                        }

                        if ("ask".Equals(requestPresence.Status))
                        {
                            UserInfo userInfo = null;
                            bool isVisible = true;

                            if (UserPresenceReceivedAsk != null)
                            {
                                UserPresenceReceivedAsk(out isVisible, ref userInfo);
                            }

                            if (isVisible)
                            {
                                Udp.Send(Helper.XmlSerialize(new RequestPresence()
                                {
                                    Status = "presence",
                                    UserAddress = new UserAddress()
                                    {
                                        Address = Helper.LocalIPAddress(),
                                        Port = Udp.DefaultPort
                                    },
                                    UserInfo = userInfo
                                }), new IPEndPoint(IPAddress.Parse(requestPresence.UserAddress.Address), 
                                    requestPresence.UserAddress.Port));

                                Log.WriteLn("Send: presence - {0}", requestPresence.UserAddress.Address.ToString());

                                IPEndPointHolder ipEndPointHolder = listOfAvailablePCs.Find(
                                    item => item.IpEndPoint.Address.Equals(IPAddress.Parse(requestPresence.UserAddress.Address)));
                                if (ipEndPointHolder == null)
                                {
                                    Udp.Send(Helper.XmlSerialize(new RequestPresence()
                                    {
                                        Status = "ask",
                                        UserAddress = new UserAddress()
                                        {
                                            Address = Helper.LocalIPAddress(),
                                            Port = Udp.DefaultPort
                                        },
                                        UserInfo = null
                                    }), new IPEndPoint(IPAddress.Parse(requestPresence.UserAddress.Address),
                                        requestPresence.UserAddress.Port));

                                    Log.WriteLn("Send: ask - {0}", requestPresence.UserAddress.Address.ToString());
                                }
                                else
                                {
                                    ipEndPointHolder.IsAvailable = true;
                                }
                            }
                        }

                        if ("left".Equals(requestPresence.Status))
                        {
                            IPEndPointHolder ipEndPointHolder = listOfAvailablePCs.Find(
                                item => item.IpEndPoint.Address.Equals(remotePoint.Address));
                            if (ipEndPointHolder != null)
                            {
                                ipEndPointHolder.IsAvailable = false;

                                if (UserPresenceGotTimeout != null)
                                {
                                    UserPresenceGotTimeout(ipEndPointHolder);
                                }

                                listOfAvailablePCs.Remove(ipEndPointHolder);
                            }
                        }
                    }
                    catch
                    {
                    }
                }
                catch
                {
                }
            }
            catch (ThreadAbortException)
            {
                UserInfo userInfo = null;
                bool isVisible = false;

                if (UserPresenceReceivedAsk != null)
                {
                    UserPresenceReceivedAsk(out isVisible, ref userInfo);
                }

                foreach (IPEndPointHolder ipEndPointHolder in listOfAvailablePCs)
                {
                    Udp.Send(Helper.XmlSerialize(new RequestPresence()
                    {
                        Status = "left",
                        UserAddress = new UserAddress()
                        {
                            Address = Helper.LocalIPAddress(),
                            Port = Udp.DefaultPort
                        },
                        UserInfo = null
                    }), ipEndPointHolder.IpEndPoint);

                    Log.WriteLn("Send: left - {0}", ipEndPointHolder.IpEndPoint.Address.ToString());
                }
            }
        }
    }
}
Передача файлов
Как было раньше написано, будем использовать TCP. При запуске сервера, начинаем слушать порт (3001) по протоколу TCP в ожидании подключения клиента. В теории, никто из пользователей не знает IP, к которому подключиться и начинать слать файлы. Для этого мы уже описали процесс обнаружения, который и занимается предоставлением всей необходимой информации о том, кому и от кого, и что слать.

Если посмотреть данный слой, передачу файлов, последовательно, то увидим следующее:
  1. Отправитель файлов подключается к серверу (получателю)
  2. Отправитель формирует сообщение включающее: список файлов, их размеры и др.
  3. Получатель ожидает сообщение с информацией о файлах, после запрашивает у пользователя разрешение на прием файлов
  4. После получения разрешения, сервер отправляет ответ со статусами разрешения передачи 'allow' или отмены - 'denied'
  5. После получения разрешения, отправитель начинает последовательно открыть отправляемые файлы и слать прочитанные байты в одном потоке, без каких-либо разделителей
  6. Сервер получая данные из потока делит на файлы, исходя из раннее полученного списка передаваемых файлов и их размеров в байтах
На первый взгляд тяжело, но на самом деле логика проста, а может и нет.
private void ListeningFiles()
{
    tcpListener = new TcpListener(IPAddress.Any, AirClient.DefaultPort);
    tcpListener.Start();

    while (this.listeningFilesThread.IsAlive)
    {
        try
        {
            TcpClient client = tcpListener.AcceptTcpClient();
            new Thread(new ParameterizedThreadStart(AcceptTcpClient)).Start(client);
        }
        catch
        {
        }
    }
}

private void AcceptTcpClient(object param)
{
    TcpClient client = (TcpClient)param;
    try
    {
        byte[] bytes = new byte[256];
        StringBuilder stringBuilder = new StringBuilder();
        NetworkStream networkStream = client.GetStream();

        int i;
        while ((i = networkStream.Read(bytes, 0, bytes.Length)) != 0)
        {
            stringBuilder.Append(Encoding.UTF8.GetString(bytes, 0, i));
            if (i < bytes.Length)
                break;
        }

        // Sending a list of files?
        try
        {
            SendFiles sendFiles = Helper.XmlDeserialize(stringBuilder.ToString());

            if (UserWantToSendFiles != null)
            {
                AirServer.IPEndPointHolder ipEndPointHolder = new IPEndPointHolder();
                ipEndPointHolder.IpEndPoint = new IPEndPoint(IPAddress.Parse(sendFiles.UserAddress.Address), sendFiles.UserAddress.Port);
                ipEndPointHolder.IsAvailable = true;

                UserWantToSendFiles(sendFiles, ipEndPointHolder, client);
            }
            else
            {
                client.Close();
            }
        }
        catch
        {
        }
    }
    catch
    {
        client.Close();
    }
}

public void ReceiveFilesFrom(IPEndPointHolder ipEndPoint, TcpClient client, SendFiles sendFiles, bool receive, object state, 
    UserReceiveFilesProgress progress)
{
    try
    {
        byte[] bytes = new byte[1024];
        NetworkStream networkStream = client.GetStream();
        {
            byte[] buffer = Encoding.UTF8.GetBytes(Helper.XmlSerialize(new SendFilesData()
            {
                UserAddress = new UserAddress() 
                { 
                    Address = Helper.LocalIPAddress(),
                    Port = AirClient.DefaultPort
                },
                Status = receive ? "allowed" : "denied"
            }));
            networkStream.Write(buffer, 0, buffer.Length);
        }

        if (receive)
        {
            int i = 0;
            long n = 0;
            bool cancel = false;

            long totalSize = 0, receivedTotal = 0, receivedCurrent = 0;

            foreach (SendFile file in sendFiles.Files)
            {
                totalSize += file.Size;
            }

            string folder = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + "\\Air File Exchange\\";
            Directory.CreateDirectory(folder);

            foreach (SendFile file in sendFiles.Files)
            {
                using (FileStream fileStream = new FileStream(folder + file.Name, FileMode.OpenOrCreate, FileAccess.Write))
                {
                    fileStream.SetLength(0);

                    receivedCurrent = 0;

                    if (i - n > 0)
                    {
                        fileStream.Write(bytes, (int)n, i - (int)n);

                        receivedCurrent += i - (int)n;
                        receivedTotal += i - (int)n;

                        if (progress != null)
                        {
                            progress(file, ipEndPoint, receivedCurrent, file.Size, receivedTotal, totalSize, state, out cancel);
                        }
                    }

                    while (!cancel && (i = networkStream.Read(bytes, 0, bytes.Length)) != 0)
                    {
                        n = file.Size - fileStream.Length;
                        if (i <= n)
                        {
                            n = i;
                        }
                        fileStream.Write(bytes, 0, (int)n);

                        receivedCurrent += n;
                        receivedTotal += n;

                        if (progress != null)
                        {
                            progress(file, ipEndPoint, receivedCurrent, file.Size, receivedTotal, totalSize, state, out cancel);
                        }

                        if (file.Size == fileStream.Length)
                        {
                            break;
                        }
                    }
                }
                if (cancel)
                {
                    throw new OperationCanceledException();
                }
            }
            if (totalSize > receivedTotal)
            {
                throw new OperationCanceledException();
            }
        }
    }
    finally
    {
        client.Close();
    }
}

public void ReceiveFilesFromAsync(IPEndPointHolder ipEndPoint, TcpClient client, SendFiles sendFiles, bool receive, object state, 
    UserReceiveFilesProgress progress, UserReceiveFilesComplete complete)
{
    new Thread(new ThreadStart(() =>
        {
            try
            {
                ReceiveFilesFrom(ipEndPoint, client, sendFiles, receive, state, progress);
                if (complete != null)
                {
                    complete(sendFiles, ipEndPoint, state, null);
                }
            }
            catch (Exception e)
            {
                if (complete != null)
                {
                    complete(sendFiles, ipEndPoint, state, e);
                }
            }
        })).Start();
}

No comments:

Post a Comment