我在这里看到很多关于套接字的资源 . 我相信他们都没有涵盖我想知道的细节 . 在我的应用程序中,服务器执行所有处理并向客户端发送定期更新 .
本文的目的是涵盖开发套接字应用程序和讨论最佳实践时所需的所有基本思想 . 以下是几乎所有基于套接字的应用程序都会看到的基本内容 .
1 - Binding and listening on a socket
我使用以下代码 . 它在我的机器上运行良好 . 在真实服务器上部署时,是否需要注意其他事项?
IPHostEntry localHost = Dns.GetHostEntry(Dns.GetHostName());
IPEndPoint endPoint = new IPEndPoint(localHost.AddressList[0], 4444);
serverSocket = new Socket(endPoint.AddressFamily, SocketType.Stream,
ProtocolType.Tcp);
serverSocket.Bind(endPoint);
serverSocket.Listen(10);
2 - Receiving data
我使用了255个大小的字节数组 . 因此,当我收到超过255个字节的数据时,我需要调用receive方法,直到获得完整数据,对吧?获得完整数据后,我需要追加到目前为止收到的所有字节以获取完整的消息 . 那是对的吗?还是有更好的方法?
3 - Sending data and specifying the data length
由于TCP无法找到要接收的消息长度,因此我计划在消息中添加长度 . 这将是数据包的第一个字节 . 因此客户端系统知道有多少数据可供读取 .
还有其他更好的方法?
4 - Closing the client
当客户端关闭时,它将向服务器发送一条消息,指示关闭 . 服务器将从其客户端列表中删除客户端详细信息 . 以下是客户端用于断开套接字的代码(消息传递部分未显示) .
client.Shutdown(SocketShutdown.Both);
client.Close();
有什么建议或问题吗?
5 - Closing the server
服务器向所有客户端发送消息,指示关闭 . 每个客户端在收到此消息时将断开套接字 . 客户端将关闭消息发送到服务器并关闭 . 一旦服务器收到来自所有客户端的关闭消息,它将断开套接字并停止监听 . 在每个客户端套接字上调用Dispose以释放资源 . 这是正确的方法吗?
6 - Unknown client disconnections
有时,客户端可能会断开连接而不通知服务器 . 我的处理方法是:当服务器向所有客户端发送消息时,检查套接字状态 . 如果未连接,请从客户端列表中删除该客户端并关闭该客户端的套接字 .
任何帮助都会很棒!
3 回答
由于这是“开始”,我的答案将坚持一个简单的实现,而不是一个高度可扩展的实现 . 在使事情变得更复杂之前,最好先采用简单的方法 .
1 - Binding and listening
您的代码对我来说似乎很好,我个人使用:
而不是走DNS路线,但我不认为这两种方式都存在真正的问题 .
1.5 - Accepting client connections
只是提到这个完整性' sake... I am assuming you are doing this otherwise you wouldn' t到第2步 .
2 - Receiving data
我会使缓冲区长度超过255个字节,除非您可以预期所有服务器消息最多为255个字节 . 我认为你想要一个可能比TCP数据包大小更大的缓冲区,这样你就可以避免多次读取来接收单个数据块 .
我会说选择1500字节应该没问题,或者甚至可能是2048一个很好的整数 .
或者,也许您可以避免使用
byte[]
来存储数据片段,而是将服务器端客户端套接字包装在_2413659中,并将其包装在BinaryReader
中,以便您可以从套接字读取消息direclty的组件而无需担心缓冲区大小 .3 - Sending data and specifying data length
您的方法可以正常工作,但显然要求在开始发送之前很容易计算数据包的长度 .
或者,如果您的消息格式(其组件的顺序)以某种方式设计,那么客户端将能够在任何时间确定是否应该有更多数据(例如,代码0x01表示接下来将是一个int和一个字符串,代码0x02表示接下来将是16字节等,等等) . 结合客户端的
NetworkStream
方法,这可能是一种非常有效的方法 .为了安全起见,您可能希望添加正在接收的组件的验证,以确保您只处理合理的值 . 例如,如果您收到长度为1TB的字符串的指示,则可能在某处损坏了数据包,并且关闭连接并强制客户端重新连接并“重新开始”可能更安全 . 这种方法可以在出现意外故障时为您提供非常好的全能行为 .
4/5 - Closing the client and the server
就个人而言,如果没有进一步的消息,我会选择
Close
;什么时候连接关闭后,您将在连接的另一端阻塞读/写时遇到异常,您必须满足该连接的另一端 .因为无论如何你必须迎合“未知的断开连接”以获得一个强大的解决方案,所以断开任何更复杂的连接通常都是毫无意义的 .
6 - Unknown disconnections
我甚至不相信套接字状态......有可能连接到客户端/服务器之间的路径某处而没有客户端或服务器注意到 .
告诉连接意外死亡的唯一保证方法是下次尝试沿连接发送内容时 . 此时,如果连接出现任何问题,您将始终收到异常,指示失败 .
因此,检测所有意外连接的唯一万无一失的方法是实现“ping”机制,理想情况下,客户端和服务器将定期向另一端发送消息,该消息仅导致响应消息指示'ping'收到了 .
为了优化不必要的ping,您可能希望有一个“超时”机制,只有在一段时间内没有从另一端收到其他流量时才发送ping(例如,如果最后一条消息来自服务器超过x秒,客户端发送ping以确保连接没有通知而死亡 .
More advanced
如果您需要高可伸缩性,则必须查看所有套接字操作的异步方法(接受/发送/接收) . 这些是'Begin/End'变体,但它们使用起来要复杂得多 .
我建议不要尝试这个,直到你有简单的版本和工作 .
另请注意,如果您不打算进一步扩展到几十个客户端,那么无论如何这实际上都不会成为问题 . 如果您打算扩展到数千或数十万个连接的客户端而没有让您的服务器彻底死亡,那么异步技术实际上是必需的 .
I probably have forgotten a whole bunch of other important suggestions, but this should be enough to get you a fairly robust and reliable implementation to start with
1 - Binding and listening on a socket
看起来很好 . 您的代码将仅将套接字绑定到一个IP地址 . 如果您只想监听任何IP地址/网络接口,请使用
IPAddress.Any
:为了将来证明,您可能希望支持IPv6 . 要侦听任何IPv6地址,请使用
IPAddress.IPv6Any
代替IPAddress.Any
.请注意,除非使用Dual-Stack Socket,否则您无法同时侦听任何IPv4和任何IPv6地址 . 这将要求您取消设置
IPV6_V6ONLY
套接字选项:要使用套接字启用Teredo,需要设置
PROTECTION_LEVEL_UNRESTRICTED
套接字选项:2 - Receiving data
我建议使用NetworkStream将套接字包装在
Stream
中,而不是手动读取块 .读取固定数量的字节有点尴尬:
NetworkStream
真的很棒,你可以像任何其他Stream
一样使用它 . 如果安全性很重要,只需将NetworkStream
包装在SslStream中,即可对服务器和(可选)具有X.509证书的客户端进行身份验证 . 压缩的工作方式相同 .3 - Sending data and specifying the data length
你的方法应该有效,尽管你可能不想继续重新发明轮子并为此设计新的协议 . 看看BEEP或者甚至是像protobuf这样简单的东西 .
根据您的目标,可能值得考虑在WCF或其他RPC机制之类的套接字上选择抽象 .
4/5/6 - Closing & Unknown disconnections
什么jerryjvl说:-)唯一可靠的检测机制是ping或在连接空闲时发送保持活动 .
虽然你必须在任何情况下处理未知的断开连接,但我个人会保留一些协议元素以在相互协议中关闭连接,而不是在没有警告的情况下关闭它 .
考虑使用异步套接字 . 您可以在中找到有关该主题的更多信息
Using an Asynchronous Server Socket
Using an Asynchronous Client Socket