mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-16 08:15:53 +01:00
1020 lines
28 KiB
ObjectPascal
1020 lines
28 KiB
ObjectPascal
|
{******************************************************************************}
|
|||
|
{ }
|
|||
|
{ Delphi cross platform socket library }
|
|||
|
{ }
|
|||
|
{ Copyright (c) 2017 WiNDDRiVER(soulawing@gmail.com) }
|
|||
|
{ }
|
|||
|
{ Homepage: https://github.com/winddriver/Delphi-Cross-Socket }
|
|||
|
{ }
|
|||
|
{******************************************************************************}
|
|||
|
unit Net.CrossSocket.Kqueue;
|
|||
|
|
|||
|
interface
|
|||
|
|
|||
|
uses
|
|||
|
System.SysUtils,
|
|||
|
System.Classes,
|
|||
|
System.Generics.Collections,
|
|||
|
Posix.SysSocket,
|
|||
|
Posix.NetinetIn,
|
|||
|
Posix.UniStd,
|
|||
|
Posix.NetDB,
|
|||
|
Posix.Pthread,
|
|||
|
Posix.Errno,
|
|||
|
BSD.kqueue,
|
|||
|
Net.SocketAPI,
|
|||
|
Net.CrossSocket.Base;
|
|||
|
|
|||
|
type
|
|||
|
TIoEvent = (ieRead, ieWrite);
|
|||
|
TIoEvents = set of TIoEvent;
|
|||
|
|
|||
|
TKqueueListen = class(TCrossListenBase)
|
|||
|
private
|
|||
|
FLock: TObject;
|
|||
|
FIoEvents: TIoEvents;
|
|||
|
|
|||
|
procedure _Lock; inline;
|
|||
|
procedure _Unlock; inline;
|
|||
|
|
|||
|
function _ReadEnabled: Boolean; inline;
|
|||
|
function _UpdateIoEvent(const AIoEvents: TIoEvents): Boolean;
|
|||
|
public
|
|||
|
constructor Create(const AOwner: TCrossSocketBase; const AListenSocket: THandle;
|
|||
|
const AFamily, ASockType, AProtocol: Integer); override;
|
|||
|
destructor Destroy; override;
|
|||
|
end;
|
|||
|
|
|||
|
PSendItem = ^TSendItem;
|
|||
|
TSendItem = packed record
|
|||
|
Data: PByte;
|
|||
|
Size: Integer;
|
|||
|
Callback: TCrossConnectionCallback;
|
|||
|
end;
|
|||
|
|
|||
|
TSendQueue = class(TList<PSendItem>)
|
|||
|
protected
|
|||
|
procedure Notify(const Value: PSendItem; Action: TCollectionNotification); override;
|
|||
|
end;
|
|||
|
|
|||
|
TKqueueConnection = class(TCrossConnectionBase)
|
|||
|
private
|
|||
|
FLock: TObject;
|
|||
|
FSendQueue: TSendQueue;
|
|||
|
FIoEvents: TIoEvents;
|
|||
|
FConnectCallback: TCrossConnectionCallback; // 用于 Connect 回调
|
|||
|
|
|||
|
procedure _Lock; inline;
|
|||
|
procedure _Unlock; inline;
|
|||
|
|
|||
|
function _ReadEnabled: Boolean; inline;
|
|||
|
function _WriteEnabled: Boolean; inline;
|
|||
|
function _UpdateIoEvent(const AIoEvents: TIoEvents): Boolean;
|
|||
|
public
|
|||
|
constructor Create(const AOwner: TCrossSocketBase; const AClientSocket: THandle;
|
|||
|
const AConnectType: TConnectType); override;
|
|||
|
destructor Destroy; override;
|
|||
|
end;
|
|||
|
|
|||
|
// KQUEUE 与 EPOLL 队列的差异
|
|||
|
// KQUEUE的队列中, 一个Socket句柄可以有多条记录, 每个事件一条,
|
|||
|
// 这一点和 EPOLL 不一样, EPOLL中每个Socket句柄只会有一条记录
|
|||
|
// 要监测多个事件时, 只需要将多个事件做位运算加在一起调用 epoll_ctl 即可
|
|||
|
//
|
|||
|
// EV_DISPATCH 和 EV_CLEAR 是令 kqueue 支持线程池的关键
|
|||
|
// 该参数组合可以令事件触发后就立即被禁用, 避免让同一个Socket的同一个事件
|
|||
|
// 同时被多个工作线程触发
|
|||
|
//
|
|||
|
// EVFILT_READ
|
|||
|
// 用于监测接收缓冲区是否可读了
|
|||
|
// 对于监听Socket来说,表示有新的连接到来
|
|||
|
// 对于已连接的Socket来说,表示有数据到达接收缓冲区
|
|||
|
// 为了支持线程池, 必须带上参数 EV_CLEAR or EV_DISPATCH
|
|||
|
// 该参数组合表示, 该事件一旦触发立即清除该事件的状态并禁用它
|
|||
|
// 处理完连接或者读取数据之后再投递一次 EVFILT_READ, 带上参数
|
|||
|
// EV_ENABLE or EV_CLEAR or EV_DISPATCH, 让事件继续监测
|
|||
|
//
|
|||
|
// EVFILT_WRITE
|
|||
|
// 用于监测发送缓冲区是否可写了
|
|||
|
// 对于Connect中的Socket,投递EV_ENABLE,等到事件触发时表示连接已建立
|
|||
|
// 对于已连接的Socket,在Send之后立即投递EVFILT_WRITE,等到事件触发时表示发送完成
|
|||
|
// 对于EVFILT_WRITE都应该带上EV_ONESHOT参数,让该事件只会被触发一次
|
|||
|
// 否则,只要发送缓冲区是空的,该事件就会一直触发,这并没有什么意义
|
|||
|
// 我们只需要用EVFILT_WRITE去监测连接或者发送是否成功
|
|||
|
//
|
|||
|
// KQUEUE 发送数据
|
|||
|
// 最好的做法是将实际发送数据的动作放到 EVFILT_WRITE 触发时进行, 该
|
|||
|
// 事件触发表明 Socket 发送缓存有空闲空间了。IOCP可以直接将待发送的数据及
|
|||
|
// 回调同时扔给 WSASend, 发送完成后去调用回调即可; KQUEUE 机制不一样, 在 KQUEUE
|
|||
|
// 中没有类似 WSASend 的函数, 只能自行维护发送数据及回调的队列
|
|||
|
// EPOLL要支持多线程并发发送数据必须创建发送队列, 否则同一个 Socket 的并发发送
|
|||
|
// 极有可能有一部分会被其它发送覆盖掉
|
|||
|
//
|
|||
|
// 由于 KQUEUE 中每个套接字在队列中的 EV_WRITE 和 EV_READ 是分开的两条记录
|
|||
|
// 所以修改套接字的监听事件时不会互相覆盖, 也就是说每个事件都会对应到一次
|
|||
|
// 触发, 这样就可以方便的使用接口的引用计数机制保持连接的有效性, 也不会出现
|
|||
|
// 内存泄漏
|
|||
|
TKqueueCrossSocket = class(TCrossSocketBase)
|
|||
|
private const
|
|||
|
MAX_EVENT_COUNT = 2048;
|
|||
|
SHUTDOWN_FLAG = Pointer(-1);
|
|||
|
private class threadvar
|
|||
|
FEventList: array [0..MAX_EVENT_COUNT-1] of TKEvent;
|
|||
|
private
|
|||
|
FKqueueHandle: THandle;
|
|||
|
FIoThreads: TArray<TIoEventThread>;
|
|||
|
FIdleHandle: THandle;
|
|||
|
FIdleLock: TObject;
|
|||
|
FStopHandle: TPipeDescriptors;
|
|||
|
|
|||
|
// 利用 pipe 唤醒并退出IO线程
|
|||
|
procedure _OpenStopHandle; inline;
|
|||
|
procedure _PostStopCommand; inline;
|
|||
|
procedure _CloseStopHandle; inline;
|
|||
|
|
|||
|
procedure _OpenIdleHandle; inline;
|
|||
|
procedure _CloseIdleHandle; inline;
|
|||
|
|
|||
|
// 在向一个已经关闭的套接字发送数据时系统会直接抛出EPIPE异常导致程序非正常退出
|
|||
|
// LINUX下可以在send时带上MSG_NOSIGNAL参数就能避免这种情况的发生
|
|||
|
// OSX中可以通过设置套接字的SO_NOSIGPIPE参数达到同样的目的
|
|||
|
procedure _SetNoSigPipe(ASocket: THandle); inline;
|
|||
|
|
|||
|
procedure _HandleAccept(const AListen: ICrossListen);
|
|||
|
procedure _HandleConnect(const AConnection: ICrossConnection);
|
|||
|
procedure _HandleRead(const AConnection: ICrossConnection);
|
|||
|
procedure _HandleWrite(const AConnection: ICrossConnection);
|
|||
|
protected
|
|||
|
function CreateConnection(const AOwner: TCrossSocketBase; const AClientSocket: THandle;
|
|||
|
const AConnectType: TConnectType): ICrossConnection; override;
|
|||
|
function CreateListen(const AOwner: TCrossSocketBase; const AListenSocket: THandle;
|
|||
|
const AFamily, ASockType, AProtocol: Integer): ICrossListen; override;
|
|||
|
|
|||
|
procedure StartLoop; override;
|
|||
|
procedure StopLoop; override;
|
|||
|
|
|||
|
procedure Listen(const AHost: string; const APort: Word;
|
|||
|
const ACallback: TCrossListenCallback = nil); override;
|
|||
|
|
|||
|
procedure Connect(const AHost: string; const APort: Word;
|
|||
|
const ACallback: TCrossConnectionCallback = nil); override;
|
|||
|
|
|||
|
procedure Send(const AConnection: ICrossConnection; const ABuf: Pointer;
|
|||
|
const ALen: Integer; const ACallback: TCrossConnectionCallback = nil); override;
|
|||
|
|
|||
|
function ProcessIoEvent: Boolean; override;
|
|||
|
public
|
|||
|
constructor Create(const AIoThreads: Integer); override;
|
|||
|
destructor Destroy; override;
|
|||
|
end;
|
|||
|
|
|||
|
implementation
|
|||
|
|
|||
|
{$I Net.Posix.inc}
|
|||
|
|
|||
|
{ TKqueueListen }
|
|||
|
|
|||
|
constructor TKqueueListen.Create(const AOwner: TCrossSocketBase;
|
|||
|
const AListenSocket: THandle; const AFamily, ASockType, AProtocol: Integer);
|
|||
|
begin
|
|||
|
inherited;
|
|||
|
|
|||
|
FLock := TObject.Create;
|
|||
|
end;
|
|||
|
|
|||
|
destructor TKqueueListen.Destroy;
|
|||
|
begin
|
|||
|
FreeAndNil(FLock);
|
|||
|
|
|||
|
inherited;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueListen._Lock;
|
|||
|
begin
|
|||
|
System.TMonitor.Enter(FLock);
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueListen._ReadEnabled: Boolean;
|
|||
|
begin
|
|||
|
Result := (ieRead in FIoEvents);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueListen._Unlock;
|
|||
|
begin
|
|||
|
System.TMonitor.Exit(FLock);
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueListen._UpdateIoEvent(const AIoEvents: TIoEvents): Boolean;
|
|||
|
var
|
|||
|
LOwner: TKqueueCrossSocket;
|
|||
|
LCrossData: Pointer;
|
|||
|
LEvents: array [0..1] of TKEvent;
|
|||
|
N: Integer;
|
|||
|
begin
|
|||
|
FIoEvents := AIoEvents;
|
|||
|
|
|||
|
if (FIoEvents = []) or IsClosed then Exit(False);
|
|||
|
|
|||
|
LOwner := TKqueueCrossSocket(Owner);
|
|||
|
LCrossData := Pointer(Self);
|
|||
|
N := 0;
|
|||
|
|
|||
|
if _ReadEnabled then
|
|||
|
begin
|
|||
|
EV_SET(@LEvents[N], Socket, EVFILT_READ,
|
|||
|
EV_ADD or EV_ONESHOT or EV_CLEAR or EV_DISPATCH, 0, 0, Pointer(LCrossData));
|
|||
|
|
|||
|
Inc(N);
|
|||
|
end;
|
|||
|
|
|||
|
if (N <= 0) then Exit(False);
|
|||
|
|
|||
|
Result := (kevent(LOwner.FKqueueHandle, @LEvents, N, nil, 0, nil) >= 0);
|
|||
|
|
|||
|
{$IFDEF DEBUG}
|
|||
|
if not Result then
|
|||
|
_Log('listen %d kevent error %d', [Socket, GetLastError]);
|
|||
|
{$ENDIF}
|
|||
|
end;
|
|||
|
|
|||
|
{ TSendQueue }
|
|||
|
|
|||
|
procedure TSendQueue.Notify(const Value: PSendItem;
|
|||
|
Action: TCollectionNotification);
|
|||
|
begin
|
|||
|
inherited;
|
|||
|
|
|||
|
if (Action = TCollectionNotification.cnRemoved) then
|
|||
|
begin
|
|||
|
if (Value <> nil) then
|
|||
|
begin
|
|||
|
Value.Callback := nil;
|
|||
|
FreeMem(Value, SizeOf(TSendItem));
|
|||
|
end;
|
|||
|
end;
|
|||
|
end;
|
|||
|
|
|||
|
{ TKqueueConnection }
|
|||
|
|
|||
|
constructor TKqueueConnection.Create(const AOwner: TCrossSocketBase;
|
|||
|
const AClientSocket: THandle; const AConnectType: TConnectType);
|
|||
|
begin
|
|||
|
inherited;
|
|||
|
|
|||
|
FSendQueue := TSendQueue.Create;
|
|||
|
FLock := TObject.Create;
|
|||
|
end;
|
|||
|
|
|||
|
destructor TKqueueConnection.Destroy;
|
|||
|
var
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LSendItem: PSendItem;
|
|||
|
begin
|
|||
|
LConnection := Self;
|
|||
|
|
|||
|
_Lock;
|
|||
|
try
|
|||
|
// 连接释放时, 调用连接回调, 告知连接失败
|
|||
|
// 连接成功后 FConnectCallback 会被置为 nil,
|
|||
|
// 所以如果这里 FConnectCallback 不等于 nil, 则表示连接释放时仍未连接成功
|
|||
|
if Assigned(FConnectCallback) then
|
|||
|
begin
|
|||
|
FConnectCallback(LConnection, False);
|
|||
|
FConnectCallback := nil;
|
|||
|
end;
|
|||
|
|
|||
|
// 连接释放时, 调用所有发送队列的回调, 告知发送失败
|
|||
|
if (FSendQueue.Count > 0) then
|
|||
|
begin
|
|||
|
for LSendItem in FSendQueue do
|
|||
|
if Assigned(LSendItem.Callback) then
|
|||
|
LSendItem.Callback(LConnection, False);
|
|||
|
|
|||
|
FSendQueue.Clear;
|
|||
|
end;
|
|||
|
|
|||
|
FreeAndNil(FSendQueue);
|
|||
|
finally
|
|||
|
_Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
FreeAndNil(FLock);
|
|||
|
|
|||
|
inherited;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueConnection._Lock;
|
|||
|
begin
|
|||
|
System.TMonitor.Enter(FLock);
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueConnection._ReadEnabled: Boolean;
|
|||
|
begin
|
|||
|
Result := (ieRead in FIoEvents);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueConnection._Unlock;
|
|||
|
begin
|
|||
|
System.TMonitor.Exit(FLock);
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueConnection._UpdateIoEvent(const AIoEvents: TIoEvents): Boolean;
|
|||
|
var
|
|||
|
LOwner: TKqueueCrossSocket;
|
|||
|
LCrossData: Pointer;
|
|||
|
LEvents: array [0..1] of TKEvent;
|
|||
|
N: Integer;
|
|||
|
begin
|
|||
|
FIoEvents := AIoEvents;
|
|||
|
|
|||
|
if (FIoEvents = []) or IsClosed then Exit(False);
|
|||
|
|
|||
|
LOwner := TKqueueCrossSocket(Owner);
|
|||
|
LCrossData := Pointer(Self);
|
|||
|
N := 0;
|
|||
|
|
|||
|
// kqueue中同一个套接字的EVFILT_READ和EVFILT_WRITE事件在队列中会有两条记录
|
|||
|
// 并且可能会在不同的线程中同时被触发, 如果其中一个线程关闭了连接, 在没有
|
|||
|
// 引用计数保护的情况下, 就会导致连接对象被释放, 另一个线程再访问连接对象
|
|||
|
// 就会引起异常, 这里为了保证连接对象的有效性, 在添加事件时手动增加连接对象
|
|||
|
// 的引用计数, 到事件触发时再减少引用计数
|
|||
|
// 注意关闭连接一定要使用shutdown而不能直接close, 否则无法触发kqueue事件,
|
|||
|
// 导致引用计数无法回收
|
|||
|
|
|||
|
if _ReadEnabled then
|
|||
|
begin
|
|||
|
Self._AddRef;
|
|||
|
|
|||
|
EV_SET(@LEvents[N], Socket, EVFILT_READ,
|
|||
|
EV_ADD or EV_ONESHOT or EV_CLEAR or EV_DISPATCH, 0, 0, Pointer(LCrossData));
|
|||
|
|
|||
|
Inc(N);
|
|||
|
end;
|
|||
|
|
|||
|
if _WriteEnabled then
|
|||
|
begin
|
|||
|
Self._AddRef;
|
|||
|
|
|||
|
EV_SET(@LEvents[N], Socket, EVFILT_WRITE,
|
|||
|
EV_ADD or EV_ONESHOT or EV_CLEAR or EV_DISPATCH, 0, 0, Pointer(LCrossData));
|
|||
|
|
|||
|
Inc(N);
|
|||
|
end;
|
|||
|
|
|||
|
if (N <= 0) then Exit(False);
|
|||
|
|
|||
|
Result := (kevent(LOwner.FKqueueHandle, @LEvents, N, nil, 0, nil) >= 0);
|
|||
|
|
|||
|
if not Result then
|
|||
|
begin
|
|||
|
{$IFDEF DEBUG}
|
|||
|
_Log('connection %d kevent error %d', [Socket, GetLastError]);
|
|||
|
{$ENDIF}
|
|||
|
|
|||
|
while (N > 0) do
|
|||
|
begin
|
|||
|
Self._Release;
|
|||
|
Dec(N);
|
|||
|
end;
|
|||
|
end;
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueConnection._WriteEnabled: Boolean;
|
|||
|
begin
|
|||
|
Result := (ieWrite in FIoEvents);
|
|||
|
end;
|
|||
|
|
|||
|
{ TKqueueCrossSocket }
|
|||
|
|
|||
|
constructor TKqueueCrossSocket.Create(const AIoThreads: Integer);
|
|||
|
begin
|
|||
|
inherited;
|
|||
|
|
|||
|
FIdleLock := TObject.Create;
|
|||
|
end;
|
|||
|
|
|||
|
destructor TKqueueCrossSocket.Destroy;
|
|||
|
begin
|
|||
|
FreeAndNil(FIdleLock);
|
|||
|
|
|||
|
inherited;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._CloseIdleHandle;
|
|||
|
begin
|
|||
|
FileClose(FIdleHandle);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._CloseStopHandle;
|
|||
|
begin
|
|||
|
FileClose(FStopHandle.ReadDes);
|
|||
|
FileClose(FStopHandle.WriteDes);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._HandleAccept(const AListen: ICrossListen);
|
|||
|
var
|
|||
|
LListen: ICrossListen;
|
|||
|
LKqListen: TKqueueListen;
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
LSocket, LError: Integer;
|
|||
|
LListenSocket, LClientSocket: THandle;
|
|||
|
LSuccess: Boolean;
|
|||
|
begin
|
|||
|
LListen := AListen;
|
|||
|
LListenSocket := LListen.Socket;
|
|||
|
|
|||
|
while True do
|
|||
|
begin
|
|||
|
LSocket := TSocketAPI.Accept(LListenSocket, nil, nil);
|
|||
|
|
|||
|
// Accept失败
|
|||
|
// EAGAIN 所有就绪的连接都已处理完毕
|
|||
|
// EMFILE 进程的文件句柄已经用完了
|
|||
|
if (LSocket < 0) then
|
|||
|
begin
|
|||
|
LError := GetLastError;
|
|||
|
|
|||
|
// 当句柄用完了的时候, 释放事先占用的临时句柄
|
|||
|
// 然后再次 accept, 然后将 accept 的句柄关掉
|
|||
|
// 这样可以保证在文件句柄耗尽的时候依然能响应连接请求
|
|||
|
// 并立即将新到的连接关闭
|
|||
|
if (LError = EMFILE) then
|
|||
|
begin
|
|||
|
System.TMonitor.Enter(FIdleLock);
|
|||
|
try
|
|||
|
_CloseIdleHandle;
|
|||
|
LSocket := TSocketAPI.Accept(LListenSocket, nil, nil);
|
|||
|
TSocketAPI.CloseSocket(LSocket);
|
|||
|
_OpenIdleHandle;
|
|||
|
finally
|
|||
|
System.TMonitor.Exit(FIdleLock);
|
|||
|
end;
|
|||
|
end;
|
|||
|
|
|||
|
Break;
|
|||
|
end;
|
|||
|
|
|||
|
LClientSocket := LSocket;
|
|||
|
TSocketAPI.SetNonBlock(LClientSocket, True);
|
|||
|
SetKeepAlive(LClientSocket);
|
|||
|
_SetNoSigPipe(LClientSocket);
|
|||
|
|
|||
|
LConnection := CreateConnection(Self, LClientSocket, ctAccept);
|
|||
|
TriggerConnecting(LConnection);
|
|||
|
TriggerConnected(LConnection);
|
|||
|
|
|||
|
// 连接建立后监视Socket的读事件
|
|||
|
LKqConnection := LConnection as TKqueueConnection;
|
|||
|
LKqConnection._Lock;
|
|||
|
try
|
|||
|
LSuccess := LKqConnection._UpdateIoEvent([ieRead]);
|
|||
|
finally
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
if not LSuccess then
|
|||
|
LConnection.Close;
|
|||
|
end;
|
|||
|
|
|||
|
// 继续接收新连接
|
|||
|
LKqListen := LListen as TKqueueListen;
|
|||
|
LKqListen._Lock;
|
|||
|
LKqListen._UpdateIoEvent([ieRead]);
|
|||
|
LKqListen._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._HandleConnect(const AConnection: ICrossConnection);
|
|||
|
var
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
LConnectCallback: TCrossConnectionCallback;
|
|||
|
LSuccess: Boolean;
|
|||
|
begin
|
|||
|
LConnection := AConnection;
|
|||
|
|
|||
|
// Connect失败
|
|||
|
if (TSocketAPI.GetError(LConnection.Socket) <> 0) then
|
|||
|
begin
|
|||
|
{$IFDEF DEBUG}
|
|||
|
_LogLastOsError;
|
|||
|
{$ENDIF}
|
|||
|
LConnection.Close;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
TriggerConnected(LConnection);
|
|||
|
|
|||
|
LKqConnection := LConnection as TKqueueConnection;
|
|||
|
|
|||
|
LKqConnection._Lock;
|
|||
|
try
|
|||
|
LConnectCallback := LKqConnection.FConnectCallback;
|
|||
|
LKqConnection.FConnectCallback := nil;
|
|||
|
LSuccess := LKqConnection._UpdateIoEvent([ieRead]);
|
|||
|
finally
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
if Assigned(LConnectCallback) then
|
|||
|
LConnectCallback(LConnection, LSuccess);
|
|||
|
|
|||
|
if not LSuccess then
|
|||
|
LConnection.Close;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._HandleRead(const AConnection: ICrossConnection);
|
|||
|
var
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LRcvd, LError: Integer;
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
LSuccess: Boolean;
|
|||
|
begin
|
|||
|
LConnection := AConnection;
|
|||
|
|
|||
|
while True do
|
|||
|
begin
|
|||
|
LRcvd := TSocketAPI.Recv(LConnection.Socket, FRecvBuf[0], RCV_BUF_SIZE);
|
|||
|
|
|||
|
// 对方主动断开连接
|
|||
|
if (LRcvd = 0) then
|
|||
|
begin
|
|||
|
// _Log('%d close on read 0, ref %d', [LConnection.Socket, TInterfacedObject(LConnection).RefCount]);
|
|||
|
LConnection.Close;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
if (LRcvd < 0) then
|
|||
|
begin
|
|||
|
LError := GetLastError;
|
|||
|
|
|||
|
// 被系统信号中断, 可以重新recv
|
|||
|
if (LError = EINTR) then
|
|||
|
Continue
|
|||
|
// 接收缓冲区中数据已经被取完了
|
|||
|
else if (LError = EAGAIN) or (LError = EWOULDBLOCK) then
|
|||
|
Break
|
|||
|
else
|
|||
|
// 接收出错
|
|||
|
begin
|
|||
|
// _Log('%d close on read error %d, ref %d', [LConnection.Socket, GetLastError, TInterfacedObject(LConnection).RefCount]);
|
|||
|
LConnection.Close;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
end;
|
|||
|
|
|||
|
TriggerReceived(LConnection, @FRecvBuf[0], LRcvd);
|
|||
|
|
|||
|
if (LRcvd < RCV_BUF_SIZE) then Break;
|
|||
|
end;
|
|||
|
|
|||
|
LKqConnection := LConnection as TKqueueConnection;
|
|||
|
LKqConnection._Lock;
|
|||
|
try
|
|||
|
LSuccess := LKqConnection._UpdateIoEvent([ieRead]);
|
|||
|
finally
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
if not LSuccess then
|
|||
|
LConnection.Close;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._HandleWrite(const AConnection: ICrossConnection);
|
|||
|
var
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
LSendItem: PSendItem;
|
|||
|
LCallback: TCrossConnectionCallback;
|
|||
|
LSent: Integer;
|
|||
|
begin
|
|||
|
LConnection := AConnection;
|
|||
|
LKqConnection := LConnection as TKqueueConnection;
|
|||
|
|
|||
|
LKqConnection._Lock;
|
|||
|
|
|||
|
// 队列中没有数据了, 清除 ioWrite 标志
|
|||
|
if (LKqConnection.FSendQueue.Count <= 0) then
|
|||
|
begin
|
|||
|
LKqConnection._UpdateIoEvent([]);
|
|||
|
LKqConnection._Unlock;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
// 获取Socket发送队列中的第一条数据
|
|||
|
LSendItem := LKqConnection.FSendQueue.Items[0];
|
|||
|
|
|||
|
// 发送数据
|
|||
|
LSent := PosixSend(LConnection.Socket, LSendItem.Data, LSendItem.Size);
|
|||
|
|
|||
|
{$region '全部发送完成'}
|
|||
|
if (LSent >= LSendItem.Size) then
|
|||
|
begin
|
|||
|
// 先保存回调函数, 避免后面删除队列后将其释放
|
|||
|
LCallback := LSendItem.Callback;
|
|||
|
|
|||
|
// 发送成功, 移除已发送成功的数据
|
|||
|
if (LKqConnection.FSendQueue.Count > 0) then
|
|||
|
LKqConnection.FSendQueue.Delete(0);
|
|||
|
|
|||
|
// 队列中没有数据了, 清除 ioWrite 标志
|
|||
|
if (LKqConnection.FSendQueue.Count <= 0) then
|
|||
|
LKqConnection._UpdateIoEvent([]);
|
|||
|
|
|||
|
LKqConnection._Unlock;
|
|||
|
|
|||
|
if Assigned(LCallback) then
|
|||
|
LCallback(LConnection, True);
|
|||
|
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
{$endregion}
|
|||
|
|
|||
|
{$region '连接断开或发送错误'}
|
|||
|
// 发送失败的回调会在连接对象的destroy方法中被调用
|
|||
|
if (LSent < 0) then
|
|||
|
begin
|
|||
|
LKqConnection._Unlock;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
{$endregion}
|
|||
|
|
|||
|
{$region '部分发送成功,在下一次唤醒发送线程时继续处理剩余部分'}
|
|||
|
Dec(LSendItem.Size, LSent);
|
|||
|
Inc(LSendItem.Data, LSent);
|
|||
|
{$endregion}
|
|||
|
|
|||
|
LKqConnection._UpdateIoEvent([ieWrite]);
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._OpenIdleHandle;
|
|||
|
begin
|
|||
|
FIdleHandle := FileOpen('/dev/null', fmOpenRead);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._OpenStopHandle;
|
|||
|
var
|
|||
|
LEvent: TKEvent;
|
|||
|
begin
|
|||
|
pipe(FStopHandle);
|
|||
|
|
|||
|
// 这里不使用 EV_ONESHOT
|
|||
|
// 这样可以保证通知退出的命令发出后, 所有IO线程都会收到
|
|||
|
EV_SET(@LEvent, FStopHandle.ReadDes, EVFILT_READ,
|
|||
|
EV_ADD, 0, 0, SHUTDOWN_FLAG);
|
|||
|
kevent(FKqueueHandle, @LEvent, 1, nil, 0, nil);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._PostStopCommand;
|
|||
|
var
|
|||
|
LStuff: UInt64;
|
|||
|
begin
|
|||
|
LStuff := 1;
|
|||
|
// 往 FStopHandle.WriteDes 写入任意数据, 唤醒工作线程
|
|||
|
Posix.UniStd.__write(FStopHandle.WriteDes, @LStuff, SizeOf(LStuff));
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket._SetNoSigPipe(ASocket: THandle);
|
|||
|
begin
|
|||
|
TSocketAPI.SetSockOpt<Integer>(ASocket, SOL_SOCKET, SO_NOSIGPIPE, 1);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket.StartLoop;
|
|||
|
var
|
|||
|
I: Integer;
|
|||
|
begin
|
|||
|
if (FIoThreads <> nil) then Exit;
|
|||
|
|
|||
|
_OpenIdleHandle;
|
|||
|
|
|||
|
FKqueueHandle := kqueue();
|
|||
|
SetLength(FIoThreads, GetIoThreads);
|
|||
|
for I := 0 to Length(FIoThreads) - 1 do
|
|||
|
FIoThreads[i] := TIoEventThread.Create(Self);
|
|||
|
|
|||
|
_OpenStopHandle;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket.StopLoop;
|
|||
|
var
|
|||
|
I: Integer;
|
|||
|
LCurrentThreadID: TThreadID;
|
|||
|
begin
|
|||
|
if (FIoThreads = nil) then Exit;
|
|||
|
|
|||
|
CloseAll;
|
|||
|
|
|||
|
while (ListensCount > 0) or (ConnectionsCount > 0) do Sleep(1);
|
|||
|
|
|||
|
_PostStopCommand;
|
|||
|
|
|||
|
LCurrentThreadID := GetCurrentThreadId;
|
|||
|
for I := 0 to Length(FIoThreads) - 1 do
|
|||
|
begin
|
|||
|
if (FIoThreads[I].ThreadID = LCurrentThreadID) then
|
|||
|
raise ECrossSocket.Create('不能在IO线程中执行StopLoop!');
|
|||
|
|
|||
|
FIoThreads[I].WaitFor;
|
|||
|
FreeAndNil(FIoThreads[I]);
|
|||
|
end;
|
|||
|
FIoThreads := nil;
|
|||
|
|
|||
|
FileClose(FKqueueHandle);
|
|||
|
_CloseIdleHandle;
|
|||
|
_CloseStopHandle;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket.Connect(const AHost: string; const APort: Word;
|
|||
|
const ACallback: TCrossConnectionCallback);
|
|||
|
|
|||
|
procedure _Failed1;
|
|||
|
begin
|
|||
|
if Assigned(ACallback) then
|
|||
|
ACallback(nil, False);
|
|||
|
end;
|
|||
|
|
|||
|
function _Connect(const ASocket: THandle; const AAddr: PRawAddrInfo): Boolean;
|
|||
|
var
|
|||
|
LConnection: ICrossConnection;
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
begin
|
|||
|
if (TSocketAPI.Connect(ASocket, AAddr.ai_addr, AAddr.ai_addrlen) = 0)
|
|||
|
or (GetLastError = EINPROGRESS) then
|
|||
|
begin
|
|||
|
LConnection := CreateConnection(Self, ASocket, ctConnect);
|
|||
|
TriggerConnecting(LConnection);
|
|||
|
LKqConnection := LConnection as TKqueueConnection;
|
|||
|
|
|||
|
LKqConnection._Lock;
|
|||
|
try
|
|||
|
LKqConnection.ConnectStatus := csConnecting;
|
|||
|
LKqConnection.FConnectCallback := ACallback;
|
|||
|
if not LKqConnection._UpdateIoEvent([ieWrite]) then
|
|||
|
begin
|
|||
|
LConnection.Close;
|
|||
|
if Assigned(ACallback) then
|
|||
|
ACallback(LConnection, False);
|
|||
|
Exit(False);
|
|||
|
end;
|
|||
|
finally
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
end else
|
|||
|
begin
|
|||
|
if Assigned(ACallback) then
|
|||
|
ACallback(nil, False);
|
|||
|
TSocketAPI.CloseSocket(ASocket);
|
|||
|
Exit(False);
|
|||
|
end;
|
|||
|
|
|||
|
Result := True;
|
|||
|
end;
|
|||
|
|
|||
|
var
|
|||
|
LHints: TRawAddrInfo;
|
|||
|
P, LAddrInfo: PRawAddrInfo;
|
|||
|
LSocket: THandle;
|
|||
|
begin
|
|||
|
FillChar(LHints, SizeOf(TRawAddrInfo), 0);
|
|||
|
LHints.ai_family := AF_UNSPEC;
|
|||
|
LHints.ai_socktype := SOCK_STREAM;
|
|||
|
LHints.ai_protocol := IPPROTO_TCP;
|
|||
|
LAddrInfo := TSocketAPI.GetAddrInfo(AHost, APort, LHints);
|
|||
|
if (LAddrInfo = nil) then
|
|||
|
begin
|
|||
|
_Failed1;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
P := LAddrInfo;
|
|||
|
try
|
|||
|
while (LAddrInfo <> nil) do
|
|||
|
begin
|
|||
|
LSocket := TSocketAPI.NewSocket(LAddrInfo.ai_family, LAddrInfo.ai_socktype,
|
|||
|
LAddrInfo.ai_protocol);
|
|||
|
if (LSocket = INVALID_HANDLE_VALUE) then
|
|||
|
begin
|
|||
|
_Failed1;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
TSocketAPI.SetNonBlock(LSocket, True);
|
|||
|
SetKeepAlive(LSocket);
|
|||
|
_SetNoSigPipe(LSocket);
|
|||
|
|
|||
|
if _Connect(LSocket, LAddrInfo) then Exit;
|
|||
|
|
|||
|
LAddrInfo := PRawAddrInfo(LAddrInfo.ai_next);
|
|||
|
end;
|
|||
|
finally
|
|||
|
TSocketAPI.FreeAddrInfo(P);
|
|||
|
end;
|
|||
|
|
|||
|
_Failed1;
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueCrossSocket.CreateConnection(const AOwner: TCrossSocketBase;
|
|||
|
const AClientSocket: THandle; const AConnectType: TConnectType): ICrossConnection;
|
|||
|
begin
|
|||
|
Result := TKqueueConnection.Create(AOwner, AClientSocket, AConnectType);
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueCrossSocket.CreateListen(const AOwner: TCrossSocketBase;
|
|||
|
const AListenSocket: THandle; const AFamily, ASockType, AProtocol: Integer): ICrossListen;
|
|||
|
begin
|
|||
|
Result := TKqueueListen.Create(AOwner, AListenSocket, AFamily, ASockType, AProtocol);
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket.Listen(const AHost: string; const APort: Word;
|
|||
|
const ACallback: TCrossListenCallback);
|
|||
|
var
|
|||
|
LHints: TRawAddrInfo;
|
|||
|
P, LAddrInfo: PRawAddrInfo;
|
|||
|
LListenSocket: THandle;
|
|||
|
LListen: ICrossListen;
|
|||
|
LKqListen: TKqueueListen;
|
|||
|
LSuccess: Boolean;
|
|||
|
|
|||
|
procedure _Failed;
|
|||
|
begin
|
|||
|
if Assigned(ACallback) then
|
|||
|
ACallback(nil, False);
|
|||
|
end;
|
|||
|
|
|||
|
begin
|
|||
|
FillChar(LHints, SizeOf(TRawAddrInfo), 0);
|
|||
|
|
|||
|
LHints.ai_flags := AI_PASSIVE;
|
|||
|
LHints.ai_family := AF_UNSPEC;
|
|||
|
LHints.ai_socktype := SOCK_STREAM;
|
|||
|
LHints.ai_protocol := IPPROTO_TCP;
|
|||
|
LAddrInfo := TSocketAPI.GetAddrInfo(AHost, APort, LHints);
|
|||
|
if (LAddrInfo = nil) then
|
|||
|
begin
|
|||
|
_Failed;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
P := LAddrInfo;
|
|||
|
try
|
|||
|
while (LAddrInfo <> nil) do
|
|||
|
begin
|
|||
|
LListenSocket := TSocketAPI.NewSocket(LAddrInfo.ai_family, LAddrInfo.ai_socktype,
|
|||
|
LAddrInfo.ai_protocol);
|
|||
|
if (LListenSocket = INVALID_HANDLE_VALUE) then
|
|||
|
begin
|
|||
|
_Failed;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
TSocketAPI.SetNonBlock(LListenSocket, True);
|
|||
|
TSocketAPI.SetReUsePort(LListenSocket, True);
|
|||
|
|
|||
|
if (LAddrInfo.ai_family = AF_INET6) then
|
|||
|
TSocketAPI.SetSockOpt<Integer>(LListenSocket, IPPROTO_IPV6, IPV6_V6ONLY, 1);
|
|||
|
|
|||
|
if (TSocketAPI.Bind(LListenSocket, LAddrInfo.ai_addr, LAddrInfo.ai_addrlen) < 0)
|
|||
|
or (TSocketAPI.Listen(LListenSocket) < 0) then
|
|||
|
begin
|
|||
|
_Failed;
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
LListen := CreateListen(Self, LListenSocket, LAddrInfo.ai_family,
|
|||
|
LAddrInfo.ai_socktype, LAddrInfo.ai_protocol);
|
|||
|
LKqListen := LListen as TKqueueListen;
|
|||
|
|
|||
|
// 监听套接字的读事件
|
|||
|
// 读事件到达表明有新连接
|
|||
|
LKqListen._Lock;
|
|||
|
try
|
|||
|
LSuccess := LKqListen._UpdateIoEvent([ieRead]);
|
|||
|
finally
|
|||
|
LKqListen._Unlock;
|
|||
|
end;
|
|||
|
|
|||
|
if not LSuccess then
|
|||
|
begin
|
|||
|
_Failed;
|
|||
|
|
|||
|
Exit;
|
|||
|
end;
|
|||
|
|
|||
|
// 监听成功
|
|||
|
TriggerListened(LListen);
|
|||
|
if Assigned(ACallback) then
|
|||
|
ACallback(LListen, True);
|
|||
|
|
|||
|
// 如果端口传入0,让所有地址统一用首个分配到的端口
|
|||
|
if (APort = 0) and (LAddrInfo.ai_next <> nil) then
|
|||
|
Psockaddr_in(LAddrInfo.ai_next.ai_addr).sin_port := LListen.LocalPort;
|
|||
|
|
|||
|
LAddrInfo := PRawAddrInfo(LAddrInfo.ai_next);
|
|||
|
end;
|
|||
|
finally
|
|||
|
TSocketAPI.FreeAddrInfo(P);
|
|||
|
end;
|
|||
|
end;
|
|||
|
|
|||
|
procedure TKqueueCrossSocket.Send(const AConnection: ICrossConnection;
|
|||
|
const ABuf: Pointer; const ALen: Integer; const ACallback: TCrossConnectionCallback);
|
|||
|
var
|
|||
|
LKqConnection: TKqueueConnection;
|
|||
|
LSendItem: PSendItem;
|
|||
|
begin
|
|||
|
// 测试过先发送, 然后将剩余部分放入发送队列的做法
|
|||
|
// 发现会引起内存访问异常, 放到队列里到IO线程中发送则不会有问题
|
|||
|
{$region '放入发送队列'}
|
|||
|
GetMem(LSendItem, SizeOf(TSendItem));
|
|||
|
FillChar(LSendItem^, SizeOf(TSendItem), 0);
|
|||
|
LSendItem.Data := ABuf;
|
|||
|
LSendItem.Size := ALen;
|
|||
|
LSendItem.Callback := ACallback;
|
|||
|
|
|||
|
LKqConnection := AConnection as TKqueueConnection;
|
|||
|
|
|||
|
LKqConnection._Lock;
|
|||
|
try
|
|||
|
// 将数据放入队列
|
|||
|
LKqConnection.FSendQueue.Add(LSendItem);
|
|||
|
|
|||
|
// 由于 kqueue 队列中每个套接字的读写事件是分开的两条记录
|
|||
|
// 所以发送只需要添加写事件即可, 不用管读事件, 否则反而会引起引用计数异常
|
|||
|
if not LKqConnection._WriteEnabled then
|
|||
|
LKqConnection._UpdateIoEvent([ieWrite]);
|
|||
|
finally
|
|||
|
LKqConnection._Unlock;
|
|||
|
end;
|
|||
|
{$endregion}
|
|||
|
end;
|
|||
|
|
|||
|
function TKqueueCrossSocket.ProcessIoEvent: Boolean;
|
|||
|
var
|
|||
|
LRet, I: Integer;
|
|||
|
LEvent: TKEvent;
|
|||
|
LCrossData: TCrossData;
|
|||
|
LListen: ICrossListen;
|
|||
|
LConnection: ICrossConnection;
|
|||
|
begin
|
|||
|
LRet := kevent(FKqueueHandle, nil, 0, @FEventList[0], MAX_EVENT_COUNT, nil);
|
|||
|
if (LRet < 0) then
|
|||
|
begin
|
|||
|
LRet := GetLastError;
|
|||
|
// EINTR, kevent 调用被系统信号打断, 可以进行重试
|
|||
|
Exit(LRet = EINTR);
|
|||
|
end;
|
|||
|
|
|||
|
for I := 0 to LRet - 1 do
|
|||
|
begin
|
|||
|
LEvent := FEventList[I];
|
|||
|
|
|||
|
// 收到退出命令
|
|||
|
if (LEvent.uData = SHUTDOWN_FLAG) then Exit(False);
|
|||
|
|
|||
|
if (LEvent.uData = nil) then Continue;
|
|||
|
|
|||
|
{$region '获取连接或监听对象'}
|
|||
|
LCrossData := TCrossData(LEvent.uData);
|
|||
|
|
|||
|
if (LCrossData is TKqueueListen) then
|
|||
|
LListen := LCrossData as ICrossListen
|
|||
|
else
|
|||
|
LListen := nil;
|
|||
|
|
|||
|
if (LCrossData is TKqueueConnection) then
|
|||
|
LConnection := LCrossData as ICrossConnection
|
|||
|
else
|
|||
|
LConnection := nil;
|
|||
|
{$endregion}
|
|||
|
|
|||
|
{$region 'IO事件处理'}
|
|||
|
if (LListen <> nil) then
|
|||
|
begin
|
|||
|
if (LEvent.Filter = EVFILT_READ) then
|
|||
|
_HandleAccept(LListen);
|
|||
|
end else
|
|||
|
if (LConnection <> nil) then
|
|||
|
begin
|
|||
|
LConnection._Release;
|
|||
|
|
|||
|
// kqueue的读写事件同一时间只可能触发一个
|
|||
|
if (LEvent.Filter = EVFILT_READ) then
|
|||
|
_HandleRead(LConnection)
|
|||
|
else if (LEvent.Filter = EVFILT_WRITE) then
|
|||
|
begin
|
|||
|
if (LConnection.ConnectStatus = csConnecting) then
|
|||
|
_HandleConnect(LConnection)
|
|||
|
else
|
|||
|
_HandleWrite(LConnection);
|
|||
|
end;
|
|||
|
end;
|
|||
|
{$endregion}
|
|||
|
end;
|
|||
|
|
|||
|
Result := True;
|
|||
|
end;
|
|||
|
|
|||
|
end.
|