(原创翻译文章·转载请注明来源:http://blog.csdn.net/hulihui/archive/2008/11/05/3230503.aspx)
前言 |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
2000年以来,我一直使用Delphi5.0和一些第三方库(Synapse)做Socket相关的工作。我的第一个Socket应用系统仅仅是在多个客户端和一台服务器之间复制文件。客户端软件检查文件夹中的文件否存在,并请求网络中的拷贝文件服务器,拷贝文件后,在数据库记录中做个标记表明一个文件被移动过。服务器侦听客户端连接请求,双方交换表明每个文件拷贝状态的XML消息。Synapse是一个阻塞式Socket第三方包,我需要一个类似HTTP服务器的线程池机制,因为我不能保持连接一直是打开的(每个连接一个线程) 。我的解决办法是使用某个IOCP(IO Complete Port,完成端口——译者注)功能,在线程池中缓存客户端请求(代码)并在消息交换后关闭连接。
现在,我决定用C#写一个Socket服务器和客户端程序库。然后,我只需要考虑消息交换(过程),让.NET完成困难的任务。为此,我需要以下一些特点:
Socket连接(Socket Connection) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
ISocketConnection是Socket连接的基接口,描述了所有的连接属性和方法:ConnectionID属性是一个GUID字符串,定义了唯一连接ID;CustomData属性定义了一个与连接关联的自定义对象;Header属性是每个消息使用的Socket服务消息头,封装在数据包消息中,只有包含指定头的消息才能被接受;LocalEndPoint与RemoteEndPoint是连接中Socket IP端点(end points)对象;SocketHandle是底层操作系统给出的Socket句柄(handle)。
IClientSocketConnection与IServerSocketConnection继承自ISocketConnection,各有一些特殊功能。IClientSocketConnection使用BeginReconnect方法重连服务器,IServerSocketConnection应用BeginSendTo和BeginSendToAll方法使服务器主机与其它连接通信,并可使用GetConnectionById方法获得ConnectionId。每个连接都知道主机、加密算法和压缩方式,可以发送、接收和断开自己与对方的连接。ISocketService接口中用到了这个接口,允许用户与Socket连接交互。
在程序库的内部实现中,所有的连接接口都使用基连接实现来创建:BaseSocketConnection、ClientSocketConnection与 ServerSocketConnection。
Socket服务(Socket Service) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
ISocketService描述了连接事件,它们由主机激发并有一个ConnectionEventArgs参数,该参数包含有标识连接的ISocketConnection。在OnReceived和OnSent事件中传递的MessageEventArgs参数,它包含已经发送和接收的字节数组。在OnDisconnected事件中传递DisconnectedEventArgs参数,该参数的异常属性指出连接断开是否由异常引发的。
下面是一个ISocketService实现的代码举例:
public class SimpleEchoService: ISocketService { public void OnConnected(ConnectionEventArgs e) { // -----检查主机! if (e.Connection.HostType == HostType.htServer) { // ----- 开始异步接收! e.Connection.BeginReceive() } else { // ----- 开始异步发送自定义消息! byte [] b = GetMessage(e.Connection.SocketHandle.ToInt32()); e.Connection.BeginSend(b); } } public void OnSent(MessageEventArgs e) { // -----检查主机。 在这种情况下,双方都开始接收! if (e.Connection.HostType == HostType.htServer) { // ----- 开始异步接收! e.Connection.BeginReceive(); } else { // ----- 开始异步接收! e.Connection.BeginReceive(); } } public override void OnReceived(MessageEventArgs e) { // -----检查主机! if (e.Connection.HostType == HostType.htServer) { // -----如果是服务器,发送接收到的缓冲区数据! byte [] b = e.Buffer; e.Connection.BeginSend(b); } else { // -----如果是客户端,生成另一个定制消息并发送! byte [] b = GetMessage(e.Connection.SocketHandle.ToInt32()); e.Connection.BeginSend(b); } } public override void OnDisconnected(DisconnectedEventArgs e) { // -----检查主机! if (e.Connection.HostType == HostType.htServer) { // -----没有! } else { // -----重连服务器! e.Connection.AsClientConnection().BeginReconnect(); } } }
ISocketService可以在同一个主机程序集中实现,或主机引用的其它程序集中实现。这允许用户从Socket服务中分离出主机实现,帮助服务器或域中的管理工作。
连接主机(Connection Host) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
创建ISocketService的同时,需要宿主服务和服务连接。服务器和客户端主机都具有相同的父类:BaseSocketConnectionHost,这个类保持了一个连接链表,具有功能:加密和压缩缓冲区数据,加入请求服务到队列,确保所有的缓冲区数据被发送和接收,检查消息头,检查空闲连接。CheckTimeoutTimer按时间间隔IdleCheckInterval定期检查连接是否空闲,IdleTimeOutValue表示空闲超时;Header是主机使用的Socket服务消息头;HostType表明主机是服务器或客户端;SocketBufferSize定义了Socket发送和接收缓冲区的大小;SocketService是驱动连接间消息交换的ISocketService实例。
加密和压缩(Encrypt and Compress) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
每次发送和接收消息时,主机检查数据是否须要加密与/或压缩,该项工作由CryptUtils静态类完成。CreateSymmetricAlgoritm创建一个加密类型为encryptType的ISymmetricAlgoritm;DecryptData和DecryptDataForAuthenticate分别用于解密收到的消息并在认证过程中检查Hash签名;EncryptData和EncryptDataForAuthenticate分别用于加密发送数据和给认证消息签名。
给加密的缓冲区数据标记服务消息头和数据缓冲区长度后,就构成了一个数据包,它由MessageBuffer类控制,这个类包含有一些信息如:数据包缓冲区偏移、长度、剩余字节以及原生缓冲区(raw buffer)。
请求入队(Enqueuing requests) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
在BeginSend方法中使用了下面的入队操作:
internal void BeginSend(BaseSocketConnection connection, byte [] buffer) { ... // -----检查队列! lock (connection.WriteQueue) { if (connection.WriteQueueHasItems) { // -----如果连接正在发送,消息入队! connection.WriteQueue.Enqueue(writeMessage); } else { // -----如果连接不发送,发送消息! connection.WriteQueueHasItems = true; ...
在消息发送后的发送回调方法中,如果需要,主机将再次检查队列并初始化另一个发送进程:
private void BeginSendCallback(IAsyncResult ar) { ... // -----检查队列! lock (connection.WriteQueue) { if (connection.WriteQueue.Count > 0 ) { // -----如果有项,发送它! MessageBuffer dequeueWriteMessage = connection.WriteQueue.Dequeue(); ... } else { connection.WriteQueueHasItems = false; } } ...
同样的技术也适用于接收方法:如果接收方法是活动的,所有的BeginReceive调用均入队。如果没有接收过程被初始化,主机开始接收:
internal void BeginReceive(BaseSocketConnection connection) { ... // -----检查队列! lock (connection.SyncReadCount) { if (connection.ReadCanEnqueue) { if (connection.ReadCount == 0) { // -----如果连接不在接收,开始接收! MessageBuffer readMessage = new MessageBuffer(FSocketBufferSize); ... } // -----增加 read count! connection.ReadCount++; } } ...
之后,当消息收到并在接收回调方法中解析后,主机再次检查读队列,如果需要,初始化另一个接收过程:
private void BeginReadCallback(IAsyncResult ar) { ... // -----检查队列! lock (connection.SyncReadCount) { connection.ReadCount--; if (connection.ReadCount > 0) { // -----如果读队列有项,开始接收! ... } } ...
确保发送和接收(Ensure send and receive) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
private void BeginSendCallback(IAsyncResult ar) { ... byte [] sent = null; int writeBytes = .EndSend(ar); if (writeBytes < writeMessage.PacketBuffer.Length) { // -----继续发送,直到所有字节被发送! 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) { ( ... // -----有字节! ... // -----处理接收到的数据! readMessage.PacketOffSet += readBytes; ... if (readSocket) { // -----继续读! .BeginReceive(readMessage.PacketBuffer, readMessage.PacketOffSet, readMessage.PacketRemaining, SocketFlags.None, ...); } } ...
检查消息头(Check message header) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
第一个标签部分是Socket服务消息头,它是任意长度的字节数组。需要注意:如果选择一个非常小的消息头,可能在其它地方有一个同样的字节数组,那么主机将遗弃该序列;如果选择一个很长的字节数组,主机需要消耗一些处理器时间来验证消息头是否与当前Socket服务一致。第二部分是数据包长度,它由原生数据缓冲区长度、加密与/或压缩数据长度以及消息头长度计算得到。
正如前面提到的,每次发送消息,主机检查数据是否必须加密和/或压缩,并且如果选择使用某种消息头,那么原生数据缓冲区将由MessageBuffer类控制,该类由GetPacketMessage静态方法创建:
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) { // -----需要消息头! int headerSize = connection.Header.Length + 2; byte [] result = new byte [workBuffer.Length + headerSize]; int messageLength = result.Length; // -----消息头! for (int i = 0; i < connection.Header.Length; i++) { result[i] = connection.Header[i]; } // -----长度! 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 { // -----无消息头! return new MessageBuffer(ref buffer, ref workBuffer); } }
如果使用了某个Socket服务消息头,接收过程中需要检查消息头,并继续读取字节直到所有数据包消息接收完毕,这个过程是在读回调方法中执行的:
private void BeginReadCallback(IAsyncResult ar) { ... byte [] received = null byte [] rawBuffer = null; byte [] connectionHeader = connection.Header; readMessage.PacketOffSet += readBytes; if ((connectionHeader != null) && (connectionHeader.Length > 0)) { // -----有消息头! int headerSize = connectionHeader.Length + 2; bool readPacket = false; bool readSocket = false; do { connection.LastAction = DateTime.Now; if (readMessage.PacketOffSet > headerSize) { // -----有消息头! for (int i = 0; i < connectionHeader.Length; i++) { if (connectionHeader[i] != readMessage.PacketBuffer[i]) { // ----- 消息头损坏! throw new BadHeaderException( "Message header is different from Host header." ); } } // ----- 获取长度! 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."); } // -----检查长度! if (messageLength == readMessage.PacketOffSet) { // -----相等,获取 rawBuffer! rawBuffer = readMessage.GetRawBuffer(messageLength, headerSize); readPacket = false; readSocket = false; } else { if (messageLength < readMessage.PacketOffSet) { // -----小于, 获取 rawBuffer 并激发事件! rawBuffer = readMessage.GetRawBuffer(messageLength, headerSize); // -----解密! 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) { // -----大于,读Socket! if (messageLength > readMessage.PacketLength) { readMessage.Resize(messageLength); } readPacket = false; readSocket = true; } } } } else { if (readMessage.PacketRemaining < headerSize) { // -----增加数据包空间! readMessage.Resize(readMessage.PacketLength + headerSize); } readPacket = false; readSocket = true; } } while (readPacket); if (readSocket) { // -----继续读! ... .BeginReceive(readMessage.PacketBuffer, readMessage.PacketOffSet, readMessage.PacketRemaining, SocketFlags.None, ...); ... } } else { // -----没有消息头! rawBuffer = readMessage.GetRawBuffer(readBytes, 0); } if (rawBuffer != null) { // -----解密! 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); ...
读回调方法首先检查连接是否有某个消息头,如果没有,仅仅获得原生缓冲区数据并继续。如果连接有某个消息头,该方法需要与Socket服务的消息头比较。之前,它会检查数据包消息长度是否大于连接消息头长度,以确保它能够解析整个消息长度。否则,它只会读取部分字节数据。检查消息头之后,该方法解析消息长度,并用数据包长度检查消息。如果长度相等,读得原生缓冲区数据并终止循环。如果消息长度小于数据包消息的长度,将附加一些数据到消息上。因此,该方法得到原生缓冲区数据,并继续使用同一个MessageBuffer类读数据。如果消息长度大于数据包消息长度,在读一些数据前,调整数据包缓冲区大小为消息的大小,确保足够读数据字节空间。
检查空闲连接(Checking idle connections) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
加密服务(Crypto Service) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
ICryptoService描述了连接对方时激发的认证方法。使用EncryptType.etRijndael或EncryptType.etTripleDES时激发OnSymmetricAuthenticate,使用EncryptType.etSSL时激发OnSSLXXXXAuthentication。类似ISocketService,ICryptService可以在同一个主机程序集中实现,或者在主机引用的另一个程序集中实现,这样就可以在许多ISocketService实现中共享一个ICryptoService实现。
SSL认证(SSL authentication) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
在客户端和服务器两端都要做SslStream认证,但各有不同的参数。在服务器端,需要使用X509Certificate2类传递一个证书,不论是使用X509Store在证书商店寻找到的证书,还是从一个认证文件(.cer)创建的证书。此外,也可以请求客户端认证,并检查证书撤销。下面是一个使用ICryptService做SSL服务器认证的代码举例:
public void OnSSLServerAuthenticate(out X509Certificate2 certificate, out bool clientAuthenticate, ref bool checkRevocation) { // -----设置服务器证书,客户端认证和证书撤销! 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"; /* //-----使用客户端证书! 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 post、以及这个网站。
对称认证(Symmetric authentication) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
public void OnSymmetricAuthenticate(HostType hostType, out RSACryptoServiceProvider serverKey) { /* * 需要一个RSACryptoServiceProvider用于加密和发送会话密钥。 * 在服务器端需要公钥和私匙解密会话密钥。 * 在客户端只需要公钥用以加密会话密钥。 * * 可以从一个字符串创建一个RSACryptoServiceProvider * (文件, 注册表), CspParameters或一个证书。 */ // -----用字符串! /* serverKey = new RSACryptoServiceProvider(); serverKey.FromXMLString("XML key string"); */ //----- Using CspParameters! CspParameters param = new CspParameters(); param.KeyContainerName = "ALAZ_ECHO_SERVICE"; serverKey = new RSACryptoServiceProvider(param); /* //-----使用证书商店! 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) { //-----在客户端只要公钥! serverKey = (RSACryptoServiceProvider)certificate.PublicKey.Key; } else { //-----在服务器,既需要公钥也需要私钥! serverKey.FromXmlString(certificate.PrivateKey.ToXmlString(true)); } store.Close(); */ }
对称认证使用AuthMessage结构在客户端和服务器之间交换会话密钥。SessionKey和SessionIV属性分别是对称密钥和算法初始化向量。Sign属性是Hash码,它由客户端使用内部创建的签名RSACryptoServiceProvider类产生,并且其公钥由属性SourceKey交换。为签名AuthMessage,这个内部签名密钥对是必要的,且服务器可以确保AuthMessage是正确的。这个过程由如下代码完成:
... // -----签名消息! private byte [] signMessage = new byte [] { <sign message array of bytes for authentication> }; ... protected virtual void InitializeConnection(BaseSocketConnection connection) { ... // -----对称! if (connection.EncryptType == EncryptType.etRijndael || connection.EncryptType == EncryptType.etTripleDES) { if (FHost.HostType == HostType.htClient) { // -----获得RSA提供者! RSACryptoServiceProvider serverPublicKey; RSACryptoServiceProvider clientPrivateKey = new RSACryptoServiceProvider(); FCryptoService.OnSymmetricAuthenticate(FHost.HostType, out serverPublicKey); // -----生成对称算法! SymmetricAlgorithm sa = CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType); sa.GenerateIV(); sa.GenerateKey(); // -----产生连接加密者! connection.Encryptor = sa.CreateEncryptor(); connection.Decryptor = sa.CreateDecryptor(); // -----创建认证结构! 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); // ----- 使用am.SourceKey签名消息、am.SessionKey和signMessage! // ----- 签名中要使用PaddingMode.PKCS7! 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()); // ----- 序列化认证消息! XmlSerializer xml = new XmlSerializer(typeof(AuthMessage)); m.SetLength(0); xml.Serialize(m, am); // ----- 发送结构! 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由加密的SessionKey、SessionIV以及签名公钥填充。为签名消息,使用了SourceKey、SessionKey以及signMessage,并把结果Hash值赋给Sign属性。
protected virtual void InitializeConnection(BaseSocketConnection connection) { ... if (FHost.HostType == HostType.htClient) { ... } else { // -----创建空认证结构! MessageBuffer mb = new MessageBuffer(8192); // -----开始接收结构! 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转化错误! } if ((message != null ) && (Encoding.Default.GetString(message).Contains("</AuthMessage>"))) { // -----获得RSA提供者! RSACryptoServiceProvider serverPrivateKey; RSACryptoServiceProvider clientPublicKey = new RSACryptoServiceProvider(); FCryptoService.OnSymmetricAuthenticate(FHost.HostType, out serverPrivateKey); // -----反序列化认证消息! 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); // -----生成对称算法! SymmetricAlgorithm sa = CryptUtils.CreateSymmetricAlgoritm(connection.EncryptType); sa.Key = serverPrivateKey.Decrypt(am.SessionKey, false); sa.IV = serverPrivateKey.Decrypt(am.SessionIV, false); // ----- 产生连接加密者! connection.Encryptor = sa.CreateEncryptor(); connection.Decryptor = sa.CreateDecryptor(); // -----验证签名! 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接收Socket缓冲区数据。读回调方法连续读数据直到AuthMessage全被收到。该方法使用这个消息并调用OnSymmetricAuthenticate获得RSACryptoServiceProvider,并用它解密SessionKey、SessionIV以及签名公钥。所有钥匙解密后,该方法使用SourceKey、SessionKey和signMessage验证Sign属性,确保AuthMessage是正确的。
连接创建者(Connection Creator) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
虽然BaseSocketConnectionHost可以管理ISocketConnection连接,但不能创建连接。此项工作由BaseSocketConnectionCreator完成,由它创建和初始化ISocketConnections。CompressionType和EncryptType属性分别定义了连接中的压缩和加密类型。如果需要,CryptoService定义了ICrytoService实例用以初始化连接。Host属性是BaseSocketConnectionCreator 主机,它既可以是服务器也可以是客户端。LocalEndPoint定义了连接中的Socket IP端点,它可以有不同的行为,这取决于创建者的类型。
Socket服务器与Socket侦听者(SocketServer and SocketListener) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
SocketServer与SocketListener是创建Socket服务器的类。SocketServer继承自BaseSocketConnectionHost,管理ISocketConnections。SocketListener继承自BaseSocketConnectionCreator,侦听传入连接,接受连接,并创建一个新的可用ISocketConnection。如果需要,一个SocketServer可以附带多个SocketListener,每个分配一个本地侦听端口。
Socket服务器构造函数和方法(SocketServer constructor and methods) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
在SocketServer构造函数中:socketService是服务器使用的ISocketService实例;Header是消息头交换用的字节数组;socketBufferSize确定Socket缓冲区大小;messageBufferSize确定服务的最大消息大小;idleCheckInterval定义空闲连接检查的时间间隔(毫秒);idleTimeoutValue定义了与连接的LastAction属性比较时的超时值(毫秒)。
如果要在SocketServer中新增SocketListener项,就使用AddListener方法。localEndPoint定义了用于侦听连接的本地Socket IP端点;encryptType与compressionType分别是新建连接的加密和压缩方式;cryptoService定义了用于认证所选的加密方法的ICryptoService;backLog在操作系统层限制Socket侦听队列长度;acceptThreads记录Socket的BeginAccept方法调用计数,它用于提高接收性能。
本文的程序库中使用异步通信Socket,自然要使用.NET的ThreadPool(线程池)。在.NET 2.0上,ThreadPool可以使用SetMaxThreads和SetMinThreads方法控制线程数量,我认为这个类有较多的改善余地。如果不想使用.NET类,可以使用托管线程池HostThreadPool,它与Stephen的Toub's ManagedThreadPool非常相似。HostThreadPool使用托管线程链表保存不断增加的入队请求。如果SocketServer使用这个类而不是.NET的ThreadPool,只需设置它的构造函数参数minThreads和maxThreads为非零数即可。
下面是使用SocketServer与SocketListener代码举例:
// 简单服务器! SocketServer server = new SocketServer(new SimpleEchoService()); // -----简单的侦听者! server.AddListener(new IPEndPoint(IPAddress.Any, 8087)); server.Start(); // -----有消息头的服务器! SocketServer server = new SocketServer(new SimpleEchoService(), new byte [] { 0xFF, 0xFE, 0xFD }); // -----有简单加密的侦听者! server.AddListener(new IPEndPoint(IPAddress.Any, 8087), EncryptType.etBase64, CompressionType.ctNone, null); server.Start(); // -----有消息头和缓冲区大小的服务器, // -----没有设置hostthreadpool和空闲检查! SocketServer server = new SocketServer(new SimpleEchoService(), new byte [] { 0xFF, 0xFE, 0xFD }, 2048, 8192, 0, 0, 60000, 30000); // -----多于一个、有不同端口号的侦听者! 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();
Socket客户端与Socket连接者(SocketClient and SocketConnector) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
SocketClient与SocketConnector用于创建Socket客户端。类似SocketServer,SocketClient继承自BaseSocketConnectionHost ,管理ISocketConnections。SocketConnector继承自BaseSocketConnectionCreator,它连接Socket服务器以及创建一个新的可用ISocketConnection。如果需要,一个SocketClient可以附带多个SocketConnector,每个连接到一个Socket服务器,它们可以分配一个本地地址和本地端口来启动连接。
Socket客户端构造函数和方法(SocketClient constructor and methods) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
SocketClient构造函数与SocketServer类有相同的参数签名。若要在SocketClient中增加SocketConnector项,必须使用方法AddConnector。remoteEndPoint定义了用于连接的远程Socket IP端点;encryptType和compressionType分别是新连接中的加密和压缩方法;cryptoService定义了用来认证所选加密方法的ICryptoService;reconnectAttempts和reconnectAttemptInterval分别是BeginReconnect方法的重连次数和重连时间间隔;localEndPoint是启动处理远程连接的本地Socket IP端点。
下面是使用SocketClient和SocketConnector的代码举例:
// -----简单客户端! SocketClient client = new SocketClient(new SimpleEchoService()); // -----简单连接者! client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087)); client.Start(); // -----有消息头的客户端! SocketClient client = new SocketClient(new SimpleEchoService(), new byte [] { 0xFF, 0xFE, 0xFD }); // -----有简单加密的连接者! client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087), EncryptType.etBase64, CompressionType.ctNone, null); client.Start(); // -----有消息头和缓冲区大小的客户端 // -----没有使用hostthreadpool和设置闲置检查! SocketClient client = new SocketClient(new SimpleEchoService(), new byte [] { 0xFF, 0xFE, 0xFD }, 2048, 8192, 0, 0, 60000, 30000); // -----带加密和重连的连接者! client.AddConnector(new IPEndPoint(IPAddress.Parse("10.10.1.1"), 8087), EncryptType.etSSL, CompressionType.ctGZIP, new SimpleEchoCryptService(), 5, 30000); client.Start(); // -----有消息头和缓冲区大小的客户端 // -----使用hostthreadpool和设置闲置检查! SocketClient client = new SocketClient(new SimpleEchoService(), new byte [] { 0xFF, 0xFE, 0xFD }, 4096, 8192, 5, 50, 60000, 30000); // -----有加密、重新和本地端点的连接者! 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(); // -----简单客户端! SocketClient client = new SocketClient( new SimpleEchoService()); // 一个以上的连接器,每个对应不同的远程Socket服务器! 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 Demo Project) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
下载文件有一个应答演示项目,它使用控制台、窗体和Windows服务充当主机和客户端,它们都使用相同的EchoSocketService和EchoCryptService。演示程序分为如下几类:
主机(Hosts) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
服务(Services) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
结语(Conclusion) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
到这,已经写了很许内容了。我想这个程序库可以帮助那些希望在异步Socket中使用加密和压缩的读者,欢迎任何评论。
版本历史(History) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |