Авторские статьи Облегчённая работа с сокетами Delphi

Discussion in 'Статьи' started by #Smith, 23 Feb 2011.

  1. #Smith

    #Smith New Member

    Joined:
    20 Jun 2010
    Messages:
    96
    Likes Received:
    2
    Reputations:
    0
    Проект mySockLib


    Краткое предисловие:

    Сегодня я решил опубликовать одну из своих сетевых библиотек, написанную ещё давно для облегчённой работы с сокетами.
    Эта библиотека довольно сильно облегчает создание различных клиентских приложений и не подгружает много лишнего кода.


    Описание функционала:

    function mySock_Startup: Integer; - инициализирует сетевую библиотеку WinSock2 для дальнейшей работы.
    Даже в многопоточном приложении вполне достаточно одного вызова инициализации библиотеки.
    После завершения работы с библиотекой должна быть вызвана функция деинциализации mySock_Cleanup столько же раз,
    сколько была вызвана функция инициализации.

    function mySock_Open(Host: String; Port: Word): TSocket; - создаёт сокет и выполняет соединение.
    В случае успешного вызова возвращает дескриптор сокета, в случае ошибки возвращает 0.
    В параметре Host может быть передано как и доменное имя, так и IP адрес.

    function mySock_SetTimeout(Sock: TSocket; millisec: Integer): Integer; - задаёт время жизни сокета в миллисекундах. Например, если вы хотите задать сокету время жизни равное 10 секундам - то параметр millisec должен быть равен 10000.
    После истечения таймаута вы уже не сможете работать с этим сокетом и вам придётся его закрыть.

    function mySock_SetBlockMode(Sock: TSocket; NoneBlock: Boolean): Integer; - Несложно догадатьсе, что данная функция задаёт режим работы сокета.
    Соответственно, если параметр NoneBlock будет равен True - то сокет будет переведён в неблокирующий, в случае с False - сокет будет переведён в блокирующий режим.
    Различия блокирующего и неблокирующего режимов состоят в том, что в блокирующем режиме сокет будет ждать, пока данные отправятся/примуться, что вызывает так сказать блокирование и подвисание программы на этом этапе.
    В неблокирующем режиме сокет не будет ждать данных и в случае ошибки сразу же вернёт -1.
    Так как данные всёже не приходят и не уходят мгновенно - то рекомендую перед приёмом/передачей использовать функции mySock_WaitRecv/mySock_WaitSend соответственно.

    function mySock_WaitSend(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean; - как я уже сказал выше, данная функция проверяет готовность сокета к отправке данных втечении определённого таймаута.
    В случае вызова без параметров mySock_WaitSend(mySock) - интервал ожидания булет равен 15 секундам.
    В случае, если за указанный интервал сокет будет готов к передаче данных - то функция вернёт True иначе False
    При использовании данной функции вам следует учитывать, что если процесс затянется - то это вызовет блокировку (подвисание) программы на указанный интервал.
    Пример вызова для ожидания в 10.450 секунд:
    mySock_WaitSend(mySock, 10, 450)

    function mySock_WaitRecv(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean; - вызов данной функции аналогичен предыдущей, заисключением что она используется для ожидания готовности принять данные.

    function mySock_Send(Sock: TSocket; Buff: Pointer; Size: Integer): Integer; - функция предназначена для передачи данных.
    В случае успешной передачи вернёт количество переданных байт.
    В случае ошибки вернёт -1.
    Buff - указатель на данные, которые нужно передать.
    Size - объём переданных данных в байтах.
    Пример передачи текстового заголовка req: String:
    mySock_Send(mySock, PChar(req), Length(req));
    Важно понимать, что если предстоит передать/принять некоторый объём данных - то не обязательно передавать/принимать всё сразу.
    Это можно делать порциями, используя промежуточный буффер, равный например 1024 байта

    function mySock_Recv(Sock: TSocket; Buff: Pointer; Size: Integer): Integer; - вызов этой функции немного отличается от вызова предыдущей.
    Часто мы заранее не знаем, какой объём данных нам предстоит принять.
    Поэтому оптимальным решением будет выделить некоторый объём памяти под временный буффер, а после получения всех данных его освободить.
    Функция может вернуть целое положительное число в случае удачной приёмки данных,
    0 - в случае закрытия сервером сокета и отсутствии данных для приёма и
    соответственно -1 - в случае ошибки.

    Пример организации приёмки данных в цикле:
    Code:
        { Пытаемся взять память для буфера }
        Buff := GetMemory(BuffSize);
        if (Buff = nil) then raise Exception.Create('Error! Cannot get memory!');
        Size := 0;
        Stream.Position := 0;
        Resp := '';
        try
          Repeat
            { В случае таймаута ожидания данных - выходим }
            if not mySock_WaitRecv(Sock, TmSec, TmMillisec) then Exit;
            { Чистим память буфера }
            ZeroMemory(Buff, BuffSize);
            { Принимаем данные }
            iSize := mySock_Recv(Sock, Buff, BuffSize);
            { В случае ошибки - выходим }
            if (iSize < 0) then Exit;
            { Если данные были приняты - то дописываем данные к уже принятым и
              увеличиваем счётчик всего принятых байт }
            if (iSize <> 0) then
                                begin
                                  { Пример записи принятых данных в поток }
                                  Stream.Write(Buff^, iSize);
                                  { Пример добавления принятых данных в строку }
                                  Resp := Resp + Copy(Buff, 1, iSize);
                                  { Увеличиваем счётчик всего принятых байт }
                                  Inc(Size, iSize);
                                end;
          Until (iSize = 0);
        finally
          FreeMemory(Buff);
        end;
    function mySock_Close(Sock: TSocket): Integer; - закрывает сокет и освобождает все ресурсы, занимаемые им.

    function mySock_Cleanup: Integer; - как я уже говорил, эта функция предназначена для инициализации сокетной библиотеки.
    Не забывайте, вызывать деинициализацию после окончания пользования библиотекой.

    procedure mySock_GetLastError(var ErrCode: Integer; var ErrStr: String); - очень полезная процедура для отладочных целей.
    Я постарался собрать наиболее частые сетевые ошибки и составить их описания.
    В переменную ErrCode запишется код последней возникшей ошибки, а в переменную ErrStr - запишется её описание.


    Послесловие:

    Ну вот собственно и подошла к логическому финалу моя первая статья.
    Надеюсь, что кому-то это окажется полезным.
    Я постарался сделать исходник и описание наиболее простыми и понятными. Вы можете дополнить этот исходный код по своему усмотрению. Если что-то будет непонятно - пишите в этой теме, я постараюсь объяснить.


    Особую благодарность я хотел бы выразить пользователю под ником Slesh, а также всем, кто помогал мне в освоении темы сетевого программирования.





    Исходный код модуля:
    Code:
    unit mySockLib;
    
    interface
    
    uses
      Windows, WinSock2;
    
    type
      TSocket = DWORD;
    
    function mySock_Startup: Integer;
    function mySock_Open(Host: String; Port: Word): TSocket;
    function mySock_SetTimeout(Sock: TSocket; millisec: Integer): Integer;
    function mySock_SetBlockMode(Sock: TSocket; NoneBlock: Boolean): Integer;
    function mySock_WaitSend(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean;
    function mySock_WaitRecv(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean;
    function mySock_Send(Sock: TSocket; Buff: Pointer; Size: Integer): Integer;
    function mySock_Recv(Sock: TSocket; Buff: Pointer; Size: Integer): Integer;
    function mySock_Close(Sock: TSocket): Integer;
    function mySock_Cleanup: Integer;
    procedure mySock_GetLastError(var ErrCode: Integer; var ErrStr: String);
    
    implementation
    
    
    function mySock_Startup: Integer;
    var
      wData: TWSAData;
    begin
      Result := WSAStartup($0202, wData);
    end;
    
    function mySock_Open(Host: String; Port: Word): TSocket;
    var
      Sock: TSocket;
      HostEnt: PHostEnt;
      SockAddr: TSockAddrIn;
    begin
      Result := 0;
      HostEnt := GetHostByName(PChar(Host));
      if (HostEnt = nil) then Exit;
      Sock := Socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
      if (Sock = INVALID_SOCKET) then Exit;
      ZeroMemory(@SockAddr, SizeOf(SockAddr));
      With SockAddr do
        begin
          sin_family := AF_INET;
          sin_port := HToNS(Port);
          With sin_addr, HostEnt^ do
            s_addr := PDWORD(h_addr^)^;
        end;
      if (Connect(Sock, @SockAddr, SizeOf(SockAddr)) <> 0) then
                                                               begin
                                                                 CloseSocket(Sock);
                                                                 Exit;
                                                               end;
      Result := Sock;
    end;
    
    function mySock_SetTimeout(Sock: TSocket; millisec: Integer): Integer;
    var
      tv: TTimeVal;
    begin
      tv.tv_sec := 0;
      tv.tv_usec := millisec;
      Result := setsockopt(Sock, SOL_SOCKET, SO_RCVTIMEO, @tv, SizeOf(tv));
    end;
    
    function mySock_SetBlockMode(Sock: TSocket; NoneBlock: Boolean): Integer;
    var
      iMode: DWORD;
    begin
      iMode := DWORD(NoneBlock);
      Result := IOCtlSocket(Sock, FIONBIO, iMode);
    end;
    
    function mySock_WaitSend(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean;
    var
      FDSet: TFDSet;
      tv: TTimeVal;
    begin
      Result := False;
      FD_ZERO(FDSet);
      FD_SET(Sock, FDSet);
      tv.tv_sec := sec;
      tv.tv_usec := millisec * 1000;
      if (select(0, nil, @FDSet, nil, @tv) = 1) then Result := True;
    end;
    
    function mySock_WaitRecv(Sock: TSocket; sec: Integer = 15; millisec: Integer = 0): Boolean;
    var
      FDSet: TFDSet;
      tv: TTimeVal;
    begin
      Result := False;
      FD_ZERO(FDSet);
      FD_SET(Sock, FDSet);
      tv.tv_sec := sec;
      tv.tv_usec := millisec * 1000;
      if (select(0, @FDSet, nil, nil, @tv) = 1) then Result := True;
    end;
    
    function mySock_Send(Sock: TSocket; Buff: Pointer; Size: Integer): Integer;
    begin
      Result := send(Sock, Buff^, Size, 0);
    end;
    
    function mySock_Recv(Sock: TSocket; Buff: Pointer; Size: Integer): Integer;
    begin
      Result := recv(Sock, Buff^, Size, 0);
    end;
    
    function mySock_Close(Sock: TSocket): Integer;
    begin
      Result := CloseSocket(Sock);
    end;
    
    procedure mySock_GetLastError(var ErrCode: Integer; var ErrStr: String);
    begin
      ErrCode := WSAGetLastError;
      Case ErrCode of
       0: ErrStr := 'Ошибки нет';
       WSANOTINITIALISED: ErrStr := 'Нужно сначала вызвать функцию WSASturtup';
    	 WSAENETDOWN: ErrStr := 'Проблема с сетью';
    	 WSAEADDRINUSE: ErrStr := 'Указанный адрес уже используется';
    	 WSAEPROTONOSUPPORT: ErrStr := 'Протокол не поддерживается';
    	 WSAEPROTOTYPE: ErrStr := 'Некорректный протокол для данного типа сокета';
    	 WSAEFAULT: ErrStr := 'Параметры name и namelen не соответствуют данной адресации';
    	 WSAEINPROGRESS: ErrStr := 'Уже выполняется операция в блокирующем режиме';
    	 WSAEINVAL: ErrStr := 'Сокет уже связан с адресом';
       WSAESOCKTNOSUPPORT: ErrStr := 'Некорректный сокет для данной адресации';
    	 WSAENOBUFS: ErrStr := 'Недостаточно памяти';
    	 WSAENOTSOCK: ErrStr := 'Неверный дескриптор сокета';
       WSAEISCONN: ErrStr := 'Сокет уже подключён';
       WSAEMFILE: ErrStr := 'Нет больше доступных дескрипторов';
       else ErrStr := 'Неизвестная ошибка';
      end;
    end;
    
    function mySock_Cleanup: Integer;
    begin
      Result := WSACleanup;
    end;
    end.
     
    #1 #Smith, 23 Feb 2011
    Last edited: 24 Feb 2011
    1 person likes this.
  2. Chrome~

    Chrome~ Elder - Старейшина

    Joined:
    13 Dec 2008
    Messages:
    936
    Likes Received:
    162
    Reputations:
    27
    Кода немного, и это хорошо. Маленький модуль, который справляеться с поставленной целью. Для новичков сойдет!

    Настоятельно рекомендую избавиться от SysUtils в uses, он здесь ни к чему.
     
  3. #Smith

    #Smith New Member

    Joined:
    20 Jun 2010
    Messages:
    96
    Likes Received:
    2
    Reputations:
    0
    Верно.
    Поправил.