Форум программистов, компьютерный форум, киберфорум
Наши страницы
LostCoast
Войти
Регистрация
Восстановить пароль
Рейтинг: 3.00. Голосов: 2.

Обучающая статья по использованию асинхронных сокетов. Передача данных.

Запись от LostCoast размещена 29.11.2012 в 14:42
Обновил(-а) LostCoast 17.12.2012 в 09:54
Метки c-sharp

Содержание:
  1. Базовые функции
    1. Введение
    2. Передача данных
    3. Регистрация
    4. Авторизация
    5. Оптимизация
  2. Возможности для клиентов
    1. Изменение архитектуры
    2. Передача сообщений между клиентами
    3. Статус клиентов
    4. Список пользователей
    5. Передача файлов
Передача данных.

В предыдущей главе мы научились устанавливать соединение между сервером и клиентом. Пора двигаться дальше. В данной статье разберем, как передавать различные данные.

Часть 1. Подготовка.

Прежде чем приступить к написанию кода, я расскажу, как все будет устроено. Обычно сервер принимает пассивную роль по отношению к клиенту. Все, что поступает на сервер, обрабатывается и в зависимости от принятого, делаются те или иные действия. Для удобства написания кода, вынесем всю логику работы с клиентом в отдельный класс. Добавьте в проект ConsoleServer новый класс и назовите его ServerHandler. Сразу добавьте в него переменную типа Socket с именем client_socket и добавьте конструктор, который принимает один аргумент типа Socket. Как все будет готово в конструкторе, присвойте переменной client_socket переданный аргумент конструктора. Код:
C#
1
2
3
4
5
6
7
8
public class ServerHandler
{
    private Socket client_socket;
        public ServerHandler(Socket client_socket)
    {
        this.client_socket = client_socket;
    }
}
Добавим еще один класс, над классом ServerHandler, и назовем его StateObject. Данный класс рекомендует использовать MSDN, и действительно, это очень хороший способ. В данном классе будет храниться информация о принятых данных и принявшем сокете. Выглядит он следующим образом:
C#
1
2
3
4
5
6
7
class StateObject
   {
        public Socket workSocket = null;               //сокет клиента
        public const int bufferSize = 1024;            //максимальные размер буфера
        public byte[] buffer = new byte[bufferSize];   //буфер 
        public StringBuilder sb = new StringBuilder(); //полученные данные в виде строки
   }
Часть 2. Прием данных.

Теперь у нас есть все, что нужно для принятия данных. Вернемся к классу ServerHandler и добавим в нем метод Receive(), который будет инициализировать новый экземпляр класса StateObject и вызывать асинхронную операцию приема данных BeginReceive(). BeginReceive() принимает следующие аргументы: буфер (он же массив байт), смещение, размер буфера, флаг сокета(используется для различных режимов приема данных, мы не будем использовать какой-либо из них, так как нет в этом необходимости), метод для завершения операции и объект для хранения данных. Код метода:
C#
1
2
3
4
5
6
private void Receive()
        {
                StateObject state = new StateObject();
                state.workSocket = this.client_socket;
                state.workSocket.BeginReceive(state.buffer, 0, StateObject.bufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallBack), state);
        }
До этого мы в качестве последнего аргумента указывали сокет, но теперь нам надо получить принятые данные, а не только сокет, поэтому здесь и используется класс StateObject.
Опишем метод завершения операции приема. В нем будет происходить извлечение класса StateObject, проверка на количество принятых байт, конвертирование байт в строку, вывод принятых данных и повторный вызов метода Receive().
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void ReceiveCallBack(IAsyncResult ar)
        {
            try
            {
                string cmd = String.Empty;
                StateObject state = ar.AsyncState as StateObject;
                Socket handler = state.workSocket;
                if (handler.Connected)
                {
                    //количество принятых байт
                    int bytes = handler.EndReceive(ar);
                    if (bytes > 0)
                    {
                        //сохраняем принятые данных в классе
                        state.sb.Append(Encoding.UTF8.GetString(state.buffer, 0, bytes));
                        cmd = Encoding.UTF8.GetString(state.buffer, 0, bytes);
                        if (!String.IsNullOrEmpty(cmd))
                        {
                            
                            Console.WriteLine("Receive: " + cmd);
                        }
                        else
                            //если приняли пустую строку, то принимаем дальше
                            handler.BeginReceive(state.buffer, 0, StateObject.bufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallBack), state);
                    }
                }
            }
            catch (Exception) { Console.WriteLine("Error"); }
        }
Разберем код по порядку. Первое что происходит, это создается пустая строка, для хранения принятых данных. Далее инициализируется новый экземпляр класса StateObject из переменной ar. Теперь нам надо создать сокет, который в дальнейшем завершит операцию. Когда мы вызываем метод EndReceive(ar) в качестве возвращаемого аргумента, мы получаем количество принятых байт, поэтому мы сохраняем это количество в переменную bytes. Далее надо проверить, сколько байт мы получили, если их количество равно нулю, то вызываем повторно метод BeginReceive(), передаем те же переменные, что в методе Receive(). Если все нормально, то есть количество принятых байт больше нуля, то сохраняем эти байты, перекодировав байты в строку. Перекодировкой занимается метод GetString(). Но прежде чем к нему перейти, надо выбирать вид кодировки. Я использовал UTF-8, так как нам пригодятся русские буквы. Чтобы получить строку методу нужно передать буфер, смещение и длину буфера. Все эти данные достаются из класса StateObject, за исключение последнего. Количество принятых данных мы получаем после завершения операции, поэтому передаем то количество, которое сохранили в переменную bytes.
Часть 3. Отправка данных.

Отправку данных реализуем на стороне клиента, а после добавим это для сервера и добавим прием данных на стороне клиента.
Для того чтобы отправить какие-либо данные серверу, их нужно перекодировать в массив байт. В нашем случае, мы будем слать текстовые сообщения. Перекодировать байт в строку мы умеем, теперь надо сделать обратное.
Перейдем в проект ConsoleClient.Добавим в класс Client метод с именем Send(), который будет принимать в качестве параметра строку. В теле метода будет происходить перекодирование строки в массив байт и вызов асинхронной операции передачи данных. Код:
C#
1
2
3
4
5
6
public void Send(string msg)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(msg);
            this.client_socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(SendCallBack), this.client_socket);
 
        }
Чтобы получить массив байт из строки, достаточно вызвать метод GetBytes(). Метод принимает строку в качестве аргумента и возвращает массив байт. При кодировании строки в байты, надо выбрать вид кодировки, в данном случае также выбран UTF-8.
Осталось описать метод завершения операции. Он будет выглядеть следующим образом:
C#
1
2
3
4
5
private void SendCallBack(IAsyncResult ar)
        {
            Socket handler = ar.AsyncState as Socket;
            handler.EndSend(ar);
        }
Как в любом методе завершающем асинхронную операцию, вытаскиваем сокет из переменной ar и вызываем EndSend(ar) для завершения операции.

Часть 4. Отправка сообщения.

Вот мы и дошли до финальной части. Внесем некоторые изменения. Начнем с проекта ConsoleServer.
Добавим новый метод в класс ServerHandler, назовем его Start. Этот метод будет вызывать метод Receive. Код:
C#
1
2
3
4
public void Start()
        {
            Receive();
        }
Теперь внесем изменения в классе Server, а именно в методе AcceptCallBack. Добавим инициализацию класса ServerHandler, передав ему сокет клиента, и вызовем метод Start класса ServerHandler. Код:
C#
1
2
3
4
5
6
7
8
9
10
private void AcceptCallBack(IAsyncResult ar)
        {
            Socket socket = (Socket)ar.AsyncState;
            Socket accept_socket = socket.EndAccept(ar);
            //класс по работе с клиентом
            ServerHandler handler = new ServerHandler(accept_socket);
            handler.Start();
            this.acceptEvent.Set();
            Console.WriteLine("A new connection. IP:port = " + accept_socket.RemoteEndPoint.ToString());
        }
В проекте ConsoleServer все. Теперь внесем изменения в проекте ConsoleClient. Перейдем в метод Main. В нем надо добавить вызов метода Send. Можно написать следующий код:
C#
1
2
3
4
5
6
7
Client client = new Client(GetIp(), 5555);
client.Connect();
Console.Write(“Введите сообщение:);
string msg = Console.ReadLine();
client.Send(msg);
client.Disconnect();
Console.Read();
Все готово. Можно запустить сервер и клиента. После подключения клиента к серверу введите любое сообщение, и оно отобразится на стороне сервера.

Часть 5. Эхо сервер.

Давайте немного расширим функционал нашего сервера и научим его отвечать принятым сообщением. Перейдем к проекту ConsoleServer, выберем класс ServerHandler. В классе создадим метод Send, точно такой же как у клиента. Код:
C#
1
2
3
4
5
6
private void Send(string msg)
        {
                Socket socket = this.client_socket;
                byte[] buffer = Encoding.UTF8.GetBytes(msg);
                socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(SendCallBack), socket);
        }
И создадим метод SendCallBack. Код:
C#
1
2
3
4
5
6
7
private void SendCallBack(IAsyncResult ar)
        {
                Socket handler = ar.AsyncState as Socket;
                handler.EndSend(ar);
                //продолжить прием данных
                Receive();
        }
В методе SendCallBack, после завершения операции отправки, добавим вызов метода Receive, чтобы сервер продолжал принимать данные.
Осталось добавить вызов метода Send. Добавим в методe ReceiveCallBack после строки Console.WriteLine(“Receive “ + cmd) следующую строку:

C#
1
Send(cmd);
На стороне сервера все готово. Как только сервер получит какие-либо данные, в нашем случае это сообщения, то он тут же отправит их обратно отправителю.

Часть 6. Прием данных на стороне клиента.

Чтобы все это проверить, надо научить клиента принимать данные от сервера. Для этого надо будет добавить аналогичный метод Receive и ReceiveCallBack, как на сервере.
Приступим. Перейдем к проекту ConsoleClient и откроем класс Client. Первое, с чего начнем, это скопируем класс StateObject в проект ConsoleClient, поместите его над классом Client. Второе, создадим метод Receive. Код:

C#
1
2
3
4
5
6
public void Receive()
        {
                StateObject state = new StateObject();
                state.workSocket = this.client_socket;
                state.workSocket.BeginReceive(state.buffer, 0, StateObject.bufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallBack), state);
        }
Как видите, метод Receive практически не отличается, как метод ReceiveCallBack. Опишем его:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void ReceiveCallBack(IAsyncResult ar)
        {
            try
            {
                string cmd = String.Empty;
                StateObject state = ar.AsyncState as StateObject;
                Socket handler = state.workSocket;
                if (handler.Connected)
                {
                    int bytes = handler.EndReceive(ar);
                    if (bytes > 0)
                    {
                        cmd = Encoding.UTF8.GetString(state.buffer, 0, bytes);
                        if (!String.IsNullOrEmpty(cmd))
                        {
                            //....обработка принятых данных
                            Console.Write(cmd);
                        }
                        else
                            handler.BeginReceive(state.buffer, 0, StateObject.bufferSize, SocketFlags.None, new AsyncCallback(ReceiveCallBack), state);
                    }
                }
            }
            catch (Exception) { }
        }
Осталось добавить вызов метода Receive после отправки сообщения. Поместим его в методе SendCallBack, после строки handler.EndSend(ar). Код:

C#
1
2
3
4
5
6
private void SendCallBack(IAsyncResult ar)
        {
            Socket handler = ar.AsyncState as Socket;
            handler.EndSend(ar);
            Receive();
        }
Вот и все. Теперь каждый раз при отправке сообщения, будет приходить ответ от сервера. Для эксперимента, можете добавить цикл в методе Main клиента и перенести туда строки отправки сообщений. Это позволит убедиться, что все работает как надо.
Но если у вас не удалось ничего увидеть, возможно, из-за того, что после отправки в нашем коде идет отключение, поэтому клиент не успевает принять сообщение. Как вариант решения этой проблема, можно просто добавить приостановление потока. Это делается методом System.Threading.Thread.Sleep(1000). Значение 1000 соответствует одной секунде, так как метод воспринимает аргумент в виде миллисекунд. Осталось добавить этот метод в функцию Main у клиента после метода Send.

Важно подчеркнуть, что надо соблюдать вызовы Receive и Send на обеих сторонах. Если вам понадобится отправить два подряд сообщения от клиента, и при этом не получать ответа и записывать данные, например передача файла, то сервер надо будет перестроить под вызовы Send методов. Грубо говоря, вы описываете свой протокол общения между клиентом и сервером. На данный момент мы описали простое взаимодействия клиента и сервера.
В следующей главе мы приступим к более интересным вещам. И первой из них будет создание регистрации пользователей на стороне сервера.
Размещено в Без категории
Просмотров 14668 Комментарии 4
Всего комментариев 4
Комментарии
  1. Старый комментарий
    Аватар для programina

    Когда напишут клиент-сервер под UNIX и на языке Си?
    Запись от programina размещена 30.11.2012 в 01:38 programina вне форума
    Обновил(-а) programina 30.11.2012 в 01:52
  2. Старый комментарий
    Аватар для basili4
    >Когда напишут клиент-сервер под UNIX и на языке Си?
    Надо для практических задач или просто посмотреть код для само образования ?
    Запись от basili4 размещена 30.11.2012 в 10:37 basili4 вне форума
  3. Старый комментарий
    >Когда напишут клиент-сервер под UNIX и на языке Си?
    Не вижу никаких проблем писать на Си, в инете же куча вариантов, например вот: http://www.ti.chernigov.ua/labs/comm/docs/sockets.html
    Запись от LostCoast размещена 30.11.2012 в 11:01 LostCoast вне форума
  4. Старый комментарий
    Или вам интересно увидеть, то что в моей статье, но только под unix и на Си ?
    Запись от LostCoast размещена 30.11.2012 в 11:02 LostCoast вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru