Эта функция будет использоваться в дальнейшем в нескольких наших примерах.
2.1.12. Примеры передачи данных с помощью TCP
Теперь у нас достаточно знаний, чтобы написать TCP-клиент и TCP-сервер. Как и в случае с UDP, сначала нужно договориться о том, какими данными и в каком формате будут обмениваться наши программы. С протоколом, описанным здесь, нам предстоит работать на протяжении всей главы. По мере изучения новых возможностей библиотеки сокетов мы будем реализовывать новые варианты серверов и клиентов, но почти все они будут поддерживать один и тот же протокол, поэтому любой клиент сможет работать с любым сервером.
Наши сервер и клиент будут обмениваться строковыми сообщениями: клиент пошлет строку, сервер отправит ответ. Мы уже не можем, как в случае UDP, просто отправить строку, потому что при использовании TCP несколько строк могут быть отправлены одним пакетом, или наоборот, одна строка разбита на несколько пакетов. Соответственно, наш протокол должен позволять определить, где заканчивается одна строка и начинается другая.
Ранее мы уже упоминали три основных способа определения границ логического пакета в TCP: все пакеты могут иметь одинаковую длину, пакет может предваряться фиксированным заголовком, содержащим длину, между пакетами может вставляться заранее оговоренная последовательность байт. Первый способ самый легкий в реализации, но он накладывает существенные ограничения на передаваемые данные. В частности, нам он не подходит, потому что мы будем передавать строки произвольной длины. Второй и третий способы приемлемы для передачи строк, и чтобы проиллюстрировать как можно больше различных вариантов в наших примерах, мы будем использовать их оба. При передаче данных от клиента серверу, мы будем перед строгой передавать четырёхбайтное значение — длину строки, а при передаче данных от сервера клиенту длину строки мы передавать не будем, но к каждой строке будет добавляться символ #0
, указывающий на завершение строки. Таким образом, получается, что строки, передаваемые клиентом, могут содержать символ #0
в середине, а передаваемые сервером — нет.
Все серверы, которые мы напишем, будут возвращать клиенту присланную строку, но слегка преобразованную. Во-первых, все символы #0
будут в ней заменены на подстроку "#0
", во-вторых, все буквы превращены в заглавные, а в-третьих, добавлено имя сервера, который ответил.
Практическое знакомство с TCP мы начнем с написания простейшего сервера. На компакт-диске этот сервер находится в папке SimplestServer
. Сразу отметим, что это чисто учебный пример, и брать его за основу при создании реальных серверов ни в коем случае нельзя. Чуть позже мы напишем другой сервер, который уже может служить образцом для подражания.
Наш простейший сервер будет использовать только одну нить. Как мы помним, сервер должен вызывать две функции, которые блокируют работу нити: accept и recv. Очевидно, что задействовать их обе сразу в одной нити не получится, именно поэтому наш сервер сможет работать только с одним клиентом одновременно. И чтобы не блокировать пользовательский интерфейс, наш сервер будет консольным приложением. В командной строке ему передается номер порта, к которому привязывается слушающий сокет.
Первое, что должен сделать сервер, — это создать сокет. привязать его к требуемому адресу и перевести в режим прослушивания. Этот код мало чем отличается от приведенного ранее примера создания сокета для UDP (см. листинг 2.8). Вся разница только в том, что вместо сокета типа SOCK_DGRAM
создается сокет типа SOCK_STREAM
, а в конце еще вызывается функция listen
(листинг 2.14).
var
// Порт, который будет "слушать" сервер
Port: Word;
// "Слушающей" сокет
MainSocket: TSocket;
// Сокет, создающийся для обслуживания клиента
ClientSocket: TSocket;
// Адрес "слушающего" сокета
MainSockAddr: TSockAddr;
// Адрес подключившегося клиента
ClientSockAddr: TSockAddr;
// Размер адреса подключившегося клиента
ClientSockAddrLen: Integer;
//Без этой переменной не удастся инициализировать библиотеку сокетов
WSAData: TWSAData;
StrLen: Integer;
Str: string;
begin
try
if ParamCount = 0 then
// Если в командной строке порт не задан, назначаем его
Port := 12345;
else
// В противном случае анализируем командную строку и назначаем порт
try