从零开始的服务端教程 - 第一节:协议选型
为了实现服务端,我们需要首先说明:什么是服务端?
我的理解是,服务端是一种在网络中为客户端提供数据、计算或应用服务的计算机系统或程序,其核心任务包括处理请求、执行业务逻辑、访问数据库并返回响应等,典型形式有 Web 服务器、应用服务器与数据库服务器。
既然要为客户端提供服务,我们首先需要建立一种能够与客户端进行通信的机制。按照国际标准化组织(ISO)提出的开放系统互连参考模型(Open Systems Interconnection Model, OSI),网络通信过程可以被抽象为自上而下的七个层次:应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。我们时常听到的 HTTP、FTP、SMTP 等,都是工作在应用层的通信协议。
从承载比特传输的物理介质,到负责寻址、传输乃至最终应用交互的上层协议,现代互联网正是建立在这些层层衔接、彼此协作的协议与技术规范之上的。
本教程无意讨论那些太过底层的技术细节,因此我们将直接从传输层开始:在这一层中,我们将见到一些耳熟能详的协议:TCP、UDP,以及近年逐渐兴起的、为 HTTP/3 奠定了技术基础的基于 UDP 的 QUIC 等。当然,也有其他的协议,如 SCTP。
为了实现我们的服务端,我们首先需要考虑服务端应该采用什么样的传输层协议。选择基于什么样的协议进行开发是很重要的,因为这直接影响我们能够拥有哪些独属于某个协议的特性,以及为了让尽可能多的设备能够与我们的服务端进行通信,我们到底需要吃上多少苦头。从实践角度出发,在这一层上,我不建议任何普通人轻易尝试那些相对冷门的协议(如 SCTP),甚至自己实现一个协议并试图将它用于实际业务——如果你真的这么做的话,你大概会后悔的。
这么看来,我们的选择就只剩下几个:TCP,UDP,还是近期愈发成熟的 QUIC?
长期以来,“TCP 可靠、UDP 轻量快捷”几乎构成了人们对这两种传输层协议最普遍的印象。而 QUIC,它似乎正试图兼取 TCP 与 UDP 之长:它建立在 UDP 之上,却提供了类似 TCP 的可靠传输、拥塞控制等能力,同时又借助多路复用、连接迁移等机制获得了更大的灵活性。
如何在这几个协议中做出选择是开发者需要根据实际需求进行考虑的事情。从“实现一个保密档案管理系统”的需求出发,我们似乎不太需要低延迟,同时又对传输的可靠性有一定要求——基于这种考量,我选择了 TCP 作为传输层协议。之所以没有选择使用 QUIC,是因为我并不希望将一部分精力用来处理消息的 framing ——在学习实现服务端的初期,这个问题让我吃尽了苦头,所以我倾向于使用那些已经妥善地处理好了此问题的成熟的上层抽象,而在 TCP 之上,恰好就有符合要求的一个。
但是,在选定了传输层协议后,通常并不直接在这一层协议之上实现自己的通信方法,因为我们所实现的业务对通信协议的许多需求并不独特(但传输层协议也并不原生支持它们),而我觉得你为了满足这些需求而自行造出的轮子,恐怕未必要比那些早已存在且相当成熟的上层协议更为高明。当然,如果只是为了学习,你当然可以做出自己的尝试。
Don’t reinvent the wheel.
不要重新造轮子。
那么,在传输层之上的应用层、表示层和会话层(这三层在实践中有时界限并不明晰)中,我们其实也已经听说过了很多延续至今且仍然十分常见的通信协议,如 HTTP、FTP、SMTP 等,这些协议为各种需求场景提供了可行的通信规范,其中 HTTP 是如今我们使用浏览器访问互联网的基础。
在我们的业务场景中,显然,我们需要至少能够允许客户端同服务端交换一些文件,能够判断并决定哪些来自客户端的请求应该予以通过,以及确保信息在传输过程中的安全——而在这样的需求之下,我们实际要求目标协议能够实现全双工通信,具有加密通信的能力,并且最好能够在协议层面上就解决粘包的问题,而不必让我们的业务代码负责解析。
HTTP 看起来是一个很合适的选项,HTTP API 的工程设计经过多年实践已经相当成熟,不过,我们希望我们的这套系统不会让无关访客利用常见的浏览器就可进行访问,所以需要在 HTTP 之外寻找合适的协议。经过一番筛选,WebSocket 似乎是一个不错的选择:它通过 HTTP 协议完成握手,全双工通信并可使用加密传输,也无需担忧粘包问题。此外,相比传统 HTTP 协议,由于建立的是长连接,WebSocket 不存在需要频繁建立和关闭连接的问题(不过,大量长期保持的连接也会增加服务端的连接管理和资源占用压力)。
总之,就让我们暂且选定 WebSocket 作为我们的通信协议吧。在下一节,我们将真正开始利用 Python 创建我们的第一个能够监听指定地址的服务端。