Files
ewsdr/WsClient.pas
T

215 lines
6.4 KiB
ObjectPascal

{
Copyright (C)
2026 - Uladzimir Karpenka, EW8BAK
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
}
unit WsClient;
{
WsClient.pas — WebSocket клиент (одно соединение).
Инкапсулирует:
- TCP-сокет
- Состояние WS (handshake / open / closed)
- Буфер приёма
- Отправку raw-байт, WS-фреймов (text / binary)
- Basic-Auth флаг
}
{$IFDEF FPC}
{$MODE Delphi}
{$LONGSTRINGS ON}
{$ENDIF}
interface
uses
SyncObjs, WebUtils
{$IFDEF WINDOWS}, WinSock2{$ELSE}, Sockets{$ENDIF};
const
{ Приёмный буфер соединения. 4 КБ хватало командам и web-запросам, но блок
бинарного потока TCI (§3.4) — это заголовок 64 байта плюс data[16384],
и кадр крупнее буфера не собирается НИКОГДА: BufLen упирается в потолок и
разбор встаёт. Поэтому потолок держим с запасом на кадр целиком вместе с
маской и хвостом соседнего сообщения. }
WS_BUF_SIZE = 32768;
type
TWsState = (wsHandshake, wsOpen, wsClosed);
TWsClient = class
private
FSocket: TSocket;
FState: TWsState;
FLock: TCriticalSection;
FBuf: array[0..WS_BUF_SIZE-1] of Byte;
FBufLen: Integer;
FAuthed: Boolean;
public
constructor Create(ASocket: TSocket);
destructor Destroy; override;
{ Отправка raw-байт (вызывать держа FLock) }
function SendRaw(const Data; Len: Integer): Boolean;
{ Отправка WebSocket-фрейма (text или binary) }
function SendWsFrame(Opcode: Byte; const Data; Len: Integer): Boolean;
{ Текстовый WS-фрейм (opcode $01) }
function SendText(const S: string): Boolean;
{ Бинарный WS-фрейм (opcode $02) }
function SendBinary(const Data; Len: Integer): Boolean;
{ Читает данные в FBuf, возвращает кол-во байт (-1 = ошибка/закрыто) }
function Recv: Integer;
{ Указатель на начало буфера приёма }
function BufData: PByte; inline;
{ Ёмкость приёмного буфера: разбору фреймов нужен потолок, чтобы вовремя
закрыть соединение, а не встать намертво на несобираемом кадре. }
function BufCapacity: Integer; inline;
property Socket: TSocket read FSocket;
property State: TWsState read FState write FState;
property Authed: Boolean read FAuthed write FAuthed;
property Lock: TCriticalSection read FLock;
property BufLen: Integer read FBufLen write FBufLen;
end;
implementation
{ ═══════════════════════════════════════════════════════════════════════════
TWsClient
═══════════════════════════════════════════════════════════════════════════ }
constructor TWsClient.Create(ASocket: TSocket);
begin
inherited Create;
FSocket := ASocket;
FState := wsHandshake;
FBufLen := 0;
FAuthed := False;
FLock := TCriticalSection.Create;
end;
destructor TWsClient.Destroy;
begin
if FSocket <> SOCK_INVALID then
SockClose(FSocket);
FLock.Free;
inherited;
end;
function TWsClient.SendRaw(const Data; Len: Integer): Boolean;
var
Sent, R: Integer;
P: PByte;
begin
Result := False;
if (FSocket = SOCK_INVALID) or (Len <= 0) then Exit;
P := @Data;
Sent := 0;
while Sent < Len do
begin
R := SockSend(FSocket, P + Sent, Len - Sent, 0);
if R <= 0 then Exit;
Inc(Sent, R);
end;
Result := True;
end;
function TWsClient.SendWsFrame(Opcode: Byte; const Data; Len: Integer): Boolean;
var
Header: array[0..9] of Byte;
HLen: Integer;
P: PByte;
begin
Result := False;
if FState <> wsOpen then Exit;
// FIN=1 + opcode
Header[0] := $80 or (Opcode and $0F);
if Len <= 125 then
begin
Header[1] := Byte(Len);
HLen := 2;
end
else if Len <= 65535 then
begin
Header[1] := 126;
Header[2] := Byte(Len shr 8);
Header[3] := Byte(Len);
HLen := 4;
end
else
begin
Header[1] := 127;
Header[2] := 0; Header[3] := 0; Header[4] := 0; Header[5] := 0;
Header[6] := Byte(Len shr 24); Header[7] := Byte(Len shr 16);
Header[8] := Byte(Len shr 8); Header[9] := Byte(Len);
HLen := 10;
end;
FLock.Enter;
try
Result := SendRaw(Header[0], HLen);
if Result and (Len > 0) then
begin
P := @Data;
Result := SendRaw(P^, Len);
end;
// Ошибка отправки (таймаут SO_SNDTIMEO, разрыв) — немедленно закрываем
// клиент чтобы BroadcastBinary не держал FClientLock на следующих тиках.
if not Result then FState := wsClosed;
finally
FLock.Leave;
end;
end;
function TWsClient.SendText(const S: string): Boolean;
begin
if Length(S) = 0 then begin Result := True; Exit; end;
Result := SendWsFrame($01, S[1], Length(S));
end;
function TWsClient.SendBinary(const Data; Len: Integer): Boolean;
begin
Result := SendWsFrame($02, Data, Len);
end;
function TWsClient.Recv: Integer;
begin
Result := SockRecv(FSocket, @FBuf[FBufLen], SizeOf(FBuf) - FBufLen, 0);
if Result > 0 then Inc(FBufLen, Result);
end;
function TWsClient.BufData: PByte;
begin
Result := @FBuf[0];
end;
function TWsClient.BufCapacity: Integer;
begin
Result := SizeOf(FBuf);
end;
end.