一个异步套接字服务器和客户端






4.89/5 (260投票s)
具有加密和压缩功能的异步套接字服务器和客户端。

引言
我从2000年就开始使用套接字,当时使用的是Delphi 5.0和一些第三方库(Synapse)。我的第一个套接字应用程序只是在一台服务器和许多客户端之间复制文件。客户端应用程序检查一个文件夹以查看文件是否存在,然后询问服务器在网络上复制文件的位置,复制完文件后,将一个标志添加到数据库记录中,表示文件已移动。服务器监听客户端连接,两者交换XML消息来指示每个文件复制的状态。Synapse是一个阻塞式套接字实现,我需要一个线程池机制,其工作方式类似于HTTP服务器,因为我无法保持连接打开(每连接一个线程)。我的解决方案是使用一些IOCP函数来池化客户端请求(代码),并在消息交换完成后终止连接。
现在,使用C#,我决定编写一个套接字服务器和客户端库,它能帮助我只关注消息交换(流程),而将繁重的工作交给.NET来完成。因此,我需要以下功能:
- 异步处理
- 一些加密和压缩功能
- 封装套接字,在接口中加密服务,并将其与宿主实现分离
套接字连接

ISocketConnection
是套接字连接的基接口,描述了所有连接属性和方法。ConnectionID
属性使用GUID字符串定义了一个唯一的连接ID。CustomData
属性定义了一个可以与连接关联的自定义对象。Header
属性是每个封装在数据包消息中的消息使用的套接字服务头部。只有带有已定义头部的消息才会被接受。LocalEndPoint
和 RemoteEndPoint
是连接中使用的套接字IP端点。SocketHandle
是操作系统提供的套接字句柄。
IClientSocketConnection
和 IServerSocketConnection
继承了 ISocketConnection
,并且各自具有特殊的功能。IClientSocketConnection
可以使用 BeginReconnect
方法重新连接到服务器,而 IServerSocketConnection
可以使用 BeginSendTo
和 BeginSendToAll
方法与服务器宿主中的其他连接进行通信,并可以使用 GetConnectionById
方法获取 ConnectionId
。每个连接都知道宿主、加密、压缩类型,并且可以发送、接收和断开自身与其他部分的连接。该接口在 ISocketService
接口中使用,允许用户与套接字连接进行交互。

在库实现内部,所有连接接口都是使用基础连接实现创建的:BaseSocketConnection
、ClientSocketConnection
和 ServerSocketConnection
。
套接字服务

ISocketService
描述了连接事件。这些事件由宿主触发,并带有一个 ConnectionEventArgs
参数,该参数包含一个标识连接的 ISocketConnection
。在 OnReceived
和 OnSent
事件中,会传递一个 MessageEventArgs
,其中包含已发送或接收的字节数组。在 OnDisconnected
事件中,会传递一个 DisconnectedEventArgs
;Exception
属性指示断开连接是否由异常引起。
以下是 ISocketService
实现的一个示例
public class SimpleEchoService : ISocketService
{
public void OnConnected(ConnectionEventArgs e)
{
//----- Check the host!
if (e.Connection.HostType == HostType.htServer)
{
//----- Enqueue receive!
e.Connection.BeginReceive();
}
else
{
//----- Enqueue send a custom message!
byte[] b =
GetMessage(e.Connection.SocketHandle.ToInt32());
e.Connection.BeginSend(b);
}
}
public void OnSent(MessageEventArgs e)
{
//----- Check the host. In this case both start a receive!
if (e.Connection.HostType == HostType.htServer)
{
//----- Enqueue receive!
e.Connection.BeginReceive();
}
else
{
//----- Enqueue receive!
e.Connection.BeginReceive();
}
}
public override void OnReceived(MessageEventArgs e)
{
//----- Check the host!
if (e.Connection.HostType == HostType.htServer)
{
//----- If server, send the data buffer received!
byte[] b = e.Buffer;
e.Connection.BeginSend(b);
}
else
{
//----- If client, generate another
//----- custom message and send it!
byte[] b = GetMessage(e.Connection.SocketHandle.ToInt32());
e.Connection.BeginSend(b);
}
}
public override void OnDisconnected(DisconnectedEventArgs e)
{
//----- Check the host!
if (e.Connection.HostType == HostType.htServer)
{
//----- Nothing!
}
else
{
//----- Reconnect with server!
e.Connection.AsClientConnection().BeginReconnect();
}
}
}
ISocketService
实现可以在同一个宿主程序集中完成,也可以在宿主引用的另一个程序集中完成。这允许用户将宿主实现与套接字服务分开,有助于服务器或域的管理。
连接宿主

有了 ISocketService
后,您需要托管服务和服务连接。服务器和客户端宿主都具有相同的父类 BaseSocketConnectionHost
,该类维护连接列表、加密和压缩数据缓冲区、排队服务请求并确保所有数据缓冲区已发送或接收、检查消息头部以及检查空闲连接。CheckTimeoutTimer
会定期(以 IdleCheckInterval
为间隔)使用 IdleTimeOutValue
作为空闲超时值来检查连接是否变为空闲。Header
是宿主使用的套接字服务头部。HostType
指示宿主是服务器还是客户端宿主。SocketBufferSize
定义了套接字发送和接收缓冲区的大小。SocketService
是 ISocketService
的实例,用于驱动连接之间的消息交换。
加密和压缩

每次发送和接收消息时,宿主都会检查数据是否需要加密和/或压缩,这些工作由 CryptUtils
静态类完成。CreateSymmetricAlgoritm
根据 encryptType
参数创建一个 ISymmetricAlgoritm
。DecryptData
和 DecryptDataForAuthenticate
分别用于解密接收到的消息并检查身份验证过程中的哈希签名。EncryptData
和 EncryptDataForAuthenticate
分别加密要发送的数据并签名经过身份验证的消息。

加密的数据缓冲区用服务头部和数据缓冲区长度进行标记,形成一个数据包缓冲区。这个数据包缓冲区由 MessageBuffer
类控制,该类保存有关数据包缓冲区偏移量、长度、剩余字节和原始缓冲区的信息。
排队请求
每次在 ISocketService
中调用 BeginReceive
或 BeginSend
时,宿主都会检查是否已启动某个请求。如果请求正在处理中,宿主会排队该请求。如果未启动,则触发该请求。
发送请求
在 BeginSend
方法中,使用以下排队机制:
internal void BeginSend(BaseSocketConnection connection, byte[] buffer)
{
...
//----- Check Queue!
lock (connection.WriteQueue)
{
if (connection.WriteQueueHasItems)
{
//----- If the connection is sending, enqueue the message!
connection.WriteQueue.Enqueue(writeMessage);
}
else
{
//----- If the connection is not sending, send the message!
connection.WriteQueueHasItems = true;
...
消息发送后,在发送回调中,宿主会再次检查队列,并在需要时启动另一个发送过程。
private void BeginSendCallback(IAsyncResult ar)
{
...
//----- Check Queue!
lock (connection.WriteQueue)
{
if (connection.WriteQueue.Count > 0)
{
//----- If has items, send it!
MessageBuffer dequeueWriteMessage =
connection.WriteQueue.Dequeue();
...
}
else
{
connection.WriteQueueHasItems = false;
}
}
...
接收请求
同样的技术也适用于接收方法:所有对 BeginReceive
的调用都会被排队,如果接收方法正在执行。如果没有启动接收过程,宿主就会开始接收。
internal void BeginReceive(BaseSocketConnection connection)
{
...
//----- Check Queue!
lock (connection.SyncReadCount)
{
if (connection.ReadCanEnqueue)
{
if (connection.ReadCount == 0)
{
//----- if the connection is not receiving, start the receive!
MessageBuffer readMessage = new MessageBuffer
(FSocketBufferSize);
...
}
//----- Increase the read count!
connection.ReadCount++;
}
}
...
之后,当消息在接收回调中接收并解析后,宿主会再次检查读取队列,并在需要时启动另一个接收过程。
private void BeginReadCallback(IAsyncResult ar)
{
...
//----- Check Queue!
lock (connection.SyncReadCount)
{
connection.ReadCount--;
if (connection.ReadCount > 0)
{
//----- if the read queue has items, start to receive!
...
}
}
...
确保发送和接收
为了确保所有数据缓冲区都已发送,BaseSocketConnectionHost
会检查已发送的字节数,并将其与 MessageBuffer
类进行比较。它会继续发送剩余的字节,直到所有数据缓冲区都已发送。
private void BeginSendCallback(IAsyncResult ar)
{
...
byte[] sent = null;
int writeBytes = .EndSend(ar);
if (writeBytes < writeMessage.PacketBuffer.Length)
{
//----- Continue to send until all bytes are sent!
writeMessage.PacketOffSet += writeBytes;
.BeginSend(writeMessage.PacketBuffer, writeMessage.PacketOffSet,
writeMessage.PacketRemaining, SocketFlags.None ...);
}
else
{
sent = new byte[writeMessage.RawBuffer.Length];
Array.Copy(writeMessage.RawBuffer, 0, sent, 0,
writeMessage.RawBuffer.Length);
FireOnSent(connection, sent);
}
}
接收数据缓冲区也使用相同的方法,因为读取数据时,MessageBuffer
被用作读取缓冲区。当调用接收回调时,它会继续读取,直到消息中的所有字节都已读取。
private void BeginReadCallback(IAsyncResult ar)
{
...
CallbackData callbackData = (CallbackData)ar.AsyncState;
connection = callbackData.Connection;
readMessage = callbackData.Buffer;
int readBytes = 0;
...
readBytes = .EndReceive(ar);
...
if (readBytes > 0)
{
...
//----- Has bytes!
...
//----- Process received data!
readMessage.PacketOffSet += readBytes;
...
if (readSocket)
{
//----- Read More!
.BeginReceive(readMessage.PacketBuffer,
readMessage.PacketOffSet,
readMessage.PacketRemaining,
SocketFlags.None, ...);
}
}
...
检查消息头部
如果套接字服务使用某个头部,那么所有发送和接收过程都需要创建一个数据包消息,指示头部和消息长度。这个数据包标签使用以下结构创建:

第一个标签部分是套接字服务头部。头部是任意长度的字节数组,这里需要一些建议:如果您选择一个非常小的头部,可能会出现一个消息具有与其他消息相同的字节数组的情况,导致宿主丢失顺序。如果您选择一个非常长的字节数组,宿主可能会花费处理器时间来验证消息头部是否等于套接字服务头部。第二部分是数据包消息的长度。此长度通过将原始消息数据缓冲区长度(加密和/或压缩后)加上头部长度来计算。
发送数据包
如前所述,每次发送消息时,宿主都会检查数据是否需要加密和/或压缩,如果您选择使用某个头部,则原始缓冲区由 MessageBuffer
类控制。此类的创建使用 GetPacketMessage
static
方法。
public static MessageBuffer GetPacketMessage(
BaseSocketConnection connection, ref byte[] buffer)
{
byte[] workBuffer = null;
workBuffer = CryptUtils.EncryptData(connection, buffer);
if (connection.Header != null && connection.Header.Length >= 0)
{
//----- Need header!
int headerSize = connection.Header.Length + 2;
byte[] result = new byte[workBuffer.Length + headerSize];
int messageLength = result.Length;
//----- Header!
for (int i = 0; i < connection.Header.Length; i++)
{
result[i] = connection.Header[i];
}
//----- Length!
result[connection.Header.Length] =
Convert.ToByte((messageLength & 0xFF00) >> 8);
result[connection.Header.Length + 1] =
Convert.ToByte(messageLength & 0xFF);
Array.Copy(workBuffer, 0, result,
headerSize, workBuffer.Length);
return new MessageBuffer(ref buffer, ref result);
}
else
{
//----- No header!
return new MessageBuffer(ref buffer, ref workBuffer);
}
}
接收数据包
接收过程(如果您使用某种套接字服务头部)需要检查头部,并继续读取字节,直到接收到整个数据包消息。此过程在读取回调中执行。
private void BeginReadCallback(IAsyncResult ar)
{
...
byte[] received = null
byte[] rawBuffer = null;
byte[] connectionHeader = connection.Header;
readMessage.PacketOffSet += readBytes;
if ((connectionHeader != null) && (connectionHeader.Length > 0))
{
//----- Message with header!
int headerSize = connectionHeader.Length + 2;
bool readPacket = false;
bool readSocket = false;
do
{
connection.LastAction = DateTime.Now;
if (readMessage.PacketOffSet > headerSize)
{
//----- Has Header!
for (int i = 0; i < connectionHeader.Length; i++)
{
if (connectionHeader[i] != readMessage.PacketBuffer[i])
{
//----- Bad Header!
throw new BadHeaderException(
"Message header is different from Host header.");
}
}
//----- Get Length!
int messageLength =
(readMessage.PacketBuffer[connectionHeader.Length] << 8) +
readMessage.PacketBuffer[connectionHeader.Length + 1];
if (messageLength > FMessageBufferSize)
{
throw new MessageLengthException("Message " +
"length is greater than Host maximum message length.");
}
//----- Check Length!
if (messageLength == readMessage.PacketOffSet)
{
//----- Equal -> Get rawBuffer!
rawBuffer =
readMessage.GetRawBuffer(messageLength, headerSize);
readPacket = false;
readSocket = false;
}
else
{
if (messageLength < readMessage.PacketOffSet)
{
//----- Less -> Get rawBuffer and fire event!
rawBuffer =
readMessage.GetRawBuffer(messageLength, headerSize);
//----- Decrypt!
rawBuffer = CryptUtils.DecryptData(connection,
ref rawBuffer, FMessageBufferSize);
readPacket = true;
readSocket = false;
received = new byte[rawBuffer.Length];
Array.Copy(rawBuffer, 0, received, 0,
rawBuffer.Length);
FireOnReceived(connection, received, false);
}
else
{
if (messageLength > readMessage.PacketOffSet)
{
//----- Greater -> Read Socket!
if (messageLength > readMessage.PacketLength)
{
readMessage.Resize(messageLength);
}
readPacket = false;
readSocket = true;
}
}
}
}
else
{
if (readMessage.PacketRemaining < headerSize)
{
//----- Adjust room for more!
readMessage.Resize(readMessage.PacketLength + headerSize);
}
readPacket = false;
readSocket = true;
}
} while (readPacket);
if (readSocket)
{
//----- Read More!
...
.BeginReceive(readMessage.PacketBuffer, readMessage.PacketOffSet,
readMessage.PacketRemaining, SocketFlags.None, ...);
...
}
}
else
{
//----- Message with no header!
rawBuffer = readMessage.GetRawBuffer(readBytes, 0);
}
if (rawBuffer != null)
{
//----- Decrypt!
rawBuffer = CryptUtils.DecryptData(connection,
ref rawBuffer, FMessageBufferSize);
received = new byte[rawBuffer.Length];
Array.Copy(rawBuffer, 0, received, 0, rawBuffer.Length);
FireOnReceived(connection, received, true);
readMessage.Resize(FSocketBufferSize);
...
读取回调方法首先检查连接是否具有某个头部,如果没有,则直接获取原始缓冲区并继续。如果连接具有某个头部,则该方法需要将消息头部与套接字服务头部进行比较。在此之前,它会检查数据包消息长度是否大于连接头部长度,以确保能够解析总消息长度。如果不是,它会读取一些字节。检查头部后,该方法会解析消息长度,并与数据包长度进行比较。如果长度相等,则获取原始缓冲区并终止循环。如果消息长度小于数据包消息的长度,则我们得到消息加上一些额外数据。因此,该方法获取原始缓冲区并使用相同的 MessageBuffer
类继续读取。如果消息长度大于数据包消息的长度,在读取一些数据之前,它会简单地将数据包缓冲区的大小调整为消息大小,从而确保有足够的空间供更多读取的字节。
检查空闲连接
使用 ISocketConnection
的 BeginSend
和 BeginReceive
方法不返回任何 IAsyncResult
来知道方法是否已完成,从而允许在某个超时值后断开连接。为了防止这种情况,BaseSocketConnectionHost
有一个 System.Threading.Timer
,它会定期检查 BaseSocketConnection
的 LastAction
属性。如果 LastAction
大于空闲超时值,则连接将关闭。
加密服务

ICryptoService
描述了在连接到对方时触发的身份验证方法。当使用 EncryptType.etRijndael
或 EncryptType.etTripleDES
时,会触发 OnSymmetricAuthenticate
方法,当使用 EncryptType.etSSL
时,会触发 OnSSLXXXXAuthentication
。与 ISocketService
类似,ICryptService
可以在同一个宿主程序集中完成,或者在宿主引用的另一个程序集中完成,因此您可以使用一个 ICryptoService
实现来应用于多个 ISocketService
实现。
SSL 身份验证
.NET 2.0 中有一个名为 SslStream
的新流类,它可以进行 SSL 流的身份验证。SslStream
的构造函数接受一个 NetworkStream
类,而这个流是使用 Socket
类创建的。因此,使用 SslStream
,您可以通过套接字连接发送和接收数据缓冲区。
服务器身份验证
SslStream
的身份验证在客户端和服务器上都进行,但它们各自有不同的参数。在服务器端,您需要通过 X509Certificate2
类传递一个证书,可以通过 X509Store
在证书存储中查找,或者从证书文件(* .cer)创建。此外,您还可以请求客户端身份验证并检查证书的吊销情况。以下代码是一个使用 ICryptService
进行 SSL 服务器身份验证的示例:
public void OnSSLServerAuthenticate(out X509Certificate2 certificate,
out bool clientAuthenticate, ref bool checkRevocation)
{
//----- Set server certificate, client
//----- authentication and certificate revocation!
X509Store store = new X509Store(StoreName.My,
StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs =
store.Certificates.Find(X509FindType.FindBySubjectName,
"ALAZ Library", false);
certificate = certs[0];
clientAuthenticate = false;
checkRevocation = false;
store.Close();
}
客户端身份验证
在 SSL 身份验证的客户端,您需要传递服务器证书的主机名,如果该名称不匹配,身份验证将失败。您可以使用 X509Certificate2Collection
传递客户端证书集合。如果服务器不请求客户端身份验证,您则无需传递集合,但如果服务器请求,您可以使用 X509Store
查找证书。您还可以请求客户端证书的吊销情况。这是 ICryptoService
中 SSL 客户端身份验证的示例:
public void OnSSLClientAuthenticate(out string serverName,
ref X509Certificate2Collection certs, ref bool checkRevocation)
{
serverName = "ALAZ Library";
/*
//----- Using client certificate!
X509Store store = new X509Store(StoreName.My,
StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
certs = store.Certificates.Find(
X509FindType.FindBySubjectName,
serverName, true);
checkRevocation = false;
store.Close();
*/
}
证书
要创建证书,您可以使用 .NET 中的 MakeCert.exe 工具,并且有很多关于它的信息。您可以查看 John Howard 的页面、这个 MS 帖子 和 这个网站。
对称身份验证
为了在此库中实现一些对称加密和身份验证,我决定在 Microsoft 新闻组中发布一个帖子。不幸的是,虽然该帖子内容如此,但幸运的是(非常感谢 Joe Kaplan、Dominick Baier 和 Valery Pryamikov)为了知识共享,我决定使用 William Stacey 的实现示例 **“一种使用交换会话密钥发送安全消息的通用方法”**。在该代码中,会话中使用的对称密钥使用 RSA 密钥对进行加密和签名,而客户端部分需要知道加密后的服务器公钥,这意味着该密钥并非在身份验证过程中从服务器接收。客户端和服务器都需要通过手动方式了解此密钥。为了确保这一点,OnSymmetricAuthenticate
需要一个 RSACryptoServiceProvider
类来提供用于加密的密钥对。您可以从 XML 字符串、文件、CspParameters
类或证书中填充 RSACryptoServiceProvider
。以下是一个对称身份验证的示例:
public void OnSymmetricAuthenticate(HostType hostType,
out RSACryptoServiceProvider serverKey)
{
/*
* A RSACryptoServiceProvider is needed to encrypt and send session key.
* In server side you need public and private key to decrypt session key.
* In client side you need only public key to encrypt session key.
*
* You can create a RSACryptoServiceProvider from a string
* (file, registry), a CspParameters or a certificate.
*/
//----- Using string!
/*
serverKey = new RSACryptoServiceProvider();
serverKey.FromXMLString("XML key string");
*/
//----- Using CspParameters!
CspParameters param = new CspParameters();
param.KeyContainerName = "ALAZ_ECHO_SERVICE";
serverKey = new RSACryptoServiceProvider(param);
/*
//----- Using Certificate Store!
X509Store store = new X509Store(StoreName.My,
StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2 certificate = store.Certificates.Find(
X509FindType.FindBySubjectName,
"ALAZ Library", true)[0];
serverKey = new RSACryptoServiceProvider();
if (hostType == HostType.htClient)
{
//----- In client only public key is needed!
serverKey = (RSACryptoServiceProvider)certificate.PublicKey.Key;
}
else
{
//----- In server, both public and private key is needed!
serverKey.FromXmlString(certificate.PrivateKey.ToXmlString(true));
}
store.Close();
*/
}
身份验证消息

对称身份验证使用 AuthMessage
结构在客户端和服务器之间交换会话密钥。SessionKey
和 SessionIV
属性分别是算法的对称密钥和初始化向量。Sign
属性是客户端使用内部创建的签名 RSACryptoServiceProvider
类生成的哈希码,其公钥通过 SourceKey
属性进行交换。这个内部签名密钥对对于签名 AuthMessage
是必需的,服务器可以确保 AuthMessage
是准确的。此过程通过以下代码完成:
客户端
...
//----- Sign Message!
private byte[] signMessage = new byte[]
{ <sign message array of bytes for authentication> };
...
protected virtual void InitializeConnection(BaseSocketConnection connection)
{
...
//----- Symmetric!
if (connection.EncryptType == EncryptType.etRijndael ||
connection.EncryptType == EncryptType.etTripleDES)
{
if (FHost.HostType == HostType.htClient)
{
//----- Get RSA provider!
RSACryptoServiceProvider serverPublicKey;
RSACryptoServiceProvider clientPrivateKey =
new RSACryptoServiceProvider();
FCryptoService.OnSymmetricAuthenticate(FHost.HostType,
out serverPublicKey);
//----- Generates symmetric algorithm!
SymmetricAlgorithm sa =
CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType);
sa.GenerateIV();
sa.GenerateKey();
//----- Adjust connection cryptors!
connection.Encryptor = sa.CreateEncryptor();
connection.Decryptor = sa.CreateDecryptor();
//----- Create authenticate structure!
AuthMessage am = new AuthMessage();
am.SessionIV = serverPublicKey.Encrypt(sa.IV, false);
am.SessionKey = serverPublicKey.Encrypt(sa.Key, false);
am.SourceKey =
CryptUtils.EncryptDataForAuthenticate(sa,
Encoding.UTF8.GetBytes(clientPrivateKey.ToXmlString(false)),
PaddingMode.ISO10126);
//----- Sign message with am.SourceKey,
//----- am.SessionKey and signMessage!
//----- Need to use PaddingMode.PKCS7 in sign!
MemoryStream m = new MemoryStream();
m.Write(am.SourceKey, 0, am.SourceKey.Length);
m.Write(am.SessionKey, 0, am.SessionKey.Length);
m.Write(signMessage, 0, signMessage.Length);
am.Sign = clientPrivateKey.SignData(
CryptUtils.EncryptDataForAuthenticate(sa,
m.ToArray(), PaddingMode.PKCS7),
new SHA1CryptoServiceProvider());
//----- Serialize authentication message!
XmlSerializer xml = new XmlSerializer(typeof(AuthMessage));
m.SetLength(0);
xml.Serialize(m, am);
//----- Send structure!
MessageBuffer mb = new MessageBuffer(0);
mb.PacketBuffer =
Encoding.Default.GetBytes(Convert.ToBase64String(m.ToArray()));
connection.Socket.BeginSend(
mb.PacketBuffer, mb.PacketOffSet,
mb.PacketRemaining, SocketFlags.None,
new AsyncCallback(InitializeConnectionSendCallback),
new CallbackData(connection, mb));
m.Dispose();
am.SessionIV.Initialize();
am.SessionKey.Initialize();
serverPublicKey.Clear();
clientPrivateKey.Clear();
}
...
}
在对称身份验证的客户端,会调用 OnSymmetricAuthenticate
,获取 RSACryptoServiceProvider
来加密 CryptUtils.CreateSymmetricAlgoritm
方法生成的会话密钥。AuthMessage
会用加密的会话密钥、会话 IV 和签名公钥进行填充。为了签名消息,会使用 SourceKey
、SessionKey
和 signMessage
,并将生成的哈希分配给 Sign
属性。
服务器端
protected virtual void InitializeConnection(BaseSocketConnection connection)
{
...
if (FHost.HostType == HostType.htClient)
{
...
}
else
{
//----- Create empty authenticate structure!
MessageBuffer mb = new MessageBuffer(8192);
//----- Start receive structure!
connection.Socket.BeginReceive(mb.PacketBuffer, mb.PacketOffSet,
mb.PacketRemaining, SocketFlags.None,
new AsyncCallback(InitializeConnectionReceiveCallback), ...);
}
}
private void InitializeConnectionReceiveCallback(IAsyncResult ar)
{
...
bool readSocket = true;
int readBytes = ....EndReceive(ar);
if (readBytes > 0)
{
readMessage.PacketOffSet += readBytes;
byte[] message = null;
try
{
message = Convert.FromBase64String(
Encoding.Default.GetString(readMessage.PacketBuffer,
0, readMessage.PacketOffSet));
}
catch (FormatException)
{
//----- Base64 transformation error!
}
if ((message != null) &&
(Encoding.Default.GetString(message).Contains("</AuthMessage>")))
{
//----- Get RSA provider!
RSACryptoServiceProvider serverPrivateKey;
RSACryptoServiceProvider clientPublicKey =
new RSACryptoServiceProvider();
FCryptoService.OnSymmetricAuthenticate(FHost.HostType,
out serverPrivateKey);
//----- Deserialize authentication message!
MemoryStream m = new MemoryStream();
m.Write(message, 0, message.Length);
m.Position = 0;
XmlSerializer xml = new XmlSerializer(typeof(AuthMessage));
AuthMessage am = (AuthMessage)xml.Deserialize(m);
//----- Generates symmetric algorithm!
SymmetricAlgorithm sa =
CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType);
sa.Key = serverPrivateKey.Decrypt(am.SessionKey, false);
sa.IV = serverPrivateKey.Decrypt(am.SessionIV, false);
//----- Adjust connection cryptors!
connection.Encryptor = sa.CreateEncryptor();
connection.Decryptor = sa.CreateDecryptor();
//----- Verify sign!
clientPublicKey.FromXmlString(Encoding.UTF8.GetString(
CryptUtils.DecryptDataForAuthenticate(sa,
am.SourceKey, PaddingMode.ISO10126)));
m.SetLength(0);
m.Write(am.SourceKey, 0, am.SourceKey.Length);
m.Write(am.SessionKey, 0, am.SessionKey.Length);
m.Write(signMessage, 0, signMessage.Length);
if (!clientPublicKey.VerifyData(
CryptUtils.EncryptDataForAuthenticate(sa, m.ToArray(),
PaddingMode.PKCS7),
new SHA1CryptoServiceProvider(), am.Sign))
{
throw new
SymmetricAuthenticationException("Symmetric sign error.");
}
readSocket = false;
m.Dispose();
am.SessionIV.Initialize();
am.SessionKey.Initialize();
serverPrivateKey.Clear();
clientPublicKey.Clear();
FHost.FireOnConnected(connection);
}
if (readSocket)
{
....BeginReceive(readMessage.PacketBuffer,
readMessage.PacketOffSet,
readMessage.PacketRemaining,
SocketFlags.None,
new AsyncCallback(
InitializeConnectionReceiveCallback), ...);
}
}
在对称身份验证的服务器端,会使用 MessageBuffer
来接收套接字缓冲区。读取回调方法会继续读取,直到接收到完整的 AuthMessage
。使用此消息,该方法会调用 OnSymmetricAuthenticate
来获取 RSACryptoServiceProvider
以解密会话密钥、会话 IV 和签名公钥。在所有密钥都解密后,该方法会验证 Sign
属性,以确保 AuthMessage
是准确的,使用 SourceKey
、SessionKey
和 signMessage
。
连接创建者

虽然 BaseSocketConnectionHost
可以管理 ISocketConnection
连接,但它不能创建它们。这项工作由 BaseSocketConnectionCreator
完成,它负责创建和初始化 ISocketConnection
。CompressionType
和 EncryptType
属性分别定义了连接中将使用的压缩和加密类型。CryptoService
定义了初始化连接时(如果需要)使用的 ICrytoService
实例。Host
属性定义了 BaseSocketConnectionCreator
的宿主;它可以是服务器或客户端宿主。LocalEndPoint
定义了连接中使用的套接字 IP 端点,并且其行为可能因创建者的类型而异。
SocketServer 和 SocketListener

SocketServer
和 SocketListener
是创建套接字服务器所需的类。SocketServer
继承自 BaseSocketConnectionHost
,并管理 ISocketConnections
。SocketListener
继承自 BaseSocketConnectionCreator
,它监听传入的连接,接受连接,并创建一个新的 ISocketConnection
来使用。一个 SocketServer
可以附加任意数量的 SocketListener
,每个 SocketListener
分配给一个本地端口进行监听。
SocketServer 构造函数和方法

在 SocketServer
构造函数中,socketService
参数定义了服务器使用的 ISocketService
实例。头部参数定义了在消息头部交换中使用的字节数组。socketBufferSize
调整套接字缓冲区大小。messageBufferSize
定义了服务的最大消息大小。idleCheckInterval
指示空闲连接检查的间隔(以毫秒为单位)。idleTimeoutValue
定义了超时值(以毫秒为单位),用于与每个连接的 LastAction
属性进行比较。
要将 SocketListener
项添加到 SocketServer
中,必须使用 AddListener
方法。localEndPoint
参数定义了用于监听连接的本地套接字 IP 端点。encryptType
和 compressionType
分别定义了在新接受的连接中使用的加密和压缩方法。cryptoService
定义了用于对所选加密方法进行身份验证的 ICryptoService
。backLog
将操作系统的套接字监听队列限制为定义的数量,而 acceptThreads
设置了套接字 BeginAccept
的调用次数以提高接受性能。
HostThreadPool

此库使用异步套接字通信,而这又使用了 .NET 的 ThreadPool
。在 .NET 2.0 ThreadPool
中,可以使用 SetMaxThreads
和 SetMinThreads
方法控制线程数,我认为这个类有很多改进。但是,如果您不想使用 .NET 类,可以使用一个托管线程池,称为 HostThreadPool
,它非常类似于 Stephen Toub 的 ManagedThreadPool。HostThreadPool
使用一个托管线程列表,该列表随着提供的排队任务的增多而不断增加。要使用这个类而不是 SocketServer
中的 .NET ThreadPool
,只需将构造函数参数 minThreads
和 maxThreads
设置为非零值即可。
以下是一些使用 SocketServer
和 SocketListener
的示例
//----- Simple server!
SocketServer server = new SocketServer(new SimpleEchoService());
//----- Simple listener!
server.AddListener(new IPEndPoint(IPAddress.Any, 8087));
server.Start();
//----- Server with header!
SocketServer server = new SocketServer(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD });
//----- Listener with simple encryption!
server.AddListener(new IPEndPoint(IPAddress.Any, 8087),
EncryptType.etBase64, CompressionType.ctNone, null);
server.Start();
//----- Server with header and buffer
//----- sizes, no hostthreadpool and idle check setting!
SocketServer server = new SocketServer(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
2048, 8192, 0, 0, 60000, 30000);
//----- More than one listener each one with different listen port number!
server.AddListener(new IPEndPoint(IPAddress.Any, 8087));
server.AddListener(new IPEndPoint(IPAddress.Any, 8088),
EncryptType.etBase64, CompressionType.ctNone, null);
server.AddListener(new IPEndPoint(IPAddress.Any, 8089),
EncryptType.etRijndael, CompressionType.ctGZIP,
new SimpleEchoCryptService(), 50, 10);
server.AddListener(new IPEndPoint(IPAddress.Any, 8090),
EncryptType.etSSL, CompressionType.ctNone,
new SimpleEchoCryptService());
server.Start();
SocketClient 和 SocketConnector

SocketClient
和 SocketConnector
是创建套接字客户端所需的类。SocketClient
继承自 BaseSocketConnectionHost
,并且与 SocketServer
一样,管理 ISocketConnections
。SocketConnector
继承自 BaseSocketConnectionCreator
,它连接到套接字服务器并创建一个新的 ISocketConnection
来使用。一个 SocketClient
可以附加任意数量的 SocketConnector
,每个 SocketConnector
连接到一个套接字服务器,并且可以分配给一个本地地址和一个本地端口来启动连接。
SocketClient 构造函数和方法

SocketClient
构造函数与 SocketServer
类的参数签名相同。要将 SocketConnector
项添加到 SocketClient
中,必须使用 AddConnector
方法。remoteEndPoint
参数定义了用于连接的远程套接字 IP 端点。encryptType
和 compressionType
分别定义了在新连接中使用的加密和压缩方法。cryptoService
定义了用于对所选加密方法进行身份验证的 ICryptoService
。reconnectAttempts
和 reconnectAttemptInterval
分别定义了在使用 BeginReconnect
方法时重新连接的尝试次数以及重新连接的时间间隔。localEndPoint
定义了用于启动与远程端点连接过程的本地套接字 IP 端点。
以下是一些使用 SocketClient
和 SocketConnector
的示例
//----- Simple client!
SocketClient client = new SocketClient(new SimpleEchoService());
//----- Simple connector!
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087));
client.Start();
//----- Client with header!
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD });
//----- Connector with simple encryption!
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etBase64, CompressionType.ctNone, null);
client.Start();
//----- Client with header and buffer sizes,
//----- no hostthreadpool and idle check setting!
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
2048, 8192, 0, 0, 60000, 30000);
//----- Connector with encryption and reconnect!
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etSSL, CompressionType.ctGZIP,
new SimpleEchoCryptService(),
5, 30000);
client.Start();
//----- Client with header and buffer sizes,
//----- using hostthreadpool and idle check setting!
SocketClient client = new SocketClient(new SimpleEchoService(),
new byte[] { 0xFF, 0xFE, 0xFD },
4096, 8192, 5, 50, 60000, 30000);
//----- Connector with encryption, reconnect and local endpoint!
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087),
EncryptType.etSSL, CompressionType.ctGZIP,
new SimpleEchoCryptService(),
5, 30000,
new IPEndPoint(IPAddress.Parse("10.10.3.1"), 2000));
client.Start();
//----- Simple client!
SocketClient client = new SocketClient(new SimpleEchoService());
//----- More than one connector each one with different remote socket servers!
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087));
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.2"), 8088),
EncryptType.etBase64, CompressionType.ctNone, null);
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.3"), 8089),
EncryptType.etRijndael, CompressionType.ctGZIP,
new SimpleEchoCryptService());
client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.4"), 8090),
EncryptType.etSSL, CompressionType.ctNone,
new SimpleEchoCryptService(),
5, 30000,
new IPEndPoint(IPAddress.Parse("10.10.3.1"), 2000));
client.Start();
Echo 演示项目
在文章下载文件中提供了一个 Echo
演示项目,使用 **控制台**、**Windows 窗体** 和 **Windows 服务** 宿主和客户端,所有这些都使用相同的 EchoSocketService
和 EchoCryptService
。演示按照其类型进行了划分,如下所示:
宿主
- 控制台
EchoConsoleClient
EchoConsoleServer
- Windows Forms
EchoFormClient
EchoFormServer
Echo<code>
Form (窗体模板)
- Windows 服务
EchoWindowsServiceServer
服务
EchoSocketService
EchoCryptService
结论
这里有很多内容,我认为这个库可以帮助任何想要编写具有加密和压缩功能的异步套接字的人。欢迎任何评论。
历史
- 2006年5月15日: 初始版本
- 2006年5月19日: 一些英文文本修正(抱歉,我还在学习!),并重新检查了演示源代码
- 2006年6月6日: 版本 1.2 包含以下更改:
- 修复了次要 bug
- 所有“Sended”已改为“Sent”(感谢 vmihalj)
ReadCanEnqueue
现在可以正确地与HostThreadPool
一起工作(感谢 PunCha)- 为客户端连接添加了
reconnectAttempts
和reconnectAttemptInterval
,以允许客户端连接在指定的时间间隔内进行任意次数的重连(感谢 Tobias Hertkorn)
- 2007年4月1日: 版本 1.3 包含以下更改:
- 修复了
rawbuffer = null
的问题 - 修复了
BeginAcceptCallback
:发生异常时停止接受 - 修复了
BeginSendCallback
:应使用PacketRemaining
字节 - 在演示中添加了套接字配置部分
- 新的消息大小(64K)
- 移除了
HosThreadPool
- 头部更改为 Delimiter 属性,并新增了分隔符选项
dtNone
:无消息分隔符dtPacketHeader
:与 1.2 版本兼容dtMessageTailExcludeOnReceive
:在消息末尾使用自定义分隔符(接收时排除分隔符)dtMessageTailIncludeOnReceive
:在消息末尾使用自定义分隔符(接收时包含分隔符)
- 新的连接对象属性/方法
- Nagle、Linger 和 TTL 算法选项
- 宿主和创建者
- 在服务类中加密签名消息
- 服务类中的异常事件
- 新的创建者名称属性
- 修复了
- 2007年7月22日: 版本 1.4 包含以下更改:
- 连接初始化过程在同一线程中执行(不在
ThreadPool
中排队) - 连接断开现在会检查 Windows 版本并执行正确的断开过程
- 连接活动检查
Disposed
- 修复了
CheckSocketConnections
的 disposed 检查 - 包含了
CryptUtils
的Flush()
方法 - 修复了客户端连接
BeginConnect()
的异常 - 修复了服务器连接
BeginSendToAll
的数组缓冲区 - 新增
SocketClientSync
类用于同步使用(包含 WinForms 演示)
- 连接初始化过程在同一线程中执行(不在
- 2007年9月5日: 版本 1.5 包含以下更改:
SocketClient
支持代理身份验证(SOCKS5、Basic HTTP)- 修复了
BeginRead
错误(messagetail
) - 更改了
BeginDisconnect
(threadpool
) - 审查了
BeginSendToAll
(disposed 检查) - 新增
OnSSLClientValidateServerCertificate
事件,用于验证服务器证书 - 将空闲检查间隔设置为 0,仅在大于 0 时创建
- 使用
Buffer.BlockCopy
替代Array.Copy
- 新的聊天演示