суббота, 19 ноября 2016 г.

Структура сетевого приложения на примере загрузки файла с веб-сервера


В настоящее время существует много библиотек высокого уровня, которые позволяют программистам манипулировать сетевыми функциями. Приложения, которые их используют, в основном обращаются к интерфейсам прикладного уровня ОС (например WinHTPP в MS Windows). Однако для понимания принципов работы сети необходимо знать как все работает и на более низких уровнях. Канальный и сетевой уровни обычно реализуются аппаратно, и особого интереса не представляют. В данной статье я опишу сетевое приложение, обращающееся непосредственно к модулям транспортного уровня ОС.
Транспортный уровень - уровень модели OSI, который отвечает за доставку данных от одного приложения к другому с заданным уровнем надежности. Этот уровень особенно интересует программистов. Существует две модели транспортного уровня - датаграммнаяпередача и передача с установлением соединения. Датаграммы - небольшого размера сообщения (обычно служебные), которыми обмениваются приложения. Например протокол DNS использует датаграммы. И протокол DHT (в файлообменных сетях) тоже их использует. Но при передаче датаграмм невозможно восстановление поврежденных данных, поэтому при передаче файлов их не используют. Техника передачи с установлением соединения используется в протоколах HTTP, FTP, BitTorrent и др. Суть ее в том, что узлы перед процессом передачи данных устанавливают соединение, а в процессе передачи потерянные пакеты восстанавливаются. Вся прелесть этой техники в том, что программист может не париться, как именно пакеты восстанавливаются: важно то, что все пакеты дойдут именно в нужной последовательности (кроме случаев разрыва соединения). В стеке TCP/IP протокол TCP реализует эту технику.
Программный интерфейс транспортного уровня называется sockets (Он есть в разных системах, конкретно для Windows это Winsock). Этот интерфейс предоставляет программистам функции, которые работают независимо от протоколов и сетевого оборудования (даже от системы адресации). И это круто! Например функции getaddrinfo можно передать как доменное имя, так и NetBIOS имя компьютера - и все будет одинаково работать. В принципе процесс передачи данных через протоколы разной технологии почти не отличается в Winsock - например в протоколе UDP можно вызвать функцию connect , хотя реально соединение не устанавливается. Вместе с тем можно писать приложения ориентируясь и на конкрентный протокол. Словом, этот интерфейс очень гибкий. Но написание приложений в его среде довольно сложно. Поэтому на практике часто применяются другие интерфейсы. Но мы рассмотрим его с теоретической точки зрения, чтобы увидеть "внутренности" работы сетевых приложений.
Приложение WinSock тебует подключения библиотеки импорта ws2_32.lib (Это в среде разработки VisualStudio). Функции WinSock хранятся в библитеке ws2_32.dll .

Далее разбирается приложение, загружающее файл с веб-сервера по протоколу HTTP и отображающее его в экране консоли. Точнее оно отображает полный ответ сервера, включая заголовок. Интерес этого приложения также в том, что оно позволяет увидеть "своими глазами" ответы сервера, которые обычно скрыты. В начале приложения мы включаем заголовочные файлы.

Код:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>

Объявление переменных:

Код:
WSADATA wsaData; //для инициализации сетевой подсистемы
char rq[]=//строка с запросом
"GET / HTTP/1.1\r\n\
Host: ya.ru\r\n\
User-Agent: Mozilla/4.05 (WinNT; 1)\r\n\
Accept: */*\r\n\r\n\
";
char buf[5000]="";//переменная для хранения ответа сервера
Здесь в строковой константе задан HTTP-запрос GET. Путь к документу обозначен "/", что значит главную страницу сайта. В качестве параметров задается версия протокола, имя узла, браузер - на всякий случай говорим что мы Mozilla Firefox. Параметр Accept: */* задает что мы принимаем любой тип данных.

Теперь сама точка входа:

Код:
int main()
{
int iResult;
struct addrinfo *result = NULL;
struct addrinfo hints;
SOCKET ConnectSocket = INVALID_SOCKET;

// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
system("PAUSE");
return 1;
}
ZeroMemory( &hints, sizeof(hints) );
hints.ai_family = AF_UNSPEC; //любое семейство адресов
hints.ai_socktype = SOCK_STREAM;//тип сокета
hints.ai_protocol = IPPROTO_TCP;//протокол TCP
iResult = getaddrinfo("ya.ru", "http", &hints, &result);//разрешение доменного имени
if (iResult != 0) {
printf("getaddrinfo failed: %d\n", iResult);
system("PAUSE");
WSACleanup();
return 1;
}

В данной части происходит инициализация сетевой подсистемы, а затем разрешение доменного имени узла - в нашем случае "ya.ru" - в сетевой адрес. На этом этапе происходит запрос к DNS-серверу. В структуре hints мы задаем желаемые требования - семейство адресов Ipv4, Ipv6 или любое; тип сокета - для нашего случая это сокет-поток, который соответствует протоколу TCP (для других протколов мог быть тип датаграмного сокета). Ну и задаем непосредственно протокол. Хотя это параметры необязательные - и так наверно все будет работать! У функции getaddrinfo второй параметр - номер порта или проткола, у нас "http", стандартный поpт 80. От DNS-сервера нам приходит IP-адрес сервера ( вс перемнной result), и теперь мы готовы соединится с ним. Для соединения мы создаем специальный объект - сокет. Фактически это набор ресурсов, связанных с сетевым соединением.

Код:
ConnectSocket=socket(result->ai_family,result->ai_socktype,result->ai_protocol);//создание сокета

if (ConnectSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
system("PAUSE");
freeaddrinfo(result);
WSACleanup();
return 1;
}
iResult=connect(ConnectSocket,result->ai_addr,(int)result->ai_addrlen); //соединение с сервером
if (iResult == SOCKET_ERROR) {
iResult=WSAGetLastError();
printf("\nConnect error: %i\n",iResult);
system("PAUSE");
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
}
freeaddrinfo(result);//информация об адресе нам больше не нужна

Вот мы соединились с сервером. Теперь посылаем запрос.

Код:
// Send an initial buffer
iResult = send(ConnectSocket, rq, (int) strlen(rq), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
system("PAUSE");
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("\nSending a request - bytes Sent: %ld\n", iResult);
// shutdown the connection for sending since no more data will be sent
// the client can still use the ConnectSocket for receiving data
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
system("PAUSE");
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
Функцией shutdown мы сигнализируем серверу, что посылка данных завершена, и мы ждем ответа. Ее вызов можно опустить (иногда он даже вреден). Теперь мы получаем ответ порциями по 5 кб и выводим на экран. Когда сервер закрывает соединение, выходим из цикла.

Код:
do {
iResult = recv(ConnectSocket, buf, 5000, 0);//прием данных
if (iResult > 0){ //данные успешно приняты
//Start receiving answer
printf("Server Answered.Bytes received: %d\n\n", iResult);

printf("%s",buf);
system("PAUSE");

}
else if (iResult == 0) //соединение закрыто сервером
printf("Connection closed\n");

else //произошла ошибка
printf("recv failed: %d\n", WSAGetLastError());
} while (iResult > 0);

system("PAUSE");
return 0;
}

Пример ответа сервера:



Вот и все. Попробуйте усовершенствовать это приложение, чтобы оно сохраняло данные на жесткий диск. На основе него можно написать целый менеджер закачек. Ах да, еще - отключите сетевой экран и брендмауэр перед тестированием приложения, а то они почему-то могут его блокировать. Видимо считается хакерские технологии...
Таким образом, прочитав эту статью, вы знаете как устроено простейшее клиент-серверное сетевоe приложение, надеюсь это вам поможет в понимании архитектуры современных информационных систем.

Комментариев нет:

Отправить комментарий