【问题标题】:C# Socket Client: Understand and Manage messagesC# Socket 客户端:理解和管理消息
【发布时间】:2015-04-10 20:01:21
【问题描述】:

我需要在 C# 中实现一个 Socket 客户端。

socket服务器是一个软件,通过3000端口与我的C#客户端连接。

每条消息的组成如下:

  1. 某些字段:4 个字节
  2. 消息长度:2 个字节
  3. 某些字段:4 个字节
  4. 消息索引:2 个字节
  5. 一些字段:取决于“消息长度”字段

客户端接收时,缓冲区可以包含多个单条消息和重复消息。

我必须在消息中拆分缓冲区的内容,如果该消息尚未出现在此列表中,则将消息保存在列表中。

如果消息的索引在列表中,我了解该消息已经存在于列表中。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;

namespace Client
{
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    Socket sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);


    private void btnC_Click(object sender, EventArgs e)
    {

        sck.Connect(endPoint);

        if (sck.Connected)
        {
            Form1.ActiveForm.Text = Form1.ActiveForm.Text + " - Connected";
        }

        byte[] buffer = new byte[255];
        int rec;

        while (true)
        {
            buffer = new byte[255];
            rec = sck.Receive(buffer, 0, buffer.Length, 0);
            Array.Resize(ref buffer, rec);


            /* Understand and Manage the messages */


        }
    }
}
 }

您对实施正确的代码以理解和管理收到的消息有什么建议吗??

提前致谢!

法比奥

【问题讨论】:

  • 为了简单起见,我首先使用TcpClient 而不是原始套接字。然后从流中读取——读取你知道的数据量,解释它,然后读取其余的。 BinaryReader 对此非常有帮助。
  • 您需要查看消息框架。
  • 在这种情况下使用 Tcp 会产生比您预期的要复杂得多的代码。例如,您也可能只收到消息的一部分。甚至只有一个字节是可能的,因此您甚至不知道消息长度,而只是接收它的第一部分。这是因为 TCP 是一种流协议。如果可能,请在您的情况下使用 UDP。使用 UDP,您将收到单个消息。这种设计要简单得多。用于客户端和服务器。
  • 我通常使用state machine 完成此类处理。要从一种状态前进到下一种状态,您需要验证缓冲区是否包含至少一条完整的信息,例如一个完整的 12 字节消息头。您不断在缓冲区中累积数据并尝试根据当前状态和缓冲区内容转换到新状态。请注意,由于接收数据,可能会发生几种状态转换,例如您可能收到了 3 7/8 条新消息。
  • 实际上你的代码在这一点上是毫无用处的。使用 TCP 套接字,事物是面向连接的。您将有一个侦听套接字,在其中调用 accept 将产生一个服务套接字,您可以在该套接字上开始接收数据。您将需要启动一个单独的线程来处理该套接字,并且仍然准备好接受列表套接字上的新连接。正如我所说,TCP 比 UDP 更复杂。 UDP 不是面向连接的,因此您的代码更加一致。因为你会有一个简单的recfrom,就是这样。

标签: c# sockets client


【解决方案1】:

您可以这样实现您的消息:

public abstract class NetworkMessage
{
    private List<byte> _buffer;

    protected abstract void InternalDeserialize(BinaryReader reader);

    protected NetworkMessage()
    {
        _buffer = new List<byte>();
    }

    public void Deserialize()
    {
        using (MemoryStream stream = new MemoryStream(_buffer.ToArray()))
        {
            BinaryReader reader = new BinaryReader(stream);
            this.InternalDeserialize(reader);
        }
    }
}

public class YourMessage : NetworkMessage
{
    public int YourField
    {
        get;
        set;
    }

    protected override void InternalDeserialize(BinaryReader reader)
    {
        YourField = reader.ReadInt32();
    }
}

您必须关心非阻塞网络。我的意思是,如果您正在等待(无限时间)在按钮事件中接收一些数据,您将阻止您的客户端。有很多关于它的教程:)

而且我认为在您的“1. Some fields 4 bytes”中,每条消息都有一个 Id。您最终可以创建一个Dictionnary&lt;id, Networkmessage&gt;,它将通过您的 id 返回好消息(如果您对反射有所了解,您可以创建一个 Func 来生成您的消息)或只是一个开关。我不确定我是否正确解释了我的想法。

【讨论】:

    【解决方案2】:

    通过将Socket 包装在NetworkStream 中似乎最有效地解决了您的情况,而BinaryReader 又可以包装BinaryReader。因为您在 Winforms 程序中使用它,所以您还希望避免阻塞 UI 线程,即不要在 btnC_Click() 事件处理程序方法中运行 I/O 本身。

    很遗憾,现有的BinaryReader 不提供async 方法,因此最简单的解决方案是使用同步I/O,但在Task 中执行。

    这样的事情可能对你有用(为清楚起见省略了错误处理):

    // Simple holder for header and data
    class Message
    {
        public int Field1 { get; private set; }
        public short Length { get; private set; }
        public int Field2 { get; private set; }
        public short Index { get; private set; }
        public byte[] Data { get; private set; }
    
        public Message(int field1, short length, int field2, int index, byte[] data)
        {
            Field1 = field1;
            Length = length;
            Field2 = field2;
            Index = index;
            Data = data;
        }
    }
    
    private void btnC_Click(object sender, EventArgs e)
    {
        sck.Connect(endPoint);
    
        // If Connect() completes without an exception, you're connected
        Form1.ActiveForm.Text = Form1.ActiveForm.Text + " - Connected";
    
        using (NetworkStream stream = new NetworkStream(sck))
        using (BinaryReader reader = new BinaryReader(stream))
        {
            Message message;
    
            while ((message = await Task.Run(() => ReadMessage(reader))) != null)
            {
                // process message here, preferably asynchronously
            }
        }
    }
    
    private Message ReadMessage(BinaryReader reader)
    {
        try
        {
            int field1, field2;
            short length, index;
            byte[] data;
    
            field1 = reader.ReadInt32();
            length = reader.ReadInt16();
            field2 = reader.ReadInt32();
            index = reader.ReadInt16();
    
            // NOTE: this is the simplest implementation based on the vague
            // description in the question. I assume "length" contains the
            // actual length of the _remaining_ data, but it could be that
            // the number of bytes to read here needs to take into account
            // the number of bytes already read (e.g. maybe this should be
            // "length - 20"). You also might want to create subclasses of
            // Message that are specific to the actual message, and use
            // the BinaryReader to initialize those based on the data read
            // so far and the remaining data.
    
            data = reader.ReadBytes(length);
        }
        catch (EndOfStreamException)
        {
            return null;
        }
    }
    

    一般来说,异步 I/O 会更好。以上适用于连接数比较少的情况。鉴于这是客户端,很可能您只有一个连接。所以我提供的例子可以正常工作。但请注意,将单个线程专用于每个套接字会非常很差地扩展,即它只能有效地处理相对较少数量的连接。

    因此,当您发现自己编写更复杂的套接字场景时,请记住这一点。在这种情况下,您将需要额外的麻烦来实现某种基于状态的机制,该机制可以在每个消息通过原始字节接收时为每个消息累积数据,您可以使用NetworkStream.ReadAsync() 来做到这一点。


    最后,一些笔记来解决您收到的一些问题本身的问题:

    • 你肯定确实想在这里使用 TCP。 UDP 是不可靠的——是的,它是基于消息的,但您还必须处理这样一个事实,即给定消息可能会被多次接收,消息的接收顺序可能与发送它们的顺序不同,并且消息可能根本收不到。

    是的,TCP 是面向流的,这意味着您必须对其施加自己的消息边界。但与 UDP 相比,这是唯一的复杂性,否则 UDP 处理起来会很多复杂(当然,除非你的代码本质上不需要可靠性……UDP 对于某些事情当然是好的,但它是绝对不是新网络程序员的起点)。

    • 您的问题没有任何迹象表明您需要一个“监听”套接字。只需要监听 TCP 连接的一端;那是服务器。您已经明确声明您正在实现 client,因此不需要监听。您只需要连接到服务器(您的代码就是这样做的)。

    【讨论】:

      【解决方案3】:
      bool getHeader = True;
      int header_size = 4+2+4+2;
      int payload_size = 0;
      byte[] header = new byte[header_size];
      byte[] buffer = new byte[255];
      int rec;
      
      while (true)
      {
          if(getHeader)
          {
              rec = sck.Receive(header, 0, header_size, 0);
              payload_size = ..... parse header[4] & header[5] to payload size (int)
              getHeader = False;
          }else{
              rec = sck.Receive(buffer, 0, payload_size, 0);
              getHeader = True;
          }  
      }
      

      【讨论】:

      • 应该解释代码的作用,而不是仅仅把它丢给可能不知道如何阅读它的人。让我们知道您不只是复制粘贴此答案
      猜你喜欢
      • 1970-01-01
      • 2013-04-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多