В прошлой статье мы рассмотрели основы работы с бибилиотекой WinSock, и
научились устанавливать соединения, в этой части мы научимся делать то,
собственно ради чего и устанавливается соединение :) Чаще всего для обмена данными.
Предлагаю сразу перейти к делу, расчитовая на то, что у тебя уже имеется
примерно следующий код:
uses WinSock;
var
Sock: TSocket;
procedure TForm1.Button1Click(Sender: TObject);
var
Addr: sockaddr_in;
WSAData: TWSAData;
begin
WSAStartup( $0101, WSAData );
Sock := Socket( PF_INET, SOCK_STREAM, IPPROTO_IP );
with Addr do
begin
sin_family := PF_INET;
sin_port := htons( 12345 );
sin_addr.S_addr := inet_addr( '127.0.0.1' );
end;
if Connect( Sock, Addr, SizeOf( Addr ) ) = SOCKET_ERROR then
Button2.Click;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
CloseSocket( Sock );
WSACleanup;
end;
Это код клиетской части, который я приводил в 1-ой части статьи.
Допустим что клиенту всё таки удалось приконектица, после этого
необходимо знать когда именно приходят данные посылаемые нам сервером,
когда сервер закрывает соединение ... вобщем отлавивать события. Для
того чтобы можно было отлавливать события применим функцию
WSAAsyncSelect:
function WSAAsyncSelect( s: Integer; HWindow: HWND; wMsg: Integer; IEvent: Integer ) : Integer;
s - Уже инициализируемый сокет, в нашем случаем "Sock".
HWindow - дескриптор окна которому будут посылться мессаги, мы укажем Handle формы.
wMsg - Идентификатор сообщения(будет опредилять вручную).
IEvent - Тут мы должны перечислить события которые хотим отлавливать.
Вот самые основные:
FD_ACCEPT - Происходит в момент подключения клиента к серверу.(только для сервера)
FD_READ - Тут всё понятно.(Чтение)
FD_CLOSE - Здесь тоже ничего сложного ) (Разрыв связи)
Эту ф-цию будем использовать только после того как клиент был подключен
к серверу,а если для сервера, то после того как север успешно
запустился. В случае неудачи, ф-ция вернёт SOCKET_ERROR.
Из всех перечисленных аргументов данной ф-ции нам нехватает только одного -
идентификатора сообщения которое будет посылаться окну чей Handle мы укажем,
как я уже говорил это будет Handle формы, обьявим константу:
const
WM_SOCKET = WM_USER + 100; // Наш идентификатор сообщения
...
После чего для того чтобы отловить обозначенное нами сообщение, воспользуемя известным способом:
Создадим процедуру которой мы будем ловить эту мессагу, для этого
в разделе "private" у формы обьявим её:
...
type
TForm1 = class(TForm)
private
procedure SocketEvent(var Msg: TSockMsg); Message WM_SOCKET;
...
Единственным параметром этой процедуры является структура TSockMsg, которую мы должы описать в самом начале модуля:
unit Unit1;
interface
uses
Windows, Messages, WinSock ...;
const
WM_SOCKET = WM_USER + 100; // Наш идентификатор сообщения
type
TSockMsg = record // Структура
Msg: UINT;
Sock: Integer;
SelectEvent: WORD;
SelectError: WORD;
Result: LongInt;
end;
...
И опишем саму процедуру:
...
procedure TForm1.SocketEvent(var Msg: TSockMsg);
begin
case Msg.SelectEvent of
FD_ACCPET: ...;
FD_READ: ...;
FD_CLOSE: ...;
end;
end;
...
Таким образом мы подготовились к отлову сообщений, и можно
применить ф-цию WSAAsyncSelect.
Усовершенствуем код:
uses WinSock;
var
Sock: TSocket;
procedure TForm1.Button1Click(Sender: TObject);
var
...
begin
...
if Listen( Sock, SOMAXCONN ) SOCKET_ERROR then
WSAAsyncSelect( Sock, Handle, WM_SOCKET, FD_ACCEPT or FD_READ or FD_CLOSE )
else
Button2.Click;
end;
В случае с клиентом эту ф-цию вызывай только после успешного коннекта, т.е после ф-ции Connect:
uses WinSock;
var
Sock: TSocket;
procedure TForm1.Button1Click(Sender: TObject);
var
...
begin
...
if Connect( Sock, Addr, SizeOf( Addr ) ) SOCKET_ERROR then
WSAAsyncSelect( Sock, Handle, WM_SOCKET, FD_READ or FD_CLOSE )
else
Button2.Click;
end;
Перейдём к рассмотрению действий которые необходимо выполнять в момент
получения данных, начнём с клиента потому что с ним всё немного проще
чем с сервером.Вернёмся к процедуре которая ловит мессагу WM_SOCKET, и
научимся получать данные.
Для получения данных используется ф-ция Recv:
function Recv( s: Integer; var Buff; Count: Integer; Flags: Integer) : Integer;
Первым аргуметном является сокет на который пришли данные.
Вторым, - буфер, т.е во что должны поместиться данные.
Третьим, - кол-во байт которые необходимо получить.
Четвёртый, - дополнительные параметры чтения, в нашем
случае не используются.
Если при получении произойдёт ошибка, ф-ция вернёт SOCKET_ERROR, иначе кол-во полученных байт.
procedure TForm1.SocketEvent(var Msg: TSockMsg);
var
S: String; // Для получения данных в виде строки
Buff: array [0..4096] of Byte; // Допустим это буфер
DataLen: LongInt; // Сюда поместиться кол-во полученных байт
begin
case Msg.SelectEvent of
...
FD_READ: begin
ioctlsocket( Sock, FIONREAD, DataLen ); // Узнаём кол-во байт
SetLength( S, DataLen ); // Устанавливаем длину
// Получаем в виде строки:
if Recv( Sock, PChar( S )^, DataLen, 0 ) = SOCKET_ERROR then
ShowMessage( 'Recv Error !' );
// А так получать в буфер:
if Recv( Sock, Buff, DataLen, 0 ) = SOCKET_ERROR then
ShowMessage( 'Recv Error !' );
end;
end;
Прошу заметить, то что в примере ф-ция Recv вызывается 2 раза подрят,
и в обоих случаях к качестве кол-ва байт которое нужно получить, указовается
DataLen, а в этой переменной храниться общее кол-во байт которые доступны
для получения, это значет что при получении данных в буфер произойдёт ошибка,
надеюсь понятно почему ) я так сделал только ради того чтоб показать как
получать данные в виде строки, а как в буфер.
С клиентом можно заканчивать, потому что, с теми тействиями которые надо
выполнить в момент подключения и отключения, я думаю ты разберёшься сам :)
Перейдём к серверу.
В момент установления связи с сервером, первое событие которое
произойдёт это - FD_ACCEPT, в этот момент сервер должен принять
клиента, для этого предназначена функция accept:
function Accept( s: Integer; addr: PSockAddr; AddrLen: PInteger ) : Integer;
(Accept - перевод с англ. "Принять")
s - Сюда подставим указатель на клиента который подключился(см. ниже)
addr - Суда поместиться информация о клиетне.
AddrLen - Здесь мы должны указать длину addr.
Если ф-ция выполниться успешно, она создаст новый сокет, который будет
идентифицировать клиента, в противном случае вернёт INVALID_SOCKET.
Посмотрим какие действия необходио выполнить в момент подлючения клиента:
procedure TForm1.SocketEvent(var Msg: TSockMsg);
var
...
RemoteAddr: sockaddr_in;
RemoteAddrLen: Integer;
begin
RemoteAddrLen := SizeOf( sockaddr_in );
case Msg.SelectEvent of
FD_ACCEPT: if Accept( Msg.Sock, @RemoteAddr, @RemoteAddrLen ) = INVALID_SOCKET then ShowMessage('Accept error');
...
end;
end;
Теперь разберёмся с тем, как сервер должен получать данные:
procedure TForm1.SocketEvent(var Msg: TSockMsg);
var
S: String;
Buff: array [0..4096] of Byte;
DataLen: LongInt;
...
begin
case Msg.SelectEvent of
...
FD_READ: begin
ioctlsocket( Msg.Sock, FIONREAD, DataLen );
SetLength( S, DataLen );
if Recv( Msg.Sock, PChar( S )^, DataLen, 0 ) = SOCKET_ERROR then
ShowMessage( 'Recv Error !' );
if Recv( Msg.Sock, Buff, DataLen, 0 ) = SOCKET_ERROR then
ShowMessage( 'Recv Error !' );
end;
end;
Т.е практически всё тоже что и с клиентом, за исключением того что
вместо сокета сервера, подставляем сокет который помещён в структуру
TSockMsg.
Всё, самое сложное позади ) Осталось научиться отправлять данные,
это делается очень просто - ф-цией Send.
Ф-ция Send в принципе по первым 3-ём параметрам аналогична ф-ции
Recv:
Первым, - сокет c которого будем отправлять данные.
Вторым, - буфер, т.е сами данные для отправки.
Третьим, - кол-во отправляемых байт.
При ошибке вернёт SOCKET_ERROR, иначе кол-во отправленных байт.
Пример(для клиента):
var
S: String;
Buff: array [0..4096] of Byte;
...
begin
...
Send( Sock, PChar( S )^, Length( S ) ); // Строка
Send( Sock, Buff, SizeOf ( Buff ) ); // Буфер
...
end;
Сервером отправка данных осуществяется так же, но, вместо сокета
сервера "Sock" , указывается сокет клиента которому будет отправленно
сообщение. Сокет клиента возвращяет ф-ция accept о которой было сказано
выше.
Пример:
var
...
ClientSock: TSocket; // обьявим ещё одну глобальную переменную
...
procedure TForm1.SocketEvent(var Msg: TSockMsg);
var
...
RemoteAddr: sockaddr_in;
RemoteAddrLen: Integer;
begin
case Msg.SelectEvent of
FD_ACCEPT: ClientSock := Accept( Msg.Sock, @RemoteAddr, @RemoteAddrLen );
...
end;
end;
Таким образом был получен сокет идентифицирующий клиента, естественно что
клиент может быть не один :) Как запомнить каждого клиента предлагаю тебе разобраться самому ;)
Пример отправления данных клиенту:
var
S: String;
...
begin
...
if Send( ClientSock, PChar( S )^, Length( S ) ) Length( S ) then
ShowMessage( 'Ошибка при отправке строки' );
...
end;
Ну вот наверно и всё :) Благодарю за внимание.
|