流控制传输协议简介
大多数编写过网络软件的人都熟悉 TCP 和 UDP 协议。这些协议用于连接分布式应用程序,并允许消息在它们之间流动。这些协议已成功用于构建我们所知的互联网应用程序:电子邮件、HTTP、名称服务等等。但是,这些协议已经有 20 多年的历史了,随着时间的推移,它们的一些缺陷已经变得众所周知。尽管已经有很多尝试设计 IP 层之上的新通用传输协议,但到目前为止,只有一种协议获得了 IETF 的认可:SCTP(流控制传输协议)。SCTP 背后的核心动机是提供比 TCP 或 UDP 更可靠、更健壮的协议,该协议可以利用多宿主等功能。
SCTP 并不是对 TCP 或 UDP 的彻底背离。它借鉴了两者,但与 TCP 最为相似。它是一种可靠的面向会话的协议,就像 TCP 一样。它添加了新功能和选项,并允许对数据包的传输进行更精细的控制。在除“边缘”情况外的所有情况下,它都可以用作 TCP 的直接替代品。这意味着 TCP 应用程序通常可以轻松移植到 SCTP。当然,要真正受益于 SCTP 的新功能,您需要使用 SCTP 的附加 API 调用。
SCTP 的第一个附加功能是更好地支持多宿主设备——即具有多个网络接口的计算机。曾经,这仅意味着连接互联网不同部分的路由器和网桥,但现在即使是网络边缘的计算机也可以是多宿主的。大多数笔记本电脑都内置了以太网卡和 Wi-Fi 卡,许多还配备了蓝牙卡(通过蓝牙 PPP 堆栈支持 IP)。一些笔记本电脑现在配备了 WiMAX 卡,甚至可以通过红外端口运行 IP!因此,标准的笔记本电脑至少是双宿主的,最多可能有五个不同的 IP 网络接口。
TCP 和 UDP 仅允许使用一个或所有接口。但是,如果您将笔记本电脑作为对等方运行在文件共享服务中,该怎么办?使用蓝牙和红外接口可能很傻。WiMAX 传输大量数据可能非常昂贵。但是,同时使用以太网和 Wi-Fi 接口是有意义的。SCTP 可以支持这种接口的选择性选择。一些实现甚至可以动态添加和删除接口,因此当您拔下笔记本电脑并离开家时,如果需要,应用程序可以切换到 WiMAX 接口。
第二个主要新功能是多流——也就是说,一个“关联”(从 TCP 中的“连接”重命名而来)可以支持多个数据流。不再需要打开多个套接字;相反,单个套接字可以用于到连接主机的多个流。几个 TCP 应用程序可以从中受益。例如,FTP(主要的 file transfer protocol)使用两个流:一个在端口 21 上用于控制消息,另一个在端口 20 上用于数据。这给防火墙带来了问题。客户端可以通过防火墙连接到服务器,但由于防火墙,服务器无法连接到客户端进行数据传输。FTP 协议必须扩展以允许“被动”连接来克服这个问题。在 SCTP 下,将不需要这样的扩展——只需在客户端建立的关联中的单独流上发送数据即可。
X Window 系统也使用多个端口上的多个套接字。虽然不常见,但一台计算机可以有多个显示设备。通常,第一个在端口 6000 上,第二个在端口 6001 上,依此类推。在 SCTP 下,这些都可以是单个关联上的单独流。HTML 文档通常包含对图像文件的嵌入式引用,并且要正确显示页面,需要下载原始页面和所有这些图像(或嵌入式框架)。HTTP 最初为每个下载的 URL 使用单独的 TCP 连接,这既昂贵又耗时。HTTP 1.1 引入了“持久连接”,以便可以将单个套接字重用于所有这些顺序下载。在 SCTP 下,可以在单个关联上的单独流中并发下载单独的图像。
SCTP 多流还有更微妙的用途。MPEG 电影由不同类型的帧组成:I 帧、P 帧和 B 帧。I 帧编码完整图像,其他两种类型测量帧之间的差异。通常,每十帧有一个 I 帧,其他帧从这些帧“预测”而来。I 帧的交付至关重要,但 P 帧和 B 帧则不然。虽然 SCTP 不是作为服务质量协议设计的,但它确实允许在关联内的不同流上使用不同的交付参数,以便可以更可靠地交付 I 帧。
SCTP 还有更多功能,例如
TCP 是面向字节的协议,而 UDP 是面向消息的协议。大多数应用程序是面向消息的,使用 TCP 的应用程序必须费尽周折,例如将消息长度作为第一个参数发送。SCTP 是面向消息的,因此不需要这种技巧。
单个套接字可以支持多个关联——也就是说,一台计算机可以使用单个套接字与多台计算机通信。这不是多播,但在对等网络情况下可能很有用。
SCTP 没有“带外”消息,但可以将大量事件交错到单个关联上,以便应用程序可以监视关联的状态(例如,当另一端向关联添加另一个接口时)。
套接字选项的范围大于 TCP 或 UDP。这些选项还可以用于控制单个关联或单个关联内的各个流。例如,与另一流上的消息相比,一个流上的消息可以被赋予更长的生存时间,从而增加其交付的可能性。
SCTP 网站 (www.sctp.org) 列出了 SCTP 的实现。有 BSD 和 Windows 的实现,自 2001 年以来,sourceforge.net/projects/lksctp 上一直有一个 Linux 内核项目 (sourceforge.net/projects/lksctp)。目前,SCTP 尚未包含在任何 Microsoft 版本中,因此在 Windows 上运行的应用程序需要安装可用的堆栈之一。
SCTP 作为实验性网络协议包含在 Linux 内核中。SCTP 通常构建为模块。可能需要使用以下命令加载模块modprobe sctp。要构建用户应用程序,您可能需要安装 SCTP 工具——在 Fedora Core 6 中,这些工具位于 RPM 包 lksctp-tools-1.0.6-1.fc6.i386.rpm 和 lksctp-tools-devel-1.0.6-1.fc6.i386.rpm 中。在 Fedora Core 6 上,我还必须添加从 /usr/lib/libsctp.so 到 /usr/lib/libsctp.so.1 的符号链接。
lksctp-tools 包包含运行 SCTP 应用程序的库。它还包含一个名为 checksctp 的程序,该程序告诉您您的内核是否支持 SCTP。当您运行此程序时,它会打印“SCTP supported”或错误消息。
devel 包包含 sctp.h 头文件,因此您可以编译和构建自己的应用程序,以及 SCTP 函数调用的手册页。
大多数防火墙都可以配置为处理 SCTP 数据包,但每个防火墙的文档可能没有明确提及 SCTP。例如,iptables 的手册页说,“[规则中]指定的协议可以是 tcp、udp、icmp 或 all...”。但是,它接着说,“也允许使用 /etc/protocols 中的协议名称”,在该文件中,我们发现协议 132 是 sctp。因此,可以将 SCTP 的规则添加到 iptables 中,方式与 TCP 和 UDP 规则相同。
例如,接受到端口 13 的 SCTP 连接的 iptables 规则将是
-A INPUT -p sctp -m sctp -i eth0 --dport 13 -j ACCEPT
Webmin 是一种流行的管理工具,用于管理 iptables 规则等内容。不幸的是,截至 1.340 版本,它无法接受此规则,因为它被硬连线为仅接受 TCP 和 UDP 的端口号,而没有意识到 SCTP 也使用端口号。这样的规则需要手动输入到 iptables 配置文件 /etc/sysconfig/iptables 中。在我记录错误报告后,这将在更高版本的 Webmin 中修复,但在其他工具中可能会出现类似的问题。
与 TCP 和 UDP 一样,SCTP 为应用程序提供套接字 API。服务器创建一个绑定到端口的套接字,然后使用它来接受来自客户端的连接。客户端也创建一个套接字,然后连接到服务器。然后两者都使用套接字文件描述符来读取和写入消息。SCTP 不是 TCP 的超集。然而,当限制为类似于 TCP 的连接样式时,存在足够的相似性,以至于 SCTP 套接字通常可以用作 TCP 套接字的直接替代品。以这种方式使用时,SCTP 套接字称为一对一套接字,因为它们只是将一个主机连接到单个其他主机。
要创建 TCP 套接字,请使用系统调用
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
这将创建一个 IPv4 套接字。要创建 IPv6 套接字,请将第一个参数替换为 AF_INET6。最后一个参数通常给出为零,意思是“使用族中唯一的协议值”。最好显式使用 IPPROTO_TCP,因为 SCTP 引入了另一个可能的值。
要创建 SCTP 一对一套接字,只需将 IPPROTO_TCP 替换为 IPPROTO_SCTP
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP)
就可以了(在很多情况下)!客户端或服务器现在正在使用 SCTP 协议而不是 TCP 进行通信。
为了看到实际效果,清单 1 (echo_client.c) 和清单 2 (echo_server.c) 给出了一个简单的回显客户端和服务器,其中服务器返回客户端连接到它时发送给它的字符串。客户端和服务器中只需要更改上面的一行(还需要一个额外的包含文件 sctp.h)。
清单 1. echo_client.c
#define USE_SCTP #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #ifdef USE_SCTP #include <netinet/sctp.h> #endif #define SIZE 1024 char buf[SIZE]; char *msg = "hello\n"; #define ECHO_PORT 2013 int main(int argc, char *argv[]) { int sockfd; int nread; struct sockaddr_in serv_addr; if (argc != 2) { fprintf(stderr, "usage: %s IPaddr\n", argv[0]); exit(1); } /* create endpoint using TCP or SCTP */ sockfd = socket(AF_INET, SOCK_STREAM, #ifdef USE_SCTP IPPROTO_SCTP #else IPPROTO_TCP #endif ); if (sockfd < 0) { perror("socket creation failed"); exit(2); } /* connect to server */ serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(ECHO_PORT); if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { perror("connect to server failed"); exit(3); } /* write msg to server */ write(sockfd, msg, strlen(msg) + 1); /* read the reply back */ nread = read(sockfd, buf, SIZE); /* write reply to stdout */ write(1, buf, nread); /* exit gracefully */ close(sockfd); exit(0); }
清单 2. echo_server.c
#define USE_SCTP #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #ifdef USE_SCTP #include <netinet/sctp.h> #endif #define SIZE 1024 char buf[SIZE]; #define ECHO_PORT 2013 int main(int argc, char *argv[]) { int sockfd, client_sockfd; int nread, len; struct sockaddr_in serv_addr, client_addr; /* create endpoint using TCP or SCTP */ sockfd = socket(AF_INET, SOCK_STREAM, #ifdef USE_SCTP IPPROTO_SCTP #else IPPROTO_TCP #endif ); if (sockfd < 0) { perror("socket creation failed"); exit(2); } /* bind address */ serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(ECHO_PORT); if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { perror("bind failed"); exit(3); } /* specify queue length */ listen(sockfd, 5); for (;;) { len = sizeof(client_addr); /* get a connection from client */ client_sockfd = accept(sockfd, (struct sockaddr *) &client_addr, &len); if (client_sockfd == -1) { perror("accept failed"); continue; } /* transfer data */ nread = read(client_sockfd, buf, SIZE); /* write to stdout */ write(1, buf, nread); /* and echo it back to client */ write(client_sockfd, buf, nread); /* no more for this client */ close(client_sockfd); } }
可以使用常用的 C 编译命令来创建目标模块和可执行文件。如果程序使用 SCTP 特定的函数(清单 1 和 2 中的程序没有),您还需要链接到 SCTP 库
cc -o echo_client echo_client.c -lsctp
将运行在 TCP 上的应用程序迁移到 SCTP 是否值得?缺点是 SCTP 不如 TCP 受支持,工具有时不了解 SCTP,并且 API 仍在发展中。另一方面,它受益于 20 年来在实践中看到 TCP 和 UDP 应用程序的经验。例如,SCTP 在设计上可以防御 SYN 攻击,并且该协议没有已知的安全漏洞。SCTP 还将在需要时自动利用多宿主。如果数据包由于拥塞等原因而丢失,SCTP 将使用不同的接口来尝试避免丢失,这可能会提高吞吐量。
在前一节中,我讨论了如何更改客户端或服务器的源代码以使用 SCTP 而不是 TCP。sctp-tools 包包含一个名为 withsctp 的程序,它本质上对二进制代码执行相同的操作。此程序充当 TCP 应用程序的包装器,以将其转换为 SCTP 应用程序。它首先保存“真实” socket() 函数调用的地址,然后将其自己的 socket() 版本插入到加载库路径中。这个新版本的 socket() 只是获取函数调用的参数,将第三个参数从 IPPROTO_TCP 更改为 IPPROTO_SCTP,并调用“真实” socket() 函数。
例如,xinetd 守护程序可以运行一组 TCP 和 UDP 服务。这些服务是 /etc/xinetd.d 目录中列出的服务,它们具有enable = yes或disable = no。所有 TCP 服务都可以通过以下方式在 SCTP 上运行
withsctp xinetd
xinetd 运行的最简单的服务之一是 daytime。该服务接受连接并返回当前日期的 ASCII 字符串。快速 Google 搜索会找到许多客户端的源代码,但最简单的方法是运行 Telnet
telnet <host-name> 13
如果您有 daytime 作为 SCTP 服务而不是 TCP 服务运行,请使用 withsctp 连接到它
withsctp telnet <host-name> 13
这是一种快速测试 TCP 服务是否可以转换为 SCTP 的方法。
TCP 是一种面向字节的协议——也就是说,您写入字节并读取字节。UNIX 系统调用 read() 和 write() 通常用于此。TCP 也有 send()/recv(),它们有一个额外的标志参数,但这些不会改变字节传输模型。
另一方面,SCTP 是面向消息的,更像 UDP。大多数互联网应用程序的通信都具有消息结构,而不仅仅是字节序列。例如,单个 HTTP 请求具有标头和正文部分,甚至标头部分也由任意数量的行组成。发送者必须将各个部分组合成单个请求,而此类消息的接收者必须将其解析回其组成消息。少数协议仅面向字节(例如,FTP 的文件传输模式),但这些是少数。
SCTP 使使用基于消息的结构变得容易——在限制范围内。write() 调用写入完整消息。相应的 read() 读取此完整消息。因此,要通过 SCTP 发送 HTTP 标头,您可以对每一行执行写入操作,然后写入空行。接收者会将每一行作为单独的消息读取,并在读取空行后停止。无需在处理每一行之前将接收到的字节解析为一组行。请注意,如果原始 TCP 应用程序已经使用了一系列写入,然后是单个读取,期望 TCP 连接所有消息,则需要修改应用程序以将每个写入与相应的读取语句匹配。
需要注意的是大消息。想要利用这些消息传递功能的应用程序在发送大消息(例如 32KB 或更大)时必须小心。要发送消息,您不仅仅是将指针传递给堆栈上的数据,您实际上是在网络上传输该数据。这意味着将其放入发送方的缓冲区中,通过中间节点中的缓冲区传递,最后将其传递到读取应用程序中的缓冲区。所有这些缓冲区都有不能超过的限制。
例如,假设发送者使用大小由套接字选项 SO_SNDBUF 设置的缓冲区。尝试写入大于该缓冲区大小的消息将失败并返回 -1。它的大小很慷慨,通常约为 64KB。可以使用以下命令更改它setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &val, &val_len),其中 val 是一个整数变量,包含您要将缓冲区设置为的长度。但是,然后可能会出现其他限制。从发送者到接收者的路由上的每个主机都将具有它将传递的最大数据包大小。路径最大传输单元 (PMTU) 是所有这些中的最小值。如果消息(加上任何 IP 和 SCTP 标头)大于 PMTU,则它将被分片并分段交付。发送者可以通过设置 SCTP 选项 SCTP_DISABLE_FRAGMENTS 来防止这种情况,以便消息作为单个实体交付或根本不交付,但这通常只会减小最大可能的消息大小。
消息的接收者也有一个接收缓冲区大小,该大小由套接字选项 SO_RCVBUF 控制。它不会接收大于此大小的消息——如有必要,会对其进行分片。接收方的主要问题是如何处理分片消息。系统调用 read() 和 recv() 不包含有关消息边界的任何信息,因为它们是面向字节的。幸运的是,SCTP 有一个新的系统调用 sctp_recvmsg(),它在整数参数中返回有关读取的状态信息。特别是,如果设置了 MSG_EOR 位(消息记录结束),则消息读取已完成。如果未设置,则消息已被分片,需要读取更多消息。读取器可以使用它来构建完整的消息,然后再对其进行处理。
清单 3 显示了如何使用 sctp_recvmsg() 调用来接收分片消息并将它们构建成完整消息。它通过读取消息的每个部分(当它到达时)并将其添加到已接收的部分来实现。当带有 MSG_EOR 位(在标志中设置)的部分到达时,消息就完成了,可以返回给读取应用程序。
清单 3. read_sctp_msg.c
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <netinet/sctp.h> /* call by nread = read_sctp_msg(sockfd, &msg) */ int read_sctp_msg(int sockfd, uint8_t **p_msg) { int rcv_buf_size; int rcv_buf_size_len = sizeof(rcv_buf_size); uint8_t *buf; struct sockaddr_in peeraddr; int peer_len = sizeof(peeraddr); struct sctp_sndrcvinfo sri; int total_read = 0; *p_msg = NULL; /* default fail value */ if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, &rcv_buf_size_len) == -1) { return -1; } if ((buf = malloc(rcv_buf_size)) == NULL) { return -1; } while (1) { int nread; int flags; nread = sctp_recvmsg(sockfd, buf+total_read,rcv_buf_size, (struct sockaddr *) &peeraddr,&peer_len, &sri, &flags); if (nread < 0) { return nread; } total_read += nread; if (flags & MSG_EOR) { /* trim the buf and return msg */ printf("Trimming buf to %d\n", total_read); *p_msg = realloc(buf, total_read); return total_read; } buf = realloc(buf, total_read + rcv_buf_size); } /* error to get here? */ free(buf); return -1; }
SCTP 开箱即用地完全支持 IPv6 以及 IPv4。您只需使用 IPv6 套接字地址而不是 IPv4 套接字地址。如果您创建 IPv4 套接字,SCTP 将仅处理 IPv4 地址。但是,如果您创建 IPv6 套接字,SCTP 将同时处理 IPv4 和 IPv6 地址。
资源
SCTP 的主要站点(包含指向 SCTP 的 RFC 和 Internet 草案的指针):www.sctp.org
Linux 内核项目主页:https://lists.sourceforge.net/lists/listinfo/lksctp-developers。
流控制传输协议 (SCTP):参考指南,作者:Randall Stewart 和 Qiaobing Xie,Addison-Wesley 出版社。
Unix 网络编程(第 1 卷,第 3 版),作者:W. Richard Stevens 等,有几章关于 SCTP 的内容,尽管其中一些已过时。
Jan Newmarch 是莫纳什大学的名誉高级研究员。他从内核 0.98 开始使用 Linux。他撰写了四本书和许多论文,并讲授了许多技术主题的课程,最近六年专注于网络编程。他的网站是 jan.newmarch.name。