Работа в неблокирующем режиме
По умолчанию, большинство функций, работающих с сокетами, таких как connect, send, recv, accept блокируют выполнение вызвавшего их потока до завершения выполнения операции. При работе с сетью такие задержки могут быть очень большими, а в случае функции accept – вообще потенциально бесконечными, если никто не захочет соединиться с нашим сокетом. Хотя такой режим может быть удобен для написания простейших программ, во многих случаях он оказывается неприемлем. Функция select облегчает ситуацию, однако она не позволяет определить, сколько точно байт доступно для чтения или сколько места имеется в системных буферах отправки данных. Поэтому, если мы запросим больше данных, чем доступно, вызов recv (или send) может все равно быть заблокирован.
Полностью предотвратить блокирование нашего потока мы можем, переведя сокет в неблокирующий (non-blocking) режим. Для этого используется функция fcntl:
#include <fcntl.h> int fcntl(int s, long cmd, ...); |
Эта функция может выполнять различные операции, в зависимости от значения параметра cmd. Для включения неблокирующего режима необходимо установить флаг O_NONBLOCK. Текущее значение флагов может быть прочитано с помощью команды
F_GETFL, а установлено с помощью F_SETFL. Типичный код для переключения сокета в неблокирующий режим выглядит так:
int flag; int sock; // сокет, необходимо сначала создать ... if ((flag = fcntl(sock, F_GETFL, 0)) >= 0) flag = fcntl(sock, F_SETFL, flag | O_NONBLOCK); if (flag < 0) { // ошибка } |
В неблокирующем режиме все функции работы с сокетами возвращают управление немедленно, вне зависимости от того, полностью ли выполнена запрошенная операция. Функции передачи и приема данных в этом режиме вернут число отправленных или переданных данных, которое в данном случае может принимать значение от 0 до запрошенного размера. Программист должен учитывать это и соответствующим образом обрабатывать не полностью выполненные запросы. Завершение установления соединения или поступление нового соединения при работе в неблокирующем режиме может быть проверено с помощью функции select.
Функция setsockopt также позволяет задать несколько параметров, которые
воздействуют на блокирующее поведение функций send и recv.
· SO_RCVTIMEO
Максимальный таймаут для функции recv. Функция не будет блокировать процесс на время, больше установленного этим параметром. По истечению таймаута будет возвращено число принятых байт или установлен код ошибки EWOULDBLOCK, если никакие данные приняты не были.
· SO_RCVLOWAT
Минимальное число байт, получения которого будет ждать блокирующий вызов recv. Значение по умолчанию 1.
· SO_SNDTIMEO
Максимальный таймаут для функции send. Функция не будет блокировать на большее время и вернёт число переданных байт или установит код ошибки EWOULDBLOCK.
· SO_SNDLOWAT
Минимальное число байт для операций вывода. Функция select вернёт сокет как готовый для записи, если возможно без блокирования отправить указанный объём данных.
Пример использования неблокирующего режима и функции select
Рассмотрим пример программы, использующей функцию select и неблокирующий режим работы для одновременной работы с большим количеством сетевых соединений в одном потоке. Наша программа будет представлять собой сервер, перекодирующий поступающий от клиентов текст в верхний регистр и отправляющий его назад.
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> |
Для отправки данных через сокет в неблокирующем режиме потребуется буфер, в который будут помещаться данные для отправки. Поскольку детали реализации буфера не имеют прямого отношения к работе с сетью, он не будет рассмотрен подробно, будем считать, что он реализован в классе со следующим интерфейсом.
#include <string.h> #include <vector> // будем использовать vector для хранения // активных соединений using namespace std; class buffer { public: |
buffer() ~buffer(); bool AddData(void *ptr,int len); // Добавить данные в конец буфера: // ptr – указатель на начало данных // len – размер данных в байтах int ReadySize(); // Возвращает размер данных, доступных для отправки. // Если данных нет, возвращается 0. const char *ReadyData(); // Возвращает указатель доступные для отправки данные. // Размер этих данных возвращается ReadySize(). // Если данных нет, возвращается NULL. void RemoveData(int len); // Удаляет данные из начала буфера. // len – размер данных для удаления, для корректной работы // должен быть <= значения возвращенного ReadySize(). }; |
В сервере будут использоваться два вида сокетов: сокеты для обмена данными с клиентами и один сокет для приёма соединений. Чтобы избежать блокировки все они должны находится в неблокирующем режиме и опрашиваться с помощью функции select. Оба вида сокетов будут инкапсулированы в отдельных классах, а следующий класс будет являться для них базовым, определяя общий интерфейс:
class PollSocket { public: PollSocket() { _sock=INVALID_SOCKET; } |
virtual ~PollSocket() { |
// деструктор – закроем сокет |
if(_sock!=INVALID_SOCKET) closesocket(_sock); |
} |
virtual bool PrepareForSelect(fd_set *read_fs,
fd_set *write_fs, int *max)
// функция должна поместить сокет в набор на чтение или на
// запись в зависимости от необходимости, и скорректировать
// переменную max, в которой находится максимальный номер
// установленного в наборах дескриптора.
// Возвращаемое значение: true – операция выполнена
// false – операция не выполнена, объект надо уничтожить
// (например после закрытия соединения с клиентом).
{ return false; }
virtual bool PerformOperation(fd_set *read_fs,
fd_set *write_fs)
// Выполнить операции чтения или записи если наш сокет
// присутствует в соответствующих наборах.
// Возвращаемое значение: true – операция выполнена
// false – операция не выполнена, объект надо уничтожить
{ return false; }
bool MakeNonBlocking()
// переводит сокет в неблокирующий режим
{
unsigned long op=1;
return ioctlsocket(_sock,FIONBIO,&op)==0;
}
protected:
SOCKET _sock; // сокет, с которым мы работаем.
};
vector<PollSocket *> Sockets;
// динамический массив, в котором будут хранится рабочие сокеты
Теперь приступим к реализации обработчика клиентского соединения. Процедура чтения в этом случае большой сложности не представляет: она будет принимать данные в доступном объеме и выполонять обработку – переводить символы в верхний регистр. Однако с отправкой придётся быть чуть более осторожным – поскольку сокет работает в неблокирующем режиме, мы не можем надеяться что все данные, которые потребуется отправить обратно, удастся сразу же отправить. Поэтому придётся использовать промежуточный буфер для хранения исходящих данных, откуда они и будут по возможности отправлятся.
#define RCV_BUFFER 1024 // размер буфера приёма данных class ClientConnection: public PollSocket { public: ClientConnection(int accept_sock); // конструктор – принимаем соединение // accept_sock – дескриптор сокета, с которого нужно принять // соединение ~ClientConnection(); virtual bool PrepareForSelect(fd_set *read_fs, fd_set *write_fs,int *max); |
// будем регистировать наш сокет на чтение – всегда,
// на запись если имеются данные в выходном буфере
virtual bool PerformOperation(fd_set *read_fs,
fd_set *write_fs);
// будем выполнять операции записи или чтения
protected:
bool DoRead();
// Читаем и обрабатываем данные от клиента.
// Возвращает false если соединение закрыто.
bool DoWrite();
// Отправляем данные из буфера.
buffer _out; // буфер исходящих данных struct sockaddr_in _addr; // адрес клиента
};
ClientConnection::ClientConnection(SOCKET accept_sock)
{
int len=sizeof(_addr);
memset(&_addr,0,sizeof(_addr));
// принимаем входящее соединение
_sock=accept(accept_sock,(sockaddr*)&_addr,&len); if(_sock!=INVALID_SOCKET)
{
// печатаем адрес клиента
printf("Accepted connection from %s:%d\n", inet_ntoa(_addr.sin_addr),ntohs(_addr.sin_port));
}
else
// переводим сокет в неблокирующий режим MakeNonBlocking();
{ // ошибка приёма соединения printf("Accept() failed!\n");
}
}
ClientConnection::~ClientConnection()
{
if(_sock!=INVALID_SOCKET)
{
}
else
// закрываем сокет и печатаем сообщение closesocket(_sock);
printf("Closed connection with %s:%d\n", inet_ntoa(_addr.sin_addr),ntohs(_addr.sin_port));
printf("Closing invalid client object");
}
bool ClientConnection::PrepareForSelect(fd_set *read_fs,
fd_set *write_fs, int * max)
{
// регистрируем наш сокет в наборах для вызова select()
// если у нас неправильный сокет (не сработал accept)
// то объект надо уничтожить – возвращаем false. if(_sock==INVALID_SOCKET) return false;
// регистрируем на запись если есть что отправлять if(_out.ReadySize()>0) FD_SET(_sock,write_fs);
// регистрируем на чтение всегда FD_SET(_sock,read_fs);
// корректируем максимальное значение if(_sock>*max) *max=_sock;
return true; // всё в порядке
}
bool ClientConnection::PerformOperation(fd_set *read_fs,
fd_set *write_fs)
{
// выполняем операции, к которым наш сокет готов if(_sock==INVALID_SOCKET) return false;
// проверяем и выполняем чтение. if(FD_ISSET(_sock,read_fs))
if(!DoRead()) return false; // если соединение закрыто
// наш объект можно удалять
// проверяем и выполняем запись if(FD_ISSET(_sock,write_fs))
DoWrite();
return true;
}
bool ClientConnection::DoRead() // чтение и обработка
{
int len,i;
char buff[RCV_BUFFER];
// принимаем данные с сокета len=recv(_sock,buff,RCV_BUFFER,0);
if(len>0) // данные приняты
{
// обрабатываем – переводим в верхний регистр for(i=0;i<len;i++)
buff[i]=toupper(buff[i]);
// помещаем обработанные данные в буфер отправки
_out.AddData(buff,len); return true; } else // ошибка или соединение закрыто return false; } bool ClientConnection::DoWrite() // отправка данных { int len; // отправляем len=send(_sock,_out.ReadyData(),_out.ReadySize(),0); if(len>0) // len байт отправлено, это может быть меньше, // чем мы попросили (_out.ReadySize()). // Удаляем из буфера отправленное, остальное, // если есть, отправим в следующий раз. _out.RemoveData(len); return len!=SOCKET_ERROR; } |
Класс Acceptor реализует работу с сокетом, принимающим соединения. Он обрабатывает только одну операцию – чтение, которая сигнализирует о поступлении нового соединения. Для обработки соединения будем создавать новый экземпляр класса ClientConnection.
#define SERVER_PORT 3000 |
// номер порта для приёма соединений |
class Acceptor: public PollSocket { public: Acceptor(); // создаем сокет и переводим его в режим приёма ~Acceptor(); virtual bool PrepareForSelect(fd_set *read_fs, fd_set *write_fs,int *max); // регистрируемся в списке на чтение virtual bool PerformOperation(fd_set *read_fs, fd_set *write_fs); // принимаем соединение }; |
Acceptor::Acceptor() { // создаём сокет _sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(_sock==INVALID_SOCKET) { printf("Socket() failed\n"); return; |
}
// разрешаем повторное использование порта BOOL val=TRUE;
setsockopt(_sock,SOL_SOCKET,SO_REUSEADDR,(char*)&val,
sizeof(val));
// закрепляемся за портом struct sockaddr_in addr; memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET;
addr.sin_addr.S_un.S_addr=INADDR_ANY; addr.sin_port=htons(SERVER_PORT);
if(bind(_sock,(sockaddr*)&addr,sizeof(addr))!=0)
{
printf("bind() failed\n"); closesocket(_sock);
_sock=INVALID_SOCKET;
return;
}
// переходим в режим приёма соединений if(listen(_sock,10)!=0)
{
printf("listen() failed\n"); closesocket(_sock);
_sock=INVALID_SOCKET;
return;
}
}
Acceptor::~Acceptor()
{
if(_sock!=INVALID_SOCKET) // закрываем сокет
{
}
else
}
closesocket(_sock);
printf("Closed accepting socket\n"); printf("Closing invalid accepting socket object");
bool Acceptor::PrepareForSelect(fd_set *read_fs,
fd_set *write_fs,int * max)
{
if(_sock==INVALID_SOCKET) return false;
// регистрируемся только на чтение (приём соединения) FD_SET(_sock,read_fs);
if(_sock>*max) *max=_sock;
return true;
}
bool Acceptor::PerformOperation(fd_set *read_fs, fd_set *write_fs) { if(_sock==INVALID_SOCKET) return false; // если сокет готов на чтение значит есть соединение if(FD_ISSET(_sock,read_fs)) { // создаём клиентский объект, он вызовет accept() ClientConnection *client=new ClientConnection(_sock); // заносим клиента в список активных сокетов Sockets.push_back(client); } return true; } |
Теперь все объекты готовы. В функции main остаётся создать объект-обработчик входящих соединений и поместить его в список рабочих.
Далее программа будет в цикле регистрировать рабочие сокеты в списках проверки готовности на чтение и запись и передавать эти списки функции select. Когда она вернёт управление, в списках останутся только те дескрипторы, которые готовы к выполнению запрошенной операции. Программа ещё раз пройдет по списку рабочих объектов и попросит их выполнить необходимые действия.
Если один из объектов вернёт false, его надо будет удалить из списка.
int main(int argc, char* argv[]) { fd_set read_fs,write_fs; // наборы дескрипторов на чтение/запись int max; // максимальный дескриптор в наборе int i; // создаем обработчик входящих соединений Acceptor *a=new Acceptor(); // добавляем его в список Sockets.push_back(a); // Пока у нас есть рабочие объекты работаем. // Так как наш acceptor() никогда не возвращает false // в нормальной ситуации, то выход из этого цикла // возможен только в случае ошибки создания принимающего // сокета while(!Sockets.empty()) { // стираем наборы FD_ZERO(&read_fs); FD_ZERO(&write_fs); max=0; // готовим списки сокетов на чтение и запись for(i=0; i!=Sockets.size() ; i++) { |
if(!Sockets[i]->PrepareForSelect(&read_fs, &write_fs, &max)) { // уничтожаем текущий объект delete Sockets[i]; Sockets.erase(Sockets.begin()+i); i--; // корректируем индекс } } // вызываем select c бесконечным таймаутом select(max,&read_fs,&write_fs,NULL,NULL); for(i=0; i!=Sockets.size() ; i++) { if(!Sockets[i]->PerformOperation(&read_fs, &write_fs)) { // уничтожаем текущий объект delete Sockets[i]; Sockets.erase(Sockets.begin()+i); i--; // корректируем индекс } } } return 0; } |
Работа с протоколом UDP
В отличие от протокола TCP, который подразумевает установку связи между двумя системами, протокол UDP обеспечивает лишь отправку отдельных сообщений (датаграмм). Для работы с протоколом UDP при создании сокета нужно указать тип SOCK_DGRAM и код протокола IPPROTO_UDP. Поскольку соединения в UDP не предусмотрены, то соответственно функции connect, listen и accept для работы с UDP сокетами не используются. Достаточно использовать функцию bind чтобы определить локальный адрес и порт, который будет использоваться для отправки или приема датаграмм.
Связав сокет с портом, можно начинать передавать и принимать данные с помощью функций sendto и recvfrom:
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen); ssize_t recvfrom(int s, void * buf, size_t len, int flags, struct sockaddr * from, socklen_t * fromlen); |
Наряду с параметрами, знакомыми по функциям send и recv, тут можно видеть указатель на структуру sockaddr (вместо которой подставляется sockaddr_in) и её размер. В этой структуре указывается адрес получателя при отправке, а при приеме
система записывает по этому указателю адрес системы, от которой была принята датаграмма. В остальном эти функции действуют так же, как и функции send и recv.
При передаче данных пакетами, необходимо следить, чтобы размер передаваемых
данных не превысил возможностей стека протоколов. Максимальный размер датаграммы можно узнать с помощью функции sysctl с параметром UDPCTL_MAXDGRAM. В качестве размера данных можно передать ноль, в этом случае на адрес получателя отправляется пакет с пустым полем данных.
Работая с протоколом UDP, нужно учитывать, что он не гарантирует получение данных удаленной системой. Также возможно нарушение порядка пакетов при приеме или их дублирование. В случае, если такие явления нежелательны, необходима дополнительная обработка этих ситуаций в программе. С другой стороны, протокол UDP позволяет организовать передачу данных с минимально возможными задержками, что может быть полезным для приложений, для которых необходимо иметь как можно более свежую информацию, возможно даже за счет потери задержавшихся в пути старых данных, как, например, в компьютерных играх.
Пример передачи данных с помощью UDP
В качестве примера работы с UDP протоколом, рассмотрим реализацию сервиса эхо – программы, принимающей UDP датаграммы и отсылающей их назад отправителю.
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #define MAX_LEN 1024 // максимальный размер // принимаемого пакета void main(int argc,char *argv[]) { int s; char buff[MAX_LEN+1]; int len,addr_len; // создаём сокет для приёма соединений // обратите внимание на 2-ой и 3-ий параметры s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); if(s==INVALID_SOCKET) { printf("socket() error\n"); return 1; } // заполняем структуру локальным адресом memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET; addr.sin_port=htons(1234); // порт для приёма соединений addr.sin_addr.S_un.S_addr=INADDR_ANY; |
// используются все локальные интерфейсы // связываем сокет с локальным портом if(bind(s,(sockaddr*)&addr,sizeof(addr))!=0) { printf("bind() failed\n"); closesocket(s); return 2; } // цикл приёма пакетов for(;;) { addr_len=sizeof(addr); // принимаем пакет len=recvfrom(s,buff,MAX_LEN,0, (sockaddr*)&addr, &addr_len); if(len>=0) { // распечатываем принятые данные buff[len]=’\0’; printf(“Datagramm from %s:%d\n%s\n”, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),buff); // посылаем пакет обратно sendto(s,buff,len,0, (sockaddr*)&addr,addr_len); } else { printf(“recvfrom() error\n”); break; } } return 0; } |
Использованная в примере функция inet_ntoa преобразует IP-адрес в текстовую строку в стандартной нотации «a.b.c.d». Обратите внимание на указанные в справке ограничения по времени использования возвращаемого этой функцией значения.
Отключите в сервере отправку ответа и попробуйте послать «ответ» самостоятельно с помощью клиента с другого компьютера. Какая информация вам для этого потребуется?
Реализуйте ожидание ответа от сервера в течение ограниченного времени.
Использование базы данных DNS
До сих пор наши программы могли работать только с IP адресами в цифровом виде. Однако прикладные программы обычно получают от пользователя сетевые адреса в текстовом виде. К счастью, нет необходимости изучать и самостоятельно реализовывать работу с протоколом DNS. Для выполнения преобразования можно воспользоваться функциями, которые имеются в стандартной библиотеке.
Наиболее простой является функция gethostbyname.
#include <netdb.h> int h_errno; struct hostent * gethostbyname(const char *name); |
В качестве единственного параметра передаётся имя компьютера, информацию о котором требуется получить. Результат возвращается в виде указателя на структуру hostent, которая определена следующим образом.
#include <netdb.h> struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0] |
Элементы структуры имеют следующее значение:
h_name
Официальное доменное имя хоста.
h_aliases
Список альтернативных имён, заканчивается элементом NULL.
h_addrtype
Тип возвращаемого адреса, обычно AF_INET.
h_length
Длина возвращенного адреса в байтах.
h_addr_list
Список адресов хоста, в двоичном виде в сетевом порядке байтов. Заканчивается элементом NULL.
h_addr
Первый элемент списка адресов, для совместимости со старым кодом.
В случае ошибки gethostbyname возвращает значение NULL и помещает код ошибки в глобальную переменную h_errno. Предусмотрены четыре возможных варианта:
HOST_NOT_FOUND
Указанное имя не существует.
TRY_AGAIN
Временная ошибка, можно попробовать повторить запрос позже. Возможной причиной может являться отсутствие ответа авторизованного DNS сервера.
NO_RECOVERY
Произошла ошибка, повторный запрос не поможет.
NO_DATA
Для указанного адреса отсутствует запись об IP адресе. Такое происходит, если в базе данных первичного DNS сервера для данного хоста не указана запись с IP адресом.
Возвращаемый этой функцией адрес является указателем на внутреннюю переменную библиотеки. Благодаря использованию локальной памяти потока(TLS) использование функции gethosbyname в многопоточном приложении безопасно. Однако необходимо скопировать возвращенные значения, так как следующий вызов этой функции изменит сохранённую в структуре информацию. Не следует каким-либо образом модифицировать записи в структуре или пытаться освободить занимаемую ей память.
Наряду с наличием символьных имён для хостов, имеются и база данных для текстовых названий служб и соответствующих им номеров портов. На UNIX-системах эта информация о соответствии хранится в файле /etc/services24. Преобразовать имя службы в
номер порта можно с помощью функции getservbyname.
#include <netdb.h> struct servent * getservbyname(const char *name, const char *proto); struct servent { |
char char int char }; |
*s_name; **s_aliases; s_port; *s_proto; |
/* официальное имя */ /* список синонимов */ /* номер порта */ /* имя протокола */ |
Работа c этой функцией сходна с использованием функции gethostbyname. В параметре name задаётся имя сервиса, которое необходимо преобразовать в порт, а в параметре proto – название используемого протокола. Если в качестве proto передать NULL, то будут возвращены записи для всех протоколов.
Существуют и функции, выполняющие обратное преобразование: функция
gethostbyaddr позволяет получить имя хоста по его IP адресу, а getservbyport – узнать имя сервиса по номеру порта.
#include <netdb.h> struct servent * getservbyport(int port, const char *proto); struct hostent * gethostbyaddr(const void *addr, socklen_t len, int type); |
Более развитым вариантом функции, выполняющей преобразование имени хоста и сервиса в адрес и порт, является функция getaddrinfo.
#include <sys/types.h> #include <sys/socket.h> |
24 В Windows это файл system32\drivers\etc\services.
#include <netdb.h> int getaddrinfo(const char *hostname, const char *servname, const struct addrinfo *hints, struct addrinfo **res); struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; }; |
Функция getaddrinfo возвращает информацию о хосте, имя которого задано параметром hostname. В качестве имени может быть указано имя хоста, текстовая строка, в которой указан разделенный точками цифровой IPv4 адрес или IPv6 адрес. Параметр servname задаёт порт: может быть указано имя порта из /etc/services или номер в десятичной системе. Вместо любого из этих параметров можно передать NULL.
Полученная функцией информация возвращается в виде связанного списка структур addrinfo, указатель на первую из которых помещается в переменную res. Указателем на следующую структуру является поле ai_next. Значения членов структуры ai_family, ai_socktype и ai_protocol возвращаются в виде, пригодном для использования в вызове функции socket. Элемент ai_addr указывает на заполненную структуру адреса размером ai_addrlen.
Аналогичного вида структура, указатель на которую может быть передан параметром
hints, используется для передачи системе информации о виде необходимой информации. Поле ai_family задаёт семейство протоколов, адреса которого требуется получить. Значение PF_UNSPEC говорит о готовности работать с адресами любого типа. Аналогично, значения элементов ai_socktype и ai_protocol задают тип сокета и протокол. Нулевое значение говорит о готовности получить информацию о любом типе сокета/протоколе. В параметре ai_flag могут быть заданы различные флаги, которые позволяют задать режимы работы с IPv4 и IPv6 адресами, отключить преобразование имён хостов/служб или сообщить функции, что возвращённая информация будет использована в пассивном соединении. Остальные элементы этой структуры должны быть равны нулю.
Если в качестве hints передаётся NULL, то функция getaddrinfo работает так, как если бы ей была передана структура hints, элемент ai_family которой равен PF_UNSPEC, а остальные – нулю.
Все возвращаемые функцией getaddrinfo структуры данных выделяются динамически, поэтому нет необходимости копировать их содержание перед следующим вызовом. Однако эту память необходимо освободить после использования, для чего необходимо воспользоваться функцией freeaddrinfo.
#include <netdb.h> void freeaddrinfo(struct addrinfo *ai); |
Задание для самостоятельного выполнения
Напишите программу, которая бы выводила всю доступную через DNS информацию о хосте, заданном IP адресом или именем.
Особенности работы с TCP/IP в Windows
Работа с протоколом TCP/IP в Windows осуществляется с помощью библиотеки Windows Sockets, или Winsock. В этом разделе мы рассмотрим особенности работы со второй версией этой библиотеки, используемой в 32-битных ОС семейства Windows.
Интерфейс библиотеки Winsock реализует все возможности работы с сокетами библиотеки Berkley, что упрощает написание переносимых приложений. Кроме того, в ней имеется ряд специфических функций, которые могут быть удобны для работы в операционной системе Windows, однако не имеют аналогов в UNIX. Все специфичные для Windows функции библиотеки Winsock начинаются на буквы WSA, что позволяет легко отличить их от стандартных функций библиотеки Berkley Sockets.
Имеется и ряд отличий, которые все же потребуют некоторых изменений при переносе программы между Windows и UNIX:
в отличие от UNIX нужно подключить только один заголовочный файл winsock2.h и библиотеку ws2_32.lib;
перед началом работы необходимо вызвать функцию WSAStartup для инициализации библиотеки, а по окончании работы – функцию WSACleanup для освобождения ресурсов;
могут встретиться отдельные случаи использования специфичных для Windows типов данных и констант, определенных в заголовочных файлах Windows;
не совпадают параметры сокетов, устанавливаемые вызовами
setsockopt/getsockopt, fcntl, ioctl;
вместо функции close для закрытия сокета используется функция
closesocket.
используются различные способы получения информации об ошибках.
Во многом отличия между интерфейсами Berkley Sockets и Winsock обусловлены тем, что тогда как в UNIX системах поддержка сети исходно внедрялась как постоянная функция ОС, в Windows она появилась и первое время существовала как независимая система, часто реализованная третьими фирмами. Оставила свой отпечаток и разная архитектура операционных систем.
В целом, эти отличия не принципиальны и, при желании и соблюдении некоторой осторожности, можно написать переносимую между Windows и UNIX программу.
Инициализация и освобождение библиотеки
Итак, перед началом работы необходимо инициализировать библиотеку Winsock, вызвав функцию WSAStartup:
#include <winsock2.h> int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData); |
Первый параметр этой функции, wVersionReauested, позволяет приложению запросить версию библиотеки, с которой оно хочет работать. Основная версия задается в младшем байте, в старшем байте задается номер ревизии. Текущая основная версия библиотеки – 2. Номера ревизии, которые необходимы для поддержки той или иной функции можно найти в документации.
Вторым параметром передается указатель на структуру типа WSADATA, которая заполняется в процессе вызова библиотекой Winsock и содержит информацию о текущей версии и возможностях библиотеки.
В случае удачного вызова функция WSAStartup возвращает значение 0, в случае ошибки – один из нескольких кодов ошибки, с которыми можно ознакомиться в документации.
По окончании работы с библиотекой Winsock нужно вызвать функцию WSACleanup.
#include <winsock2.h> int WSACleanup (void); |
Соответствие типов
Как в Windows, так и в UNIX используются специфические для этих систем нестандартные типы данных. В следующей таблице делается попытка сопоставить используемые с сетевыми функциями типы данных, имеющие разные названия в этих системах. Название типов для UNIX-систем дано по стандарту POSIX.
Тип POSIX | Тип Windows | Комментарий |
Дескрипторы
Int | SOCKET | В Windows введён специальный тип для дескриптора сокета. |
Адреса
in_port_t | unsigned short | Порт UDP/TCP, целое без знака 16 бит. |
in_addr_t | unsigned long | IP адрес. Целое без знака 32 бита. |
Целочисленные переменные с заданным числом бит.
uint8_t | u_char, BYTE | 8 бит целое без знака (unsigned char). |
uint16_t | u_short, WORD | 16 бит целое без знака (unsigned short). |
uint32_t | u_long, DWORD | 32 бита целое без знака (unsigned long). |
Размеры
ssize_t | int | Размер буферов для чтения/записи. |
socklen_t | int | Размер адреса. |
sa_family_t | unsigned char или unsigned short | Тип для номера стека протоколов. 8 бит в системах, поддерживающих поле sin_len в структуре sockaddr_in, 16 бит - в не поддерживающих. |
FAR | Объявляет указатель как дальний, в современных ОС не используется и определён в Win32 как пустая строка. |
Надо заметить, что различия могут иметь место и между разными UNIX-системами. Например, в Linux вместо типа данных socklen_t (POSIX, BSD) используется тип sockaddr_len и т.п.
В Windows определены константы INVALID_SOCKET и SOCKET_ERROR, которые используются как возвращаемые в случае ошибок значения функциями socket и другими. В UNIX специальные такие константы не объявляются, об ошибке, как правило25, сигнализирует значение –1. В Windows SOCKET_ERROR определёно как –1, а INVALID_SOCKET как его беззнаковый аналог: ~0. При портировании кода из Windows в UNIX можно определить обе эти константы как –1.
Обратите внимание, что тип SOCKET в Windows определён как беззнаковое целое (unsigned int), в то время как в операционных системах UNIX функцией socket возвращается целое со знаком (int). Соответственно, следующий код будет корректно работать в UNIX, но не в Windows:
.... sock_fd = socket(ai_family,ai_socktype,ai_protocol); if (sock_fd < 0) { .... // обработка ошибки – не работает в Windows |
В Windows условие sock_fd<0 никогда не будет выполнено, так как беззнаковый SOCKET всегда больше или равен нуля, а константа INVALID_SOCKET в Windows соответствует максимальному беззнаковому целому числу. Код обработки ошибки никогда не получит управление. Такая ошибка может появиться в проектах, код которых перенёсен из UNIX в Windows.
Управление параметрами сокетов
Существенно различаются между Windows и UNIX интерфейсы для управления параметрами сокетов. Те задачи, которые в Berkley Sockets решаются с помощью вызовов ioctl / fcntl, в Winsock возложены на ioctlsocket и WSAIoctl. При этом используются разные наборы команд, а обозначения для одинаковых команд могут не совпадать.
Рассмотрим, например, такую операцию как перевод сокета в неблокирующий режим.
В UNIX она осуществляется с помощью вызова fcntl, а в Windows – ioctlsocket.
#include <winsock2.h> int ioctlsocket( SOCKET s, long cmd, u_long FAR *argp ); |
Также как и ioctl в UNIX эта функция может выполнять различные операции, в зависимости от значения параметра cmd. Для управления блокирующим режимом используется значение команды FIONBIO. Параметр argp будет определять, хотим ли мы включить или выключить блокирующий режим. Он должен быть указателем на переменную типа unsigned long, имеющую нулевое значение для блокирующего режима и ненулевое для не блокирующего.
Таким образом, чтобы перевести сокет в неблокирующий режим в Windows нам нужно написать следующий код.
#include <windows.h> SOCKET s; u_long l; ... |
25 Всегда проверяйте по справке возвращаемое в случае ошибки значение в тех случаях, когда вы его точно не помните.
l=1; ioctlsocket(s,FIONBIO,&l); |
Нужно отметить, что не все опции и режимы работы сокетов предусмотренные Berkley Sockets в принципе могут быть использованы в Windows. Например, не поддерживается асинхронный режим работы сокетов с уведомлениями через механизм сигналов. В Windows похожий режим может быть реализован, например, с применением механизмов Overlapped IO, что, однако потребует использования специфических функций. Функции setsockopt / getsockopt реализованы как в Berkley Sockets, так и в Winsock, однако набор устанавливаемых ими опций также различается. Например, в Windows максимальный размер датаграммы UDP можно узнать, запросив с помощью setsockopt параметр SO_MAX_MSG_SIZE, а в UNIX для этого нужно воспользоваться
системным вызовом, который позволяет узнать параметры ядра.
При написании кросплатформенного кода лучшим решением, вероятно, будет реализовать набор специфичных для каждой системы функций, которые решали какие-то задачи, например, включали или выключали блокирующий режим.
Доступ к кодам ошибки
В UNIX функции библиотеки Sockets, как и другие функции стандартной библиотеки, при возникновении ошибки возвращают специальное значение (как правило –1) и записывают код ошибки в глобальную переменную errno.
В отличие от Berkley Sockets, Winsock с переменной errno не работает, так как не является частью стандартной библиотеки языка С в Windows. Для того чтобы получить код ошибки Winsock нужно вызвать функцию WSAGetLastError.
#include <winsock2.h> int WSAGetLastError (void); |
Другие функции библиотеки Winsock
Операционная система UNIX создавалась под лозунгом «всё является файлом». Сетевая подсистема в UNIX полностью соответствует этой концепции и дескриптор сокета равноправен с дескриптором файла. Таким образом, при разработке приложения в UNIX мы можем использовать, например функцию select для того, чтобы одновременно проверять готовность к вводу-выводу сетевого соединения и наличие ввода пользователя с клавиатуры. Однако подсистема ввода-вывода в Windows построена в соответствии с другими концепциями, и при переносе на эту платформу было невозможно сохранить такую же высокую степень интеграции сетевой подсистемы и подсистемы ввода-вывода. Так функция select сохранила свою функциональность применительно к сетевым сокетам, но не может использоваться с обычными файлами или другими объектами.
Наряду с функциями для инициализации и закрытия библиотеки, в библиотеке Winsock есть ряд специфичных для Windows функций. Использование этих функций сильно затруднит портирование кода на UNIX-системы, однако для Windows приложений они обеспечивают доступ к сетевым средствам с использованием базовых возможностей операционной системы Windows, таким как механизм сообщений, средства синхронизации и асинхронного ввода-вывода.
Так, например, функция WSAAsyncSelect позволяет приложению получать уведомления о событиях, связанных с сокетом, посредством механизма сообщений.
Функции WSASend, WSARecv, WSASendTo, WSARecvFrom поддерживают наряду с обычными режимами режим Overlapped IO и работу с несколькими буферами приема/передачи данных.
Пример программы, использующей Windows Sockets
В качестве примера рассмотрим запрос корневой директории с web-сервера. Эта программа полностью соответствует рассматриваемому ранее примеру, за исключением необходимых для работы с winsock изменений: использования типа SOCKET, вызова функций инициализации и закрытия библиотеки и использования closesocket вместо socket.
#include <winsock2.h>
#include <string.h>
#include <stdio.h>
int main(int argc,char * argv[])
{
WSADATA wd;
SOCKET s; // используем тип SOCKET sockaddr_in addr;
int i;
char str[101];
if(argc<2)
{
printf("enter IP address of server as parameter"); return 100;
}
// инициализируем библиотеку windows sockets if(WSAStartup(MAKEWORD(2,2),&wd)!=0)
{
printf("Winsock initialization failed."); return 200;
}
// создаём сокет s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(s==INVALID_SOCKET)
{
printf("socket() error\n"); return 1;
}
// заполняем структуру с адресом сервера memset(&addr,0,sizeof(addr)); addr.sin_family=AF_INET;
addr.sin_port=htons(80); // для web-ceрверов обычно
// используется порт 80 addr.sin_addr.S_un.S_addr=inet_addr(argv[1]);
// IP адрес сервера задаётся
// в командной строке
// устанавливаем соединение if(connect(s,(const sockaddr*)&addr,sizeof(addr))!=0) { printf("connect() error\n"); closesocket(s); return 2; } // посылаем запрос согласно протокола HTTP send(s,"GET /\n\n",7,0); shutdown(s,SD_SEND); // закрываем соединение на передачу do { i=recv(s,str,100,0); // получаем данные if(i>0) { str[i]='\0'; printf("%s",str); } } while(i>0); // при завершении передачи данных сервером // функция recv вернёт 0 shutdown(s,SD_BOTH);// окончательно закрываем соедин< Наши рекомендации
|