使用TcpClient在网络上发送和接收数据

37

我需要开发一个服务,连接到一个TCP服务器。主要任务是读取传入的消息,并在十分钟内向服务器发送命令,如同步命令。例如,我使用了下面显示的TcpClient对象:

...
TcpClient tcpClient = new TcpClient();
tcpClient.Connect("x.x.x.x", 9999);
networkStream = tcpClient.GetStream();
clientStreamReader = new StreamReader(networkStream);
clientStreamWriter = new  StreamWriter(networkStream);
while(true)
{
   clientStreamReader.Read()
}

此外,每当我需要在任何方法中书写一些内容时,我都使用:

 clientStreamWriter.write("xxx");

这个用法正确吗?或者有更好的方法吗?

5个回答

24

首先,我建议您使用WCF、.NET Remoting或其他更高级别的通信抽象。使用“简单”的套接字的学习曲线几乎与WCF一样高,因为在直接使用TCP/IP时存在许多非明显的陷阱。

如果您决定继续使用TCP/IP,请查看我的 .NET TCP/IP FAQ,特别是有关消息框架应用程序协议规范的部分。

此外,请使用异步套接字API。同步API不可扩展,在某些错误情况下可能会导致死锁。同步API可以用作漂亮的示例代码,但真实世界的生产质量代码使用异步API。


14
大家普遍认为一切都必须自带可扩展性,这种想法真是奇怪。 - maazza

23

警告 - 这是一个非常古老而繁琐的“解决方案”。

顺便提一下,您可以使用序列化技术来发送字符串、数字或任何支持序列化的对象(大多数.NET数据存储类和结构体都是[Serializable])。 在此,您应首先将Int32长度以四个字节发送到流中,然后将二进制序列化(System.Runtime.Serialization.Formatters.Binary.BinaryFormatter)数据发送到其中。

在连接的另一端(实际上是在两端),您肯定应该有一个byte[]缓冲区,当数据到来时,您将在运行时追加和修剪左侧。

我正在使用类似以下的代码:

namespace System.Net.Sockets
{
    public class TcpConnection : IDisposable
    {
        public event EvHandler<TcpConnection, DataArrivedEventArgs> DataArrive = delegate { };
        public event EvHandler<TcpConnection> Drop = delegate { };

        private const int IntSize = 4;
        private const int BufferSize = 8 * 1024;

        private static readonly SynchronizationContext _syncContext = SynchronizationContext.Current;
        private readonly TcpClient _tcpClient;
        private readonly object _droppedRoot = new object();
        private bool _dropped;
        private byte[] _incomingData = new byte[0];
        private Nullable<int> _objectDataLength;

        public TcpClient TcpClient { get { return _tcpClient; } }
        public bool Dropped { get { return _dropped; } }

        private void DropConnection()
        {
            lock (_droppedRoot)
            {
                if (Dropped)
                    return;

                _dropped = true;
            }

            _tcpClient.Close();
            _syncContext.Post(delegate { Drop(this); }, null);
        }

        public void SendData(PCmds pCmd) { SendDataInternal(new object[] { pCmd }); }
        public void SendData(PCmds pCmd, object[] datas)
        {
            datas.ThrowIfNull();
            SendDataInternal(new object[] { pCmd }.Append(datas));
        }
        private void SendDataInternal(object data)
        {
            if (Dropped)
                return;

            byte[] bytedata;

            using (MemoryStream ms = new MemoryStream())
            {
                BinaryFormatter bf = new BinaryFormatter();

                try { bf.Serialize(ms, data); }
                catch { return; }

                bytedata = ms.ToArray();
            }

            try
            {
                lock (_tcpClient)
                {
                    TcpClient.Client.BeginSend(BitConverter.GetBytes(bytedata.Length), 0, IntSize, SocketFlags.None, EndSend, null);
                    TcpClient.Client.BeginSend(bytedata, 0, bytedata.Length, SocketFlags.None, EndSend, null);
                }
            }
            catch { DropConnection(); }
        }
        private void EndSend(IAsyncResult ar)
        {
            try { TcpClient.Client.EndSend(ar); }
            catch { }
        }

        public TcpConnection(TcpClient tcpClient)
        {
            _tcpClient = tcpClient;
            StartReceive();
        }

        private void StartReceive()
        {
            byte[] buffer = new byte[BufferSize];

            try
            {
                _tcpClient.Client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, DataReceived, buffer);
            }
            catch { DropConnection(); }
        }

        private void DataReceived(IAsyncResult ar)
        {
            if (Dropped)
                return;

            int dataRead;

            try { dataRead = TcpClient.Client.EndReceive(ar); }
            catch
            {
                DropConnection();
                return;
            }

            if (dataRead == 0)
            {
                DropConnection();
                return;
            }

            byte[] byteData = ar.AsyncState as byte[];
            _incomingData = _incomingData.Append(byteData.Take(dataRead).ToArray());
            bool exitWhile = false;

            while (exitWhile)
            {
                exitWhile = true;

                if (_objectDataLength.HasValue)
                {
                    if (_incomingData.Length >= _objectDataLength.Value)
                    {
                        object data;
                        BinaryFormatter bf = new BinaryFormatter();

                        using (MemoryStream ms = new MemoryStream(_incomingData, 0, _objectDataLength.Value))
                            try { data = bf.Deserialize(ms); }
                            catch
                            {
                                SendData(PCmds.Disconnect);
                                DropConnection();
                                return;
                            }

                        _syncContext.Post(delegate(object T)
                        {
                            try { DataArrive(this, new DataArrivedEventArgs(T)); }
                            catch { DropConnection(); }
                        }, data);

                        _incomingData = _incomingData.TrimLeft(_objectDataLength.Value);
                        _objectDataLength = null;
                        exitWhile = false;
                    }
                }
                else
                    if (_incomingData.Length >= IntSize)
                    {
                        _objectDataLength = BitConverter.ToInt32(_incomingData.TakeLeft(IntSize), 0);
                        _incomingData = _incomingData.TrimLeft(IntSize);
                        exitWhile = false;
                    }
            }
            StartReceive();
        }


        public void Dispose() { DropConnection(); }
    }
}

那只是一个例子,你需要根据自己的需求进行编辑。

为什么BufferSize = 8 * 1024? - Nathvi

13

我曾经使用过socket对象来进行编程(而不是TCP客户端)。我创建了一个类似于以下代码的Server对象(为了简洁,我省略了一些东西,如异常处理,但我希望这个想法能够通过)...

public class Server()
{
    private Socket sock;
    // You'll probably want to initialize the port and address in the
    // constructor, or via accessors, but to start your server listening
    // on port 8080 and on any IP address available on the machine...
    private int port = 8080;
    private IPAddress addr = IPAddress.Any;

    // This is the method that starts the server listening.
    public void Start()
    {
        // Create the new socket on which we'll be listening.
        this.sock = new Socket(
            addr.AddressFamily,
            SocketType.Stream,
            ProtocolType.Tcp);
        // Bind the socket to the address and port.
        sock.Bind(new IPEndPoint(this.addr, this.port));
        // Start listening.
        this.sock.Listen(this.backlog);
        // Set up the callback to be notified when somebody requests
        // a new connection.
        this.sock.BeginAccept(this.OnConnectRequest, sock);
    }

    // This is the method that is called when the socket recives a request
    // for a new connection.
    private void OnConnectRequest(IAsyncResult result)
    {
        // Get the socket (which should be this listener's socket) from
        // the argument.
        Socket sock = (Socket)result.AsyncState;
        // Create a new client connection, using the primary socket to
        // spawn a new socket.
        Connection newConn = new Connection(sock.EndAccept(result));
        // Tell the listener socket to start listening again.
        sock.BeginAccept(this.OnConnectRequest, sock);
    }
}

然后,我使用一个单独的Connection类来管理与远程主机的个别连接。大致看起来像这样...

public class Connection()
{
    private Socket sock;
    // Pick whatever encoding works best for you.  Just make sure the remote 
    // host is using the same encoding.
    private Encoding encoding = Encoding.UTF8;

    public Connection(Socket s)
    {
        this.sock = s;
        // Start listening for incoming data.  (If you want a multi-
        // threaded service, you can start this method up in a separate
        // thread.)
        this.BeginReceive();
    }

    // Call this method to set this connection's socket up to receive data.
    private void BeginReceive()
    {
        this.sock.BeginReceive(
                this.dataRcvBuf, 0,
                this.dataRcvBuf.Length,
                SocketFlags.None,
                new AsyncCallback(this.OnBytesReceived),
                this);
    }

    // This is the method that is called whenever the socket receives
    // incoming bytes.
    protected void OnBytesReceived(IAsyncResult result)
    {
        // End the data receiving that the socket has done and get
        // the number of bytes read.
        int nBytesRec = this.sock.EndReceive(result);
        // If no bytes were received, the connection is closed (at
        // least as far as we're concerned).
        if (nBytesRec <= 0)
        {
            this.sock.Close();
            return;
        }
        // Convert the data we have to a string.
        string strReceived = this.encoding.GetString(
            this.dataRcvBuf, 0, nBytesRec);

        // ...Now, do whatever works best with the string data.
        // You could, for example, look at each character in the string
        // one-at-a-time and check for characters like the "end of text"
        // character ('\u0003') from a client indicating that they've finished
        // sending the current message.  It's totally up to you how you want
        // the protocol to work.

        // Whenever you decide the connection should be closed, call 
        // sock.Close() and don't call sock.BeginReceive() again.  But as long 
        // as you want to keep processing incoming data...

        // Set up again to get the next chunk of data.
        this.sock.BeginReceive(
            this.dataRcvBuf, 0,
            this.dataRcvBuf.Length,
            SocketFlags.None,
            new AsyncCallback(this.OnBytesReceived),
            this);

    }
}

您可以通过直接调用 Connection 对象的 Socket 来发送数据,方法如下...
this.sock.Send(this.encoding.GetBytes("Hello to you, remote host."));

正如我所说,我已经尝试编辑这里的代码以进行发布,因此如果有任何错误,请见谅。


嗨,感谢回复。我也有两个应用程序来连接第三方TCP服务器。我认为一个应用程序用于向第三方服务器发送命令,另一个应用程序(作为Windows服务)将只读取传入的字节。在这种情况下,当我使用另一个TcpClient对象在第一个应用程序上发送数据时,数据被发送了,但是当我使用第二个应用程序的tcpclient对象读取数据时,传入的数据没有改变。在这种情况下,我是否需要在Windows服务中生成所有读写方法,并且客户端应用程序应该通过远程连接进行连接?多个客户端可以连接到服务,如Web应用程序。 - dankyy1
1
我通常会为每个连接使用一个线程。为此,在Connection构造函数中,不要直接调用BeginReceive(),而是像这样做:Thread t = new Thread(new ThreadStart(this.BeginReceive)); t.Start(); 使用这种方法,许多客户端可以同时连接。我是否正确理解了你的问题?你提到了“远程”,但我不确定你是特指.Net Remoting,还是只是指“从远程客户端连接”。 - Pat Daburu
嗨,我的应用程序架构将连接到第三方TCP服务器。我有两个主要任务。一个是发送命令(可以使用我们网络中的任何内部客户端应用程序完成此任务),另一个任务是监听传入数据,这可以通过集中式Windows服务应用程序完成。所以我的问题是,我们网络中的命令发送客户端应用程序应该通过远程连接连接到我们的Windows服务应用程序来发送命令,还是客户端应用程序应该使用自己的TcpClient对象连接到第三方TCP服务器直接发送命令?谢谢。 - dankyy1
抱歉耽搁了(我还没有弄清楚如何在添加评论时获取电子邮件通知)。根据您的描述,似乎您可以使用远程调用(或另一种类似于WCF的Windows技术)从客户端应用程序向Windows服务发送消息,然后让Windows服务使用套接字连接中继这些消息。或者您可以直接连接客户端应用程序,省去中间Windows服务的需要。 - Pat Daburu

4

首先,TCP不能保证您发送的所有内容在另一端以相同的读取方式接收。它只保证您发送的所有字节将按正确顺序到达。

因此,当从流中读取时,您需要不断地建立缓冲区。您还需要知道每个消息的大小。

最简单的方法是使用一个不可打印的ASCII字符来标记数据包的结尾,并在接收到的数据中查找它。


谢谢您的回复。我将尝试读取缓冲区中的字节...在我的问题中,我也想知道使用while(true)监听传入数据是否是一个好方法。 - dankyy1
1
还有另一种解决方案。在将数据转换为byte[]之前,尝试将字符串长度与数据一起发送。我发送的数据格式为[content_length,content]。因此,在两端都可以计算长度并进行匹配,以检查接收到的数据包是否已完成。对于长度,您可以使用ASCIIEncoding.ASCII.GetByteCount(data).ToString("X4")。 - Asad Ali

1
我开发了一个可能会有用的dotnet库。我解决了当数据超出缓冲区时从未获取所有数据的问题,这是许多帖子所忽略的。解决方案仍然存在一些问题,但运行得相当不错。https://github.com/Apollo013/DotNet-TCP-Communication

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