译文:异步Socket服务器与客户端(An Asynchronous Socket Server and Client) (转)
http://blog.csdn.net/hulihui/article/details/3230503#sa
(原创翻译文章·转载请注明来源:http://blog.csdn.net/hulihui)
目录
- 前言
- Socket连接(Socket Connection)
- Socket服务(Socket Service)
- 连接主机(Connection Host)
- 加密与压缩(Encrypt与Compress)
- 请求入队(Enqueuing Requests)
- 确保发送和接收(Ensure send and recieve)
- 检查消息头(Check message header)
- 检查空闲连接(Checking idle connections)
- 加密服务
- 连接创建者(Connection Creator)
- Socket服务器与Socket侦听者(SocketServer and SocketListener)
- Socket客户端与Socket连接者(SocketClient and SocketConnector)
- 应答演示项目(Echo Demo Project)
- 结语(Conclusion)
- 版本历史(History)
前言 |
[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连接(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] |
ISocketService每次调用BeginReceive或BeginSend,主机检查是否某个请求被初始化。如果一个请求正在处理,主机就入队该请求;如果没有,它就激发该请求。
发送请求
在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] |
为了确保所有的缓冲区数据被发送,BaseSocketConnectionHost检查发送的字节数,比较其MessageBuffer类,继续发送剩余字节直到缓冲区数据都被发送:
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服务消息头,它是任意长度的字节数组。需要注意:如果选择一个非常小的消息头,可能在其它地方有一个同样的字节数组,那么主 机将遗弃该序列;如果选择一个很长的字节数组,主机需要消耗一些处理器时间来验证消息头是否与当前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] |
使用ISocketConnection的BeginSend和BeginReceive方法不会返回某个IAsyncResult以确定该方法完成与 否,或在某个超时值后仍然不允许断开连接。为了防止这一点,BaseSocketConnectionHost有一个 System.Threading.Timer对象,它定期检查BaseSocketConnection的LastAction属性。如果 LastAction大于闲置超时值,就关闭连接。
加密服务(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] |
.NET 2.0有一个新的SslStream流类型认证SSL流。SslStream的构造函数接受一个NetworkStream类,该类由Socket类创建。因此,使用SslStream可以在Socket连接中发送和接收缓冲区数据。
服务器验证
在客户端和服务器两端都要做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] |
为了在本文的程序库中实现某个对称加密和认证,我决定提交一个微软新闻组的新闻列表。 该新闻列表没有受到重视,但因知识分享而幸运(非常感谢Joe Kaplan、Dominick Baier以及Valery Pryamikov),我决定用William Stacey的实现样例“使用交换的会话密钥发送安全消息的通用方法”。在其代码中,会话中的对称密钥使用RSA密钥对加密和签名,客户端需要知道加密服 务器的公钥,但该密钥不会在认证过程中从服务器收到,客户端和服务器都需要通过人为方式知道这个公钥。为了确保这一 点,OnSymmetricAuthenticate需要一个RSACryptoServiceProvider类提供一个密钥对。可以从XML字符串中 填充RSACryptoServiceProvider,或从文件,或从CspParameters类,或从一个证书。下面是对称认证的代码举例:
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方法调用计数,它用于提高接收性能。
主机线程池(HostThreadPool)
本文的程序库中使用异步通信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] |
- 控制台(Console)
- EchoConsoleClient
- EchoConsoleServer
- Windows窗体
- EchoFormClient
- EchoFormServer
- Echo<code>Form (Forms template)
- Windows服务
- EchoWindowsServiceServer
服务(Services) |
[H]、 [0]、[1]、[2]、 [3]、[4]、[5]、 [6]、[7]、[8]、 [9]、[10] |
- EchoSocketService
- EchoCryptService
结语(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] |
- May 15, 2006:初版
- May 19, 2006:更正一些英文文本(抱歉,我还在学习!),复查演示源码
- June 06, 2006:版本1.2有如下更改:
- 更正了一些小错误
- 所有的“Sended”改为“Sent”(感谢vmihalj)
- 现在,使用HostThreadPool的ReadCanEnqueue正常运行(感谢PunCha)
- 增加reconnectAttempts和reconnectAttemptInterval,允许客户端在时间间隔内重连多次(感谢Tobias Hertkorn)
- April 01, 2007:版本1.3有如下更改:
- 更正了rawbuffer = null
- 更正了BeginAcceptCallback:发生异常停止接收连接侦听
- 更正了BeginSendCallback :应使用PacketRemaining字节数组
- 在演示程序中增加了Socket配置节(config section)
- 新消息大小(64K)
- 删除HosThreadPool
- 消息头改为有新界限符选项的Delimiter属性:
- dtNone:没有信息界限符
- dtPacketHeader:兼容1.2版
- dtMessageTailExcludeOnReceive:在消息末尾使用定制界限符(接收时剔除界限符)
- dtMessageTailIncludeOnReceive:在消息末尾使用定制界限符(接收时包含界限符)
- 新的连接对象属性/方法:
- Nagle、Linger与TTL算法选项
- 主机与创建者
- 服务类中加密签名消息
- 服务类中的异常事件
- 新的创建者名字属性
- July 22, 2007:版本1.4有如下改动:
- 在同一线程中执行连接初始化过程(不是ThreadPool中的队列)
- 现在,断开连接时检查Windows版本和执行正确的断开程序
- 对Disposed检查连接有效性
- 更正了CheckSocketConnections的disposed检查
- 包括了CryptUtils Flush()方法
- 更正了客户端连接的BeginConnect()异常
- 更正了服务器连接的BeginSendToAll数组缓冲区
- 新的异步SocketClientSync类(包括在WinForms演示中)
- September 5, 2007:版本1.5有如下改动:
- 具有代理认证的SocketClient(SOCKS5、基本HTTP)
- 更正了BeginRead bug(消息尾)
- 修改了BeginDisconnect(threadpool)
- 审查了BeginSendToAll(disposed检查)
- 新的OnSSLClientValidateServerCertificate事件中验证服务器证书
- 空闲检查间隔设定为0,只有大于0时创建
- 使用Buffer.BlockCopy代替Array.Copy
- 新的聊天演示程序
附注:译者添加了文章的目录和导航,第二次翻译文章,感觉比第一次好点,欢迎指正。