|
Программирование
для сети Интернет.
Содержание статьи:
Вступление.
Теоретическая
часть.
Практическая часть.
Работа с адресами.
Подсматривание за
сокетами.
Введение.
к началу статьи
Ахой. Идея написать эту статью возникла неожиданно. Просто в институте
начался курс Интернет технологий, и я подумал: “Почему это я
один таким нереальным счастьем охвачен? Может кому-нибудь это тоже
будет интересно?”. Посему предлагаю Вам очередной туториал.
Начнется он с теоретического материала, который затем плавно перетечет
в прикладную область. В ней мы создадим иерархию классов необходимых
для более-менее удобной работы. Можно начинать?
Теоретическая
часть.
к началу статьи
Говоря о сетевом программировании, очень часто упоминают о протоколах
TCP/IP. Не будем отступать от общепринятой концепции и познакомимся с
ними с помощью приведенной схемы.
На самом
верхнем (прикладном) уровне находятся приложения, обменивающиеся
данными с удаленными клиентами/сервером (здесь имеются ввиду другие
приложения, а не компьютеры). Эти данные поступают на транспортный
уровень, где разбиваются на пакеты (обычно размером менее 1000 байт).
Затем они попадают на сетевой уровень, на котором протокол IP руководит
передачей пакетов по физическому уровню. Пройдя по сети, наши данные
попадают на сетевой уровень удаленного клиента/сервера (который в
данном случае уже отвечает не за отправку, а за прием пакетов). Затем
на транспортном уровне пакеты собираются воедино (это
осложняется тем,
что пакеты могут прийти в произвольном порядке, а не в том в котором
были посланы). В случае если како-то пакет не дошел до адресата, на
транспортном уровне создается запрос на повторную отправку недостающего
пакета и только когда передача всех пакетов завершена, данные
передаются приложению.
Как вы,
наверное, уже могли догадаться, разница между IP и TCP (если не
вдаваться в тонкости реализации) заключается в том, что, отправляя
данные по протоколу IP, Вы не дождетесь от него сообщений о том, что
какой-то пакет потерялся – потерялся и Бог с ним. При
соединении через TCP, программа-отправитель посылает данные и ЖДЕТ
подтверждения от программы-получателя. UDP же находится на том же
уровне что и TCP, но подобно UDP о судьбе отправленных пакетов не
догадывается.
В завершении
теоретической части стоит сказать о способе адресации в сети Интернет.
Каждая машина в сети имеет 4-х байтовый идентификатор, называемый
IP-адресом (записывается в виде 4 чисел через точку, например
127.0.0.1). Каждое приложение устанавливает соединение через
определенный порт (не более одного приложения на порт). Таким образом,
адрес приложения в сети состоит из 4-х байтового IP-адреса машины и
2-байтового номера порта. Порты с номерами <32000 считаются
зарезервированными, и использовать их крайне не рекомендуется (можете,
конечно, попробовать, вдруг наткнетесь на неиспользуемый).
Практическая
часть.
Работа с
адресами.
к началу статьи
Для задания сетевого адреса машины используется ряд структур, часть
которых мы сейчас рассмотрим: |
struct sockaddr{ u_short sa_family;//семейство адресов char sa_data[14];//адрес };
struct sockaddr_in{ u_short sin_family;//семейство адресов в нашем случае AF_INET u_short sin_port;//порт in_addr sin_addr;//IP адрес char sin_zero[ 8 ];//не используется };
|
Стоит отметить, что
байты порта хранятся
в сетевом порядке, т.е. “младший байт по старшему
адресу” (а не в интеловском – “младший
байт по младшему адресу”). Естественно Вас никто не
собирается заставлять менять порядок байт, для этого есть специальная
функция – htons( u_short
). |
#define PORT 40000 sockaddr_in sc_addr; sc_addr.sin_port = htons( PORT );
|
Еще одно неудобство
связано с тем, что IP
адрес хранится в виде 4 байтов, хотя удобнее работать со строкой или с
доменным именем. Всё опять-таки уже сделано за нас: |
//структура в которой хранятся IP’шники struct in_addr{ union{ struct{ u_char s_b1,s_b2,s_b3,s_b3; }; struct{ u_short s_w1,s_w2; }; u_long s_addr; }s_un; }; //теперь получить IP sc_addr.sin_addr.s_un.s_addr = inet_addr(“127.0.0.1”); if( sc_addr.sin_addr.s_un.s_addr == INADDR_NONE ){ //произошла ошибка конвертации имени }
|
Осталось только
научиться получать адрес
по доменному имени: |
struct hostent{ char *h_name;//имя узла char **h_aliases;//псевдонимы short h_addrtype; short h_length;//длина адреса char **h_addr_list; //список адресов (у доменного имени может быть //несколько IP адресов) #define h_addr h_addr_list[ 0 ]//основной адрес };
hostent *phe; phe = gethostbyname(“www.microsoft.com”); if( !phe ){ //ошибка } memcpy( &sc_addr.sin_addr , phe->h_addr , phe->h_length );
|
Вот и все, что я хоте
рассказать про
адресацию в сети. Впереди нас ждет…
…Иерархия
классов для подключения.
к началу статьи
Но прежде всего нужно инициализировать библиотеку. Без неё сокеты
работать не будут. Заметьте: каждому сокету при инициализации
присваивается уникальный идентификатор. Сейчас выгода от этого
неочевидна, но, поверить мне, она существенная, потому что Вам рано или
поздно придется определять, от какого клиента пришел запрос. С каждым
клиентом связан ОДИН сокет, у каждого сокета есть ID…
Улавливаете? |
class cWSAStartUp{ WSADATA wsaData; public: cWSAStartUp( void ){ int iResult = WSAStartup( MAKEWORD(2,2), &wsaData ); if ( iResult != NO_ERROR ){ MessageBox( 0 , "Ошибка активации библиотеки WS2_32.lib" , "Ошибка" , 0 ); exit( 0 ); } } ~cWSAStartUp(){WSACleanup();} };
|
Согласен. Я слегка
перегнул с этим
классом. Зато как удобно не заботиться об освобождении ресурсов!
А вот этот класс абсолютно оправдан. В нем скрыта рутина инициализации
сокета: |
class cSocket{ SOCKET Sock; int SOCKET_ID; static int SOCKET_ID_KEEPER; public: //всевозможные конструкторы cSocket( void ); cSocket( int , int , int ); cSocket( int ); cSocket( cSocket &s ){Sock = s.Sock;SOCKET_ID = s.SOCKET_ID;} ~cSocket(){closesocket( Sock );} //уничтожение сокета void Release( void ){closesocket( Sock );Sock=SOCKET_ERROR;} SOCKET &GetSocket( void ){return( Sock );} int GetSocketID( void ){return( SOCKET_ID );} //установка уникального идентификатора сокета void SetSocketID( void ){SOCKET_ID =SOCKET_ID_KEEPER++;} //создание сокета вручную void CreateSocket( int , int , int ); };
int cSocket::SOCKET_ID_KEEPER = 0;
cSocket::cSocket( int _MODE ){ switch( _MODE ){ case( SOCK_STD_SETTING ): SOCKET_ID = SOCKET_ID_KEEPER ++;
Sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
if ( Sock == INVALID_SOCKET ) { WSACleanup(); exit( 0 ); } break; default: Sock = SOCKET_ERROR; break; } }
cSocket::cSocket( void ){Sock = SOCKET_ERROR;}
void cSocket::CreateSocket( int af , int type , int protocol ){ SOCKET_ID = SOCKET_ID_KEEPER ++;
Sock = socket( af , type , protocol );
if( Sock == INVALID_SOCKET ){ WSACleanup(); MessageBox( 0 , "Ошибка создания сокета" , "Ошибка" , 0 ); exit( 0 ); } }
cSocket::cSocket( int af , int type , int protocol ){ SOCKET_ID = SOCKET_ID_KEEPER ++; Sock = socket( af , type , protocol );
if( Sock == INVALID_SOCKET ){ WSACleanup(); MessageBox( 0 , "Ошибка создания сокета" , "Ошибка" , 0 ); exit( 0 ); } }
|
Если объяснять на
пальцах, то сокет это
хендл специфического потока ввода/вывода. Вся его специфичность
заключается в том, что через этот поток обмениваются данными два
удаленных (в общем случае) приложения. Как видно из названия следующего
класса, в нем отражен базовый функционал – получение/отправка
данных (Send(…)/Receive(…)), закрепление за
конкретным приложением, которое будет обрабатывать сообщения нашего
клиента/сервера (AttachToWindow(…)) и, конечно же,
освобождение ресурсов (совсем забыл сказать: нам понадобятся некоторые
классы из этого урока). |
class cCommonFunctionality:public cSocket{ cThread ReceiveThread; protected: HWND hWnd; public: cCommonFunctionality( void ):cSocket(){} cCommonFunctionality( float f ):cSocket( f ){}
//с помощью следующей функции мы можем сообщить клиенту/серверу //кому можно послать сообщение в случае чего void AttachToWindow( HWND hw ){hWnd = hw;} HWND GetHWND( void ){return( hWnd );} //функция отправки сообщений void SendMessage( UINT msg , UINT w , LONG l ){ ::SendMessage( hWnd , msg , w , l ); }
//отправка данных void Send( char* , int ); __forceinline void Send( LPVOID ptr , int len ){ Send( ( char * )ptr , len ); } __forceinline void Send( cThreadParam &TP ){ Send( TP.Params , TP.GetParamSize() + 8 ); } //эта функция запускает отдельный поток для принятия/отправки данных __forceinline void RunReceiveThread( void ); //принятие данных int Receive( char * , int ); //освобождение ресурсов void Release( void ){ ReceiveThread.TerminateThread(); cSocket::Release(); } void StopReceiveThread( void ){ ReceiveThread.TerminateThread(); } };
//простая обертка для простой функции int cCommonFunctionality::Receive( char * RecvBuffer , int BufferSize ){ int RecvBytes = recv( GetSocket() , RecvBuffer , BufferSize , 0 ); return( RecvBytes ); }
|
Следующая функция
создает поток для
приема/передачи данных. Это жизненно необходимо, т.к. функция
recv(…) блокирует поток, в котором была вызвана (блокирует
до тех пор пока не будут приняты данные) |
void cCommonFunctionality::RunReceiveThread( void ){ cThreadParam TP;
AddParamPtr( TP , *this ); ReceiveThread.CreateThread( ::Receive_cf , TP ); }
|
А вот и сама потоковая
функция: |
DWORD Receive_cf( LPVOID PTR ){ cThreadParam TP( PTR ); cCommonFunctionality *CF = ( cCommonFunctionality * )TP.GetParami( 0 ); int result;
TP.Release(); //буфер, в который будем читать пришедшие данные char Buffer[ BUFFER_SIZE ]; int BytesRecv;
//основной цикл while( 1 ){ BytesRecv = CF->Receive( Buffer , BUFFER_SIZE );
if( BytesRecv <= 0 ){ //произошла ошибка goto end; } else{ cThreadParam COMMAND_LINE; COMMAND_LINE.CopyByteSet( Buffer , BytesRecv ); for( int i( 0 ) ; i < ( COMMAND_LINE.GetParamSize() + 7 ) / BUFFER_SIZE ; i++ ){ CF->Receive( Buffer , BUFFER_SIZE ); COMMAND_LINE.AppendByteSet( i * BUFFER_SIZE + BUFFER_SIZE , Buffer , BUFFER_SIZE ); }
switch( * ( ( int * )COMMAND_LINE.GetParam( 0 ) ) ){ case(DO_SOMTHING): //обрабатываем присланные данные break; default: MessageBox( 0 , "Unknown command line signature." , "Error" , 0 ); exit( 0 ); break; } } } end:; return( 0 ); }
|
Подробнее хочется
остановиться на
следующем фрагменте: |
BytesRecv = CF->Receive( Buffer , BUFFER_SIZE );
if( BytesRecv <= 0 ){ //произошла ошибка goto end; } else{ cThreadParam COMMAND_LINE; COMMAND_LINE.CopyByteSet( Buffer , BytesRecv ); //в этом цикле осуществляется “допрочитка” данных for( int i( 0 ) ; i < ( COMMAND_LINE.GetParamSize() + 7 ) / BUFFER_SIZE ; i++ ){ CF->Receive( Buffer , BUFFER_SIZE ); //сохраняем принфтые данные COMMAND_LINE.AppendByteSet( i * BUFFER_SIZE + BUFFER_SIZE , Buffer , BUFFER_SIZE ); } }
|
Проблема в том, что
когда приходят
данные, мы не знаем сколько их пришло. Может килобайт, а может и
гигабайт, а размер буфера, в который происходит выгрузка, у нас
конечный. Поэтому надо реализовать “допрочитывание”
данных, если они не влезают в буфер. Это как раз и делается в цикле. |
//ну тут просто отправляем данные. никаких хитростей. void cCommonFunctionality::Send( char *Buffer , int BufferLength ){ int BytesSent; BytesSent = send( GetSocket() , Buffer , BufferLength , 0 ); if( BytesSent == SOCKET_ERROR ){ MessageBox( 0 , "Ошибка отправки данных" , "Ошибка" , 0 ); exit( 0 ); } }
|
Однако сразу отправить
данные не
получится – надо еще установить соединение. У сервера и
клиента есть свои нюансы. Из-за этих-то нюансов мы организуем ещё два
класса, в которые вынесем все особенности создания соединения: |
class cServerFunctionality:public cCommonFunctionality{ protected: //этим мьютексом будем лочить список клиентов cMutex ClientsMutex; //привязка сокета к порту протокола void Bind( char* , unsigned short ); //переход в режим прослушивания, в этом режиме сервер будет находиться //ожидая запросов на подключение от клиентов void Listen( void ); public: //список (именно список) всех подключенных клиентов cOWL<cCommonFunctionality> Clients; //не делающие ничего особенного конструкторы cServerFunctionality( void ):cCommonFunctionality(){Clients.Number=0;} cServerFunctionality( float i ):cCommonFunctionality( i ){Clients.Number=0;} void LockClients( void ){ ClientsMutex.EnterCriticalSection(); } void UnLockClients( void ){ ClientsMutex.LeaveCriticalSection(); } //освобождение ресурсов void Release( void ){ for( int i( 0 ) ; i < Clients.Number ; i++ ){ Clients[ i ].Release(); } Clients.Release(); ClientsMutex.Release(); cCommonFunctionality::Release(); } };
void cServerFunctionality::Bind( char *IP_ADDRESS , unsigned short HOST ){ sockaddr_in service;
service.sin_family = AF_INET; service.sin_addr.s_addr = inet_addr( IP_ADDRESS ); service.sin_port = htons( HOST );
if ( bind( GetSocket() , ( SOCKADDR* ) &service , sizeof( service ) ) == SOCKET_ERROR ) { MessageBox( 0 , "Ошибка связывания" , "Ошибка" , 0 ); closesocket( GetSocket() ); WSACleanup(); exit( 0 ); } }
void cServerFunctionality::Listen( void ){ if ( listen( GetSocket() , 1 ) == SOCKET_ERROR ){ MessageBox( 0 , "Ошибка перехода в режим прослушивания" , "Ошибка" , 0 ); closesocket( GetSocket() ); WSACleanup(); exit( 0 ); } }
|
Теперь, собственно,
класс самого сервера: |
class cServer:public cServerFunctionality{ cThread ConnectionThread; public: void Release( void ); void WaitForConnection( char* , unsigned short , int ); };
|
Методов немного, да и
простые они.
Поэтому особо не задерживаемся: |
//освобождение ресурсов //грохаем только поток, остальные ресурсы освобождаются //классами, расположенными выше по иерархии void cServer::Release( void ){ ConnectionThread.TerminateThread(); cServerFunctionality::Release(); }
//потоковая функция подключения клиентов DWORD WaitForConnection( LPVOID PTR ){ cThreadParam TP( PTR ); //единственный параметр – указатель на сервер, создавший поток cServer *SF; SF = ( cServer * )TP.GetParami( 0 );
TP.Release();
while( 1 ){
SOCKET AcceptedSocket;
AcceptedSocket = SOCKET_ERROR;
//ждем клиента while( AcceptedSocket == SOCKET_ERROR ){ AcceptedSocket = accept( SF->GetSocket() , NULL , NULL ); }
//клиент подключился, надо его добавить в список //лочим, список от греха подальше SF->LockClients(); SF->Clients.AddEnd( NULL ); //устанавливаем сокет SF->Clients[ SF->Clients.Number - 1 ].GetSocket() = AcceptedSocket; //устанавливаем уникальный идентификатор SF->Clients[ SF->Clients.Number - 1 ].SetSocketID(); //сообщаем клиенту, какому окну он может посылать сообщения //(так, на всякий случай) SF->Clients[ SF->Clients.Number - 1 ].AttachToWindow( SF->GetHWND() ); //запускаем поток приема данных SF->Clients[ SF->Clients.Number - 1 ].RunReceiveThread(); //разлочиваем список SF->UnLockClients(); }
return( 0 ); }
//с помощью этой функции Вы будете осуществлять запуск Вашего сервера //в качестве параметра – адрес сервера и режим подключения //в зависимости от режима (THREADED/NON_THREADED) подключение //будет осуществляться либо в отдельном потоке, либо в том потоке, в //котором была вызвана эта функция void cServer::WaitForConnection( char *IP_ADDRESS , unsigned short HOST , int _MODE = NON_THREADED ){ cThreadParam TPc;
if( GetSocket() == SOCKET_ERROR ){ CreateSocket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); Bind( IP_ADDRESS , HOST ); Listen(); }
ClientsMutex.CreateMutex();
switch( _MODE ){ case( NON_THREADED ):{
cout<<"Waiting for connection..."<<endl; while( 1 ){ Clients.AddEnd( NULL );
Clients[ Clients.Number - 1 ].GetSocket() = SOCKET_ERROR;
while( Clients[ Clients.Number - 1 ].GetSocket() == SOCKET_ERROR ){ Clients[ Clients.Number - 1 ].GetSocket() = accept( GetSocket() , NULL , NULL ); }
cout<<"Client connected."<<endl;
break; case( THREADED ): //готовим данные для передачи в поток AddParamPtr( TPc , *this ); //создаем поток ConnectionThread.CreateThread( ::WaitForConnection , TPc ); break; default: MessageBox( 0 , "Неопознаный режим подключения." , "Ошибка." , 0 ); exit( 0 ); break; } }
|
Теперь клиентский
функционал: |
class cClientFunctionality:public cCommonFunctionality{ public: //конструкторы cClientFunctionality( void ):cCommonFunctionality( SOCK_STD_SETTING ){} cClientFunctionality( float ):cCommonFunctionality( SOCK_STD_SETTING ){} //подключение к серверу void ConnectToServer( char* , unsigned short ); //более низкоуровневый аналог предыдущей функции, здесь //практически все то же самое, просто не обрабатываются ошибочные //ситуации. Вместо этого возвращается код, с которым завершилась //функция connect() //в потоке рекомендую юзать именно её int _ConnectToServer( char* , unsigned short ); };
int cClientFunctionality::_ConnectToServer( char *ip_addr , unsigned short h ){ //проверяем инициализирован ли сокет //если нет, то делаем это if( GetSocket() == SOCKET_ERROR ) CreateSocket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
sockaddr_in clientService;
clientService.sin_family = AF_INET; clientService.sin_addr.s_addr = inet_addr( ip_addr ); clientService.sin_port = htons( h );
return( connect( GetSocket() , ( SOCKADDR* ) &clientService , sizeof( clientService ) ) ); }
void cClientFunctionality::ConnectToServer( char *ip_addr , unsigned short h ){ if( GetSocket() == SOCKET_ERROR ) CreateSocket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
sockaddr_in clientService;
clientService.sin_family = AF_INET; clientService.sin_addr.s_addr = inet_addr( ip_addr ); clientService.sin_port = htons( h );
if ( connect( GetSocket() , ( SOCKADDR* ) &clientService , sizeof( clientService ) ) == SOCKET_ERROR ){ MessageBox( 0 , "Невозможно подконнектится к серверу." , "Ошибка" , 0 ); closesocket( GetSocket() ); WSACleanup(); exit( 0 ); return; } cout<<"Connection was made..."<
|
Собственно сам клиент:
|
class cClient:public cClientFunctionality{ //поток в котором будем коннектиться cThread ConnectionThread; public: //конструкторы cClient( void ):cClientFunctionality(){} cClient( HWND hw ):cClientFunctionality(){hWnd = hw;} //функция void ConnectToServer( char * , unsigned short , int , bool ); void StopConnection( void ){ConnectionThread.TerminateThread();} };
DWORD ConnectToServer( LPVOID PARAM ){ cThreadParam TP( PARAM );
//указатель на клиента, который создал поток cClient *Client = ( cClient * )TP.GetParami( 0 ); //количество попыток подкрннектиться int attempts( * ( ( int * )TP.GetParam( 1 ) ) );
//порт unsigned short h( *( ( unsigned short * )TP.GetParam( 2 ) ) );
//если incr == true, то функция будет пытаться подконнектится //к разным портам int incr( * ( ( bool * )TP.GetParam( 3 ) ) );
char ip_addr[20]; strcpy( ip_addr , ( char * )TP.GetParam( 4 ) );
TP.Release();
for( int i( 0 ) ; i < attempts && Client->ConnectToServer( ip_addr , h ) == SOCKET_ERROR ; i++ ){ //выбираем новый порт для подключения if( incr && h <= 65535 ) h++; } if( i != attempts ){ //соединение произошло Client->RunReceiveThread(); }
return( 0 ); }
//просто создаем поток, в котором будет вызываться cClientFunctionality::ConnectToServer void cClient::ConnectToServer( char *ip_addr , unsigned short h , int attempts , bool incr = false ){ cThreadParam TP; AddParamPtr( TP , *this ); AddParam_cd_type( TP , attempts ); AddParam_cd_type( TP , h ); AddParam_cd_type( TP , incr ); TP.AddParamByteSet( ip_addr , strlen( ip_addr ) + 1 ); ConnectionThread.CreateThread( ::ConnectToServer , TP ); }
|
Теперь, если Вы
захотите использовать эти
классы для своих нужд, то нужно только дописать функцию Receive_cf. А
пока маленький пример: |
cServer Server; Server.WaitForConnection( “127.0.0.1” , 27020 , THREADED ); cClient Client; Client.ConnectToServer( “127.0.0.1” , 27015 , 10 , true );
|
Здесь мы создали
сервер и запустили
функцию обработки заявок на подключение. Затем создали клиента, который
просканировал бы не более 10 портов, но при запущенном сервере,
подконнектился бы с пятой попытки. Как видите, все легко и просто.
Подсматривание за сокетами.
к началу статьи
Как Вы могли заметить в приведенном выше коде, ена сервере для каждого
клиента создается отдельный поток приема данных. Если клиентов не очень
много, то такой способ вполне подойдет. Если же планируется создать
что-то грандиозное, то придется что-то с этим поделать. Оказывается
можно заставить систему посылать сообщения нашей программе, когда с
сокетом что-то происходит. Это делается с помощью функции
|
int WSAAsyncSelect( SOCKET s , HWND hWnd , unsigned int wMsg , long lEvent );
|
Где
s - сокет, за которым мы будем следить.
hWnd - хендл окна, которому будет посылаться сообщение, о том, что с
сокетом что-то произошло.
wMsg - собственно посылаемое сообщение
lEvent - битовая маска событий, которые мы планируем обрабатывать.
Задается следующим образом:
FD_READ - можно читать данные (recv)
FD_ACCEPT - клиент хочет подключиться
FD_CLOSE - коннект был разорван
Зная это, Вам остается при инициализации вызвать
эту функцию например так:
|
WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_WRITE);
|
...и ждать сообщениё в
WndProc.
к началу статьи
На этом позвольте закончить этот урок. До встречи!
ЗЫ: способ связи прежний – dodonov_a_a(___AT)inbox.ru
Исходные коды (VC 6.0).
Исходные
коды (VC 7.0). |
|
|