65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (260投票s)

2006年5月18日

CPOL

16分钟阅读

viewsIcon

4243535

downloadIcon

25701

具有加密和压缩功能的异步套接字服务器和客户端。

Article screenshot

引言

我从2000年就开始使用套接字,当时使用的是Delphi 5.0和一些第三方库(Synapse)。我的第一个套接字应用程序只是在一台服务器和许多客户端之间复制文件。客户端应用程序检查一个文件夹以查看文件是否存在,然后询问服务器在网络上复制文件的位置,复制完文件后,将一个标志添加到数据库记录中,表示文件已移动。服务器监听客户端连接,两者交换XML消息来指示每个文件复制的状态。Synapse是一个阻塞式套接字实现,我需要一个线程池机制,其工作方式类似于HTTP服务器,因为我无法保持连接打开(每连接一个线程)。我的解决方案是使用一些IOCP函数来池化客户端请求(代码),并在消息交换完成后终止连接。

现在,使用C#,我决定编写一个套接字服务器和客户端库,它能帮助我只关注消息交换(流程),而将繁重的工作交给.NET来完成。因此,我需要以下功能:

  • 异步处理
  • 一些加密和压缩功能
  • 封装套接字,在接口中加密服务,并将其与宿主实现分离

套接字连接

ISocketConnection 是套接字连接的基接口,描述了所有连接属性和方法。ConnectionID 属性使用GUID字符串定义了一个唯一的连接ID。CustomData 属性定义了一个可以与连接关联的自定义对象。Header 属性是每个封装在数据包消息中的消息使用的套接字服务头部。只有带有已定义头部的消息才会被接受。LocalEndPointRemoteEndPoint 是连接中使用的套接字IP端点。SocketHandle 是操作系统提供的套接字句柄。

IClientSocketConnectionIServerSocketConnection 继承了 ISocketConnection,并且各自具有特殊的功能。IClientSocketConnection 可以使用 BeginReconnect 方法重新连接到服务器,而 IServerSocketConnection 可以使用 BeginSendToBeginSendToAll 方法与服务器宿主中的其他连接进行通信,并可以使用 GetConnectionById 方法获取 ConnectionId。每个连接都知道宿主、加密、压缩类型,并且可以发送、接收和断开自身与其他部分的连接。该接口在 ISocketService 接口中使用,允许用户与套接字连接进行交互。

在库实现内部,所有连接接口都是使用基础连接实现创建的:BaseSocketConnectionClientSocketConnectionServerSocketConnection

套接字服务

ISocketService 描述了连接事件。这些事件由宿主触发,并带有一个 ConnectionEventArgs 参数,该参数包含一个标识连接的 ISocketConnection。在 OnReceivedOnSent 事件中,会传递一个 MessageEventArgs,其中包含已发送或接收的字节数组。在 OnDisconnected 事件中,会传递一个 DisconnectedEventArgsException 属性指示断开连接是否由异常引起。

以下是 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 定义了套接字发送和接收缓冲区的大小。SocketServiceISocketService 的实例,用于驱动连接之间的消息交换。

加密和压缩

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

加密的数据缓冲区用服务头部和数据缓冲区长度进行标记,形成一个数据包缓冲区。这个数据包缓冲区由 MessageBuffer 类控制,该类保存有关数据包缓冲区偏移量、长度、剩余字节和原始缓冲区的信息。

排队请求

每次在 ISocketService 中调用 BeginReceiveBeginSend 时,宿主都会检查是否已启动某个请求。如果请求正在处理中,宿主会排队该请求。如果未启动,则触发该请求。

发送请求

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 类继续读取。如果消息长度大于数据包消息的长度,在读取一些数据之前,它会简单地将数据包缓冲区的大小调整为消息大小,从而确保有足够的空间供更多读取的字节。

检查空闲连接

使用 ISocketConnectionBeginSendBeginReceive 方法不返回任何 IAsyncResult 来知道方法是否已完成,从而允许在某个超时值后断开连接。为了防止这种情况,BaseSocketConnectionHost 有一个 System.Threading.Timer,它会定期检查 BaseSocketConnectionLastAction 属性。如果 LastAction 大于空闲超时值,则连接将关闭。

加密服务

ICryptoService 描述了在连接到对方时触发的身份验证方法。当使用 EncryptType.etRijndaelEncryptType.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 结构在客户端和服务器之间交换会话密钥。SessionKeySessionIV 属性分别是算法的对称密钥和初始化向量。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 和签名公钥进行填充。为了签名消息,会使用 SourceKeySessionKeysignMessage,并将生成的哈希分配给 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 是准确的,使用 SourceKeySessionKeysignMessage

连接创建者

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

SocketServer 和 SocketListener

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

SocketServer 构造函数和方法

SocketServer 构造函数中,socketService 参数定义了服务器使用的 ISocketService 实例。头部参数定义了在消息头部交换中使用的字节数组。socketBufferSize 调整套接字缓冲区大小。messageBufferSize 定义了服务的最大消息大小。idleCheckInterval 指示空闲连接检查的间隔(以毫秒为单位)。idleTimeoutValue 定义了超时值(以毫秒为单位),用于与每个连接的 LastAction 属性进行比较。

要将 SocketListener 项添加到 SocketServer 中,必须使用 AddListener 方法。localEndPoint 参数定义了用于监听连接的本地套接字 IP 端点。encryptTypecompressionType 分别定义了在新接受的连接中使用的加密和压缩方法。cryptoService 定义了用于对所选加密方法进行身份验证的 ICryptoServicebackLog 将操作系统的套接字监听队列限制为定义的数量,而 acceptThreads 设置了套接字 BeginAccept 的调用次数以提高接受性能。

HostThreadPool

此库使用异步套接字通信,而这又使用了 .NET 的 ThreadPool。在 .NET 2.0 ThreadPool 中,可以使用 SetMaxThreadsSetMinThreads 方法控制线程数,我认为这个类有很多改进。但是,如果您不想使用 .NET 类,可以使用一个托管线程池,称为 HostThreadPool,它非常类似于 Stephen Toub 的 ManagedThreadPoolHostThreadPool 使用一个托管线程列表,该列表随着提供的排队任务的增多而不断增加。要使用这个类而不是 SocketServer 中的 .NET ThreadPool,只需将构造函数参数 minThreadsmaxThreads 设置为非零值即可。

以下是一些使用 SocketServerSocketListener 的示例

//----- 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

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

SocketClient 构造函数和方法

SocketClient 构造函数与 SocketServer 类的参数签名相同。要将 SocketConnector 项添加到 SocketClient 中,必须使用 AddConnector 方法。remoteEndPoint 参数定义了用于连接的远程套接字 IP 端点。encryptTypecompressionType 分别定义了在新连接中使用的加密和压缩方法。cryptoService 定义了用于对所选加密方法进行身份验证的 ICryptoServicereconnectAttemptsreconnectAttemptInterval 分别定义了在使用 BeginReconnect 方法时重新连接的尝试次数以及重新连接的时间间隔。localEndPoint 定义了用于启动与远程端点连接过程的本地套接字 IP 端点。

以下是一些使用 SocketClientSocketConnector 的示例

//----- 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 服务** 宿主和客户端,所有这些都使用相同的 EchoSocketServiceEchoCryptService。演示按照其类型进行了划分,如下所示:

宿主

  • 控制台
    1. EchoConsoleClient
    2. EchoConsoleServer
  • Windows Forms
    1. EchoFormClient
    2. EchoFormServer
    3. Echo<code>Form (窗体模板)
  • Windows 服务
    1. EchoWindowsServiceServer

服务

  • EchoSocketService
  • EchoCryptService

结论

这里有很多内容,我认为这个库可以帮助任何想要编写具有加密和压缩功能的异步套接字的人。欢迎任何评论。

历史

  • 2006年5月15日: 初始版本
  • 2006年5月19日: 一些英文文本修正(抱歉,我还在学习!),并重新检查了演示源代码
  • 2006年6月6日: 版本 1.2 包含以下更改:
    • 修复了次要 bug
    • 所有“Sended”已改为“Sent”(感谢 vmihalj)
    • ReadCanEnqueue 现在可以正确地与 HostThreadPool 一起工作(感谢 PunCha)
    • 为客户端连接添加了 reconnectAttemptsreconnectAttemptInterval,以允许客户端连接在指定的时间间隔内进行任意次数的重连(感谢 Tobias Hertkorn)
  • 2007年4月1日: 版本 1.3 包含以下更改:
    • 修复了 rawbuffer = null 的问题
    • 修复了 BeginAcceptCallback:发生异常时停止接受
    • 修复了 BeginSendCallback:应使用 PacketRemaining 字节
    • 在演示中添加了套接字配置部分
    • 新的消息大小(64K)
    • 移除了 HosThreadPool
    • 头部更改为 Delimiter 属性,并新增了分隔符选项
      1. dtNone:无消息分隔符
      2. dtPacketHeader:与 1.2 版本兼容
      3. dtMessageTailExcludeOnReceive:在消息末尾使用自定义分隔符(接收时排除分隔符)
      4. dtMessageTailIncludeOnReceive:在消息末尾使用自定义分隔符(接收时包含分隔符)
    • 新的连接对象属性/方法
      1. Nagle、Linger 和 TTL 算法选项
      2. 宿主和创建者
    • 在服务类中加密签名消息
    • 服务类中的异常事件
    • 新的创建者名称属性
  • 2007年7月22日: 版本 1.4 包含以下更改:
    • 连接初始化过程在同一线程中执行(不在 ThreadPool 中排队)
    • 连接断开现在会检查 Windows 版本并执行正确的断开过程
    • 连接活动检查 Disposed
    • 修复了 CheckSocketConnections 的 disposed 检查
    • 包含了 CryptUtilsFlush() 方法
    • 修复了客户端连接 BeginConnect() 的异常
    • 修复了服务器连接 BeginSendToAll 的数组缓冲区
    • 新增 SocketClientSync 类用于同步使用(包含 WinForms 演示)
  • 2007年9月5日: 版本 1.5 包含以下更改:
    • SocketClient 支持代理身份验证(SOCKS5、Basic HTTP)
    • 修复了 BeginRead 错误(messagetail
    • 更改了 BeginDisconnectthreadpool
    • 审查了 BeginSendToAll(disposed 检查)
    • 新增 OnSSLClientValidateServerCertificate 事件,用于验证服务器证书
    • 将空闲检查间隔设置为 0,仅在大于 0 时创建
    • 使用 Buffer.BlockCopy 替代 Array.Copy
    • 新的聊天演示
© . All rights reserved.