Пример сокетных соединений (пересылка буфера).

На этот раз я решил рассмотреть пересылку не только текстовой информации. Нам приходило несколько писем с просьбами продолжить серию статей о сокетных соединениях. Спасибо что оценили, это стало мне стимулом при написании этой статьи.
Приступим. Для начала рекомендую скачать исходники (6 КБ) всего здесь далее обговоренного. Писал на Delphi 7 (напоминаю, что сокетные компоненты просто так не лежат в палитре. Меню Component->Install Packages...->Add. Указываем файл $(DELPHI)\bin\dclsockets70.bpl). В конце концов просто pas-файл покорчуете.
Сама статья будет небольшим описанием всего что Вы там сможете найти. Конечно подсказки есть, но это так, для радования глаза (моего, не Вашего).
Начнем с начала. Создаем форму. Кидаем все необходимые компоненты. Перечень:
bClient: TClientSocket;
bServer: TServerSocket;
RichEdit: TRichEdit;
RadioGroup: TRadioGroup;
edAddress: TEdit;
btnGO: TButton;
edNick: TEdit;
btnSend: TButton;
edTxt: TEdit;
btnSys: TButton;
Указываем в сокетах (сокетные компоненты bClient и bServer, где сервер а где клиент думаю понятно) ОДИНАКОВЫЕ порты, плиз будьте внимательны. У меня порт 812, как название одной известной российской панк-группы! Обязательно зацените, если не слышали, отличный отечественный продукт (мне заплатили за рекламу :)).
edAddress нам нужен для указания адреса сервера, если это будет клиент.
RichEdit - собственно поле для вывода всего: сообщения, etc.
RadioGroup - позволит нам выбрать тип приложения: сервер или клиент.
btgGo - кнопуля, с помощью которой активируем или уничтожаем сокет.
btnSend - посылает текстовое сообщение.
btnSys - посылает системное сообщение (так, для примера).
edNick и edTxt - Ваш ник и текст сообщения соответственно.
Я создал для примера два типа:
 TMes = packed record
 ID: Word; {ID = 0}
 Nick: String[20];
 Txt: String[200];
 end;

TSys = packed record ID: Word; {ID = 1} Body: String[100]; end;

Первый просто сообщения с ником. Второй - системный. ID у TMes должно быть равно 0, а у TSys - 1.
Какие при создании типов нужно соблюдать правила?
Во-первых, если пишите там строки, то ограничивайте их, иначе баги обеспечены. Хотя знакомый предложил динамическую компоновку буфера, вместо рассмотренной здесь статической. Интересное дело, нам удалось в среднем сократить размер каждого пересылаемого пакета на 40%. Да и размер используемой программой памяти тоже. Если интересует, пишите на мыло, отвечу!
Во-вторых, если разные сообщения, то для их разделения нужны идентификаторы. Правильно? Обычно одного байта хватает (у Вас больше 256 типов?!), но в примере я использовал двухбайтовую переменную ID типа Word. Учтите, что идентификаторы должны быть первыми в описании записи. Не забудьте, что TCP-пакеты разбиваются, поэтому всегда следует хорошо продумывать их структуру.
Также у меня есть глобальная константа MaxSize. В ней хранится максимальный размер пакетов. Посидите, посчитайте на калькуляторе или создайте глобальную переменную, а при создании формы SizeOf'ом вычислите максимальный. Как хотите, но это значение потом понадобится.
Я предлагаю две процедуры, преобразующие пакет в буфер и наоборот:
procedure PctToBuf(var Packet; var buf; Size: Word);
var
 arPacket: array[1..MaxSize] of byte absolute Packet;
 arBuf: array[1..MaxSize] of byte absolute buf;
 i: Word;
begin
 for i:=1 to Size do
 arBuf[i]:=arPacket[i];
end;

procedure BufToPct(var Packet; var buf; Size: Word); var arPacket: array[1..MaxSize] of byte absolute Packet; arBuf: array[1..MaxSize] of byte absolute buf; i: Word; begin for i:=1 to Size do arPacket[i]:=arBuf[i]; end;

В принципе, в примере можно обойтись и без них, но при написании сложных программ они ой как нужны.
Далее обрабатываем клик по кнопке btnGo, т. е. мы либо запускаем, либо закрываем. А что, нам поможет выбрать RadioGroup, если ItemIndex равен 0, то клиент, иначе сервер.
procedure TfmMain.btnGOClick(Sender: TObject);
begin
 if RadioGroup.ItemIndex=0 then
 if bClient.Active then
 bClient.Active:=False {Останавливаем клиента}
 else begin
 bClient.Address:=edAddress.Text;
 bClient.Active:=True; {Запускаем клиента}
 Application.ProcessMessages;
 end
 else
 if bServer.Active then begin
 bServer.Active:=False; {Останавливаем сервер}
 RichEdit.SelAttributes.Color:=clBlack;
 RichEdit.Lines.Add('>> Server closed');
 end
 else begin
 bServer.Active:=True; {Запускаем сервер}
 RichEdit.SelAttributes.Color:=clBlack;
 RichEdit.Lines.Add('>> Server started');
 end;

{Далее меняем состояния компонент, интерфейс блин} if bClient.Active or bServer.Active then begin btnGo.Caption:='STOP'; btnSend.Enabled:=True; btnSys.Enabled:=True; edAddress.ReadOnly:=True; RadioGroup.Enabled:=False; end else begin btnGo.Caption:='GO!'; btnSend.Enabled:=False; btnSys.Enabled:=False; edAddress.ReadOnly:=False; RadioGroup.Enabled:=True; end; end;

Далее описываем все события клиента, ну почти все: onError, onConnecting, onConnect, onDisconnect. Всегда их обрабатывайте, в целях избежания недоразумений, коих в нашей жизни так много (Таня, привет :))…
Приступаем к описанию событий сервера. Все разжевывать не буду, и так все понятно из названий.
При нажатии btnSend посылаем буфер TMes. TMes заполняем значениями edNick.Text и edTxt.Text. Осталось только решить, кто его посылает, сервер или клиент. Вот решение:
procedure TfmMain.btnSendClick(Sender: TObject);
var
 Mes: TMes;
 buf: array[1..SizeOf(TMes)] of byte;
 i: Word;
begin
 Mes.ID:=0;
 if bClient.Active then begin
 Mes.Nick:=edNick.Text;
 Mes.Txt:=edTxt.Text;
 PctToBuf(Mes,buf,SizeOf(TMes)); {В принципе, это совсем не нужно! Это я так, для наглядности}
 bClient.Socket.SendBuf(buf,SizeOf(TMes));
 {bClient.Socket.SendBuf(mes,SizeOf(TMes)); Вот так сразу тоже можно!}
 end
 else
 if bServer.Socket.ActiveConnections>0 then begin
 Mes.Nick:=edNick.Text;
 Mes.Txt:=edTxt.Text;
 PctToBuf(Mes,buf,SizeOf(TMes));
 for i:=0 to bServer.Socket.ActiveConnections-1 do
 bServer.Socket.Connections[i].SendBuf(buf,SizeOf(TMes));
 RichEdit.SelAttributes.Color:=clBlue;
 RichEdit.Lines.Add(Mes.Nick+': '+Mes.Txt);
 end;
end;
Пошлем теперь и буфер TSys при нажатии btnSys:
procedure TfmMain.btnSysClick(Sender: TObject);
var
 Sys: TSys;
 str: String;
 i: Word;
begin
 Sys.ID:=1;
 Str:='Fuck admin!'; {Тривиально, извините}
 InputQuery('Сообщение','Текст',Str);
 Sys.Body:=Str;
 if bClient.Active then
 bClient.Socket.SendBuf(sys,SizeOf(TSys))
 else
 if bServer.Socket.ActiveConnections>0 then begin
 for i:=0 to bServer.Socket.ActiveConnections-1 do
 bServer.Socket.Connections[i].SendBuf(sys,SizeOf(TSys));
 RichEdit.SelAttributes.Color:=clRed;
 RichEdit.Lines.Add('>> '+Sys.Body);
 end;
end;
А как же принимать информацию? Вот как это выглядит у сервера:
procedure TfmMain.bServerClientRead(Sender: TObject;
 Socket: TCustomWinSocket);
var
 buf: array[1..MaxSize] of byte;
 Size: Word;
 ID: Word;
 i: Word;
 Sys: ^TSys;
 Mes: ^TMes;
begin
 Size:=Socket.ReceiveLength; {Принимаемое кол-во байт}
 Socket.ReceiveBuf(buf,Size); {Эта функция возвращает кол-во принятых байт.
 Можете сравнить с Size для корректности}
 BufToPct(ID,buf,2);
 case ID of
 0: begin
 New(Mes);
 BufToPct(Mes^,buf,Size);
 RichEdit.SelAttributes.Color:=clBlue;
 RichEdit.Lines.Add(Mes^.Nick+': '+Mes^.Txt);
 {Теперь отправим его всем}
 for i:=0 to bServer.Socket.ActiveConnections-1 do
 bServer.Socket.Connections[i].SendBuf(Mes^,Size);
 Dispose(Mes);
 end;
 1: begin
 New(Sys);
 BufToPct(Sys^,buf,Size);
 RichEdit.SelAttributes.Color:=clRed;
 RichEdit.Lines.Add('>> '+Sys^.Body);
 {Теперь отправим его всем}
 for i:=0 to bServer.Socket.ActiveConnections-1 do
 bServer.Socket.Connections[i].SendBuf(Sys^,Size);
 Dispose(Sys);
 end;
 end;
end;
Думаю понятно, что у клиента то же самое, только без отправки данных остальным.
Если есть вопросы, или Вы заметили у меня ошибки, пожалуйста пишите мне на мыло. Красивые девушки могут и просто так писать.
Что еще осталось сказать? Если Вы заметили, то при отправке сообщений клиентом они автоматически в RichEdit не добавляются. Сервер получает их и рассылает всем, поэтому этот же клиент и получит свое сообщение, после чего его добавит. Это позволяет серверу творить различные безумия с сообщениями (цензура, например, если Вы пишите чат).

 


Страница сайта http://silicontaiga.ru
Оригинал находится по адресу http://silicontaiga.ru/home.asp?artId=5682