English Home PageGerman Home PageUkrainian Home Page
 
Пример сокетных соединений (пересылка буфера).
Автор: CyANiD
Дата: 20.10.04.
На этот раз я решил рассмотреть пересылку не только текстовой информации. Нам приходило несколько писем с просьбами продолжить серию статей о сокетных соединениях. Спасибо что оценили, это стало мне стимулом при написании этой статьи.
Приступим. Для начала рекомендую скачать исходники (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 не добавляются. Сервер получает их и рассылает всем, поэтому этот же клиент и получит свое сообщение, после чего его добавит. Это позволяет серверу творить различные безумия с сообщениями (цензура, например, если Вы пишите чат).
Сайт создан в системе uCoz