Для протокола TCP настройки обычно задаются на уровне соединения, а не отдельного пакета. Составляется список портов, открытых для внешнего подключения. Если сервер использует порт не из этого набора, клиент не сможет к нему подключиться. Для исходящих подключений тоже составляется список программ, которым разрешено это к делать, и, если клиент, отсутствующий в "белом" списке сетевого экрана, пытается подключиться к удаленному серверу, сетевой экран не допускает этого.
Примечание
Здесь описаны наиболее типичные способы локальной фильтрации пакетов сетевым экраном. В каждом конкретном случае могут применяться другие правила.
При тестировании своих примеров или примеров из этой книги вы можете столкнуться с тем, что программы по непонятным причинам не могут обмениваться данными по сети, и это может объясняться работой сетевых экранов. Проконсультируйтесь у администратора сети насчет настроек сетевых экранов в вашей сети и согласуйте с ним возможность работы с теми или иными портами и запуска ваших приложений.
До сих пор мы обсуждали только теоретические аспекты работы с сокетами. Далее будут рассматриваться конкретные функции, позволяющие осуществлять те или иные операции с сокетами. Эти функции экспортируются системной библиотекой wsock32.dll (а также библиотекой ws2_32.dll; взаимоотношение этих библиотек будет обсуждаться во втором разделе данной главы), для их использования в Delphi в раздел uses нужно добавить стандартный модуль WinSock. Полное формальное описание функций этого модуля здесь приводиться не будет (для этого есть MSDN), но для каждой функции будет дано описание, достаточно полное для понимания ее предназначения. Кроме того, мы будем также обращать внимание на некоторые моменты, которые в MSDN найти трудно. Тем не менее после знакомства с этим текстом настоятельно рекомендуется самостоятельно прочитать в MSDN описания всех упомянутых в нем функций.
Хотя ранее мы договорились, что будем обсуждать только стандартные сокеты, тем не менее, есть три функции, относящиеся к сокетам Windows, не познакомившись с которыми мы не сможем двигаться дальше. Это функции WSAStartup, WSACleanup и WSAGetLastError (префикс WSA означает Windows Sockets API и служит для именования большинства функций, относящихся к Windows-расширению библиотеки сокетов).
Функция WSAStartup предназначена для инициализации библиотеки сокетов. Эту функцию необходимо вызвать до вызова любой другой функции из этой библиотеки. Ее прототип имеет вид:
function WSAStartup(wVersionRequired: Word; var WSData: TWSAData): Integer;
Параметр wVersionRequired задает требуемую версию библиотеки сокетов. Младший байт задает основную версию, старший — дополнительную. Допустимы версии 1.0 ($0001), 1.1 ($0101), 2.0 ($0002) и 2.2 ($0202). Пока мы работаем со стандартными сокетами, принципиальной разницы между этими версиями нет, но версии 2.0 и выше пока лучше не использовать, т.к. модуль WinSock не рассчитан на их поддержку. Вопросы взаимоотношения библиотек и версий будут рассматриваться во втором разделе этой главы, а пока ограничимся версией 1.1.
Параметр WSData выходной, т.е. значение, которое имела переменная до вызова функции, игнорируется, а имеет смысл только то значение, которое эта переменная получит после вызова функции. Через этот параметр передается дополнительная информация о библиотеке сокетов. В большинстве случаев эти сведения не представляют никакого интереса, поэтому их можно игнорировать.
Нулевое значение, возвращаемое функцией, говорит об успешном завершении, в противном случае возвращается код ошибки. Обычно функция, завершившаяся с ошибкой, возвращает значение SOCKET_ERROR.
Функция WSACleanup завершает работу с библиотекой сокетов. Эта функция не имеет параметров и возвращает ноль в случае успешного завершения или код ошибки в противном случае. Функцию WSAStartup достаточно вызвать один раз, даже в многонитевом приложении, в этом ее отличие от таких функций, как, например, CoInitialize, которая должна быть вызвана в каждой нити, использующей COM. Функцию можно вызывать повторно — в этом случае ее вызов не дает никакого эффекта, но для завершения работы с библиотекой сокетов функция WSACleanup должна быть вызвана столько же раз, сколько была вызвана WSAStartup.
Большинство функций библиотеки сокетов возвращают значение, позволяющее судить только об успешном или неуспешном завершении операции, но не дающее информации о том, какая именно ошибка произошла (если она произошла). Для получения сведений об ошибке служит функция WSAGetLastError, не имеющая параметров и возвращающая целочисленный код последней ошибки, произошедшей в библиотеке сокетов в данной нити. После неудачного завершения функции из библиотеки сокетов следует вызывать функцию WSAGetLastError, чтобы выяснить причину неудачи.
Забегая чуть вперед, отметим, что библиотека сокетов содержит стандартную функцию getsockopt, которая, кроме всего прочего, также позволяет получить информацию об ошибке. Однако она менее удобна, поэтому в тех случаях, когда не требуется совместимость с другими платформами, лучше прибегнуть к WSAGetLastError. К тому же, getsockopt возвращает ошибку, связанную с указанным сокетом, поэтому с её помощью нельзя получить код ошибки, не связанной с конкретным сокетом.
Для создания сокета предусмотрена стандартная функция socket со следующим прототипом:
function socket(af, struct, protocol: Integer): TSocket;
Параметр аf задаёт семейство адресов (address family). Этот параметр определяет, какой способ адресации (т.е. по сути дела, какой стек протоколов) будет использоваться для данного сокета. Для TCP/IP этот параметр должен быть равен AF_INET, для других стеков также есть соответствующие константы, которые можно посмотреть в файле WinSock.pas.
Параметр struct указывает тип сокета и может принимать одно из двух значений: SOCK_STREAM (для потоковых протоколов) и SOCK_DGRAM (для дейтаграммных протоколов).
Параметр protocol позволяет указать, какой именно протокол будет использоваться сокетом. Этот параметр можно оставить равным нулю — тогда будет выбран протокол по умолчанию, отвечающий условиям, заданным первыми двумя параметрами. Для стека TCP/IP потоковый протокол по умолчанию — TCP, дейтаграммный — UDP. В некоторых примерах можно увидеть значение третьего параметра равно IPPROTO_IP. Значение этой константы равно 0, и ее использование только повышает читабельность кода, но приводит к тому же результату: будет выбран протокол по умолчанию. Если требуется протокол, отличный от протокола по умолчанию (например, в некоторых реализациях стека TCP/IP существует протокол RDP — Reliable Datagram Protocol, надежный дейтаграммный протокол), следует указать здесь соответствующую константу (для RDP это будет IPPROTO_RDP). Можно также явно задать TCP или UDP с помощью констант IPPROTO_TCP и IPPROTO_UDP соответственно.
Тип TSocket предназначен для хранения дескриптора сокета. Формально он совпадает с 32-битным беззнаковым целым типом, но об этом лучше не вспоминать, т.к. любые операции над значениями типа TSocket бессмысленны. Значение, возвращаемое функцией socket, следует сохранить в переменной соответствующего типа и затем использовать для идентификации сокета при вызове других функций. Если по каким-то причинам создание сокета невозможно, функция вернет значение INVALID_SOCKET. Причину ошибки можно узнать с помощью функции WSAGetLastError.
Сокет, созданный с помощью функции socket, не привязан ни к какому адресу. Привязка осуществляется с помощью функции bind, имеющей следующий прототип:
function bind(s: TSocket; var addr: TSockAddr; namelen: Integer): Integer;
Первый параметр этой функции — дескриптор сокета. который привязывается к адресу. Здесь, как и в остальных подобных случаях, требуется передать значение, которое вернула функция socket. Второй параметр содержит адрес, к которому требуется привязать сокет, а третий — длину структуры, содержащей адрес.
Функция bind предназначена для сокетов, реализующих разные протоколы из разных стеков, поэтому кодирование адреса в ней сделано достаточно универсальным. Впрочем, следует отметить, что разработчики модуля WinSock для Delphi выбрали не лучший способ перевода прототипа этой функции на Паскаль, поэтому универсальность в значительной мере утрачена. В оригинале прототип функции bind имеет следующий вид:
int bind(SOCKET s, const struct sockaddr FAR* name, int namelen);
Видно, что второй параметр — это указатель на структуру sockaddr. Однако C/C++ позволяет при вызове функции в качестве параметра передать указатель на любую другую структуру, если будет выполнено явное приведение типов. Для каждого семейства адресов предусмотрена своя структура, и в качестве фактического параметра передастся указатель на эту структурy. Если бы авторы модуля WinSock описали второй параметр как параметр-значение типа указатель, можно было бы поступать точно так же. Однако они описали этот параметр как параметр-переменную. В результате на двоичном уровне ничего не изменилось: и там, и там в стек помещается указатель. Однако компилятор при вызове функции bind не допустит использования никакой другой структуры, кроме TSockAddr, а эта структура не универсальна и удобна, по сути дела, только при использовании стека TCP/IP. В других случаях наилучшим решением будет самостоятельно импортировать функцию bind из wsock32.dll с нужным прототипом. При этом придется импортировать и некоторые другие функции, работающие с адресами. Впрочем мы здесь ограничиваемся только протоколами TCP и UDP, поэтому больше останавливаться на этом вопросе не будем.