C#实现TCP客户端服务器应用程序的最佳方式

时间:2014-10-19 12:00:11

标签: c# multithreading asynchronous client-server tcpclient

我希望扩展我在.NET框架方面的经验,并希望构建一个客户端/服务器应用程序。 实际上,客户端/服务器是一个小型销售点系统,但首先,我想关注服务器和客户端之间的通信。 将来,我想把它变成一个WPF应用程序但是现在,我只是从一个控制台应用程序开始。

2个功能:

  • 客户接收数据集,每15/30分钟更新一次,更改价格/新产品
    (因此代码将使用Thread.sleep的Async方法15/30分钟)。

  • 关闭客户端应用程序时,发送一种报告(例如,xml)

在互联网上,我发现了很多例子,但我无法确定哪一个是最好/最安全/最佳的工作方式,所以我需要一些建议,我应该采用哪种技术。

客户/服务器

我想要一个处理最多6个客户端的服务器应用程序。我读到线程使用了很多mb,也许更好的方法是使用async / await functionallity的任务。

ASYNC / AWAIT的示例

http://bsmadhu.wordpress.com/2012/09/29/simplify-asynchronous-programming-with-c-5-asyncawait/

THREADS的示例

mikeadev.net/2012/07/multi-threaded-tcp-server-in-csharp/

SOCKETS示例

codereview.stackexchange.com/questions/5306/tcp-socket-server

这似乎是套接字的一个很好的例子,然而,修改后的代码不能完全运行,因为并非包含所有类 msdn.microsoft.com/en-us/library/fx6588te(v=vs.110).aspx MSDN的这个例子对于Buffersize和消息结束的信号有很多。我不知道这只是一种“老方法”,因为在我之前的例子中,他们只是从客户端发送一个字符串到服务器就是这样。

.NET FRAMEWORK REMOTING / WCF 我还发现了一些关于.NET和WCF远程部分的内容,但不知道我是否需要实现它,因为我认为Async / Await的例子并不坏。

SERIALIZED OBJECTS / DATASET / XML

在它之间发送数据的最佳方式是什么? Juse是一个XML序列化器还是只是二进制?

数据集示例 - > XML

stackoverflow.com/questions/8384014/convert-dataset-to-xml

远程处理示例

akadia.com/services/dotnet_dataset_remoting.html

如果我应该使用Async / Await方法,那么在服务器应用中这样的话是否正确:

        while(true)
        {
            string input = Console.ReadLine();
            if(input == "products")
                SendProductToClients(port);
            if(input == "rapport")
            {
                string Example = Console.ReadLine();
            }                                
        }

2 个答案:

答案 0 :(得分:1)

以下是编写客户端/服务器应用程序的人应该考虑的几件事:

  • 应用层数据包可能跨越多个TCP数据包。
  • 单个TCP数据包中可能包含多个应用层数据包。
  • 加密。
  • 验证
  • 失去和反应迟钝的客户。
  • 数据序列化格式。
  • 基于线程或异步套接字读取器。

正确检索数据包需要围绕数据的包装器协议。协议可以非常简单。例如,它可以像指定有效负载长度的整数一样简单。我在下面提供的代码段直接取自GitHub上提供的开源客户端/服务器应用程序框架项目DotNetOpenServer。请注意,客户端和服务器都使用此代码:

private byte[] buffer = new byte[8192];
private int payloadLength;
private int payloadPosition;
private MemoryStream packet = new MemoryStream();
private PacketReadTypes readState;
private Stream stream;

private void ReadCallback(IAsyncResult ar)
{
    try
    {
        int available = stream.EndRead(ar);
        int position = 0;

        while (available > 0)
        {
            int lengthToRead;
            if (readState == PacketReadTypes.Header)
            {
                lengthToRead = (int)packet.Position + available >= SessionLayerProtocol.HEADER_LENGTH ?
                        SessionLayerProtocol.HEADER_LENGTH - (int)packet.Position :
                        available;

                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;

                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH)
                    readState = PacketReadTypes.HeaderComplete;
            }

            if (readState == PacketReadTypes.HeaderComplete)
            {
                packet.Seek(0, SeekOrigin.Begin);
                BinaryReader br = new BinaryReader(packet, Encoding.UTF8);

                ushort protocolId = br.ReadUInt16();
                if (protocolId != SessionLayerProtocol.PROTOCAL_IDENTIFIER)
                    throw new Exception(ErrorTypes.INVALID_PROTOCOL);

                payloadLength = br.ReadInt32();
                readState = PacketReadTypes.Payload;
            }

            if (readState == PacketReadTypes.Payload)
            {
                lengthToRead = available >= payloadLength - payloadPosition ?
                    payloadLength - payloadPosition :
                    available;

                packet.Write(buffer, position, lengthToRead);
                position += lengthToRead;
                available -= lengthToRead;
                payloadPosition += lengthToRead;

                if (packet.Position >= SessionLayerProtocol.HEADER_LENGTH + payloadLength)
                {
                    if (Logger.LogPackets)
                        Log(Level.Debug, "RECV: " + ToHexString(packet.ToArray(), 0, (int)packet.Length));

                    MemoryStream handlerMS = new MemoryStream(packet.ToArray());
                    handlerMS.Seek(SessionLayerProtocol.HEADER_LENGTH, SeekOrigin.Begin);
                    BinaryReader br = new BinaryReader(handlerMS, Encoding.UTF8);

                    if (!ThreadPool.QueueUserWorkItem(OnPacketReceivedThreadPoolCallback, br))
                        throw new Exception(ErrorTypes.NO_MORE_THREADS_AVAILABLE);

                    Reset();
                }
            }
        }

        stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(ReadCallback), null);
    }
    catch (ObjectDisposedException)
    {
        Close();
    }
    catch (Exception ex)
    {
        ConnectionLost(ex);
    }
}


private void Reset()
{
    readState = PacketReadTypes.Header;
    packet = new MemoryStream();
    payloadLength = 0;
    payloadPosition = 0;
}

如果您要传输销售点信息,则应加密。我建议通过.Net轻松启用TLS。代码很简单,那里有很多样本,所以为了简洁,我不打算在这里展示。如果您有兴趣,可以在DotNetOpenServer中找到一个示例实现。

应对所有连接进行身份验证。有很多方法可以实现这一目标。我使用Windows身份验证(NTLM)和Basic。尽管NTLM既强大又自动,但它仅限于特定平台。基本身份验证只是在套接字加密后传递用户名和密码。但是,基本认证仍然可以;验证本地服务器或域控制器的用户名/密码组合,实际上是冒充NTLM。后一种方法使开发人员能够轻松创建在iOS,Mac,Unix / Linux版本以及Java平台上运行的非Windows客户端应用程序(尽管某些Java实现支持NTLM)。您的服务器实现永远不应允许在会话通过身份验证之后传输应用程序数据。

我们可以指望的只有几件事:税收,网络失败和客户端应用程序挂起。这只是事物的本质。您的服务器应该实现一种方法来清理丢失和挂起的客户端会话。我通过keep-alive(AKA心跳)协议在许多客户端/服务器框架中完成了这项工作。在服务器端,我实现了一个定时器,每次客户端发送数据包时都会重置该定时器。如果服务器在超时内没有收到数据包,则会话关闭。 keep-alive协议用于在其他应用层协议空闲时发送数据包。由于您的应用程序每15分钟只发送一次XML,每分钟发送一次保持活动数据包,服务器端可以在15分钟间隔之前丢失连接时向管理员发出警报,可能使IT部门解决网络问题更加及时。

接下来,数据格式。在你的情况下,XML很棒。 XML使您可以随时根据需要更改有效负载。如果你真的需要速度,那么二进制将永远胜过字符串表示数据的臃肿本质。

最后,正如@NSFW已经说过的那样,线程或异步在你的情况下并不重要。我已经编写了基于线程和异步回调扩展到10000个连接的服务器。归结起来,这一切都是一回事。正如@NSFW所说,我们大多数人现在正在使用异步回调,而我所编写的最新服务器实现也遵循该模型。

答案 1 :(得分:0)

考虑到现代系统上可用的RAM量,线程并不是非常昂贵,所以我认为优化低线程数不会有帮助。特别是如果我们谈论1线程和2-5线程之间的差异。 (有数百或数千个线程,线程的成本开始变得重要。)

但是你确实希望优化最小的阻塞你所拥有的任何线程。因此,例如,不是使用Thread.Sleep以15分钟的间隔执行工作,只需设置一个计时器,让线程返回,并信任系统在15分钟后调用您的代码。而不是阻止通过网络读取或写入信息的操作,使用非阻塞操作。

async / await模式是.Net上异步编程的新热点,它比可以追溯到.Net 1.0的Begin / End模式有了很大的改进。使用async / await编写的代码仍在使用线程,它只是使用C#和.Net的功能来隐藏线程的大量复杂性 - 而且在大多数情况下,它隐藏了应隐藏的内容,以便您可以将注意力集中在应用程序的功能上,而不是多线程编程的细节上。

所以我的建议是对你的所有IO(网络和磁盘)使用async / await方法,并使用定时器进行定期杂务,例如发送你提到的那些更新。

关于序列化......

XML优于二进制格式的最大优势之一是您可以将XML传输保存到磁盘并使用随时可用的工具打开它们,以确认有效负载确实包含您认为可能存在的数据。所以我倾向于避免使用二进制格式,除非带宽不足 - 即使这样,使用像XML这样的文本友好格式开发大部分应用程序也很有用,然后在发送和接收数据的基本机制被充实后切换到二进制格式进行。

所以我的投票是针对XML。

关于你的代码示例,还没有异步/等待它......

但首先请注意,典型的简单TCP服务器将有一个小循环,用于侦听传入连接并启动一个线程来处理每个新连接。然后,连接线程的代码将侦听传入的数据,处理它并发送适当的响应。因此,listen-for-new-connections代码和handle-a-single-connection代码是完全独立的。

所以无论如何,连接线程代码看起来可能与你编写的类似,但不是只调用ReadLine,而是执行类似“string line = await ReadLine();”的操作。 await关键字大约是您的代码允许一个线程退出(在调用ReadLine之后)然后在另一个线程上恢复(当ReadLine的结果可用时)。除了等待的方法应该具有以Async结尾的名称,例如ReadLineAsync。从网络中读取一行文本并不是一个坏主意,但您必须自己编写ReadLineAsync,并在现有网络API的基础上进行编写。

我希望这会有所帮助。