我正在尝试创建一个既可以接受安全SSL又可以接受不安全的纯文本连接的服务器(为了向后兼容)。我的代码几乎正常工作,除了从不安全客户端收到的第一个传输数据丢失了服务器上的前5个字节(字符)。更具体地说,如果我在不安全的连接上传输30个字节,当服务器到达OnClientDataReceived()
函数时,行“int iRx = nwStream.EndRead(asyn);
”,然后iRx = 25
。从客户端发送的任何后续消息都包含所有发送的字节/字符。我怀疑连接的初始假设是SSLStream
可能正在剥离前5个字节,然后当它失败时,这5个字节已经从缓冲区中提取出来并且不再可用。有没有人知道我可以采取另一种方法来编写代码,以便服务器能够自动切换?
我试图避免做以下事情:
NetworkStream
进行连接,然后请求升级到SSL流TcpListeners
(一个用于安全,一个用于不安全)/// Each client that connects gets an instance of the ConnectedClient class.
Class Pseudo_ConnectedClient
{
//Properties
byte[] Buffer; //Holds temporary buffer of read bytes from BeginRead()
TcpClient TCPClient; //Reference to the connected client
Socket ClientSocket; //The outer Socket Reference of the connected client
StringBuilder CurrentMessage; //concatenated chunks of data in buffer until we have a complete message (ends with <ETX>
Stream Stream; //SSLStream or NetworkStream depending on client
ArrayList MessageQueue; //Array of complete messages received from client that need to be processed
}
/// When a new client connects (OnClientConnection callback is executed), the server creates the ConnectedClient object and stores its
/// reference in a local dictionary, then configures the callbacks for incoming data (WaitForClientData)
void OnClientConnection(IAsyncResult result)
{
TcpListener listener = result.AsyncState as TcpListener;
TcpClient clnt = null;
try
{
if (!IsRunning) //then stop was called, so don't call EndAcceptTcpClient because it will throw and ObjectDisposedException
return;
//Start accepting the next connection...
listener.BeginAcceptTcpClient(this.onClientConnection, listener);
//Get reference to client and set flag to indicate connection accepted.
clnt = listener.EndAcceptTcpClient(result);
//Add the reference to our ArrayList of Connected Clients
ConnectedClient conClnt = new ConnectedClient(clnt);
_clientList.Add(conClnt);
//Configure client to listen for incoming data
WaitForClientData(conClnt);
}
catch (Exception ex)
{
Trace.WriteLine("Server:OnClientConnection: Exception - " + ex.ToString());
}
}
/// WaitForClientData registers the AsyncCallback to handle incoming data from a client (OnClientDataReceieved).
/// If a certificate has been provided, then it listens for clients to connect on an SSLStream and configures the
/// BeginAuthenticateAsServer callback. If no certificate is provided, then it only sets up a NetworkStream
/// and prepares for the BeginRead callback.
private void WaitForClientData(ConnectedClient clnt)
{
if (!IsRunning) return; //Then stop was called, so don't do anything
SslStream sslStream = null;
try
{
if (_pfnClientDataCallBack == null) //then define the call back function to invoke when data is received from a connected client
_pfnClientDataCallBack = new AsyncCallback(OnClientDataReceived);
NetworkStream nwStream = clnt.TCPClient.GetStream();
//Check if we can establish a secure connection
if (this.SSLCertificate != null) //Then we have the ability to make an SSL connection (SSLCertificate is a X509Certificate2 object)
{
if (this.certValidationCallback != null)
sslStream = new SslStream(nwStream, true, this.certValidationCallback);
else
sslStream = new SslStream(nwStream, true);
clnt.Stream = sslStream;
//Start Listening for incoming (secure) data
sslStream.BeginAuthenticateAsServer(this.SSLCertificate, false, SslProtocols.Default, false, onAuthenticateAsServer, clnt);
}
else //No certificate available to make a secure connection, so use insecure (unless not allowed)
{
if (this.RequireSecureConnection == false) //Then we can try to read from the insecure stream
{
clnt.Stream = nwStream;
//Start Listening for incoming (unsecure) data
nwStream.BeginRead(clnt.Buffer, 0, clnt.Buffer.Length, _pfnClientDataCallBack, clnt);
}
else //we can't do anything - report config problem
{
throw new InvalidOperationException("A PFX certificate is not loaded and the server is configured to require a secure connection");
}
}
}
catch (Exception ex)
{
DisconnectClient(clnt);
}
}
/// OnAuthenticateAsServer first checks if the stream is authenticated, if it isn't it gets the TCPClient's reference
/// to the outer NetworkStream (client.TCPClient.GetStream()) - the insecure stream and calls the BeginRead on that.
/// If the stream is authenticated, then it keeps the reference to the SSLStream and calls BeginRead on it.
private void OnAuthenticateAsServer(IAsyncResult result)
{
ConnectedClient clnt = null;
SslStream sslStream = null;
if (this.IsRunning == false) return;
try
{
clnt = result.AsyncState as ConnectedClient;
sslStream = clnt.Stream as SslStream;
if (sslStream.IsAuthenticated)
sslStream.EndAuthenticateAsServer(result);
else //Try and switch to an insecure connections
{
if (this.RequireSecureConnection == false) //Then we are allowed to accept insecure connections
{
if (clnt.TCPClient.Connected)
clnt.Stream = clnt.TCPClient.GetStream();
}
else //Insecure connections are not allowed, close the connection
{
DisconnectClient(clnt);
}
}
}
catch (Exception ex)
{
DisconnectClient(clnt);
}
if( clnt.Stream != null) //Then we have a stream to read, start Async read
clnt.Stream.BeginRead(clnt.Buffer, 0, clnt.Buffer.Length, _pfnClientDataCallBack, clnt);
}
/// OnClientDataReceived callback is triggered by the BeginRead async when data is available from a client.
/// It determines if the stream (as assigned by OnAuthenticateAsServer) is an SSLStream or a NetworkStream
/// and then reads the data out of the stream accordingly. The logic to parse and process the message has
/// been removed because it isn't relevant to the question.
private void OnClientDataReceived(IAsyncResult asyn)
{
try
{
ConnectedClient connectClnt = asyn.AsyncState as ConnectedClient;
if (!connectClnt.TCPClient.Connected) //Then the client is no longer connected >> clean up
{
DisconnectClient(connectClnt);
return;
}
Stream nwStream = null;
if( connectClnt.Stream is SslStream) //Then this client is connected via a secure stream
nwStream = connectClnt.Stream as SslStream;
else //this is a plain text stream
nwStream = connectClnt.Stream as NetworkStream;
// Complete the BeginReceive() asynchronous call by EndReceive() method which
// will return the number of characters written to the stream by the client
int iRx = nwStream.EndRead(asyn); //Returns the numbers of bytes in the read buffer
char[] chars = new char[iRx];
// Extract the characters as a buffer and create a String
Decoder d = ASCIIEncoding.UTF8.GetDecoder();
d.GetChars(connectClnt.Buffer, 0, iRx, chars, 0);
//string data = ASCIIEncoding.ASCII.GetString(buff, 0, buff.Length);
string data = new string(chars);
if (iRx > 0) //Then there was data in the buffer
{
//Append the current packet with any additional data that was already received
connectClnt.CurrentMessage.Append(data);
//Do work here to check for a complete message
//Make sure two complete messages didn't get concatenated in one transmission (mobile devices)
//Add each message to the client's messageQueue
//Clear the currentMessage
//Any partial messsage at the end of the buffer needs to be added to the currentMessage
//Start reading again
nwStream.BeginRead(connectClnt.Buffer, 0, connectClnt.Buffer.Length, OnClientDataReceived, connectClnt);
}
else //zero-length packet received - Disconnecting socket
{
DisconnectClient(connectClnt);
}
}
catch (Exception ex)
{
return;
}
}
SSLStream
)并且从客户端建立了不安全的连接,则当从该客户端收到第一条消息时,代码会正确检测到{{1}未经过身份验证,并切换到SSLStream
的{{1}}。但是,如果在NetworkStream
上为第一条消息调用TCPClient
,则发送的消息中缺少前5个字符(字节),但仅针对第一条消息。只要EndRead
已连接,所有剩余的消息就完成了。如果客户端断开连接然后重新连接,则会剪切第一条消息,然后所有后续消息都会恢复正常。导致前5个字节被剪裁的原因是什么,我该如何避免呢?
我的项目目前正在使用.NET v3.5 ...我希望保留此版本,如果可以避免,我不会升级到4.0。
后续问题
Damien在下面的回答确实允许我保留那些丢失的5个字节,但是,我更愿意在代码中坚持NetworkStream
和TCPClient
方法以避免阻塞。在覆盖这些时,是否有任何好的教程显示“最佳实践”?更具体地说,如何使用BeginRead
对象。我知道我需要添加存储在RestartableStream缓冲区中的任何内容,然后通过内部流(base)来获取其余内容并返回toral。但由于EndRead
对象是一个自定义类,我无法弄清楚在返回之前我可以将RestartableStream的buff与内部流的buff组合在一起的通用方法。我是否还需要实现BeginRead()以便我知道用户想要存储内容的缓冲区?我想另一个解决方案是,因为丢弃的字节问题只是来自客户端的第一条消息(之后我知道是否将其用作IAsyncResult
或IAsyncResult
),将会处理第一条消息是通过直接调用RestartableStream的SSLStream
方法(暂时阻塞代码),然后对于所有将来的消息使用Async回调来读取内容,就像我现在一样。
答案 0 :(得分:3)
好的,我认为您可以做的最好的事情就是在SslStream
和NetworkStream
之间放置自己的类,在那里实现一些自定义缓冲。我在下面做了几个测试,但是在你投入生产之前我会推荐一些(可能还有一些更强大的错误处理)。我想我避免任何4.0或4.5主义:
public sealed class RestartableReadStream : Stream
{
private Stream _inner;
private List<byte[]> _buffers;
private bool _buffering;
private int? _currentBuffer = null;
private int? _currentBufferPosition = null;
public RestartableReadStream(Stream inner)
{
if (!inner.CanRead) throw new NotSupportedException(); //Don't know what else is being expected of us
if (inner.CanSeek) throw new NotSupportedException(); //Just use the underlying streams ability to seek, no need for this class
_inner = inner;
_buffering = true;
_buffers = new List<byte[]>();
}
public void StopBuffering()
{
_buffering = false;
if (!_currentBuffer.HasValue)
{
//We aren't currently using the buffers
_buffers = null;
_currentBufferPosition = null;
}
}
public void Restart()
{
if (!_buffering) throw new NotSupportedException(); //Buffering got turned off already
if (_buffers.Count == 0) return;
_currentBuffer = 0;
_currentBufferPosition = 0;
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_currentBuffer.HasValue)
{
//Try to satisfy the read request from the current buffer
byte[] rbuffer = _buffers[_currentBuffer.Value];
int roffset = _currentBufferPosition.Value;
if ((rbuffer.Length - roffset) <= count)
{
//Just give them what we have in the current buffer (exhausting it)
count = (rbuffer.Length - roffset);
for (int i = 0; i < count; i++)
{
buffer[offset + i] = rbuffer[roffset + i];
}
_currentBuffer++;
if (_currentBuffer.Value == _buffers.Count)
{
//We've stopped reading from the buffers
if (!_buffering)
_buffers = null;
_currentBuffer = null;
_currentBufferPosition = null;
}
return count;
}
else
{
for (int i = 0; i < count; i++)
{
buffer[offset + i] = rbuffer[roffset + i];
}
_currentBufferPosition += count;
return count;
}
}
//If we reach here, we're currently using the inner stream. But may be buffering the results
int ncount = _inner.Read(buffer, offset, count);
if (_buffering)
{
byte[] rbuffer = new byte[ncount];
for (int i = 0; i < ncount; i++)
{
rbuffer[i] = buffer[offset + i];
}
_buffers.Add(rbuffer);
}
return ncount;
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
//No more interesting code below here
public override void Flush()
{
throw new NotSupportedException();
}
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
用法:
在RestartableReadStream
周围构建NetworkStream
。将该实例传递给SslStream
。如果您认为SSL是错误的做事方式,请致电Restart()
,然后根据需要再次使用它。您甚至可以尝试两种以上的策略(在每种策略之间调用Restart()
)。
一旦确定了哪种策略(例如SSL或非SSL)正确,请致电StopBuffering()
。一旦完成重播它可用的任何缓冲区,它将恢复为仅在其内部流上调用Read
。如果您不调用StopBuffering
,那么从流中读取的整个历史记录将保留在_buffers
列表中,这可能会增加相当多的内存压力。
请注意,以上都不是特别考虑多线程访问的。但是如果你有多个线程在一个流上调用Read()
(特别是那个基于网络的线程),我不会指望任何理智。
答案 1 :(得分:2)
我花了几个小时搜索不在NetworkStream周围写一个流包装器,最后遇到了这个并且它对我有用。 MSDN SocketFlag.Peek 我一直在寻找建议只是写一个包装器或使用单独的端口,但我听到权威或理由时遇到了问题。
这是我的代码。 NLOLOL(不要大笑或讲课) 我还没有完全弄清楚我是否需要超过所有场景的第一个字节。
Private Async Sub ProcessTcpClient(__TcpClient As Net.Sockets.TcpClient)
If __TcpClient Is Nothing OrElse Not __TcpClient.Connected Then Return
Dim __RequestBuffer(0) As Byte
Dim __BytesRead As Integer
Using __NetworkStream As Net.Sockets.NetworkStream = __TcpClient.GetStream
__BytesRead = __TcpClient.Client.Receive(__RequestBuffer, 0, 1, SocketFlags.Peek)
If __BytesRead = 1 AndAlso __RequestBuffer(0) = 22 Then
Await Me.ProcessTcpClientSsl(__NetworkStream)
Else
Await Me.ProcessTcpClientNonSsl(__NetworkStream)
End If
End Using
__TcpClient.Close()
End Sub