流控制传输协议(Stream Control Transmission Protocol,SCTP)是一种可靠的传输协议,它在两个端点之间提供稳定、有序的数据传递服务(非常类似于 TCP),并且可以保护数据消息边界(例如 UDP)。然而,与 TCP 和 UDP 不同,SCTP 是通过多宿主(Multi-homing)和多流(Multi-streaming)功能提供这些收益的,这两种功能均可提高可用性。在本文中,我们将学习 Linux® 2.6 内核中 SCTP 的关键特性,并通过服务器和客户机的源代码了解该协议提供多流的能力。//INIT sctp协议 IETF
SCTP 是在 IP 网络上使用的一种可靠的通用传输层协议。尽管 SCTP 协议最初是为发送电话信号而设计的(RFC 2960),但带来了一个意外的收获:它通过借鉴 UDP 的优点解决了 TCP 的某些局限。SCTP 提供的特性使套接字初始化的可用性、可靠性和安全性都得以提高。(图 1 给出了 IP 堆栈的层次化架构。)
本文简要介绍了 Linux 2.6 内核中 SCTP 的概念,重点介绍了一些高级特性(例如多宿主和多流),并且给出了服务器和客户机的部分代码片断(并给出了一个可以获得更多代码的 URL),从而展示了这种协议提供多流的能力。
下面让我们开始介绍 IP 堆栈的内容。
Internet 协议套件被划分成几层;每层都提供特定功能,如图 1 所示。
自下而上:
- 链路层(link layer) 提供了通信介质的物理接口(例如以太网设备)。
- 网络层(network layer) 负责管理网络中的报文移动,具体来说就是确保报文都到达自己的目标(也称为路由)。
- 传输层(transport layer) 为应用层控制了报文在两台主机之间的流动。它还代表通信的应用程序端点,称为 端口(port)。
- 最后,应用层(application layer) 对通过套接字传递数据具有深刻的意义。这些数据可能包括通过简单邮件传输协议(Simple Mail Transport Protocol,SMTP)发送的 e-mail 消息,或通过超文本传输协议(Hypertext Transport Protocol,HTTP)呈现的 Web 页面。
所有应用层协议都使用套接字层作为与传输层协议之间的接口。Sockets API 是由 UC Berkeley 在 BSD UNIX® 操作系统上开发的。
在深入钻研 SCTP 之前,让我们首先简单回顾一下传统的传输层协议。
两种最流行的传输层协议是传输控制协议(TCP)和用户数据报协议(UDP):
- TCP 是一种可靠的协议,它可以确保有序地发送数据,并管理网络中的拥塞问题。
- UDP 是一种面向消息的协议,它不能确保有序地发送数据,也无法管理网络拥塞的问题。
然而,UDP 是一种快速协议,可以保护自己传输的消息的边界。
本文引出了另外一个选择:SCTP。它提供了像 TCP 一样可靠、有序地发送数据的功能,但却以像 UDP 一样面向消息的方式来进行操作,这可以保护消息边界。SCTP 还提供了几个高级特性:
- 多宿主(Multi-homing)
- 多流(Multi-streaming)
- 初始化保护(Initiation protection)
- 消息分帧(Message framing)
- 可配置的无序发送(Configurable unordered delivery)
- 平滑关闭(Graceful shutdown)
SCTP 相对于传统的传输层协议来说,两个重要的增强是终端主机的多宿主和多流功能。
多宿主 为应用程序提供了比 TCP 更高的可用性。多宿主主机就是一台具有多个网络接口的主机,因此可以通过多个 IP 地址来访问这台主机。在 TCP 中,连接(connection) 是指两个端点之间的一个通道(在这种情况下,就是两台主机的网络接口之间的一个套接字)。SCTP 引入了 联合(association) 的概念,它也是存在于两台主机之间,但可以使用每台主机上的多个接口进行协作。
图 2 阐述了 TCP 连接与 SCTP 联合之间的区别。
该图的上面部分是 TCP 连接,每个主机都只包含一个网络接口;连接是在每个客户机和服务器之间的单个接口之间建立的。在建立连接时,就被绑定到了每个接口上。
在该图的下面部分中,您可以看到这样一个架构:每台主机上都包含两个网络接口。通过独立网络提供了两条路径,一条是从接口 C0 到 S0,另外一条是从接口 C1 到 S1。在 SCTP 中,这两条路径可以合并到一个联合中。
SCTP 负责使用内嵌的 heartbeat 机制来监视联合的路径;在检测到一条路径失效时,协议就会通过另外一条路径来发送通信数据。应用程序甚至都不必知道发生了故障恢复。
故障转移也可以用于维护网络应用程序的连通性。例如,让我们来考虑一台包含一个无线 802.11 接口和一个以太网接口的笔记本的例子。当笔记本放到固定的位置上时,我们倾向于使用高速的以太网接口(在 SCTP 中称为 主地址(primary address));但是在这个连接丢失时(例如离开了固定位置),连接可迁移到无线接口上。在返回固定位置时,以太网连接会被重新检测到,通信就可以在这个接口上恢复。这是一种能提供更高的可用性和可靠性的强大机制。
从某种意义上来讲,SCTP 连接与 TCP 连接类似,不同之处只是 SCTP 能够在一个联合中支持多流机制。一个联合中的所有流都是独立的,但均与该联合相关(请参见图 3)。
每个流都给定了一个流编号,它被编码到 SCTP 报文中,通过联合在网络上传送。多流非常重要,因为阻塞的流(例如等待重传的流会导致报文的丢失)不会影响同一联合中的其他流。这个问题统称为 head-of-line blocking(对头阻塞)。TCP 很容易出现这类阻塞问题。
多流如何在传输数据时提供更好的响应性呢?例如,HTTP 协议会在相同套接字上共享控制和数据。Web 客户机从服务器上请求一个文件,服务器通过相同的连接将这个文件发回给客户机。多流的 HTTP 服务器可以提供更好的交互能力,因为在联合中各单独的流上可以处理多个请求。这种功能可以并行化响应,尽管速度不一定会更快,但可以同时加载 HTML 和图像映像,从而表现出更好的响应性。
多流处理是 SCTP 的一个重要特性,尤其是在协议的设计中考虑一些控制和数据的问题时更是如此。在 TCP 中,控制和数据通常都是通过相同的连接进行共享的,这可能会产生问题,因为控制报文可能会在数据报之后延时。如果控制和数据被划分成单独的流,控制数据就可以以一种更及时的方式进行处理,从而可以更好地利用可用资源。
TCP 和 SCTP 中对新连接的初始化是通过报文握手来完成的。在 TCP 中,这种机制称为 三次握手(three-way handshake)。客户机向服务器首先发送一个 SYN
报文(Synchronize 的简写),服务器使用一个 SYN-ACK
报文进行响应(Synchronize-Acknowledge)。最后,客户机使用一个 ACK
报文确认已接收到报文(请参见图 4)。
当恶意客户机使用虚假的源地址来伪造一个 IP 报文时,TCP 就会出现问题了,这会大量 TCP SYN
报文攻击服务器。服务器在接收 SYN
报文之前,要为连接分配资源,但是在大量产生 SYN
报文的情况下,最终会耗尽自己的资源,从而无法处理新的请求。这种情况就称为 服务拒绝(Denial of Service)(DoS)攻击。
SCTP 可以通过一种 4 次握手的机制并引入了 cookie 的概念来有效地防止这种攻击的产生。在 SCTP 中,客户机使用一个 INIT
报文发起一个连接。服务器使用一个 INIT-ACK
报文进行响应,其中就包括了 cookie(标识这个连接的惟一上下文)。客户机然后就使用一个 COOKIE-ECHO
报文进行响应,其中包含了服务器所发送的 cookie。现在,服务器要为这个连接分配资源,并通过向客户机发送一个 COOKIE-ACK
报文对其进行响应。
要解决使用这种 4 次握手机制解决延时数据移动的问题,SCTP 允许把数据包含到 COOKIE-ECHO
和 COOKIE-ACK
报文中。
使用消息分帧机制,就可以保护消息只在一个边界内通过 socket 进行通信;这意味着如果客户机向服务器先发送 100 个字节,然后又发送 50 个字节。那么服务器就会在两次读取操作中分别读取到 100 个字节和 50 个字节。UDP 也是这样进行操作,这对于面向消息的协议非常有益。
与此不同,TCP 是按照字节流的方式进行操作。如果没有分帧机制,一端接收到的数据可能比另外一端发送的数据多或少(这会将一次写操作划分成多次操作,或者将多次写操作合并到一个读操作中)。这种行为需要在 TCP 之上进行操作的面向消息的协议可以在应用层中提供数据缓冲和消息分帧机制(这可能是一项复杂的任务)。
SCTP 在数据传输中提供了消息分帧功能。当一端对一个套接字执行写操作时,可确保对等端读出的数据大小与此相同(请参见图 5)。
图 5. UDP/SCTP 中的消息分帧与面向字节流协议的比较
对于面向流的数据来说,例如音频和视频数据,可以没有分帧机制。
SCTP 中的消息的传输十分可靠,但未必是按照想要的次序来传输的。TCP 可以确保数据是按照次序发送的(考虑到 TCP 是一种流协议,这是一件好事)。UDP 无法确保有序地发送数据。但是如果需要,您也可以在 SCTP 中配置流来接受无序的消息。
这种特性在面向消息的协议中可能非常有用,因为其中的消息都是独立的,次序并不重要。另外,您可以在一个联合中按照逐个流配置无序发送。
TCP 和 SCTP 都是基于连接的协议,而 UDP 则是一种无连接的协议。TCP 和 SCTP 都需要在对等的两端建立和拆除连接。SCTP 与 TCP 中关闭连接的不同之处在于 TCP 中连接的删除是半关闭(half-close) 的。
图 6 给出了 TCP 和 SCTP 的关闭序列。
在 TCP 中,一端可以关闭自己这端的 socket(这样会导致发送一个 FIN
报文),但是仍然可以继续接收数据。FIN
说明这个端点不会再发送数据,但是在这一端关闭自己这端的套接字之前,它一直可以继续传输数据。应用程序很少使用这种半关闭状态,因此 SCTP 的设计者就选择放弃这种状态,并将其替换成了一个显式的终结序列。当一端关闭自己的套接字时(导致产生一个 SHUTDOWN
原语),对等的两端全部需要关闭,将来任何一端都不允许再进行数据的移动了。
现在您已经了解了 SCTP 的基本特性了,接下来让我们来看一下使用 C 编程语言编写的一个样例服务器和客户机,并展示 SCTP 的多流特性。
这个例子开发了一个服务器,它实现了一种形式的日期查询协议。这个传统的服务器会在连接上来的客户机上打印当前时间,但是对于 SCTP 来说,我们会在流 0 上打印本地时间,在流 1 上打印格林威治时间(GMT)。这个例子让我们可以展示如何使用这些 API 来开发流通信。
图 7 对整个过程进行了归纳,它不但从套接字 API 的角度展示了应用程序的流程,而且还从客户机和服务器的角度介绍了它们之间的关系。
这些应用程序是在 GNU/Linux 操作系统上开发的,其内核版本是 2.6.11,并且包含了 Linux Kernel SCTP 项目(lksctp)。其中非标准的 socket 函数是在 lksctp 工具包中提供的,这个工具包可以从 SourceForge 上获得。请参看 参考资料 中的链接。
清单 1 给出了这个多流 daytime 服务器的代码。为了可读性更好,我们在清单 1 中忽略了所有的错误检查,但是 这些展示错误检查机制的代码 与其他 SCTP 套接字扩展一样都可以通过给出的链接下载到。
int main() { int listenSock, connSock, ret; struct sockaddr_in servaddr; char buffer[MAX_BUFFER+1]; time_t currentTime; /* Create SCTP TCP-Style. Socket */ listenSock = <span class="boldcode"><strong>socket</strong></span>( AF_INET, SOCK_STREAM, IPPROTO_SCTP ); /* Accept connections from any interface */ bzero( (void *)&servaddr, sizeof(servaddr) ); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = <span class="boldcode"><strong>htonl</strong></span>( INADDR_ANY ); servaddr.sin_port = <span class="boldcode"><strong>htons</strong></span>(MY_PORT_NUM); /* Bind to the wildcard address (all) and MY_PORT_NUM */ ret = <span class="boldcode"><strong>bind</strong></span>( listenSock, (struct sockaddr *)&servaddr, sizeof(servaddr) ); /* Place the server socket into the listening state */ <span class="boldcode"><strong>listen</strong></span>( listenSock, 5 ); /* Server loop... */ while( 1 ) { /* Await a new client connection */ connSock = <span class="boldcode"><strong>accept</strong></span>( listenSock, (struct sockaddr *)NULL, (int *)NULL ); /* New client socket has connected */ /* Grab the current time */ currentTime = time(NULL); /* Send local time on stream 0 (local time stream) */ snprintf( buffer, MAX_BUFFER, "%s\n", ctime(¤tTime) ); ret = <span class="boldcode"><strong>sctp_sendmsg</strong></span>( connSock, (void *)buffer, (size_t)strlen(buffer), NULL, 0, 0, 0, LOCALTIME_STREAM, 0, 0 ); /* Send GMT on stream 1 (GMT stream) */ snprintf( buffer, MAX_BUFFER, "%s\n", asctime( gmtime( ¤tTime ) ) ); ret = <span class="boldcode"><strong>sctp_sendmsg</strong></span>( connSock, (void *)buffer, (size_t)strlen(buffer), NULL, 0, 0, 0, GMT_STREAM, 0, 0 ); /* Close the client connection */ <span class="boldcode"><strong>close</strong></span>( connSock ); } return 0; } |
清单 1 中的服务器首先创建服务器的套接字(使用 IPPROTO_SCTP
来创建一个 SCTP 的一对一的套接字)。然后创建一个 sockaddr
结构,指定这个连接可以从任何本地接口上创建(使用通配符地址 INADDR_ANY
)。我们使用 bind
调用将这个 sockaddr
结构绑定到 socket 上,然后将服务器套接字设置成监听状态。现在就可以接收到达的连接了。
注意 SCTP 使用了很多与 TCP 和 UDP 相同的套接字 API。在 lksctp 开发工具中还提供了其他一些 API 函数(请参看 参考资料)。
在服务器的循环中,一直等待新客户机的连接请求。在从 accept
函数返回时,会使用 connSock
socket 标识新客户机的连接。我们使用 time
函数来获取当前时间,然后使用 snprintf
将其转换成字符串。使用 sctp_sendmsg
函数(一个非标准的 socket 调用),可以通过指定特定的流程(LOCALTIME_STREAM
,将这个字符串发送给客户机。当发送本地时间字符串之后,我们将使用 GMT 表示的当前时间转换成一个字符串,然后将其发送到流 GMT_STREAM
上。
现在,日期查询服务器已经完成了自己的职责,因此我们就可以关闭这个 socket,然后等待一个新的客户机连接。一切都非常简单,对吗?现在让我们来看一下日期查询客户机是如何处理多流的。
多流客户机如清单 2 所示。
int main() { int connSock, in, i, flags; struct sockaddr_in servaddr; struct sctp_sndrcvinfo sndrcvinfo; struct sctp_event_subscribe events; char buffer[MAX_BUFFER+1]; /* Create an SCTP TCP-Style. Socket */ connSock = <span class="boldcode"><strong>socket</strong></span>( AF_INET, SOCK_STREAM, IPPROTO_SCTP ); /* Specify the peer endpoint to which we'll connect */ bzero( (void *)&servaddr, sizeof(servaddr) ); servaddr.sin_family = AF_INET; servaddr.sin_port = <span class="boldcode"><strong>htons</strong></span>(MY_PORT_NUM); servaddr.sin_addr.s_addr = <span class="boldcode"><strong>inet_addr</strong></span>( "127.0.0.1" ); /* Connect to the server */ <span class="boldcode"><strong>connect</strong></span>( connSock, (struct sockaddr *)&servaddr, sizeof(servaddr) ); /* Enable receipt of SCTP Snd/Rcv Data via sctp_recvmsg */ memset( (void *)&events, 0, sizeof(events) ); events.sctp_data_io_event = 1; <span class="boldcode"><strong>setsockopt</strong></span>( connSock, SOL_SCTP, SCTP_EVENTS, (const void *)&events, sizeof(events) ); /* Expect two messages from the peer */ for (i = 0 ; i < 2 ; i++) { in = <span class="boldcode"><strong>sctp_recvmsg</strong></span>( connSock, (void *)buffer, sizeof(buffer), (struct sockaddr *)NULL, 0, &sndrcvinfo, &flags ); /* Null terminate the incoming string */ buffer[in] = 0; if (sndrcvinfo.sinfo_stream == LOCALTIME_STREAM) { printf("(Local) %s\n", buffer); } else if (sndrcvinfo.sinfo_stream == GMT_STREAM) { printf("(GMT ) %s\n", buffer); } } /* Close our socket and exit */ <span class="boldcode"><strong>close</strong></span>(connSock); return 0; } |
在客户机中,我们首先创建了一个 SCTP 套接字,然后创建了一个 sockaddr
结构,其中包含了将要连接的端点。connect
函数然后建立一个到服务器的连接。要获取消息的流编号,SCTP 需要启用套接字选项 sctp_data_io_event
。
通过启用这个选项,我们就可以通过 sctp_recvmsg
API 函数接收一条消息,我们还接收到一个包含流编号的 sctp_sndrcvinfo
结构。这个编号让我们可以区分开流 0(本地时间)和流 1(GMT)的消息。
SCTP 是一个相当新的协议,它在 2000 年 10 月份才成为一个 RFC 规范。从那以后,它开始进入所有的主流操作系统,包括 GNU/Linux、BSD 和 Solaris。在 Microsoft® Windows® 操作系统上也有第三方的商业包可以使用。
在获得高可用性的同时,应用程序也已经开始使用 SCTP 作为自己的主要传输机制。诸如 FTP 和 HTTP 之类的传统应用程序已经在 SCTP 的特性基础上进行了构建。其他一些协议也正在开始使用 SCTP,例如会话初始化协议(Session Initiation Protocol,SIP)和通用通道信号系统 7(SS7)。在商业领域中,您可以在 Cisco 的 IOS 中找到 SCTP 的影子。
随着 SCTP 被吸纳到 2.6 版本的 Linux 内核中,现在我们可以构建并部署高可用性、高可靠性的网络应用程序。作为一种基于 IP 的协议,SCTP 不但可以无缝地替换 TCP 和 UDP,而且扩展了很多新服务,例如多宿主、多流,并且对安全性也有了很大的提高。现在您已经了解了 SCTP 的一些高级特性,并且探索了它的一些其他功能。Linux Kernel SCTP 项目(lksctp)提供了可以为您提供辅助的 API 扩展和文档