当前位置: 首页 > 科技观察

套接字挑战?

时间:2023-03-22 16:41:40 科技观察

软件中最常见和最重要的接口之一是SocketAPI。SocketAPI最早由加州大学伯克利分校的计算机系统研究小组开发,并于1982年作为BSD4.1c操作系统的一部分首次发布。虽然有一些API存在时间更长,例如那些处理Unix文件I/O的API,但令人印象深刻的是,API一直在使用并且几乎40年来基本保持不变。SocketAPI的主要更新是帮助程序的扩展,以适应IPv6的大地址空间。自从socketAPI诞生以来,互联网乃至整个网络世界都发生了非常重大的变化。API改变了开发人员思考和编写网络应用程序的方式。然而,网络世界和各种服务的不断变化给socketAPI带来了哪些挑战?Socket的历史从1982年到现在,网络最大的两个不同是拓扑结构和速度。人们注意到的是速度的提高,而不是拓扑结构的变化。1982年,商业长途网络链路的最大带宽为1.5Mbps。部署的以太网LAN的速度为10mbps。局域网中两台计算机之间的往返时间以几十毫秒为单位,而互联网上系统之间的往返时间以数百毫秒为单位,当然这取决于位置和数据包的跳数需要在计算机之间传输。能够通过电话线连接到任何计算设备对于家庭用户来说是一件幸福的事情。1995年,我在当时的电报局申请了BTA邮箱,激动了好久。当时的网络拓扑结构比较简单,大多数计算机只连接到一个局域网;LAN连接到原始路由器,该路由器可能与其他LAN或Internet有一些连接。对于一个应用程序到另一个应用程序,连接要么跨越LAN,要么通过一个或多个路由器。最流行的分布式编程模型是基于套接字API的客户端/服务器模型,其中有一个服务器和一组客户端。客户端向服务器发送消息,要求服务器代表他们完成工作,等待服务器完成请求的工作,并在稍后收到回复。这种计算模型已经变得如此普遍,以至于它通常是许多软件工程师所熟悉的唯一模型。然而,在设计套接字时,它被视为一种在计算机网络上扩展Unix文件I/O模型的方法。另一个原因是它支持最流行的TCP协议,该协议本质上具有点对点通信模型。SocketAPI使得client/server模型易于实现,程序员只需要在非网络代码中加入少量的系统调用就可以利用其他计算资源,这使得SocketAPI的client/server模型在网络中占主导地位计算工具。模型。Socket中的以下五个函数是API的核心,也是它与常规文件I/O的区别:socket()创建通信端点bind()将端点绑定到一组网络层参数connect()连接到服务器提交请求listen()监听链接并对请求数量设置限制accept()接受来自客户端的一个或多个请求实际上,socket()调用可以替换为open()的变体,但当时并没有这样做。Socket()和open()实际上都向程序返回相同的东西:进程唯一的文件描述符,并用于该API的所有后续操作。套接字API的简单性导致它无处不在,但它的无处不在阻碍了替代或增强API的开发,这些API可以帮助程序员开发其他类型的分布式应用程序。套接字面临的挑战客户端/服务器计算模型在开发中有很多优势。它允许许多用户共享资源,并且通过这种共享模型,可以提高资源利用率。然而,套接字AP在三个不同的网络领域表现不佳:低延迟或实时应用程序高带宽应用程序多宿主系统(即具有多个网络接口的系统)。许多人将增加网络带宽与提高性能混为一谈,因为增加带宽并不一定会减少延迟。SocketAPI面临的主要性能挑战是如何让应用程序更快地访问网络数据。任何使用套接字API的程序发送和接收数据的方式都是通过调用操作系统。所有这些调用都有一个共同点:调用者必须不断地请求要传递的数据,因为没有客户端的请求,服务器无法做任何事情。但是,如果服务是音乐或视频呢?在媒体分发服务中,可能有一个或多个数据源和多个听众。只要用户正在收听或观看媒体,应用程序很可能需要任何已到达的数据。不断请求新数据是对应用程序时间和资源的浪费。SocketAPI并没有给程序员提供这样一种方式:“只要有数据需要处理,直接调用socket进行处理”。套接字程序是从数据稀缺的角度而不是数据丰富的角度来编写的。网络程序非常习惯于等待数据,所以使用单个系统调用如select()可以监听多个数据源而不会阻塞单个请求。基于套接字的程序的典型处理循环不是简单的read()、process()、read(),而是select()、read()、process()、select()。虽然向循环中添加单个系统调用可能看起来开销不大,但事实并非如此。每个系统调用都需要将参数编组并复制到内核中,导致系统阻塞调用进程并调度另一个进程。如果在调用select()时数据对调用者可用,则所有跨越用户/内核边界的工作都将被浪费,因为read()会立即返回数据。除非连续请求之间的时间很长,否则定期检查/读取/检查是一种浪费。为了克服这个问题,需要颠倒应用程序和操作系统之间的通信模型,提供一个允许内核直接调用程序的API。但各种尝试都没有得到广泛认可。开发套接字API时存在的操作系统通常是单处理器计算机上的单线程。如果内核将API反转,就会出现调用在哪个上下文中执行的问题。这种软件架构唯一流行的地方是没有用户和虚拟内存的嵌入式系统和网络路由器。虚拟内存问题使内核调用机制的实现变得复杂。分配给用户进程的内存是虚拟内存,而网络接口等设备使用的内存是物理内存。让内核将物理内存从设备映射到用户空间会破坏虚拟内存系统提供的基本保护。面对挑战的尝试和猜测为了克服套接字API中的性能问题,有几种不同的机制,有时在不同的操作系统上实现。低延迟网络应用程序对更关心延迟的程序几乎没有作用。对于等待网络事件的程序,唯一显着的改进是增加了一组程序可以等待的内核事件,实现了异步通知机制。例如kevents()是select()机制的扩展,它包括内核可能告诉程序的任何可能事件。在kevents()之前,用户程序可以在任何文件描述符上调用select(),这样程序就可以知道一组文件描述符中的任何一个是可读的、可写的,或者有错误。当一个程序在循环中编写并等待一组文件描述符时,例如从网络读取和写入磁盘——一个select()调用就足够了,但是一旦程序想要检查其他事件,例如定时器和信号,选择()是无用的。低延迟应用程序的问题是kevents()不传递数据,只是一个数据准备就绪的信号。下一个合乎逻辑的步骤是使用基于事件的API来传递数据。让应用程序两次跨越用户/内核边界以获得内核知道应用程序需要的数据是没有意义的。高带宽网络应用会因为复制数据而降低网络协议的性能。其中一种机制是零拷贝套接字。为了提高对高带宽更感兴趣的网络应用程序的速度,对操作系统进行了修改,以避免更多的数据复制。传统上,操作系统对系统接收到的每个数据包执行两个副本。第一次拷贝是由网络驱动程序从网络设备的内存到内核内存中执行的,第二次拷贝是在用户程序读取数据时由内核中的socket层执行的。系统收到的每条消息都需要复制,导致这些复制操作的成本很高。同样的道理,当一个程序要发送消息时,必须将每条发送消息的数据从用户程序复制到内核;然后将其复制到设备用于通过网络传输的缓冲区中。数据重复是系统性能的祸根,可以努力减少内核中的这种重复。内核避免数据复制的最简单方法是让设备驱动程序直接将数据复制到内核内存中或从内核内存中复制出来。在现代网络设备上,这是内存结构的结果。驱动程序和内核共享两个数据包描述符环(一个用于传输,一个用于接收),其中每个描述符都有一个指向内存的指针。网络设备驱动程序最初使用内核内存填充这些发送/接收环。当接收到数据时,设备在正确的接收描述符中设置一个标志,通常通过中断告诉内核数据正在等待。然后内核从接收描述符环中移除填充的缓冲区,并用一个新的缓冲区替换它以供设备填充。数据包在缓冲区中向上移动网络堆栈,直到它们到达套接字层,当用户程序调用read()时,它们在套接字层从内核复制。程序发送的数据由内核以类似的方式处理,内核缓冲区最终被添加到传输描述符环中,然后设置一个标志来告诉设备它可以将数据放入网络缓冲区。内核中的所有这些工作并没有解决最后的复制问题,仍然跨用户/内核边界安全地共享内存。内核不能让用户程序可以使用它的内存,因为它会失去对内存的控制。崩溃的用户程序会导致内核丢失大量可用内存,从而导致系统性能下降。跨内核/用户边界共享内存缓冲区也存在固有的安全问题。目前,关于如何使用套接字API实现更高带宽,还没有单一的答案。多宿主Web应用程序套接字API不仅在应用程序编程中存在性能问题,而且还减少了可能发生的通信类型。客户端/服务器模型本质上是一种点对点类型的通信。尽管服务器可以处理来自不同客户端组的请求,但是每个客户端对于一个请求或一组请求只有一个到单个服务器的连接。在每台计算机只有一个网络接口的世界中,这种模型非常有意义。客户端和服务器之间的连接由标识。由于服务通常有一个众所周知的目标端口(例如,HTTP的目标端口是80),IP地址是固定的,因此唯一可以轻松更改的值是源端口。在socketAPI诞生的时代,每一台不是路由器的计算机都只有一个网络接口,这意味着为了识别一个服务,客户端计算机需要一个目的地址和端口,而它本身只有一个源地址和端口。一台计算机以多种方式获得服务的想法太复杂,实施起来成本太高。鉴于这些限制,套接字API没有理由让程序员能够编写多宿主程序来管理对他们很重要的接口或连接。这些功能作为操作系统中路由软件的一部分来实现。程序最终访问它们的唯一方法是通过一组称为路由套接字的非标准内核API。在具有多个网络接口的系统上,不可能使用标准SocketAPI编写可以轻松实现多URL的应用程序。这样,在使用两个接口时,如果其中一个接口出现故障,或者如果数据包流经的主要路由出现故障,应用程序将不会失去与服务器的连接。尽管SCTP在协议级别集成了对多宿主的支持,但无法通过套接字API导出此支持。最初提供了一些临时系统调用,这是访问此功能的唯一方法。这可能是迄今为止唯一同时具有此功能的能力和用户需求的协议,但此API尚未跨多个操作系统标准化。下表列出了SCTP添加的API:sctp_bindx()将SCTP套接字绑定或取消绑定到地址列表sctp_connectx()连接具有多个目标地址的SCTP套接字sctp_generic_recvmsg()从对端接收数据sctp_generic_sendmsg()发送数据发送到对端sctp_getaddrlen()返回地址族的地址长度sctp_getassocid()返回指定套接字地址的关联IDctp_getpaddrs()<返回地址列表给调用者sctp_peeloff()将关联从一对多套接字分离到单独的ctp_getpaddrs()返回地址列表给调用者sctp_sendx()从SCTP套接字发送消息sctp_sendmsgx()从SCTP套接字发送消息作为send(),需要扩展才能在多宿主环境中工作。现在的问题是,SocketAPI无处不在,以至于很难改变现有的API集,以免混淆用户或现有应用程序。随着越来越多的网络接口被内置到系统中,编写利用多宿主的应用程序的能力将是必要的。很容易想象在智能手机中使用这项技术,它具有三个明显的网络接口:一个通过蜂窝网络的接口、一个WiFi接口,通常还有一个蓝牙接口。即使一个网络接口正常运行,应用程序也不应该失去连接。应用程序设计人员面临的问题是,他们希望他们的代码在从手机到笔记本电脑再到台式机等众多设备上几乎不需要更改或无需更改即可工作。通过正确定义的API,可以消除阻止这种情况发生的因素。只是由于套接字API“足够好”,这个需求并没有得到满足。总结支持高带宽、低延迟和多宿主是套接字API需要面对的挑战。LAN现在为10Gbps,对于许多应用程序,客户端/服务器式通信效率太低,无法有效地使用可用带宽。扩展套接字API支持的通信范例,以允许跨内核边界共享内存,从而允许使用低延迟机制将数据传递给应用程序。此外,由于具有多个活动接口的设备正在成为网络系统中的标准,多宿主支持也应该成为套接字API的一个特性。