我在这里看到很多关于套接字的资源.我相信他们都没有涵盖我想知道的细节.在我的应用程序中,服务器执行所有处理并向客户端发送定期更新.
本文的目的是涵盖开发套接字应用程序和讨论最佳实践时所需的所有基本思想.以下是几乎所有基于套接字的应用程序都会看到的基本内容.
1 - 绑定和侦听套接字
我使用以下代码.它在我的机器上运行良好.在真实服务器上部署时,是否需要注意其他事项?
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 - 接收数据
我使用了255个大小的字节数组.因此,当我收到超过255个字节的数据时,我需要调用receive方法,直到获得完整数据,对吧?获得完整数据后,我需要追加到目前为止收到的所有字节以获取完整的消息.那是对的吗?还是有更好的方法?
3 - 发送数据并指定数据长度
由于TCP无法找到要接收的消息长度,因此我计划在消息中添加长度.这将是数据包的第一个字节.因此客户端系统知道有多少数据可供读取.
还有其他更好的方法?
4 - 关闭客户端
当客户端关闭时,它将向服务器发送一条消息,指示关闭.服务器将从其客户端列表中删除客户端详细信息.以下是客户端用于断开套接字的代码(消息传递部分未显示).
client.Shutdown(SocketShutdown.Both); client.Close();
有什么建议或问题吗?
5 - 关闭服务器
服务器向所有客户端发送消息,指示关闭.每个客户端在收到此消息时将断开套接字.客户端将关闭消息发送到服务器并关闭.一旦服务器收到来自所有客户端的关闭消息,它将断开套接字并停止监听.在每个客户端套接字上调用Dispose以释放资源.这是正确的方法吗?
6 - 未知客户端断开连接
有时,客户端可能会断开连接而不通知服务器.我的处理方法是:当服务器向所有客户端发送消息时,检查套接字状态.如果未连接,请从客户端列表中删除该客户端并关闭该客户端的套接字.
任何帮助都会很棒!
由于这是"开始",我的答案将坚持一个简单的实现,而不是一个高度可扩展的实现.在使事情变得更复杂之前,最好先采用简单的方法.
1 - 绑定和监听
你的代码对我来说似乎很好,我个人使用:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));
而不是走DNS路线,但我不认为这两种方式都存在真正的问题.
1.5 - 接受客户端连接
只是为了完整性而提到这一点......我假设你正在这样做,否则你就不会进入第2步.
2 - 接收数据
我会使缓冲区长度超过255个字节,除非您可以预期所有服务器消息最多为255个字节.我认为你想要一个可能比TCP数据包大小更大的缓冲区,这样你就可以避免多次读取来接收单个数据块.
我会说选择1500字节应该没问题,或者甚至可能是2048一个很好的整数.
或者,也许您可以避免使用a byte[]
来存储数据片段,而是将服务器端客户端套接字NetworkStream
包装在一个包装中BinaryReader
,以便您可以从套接字读取消息direclty的组件,而无需担心缓冲区大小.
3 - 发送数据和指定数据长度
您的方法可以正常工作,但显然要求在开始发送之前很容易计算数据包的长度.
或者,如果您的消息格式(其组件的顺序)以某种方式设计,那么客户端将能够在任何时间确定是否应该有更多数据(例如,代码0x01表示接下来将是一个int和一个字符串,代码0x02表示接下来将是16字节等,等等).结合NetworkStream
客户端方法,这可能是一种非常有效的方法.
为了安全起见,您可能希望添加正在接收的组件的验证,以确保您只处理合理的值.例如,如果您收到长度为1TB的字符串的指示,则可能在某处损坏了数据包,并且关闭连接并强制客户端重新连接并"重新开始"可能更安全.这种方法可以在出现意外故障时为您提供非常好的全能行为.
4/5 - 关闭客户端和服务器
我个人会选择Close
没有进一步的消息; 当连接关闭时,您将在连接的另一端阻塞读/写时遇到异常,您必须满足该连接的另一端.
因为无论如何你必须迎合"未知的断开连接"以获得一个强大的解决方案,所以断开任何更复杂的连接通常都是毫无意义的.
6 - 未知的断开连接
我甚至不信任套接字状态...连接可能会在客户端/服务器之间的某个路径上死亡而客户端或服务器没有注意到.
告诉连接意外死亡的唯一保证方法是下次尝试沿连接发送内容时.此时,如果连接出现任何问题,您将始终收到异常,指示失败.
因此,检测所有意外连接的唯一万无一失的方法是实现"ping"机制,理想情况下,客户端和服务器将定期向另一端发送消息,该消息仅导致响应消息指示'ping'收到了.
为了优化不必要的ping,您可能希望有一个"超时"机制,只有在一段时间内没有从另一端收到其他流量时才发送ping(例如,如果最后一条消息来自服务器超过x秒,客户端发送ping以确保连接没有通知而死亡.
更高级
如果您想要高可伸缩性,则必须查看所有套接字操作的异步方法(接受/发送/接收).这些是'Begin/End'变体,但它们使用起来要复杂得多.
我建议不要尝试这个,直到你有简单的版本和工作.
另请注意,如果您不打算进一步扩展到几十个客户端,那么无论如何这实际上都不会成为问题.如果您打算扩展到数千或数十万个连接的客户端而没有让您的服务器彻底死亡,那么异步技术实际上是必需的.
我可能已经忘记了很多其他重要的建议,但这应该足以让你获得一个相当强大和可靠的实现开始
1 - 绑定和侦听套接字
看起来很好.您的代码将仅将套接字绑定到一个IP地址.如果您只想监听任何IP地址/网络接口,请使用IPAddress.Any
:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 4444));
为了将来证明,您可能希望支持IPv6.要侦听任何IPv6地址,请使用IPAddress.IPv6Any
代替IPAddress.Any
.
请注意,除非使用双栈插槽,否则无法同时侦听任何IPv4和任何IPv6地址.这将要求您取消设置IPV6_V6ONLY
套接字选项:
serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, 0);
要使用套接字启用Teredo,需要设置PROTECTION_LEVEL_UNRESTRICTED
套接字选项:
serverSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)23, 10);
2 - 接收数据
我建议使用一个NetworkStream
包装套接字Stream
而不是手动读取数据块.
读取固定数量的字节有点尴尬:
using (var stream = new NetworkStream(serverSocket)) { var buffer = new byte[MaxMessageLength]; while (true) { int type = stream.ReadByte(); if (type == BYE) break; int length = stream.ReadByte(); int offset = 0; do offset += stream.Read(buffer, offset, length - offset); while (offset < length); ProcessMessage(type, buffer, 0, length); } }
凡NetworkStream
真正的亮点是,你可以使用它像任何其他Stream
.如果安全性很重要,只需将NetworkStream
a 包装在一个SslStream
以验证服务器和(可选)具有X.509证书的客户端.压缩的工作方式相同.
var sslStream = new SslStream(stream, false); sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true); // receive/send data SSL secured
3 - 发送数据并指定数据长度
你的方法应该有效,尽管你可能不想继续重新发明轮子并为此设计新的协议.看看BEEP或者甚至是像protobuf这样简单的东西.
根据您的目标,可能值得考虑在WCF或其他RPC机制之类的套接字上选择抽象.
4/5/6 - 关闭和未知断开连接
什么jerryjvl说:-)唯一可靠的检测机制是ping或在连接空闲时发送保持活动.
虽然你必须在任何情况下处理未知的断开连接,但我个人会保留一些协议元素以在相互协议中关闭连接,而不是在没有警告的情况下关闭它.