简单轻量级的 SSL IOCP 套接字






4.78/5 (6投票s)
易于使用(且可重复使用!)、轻量级的 SSL IOCP Sockets 实现
引言
市面上有很多 IOCP Sockets 的实现,也有很多 SSL Sockets 的实现。但我需要两者兼备——IOCP 和 SSL。同时,我希望它尽可能简单,不包含像 OpenSSL 这样庞大的库。幸运的是,Windows 提供了所有必需的机制,因此这种组合可以很容易地实现。(**注意**:本文假定读者至少了解一些关于 sockets 和 IOCP 的知识。)
背景
提供的 Socket 类是无锁的。这是通过遵循以下约定实现的:
- 一个 IOCP 只由一个 IOCP 线程服务。
- 一个 Socket 只由一个工作线程服务(如果存在)。
为什么需要这些约定,以及它在实践中意味着什么?IOCP 会顺序读取和写入 Socket 数据——换句话说,发送/接收的数据包顺序会得以保留。但一旦数据包离开 IOCP(即,我们开始处理它),它就掌握在我们手中了。为了进行任何有意义的处理,我们需要保留顺序。如果我们使用多个线程通过 IO 完成端口读取/写入数据,我们需要某种同步机制,以便以正确的顺序进行读取/写入。这会引入不必要的复杂性,而没有任何性能提升——测试表明,每个 IOCP 一个读取/写入线程绰绰有余(至少在 Windows 平台上)。具体来说,根据微软的说法,即使在本地 TCP 回环上,您也可以每秒获得最多 37000 次往返(6 核 AMD 3.2 GHz)——这很容易由一个线程处理,前提是它只进行发送和接收数据。所以,这就是第一个约定的由来。第二个约定是基于相同的原因:如果一个 Socket 的数据只由一个工作线程处理,那么我们就不需要任何同步机制。
在实践中,这意味着我们为多个 Sockets 创建一个 IOCP,然后由一个工作线程处理这些 Sockets 的子集(甚至全部)。当然,可以有很多 IOCP,也可以有很多工作线程;所有需要做的就是遵循上述约定。
那么,代码看起来是怎样的?
有 4 个主要类:
CSimpleIOCP
。这是实现 IO 完成端口和单个 IOCP 线程的主类。CSimpleSocket
。这是实际的 Socket 实现。它可以作为独立 Socket(即阻塞 Socket)工作,或者可以接受一个现有的CSimpleIOCP
作为参数;后者情况下,它成为非阻塞 Socket,并且只能通过CSimpleIOCP
的回调函数使用(稍后会详细介绍)。CSimpleIOCPPool
。这只是CSimpleIOCP
实例的集合(池)。CSimpleServerSocket
。这是 Socket 的一个扩展,用作服务器。它所做的就是监听指定的地址/端口。一旦收到连接,它会调用一个回调函数,您可以在其中使用给定的SOCKET
初始化CSimpleSocket
,然后继续您的程序逻辑。此外,还有一些辅助类:
CSimpleNetManager
。这个单例类由前面 4 个类内部使用,用于网络初始化、关闭、适配器查询等。CSimpleSSL
。这个类被CSimpleSocket
内部使用,用于 SSL 处理(如果需要)。
此实现使用快速委托(https://codeproject.org.cn/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible)来实现回调功能。
Using the Code
让我们开始将这些组件组合起来。
实现服务器(如果您只需要客户端,可以跳过此部分)。
以下代码是一个简单的回显服务器应用程序的骨架实现。
class CSimpleServer: public CSimpleServerSocket
{
public:
CSimpleServer() : CSimpleServerSocket(), m_useSSL(false) {};
void setUseSSL(bool useSSL) {m_useSSL = useSSL;};
bool isUseSSL() {return m_useSSL;}
private:
bool m_useSSL;
};
class CSimpleClientContext
{
public:
CSimpleSocket& getSocket() {return m_socket;}
private:
CSimpleSocket m_socket;
};
// Implementing simple echo server - send back everything it receives
class CEchoServer
{
public:
CEchoServer()
{
m_iocpPool += DELEGATE(onIOCPEvent);
m_iocpPool += DELEGATE(onSocketEvent);
}
~CEchoServer()
{
stop();
}
bool startServer(const char *pAddr, short uPort, bool useSSL = false)
{
CSimpleServer* server = new CSimpleServer();
if (!server)
return false;
server->setUseSSL(useSSL);
*server += DELEGATE(onServerAccept);
*server += DELEGATE(onConnectionVerify);
if (server->startServerOn(pAddr, uPort))
{
m_servers.push_back(server);
return true;
}
delete server;
return false;
}
bool onServerAccept(SOCKET acceptedSocket, CSimpleServerSocket& server)
{
return m_iocpPool.addSocket(acceptedSocket, &server);
}
bool onConnectionVerify(CSimpleServerSocket& server, SocketAcceptVerifyStruct& verifyData)
{
// see docs for parameters at
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms741513(v=vs.85).aspx
// for now, as an example, let's just check on IP address of the connecting party,
// and refuse connection from "127.0.0.15":
std::string remoteIP = inet_ntoa(((struct sockaddr_in *)verifyData.lpCallerId->buf)->sin_addr);
return remoteIP.compare("127.0.0.15") != 0;
}
void stop()
{
for (auto it = m_servers.begin(); it != m_servers.end(); ++it)
delete (*it); // this will also close the server
m_servers.clear();
m_iocpPool.close();
for (auto it = m_clients.begin(); it != m_clients.end(); ++it)
delete (*it);
m_clients.clear();
}
bool setIOCPInitialCount(DWORD iocpInitialCount)
{
return m_iocpPool.setIOCPCount(iocpInitialCount);
}
void onIOCPEvent(IOCPEVENT* eventData, CSimpleIOCP* iocp, DWORD eventID)
{
switch (eventID)
{
case SCD_SOCKET_ADD:
{
CSimpleClientContext* context = new CSimpleClientContext();
CSimpleSocket& sock = context->getSocket();
sock.setUserData(context);
m_clients.push_back(context);
if (sock.initSocket(iocp, eventData->data.sockData.sock))
{
sock.setNoDelayOption(true);
if (((CSimpleServer*)eventData->data.sockData.requestor)->isUseSSL() ?
sock.initSSL(false) : sock.receive(sock.getDefaultBufferSize()))
return; // success
else
sock.close(); // failed to init SSL or failed to receive data - in this app,
// we don't care; just close connection.
}
else
::closesocket(eventData->data.sockData.sock); // something of above failed,
// but we don't really care in this app wat exactly happened,
// so we just terminate connection and client socket
}
break;
case SCD_SOCKET_IOCP_STOP_REQUESTED:
for (auto it = m_clients.begin(); it != m_clients.end(); ++it)
(*it)->getSocket().close();
break;
}
}
void onSocketEvent(CSimpleSocket* sock, SIMPLEWSAOVERLAPPED* overlapped, DWORD socketEvent)
{
switch (socketEvent)
{
case SCD_SOCKET_READ:
{
if (doEcho(sock, overlapped))
return;
std::cout << "failure in server SCD_SOCKET_READ\n";
sock->close();
}
break;
case SCD_SOCKET_WRITE:
break;
case SCD_SOCKET_SSL_INIT_COMPLETED:
if (sock->receiveSSL(sock->getDefaultBufferSize(), &overlapped->ssl_leftover))
return;
std::cout << "failure in server SCD_SOCKET_SSL_INIT_COMPLETED\n";
sock->close();
break;
case SCD_SOCKET_CLOSE_COMPLETED:
std::cout << "calling delete from server SCD_SOCKET_CLOSE_COMPLETED\n";
deleteClient(sock);
break;
}
}
virtual bool doEcho(CSimpleSocket* sock, SIMPLEWSAOVERLAPPED* overlapped)
{
if (sock->isSSL() ? sock->receiveSSL(sock->getDefaultBufferSize(),
&overlapped->ssl_leftover) : sock->receive(sock->getDefaultBufferSize()))
if (!overlapped->bytesPassed || sock->isSSL() ? sock->sendSSL
(overlapped->bytesPassed, &overlapped->buffer[0]) :
sock->send(overlapped->bytesPassed, &overlapped->buffer[0]))
return true;
return false;
}
void deleteClient(CSimpleSocket* sock)
{
auto found = std::find_if(m_clients.begin(), m_clients.end(),
[&](CSimpleClientContext* cmp) { return &cmp->getSocket() == sock;} );
if (found != m_clients.end())
{
delete *found;
m_clients.erase(found);
}
}
private:
CSimpleIOCPPool m_iocpPool;
std::list<CSimpleClientContext*> m_clients;
std::list<CSimpleServer*> m_servers;
};
现在,我们逐行解释它们的作用。首先,您会看到 CSimpleServer
的声明,它派生自 CSimpleServerSocket
。在大多数情况下,您永远不需要这样做,因为您的应用程序中只有一个服务器,或者所有服务器的行为都相同。但是,在本例中,我们需要以某种方式区分服务器的功能,即,一些回显服务器将使用普通通信,而另一些将使用 SSL 通信。当然,在接受函数中,我们仍然可以通过检查其绑定的地址和端口来区分这些服务器,但在这种情况下,继承是 C++ 的做法(而且更简单)。因此,我们只是添加了一个标志成员,指示连接到此服务器的 Sockets 是否应使用 SSL。
接下来,我们声明了 CSimpleClientContext
。如您所见,这只是一个存根,只有一个成员——CSimpleSocket
。当然,对于回显服务器而言,这并不是必需的,我们可以只使用 CSimpleSocket
本身。但我希望通过它来说明一种创建客户端上下文然后使用它的方法。
现在,我们终于到了 CEchoServer
。让我们看看它的构造函数体:
m_iocpPool += DELEGATE(onIOCPEvent);
m_iocpPool += DELEGATE(onSocketEvent);
这两行建立了 m_iocpPool
的回调函数。CSimpleIOCP
(以及它们的池)有两种回调函数类型:IOCP 事件的回调,以及 Socket 事件的回调。您始终应该实现至少 Socket 事件的回调。IOCP 事件的回调不是必需的,但通常非常有帮助。此外,您还可以使用一个小技巧(虽然我看不出其原因):CSimpleIOCPPool
中的每个单独的 CSimpleIOCP
成员实例都可以有不同的回调。您可以在成员实例创建后的任何时候设置该回调,例如:
m_iocpPool.getAt(1) += DELEGATE(onIOCPEvent1);
m_iocpPool.getAt(2) += DELEGATE(onIOCPEvent2);
...
现在我们跳过析构函数——显然它会调用清理代码——然后看看 startServer
函数。接下来的两行设置了服务器的行为:
*server += DELEGATE(onServerAccept);
*server += DELEGATE(onConnectionVerify);
第一行设置了一个回调函数,在监听服务器接受连接时调用。如果您查看下面的 onServerAccept
的实现,您会发现它所做的就是调用:
m_iocpPool.addSocket(acceptedSocket, &server);
并返回该操作的结果。大多数情况下,onServerAccept
将与此代码完全相同。当然,您可以在此函数中添加额外的处理,但最好在别处进行,因为 onServerAccept
是从服务器线程调用的。池的 addSocket
函数(以及单个 CSimpleIOCP
)将依次调用 onIOCPEvent
委托,并将 eventID
参数设置为“SCD_SOCKET_ADD
”,这就是进行额外处理最方便的地方,因为它是从 IOCP 线程调用的,而不是从服务器线程调用的。
大多数情况下,onServerAccept
将是您需要为服务器实现的唯一委托。但有时,您需要根据某种情况过滤传入的连接。例如,您想将某些 IP 加入黑名单,或限制服务器负载(即,不要接受超过 X 个连接),或者在 QoS 低时不允许新连接,或者其他原因。在这种情况下,您将提供一个委托,该委托将根据您是否希望接受连接返回“true
/false
”。onConnectionVerify
就是这样一个委托的例子。在本示例代码中,我们将 IP “127.0.0.15” 的所有连接加入黑名单。
下一行代码也很明显——我们在指定的 IP/端口上启动服务器:
server->startServerOn(pAddr, uPort)
现在到清理代码(stop()
函数)。遵循指定的清理顺序非常重要:首先,我们关闭所有服务器,这样就不会有新的传入连接导致调用我们的 IOCP。(删除服务器对象也会关闭它)。然后我们调用池的清理——close()
函数将正确销毁所有内部“重叠”缓冲区。需要注意的一点:调用 CSimpleIOCPPool
(或单个 CSimpleIOCP
)上的 close()
将依次调用(每个 IOCP 成员实例的)onIOCPEvent
委托,并将 eventID
参数设置为“SCD_SOCKET_IOCP_STOP_REQUESTED
”,您必须在此处调用所有附加 Socket 的 close()
。IOCP 本身不维护附加 Socket 的列表(这需要一些同步,而我们正试图避免它),但它提供了一个回调,通知我们是时候关闭了。
我们继续下一个函数:setIOCPInitialCount
。它所做的就是设置 CSimpleIOCPPool
中 CSimpleIOCP
的初始数量。Pool
是用零个 IOCP 创建的,所以您需要将计数至少设置为一个以允许 IOCP 处理。当然,这个数字可以任意大。在实践中,建议每 1000-5000 个 Sockets 设置 1 个 IOCP。因此,如果您预计有 100 万个同时连接的 Sockets 的负载,您将此数字设置为 200(尽管这可能需要至少 4 核 CPU 系统)。
现在,我们来到了两个函数(委托),它们执行实际工作。让我们从 onIOCPEvent
开始。在此委托中,eventID
参数可以具有以下值:
SCD_SOCKET_ADD
SCD_SOCKET_IOCP_STOP_REQUESTED
SCD_SOCKET_USER_EVENT
如前所述,SCD_SOCKET_ADD
事件来自调用 addSocket
,我们在此处建立与服务器发送的(已连接的)SOCKET
以及我们的 IOCP 的关联。
sock.initSocket(iocp, eventData->data.sockData.sock)
第一个参数是指向池决定的 IOCP 成员的指针(稍后讨论)。如果 initSocket
仅使用此参数调用,则 Socket 对象将创建一个新的、未连接的 SOCKET
并将其附加到提供的 IOCP 指针。但是,如果 initSocket
的第二个参数是一个有效的 SOCKET
,它将拥有该 Socket 并将其附加到提供的 IOCP。在我们的例子中,SOCKET
已经由服务器创建并已连接,所以我们只是将其附加到 IOCP。请记住,如果调用 initSocket
失败,则意味着它未附加到 IOCP,并且不会调用 close()
事件,因此我们在这里唯一的选择就是使用 windows API 销毁我们的 SOCKET
。
if (sock.initSocket(iocp, eventData->data.sockData.sock))
{
...
}
else
::closesocket(eventData->data.sockData.sock);
SCD_SOCKET_ADD
的其余代码非常简单:如果新 Socket 对象应支持 SSL 对话,我们调用其 initSSL()
函数;否则,我们通过调用 receive()
函数开始在该 Socket 上接收数据。“receive
”函数有两个参数:它可以接收的最大数据块大小,以及用于接收数据的实际缓冲区指针。如您所见,在本例中,第一个参数设置为 Socket 的 getDefaultBufferSize()
。第二个参数很有意思。如果 Socket 是阻塞的,则此参数不能为 NULL
- 它将是阻塞接收数据的放置位置。对于非阻塞调用(IOCP),可以省略此参数(如本例所示)。如果未提供,数据将被接收到内部缓冲区中,然后在 onSocketEvent
委托的 SCD_SOCKET_READ
事件中通过“overlapped->buffer
”变量进行访问。这是获取/发送数据的最简单方法,但不是最有效的方法——您必须将数据从“overlapped->buffer
”变量复制到您的客户端上下文或其他地方进行进一步处理。因此,对于非 SSL 连接,您可以选择指定自己的缓冲区并将其放在 receive()
函数的第二个参数位置,数据将直接放置在那里而不是“overlapped->buffer
”,这样可以节省 CPU 复制资源的消耗。
initSSL()
函数启动 SSL 握手。第一个参数(bool
)指定这是服务器端序列(“false
”)还是客户端端(“true
”)。第二个(可选)参数指定证书上下文(如果存在)。如果未提供(传递 NULL
),系统将生成自签名证书并使用它。最后两个参数(“leftoverData
”和“blockingResult
”)仅用于阻塞调用。对于 IOCP Socket,它们将被忽略,但我们还是来介绍一下。一旦握手完成,可能仍然有一些数据在握手数据之后。例如,对方可能在发送其最后一部分握手数据的同时发送了一些额外的编码数据。这些额外数据将被尝试解码。解码结果(如果有)将放入“blockingResult
”,而之后的所有(仍编码的)数据将放入“leftoverData
”。对于(后续)对 receiveSSL()
函数的调用,您应始终在第二个参数位置提供任何未解码的剩余数据,否则结果将出错(部分要解码的数据将丢失)。
如前所述,当在 IOCP 上调用 close()
函数(或在 IOCP 上调用 requestStop()
)时,会为每个 IOCP 发送 SCD_SOCKET_IOCP_STOP_REQUESTED
事件。用户有责任通过关闭与该 IOCP 相关的所有 Socket 来响应此事件;否则,close()
函数可能会永远等待 Socket 关闭。
此处理程序可能的最后一个事件是 SCD_SOCKET_USER_EVENT
。本示例未使用它,但它可能非常有用。您可以通过调用 IOCP 的 postUserEvent()
函数来提交它。当您在池上调用 postUserEvent()
时,此事件会被广播到池中的每个 IOCP。使用它的原因是——您可以在 IOCP 线程的上下文中执行某些操作,而不是在您的(工作/主)线程中。
IOCP 池如何在 addSocket()
调用期间决定使用哪个 IOCP 成员?它选择 m_load
成员值最小的 IOCP。您可以通过 setLoad()
函数为单个 IOCP 设置其值。这个数字可以是 Socket 的数量,或者该 IOCP 在过去 X 分钟内处理的字节数,或者其他什么。请记住,如果您从不调用 setLoad()
,m_load
始终为零,并且 IOCP 池将始终选择同一个成员,即使您有多个成员。因此,指示 IOCP 的负载非常重要。最简单的方法是使此数字等于附加 Socket 的数量,即在 SCD_SOCKET_ADD
事件期间增加它,在 onSocketEvent
委托的 SCD_SOCKET_CLOSE_COMPLETED
事件期间减少它。
现在让我们转向 onSocketEvent
委托。在此委托中,eventID
参数可以具有以下值:
SCD_SOCKET_CONNECTED
SCD_SOCKET_READ
SCD_SOCKET_WRITE
SCD_SOCKET_SSL_INIT_COMPLETED
SCD_SOCKET_CLOSE_COMPLETED
SCD_SOCKET_CONNECTED
事件不会在服务器上触发,因为 SOCKET
已经连接,所以在我们的回显服务器示例代码中,我们不跟踪此事件。但对于客户端代码,您需要跟踪此事件,以便您可以启动您的程序逻辑(参见示例用例)。现在让我们从一个简单的开始:SCD_SOCKET_WRITE
事件。在实践中,您永远没有理由对此事件做出反应——它只是通知您写入 Socket 已完成,并且 IOCP 已将数据发送到 TCP 层。写入的数据可在“overlapped->buffer
”中获得,写入的字节数记录在“overlapped->bytesPassed
”变量中。
SCD_SOCKET_CLOSE_COMPLETED
已在前面的段落中介绍。这里,我们只提一下,收到此事件后删除 Socket 是绝对安全的——此时,Socket 已从 IOCP 分离并完全关闭。在示例代码中,您可以看到 Socket 在此事件发生时被删除。
SCD_SOCKET_SSL_INIT_COMPLETED
事件在 SSL 握手完成后收到。如果您阅读了上面 initSSL()
函数的描述,您应该还记得阻塞调用中的两个参数——“leftoverData
”和“blockingResult
”。好吧,对于非阻塞调用(IOCP),您将在“overlapped->buffer
”中始终获得解码后的数据,而剩余数据(如果有)将在 "overlapped->ssl_leftover"
中。在本例中,我们忽略初始解码数据(因为应该没有),然后开始读取传入流。
现在到最有趣的事件:SCD_SOCKET_READ
。在这里,我们获得了接收到的数据。对于非 SSL 处理,这很简单——您只需在“overlapped->buffer
”(或在调用 receive()
函数时提供的缓冲区)中获得数据,其长度在“overlapped->bytesPassed
”中,然后处理数据。对于 SSL 读取,这稍微复杂一些。Socket
尝试接收足够的数据进行解码。如果它收到垃圾数据,或者根本无法解码,它将关闭连接,并且永远不会调用 SCD_SOCKET_READ
事件。但是,如果它能够解码接收到的数据,它将把解码后的数据放入“overlapped->buffer
”并将其长度放入“overlapped->bytesPassed
”(就像普通读取一样),而剩余的数据将放入“overlapped->ssl_leftover
”。这些剩余数据应提供给下一次 receiveSSL()
函数的调用。
如您所见,在示例中,我将实际的“回显”代码移到了 doEcho()
虚拟函数中,这样您就可以通过继承来玩它。例如,您可以尝试回显反向数据(即,将每 10 个接收到的字节反转并以这种方式发送回去)。
现在我们完成了服务器部分,大部分功能都已解释清楚,您应该能够轻松理解客户端代码。这是一个普通的非 SSL IOCP 客户端示例:
void useCase3()
{
class CMyActivity
{
public:
void onSocketEvent(CSimpleSocket* sock, SIMPLEWSAOVERLAPPED* overlapped, DWORD socketEvent)
{
switch (socketEvent)
{
case SCD_SOCKET_CONNECTED:
m_count = 0;
if (sock->send("test0") && sock->receive(sock->getDefaultBufferSize()))
return;
sock->close(); // in case of failure - exit
break;
case SCD_SOCKET_READ:
std::cout << "client received:" << overlapped->buffer.c_str() << "\n";
m_count++;
if (m_count < 10)
{
std::string msg = "test" + std::to_string((long long)m_count);
if (sock->send(msg) && sock->receive(sock->getDefaultBufferSize()))
return;
std::cout << "client failure on SCD_SOCKET_READ, iteration" << m_count << "\n";
}
sock->close(); // done sending/receiving
break;
case SCD_SOCKET_WRITE:
std::cout << "client sent:" << overlapped->buffer.c_str() << "\n";
break;
case SCD_SOCKET_CLOSE_COMPLETED:
m_event.signalEvent(); // in case of failure - exit
break;
}
}
CSimpleAutoEvent m_event; //member event:
int m_count;
};
CSimpleIOCP iocp;
CSimpleSocket sockClient;
CMyActivity activity;
iocp += DELEGATE_ANY(&activity, CMyActivity::onSocketEvent);
iocp.init();
if (sockClient.connect("127.0.0.1", 27015, &iocp))
activity.m_event.waitForEvent();
iocp.close();
}
您可以在下载中找到其他示例。
本文中未涵盖的函数之一是 getAdapterInfoVector()
。它返回一个活动的网络适配器向量,其中包含一些(有限的)信息。目前,它只包含 IPV4 地址和 MTU 大小。此向量在网络启动时填充一次,然后每次调用该函数时返回。如果您想重新填充列表(例如,适配器被热插拔),请将参数“repopulate
”设置为“true
”来调用此函数。
此外,您还可以将客户端绑定到特定的适配器。例如,您可以将上面的客户端示例更改为:
sockClient.connect("127.0.0.1", 27015, &iocp, "127.0.0.15")
下载文件中包含的文件
- SimpleNet.h – 实际的 IOCP Sockets 实现
- FastDelegate.h – 委托回调功能(Don Clugston 代码)
- SimpleStorage.h – 用于缓冲的交错列表实现的辅助文件(用作重叠结构管理的基础)
- SimpleCriticalSection.h – 用于轻量级临界区实现的辅助文件(在
CSimpleNetManager
中用于网络初始化等) - SimpleThreadDefines.h – 用于编译器内建函数的辅助文件
因此,对于您的项目,您只需要这 5 个文件。下载中还有 2 个文件供您方便使用:
- SimpleNetTest.cpp – 用法示例和一些测试用例
- SimpleNetTest.vcxproj – VC10 的项目文件
当前问题
- 无文档。仅有的文档是本文,并且许多函数未被涵盖。不过,希望它们不言自明,并且使用示例已足够。
- 目前仅支持 IPV4,但更改为 IPV6 应该非常容易。
- 适配器信息可以并且应该扩展。目前,它只提供 IPV4 地址和 MTU 大小(这成为 Socket 的默认发送/接收包大小)。
- 需要更多服务函数,例如 Socket 选项的设置/重置。
- 仅支持 TCP 流协议。我不确定我是否会实现 UDP 协议。
- SSL 数据仅针对头部正确性和最大长度进行验证。如果发送了具有正确头部且长度小于最大值的错误数据,则无法检测到这种情况,因为 Windows SSL 解码函数无法检测到它。
顺便说一句,我不确定我是否会继续开发这个类。鉴于最近的披露,SSL 协议本身,以及 Windows 的实现,都存在 NSA 的后门,因此使用它毫无意义,除非您不关心数据的安全性。例如,您可以使用此类用于游戏服务器或客户端,但我强烈建议不要将 Windows 或 SSL 用于任何类型的敏感信息,因为它们对美国政府来说完全是透明的。因此,我个人希望将来能转向 Linux 世界。
历史
- 版本 1.0 - 初始实现