Программирование в сетях Windows
.pdfп ое |
l A U i b i i интерфейс прикладного программирования winsocK |
Передача данных
По сути, в сетевом программировании самое главное — уметь отправлять и принимать данные. Для пересылки данных по сокету используются функции send и WSASend. Аналогично, для приема данных существуют функции recv
и WSARecv.
Заметьте, что все буферы, используемые при отправке и приеме данных состоят из элементов типа char. To есть эти функции не предназначены для работы с кодировкой UNICODE. Это особенно важно для Windows СЕ, так как она использует UNICODE по умолчанию. Отправить строку символов UNICODE можно двумя способами: в исходном виде или привести к типу char '. Тонкость в том, что при указании количества отправляемых или принимаемых символов результат функции, определяющей длину строки, нужно умножить на 2, так как каждый UNICODE-символ занимает 2 байта строкового массива. Другой способ: сначала перевести строку из UNICODE в ASCII функцией
WideCharToMuMByte.
Все функции приема и отправки данных при возникновении ошибки возвращают код SOCKETJERROR. Для получения более подробной информации об ошибке вызовите функцию WSAGetLastError. Самые распространенные ошибки - WSAECONNABORTED и WSAECONNRESET. Обе возникают при закрытии соединения: либо по истечении времени ожидания, либо при закрытии соединения партнерским узлом. Еще одна типичная ошибка — WSAEWOULDBLOCK, обычно происходит при использовании неблокирующих или асинхронных сокетов. По существу, она означает, что функция не может быть выполнена в данный момент. В главе 8 будут описаны разные методы ввода-вывода Winsock, которые помогут избежать этих ошибок.
ФункцииsendиWSASend
API-функция send для отправки данных по сокету определена так:
int send(
SOCKET s,
const char FAR * buf,
int len, |
|П |
int flags
);
Параметр s определяет сокет для отправки данных. Второй параметр — buf, указывает на символьный буфер, содержащий данные для отправки. Третий — len, задает число отправляемых из буфера символов. И последний параметр — flags, может принимать значения О, MSGDONTROUTE, MSG_OOB, или результат логического ИЛИ над любыми из этих параметров. При указании флага MSGJDONTROUTE транспорт не будет маршрутизировать отправляемые пакеты. Обработка этого запроса остается на усмотрение базового протокола (например, если транспорт не поддерживает этот параметр", запрос игнорируется). Флаг MSGOOB указывает, что данные должны быть отправлены вне полосы (out of band), то есть срочно.
JlADM I Ul.num>ivvinouv-rv
При успешном выполнении функция send вернет количество переданных иначе — ошибку SOCKET_ERROR. Одна из типичных ошибок — WSAEoNNABORTED, происходит при разрыве виртуального соединения из-за шибки протокола или истечения времени ожидания. В этом случае сокет ж е Н быть закрыт, так как он больше не может использоваться. Ошибка yrfiAECONNRESET происходит, если приложение на удаленном узле, выполнив аппаратное закрытие, сбрасывает виртуальное соединение, или неожиданно завершается, или происходит перезагрузка удаленного узла. В этой ситуации сокет также должен быть закрыт. Еще одна ошибка — WSAETIMEDOUT, зачастую происходит при обрыве соединения по причине сбоев сети или
отказа удаленной системы без предупреждения.
функция Winsock версии 2 WSASend — аналог send, определена так:
int WSASend( SOCKET s,
LPWSABUF lpBuffers,
DWORDdwBufferCount,
LPDWORDlpNumberOfBytesSent,
DWORDdwFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE
);
Сокет является действительным описателем сеанса соединения. Второй параметр указывает на структуру WSABUF или на массив этих структур. Третий — определяет число передаваемых структур WSABUF. Помните, что структура WSABUF включает сам символьный буфер и его длину. Может возникнуть вопрос: зачем нужно отправлять более одного буфера за раз? Это называется комплексным вводом-выводом (scatter-gather I/O). Подробней мы обсудим его далее, а сейчас лишь отметим, что при использовании нескольких буферов для отправки данных по сокету соединения массив буферов отправляется, начиная с первой и заканчивая последней структурой WSABUF.
Параметр lpNumberOfBytesSent — указатель на тип DWORD, который после вызова WSASend содержит общее число переданных байт. Параметр флагов dwFlags такой же, что и в функции send. Последние два указателя — lpOverlapped и lpCompletionROUTINE используются для перекрытого ввода-вы- вода (overlapped I/O) — одной из моделей асинхронного ввода-вывода, поддерживаемых Winsock (см. также главу 8).
WSASend присваивает параметру ipNumberOjBytesSent количество записанных байт. При успешном выполнении функция возвращает 0, иначе — SO- ^KET_ERROR. Ошибки те же, что и у функции send.
ФункцияWSASendDisconnect
то специализированная функция используется редко. Она определена так:
i n t WSASendDisconnect ( |
|
SOCKET s, |
i--w |
LPWSABUFlpOUTboundDisconnectData |
|
ii интерфейс прикладного программирования Winsock
Срочные данные
Если приложению требуется отправить через потоковый сокет информацию более высокого приоритета, оно может обозначить эти сведения как срочные данные (out-of-band, OOB) Приложение с другой стороны соединения получает и обрабатывает ООВ-данные через отдельный логический канал, концептуально независимый от потока данных
ВTCP передача ООВ-данных реализована путем добавления 1 -би- тового маркера (называемого URG) и 1б-битного указателя в заголовке сегмента TCP, которые позволяют выделить важные байты в основном трафике На данный момент для TCP существуют два способа выделения срочных данных В RFC 793, описывающем TCP и концепцию срочных данных, говорится, что указатель срочности в заголовке TCP является положительным смещением байта, следующего за байтом срочных данных Однако в RFC 1122 это смещение трактуется, как указатель на сам байт срочности
Вспецификации Winsock под термином ООВ понимают как независимые'от протокола ООВ-данные, так и реализацию механизма передачи срочных данных в TCP Для проверки, есть ли в очереди срочные данные, вызовите функцию toctlsocket с параметром SIOCATMARK Подробнее об этой функции — в главе 9
ВWinsock предусмотрено несколько способов передачи срочных данных Можно встроить их в обычный поток, либо, отключив эту возможность, вызвать отдельную функцию, возвращающую только срочные данные Параметр SOJDOBINLJNE управляет поведением ООВ-дан- ных (он подробно обсуждается в главе 9)
Вряде случаев срочные данные используют программы Telnet и Rlogin Впрочем, если вы не планируете писать собственные версии этих программ, избегайте применения срочных данных — они не стандартизированы и могут иметь другие реализации на отличных от Win32 платформах Если вам нужно время от времени передавать срочно какую-то информацию, создайте отдельный управляющий сокет для срочных данных, а основное соединение предоставьте для обычной передачи данных
Функция WSASendDtsconnect начинает процесс закрытия сокета и отправляет соответствующие данные Разумеется, она доступна только для протоколов, поддерживающих постепенное закрытие и передачу данных при его осуществлении Ни один из существующих поставщиков транспорта на данный момент не поддерживает передачу данных о закрытии соединения Функция
WSASendDisconnect действует аналогично shutdown с параметром SD_SEND, но также отправляет данные, содержащиеся в параметре boundDisconnectData После ее вызова отправлять данные через сокет невозможно В случае неудачного завершения WSASendDisconnect возвращает значение SOCKET_ERROK Ошибки, встречающиеся при работе функции, аналогичны ошибкам send
Г Л А ВА 7 Основы Wmsock |
171 |
функцииrecvиWSARecv
Функция recv — основной инструмент приема данных по сокету Она определена так
int recv( SOCKET s,
char FAR» buf, int len,
int flags
),
Параметр s определяет сокет для приема данных Второй параметр — buf является символьным буфером и предназначен для полученных данных, a len указывает число принимаемых байт или размер буфера buf Последний параметр — flags, может принимать значения О, MSG_РЕЕК, MSGJDOB или результат логического ИЛИ над любыми из этих параметров Разумеется, 0 означает отсутствие особых действий Флаг MSG_PEEK указывает, что доступные данные должны копироваться в принимающий буфер и при этом оставаться в системном буфере По завершении функция также возвращает количество ожидающих байт
Считывать сообщения таким образом не рекомендуется Мало того, что из-за двух системных вызовов (одно! о — для считывания данных, и другого, без флага MSGJPEEK — для удаления данных), снижается производительность В ряде случаев этот способ просто не надежен Объем возвращаемых данных может не соответствовать их суммарному доступному количеству К тому же, сохраняя данные в системных буферах, система оставляет все меньше памяти для размещения входящих данных В результате уменьшается размер окна TCP для всех отправителей, что не позволяет приложению достичь максимальной производительности Лучше всего скопировать все данные в собственный буфер и обрабатывать их там Флаг MSGJDOB уже обсуждался ранее при рассмотрении отправки данных
Использование recv в сокетах, ориентированных на передачу сообщений или дейтаграмм, имеет несколько особенностей Если при вызове recv размер ожидающих обработки данных больше предоставляемого буфера, то после его полного заполнения возникает ошибка WSAEMSGSIZE Заметьте ошибка превышения размера сообщения происходит только при использовании протоколов, ориентированных на передачу сообщений Потоковые протоколы буферизируют поступающие данные и при запросе приложением предоставляют их в полном объеме, даже если количество ожидающих обработки данных больше размера буфера Таким образом, ошибка WSAEMSGSIZE не может произойти при работе с потоковыми протоколами
Функция WSARecv обладает дополнительными по сравнению с recv возможностями поддерживает перекрытый ввод-вывод и фрагментарные дей- Т а граммные уведомления
l n * WSARecv( SOCKET s,
LPWSABUF lpBuffers,
1 72 |
ЧАСТЬ II |
Интерфейс прикладного программирования Winsock |
DWORD |
dwBufferCount, |
|
LPDWORD lpNumberOfBytesRecvd, |
||
LPDWORD IpFlags, |
||
LPWSAOVERLAPPED |
lpOverlapped, |
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE
);
Параметр 5 — сокет соединения. Второй и третий параметры определяют буферы для приема данных. Указатель IpBuffers ссылается на массив структур WSABUF, a dwBufferCount — определяет количество таких структур в массиве. Параметр ipNumberOfBytesReceived в случае немедленного завершения операции получения данных указывает на количество принятых этим вызовом байт. Параметр IpFlags может принимать значения MSGJPEEK, MSGJDOB, MSG_PARTIAL
или результат логического ИЛИ над любыми из этих параметров.
У флага MSG_PARTIAL в зависимости от способа использования могут быть разные значения и смысл. Для протоколов, ориентированных на передачу сообщений, этот флаг задается после вызова WSARecv (если все сообщение не может быть возвращено из-за нехватки места в буфере). В этом случае каждый последующий вызов WSARecv задает флагMSG_PARTIAL, пока сообщение не будет прочитано целиком. Если этот флаг передается как входной параметр, операция приема данных должна завершиться, как только данные будут доступны, даже если это только часть сообщения. Флаг MSG_PARTIAL используется только с протоколами, ориентированными на передачу сообщений. Запись каждого протокола в каталоге Winsock содержит флаг, указывающий на поддержку этой возможности (см. также главу 5). Параметры lpOverlapped и lpCompletionROUTINE применяются в операциях перекрытого ввода-вывода (обсуждаются в главе 8).
ФункцияWSARecvDisconnect
Эта функция обратна WSASendDisconnect и определена так:
int WSARecvDisconnect( SOCKET s,
LPWSABUF lpInboundDisconnectData
);
Как и у WSASendDisconnect, ее параметрами являются описатель сокета соединения и действительная структура WSABUF для приема данных. Функция принимает только данные о закрытии соединения, отправленные с другой стороны функцией WSASendDisconnect, ее нельзя использовать для приема обычных данных. К тому же, сразу после принятия данных она прекращает прием с удаленной стороны, что эквивалентно вызову shutdown с параметромSD_RECV.
ФункцияWSARecvEx
Эта функция — специальное расширение Microsoft для Winsock 1. Она иДе* тична recv во всем, кроме того, что параметру7а§5 передается по ссылке. •#э* позволяет базовому поставщику задавать флаг MSG_PARTIAL.
Г Л А ВА 7 Основы Winsock |
173 |
int PASCAL FAR WSARecvEx( SOCKET s,
char FAR « buf, int len,
int «flags
);
Если полученные данные не составляют полного сообщения, в параметре flags возвращается флагMSG_PARTIAL. Он используется только с протоколами, ориентированными на передачу сообщений. Когда при принятии неполногосообщенияфлагMSG_PARTIALпередаетсякакчастьпараметрами^, функция завершается немедленно, вернув принятые данные. Если в буфере не хватает места, чтобы принять сообщение целиком, WSARecvEx вернет ошибкуWSAEMSGSIZE,аоставшиесяданныебудутотброшены.Обратитевнимание на разницу между флагом MSG_PARTIAL и ошибкой WSAEMSGSIZE: в случае ошибки сообщение поступило целиком, однако соответствующий буфер слишком мал для его приема. ФлагиMSG_PEEKиMSGJDOB также можноиспользоватьвWSARecvEx.
Потоковые протоколы
Большинство протоколов с установлением соединения являются потоковыми. Важно учитывать, что при использовании любой функции отправки или приема данных через потоковый сокет нет гарантии, что вы прочитаете или запишете весь запрошенный объем данных. Скажем, требуется отправить 2048 байт из символьного буфера функцией send:
char sendbuff[2048]; int nBytes = 2048;
//Заполнение буфера sendbuff 2048 байтами данных
//Присвоение s значения действительного потокового сокета соединения ret = send(s, sendbuff, nBytes, 0);
Возможно, функция send сообщит об отправке менее 2048 байт. Переменная ret будет содержать количество переданных байт, поскольку система выделяет определенное количество буферного пространства на отправку и прием сообщений для каждого сокета. При отправке данных внутренние буферы удерживают их до момента отправки непосредственно по проводу, фичиной неполной отправки может быть, например, передача большого количества данных, при этом все буферы слишком быстро заполнятся.
В TCP/IP также существует так называемый размер окна. Принимающая торона регулирует его, указывая количество данных, которое способна при-
ь.При переполнении данными получатель может задать нулевой размер
КН а ' ч т обы справиться с поступившими данными. Это приведет к приоста-
овке отправки данных, пока размер окна не станет больше 0. В нашем слу-
еРазмер буфера может оказаться равным 1024 байтам, следовательно, по-
Руется повторно отправить оставшиеся 1024 байта. Отправку всего содер- °го буфера обеспечит следующий фрагмент программы:
1 74 |
ЧАСТЬ II Интерфейс прикладного программирования Winsock |
char sendbuff[2048]; int nBytes = 2048,
nLeft,
ldx;
//Заполнение буфера sendbuff 2048 байтами данных
//Присвоение s значения действительного потокового сокета соединения nLeft = nBytes;
idx=0;
while (nLeft > 0)
{
|
ret = send(s, &sendbuff[idx], nLeft, 0); |
|
if (ret == SOCKET_ERROR) |
|
{ |
fy_ |
// Ошибка |
"" l
-Як'
nLeft -= ret; idx += ret;
}
Все, сказанное далее справедливо и для приема данных на потоковом сокете, но для нас это не очень важно. Приложение не знает, сколько данных оно в очередной раз прочтет на потоковом сокете. Если вам нужно отправить дискретные сообщения по потоковому протоколу, это не составит труда. Например, если у всех сообщений одинаковый размер (512 байт), то прочитать их можно так:
char |
recvbuff[1024]; |
int |
ret, |
|
nLeft, |
|
idx; |
nLeft = 512; idx=0;
while (nLeft > 0)
{
ret = recv(s, &recvbuff[idx], nLeft, 0); if (ret == SOCKET_ERROR)
{
// Ошибка
}
idx += ret; nLeft -= ret;
}
Ситуация несколько усложнится, если размер сообщений будет варьироваться. Тогда потребуется реализовать собственный протокол, сообщающий получателю о размере поступающего сообщения. Пусть первые 4 байта сообщения указывают его размер в виде целого числа. Преобразовав их в число при чтении, приемник узнает длину сообщения.
ГЛАВА 7 Основы Winsock |
175 |
Комплексный ввод-вывод
Впервые принцип комплексного ввода-вывода (Scatter-Gather I/O) был применен в функциях recv и writev сокетов Беркли (Berkeley Sockets). В Winsock 2 его поддерживают функции WSARecv, WSARecvFrom, WS - Sendи WSASenaTo. Комплексный ввод-вывод наиболее полезен для п; i- ложений, отправляющих и принимающих данные в специфическом формате. Например, если передаваемые клиентом серверу сообщения должны состоять из фиксированного 32-байтного заголовка, определяющего некие действия, 64-байтного блока данных и заканчиваться 1б-байтной контрольной суммой. В этом случае функция WSASend может быть вызвана для массива соответствующих трех структур WSABUF. На принимающей стороне одним из входных параметров вызываемой функции WSARecvтакже должны бытьтри структуры WSABUF, содержащие 32,64 и 16 байт.
При использовании потоковых сокетов операции комплексного ввода-вывода просто интерпретируют несколько буферов данных как один непрерывный. Функция принятия данных может завершиться раньше, чем будут заполнены все буферы. В сокетах, ориентированных на передачу сообщений, каждая операция получения данных принимает одно сообщение, длина которого не больше размера буфера. Если в буфере недостаточно места, вызов заканчивается ошибкой WSAEMSGSIZE, и данные усекаются до размера доступного пространства. Разумеется, в протоколах, поддерживающих фрагментарные сообщения, для предотвращения потери данных можно использовать флаг
MSGPARTIAL.
Завершение сеанса
По окончании работы с сокетом необходимо закрыть соединение и освободить все ресурсы, связанные с описателем сокета, вызвав функцию dosesocket. Впрочем, ее неправильное использование может привести к потере данных. Поэтому перед вызовом dosesocket сеанс нужно корректно завершить фун-
кцией shutdown.
Функцияshutdown
Правильно написанное приложение уведомляет получателя об окончании отправки данных. Так же должен поступить и узел. Такое поведение называется корректным завершением сеанса и осуществляется с помощью функции shutdown-.
int shutdown( SOCKET s, int how
Параметр how может принимать значения SD_RECEIVE, SD_SEND или SD_ BOTH. Значение SD_RECEIVE запрещает все последующие вызовы любых
1 76 |
ЧАСТЬ II Интерфейс прикладного программирования Winsock |
функций приема данных, на протоколы нижнего уровня это не действует Если в очереди ТСР-сокета есть данные, либо они поступают позже, соединение сбрасывается. UDP-сокеты в аналогиной ситуации продолжают принимать данные и ставить их в очередь. SDJSEND запрещает все последующие вызовы функций отправки данных. В случае ТСР-сокетов после подтверждения получателем приема всех отправленных данных передается пакет FIN Наконец, SDBOTH запрещает как прием, так и отправлку.
Функцияclosesocket
Эта функция закрывает сокет. Она определена так: int closesocket (SOCKET s);
Вызов closesocket освобождает дескриптор сокета, и все дальнейшие операции с сокетом закончатся ошибкой WSAENOTSOCK. Если не существует других ссылок на сокет, все связанные с дескриптором ресурсы будут освобождены, включая данные в очереди.
Ожидающие асинхронные вызовы, исходящие от любого потока данного процесса, отменяются без уведомления. Ожидающие операции перекрытого ввода-вывода также аннулируются. Все выполняющиеся события, процедура и порт завершения, связанные с перекрытым вводом-выводом, завершатсяошибкойWSA_OPERATION_ABORTED.(Асинхронныеинеблокирующие модели ввода-вывода более подробно обсуждаются в главе 8.) Другой фактор, влияющий на поведение функции closesocket, — значение параметра сокета SOJLINGER (его полное описание — в главе 9).
Пример
Разнообразие функций отправки и приема данных удивительно. Но в действительности большинству приложений для приема информации требуют- сятолькоrecvилиWSARecv,адляотправки—sendилиWSASend.Другиефунк- ции обеспечивают специальные возможности и редко используются (или поддерживаются только транспортными протоколами).
Теперь рассмотрим небольшой пример клиент-серверного взаимодействия с учетом описанных принципов и функций. В листинге 7-1 приведен код простого эхо-сервера. Приложение создает сокет, привязывает его к локальному IP-интерфейсу и порту, и слушает соединения клиентов. После приема от клиента запроса на соединение создается новый сокет, который передается клиентскому потоку. Поток читает данные и возвращает их клиенту.
Листинг 7-1. Эхо-сервер
//Имя модуля: Server.с
//Описание:
//Это пример простого сервера TCP, принимающего
//соединения клиентов. После установления соединения
//порождается процесс, который получает данные клиента
//и отправляет их обратно (если параметр возврата данных
//не выключен).
Г Л А В А 7 Основы Winsock |
177 |
Листинг 7-1. (продолжение)
II
II Параметры компиляции:
//cl -о Server Server.с ws2_32.1ib
//Параметры командной строки:
//server [-p:x] [-i:IP] [-о]
// |
-р:х |
Номер порта, |
на котором будут прослушиваться соединения |
ц |
-i:str |
Интерфейс, на |
котором будут прослушиваться соединения |
//-о Только приенимать данные, не возвращая их клиенту
//
«include <wmsock2.h>
«include <stdio.h> «include <stdlib.h>
«define |
DEFAULT_PORT |
5150 |
«define |
DEFAULT_BUFFER |
4096 |
**
HM
int |
|
iPort |
|
= DEFAULT_PORT; // Порт |
прослушивания |
клиентов |
|
BOOL |
blnterface |
= |
FALSE, |
// Прослушивать указанный интерфейс |
|||
|
|
bRecvOnly |
= |
FALSE; |
// Только прием |
данных |
|
char |
szAddress[128]; |
// Интерфейс |
прослушивания |
клиентов |
|||
// |
Функция: usage |
|
p |
|
|
|
|
// |
|
|
|
|
|
|
|
// |
Описание: |
|
^ |
|
|
|
|
// |
|
Выводит сведения о параметрах командной строки и выходит |
|||||
// |
|
|
|
|
|
|
|
void |
usage() |
|
41 |
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
printf("usage: server [-p:x] [-1:IP] [-o]\n\n"); |
|
||||
|
|
printf(" |
-p;x |
Port number to listen on\n"); |
|
||
|
|
printf(" |
-i:str |
Interface to listen on\n"); |
|
||
|
|
printf(" |
-o |
Don't echo the data back\n\n"); |
ExitProcess(i);
//«Пункция: ValidateArgs
//Описание:
//Анализирует параметры командной строки и задает
/некоторые глобальные флаги для указания выполняемых действий
*Oid V a l i d ateArgs(int argc, char |
..argv) |
|
|
|
;(0 |
Mi |
-U0 .ii^dst |
,»oo«)voe |
|
nt i; |
sNMeuqeei |
-a<|0» |
\\ |
(0 «• |
;Мвсм.след.стр.