oldi

IP-телефония — практические подходы к реализации схемы PC—PC

(Продолжение. Начало в № 2’ 2001 )

С.В. Захаров

Занятие 2. Передача данных в сети

   КЛИЕНТ

   СЕРВЕР

Занятие 2. Передача данных в сети

Как установить связь между приложениями?

Если у вас еще не установлен протокол TCP\IP, установите его.

 

Для решения задачи передачи данных существует три механизма:

  • вызовы удаленных процедур (RPC);
  • именованные программные каналы (pipes);
  • гнезда (socket).

Гнезда являются одной из форм межпрограммного взаимодействия (IPC). Именно на написании гнездовых программ мы и остановимся. При их создании можно использовать стандартный класс Visual C++ CAsyncSocket. Лично я организовал свой. Постараюсь передать суть. Для работы программы необходимо организовать Клиент и Сервер. Клиент передает данные, Сервер их обрабатывает и выдает Клиенту результат.

В начало В начало

КЛИЕНТ

Первой строкой мы подключаем необходимую нам библиотеку <winsock.h>. Для инициализации сети вызывается функция WSAStartup, которая в качестве параметра запрашивает версию 1.1 Socket DLL, а детали реализации передает обратно в структуру ImplementationDetails (листинг 1).

#include <winsock.h>
WORD VersionRequested;
WSADATA ImplementationDetails;
int nRlt;
 
VersionRequested=MAKEWORD(1,1);
nRlt=WSAStartup(VersionRequested,&ImplementationDetails);
if (nRlt!=0) <СТОП>;

 

Листинг 1. Инициализация сети

 

При создании гнезда (листинг 2), используем функцию socket, три параметра указывают, что:

  1. PF_INET — использует протокол TCP/IP.
  2. SOCK_STREM — поточное гнездо.
  3. 0 — без специальных атрибутов.
SOCKET nSocket;
SOCKADDR_IN ClientSocketAddress;
 
ServerSocket =socket(PF_INET,SOCK_STREAM,0);
if (ServerSocket = =INVALID_SOCKET) <СТОП>;
ClientSocketAddress.sin_family=AF_INET;
ClientSocketAddress.sin_port=100;
ClientSocketAddress.sin_addr.S_un.S_addr=inet_addr(<IP адрес Сервера>);
connect(ServerSocket,(LPSOCKADDR)
&ClientSocketAddress, sizeof(ClientSocketAddress));

Листинг 2. Установка соединения

 

При ошибке создания гнезда останавливаем выполнение программы. Если гнездо удачно создано, переходим к описанию Internet-структуры (SOCKADDR_IN), которая будет нам необходима для осуществления соединения с Серверной частью:

  1. AF_INET — указывает, что это структура Internet.
  2. 100 — указывает номер порта, к которому необходимо подключиться, с помощью адреса (100 — как пример).
  3. Inet_addr(<IP адрес Сервера>) — указывается IP-адрес Сервера (xxx.xxx.xxx.xxx), к которому необходимо подключиться. Адрес задается как текстовая строка и преобразуется 31-битному представлению.

Функция Connect осуществляет подключение к серверу. У данной функции три параметра:

  1. Номер гнезда.
  2. Адрес структуры SOCKADDR_IN, преобразованный к типу LPSOCKADDR.
  3. Размер структуры SOCKADDR_IN (в байтах).

При возникновении ошибки функция возвращает ее код, иначе — ноль. Если соединение прошло удачно, можно приступать к передаче и чтению данных. Но об этом чуть позже. Сейчас мы рассмотрим, как реализован Сервер и к чему мы, в конце концов, присоединились.

В начало В начало

СЕРВЕР

Инициализация сети у Сервера идентична инициализации Клиента. Вы можете смело переписать программный код (см. листинг 1). Далее создаем гнездо. И если нет ошибки, переходим к описанию Internet-структуры. Здесь начинаются различия (листинг 3):

 

SOCKET nSocket;
SOCKADDR_IN ServerSocketAddress;
 
nSocket=socket(PF_INET,SOCK_STREAM,0);
if (nSocket= =INVALID_SOCKET) <СТОП>;
ServerSocketAddress.sin_family=AF_INET;
ServerSocketAddress.sin_port=100;
ServerSocketAddress.sin_addr.S_un.S_addr=INADDR_ANY;
bind(nSocket,(LPSOCKADDR)
& ServerSocketAddress, sizeof(ServerSocketAddress));
listen(nSocket,<количество клиентов>)

Листинг 3. Установка привязок

 

Адрес гнезда сервера устанавливается в INADDR_ANY. Адрес INADDR_ANY позволяет Серверу осуществить привязку к любому IP-адресу в системе «клиент-сервер» с множественной адресацией. Обратите внимание: значение порта у Клиента и Сервера должны совпадать (см. листинги 2 и 3). Функция bind привязывает адрес, определенный в структуре ServerSocketAddress, к конкретному гнезду. Теперь для того, чтобы одновременно несколько пользователей могли подключиться к Серверу, необходимо организовать очередь Клиентов, что мы и делаем функцией listen. Второй параметр данной функции определяет максимальное количество Клиентов, которое Сервер может принять. Когда Клиент выполняет соединение, он автоматически получает подтверждение, не блокируется в функции Connect и может продолжать обработку своих данных. Клиент ставится в очередь до тех пор, пока Сервер не сможет осуществить соединение. Если Клиент начнет передавать данные еще до того, как Сервер осуществил это соединение, то данные будут буферизироваться у Клиента до тех пор, пока буфер не заполнится.

После этого клиентская программа блокируется до тех пор, пока Сервер не сможет обработать Клиента. Прием Клиентов осуществляет функция accept (листинг 4):

// ---- Блок описания -------
fd_set AllSockets, ReadS, WriteS, ExceptS;
int rv, ClientSocket;
 
FD_ZERO(&AllSockets);
FD_ZERO(&ReadS);
FD_ZERO(&WriteS);
FD_ZERO(&ExceptS);
 
SOCKADDR_IN AddressClient;
 
// ---- Блок кода -------   
if (ReadS!=NULL)     User_copy(ReadS,&AllSockets);  
if (WriteS!=NULL)      User_copy(WriteS,&AllSockets);  
if (ExceptS!=NULL)  User_copy(ExceptS,&AllSockets);  
   
rv=select(0,ReadS,WriteS,ExceptS,NULL);  
if (rv= = SOCKET_ERROR)   
{  
     <ОБРАБОТКА ОШИБОК>  
}  
if (FD_ISSET(nSocket, ReadS))  
{  
     ClientSocket =accept(nSocket,  
 (LPSOCKADDR)&AddressClient, NULL);  
     if (ClientSocket= =130  
INVALID_SOCKET)  
     {  
     <ОБРАБОТКА ОШИБОК>  
     }  
     FD_SET(ClientSocket, &AllSockets);  
     FD_CLR(nSocket, ReadS);  
}  

Листинг 4. Контроль активности, прием Клиента

 

Сначала мы описываем четыре переменных (fd_set), которые представляют собой массив гнезд:

  1. AllSockets — набор, представляющий собой список всех контролируемых гнезд.
  2. ReadS — набор, содержащий только гнезда, которые имеют доступные для считывания данные.
  3. WriteS — набор, содержащий только гнезда, которые имеют доступные для записи данные.
  4. ExceptS — набор, содержащий только гнезда, которые имеют доступные внедиапазонные данные.

Инициализируем переменные fd_set, используя для этой цели макрокоманду FD_ZERO, которая обнулит эти переменные. А в переменной AddressClient будет расположен адрес текущего Клиента, когда он принимается Сервером. Далее следует сам программный блок приема и обработки Клиента. Как правило, его вставляют в цикл с выходом по завершении программы.

Пользовательская функция User_copy осуществляет инициализацию наборов fd_setReadS, WriteS, ExceptS (листинг 5):

void User_copy(fd_set* out, fd_set* in)  
{  
  UINT i;  
   
  for(i=0;i<in->fd_count; i++)out->fd_array[i]=in->fd_array[i];  
  out->fd_count=in->fd_count;  
}  

Листинг 5. Функция копирования fd_set

 

Эта функция предназначена для копирования одного набора fd_set в другой, а также для копирования значения счета из входящей структуры в исходящую структуру.

Выполняемая следующей функция select осуществляет проверку гнезд на содержание в них нормальных или внедиапазонных данных, а также на возможность произвести запись своих данных. Если данные отсутствуют, то функция входит в режим ожидания данных. При появлении данных функция select осуществляет возвращение, набор ReadS будет содержать только гнезда, которые имеют доступные для считывания данные, WriteS — доступные для записи, ExceptS — доступные внедиапазонные данные. Последний параметр функции select задает время выдержки. Если вы не хотите осуществлять режим никакой выдержки, установите значение в NULL. Вместо любого набора fd_set можно также установить значение NULL, функция select не будет производить проверку по данному набору fd_set. Если в качестве параметра используется набор WriteS, функция всегда будет производить возврат. Необходимо отметить, что запрос Клиента на соединение с Сервером на гнезде прослушивания рассматривается как наличие доступных для считывания данных и Сервер должен произвести соединение accept. Однако, прежде чем выполнять данную функцию, необходимо убедиться, что в настоящий момент другие Клиенты не посылают на Сервер данные. Для этой цели используем макрокоманду FD_ISSET, которая покажет, находится ли в данный момент номер nSocket в наборе ReadS. Если гнездо находится в наборе ReadS, то вызывается функция accept для приема нового Клиента. Функция FD_SET используется для добавления нового гнезда к набору AllSockets — для последующего за ним контроля функцией select. После того как вы обработали подключившегося Клиента, он устраняется из набора ReadS; теперь можно обрабатывать других Клиентов.

После установления соединения Сервера и Клиента остается рассмотреть вопрос передачи/получения данных. Функции, выполняющие данные операции, — send и recv. Ниже приведен их формат.

 

int send(

   

SOCKET

s,

Идентификатор гнезда, для Клиента — значение, возвращаемое функцией connect, для Сервера — функцией accept

const char FAR

*buf,

Буфер, содержащий данные

для передач

int

len,

Длина буфера

int

flags

Индикатор типа данных:

MSG_OOB — данные OOB;

0 — нормальные данные

);

   

 

int recv(

   

SOCKET

s,

Идентификатор гнезда, для Клиента — значение, возвращаемое функцией connect, для Сервера — функцией accept

const char FAR

*buf,

Буфер, содержащий данные

для получения

int

len,

Длина буфера

int

flags

Индикатор типа данных:

MSG_OOB — данные OOB;

0 — нормальные данные

);

   

 

Пример функции передачи/получения данных из переменной класса CString (листинги 6, 7, 8):

int MySend(SOCKET nSocket, CString lpBuf,   
                                   int nBufLen, int nFlags)  
{  
   int rv=send(nSocket,(LPCTSTR)lpBuf, nBufLen, nFlags);  
   if (rv= = SOCKET_ERROR)   
  {   
       VERIFY(SOCKET_ERROR != closesocket(nSocket));  
       WSACleanup();  
   }  
return rv;  
}  

Листинг 6. Передача строки (CString)

int MyRecv(SOCKET nSocket, CString* lpBuf,   
                     int nBufLen, int nFlags)  
{  
   int rv;  
   rv=recv(nSocket,lpBuf->GetBuffer(nBufLen),nBufLen,nFlags);  
   lpBuf->ReleaseBuffer();  
   if (rv= =SOCKET_ERROR)   
   {  
       VERIFY(SOCKET_ERROR != closesocket(nSocket));  
       WSACleanup();  
    }  
return rv;  
}  

Листинг 7. Прием строки (CString)

CString sendBuff="12345678";  
CString recvBuff;  
   
SOCKET s; // значение, возвращаемое функцией connect,  
                    //   для  Сервера – функцией accept.  
   
MySend (s, sendBuff,8,0);  

Листинг 8. Пример вызова функций

 

Данные функции удобно использовать, если в качестве данных используются короткие сообщения. Класс CString обладает мощными встроенными средствами для обработки текстовой информации.

 

Продолжение следует

КомпьютерПресс 3'2001