понедельник, 19 декабря 2011 г.

WinSock Api: Основы TCP


    И так, наконец-то я добрался до второй статьи по сетевому программированию.
    И посвящается она теоретическим основам передачи данных при использовании TCP на WinSock Api.
    Сначала я опишу принципы работы данного протокола, особенности используемых функций, а потом приведу листинги базовых функций для сервера и клиента)


    Для передачи данных с помощью TCP необходимо сначала установить соединение, а после этого уже обмениваться данными.

    При программировании TCP и UDP можно применять одни и те же функции, только действовать они будут по разному. В предыдущей статье я рассмотрел функции sendto и recvfrom - отправка и получение дейтаграмм для UDP соединения. Их тоже можно использовать в случае с TCP, только адреса, указанные в этих функциях, будут игнорироваться, т.к. действующим адресом всегда будет тот с которым было установлено соединение.

    В TCP взаимодействие сторон должно разделяться на сервер и клиент.
Для начала взаимодействия клиент должен установить соединение с сервером с помощью функции connect. В данном случае она устанавливает реальное соединение, поэтому в ней происходит проверка существования сокета по указанному адресу, который должен находиться в  режиме ожидания подключения. Функция connect завершается успешно, только когда соединение установлено.

    В отличии от UDP, в TCP нельзя послать пакет на другой адрес, если уже установлено соединение. Для другого адреса нужны новый сокет и подключение.
TCP является надёжным протоколом в плане того что в случае ошибки доставки пакета, сторона отправителя будет уведомлена об этом.

    Функция recv копирует пришедшие данные из входного буфера сокета в буфер, заданный массивом байт. Скопированные данные при этом удаляются из буфера сокета. При этом нужно помнить, что все посланные данные сливаются в один поток и принимающей стороне нужно уже самой разбираться как читать их из буфера, т.е. нужно определить для себя какие-либо правила пересылки и получения данных, а это и есть процесс формирования своего пакета для сетевого приложения. Если, например, за один раз вызова recv было скопировано меньше байт, чем лежит в буфере сокета, то оставшиеся байты никуда не пропадают и могут спокойно быть дочитанными при следующем вызове. Функция recv как раз таки возвращает кол-во байт скопированных из буфера сокета, а если на момент её вызова сокет пуст, то она будет ожидать появления данных в нём, блокируя при этом управление программой.
    Если recv возвращает 0, то это означает что удалённый сокет корректно завершил закрыл соединение, а т.е., не трудно догадаться, что если послать нулевой пакет, то это послужит закрытию соединения) А если соединение разорвано некорректно, то функция recv вернёт ошибку = SOCKET_ERROR.

    Давайте теперь разберёмся, что входит в обязанности серверной части соединения.
Во первых сервак должен создать прослушивающий сокет, а т.е. перевести какой-либо сокет в слушающий режим функцией listen:
Прототип функции listen
function listen(s: TSocket; backlog: integer): integer;
    Параметр s задаёт сокет, который переводится в режим ожидания. Так же для этого сокета должна быть вызвана привязка функцией bind. Для этого сокета создаётся очередь подключений параметром backlog, если атрибут = SOMAXCONN, то очередь будет максимальной. Функция возвращает 0 при успешном завершении и SOCKET_ERROR при ошибке.
    После вызова функции connect на стороне клиента, вызывается рассмотренная функция listen на сервере и подключение добавляется в очередь, в этот момент функция connect возвращает положительный результат, но действительного соединения всё ещё нет!
Для завершения полного соединения на сервере нужно продолжить операцию подключения: извлечь из очереди информацию о подключившемся клиенте и создать для него сокет.
Этот шаг выполняется с помощью функции accept:
Прототип функции accept 
function accept(s: TSocket; addr: PSockAddr;
  addrlen: PInteger): TSocket; 
    Параметр s задаёт сокет, в очереди которого, находится информация о новом соединении, а т.е. наш слушающий сокет. Выходной параметр addr позволяет узнать адрес клиента. Ну и параметр addrlen, как можно догадаться, длина буфера addr, а т.е. = SizeOf(TSockAddr).
Возвращает функция дескриптор сокета, который создастся уже самой библиотекой сокетов для обслуживания клиента. В случае ошибки вернёт сокет для инвалидов(INVALID_SOCKET).

    В общем созданный сокет функцией accept будет уже привязан к адресу и соединён с клиентом и его сразу можно использовать для посылки и чтения пакетов.
Теперь пробежимся по всему вышесказанному для формирования правильной картины действий:
Сервер должен создать сокет с помощью функции socket, привязать его к порту функцией bind, перевести его в режим прослушки функцией listen, а уже потом создаётся новый сокет\сокеты функцией accept обслуживающий клиента\клиентов.
На стороне клиента в это время создаётся сокет, привязывается bind'ом, а затем connect'ится к прослушивающему сокету сервера.
Всё, соединение налажено, можно обмениваться данными)
Теперь приведу выдержки из кода частей, которые мы обсудили.
Код сервера 
uses
  WinSock;
var
  WSAData: TWSAData;  
  ListenSocket: TSocket;
  ClientSocket: TSocket;
  Addr: TSockAddr;
  len: integer;
begin
  WSAStartup($101, WSAData);
  ListenSocket := socket(AF_INET, SOCK_STREAM, 0);
  Addr.sin_family := AF_INET;
  Addr.sin_port := htons(1500);
  Addr.sin_addr.S_addr := INADDR_ANY;
  FillChar(Addr.sin_zero, SizeOf(Addr.sin_zero), 0);
  bind(ListenSocket, Addr, SizeOf(TSockAddr));
  listen(ListenSocket, SOMAXCONN);
  len := SizeOf(TSockAddr);
  ClientSocket := accept(ListenSocket, @Addr, @len);
  {
    А далее уже можно выполнять следующие функции
    send(ClientSocket, ...);
    recv(ClientSocket, ...);

  }
В данном примере сокет привязывается к порту 1500, но порт программист волен выбирать сам в диапазоне [1024..65535].
Код клиента
uses
  WinSock;
var
  WSAData: TWSAData;  
  CliSock: TSocket;
  Addr: TSockAddr;
begin
  WSAStartup($101, WSAData);
  CliSock := socket(AF_INET, SOCK_STREAM, 0);
  Addr.sin_family := AF_INET;
  Addr.sin_port := htons(1500);
  Addr.sin_addr.S_addr := inet_addr('127.0.0.1');
  FillChar(Addr.sin_zero, SizeOf(Addr.sin_zero), 0);
  connect(CliSock, Addr, SizeOf(TSockAddr));
  {
    Теперь соединение установлено, можно выполнять:
    send(CliSock, ...);
    recv(CliSock, ...);

  }
Клиент в свою очередь должен подключиться к адресу сервера(например IP адрес) и указать правильный порт.

Теперь некоторые особенности:
    Если при вызове accept очередь подключений пуста, то нить будет заблокирована в ожидании. Поэтому для прослушки, как правило всегда, создаётся отдельная нить.
И так же разумно создавать нить для каждого вновь подключившегося клиента.
Я слышал о некоторых изощрённых способах создания нитей для определённого кол-ва клиентов, но сам такое не практиковал.

И так, теории по TCP я думаю пока достаточно, но практику перенесу в следующие статьи)

1 комментарий:

  1. При видя таких синих окошечек с кодом вспоминается Pascal...

    ОтветитьУдалить