客户端关闭流时是否可能检测到?

10

简要概述:

我的服务通过套接字接收信息并发送回复,连接是不安全的。我想设置另一个服务来为这些连接提供TLS - 这个新服务将提供一个端口,并根据客户端证书分发连接。我不想使用stunnel有几个原因,其中之一是它会需要一个转发端口对应一个接收端口。

我目前尝试实现的解决方案:

基本上,我正在尝试将SslStream(输入)与NetworkStream(输出 - 可能是Socket,但我将其放入NetworkStream以匹配输入)耦合,并将读/写操作链接起来。这个链接将在客户端(通过SSL / TLS)和服务(通过不安全的连接)之间提供流程。

以下是我设计用于链接这些流的类:

public class StreamConnector
{
    public StreamConnector(Stream s1, Stream s2)
    {
        StreamConnectorState state1 = new StreamConnectorState(s1, s2);
        StreamConnectorState state2 = new StreamConnectorState(s2, s1);
        s1.BeginRead(state1.Buffer, 0, state1.Buffer.Length, new AsyncCallback(ReadCallback), state1);
        s2.BeginRead(state2.Buffer, 0, state2.Buffer.Length, new AsyncCallback(ReadCallback), state2);
    }

    private void ReadCallback(IAsyncResult result)
    {
        // Get state object.
        StreamConnectorState state = (StreamConnectorState)result.AsyncState;

        // Finish reading data.
        int length = state.InStream.EndRead(result);

        // Write data.
        state.OutStream.Write(state.Buffer, 0, length);

        // Wait for new data.
        state.InStream.BeginRead(state.Buffer, 0, state.Buffer.Length, new AsyncCallback(ReadCallback), state);
    }
}

public class StreamConnectorState
{
    private const int BYTE_ARRAY_SIZE = 4096;

    public byte[] Buffer { get; set; }
    public Stream InStream { get; set; }
    public Stream OutStream { get; set; }

    public StreamConnectorState(Stream inStream, Stream outStream)
    {
        Buffer = new byte[BYTE_ARRAY_SIZE];
        InStream = inStream;
        OutStream = outStream;
    }
}

问题:

当客户端完成发送信息并处置SslStream时,服务器没有任何指示表明是否已发生这种情况。 StreamConnector类愉快地一直运行,而不会抛出任何错误,并且我找不到应该停止它的任何指示。(当然,每次在ReadCallback中都会得到0长度,但我需要能够提供长时间运行的连接,因此这不是一个判断的好方法。)

另一个潜在问题是,即使没有可用数据,也会调用ReadCallback。 不确定如果我使用Socket而不是流,是否会有所不同,但似乎保持不断运行那段代码效率低下。

我的问题:

1)有没有办法从客户端端告诉流已经关闭?

2)有没有更好的方式来实现我正在尝试做的事情?

2a)有没有更有效的方法来运行异步读写循环?

编辑:感谢Robert。 结果循环一直被调用,因为我没有关闭流(由于不知道何时需要关闭流)。 我将包含完整的代码解决方案,以防其他人遇到此问题:

/// <summary>
/// Connects the read/write operations of two provided streams
/// so long as both of the streams remain open.
/// Disposes of both streams when either of them disconnect.
/// </summary>
public class StreamConnector
{
    public StreamConnector(Stream s1, Stream s2)
    {
        StreamConnectorState state1 = new StreamConnectorState(s1, s2);
        StreamConnectorState state2 = new StreamConnectorState(s2, s1);
        s1.BeginRead(state1.Buffer, 0, state1.Buffer.Length, new AsyncCallback(ReadCallback), state1);
        s2.BeginRead(state2.Buffer, 0, state2.Buffer.Length, new AsyncCallback(ReadCallback), state2);
    }

    private void ReadCallback(IAsyncResult result)
    {
        // Get state object.
        StreamConnectorState state = (StreamConnectorState)result.AsyncState;

        // Check to make sure Streams are still connected before processing.
        if (state.InStream.IsConnected() && state.OutStream.IsConnected())
        {
            // Finish reading data.
            int length = state.InStream.EndRead(result);

            // Write data.
            state.OutStream.Write(state.Buffer, 0, length);

            // Wait for new data.
            state.InStream.BeginRead(state.Buffer, 0, state.Buffer.Length, new AsyncCallback(ReadCallback), state);
        }
        else
        {
            // Dispose of both streams if either of them is no longer connected.
            state.InStream.Dispose();
            state.OutStream.Dispose();
        }
    }
}

public class StreamConnectorState
{
    private const int BYTE_ARRAY_SIZE = 4096;

    public byte[] Buffer { get; set; }
    public Stream InStream { get; set; }
    public Stream OutStream { get; set; }

    public StreamConnectorState(Stream inStream, Stream outStream)
    {
        Buffer = new byte[BYTE_ARRAY_SIZE];
        InStream = inStream;
        OutStream = outStream;
    }
}

public static class StreamExtensions
{
    private static readonly byte[] POLLING_BYTE_ARRAY = new byte[0];

    public static bool IsConnected(this Stream stream)
    {
        try
        {
            // Twice because the first time will return without issue but
            // cause the Stream to become closed (if the Stream is actually
            // closed.)
            stream.Write(POLLING_BYTE_ARRAY, 0, POLLING_BYTE_ARRAY.Length);
            stream.Write(POLLING_BYTE_ARRAY, 0, POLLING_BYTE_ARRAY.Length);
            return true;
        }
        catch (ObjectDisposedException)
        {
            // Since we're disposing of both Streams at the same time, one
            // of the streams will be checked after it is disposed.
            return false;
        }
        catch (IOException)
        {
            // This will be thrown on the second stream.Write when the Stream
            // is closed on the client side.
            return false;
        }
    }
}

请不要在标题前加上"C#",这就是标记的作用 :) - kprobst
抱歉。 :) 在我研究时看到其他标题中提到了这种语言,认为这可能会有所帮助。从现在开始将坚持使用标签。 - zimdanen
2个回答

4
您必须尝试读取或写入套接字,或任何基于它的东西,以便检测到断开连接。尝试写入将抛出异常/返回错误(取决于您所使用的语言),或可能只写入0字节。尝试读取将抛出异常/返回错误(再次取决于您所使用的语言),或返回“null”。值得注意的是,如果您正在使用基于select的服务器模型,则在套接字断开连接时,已断开连接的套接字会显示为可读,并且您尝试从中读取并获得错误或“null”。

1
那么我需要尝试向每个流写入数据(如果流没有关闭,可能会导致流中的数据不正确)? - zimdanen
猜测一下,如果我写入0字节,那应该不会有问题。 - zimdanen
2
我想补充一点,根据Nito的说法,检测到连接中断的唯一方法是向流中写入数据。 - Asaf Sitner

0

我不禁想到你的客户端应该通过某种消息告诉服务器它何时完成。最好做好准备,以防电线被剪断、电力故障或插头被拔出,但通常你希望使用某种结束消息标记来终止连接。将异常情况留给真正的问题,而不是普通的对话。


在客户端处置SslStream(拥有NetworkStream,拥有Socket)时,服务器没有发送任何内容来告诉它已经断开连接。至少,在服务器上没有关闭套接字/流的内容。 - zimdanen
@zimdanen:我认为这是SslStream的一个(大)设计问题。如果SslStream超出了您的权限,除了您已经做过的事情之外,就没有太多可以做的了。我们必须让我们的软件适应现实世界,而不是应该存在的世界。(尽管我忍不住抱怨一下。) - RalphChapin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接