使用TcpClient类的异步套接字客户端 (C#)

5
我使用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();
}

1
这个问题不更适合 https://codereview.stackexchange.com/ 吗? - Deantwo
2个回答

11

好的,我花了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 = 消息]
  - 其中 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);
    

你的代码其他部分看起来都不错。但在我看来,在你的情况下使用异步操作是没有必要的。你可以使用同步调用完成相同的工作。


由于代码从这里开始几乎可以到任何地方,为什么建议避免异步操作?在开发的这个阶段做出这样的决定是否有明显的利弊? - omJohn8372
2
你的解决方案每次想要读取时都分配一个新缓冲区,这是极其低效的。4K内存便宜且充足,CPU周期不应该浪费在这上面。 - Derf Skren

9
很难回答这个问题,因为没有确切的问题,只是一些代码审查。但仍然有一些提示:
- 您的连接机制似乎有问题。我不认为TcpClient.Connected会阻塞直到连接建立。因此,它经常会在连接正在进行时失败,然后您又重新开始。您应该切换到使用阻止或异步Connect方法。 - SocketAsyncEventArgs是高性能异步数据传输的机制。很少需要。您应该忽略它。 - 如果要异步发送数据,则应使用返回任务的Async方法,因为这些可以轻松地与async/await组合使用。 - APM模型(BeginXYZ / EndXYZ)已过时,您不应再在新代码中使用它。其中一个问题是有时会在Begin方法内同步调用End方法,这可能会导致令人惊讶的行为。如果不是这种情况,则完成回调将从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。我已经修复了这个问题。 - Matthias247
你所说的远程关闭是什么意思?这与消息已完成/到达EOF时有何区别?但未来仍可能会有更多的消息。目前我遇到的一个问题是如何在永不关闭的连接中继续读取流中的传入消息。微软文档中很少提供持久连接的示例,它们很快就会关闭连接。 - Sir

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