Files
ewsdr/CATTcp.pas
T

535 lines
15 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
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 CATTcp;
{
CATTcp.pas — многопоточный TCP-сервер команд CAT.
Архитектура:
• Один listening socket (TCATTcpServer)
• На каждое входящее соединение — отдельный поток (TCATTcpClientThread)
• Команды вида "ZZFA00014250000;" обрабатываются через TCATEngine.Parse()
• Поддержка ZZGA/ZZGR для идентификации клиентов (directed broadcast)
• Welcome-строка при подключении
Порт по умолчанию: 4992
Платформы: Linux / Windows (через WebUtils.SockClose/SockShutdown).
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils,
WebUtils, // SockClose, SockShutdown, SockSend, SOCK_INVALID
{$IFDEF MSWINDOWS}
Windows, WinSock2,
{$ELSE}
BaseUnix, Sockets,
{$ENDIF}
SyncObjs,
CATEngine;
const
CAT_TCP_DEFAULT_PORT = 4992;
CAT_TCP_BUF_SIZE = 4096;
CAT_TCP_WELCOME = '#EWSDR CAT TCP Server;';
type
TCATTcpServer = class;
{ TCATTcpClientThread — один поток на TCP клиента }
TCATTcpClientThread = class(TThread)
private
FServer: TCATTcpServer;
FSocket: TSocket;
FBuf: string;
FClientIds: TStringList;
FIdLock: TCriticalSection;
FMarkDelete: Boolean;
procedure ProcessBuffer;
procedure SendStr(const S: string);
procedure HandleCmd(const Cmd: string);
procedure AddClientId(const Id: string);
procedure RemoveClientId(const Id: string);
protected
procedure Execute; override;
public
constructor Create(AServer: TCATTcpServer; ASocket: TSocket);
destructor Destroy; override;
procedure SendData(const Msg: string; const IdLimit: TStringList = nil);
function IsMarkedForDelete: Boolean;
procedure Disconnect;
end;
{ TCATTcpServer }
TCATTcpServer = class
private
FEngine: TCATEngine;
FPort: Integer;
FListenSock: TSocket;
FServerThread: TThread;
FPurgeThread: TThread;
FClients: TList;
FClientsLock: TCriticalSection;
FRunning: Boolean;
FSendWelcome: Boolean;
FLastError: string;
FStopEvent: TEvent;
procedure ServerLoop;
procedure PurgeLoop;
function InitListen: Boolean;
public
constructor Create(AEngine: TCATEngine; APort: Integer = CAT_TCP_DEFAULT_PORT);
destructor Destroy; override;
procedure Start;
procedure Stop;
procedure Broadcast(const Msg: string; const IdLimit: TStringList = nil);
function ClientCount: Integer;
property Port: Integer read FPort write FPort;
property SendWelcome: Boolean read FSendWelcome write FSendWelcome;
property Running: Boolean read FRunning;
property LastError: string read FLastError;
end;
implementation
{ ── internal thread wrappers ─────────────────────────────────────────────── }
type
TServerThread = class(TThread)
private FServer: TCATTcpServer;
protected procedure Execute; override;
public constructor Create(S: TCATTcpServer);
end;
TPurgeThread = class(TThread)
private FServer: TCATTcpServer;
protected procedure Execute; override;
public constructor Create(S: TCATTcpServer);
end;
constructor TServerThread.Create(S: TCATTcpServer);
begin FServer := S; FreeOnTerminate := False; inherited Create(True); end;
procedure TServerThread.Execute; begin FServer.ServerLoop; end;
constructor TPurgeThread.Create(S: TCATTcpServer);
begin FServer := S; FreeOnTerminate := False; inherited Create(True); end;
procedure TPurgeThread.Execute; begin FServer.PurgeLoop; end;
{ ── TCATTcpClientThread ───────────────────────────────────────────────────── }
constructor TCATTcpClientThread.Create(AServer: TCATTcpServer; ASocket: TSocket);
begin
FServer := AServer;
FSocket := ASocket;
FBuf := '';
FMarkDelete := False;
FClientIds := TStringList.Create;
FIdLock := TCriticalSection.Create;
FreeOnTerminate := False;
inherited Create(True);
end;
destructor TCATTcpClientThread.Destroy;
begin
FClientIds.Free;
FIdLock.Free;
inherited;
end;
procedure TCATTcpClientThread.AddClientId(const Id: string);
var lo: string;
begin
lo := LowerCase(Id);
FIdLock.Acquire;
try
if FClientIds.IndexOf(lo) < 0 then FClientIds.Add(lo);
finally FIdLock.Release; end;
end;
procedure TCATTcpClientThread.RemoveClientId(const Id: string);
var lo: string; idx: Integer;
begin
lo := LowerCase(Id);
FIdLock.Acquire;
try
idx := FClientIds.IndexOf(lo);
if idx >= 0 then FClientIds.Delete(idx);
finally FIdLock.Release; end;
end;
procedure TCATTcpClientThread.SendStr(const S: string);
// TCP не обязан отдавать весь буфер за один send: длинный ответ (IF, ZZEB,
// список режимов) мог уехать обрезанным — и молча, потому что усечение здесь
// не ошибка. Дописываем остаток, пока он есть.
// Пишем через WebUtils.SockSend, а не голым fpSend: он ставит MSG_NOSIGNAL.
// Без него запись в закрытый клиентом сокет — SIGPIPE, а тот по умолчанию
// убивает весь процесс; с ним send просто вернёт -1, и клиент выбрасывается.
var
buf: AnsiString;
sent, n: Integer;
begin
if FSocket = SOCK_INVALID then Exit;
buf := AnsiString(S);
if Length(buf) = 0 then Exit;
sent := 0;
while sent < Length(buf) do
begin
n := SockSend(FSocket, @buf[sent + 1], Length(buf) - sent, 0);
if n <= 0 then
begin
FMarkDelete := True;
Terminate;
Exit;
end;
Inc(sent, n);
end;
end;
procedure TCATTcpClientThread.HandleCmd(const Cmd: string);
const
ZZGA_PFX = 'ZZGA';
ZZGR_PFX = 'ZZGR';
var
cmd2, guid, resp: string;
begin
cmd2 := Trim(Cmd);
if cmd2 = '' then Exit;
if (Length(cmd2) >= 4) and (UpperCase(Copy(cmd2, 1, 4)) = ZZGA_PFX) then begin
if Length(cmd2) >= 40 then begin // 4 + 36
guid := LowerCase(Copy(cmd2, 5, 36));
AddClientId(guid);
SendStr(ZZGA_PFX + guid + ';');
end else
SendStr('?;');
end else if (Length(cmd2) >= 4) and (UpperCase(Copy(cmd2, 1, 4)) = ZZGR_PFX) then begin
if Length(cmd2) >= 40 then begin
guid := LowerCase(Copy(cmd2, 5, 36));
RemoveClientId(guid);
SendStr(ZZGR_PFX + guid + ';');
end else
SendStr('?;');
end else begin
resp := FServer.FEngine.Parse(cmd2);
if resp <> '' then SendStr(resp);
end;
end;
procedure TCATTcpClientThread.ProcessBuffer;
var tpos: Integer; msg: string;
begin
repeat
tpos := Pos(';', FBuf);
if tpos = 0 then Break;
msg := Copy(FBuf, 1, tpos);
Delete(FBuf, 1, tpos);
HandleCmd(msg);
until tpos = 0;
if Length(FBuf) > 1024 then FBuf := '';
end;
procedure TCATTcpClientThread.Execute;
var
rawbuf: array[0..CAT_TCP_BUF_SIZE-1] of Byte;
n: Integer;
chunk: AnsiString;
begin
if FServer.SendWelcome then
SendStr(CAT_TCP_WELCOME);
while not Terminated do begin
{$IFDEF MSWINDOWS}
n := recv(FSocket, rawbuf[0], CAT_TCP_BUF_SIZE, 0);
if n = SOCKET_ERROR then begin FMarkDelete := True; Break; end;
{$ELSE}
n := fpRecv(FSocket, @rawbuf[0], CAT_TCP_BUF_SIZE, 0);
if n <= 0 then begin FMarkDelete := True; Break; end;
{$ENDIF}
SetLength(chunk, n);
Move(rawbuf[0], chunk[1], n);
FBuf := FBuf + string(chunk);
ProcessBuffer;
end;
SockShutdown(FSocket);
SockClose(FSocket);
FSocket := SOCK_INVALID;
end;
procedure TCATTcpClientThread.SendData(const Msg: string; const IdLimit: TStringList);
var
i: Integer;
doSend: Boolean;
begin
if IdLimit = nil then begin
SendStr(Msg);
Exit;
end;
doSend := False;
FIdLock.Acquire;
try
for i := 0 to FClientIds.Count-1 do
if IdLimit.IndexOf(FClientIds[i]) >= 0 then begin
doSend := True;
Break;
end;
finally FIdLock.Release; end;
if doSend then SendStr(Msg);
end;
function TCATTcpClientThread.IsMarkedForDelete: Boolean;
begin Result := FMarkDelete; end;
procedure TCATTcpClientThread.Disconnect;
begin
Terminate;
SockShutdown(FSocket);
end;
{ ── TCATTcpServer ─────────────────────────────────────────────────────────── }
constructor TCATTcpServer.Create(AEngine: TCATEngine; APort: Integer);
{$IFDEF MSWINDOWS}
var wsa: TWSAData;
{$ENDIF}
begin
inherited Create;
FEngine := AEngine;
FPort := APort;
FListenSock := SOCK_INVALID;
FClients := TList.Create;
FClientsLock := TCriticalSection.Create;
FStopEvent := TEvent.Create(nil, True, False, '');
FRunning := False;
FSendWelcome := True;
FLastError := '';
{$IFDEF MSWINDOWS}
WSAStartup($0202, wsa);
{$ENDIF}
end;
destructor TCATTcpServer.Destroy;
begin
Stop;
FClients.Free;
FClientsLock.Free;
FStopEvent.Free;
{$IFDEF MSWINDOWS}
WSACleanup;
{$ENDIF}
inherited;
end;
function TCATTcpServer.InitListen: Boolean;
var
Addr: {$IFDEF MSWINDOWS}TSockAddrIn{$ELSE}TInetSockAddr{$ENDIF};
One: Integer;
begin
Result := False;
{$IFDEF MSWINDOWS}
FListenSock := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
{$ELSE}
FListenSock := fpSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
{$ENDIF}
if FListenSock = SOCK_INVALID then begin
FLastError := 'Cannot create socket';
Exit;
end;
One := 1;
FillChar(Addr, SizeOf(Addr), 0);
Addr.sin_family := AF_INET;
Addr.sin_port := htons(FPort);
{$IFDEF MSWINDOWS}
setsockopt(FListenSock, SOL_SOCKET, SO_REUSEADDR, PChar(@One), SizeOf(One));
Addr.sin_addr.S_addr := INADDR_ANY;
if bind(FListenSock, @Addr, SizeOf(Addr)) = SOCKET_ERROR then begin
FLastError := 'bind failed';
SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit;
end;
if listen(FListenSock, 8) = SOCKET_ERROR then begin
FLastError := 'listen failed';
SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit;
end;
{$ELSE}
fpSetSockOpt(FListenSock, SOL_SOCKET, SO_REUSEADDR, @One, SizeOf(One));
Addr.sin_addr.s_addr := htonl(INADDR_ANY);
if fpBind(FListenSock, @Addr, SizeOf(Addr)) <> 0 then begin
FLastError := 'bind failed on port ' + IntToStr(FPort);
SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit;
end;
if fpListen(FListenSock, 8) <> 0 then begin
FLastError := 'listen failed';
SockClose(FListenSock); FListenSock := SOCK_INVALID; Exit;
end;
{$ENDIF}
Result := True;
end;
procedure TCATTcpServer.Start;
begin
if FRunning then Exit;
if not InitListen then Exit;
FRunning := True;
FServerThread := TServerThread.Create(Self);
TServerThread(FServerThread).Start;
FPurgeThread := TPurgeThread.Create(Self);
TPurgeThread(FPurgeThread).Start;
end;
procedure TCATTcpServer.Stop;
var
i: Integer;
cli: TCATTcpClientThread;
begin
if not FRunning then Exit;
FRunning := False;
// Будим PurgeLoop немедленно, чтобы он не ждал 5 секунд
FStopEvent.SetEvent;
// Закрываем listen socket — разблокирует fpAccept в ServerLoop
if FListenSock <> SOCK_INVALID then begin
SockShutdown(FListenSock);
SockClose(FListenSock);
FListenSock := SOCK_INVALID;
end;
// Разрываем всех клиентов до ожидания потоков
FClientsLock.Acquire;
try
for i := 0 to FClients.Count-1 do
TCATTcpClientThread(FClients[i]).Disconnect;
finally FClientsLock.Release; end;
if Assigned(FServerThread) then begin
FServerThread.Terminate;
FServerThread.WaitFor;
FreeAndNil(FServerThread);
end;
if Assigned(FPurgeThread) then begin
FPurgeThread.Terminate;
FPurgeThread.WaitFor;
FreeAndNil(FPurgeThread);
end;
// Теперь безопасно ждём завершения клиентских потоков и освобождаем
FClientsLock.Acquire;
try
for i := 0 to FClients.Count-1 do begin
cli := TCATTcpClientThread(FClients[i]);
cli.WaitFor;
cli.Free;
end;
FClients.Clear;
finally FClientsLock.Release; end;
FStopEvent.ResetEvent;
end;
procedure TCATTcpServer.ServerLoop;
var
cli: TCATTcpClientThread;
clientSock: TSocket;
clientAddr: {$IFDEF MSWINDOWS}TSockAddrIn{$ELSE}TInetSockAddr{$ENDIF};
addrLen: {$IFDEF MSWINDOWS}Integer{$ELSE}TSockLen{$ENDIF};
begin
addrLen := SizeOf(clientAddr);
while FRunning do begin
{$IFDEF MSWINDOWS}
clientSock := accept(FListenSock, @clientAddr, @addrLen);
if clientSock = INVALID_SOCKET then Break;
{$ELSE}
clientSock := fpAccept(FListenSock, @clientAddr, @addrLen);
if clientSock = SOCK_INVALID then Break;
{$ENDIF}
if not FRunning then begin SockClose(clientSock); Break; end;
cli := TCATTcpClientThread.Create(Self, clientSock);
FClientsLock.Acquire;
try FClients.Add(cli);
finally FClientsLock.Release; end;
cli.Start;
end;
end;
procedure TCATTcpServer.PurgeLoop;
var
i: Integer;
cli: TCATTcpClientThread;
del: TList;
begin
del := TList.Create;
try
while FRunning do begin
if FStopEvent.WaitFor(5000) <> wrTimeout then Break;
del.Clear;
FClientsLock.Acquire;
try
for i := FClients.Count-1 downto 0 do begin
cli := TCATTcpClientThread(FClients[i]);
if cli.IsMarkedForDelete then begin
del.Add(cli);
FClients.Delete(i);
end;
end;
finally FClientsLock.Release; end;
for i := 0 to del.Count-1 do begin
cli := TCATTcpClientThread(del[i]);
cli.WaitFor;
cli.Free;
end;
end;
finally del.Free; end;
end;
procedure TCATTcpServer.Broadcast(const Msg: string; const IdLimit: TStringList);
var
i: Integer;
cli: TCATTcpClientThread;
begin
FClientsLock.Acquire;
try
for i := 0 to FClients.Count-1 do begin
cli := TCATTcpClientThread(FClients[i]);
if not cli.IsMarkedForDelete then
cli.SendData(Msg, IdLimit);
end;
finally FClientsLock.Release; end;
end;
function TCATTcpServer.ClientCount: Integer;
var i: Integer;
begin
Result := 0;
FClientsLock.Acquire;
try
for i := 0 to FClients.Count-1 do
if not TCATTcpClientThread(FClients[i]).IsMarkedForDelete then Inc(Result);
finally FClientsLock.Release; end;
end;
end.