Программирование в сетях Windows
.pdf1 УЬ |
ЧАСТЬ II Интерфейс прикладного программирования Winsock |
Первый параметр — сокет для соединения, два последних — указатель на структуру SOCKADDR базового протокола и ее длина. Для сокетов дейтаграмм данная функция возвращает адрес, переданный вызову соединения (за исключением адресов, переданных в вызов sendto или WSASendTd).
Функцияgetsockname
Эта функция противоположна getpeernctme и возвращает адресную информацию для локального интерфейса определенного сокета:
int getsockname( SOCKET s,
struct sockaddr FAR* name, int FAR* namelen
Используются те же параметры, что и для getpeername, однако возвращается информация о локальном адресе. В случае TCP адрес совпадает с сокетом сервера, слушающим на заданном порте и IP-интерфейсе.
ФункцияWSADuplicateSocket
Данная функция применяется для создания структуры WSAPROTOCOLJNFO, которую можно передать другому процессу, что позволит ему открыть описатель того же базового сокета и оперировать данным ресурсом. Заметьте: такая необходимость возникает только между процессами. Потоки в одном и том же процессе могут свободно передавать описатели сокета. Функция определена так:
i n t WSADuplicateSocket( |
уь". |
••n\'{i\ |
SOCKET s, |
|
|
DWORD dwProcessId, |
|
,{"$Mb~ |
LPWSAPR0T0C0L_INF0 |
lpProtocolInfo |
|
); |
|
|
Первый параметр — копируемый описатель сокета. Второй — dwProcessId, код процесса, коорый будет использовать скопированный сокет. Третий па- раметр—lpProtocolInfo,указательнаструктуруWSAPROTOCOLJNFO,которая содержит информацию, необходимую целевому процессу для открытия копии описателя. Некоторые виды межпроцессного взаимодействия должны происходить таким образом, чтобы текущий процесс мог передать структуру WSAPROTOCOLJNFO целевому, который затем использует ее для создания описателя сокета (при помощи функции WSASockef).
Описатели в обоих сокетах можно использовать для ввода-вывода независимо, однако Winsock не обеспечивает контроля за доступом, поэтому программисту необходимо предусмотреть некие способы синхронизации. Все сведения о состоянии любого сокета хранятся в одном месте для всех его описателей, так как копируются описатели сокета, а не сам сокет. Например, любой параметр сокета, заданный функцией setsockopt для одного из описателей, затем можно увидеть, вызвав функцию getsockopt для любого другого его описателя. Если процесс вызывает closesocket для копии сокета, описатель
ГЛАВА 7 Основы Winsock |
nay |
в данном процессе освобождается, однако сокет останется открытым, пока функция closesocket не будет вызвана для последнего его описателя.
Кроме того, учтите общие особенности сокетов при уведомлении с использованием WSAAsyncSelect и WSAEventSelect. Эти функции применяются для асинхронного ввода-вывода (см. главу 8). При их вызове с любым из общих описателей отменяется регистрация любого предыдущего события для сокета, независимо от того, какой описатель применялся для регистрации. Поэтому, например, через общий сокет нельзя доставить события FDREAD процессу А и события FDJWRITE процессу В. Если необходимо передавать уведомления о событиях по обоим описателям, измените конструкцию приложения, использовав потоки вместо процессов.
ФункцияTransmitFile
Это расширение Microsoft позволяет быстро передавать данные из файла. Высокая эффективность обусловлена тем, что вся передача данных происходит в режиме ядра. Если приложение считывает блок данных из файла, а затем вызывает send или WSASend, происходят многократные переключения между режимами ядра и пользовательским. При вызове TransmitFile весь процесс чтения и отправки выполняется в режиме ядра. Функция определена таю
BOOL TransmitFile( SOCKET hSocket, HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend, LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, DWORD dwFlags
);
Параметр hSocket — подключенный сокет, по которому будет передан файл. Параметр hFile — описатель открытого файла. Параметр nNumberOjBytesToWrite задает количество записываемых из файла байт. Если он равен О, файл отправляется целиком. Параметр nNumberOfBytesPerSend задает размер отправляемых блоков данных для операций записи. Например, если он равен 2048, TransmitFile передаст файл порциями по 2 кб; если 0 — используется стандартный размер отправки. Параметр lpOverlapped определяет структуру OVERLAPPED, применяемую в перекрытом вводе-выводе (см. также главу 8).
Следующий параметр — lpTransmitBuffers, представляет собой структуру TRANSMIT'JFILEBUFFERS, содержащую данные, которые нужно отправить до и после передачи файла:
typedef struct _TRANSMIT_FILE_BUFFERS { PVOID Head;
DWORDHeadLength;
PVOID Tail; DWORD TailLength;
> TRANSMIT_FILE_BUFFERS-
200 ЧАСТЬ И Интерфейс прикладного программирования Winsock
Поле Head — указатель на данные, которые отправляются перед передачей файла Поле HeadLength задает количество заранее передаваемых данных Поле Tail ссылается на данные, отправляемые после передачи файла В TailLength указано количество передаваемых затем байт
Последний параметр — dwFlags, управляет режимами работы TransmitFtle Вот описание используемых в нем флагов
ШTF_DISCONNECT — инициирует закрытие сокета после передачи данных
ШTF_KEUSE_SOCKET — позволяет повторно использовать в функции АсceptEx описатель сокета в качестве клиентского сокета
К TFUSEDEFAULT WORKER и TF USE SYSTEM THREAD - указывают, что передача должна идти в контексте стандартного системного процесса Этот флаги полезны при передаче больших файлов
•TF_USE_KERNEL_APC — указывает, что передача должна выполняться ядром при помощи асинхронных вызовов процедур (Asynchronous Procedure Call, APC) Это существенно увеличивает производительность, если для считывания файла в кэш требуется лишь одна операция чтения
•TFWRITEBEHIND — указывает, что TransmitFile может завершиться, не получив подтверждений о приеме данных от удаленной системы
ДляплатформыWindowsСЕ
Вся информация из предыдущих разделов в равной степени относится к Windows СЕ Исключение — функции, специфичные для Winsock 2, поскольку Windows СЕ опирается на спецификацию Winsock 1 1 например WSA-раз- новидностей функций В Windows СЕ доступны только следующие WSAфункции WSAStartup, WSACleanup, WSAGetLastError и WSAIoctl Мы уже обсуждали первые три из них, а о последней поговорим в главе 9
Windows СЕ поддерживает протокол TCP/IP, следовательно, у вас есть до-j ступ как к TCP, так и к UDP Помимо TCP/IP поддерживаются инфракрасны^ сокеты Протокол IrDA поддерживает только потоковые соединения П использовании обоих протоколов выполняют все обычные API-вызовы Win4 sock I 1 для подготовки сокетов и передачи данных Необходимо учитывать ошибку в Windows СЕ 2 0, связанную с дейтаграммными UDP-сокетами вызов функций send или sendto влечет утечки памяти ядра Эта ошибка исправлена в Windows СЕ 2 1, но из-за того, что ядро записано в ПЗУ, в Windows СЕ 2 0 невозможно устранить данную проблему при помощи распространяемых программных обновлений Единственное решение — отказаться от использования дейтаграмм в Windows СЕ 2 0
Windows СЕ не поддерживает консольные приложения и использует только кодировку UNICODE, поэтому примеры, представленные в данной главе, предназначены для Windows 95,98, NT и 2000 Мы приводим их, чтобы дать вам возможность изучить основные концепции Winsock без утомительного рассмотрения программного кода Почти всегда необходим пользовательский интерфейс, если только вы не пишете службу для Windows СЕ — тогда потребуется создать множество дополнительных функций для обработчиков
ГЛАВА 7 Основы Winsock |
201 |
событий окон и других элементов пользовательского интерфейса, разбор которых помешает вам понять главные аспекты применения Winsock
Также существует дилемма использовать функции Winsock, поддерживающие UNICODE, или нет Выбор кодировки лежит на программисте Winsock все равно, что вы передаете функциям лишь бы это был действительный буфер (конечно, нужно привести буфер к соответсвующему типу, чтобы не появлялись предупреждения при компиляции) Если вы приведете строку UNICODE к char ', не забудьте соответственно изменить параметр длины, задающий количество отправляемых байт Для правильного отображения любых отправленных или принятых данных в Windows СЕ необходимо убедиться, что они в кодировке UNICODE Это нужно и для любых других функций Win32, требующих строк в кодировке UNICODE В общем, создание приложений Winsock в Windows СЕ более трудоемко
Для компиляции и запуска приведенных примеров в Windows СЕ потребуется незначительно изменить код Заголовочным файлом будет Winsock h, в отличие от Winsock2 h Функция WSAStartup должна загружать версию Winsock 1 1, потому что она текущяя в Windows СЕ Эта ОС не поддерживает консольных приложений, поэтому необходимо использовать функцию WtnMam вместо тат Не требуется включать окно в приложение, просто не используйте функции консольного текстового ввода-вывода, напримерргш/f
Другие семейства адресов
Все API-функции Winsock, представленные в этой главе, не зависят от протокола Поэтому их легко можно применять и для других протоколов, поддерживаемых платформами Win32 Следующие разделы объясняют примеры клиент-серверного кода для других семейств протоколов, которые находятся на прилагаемом компакт-диске
Протокол AppleTalk
Единственный пример, связанный с AppleTalk, представлен для иллюстрации основных клиент-серверных технологий и поддерживает как РАР, так и ADSP Протокол РАР ориентирован на сообщения, не требует соединения, и не надежен Этим он похож на UDP, но есть и два важных отличия РАР поддерживает фрагментарные сообщения, то есть функция WSARecvEx может вернуть лишь часть дейтаграммного сообщения При этом необходимо проверить значение флага MSG_PARTIAL, чтобы узнать, нужно ли дополнительно вызывать функцию для получения остатков сообщения Кроме того, необходимо задавать специфичные для протокола РАР параметры сокета перед каждым чтением (Подробнее о параметре SOJ4UME_READ, используемом совместно с функцией setsockopt, — в главе 9 ) Ознакомьтесь с примером Atalk с на прилагаемом компакт-диске, где иллюстрируется проверка флага MSGPARTIAL и порядок использования параметра SOPRIMEREAD
Протокол ADSP требует соединения, потоковый, надежный и во многом похож на TCP API-вызовы для AppleTalk практически не отличаются от представленных в этой главе примерах для UDP и TCP Разница лишь в разреше-
202 |
ЧАСТЬ II Интерфейс прикладного программирования Winsock |
нии имен. Помните, что для AppleTalk перед поиском или регистрацией имени необходимо выполнить привязку к пустому адресу (см. главу 6).
У протокола AppleTalk есть ограничение: его поддержка появилась в Winsock 1.1, а когда был разработан Winsock 2, оказалось, что с AppleTalk не работают некоторые новые функции. Использование любой из WSASend- или WSARecv-функций может привести к непредсказуемым результатам, например к возврату отрицательного количества байт. Данная проблема изложена в статье Q164565 базы знаний Microsoft Knowledge. Исключение составляет функция WSARecvEx, которая просто вызывает recv, а в параметре ввода-вывода/я^ можно просмотреть значение флага MSG'PARTIAL после выхода.
Инфракрасные сокеты
Поддержка IrDA появилась недавно, в Windows СЕ, 98 и 2000. Протокол IrDA требует соединения, потоковый и надежный. Снова вопрос в разрешении имен, которое значительно отличается от принятого в IP. Необходимо знать и еще одно отличие: на платформах Windows СЕ можно использовать только функции Winsock 1.1 для инфракрасных сокетов, так как Windows СЕ поддерживает только спецификацию Winsock 1.1. В Windows 98 и 2000 допустимо применение функций, специфичных для Winsock 2. Код примера использует только функции Winsock 1.1. В Windows 98 и 2000 необходимо загрузить библиотеку Winsock 2.2 или более позднюю, потому что поддержка семейства адресов AFJRDA в более ранних версиях не доступна.
Пример кода для инфракрасных сокетов приведен в файлах Ircommon.h, Ircommon.c, Irclient.c и Irserver.c. Первые два файла просто определяют две общие функции: одну — для отправки, другую — для приема данных. Они используются как клиентским, так и серверным приложениями. Клиентская часть подробно расписана в файле Irclient.c. Сначала нумеруются все устройства в зоне видимости. Затем следуют попытки подключиться к каждому из них с заданным именем службы. Дальнейшая работа ведется с первым устройством, принявшим соединение. Клиент отправляет данные и считывает их обратно. Серверная часть описана в файле Irserver.c. Сервер создает инфракрасный сокет, выполняет привязку заданного имени службы к сокету и ожидает подключений клиентов. Для каждого клиента порождается поток для приема данных и отправки их обратно.
Заметьте: данные примеры написаны для Windows 98 и 2000. Подобно примерам по TCP/IP, их нужно немного изменить для запуска в Windows СЕ. Также следует учесть два уже упоминавшихс ограничения Windows СЕ: эта ОС не поддерживает консольные приложения и требует кодировать строки в UNICODE.
ИнтерфейссNetBIOS
В главе 1 мы говорили, что NetBIOS совместима с несколькими разными транспортами, применяемыми в Winsock, а в главе 6 — обсудили, как регистрировать совместимые с NetBIOS транспорты и сокеты на основе любого из них. Для каждой комбинации «протокол — адаптер» есть две записи: SOCK_DGRAM и SOCK_SEQPACKET, соответствующих не требующим соединения дейтаграммам
Г Л А ВА 7 Основы Wmsock |
203 |
и потоковым сокетам, которые весьма похожи на сокеты UDP и TCP. Кроме разрешения имен работа с Winsock-интерфейсом NetBIOS ничем не отличается от описанной в этой главе. Помните, что хорошо написанный сервер должен прослушивать все доступные LANA, а клиент со своей стороны — пытаться установить соединение по всем LANA.
Первые два примера на компакт-диске — Wsnbsvr.c и Wsnbclnt с. Они используют потоковые сокеты SOCKSEQPACKET. Сервер создает сокет для всех LANA, которые нумеруются функцией WSAEnumProtocols, и привязывает его к стандартному имени сервера. После установления клиентом соединения, сервер создает поток для его обслуживания. С этого момента поток просто читает входящие данные и возвращает их клиенту. Аналогично, клиент пытается подключиться по всем LANA. После первого успешного соединения другие сокеты закрываются. Затем клиент отправляет данные серверу, а сервер снова их возвращает.
В файле Wsnbdgs.c приведен код для отправки и приема дейтаграммных сообщений (через сокет с типом SOCKJDGRAM). Этот протокол не требует соединения, поэтому для отправки сообщений серверу используются все доступные транспорты, так как заранее неизвестно, какие из них поддерживает сервер. Кроме того, в примере реализована поддержка уникальных, групповых и широковещательных сообщений (см. главу 1).
ПротоколIPX/SPX
В файле Sockspx.c приведен пример использования протокола IPX, а также потокового и последовательного SPXII. В одном примере реализован отправитель и приемник для всех трех протоколов. Конкретный протокол задается при помощи параметра -р в командной строке. Пример прост и легок в освоении. Функция main анализирует аргументы, а затем вызывает функцию Server или Client. Для протокола SPXII, ориентированного на соединения, это значит, что сервер привязывает сокет к адресу внутренней сети и ожидает соединений с клиентом, который, в свою очередь, пытается установить соединение с указанным в командной строке сервером. После установления соединения отправка и прием данных идут обычным образом.
Для не требующего соединения протокола IPX пример еще проще. Сервер просто выполняет привязку к внутренней сети и ожидает входящие данные, вызывая функцию recvfrom. Клиент отправляет данные получателю, указанному в командной строке, функцией sendto.
Два раздела этого примера требуют разъяснения. Функция FilllpxAddress отвечает за кодирование указанного в командной строке в кодировке ASCII IPX-адреса в структуру SOCKADDRJPX. Как упоминалось в главе б, IPX-адре- са представляются в виде шестнадцатеричных строк, то есть каждый символ адреса фактически занимает 4 бита в адресных полях структуры SOCKADDRJPX. Функция FilllpxAddress получает IPX-адрес и вызывает функцию AtoH, которая и выполняет преобразование.
Еще одна функция — EnumerateAdapters, выполняется, если в командной строке задан флаг —т. Для подсчета количества доступных локальных IPXадресов она использует параметр сокета IPX_MAX_ADAPTER_NUM, а затем для
2 04 ЧАСТЬ II Интерфейс прикладного программирования Winsock
получения каждого адреса вызывает параметр сокета IPXADDRESS. Мы используем их, потому что в нашем примере IPX-адреса заданы напрямую, разрешение имен не выполняется (подробнее — в главах 9 и 10).
ПротоколATM
Протокол ATM доступен в Winsock под управление Windows 98 и 2000. Пример для ATM состоит из файлов Wsockatm.c, Support.c, and Support.h. Два последних файла содержат вспомогательные подпрограммы, используемые в Wsockatm.c и обеспечивающие регистрацию локальных ATM-адресов и их перекодирование. ATM-адреса шестнадцатеричные, как и IPX-адреса, поэтому мы применяем уже знакомую функцию AtoH. Для получения количества локальных АТМинтерфейсовиспользуетсякомандаSIO_GET_NUMBERJDF_ATM_DEV1CES,азатем при помощи команды SIO_GET_ATM ADDRESS мы получаем фактический адрес (подробнее — в главе 9)-
В отличие от описанных ранее примеров, клиентское и серверное приложение представлено одним файлом — Wsockatm.c. Так как ATM поддерживает только подключения, требующие соединения, пример небольшой и большинство кода находится в функции main. Сервер выполняет привязку к определенному локальному интерфейсу и ожидает подключений клиентов, которые обрабатываются в том же потоке, что и слушающий сокет. Это означает, что сервер может одновременно обслуживать только одного клиента. Клиент вызывает connect с ATM-адресом сервера. После установления соединения начинается обмен данными.
А теперь несколько предостережений. После вызова сервером функции WSAAccept будет выведен адрес клиента. Однако к моменту получения сервером запроса на соединение адрес клиента еще неизвестен, потому что функция accept не устанавливает соединение до конца. Это справедливо и для клиентской стороны — клиентский вызов для установления соединения с сервером признается успешным, даже если соединение до конца не установлено. Это означает, что после вызова connect или accept немедленная отправка может сорваться без выдачи предупреждений. К сожалению, приложение не способно узнать, когда соединение полностью готово к использованию. Вдобавок, ATM поддерживает только резкое завершение: при вызове функции closesocket соединение немедленно разрывается. Для протоколов, не поддерживающих плавное завершение, при вызове closesocket данные, ожидающие в очереди на любом конце соединения, обычно отбрасываются. Это вполне приемлемое поведение. Впрочем, поставщик ATM действует предусмотрительно: при закрытии одной из сторон своего сокета во время передачи данных, Winsock все же возвращает данные из приемной очереди этого сокета.
Резюме
В этой главе обсуждались основные функции Winsock, необходимые для обмена данными по различным протоколам. Мы предоставили данную информацию в контексте только одной модели ввода-вывода: блокирующих сокетов. В следующей главе мы рассмотрим другие модели ввода-вывода, доступные в Winsock.
Г Л А В А
Ввод-вывод в Winsock
Эта глава посвящена управлению вводом-выводом через сокеты в Windowsприложениях. В Winsock такое управление реализовано с помощью режимов работы и моделей ввода-вывода. Режим (mode) сокета определяет поведение функций, работающих с сокетом. Модель (model) сокета описывает, как приложение производит ввод-вывод при работе с сокетом. Модели не зависят от режима работы и позволяют обходить их ограничения.
Winsock поддерживает два режима: блокирующий (blocking) и неблокирующий (nonblocking). Эти режимы подробно описаны в начале главы, здесь же демонстрируется их использование в приложениях для управления вво- дом-выводом. Далее приведен ряд интересных моделей, которые помогают приложению управлять несколькими сокетами одновременно в асинхронном режиме: select, WSAAsyncSelect, WSAEventS'elect, перекрытый ввод-вывод
(overlapped I/O) и порт завершения (completion port). В конце главы рассматриваются достоинства и недостатки различных режимов и моделей, а также обсуждается выбор вариантов, наиболее подходящих для конкретных случаев.
Все платформы Windows поддерживают блокирующий и неблокирующий режимы работы сокета. Впрочем, конкретная платформа может поддерживать не все модели. В Windows СЕ доступна лишь одна модель ввода-вы- вода, в Windows 98 и Windows 95 — большинство моделей, кроме портов завершения (поддержка конкретной модели зависит от версии Winsock). В Windows NT и Windows 2000 доступны все модели (табл. 8-1).
Табл. 8-1. Поддерживаемые модели ввода-вывода
Платформа |
|
Select |
WSA- |
WSА- |
Перекрытый |
Порт |
|
|
Async |
Event |
ввод- |
Port |
завершения |
|
|
|
Select |
Select |
вывод |
|
WindowsСЕ |
Да |
Нет |
Нет |
Нет |
Нет |
|
Windows 95 |
(Winsock 1) |
Да |
Да |
Нет |
Нет |
Нет |
Windows 95 |
(Winsock 2) |
Да |
Да |
Да |
Да |
Нет |
Windows 98 |
|
Да |
Да |
Да |
Да |
Нет |
Windows NT |
Да |
Да |
Да |
Да |
Да |
|
Windows 2000 |
Да |
Да |
Да |
Да |
Да |
|
|
|
|
|
|
|
|
2 06 |
ЧАСТЬ II Интерфейс прикладного программирования Winsock |
Режимы работы сокетов
В блокирующем режиме функции ввода-вывода, такие как send и recv, перед завершением ожидают окончания операции. В неблокирующем — работа функций завершается немедленно. Приложения, выполняемые на платформах Windows СЕ и Windows 95 (в случае Winsock 1), поддерживают очень мало моделей ввода-вывода и требуют от программиста описать блокирование и разблокирование сокетов в разных ситуациях.
Блокирующий режим
При блокировке сокета необходима осторожность, так как этом режиме любой вызов функции Winsock именно блокирует сокет на некоторое время. Большинство приложений Winsock следуют модели «поставщик — потребитель», в которой программа считывает или записывает определенное количество байт и затем выполняет с ними какие-либо операции (листинг 8-1).
Листинг 8-1. Простейший пример блокировки сокета
SOCKET |
sock; |
char |
buff[256]; |
int |
done = 0; |
while(ldone)
nBytes = recv(sock, buff, 65); if (nBytes == S0CKET_ERR0R)
printf("recv failed with error Xd\n" WSAGetLastErrorQ);
Return;
OoComputationOnData(buff);
}
Проблема в том, что функция recv может не завершиться никогда, так как для этого нужно считать какие-либо данные из буфера системы. В такой ситуации некоторые программисты могут соблазниться «подглядыванием» данных (чтение без удаления из буфера), используя флаг MSG_PEEK в recv или вызывая ioctlsocket с параметром FIONREAD. Подобный стиль программирования заслуживает резкой критики. Издержки, связанные с «подглядыванием», велики, так как необходимо сделать один или более системных вызовов для определения числа доступных байт, после чего все равно приходится вызывать recv для удаления данных из буфера.
Чтобы этого избежать, следует предотвратить замораживание приложения из-за недостатка данных (из-за сетевых проблем или проблем клиента)
Г Л А ВА 8 Ввод-вывод в Wmsock |
207 |
без постоянного «подглядывания» в системные сетевые буферы. Один из методов — разделить приложения на считывающий и вычисляющий потоки, совместно использующие общий буфер данных. Доступ к буферу регулируется синхронизирующим объектом, таким как событие или мьютекс. 31дача считывающего потока — постоянно читать данные из сети и поме чать их в общий буфер. Считав минимально необходимое количество данных, этот поток инициирует сигнальное событие, уведомляющее вычисляющий поток, что можно начинать вычисления. Затем вычисляющий поток удаляет часть данных из буфера и производит с ними необходимые операции. В листинге 8-2 реализованы две функции: для приема данных (ReadThread) и
их обработки (ProcessThread).
Листинг 8-2. Пример многопоточного программирования в режиме блокировки
//Перед созданием двух потоков,
//инициализируется общий буфер (data)
//и создается сигнальное событие (hEvent)
CRITICAL.SECTION |
data; |
HANDLE |
hEvent; |
TCHAR |
buff[MAX_BUFFER_SIZE]; |
int |
nbytes; |
// Считывающий поток void ReadThread(void)
{
int nTotal = 0, nRead = 0, nLeft = 0, nBytes = 0;
while (!done)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED;
while (nTotal != NUM_BYTES_REQUIRED)
{
EnterCriticalSection(&data);
nRead = recv(sock, &(buff[MAX_BUFFER_SIZE - nBytes]), nLeft);
if (nRead == -1)
{
pnntf("error\n");
ExitThreadO;
nTotal += nRead; nLeft -= nRead;
nBytes += nRead; |
Ы |
с |