SocketClient

## SocketClient для непродвинутых.

© Терехов А В. 2002

### Вступление

Статья написана для непродвинутых. В ней все объясняется своими словами, поэтому претензии по правильности изложенного не принимаются.
Итак, что такое сокеты. Сокеты условно можно разделить на две группы: сокет-клиент и сокет-сервер. Представьте себе электрическую вилку и электрическую розетку. Электрическая вилка - это сокет-клиент, электрическая розетка - это сокет-сервер. Клиент (электроутюг) просит сервер (электроподстанцию), чтобы тот ему что-то дал, в нашем примере электрический ток, что сервер и делает (если, конечно, ток в сети есть). Когда вилка вставляется в розетку происходит сокетное соединение. Вот, в принципе, и все.
Задача сокетного соединения - пересылать пакеты с данными между клиентом и сервером. Обработка этих пактеных данных уже лежит на совести как клиента, так сервера. Это и понятно, утюг нагревается - он же не холодильник, чтобы охлаждать...
Чтобы не изобретать велосипед, воспользуемся готовым способом передачи данных по протоколу HTTP. Пока у нас нет своего сервера воспользуемся каким-нибудь готовым HTTP-сервером, например, Apache или PersonalWebServer. Я пользуюсь замечательным SmallHTTPServer'ом. Вот ссылка на него http://home.lanck.net/mf/srv/index.html.

### Порядок работы

Сначала мы должны создать сокет-клиент, затем сунуть вилку в розетку - создать сокетное соединение, отправить пакет данных серверу, получить ответ и обработать его.
Чтобы все это сделать мы должны выполнить ряд действий:
1. Инициализировать поддержку сокетов в Windows - WSAStartup
2. Создать свой сокет - socket
3. Настроить параметры сокета:
3.1 указать IP-адрес сервера
3.2 указать порт (для HTTP-протокола 80-й)
3.3 указать способ передачи данных
4. Соединиться с сервером - connect
5. Подготовить данные в соответствии с протоколом HTTP
6. Передать пакет данных серверу - send
7. Получить ответ от сервера - recv
8. Обработать полученные данные
9. Закрыть сокет - closesocket
10. Уничтожить сокет - WSACleanup

ВАЖНО. При работе с сокетами используется системная библиотека wsock32.dll. Для обращения к функциям этой библиотеки существует модуль WinSock, входящий в поставку Delphi. Поэтому не забудьте указать модуль WinSock в клаузе Uses.

### Предварительные шаги

Прежде, чем приступать к программированию клиент-сокета сделаем несколько предварительных шагов:
1. откроем новый проект;
2. положим на форму два TEdit, одно TMemo и одну TButton
3. создадим новый модуль: File-New-Unit
4. сохраним модуль с названием SocketUnit.pas
5. приведем модуль SocketUnit в следующий вид:

unit SocketUnit;
interface
Uses Windows, SysUtils, WinSock;
Function MyClientSocket:String;
implementation
Function MyClientSocket:String;
Begin
//писать будем все сюда
End;
End.

6. Вернемся в главную форму и выберем File-UseUnit...-SocketUnit
Теперь можно приступать к сокетам.

### Шаг 1. Инициализация сокета Windows

Для инициализации сокета используется функция:

Function WSAStartUp(wVersionRequested:Word; WSAData:TWSAData):Integer
VersionRequested - это запрашиваемая версия сокетов

В Windows есть две версии 1.1 и 2.0. Мы будем использовать версию 1.1 Младший байт переменной wVersionRequested должен содержать номер версии (major), старший байт - номер расширения версии (minor). Для того чтобы создать нужное нам значение, воспользуемся функцией:

wVersionRequested:=MakeWord(1,1);

Второй аргумент функции несколько посложнее. При вызове функции WSAStartUp в этот аргумент будут возвращены параметры инициализации. На языке Си принято переменные типа запись (Record) называть структорой, будем придерживать такой же терминологии. Итак WSAData - это структура следующего типа:

TWSAData = Record
wVersion : Word;
wHighVersion : Word;
szDescription : Array [0..WSADESCRIPTION_LEN+1] Of Char;//где WSADESCRIPTION_LEN = 255
szSystemStatus : Array [0..WSASYS_STATUS_LEN+1] Of Char;//где WSASYS_STATUS_LEN+1 = 127
MaxSockets : Word;
MaxUdpDg : Word;
lpVendor : Word; End;

На самом деле аргумент WSAData нам не нужен, нам важен результат функции. Если результат функции равен 0, значит все в порядке, иначе - надо искать ошибку с помощью функции WSAGetLastError, которая вернет код ошибки. Затем код ошибки можно обработать используя Help Win32 Develoer's References (входит в поставку Delphi).
Напишем первые строчки кода (не забудем о включении в клаузу Uses модуля WinSock):

Function MyClientSocket:String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
WSACleanup; //уничтожим сокет см. шаг 10
Exit;
End; {If Not = 0}
WSACleanup; //уничтожим сокет см. шаг 10
End;{MyClientSocket}

Итак, если все в порядке, перейдем к созданию сокета.

### Шаг 2. Создание сокета

Для создания сокета используется функция:

Function socket(af:Integer; struct:Integer; protocol:Integer):TSocket;

где (все константы определены в модуле WinSock):
af - спецификация формата адресов, всегда PF_INET
struct - тип спецификации для нового сокета
SOCK_STREAM - для TCP-протокола,
SOCK_DGRAM - для UDP-протокола
protocol - конкретный протокол или 0, если протокол не определен
IPPROTO_TCP - TCP/IP протоколо
IPPROTO_UDP - UDP/IP протокол
ISOPROTO_TP4 - ISO протокол
NSPROTO_IPX - IPX протокол
NSPROTO_SPX - SPX протколо
NSPROTO_SPXII - SPX II протокол
результат функции - в результате возвращается хэндл (описатель) нового сокета или INVALID_SOCKET в случае неудачи.

Function MyClientSocket:String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
hSocket:TSocket;
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
closesocket(hSocket);//закрыть сокет см. шаг 9
WSACleanup; //уничтожим сокет
Exit;
End; {If Not = 0}
hSocket:=socket(PF_INET,SOCK_STREAM,0);//создаем сокет
If hSocket=INVALID_SOCKET Then
//неудача при создании сокета
Begin{INVALID_SOCKET}
Result:='';
closesocket(hSocket);//закрыть сокет см. шаг 9
WSACleanup; //уничтожим сокет
Exit;
End; {INVALID_SOCKET}
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
End;{MyClientSocket}

### Шаг 3. Настройка параметров сокета

Для настройки параметров сокетов используется довольно сложная структура:

TSockAddrIn = Record
sin_family : Word;
sin_port : Word;
sin_addr : TInAddr;
sin_zero : Array [0..7] of Char;
sa_family : Word;
sa_data : Array [0..13] Of Char;
End;

TInAddr = Record
S_un_b : SunB;
S_un_w : SunW;
S_addr : Integer;
End;

SunB = Record
s_b1 : Char;
s_b2 : Char;
s_b3 : Char;
s_b4 : Char;
End;

SunW = Record
s_w1 : Word;
s_w2 : Word;
End;

Во всей этой сложной структуре пока нас будет интересовать три параметра:
1. TSockAddrIn.sin_addr.S_addr - IP-адрес сервера
2. TSockAddrIn.sin_family - используемый протокол
3. TSockAddrIn.sin_port - используемый порт
Все остальные параметры установим в 0.

Исходные параметры:
1. пока будем работать с сервером, находящимся на локальной машине, поэтому IP-адрес у нас должен быть такого вида: "127.0.0.1"
2. работать будем по IP-протоколу
3. порт для HTTP-протокола используется 80-й

Прежде всего объявим переменные
Var
name:TSockAddrIn;
IPAddress:String;

Затем надо перевести строку "127.0.0.1" в числовой формат. Для этого существует специальная функция:
Function inet_addr(cp:PChar):Integer;
где cp - нужный IP-адрес

IPAddress:='127.0.0.1';
name.sin_addr.S_addr:=inet_addr(PChar(IPAddress));

Далее указываем по какому протоколу будем работать:
name.sin_family:=AF_INET; //для сокетов TCP/IP надо использовать константу AF_INET

И последнее, надо указать 80-й порт. Однако порядок старшинства бит, принятый для Интернет, отличается от порядка старшинства, принятого в Windows. Поэтому придется конвертировать число 80 в интернет-формат специальной функцией:
Function htons(hostshort:Word):Word;

name.sin_port:=htons(80);

Теперь наша функция будет выглядеть следующим образом:

Function MyClientSocket:String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
hSocket:TSocket;
IPAddress:String;
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
WSACleanup; //уничтожим сокет
Exit;
End; {If Not = 0}
hSocket:=socket(PF_INET,SOCK_STREAM,0);//создаем сокет
If hSocket=INVALID_SOCKET Then
//неудача при создании сокета
Begin{INVALID_SOCKET}
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
Exit;
End; {INVALID_SOCKET}
IPAddress:='127.0.0.1'; //будем использовать localhost
name.sin_family:=AF_INET;//укажем тип протокола
name.sin_addr.S_addr:=inet_addr(PChar(IPAddress));//укажем IP-адрес
name.sin_port:=htons(80);//укажем 80-й порт для HTTP-протокола
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
End;{MyClientSocket}

### Шаг 4. Соединение с сервером

Для соединения вилки с розеткой используется следующая функция:
Function connect(hSocket:Integer; Var name:TSockAddrIn; namelen:Integer):Integer;
где:
hSocket - полученый нами ранее хэндл сокета;
name - полученная нами ранее структура с параметрами соединения;
namelen - размер этой струтуры;
результат функции - если все в порядке, то 0, проблемы - SOCKET_ERROR.

Теперь посмотрим полученный код. К этому времени локальный сервер должен быть уже запущен.

Function MyClientSocket:String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
hSocket:TSocket;
IPAddress:String;
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
WSACleanup; //уничтожим сокет
Exit;
End; {If Not = 0}
hSocket:=socket(PF_INET,SOCK_STREAM,0);//создаем сокет
If hSocket=INVALID_SOCKET Then
//неудача при создании сокета
Begin{INVALID_SOCKET}
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
Exit;
End; {INVALID_SOCKET}
IPAddress:='127.0.0.1'; //будем использовать localhost
name.sin_family:=AF_INET;//укажем тип протокола
name.sin_addr.S_addr:=inet_addr(PChar(IPAddress));//укажем IP-адрес
name.sin_port:=htons(80);//укажем 80-й порт для HTTP-протокола
If connect(hSocket,name,SizeOf(name))=SOCKET_ERROR Then //соединяемся с сервером
Begin {SOCKET_ERROR}
//неудача при соедении с сервером
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup;
Exit;
End; {SOCKET_ERROR}
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
End;{MyClientSocket}

### Шаг 5. Подготовка данных в соотвествии с протоколом HTTP

Пакет с данными для протокола HTTP состоит из двух частей: заголовка и собственно данных, размер всего пакета должен быть равен 1024 байтам. В заголовке располагается служебная информация. Заголовок отделяется от данных двойным #13#10 - конец строки и перевод каретки. Мы будем использовать команду HTTP-протокола GET. Послав серверу команду GET /index.html HTTP 1.1#13#10#13#10, мы говорим серверу, чтобы он отправил нам файл index.html из корневой WEB-директории по протоколу HTTP1.1, если мы напишем GET /cgi-bin/cgi.exe HTTP 1.1, то сервер выполнит CGI-скрипт cgi.exe.
Подготовим новые переменные:
Var
...
HTTPAddress:String;
GetingString:String;
Buf:Array [0..1023] Of Char;

В переменную HTTPAddress поместим адрес нужной страницы или cgi-скрипта, в GetingString сформируем нужную командную строку и поместим ее в буфер Buf.

FillChar(Buf,SizeOf(Buf),#0);//обнулим буфер
HTTPAddress:='/index.html';//укажем нужную страницу
GetingString:='GET '+HTTPAddress+' HTTP/1.1'#13#10#13#10;//сформируем HTTP-заголовок
StrPCopy(Buf, GetingString);//положим заголовок в буфер

### Шаг 6. Передача пакета данных серверу

Для передачи данных используется функция:
Function send(hSocket:Integer; Var Buf:Untyped, len:Integer; Flags:Integer):Integer;
где:
hSocket - уже знакомый нам хэндл сокета;
Buf - буфер с данными - нетипизированная переменная, т.е. просто набор байтов;
len - длина буфера;
Flags - способ передачи данных, мы этот флаг установим в 0
результат функции - количество переданных байт в случае успеха или SOCKET_ERROR в случае неудачи.

Function MyClientSocket:String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
hSocket:TSocket;
GetingString:String;
Buf:Array [0..1023] Of Char;
IPAddress,HTTPAddress:String
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
WSACleanup; //уничтожим сокет
Exit;
End; {If Not = 0}
hSocket:=socket(PF_INET,SOCK_STREAM,0);//создаем сокет
If hSocket=INVALID_SOCKET Then
//неудача при создании сокета
Begin{INVALID_SOCKET}
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
Exit;
End; {INVALID_SOCKET}
IPAddress:='127.0.0.1'; //будем использовать localhost
name.sin_family:=AF_INET;//укажем тип протокола
name.sin_addr.S_addr:=inet_addr(PChar(IPAddress));//укажем IP-адрес
name.sin_port:=htons(80);//укажем 80-й порт для HTTP-протокола
If connect(hSocket,name,SizeOf(name))=SOCKET_ERROR Then //соединяемся с сервером
Begin {SOCKET_ERROR}
//неудача при соедении с сервером
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup;
Exit;
End; {SOCKET_ERROR}
FillChar(Buf,SizeOf(Buf),#0);//обнулим буфер
HTTPAddress:='/index.html';//укажем нужную страницу
GetingString:='GET '+HTTPAddress+' HTTP/1.1'#13#10#13#10;//сформируем HTTP-заголовок
StrPCopy(Buf, GetingString);//положим заголовок в буфер
If send(hSocket,Buf,SizeOf(Buf), 0)=SOCKET_ERROR Then //передаем данные
Begin {SOCKET_ERROR}
//неудача при передаче данных
Result:='';
closesocket(hSocket);
WSACleanup;
Exit;
End;{SOCKET_ERROR}
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
End;{MyClientSocket}

### Шаг 7. Получение ответа от сервера

Для приема данных используется следующая функция:
Function recv(hSocket:Integer; Var Buf:Untyped, len:Integer; Flags:Integer):Integer;
где:
hSocket - хэндл сокета;
Buf - буфер с данными;
len - длина буфера;
Flags - способ передачи данных, мы этот флаг установим в 0
результат функции - количество полученных байт в случае успеха или SOCKET_ERROR в случае неудачи.

Для получения данных от сервера напишем небольшой код и определим еще одну переменную.
Var
...
ReciveBytes:Integer;
ReciveBytes:=1; //инициализируем переменную
//будем принимать данные пока они не закончатся или возникнет ошибка
While Not((ReciveBytes=SOCKET_ERROR) OR (ReciveBytes=0)) Do
//ошибка при приеме данных или нет данных
Begin {SOCKET_ERROR OR нет данных}
FillChar(Buf,SizeOf(Buf),#0); //очистим буфер
ReciveBytes:=recv(hSocket,Buf,SizeOf(Buf),0); //примем данные
Result:=Result+String(Buf); //сформируем результат
End; {SOCKET_ERROR OR нет данных}

### Шаг 8. Обработка полученных данных

Мы не будем писать свой Web-броузер, поэтому данные будем смотреть именно в том виде, в котором они пришли. Прежде немного модернизируем функцию вынеся две переменные IPAddress,HTTPAddress в ее аргументы (не забудьте сделать это и в объявлении функции и в ее реализации, а также убрать IPAddress:='127.0.0.1'; и HTTPAddress:='/index.html'; из реализации функции). Обработку ошибок для наглядности оставим без изменений.

Function MyClientSocket(IPAddress,HTTPAddress:String):String;
Var
wVersionRequested:Word;
WSAData:TWSAData;
hSocket:TSocket;
GetingString:String;
Buf:Array [0..1023] Of Char;
ReciveBytes:Integer;
Begin{MyClientSocket}
wVersionRequested:=MakeWord(1,1); //инициализируем сокет Windows
FillChar(WSAData,SizeOf(WSAData),#0);//обнулим все значения WSAData
If Not WSAStartup(wVersionRequested,WSAData)=0 Then //стартуем
Begin {If Not = 0}
//неудача при старте
Result:='';
WSACleanup; //уничтожим сокет
Exit;
End; {If Not = 0}
hSocket:=socket(PF_INET,SOCK_STREAM,0);//создаем сокет
If hSocket=INVALID_SOCKET Then
//неудача при создании сокета
Begin{INVALID_SOCKET}
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
closesocket(hSocket);//закрыть сокет
Exit;
End; {INVALID_SOCKET}
name.sin_family:=AF_INET;//укажем тип протокола
name.sin_addr.S_addr:=inet_addr(PChar(IPAddress));//укажем IP-адрес
name.sin_port:=htons(80);//укажем 80-й порт для HTTP-протокола
If connect(hSocket,name,SizeOf(name))=SOCKET_ERROR Then //соединяемся с сервером
Begin {SOCKET_ERROR}
//неудача при соедении с сервером
Result:='';
closesocket(hSocket);//закрыть сокет
WSACleanup;
Exit;
End; {SOCKET_ERROR}
FillChar(Buf,SizeOf(Buf),#0);//обнулим буфер
GetingString:='GET '+HTTPAddress+' HTTP/1.1'#13#10#13#10;//сформируем HTTP-заголовок
StrPCopy(Buf, GetingString);//положим заголовок в буфер
If send(hSocket,Buf,SizeOf(Buf), 0)=SOCKET_ERROR Then //передаем данные
Begin {SOCKET_ERROR}
//неудача при передаче данных
Result:='';
closesocket(hSocket);
WSACleanup;
Exit;
End;{SOCKET_ERROR}
ReciveBytes:=1; //инициализируем переменную
//будем принимать данные пока они не закончатся или возникнет ошибка
While Not((ReciveBytes=SOCKET_ERROR) OR (ReciveBytes=0)) Do
//ошибка при приеме данных или нет данных
Begin {SOCKET_ERROR OR нет данных}
FillChar(Buf,SizeOf(Buf),#0); //очистим буфер
ReciveBytes:=recv(hSocket,Buf,SizeOf(Buf),0); //примем данные
Result:=Result+String(Buf); //сформируем результат
End; {SOCKET_ERROR OR нет данных}
closesocket(hSocket);//закрыть сокет
WSACleanup; //уничтожим сокет
End;{MyClientSocket}

Теперь вернемся в главную форму и на обытие OnClick кнопки (TButton) укажем следующее:

Memo1.Text:=MyClientSocket(Edit1.Text,Edit2.Text);

Теперь надо запускать локальный Web-сервер, убедиться в том, что файл index.html существует и запускать проект.
Если локального сервера нет, можно использовать какой-нибудь Web-сервер в Интернет. Чтобы посмотреть как перевести доменное имя в IP-адрес обратитесь к Приложению 1..

### Шаг 9. Закрытие сокета

Каждая попытка создать сокет socket должна быть завершена вызовом функции closesocket(hSocket), где hSocket - хендл сокета.

### Шаг 10. Уничтожение сокета

Каждая попытка инициализировать сокет WSAStartUp должна быть завершена вызовом функции WSACleanUp.

### Приложение 1. Получение IP-адреса из доменного имени

Function DomaneNameToIP(DomaneName:String):String;
Type
TaPInAddr = Array [0..10] Of PInAddr;
PaPInAddr = ^TaPInAddr;
Var
phe: PHostEnt;
pptr: PaPInAddr;
i: Integer;
GInitData: TWSADATA;
wVersionRequested : WORD;
Begin {DomaneNameToIP}
wVersionRequested:=MakeWord(1,1);
WSAStartup(wVersionRequested, GInitData);
Result := '';
phe :=GetHostByName(PChar(DomaneName));
If phe = NIL Then Exit;
pptr := PaPInAddr(Phe^.h_addr_list);
i := 0;
While pptr^[i] <> Nil Do
Begin {While}
result:=StrPas(inet_ntoa(pptr^[i]^));
Inc(i);
End; {While}
WSACleanup;
End; {DomaneNameToIP}

### Приложение 2. Получение доменного имени из IP-адреса

Function IpToDomaneName(IP:String):String;
Var
wVersionRequested : WORD;
wsaData : TWSAData;
Addr:Integer; p : PHostEnt;
Begin {IpToDomaneName}
Result:='Can''t reslove host';
wVersionRequested := MAKEWORD(1, 1);
WSAStartup(wVersionRequested, wsaData);
Addr:=inet_addr(PChar(IP));
p := GetHostByAddr(@Addr,128,AF_INET);
WSACleanup;
If p<>Nil Then Result:=p^.h_Name;
End; {IpToDomaneName}