【问题标题】:Async socket client using TcpClient class C#使用 TcpClient 类 C# 的异步套接字客户端
【发布时间】:2017-01-18 11:32:17
【问题描述】:

我已经使用 TcpClient 类实现了一个套接字客户端。所以我可以发送和接收数据,一切正常。但是我问了那里的一些大师:) 我的实施有问题吗?也许有更好的做事方式。特别是,我如何处理断开连接?是否有一些指示器(或者我可以自己写一个)告诉我套接字已断开连接?

我也研究了 Socket 类的异步等待功能,但无法理解“SocketAsyncEventArgs”,为什么它首先存在。 为什么我不能:await Client.SendAsync("data");

public class Client
{
    private TcpClient tcpClient;

    public void Initialize(string ip, int port)
    {
        try
        {
            tcpClient = new TcpClient(ip, port);

            if (tcpClient.Connected)
                Console.WriteLine("Connected to: {0}:{1}", ip, port);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Initialize(ip, port);
        }
    }

    public void BeginRead()
    {
        var buffer = new byte[4096];
        var ns = tcpClient.GetStream();
        ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
    }

    public void EndRead(IAsyncResult result)
    {
        var buffer = (byte[])result.AsyncState;
        var ns = tcpClient.GetStream();
        var bytesAvailable = ns.EndRead(result);

        Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesAvailable));
        BeginRead();
    }

    public void BeginSend(string xml)
    {
        var bytes = Encoding.ASCII.GetBytes(xml);
        var ns = tcpClient.GetStream();
        ns.BeginWrite(bytes, 0, bytes.Length, EndSend, bytes);
    }

    public void EndSend(IAsyncResult result)
    {
        var bytes = (byte[])result.AsyncState;
        Console.WriteLine("Sent  {0} bytes to server.", bytes.Length);
        Console.WriteLine("Sent: {0}", Encoding.ASCII.GetString(bytes));
    }
}

及用法:

static void Main(string[] args)
{
    var client = new Client();
    client.Initialize("127.0.0.1", 8778);

    client.BeginRead();
    client.BeginSend("<Names><Name>John</Name></Names>");

    Console.ReadLine();
}

【问题讨论】:

标签: c# sockets asynchronous tcp tcpclient


【解决方案1】:

好吧,我花了 10 秒钟才找到你可以解决的最大问题:

public void BeginRead()
{
    var buffer = new byte[4096];
    var ns = tcpClient.GetStream();
    ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
}

但别担心,这就是我们选择 SO 的原因。


首先让我解释一下为什么这是一个问题。

假设您要发送 4097 字节 长的消息。您的缓冲区只能接受 4096 字节,这意味着您不能将整个消息打包到此缓冲区中。

假设您要发送 12 字节 长的消息。您仍在内存中分配 4096 字节,只是为了存储 12 字节

如何处理?

每次使用网络时,您都应该考虑制定某种协议(有些人称之为消息框架,但它只是一个协议)来帮助您识别传入的包。

协议示例可以是:

[1B = 消息类型][4B = 长度][XB = 消息]
- where X == BitConvert.ToInt32(length);

  • 接收者:

    byte messageType = (byte)netStream.ReadByte();
    byte[] lengthBuffer = new byte[sizeof(int)];
    int recv = netStream.Read(lengthBuffer, 0, lengthBuffer.Length);
    if(recv == sizeof(int))
    {
        int messageLen = BitConverter.ToInt32(lengthBuffer, 0);
        byte[] messageBuffer = new byte[messageLen];
        recv = netStream.Read(messageBuffer, 0, messageBuffer.Length);
        if(recv == messageLen)
        {
            // messageBuffer contains your whole message ...
        }
    }
    
  • 发件人:

    byte messageType = (1 << 3); // assume that 0000 1000 would be XML
    byte[] message = Encoding.ASCII.GetBytes(xml);
    byte[] length = BitConverter.GetBytes(message.Length);
    byte[] buffer = new byte[sizeof(int) + message.Length + 1];
    buffer[0] = messageType;
    for(int i = 0; i < sizeof(int); i++)
    {
        buffer[i + 1] = length[i];
    }
    for(int i = 0; i < message.Length; i++)
    {
        buffer[i + 1 + sizeof(int)] = message[i];
    }
    netStream.Write(buffer);
    

您的其余代码看起来还不错。但在我看来,在你的情况下使用异步操作是没有用的。您可以对同步调用执行相同的操作。

【讨论】:

  • 由于代码几乎可以从这里转到任何地方,为什么您建议避免异步操作?在这个发展阶段做出这样的决定是否有一些明显的好处和成本?
  • 您每次要读取时分配新缓冲区的解决方案效率极低。 4K的内存便宜又丰富。不应为此浪费 CPU 周期。
【解决方案2】:

这很难回答,因为这里没有确切的问题,但更多的是某种代码审查。但还是有一些提示:

  • 您的连接机制似乎有误。我认为TcpClient.Connected 在建立连接之前不会阻塞。因此,它通常会在连接正在进行时失败,然后您重新开始。您应该改用阻塞或异步Connect 方法。
  • SocketAsyncEventArgs 是一种用于高性能异步数据传输的机制。很少需要它。你应该忽略它
  • 如果您想异步发送数据,您应该使用返回 TaskAsync 方法,因为这些方法可以轻松地与 async/await 结合使用。
  • APM 模型(BeginXYZ/EndXYZ)有点过时了,你不应该再在新代码中使用它。它的一个问题是,有时 End 方法在 Begin 方法中被同步调用,这可能会导致令人惊讶的行为。如果不是这种情况,将从 ThreadPool 上的随机线程执行完成回调。这通常也不是您想要的。 TPL 方法避免了这种情况。
  • 对于您的简单用例,阻塞方法也完全没问题,不会带来各种异步方法的复杂性。

使用 TPL 方法读取代码的一面(未经测试):

public async Task Initialize(string ip, int port)
{
    tcpClient = new TcpClient;
    await tcpClient.ConnectAsync(ip, port);

    Console.WriteLine("Connected to: {0}:{1}", ip, port);
}

public async Task Read()
{
    var buffer = new byte[4096];
    var ns = tcpClient.GetStream();
    while (true)
    {
        var bytesRead = await ns.ReadAsync(buffer, 0, buffer.Length);
        if (bytesRead == 0) return; // Stream was closed
        Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesRead));
    }
}

在初始化部分你会做:

await client.Initialize(ip, port);
// Start reading task
Task.Run(() => client.Read());

对于使用同步方法,删除所有出现的Async 并将Task 替换为Thread。

【讨论】:

  • 没关系。当流从远程端关闭时,NetworkStream.ReadAsync 返回 0。但是,根据应用程序,在这里抛出异常而不是仅仅返回可能更有意义。这只是一个例子。但是,在我的示例中,ReadAsync 之前似乎缺少await。我修好了。
  • 从远端关闭是什么意思?这与正在发送的消息完成/到达 EOF 时相比如何。但未来可能还会有更多消息。我目前正在努力解决的一件事是如何通过从未关闭的连接继续读取来自流的传入消息。 MS 文档几乎没有持久连接的例子,他们在阅读后很快就关闭了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-22
  • 1970-01-01
相关资源
最近更新 更多