流控制传输协议的多重关联
在前两篇文章 [LJ 九月和十月刊] 中,我介绍了 SCTP 的基础知识,如何使用 SCTP 替代 TCP,以及如何使用 SCTP 在单个关联中处理多个流。在最后一篇文章中,我将探讨单个端点如何处理多个关联(与其他端点的连接)。首先,我将解释 SCTP 如何通过事件提供有关正在发生的事情的额外信息。
当发生“有趣”的事情时,SCTP 协议栈可以生成事件。默认情况下,除了数据事件之外,所有事件生成都处于关闭状态。在上一篇文章中,我讨论了 SCTP 调用 sctp_rcvmsg()。默认情况下,这只会返回读取的数据。但是,我也想知道数据来自哪个流,为此,我必须启用 data_io_event 事件,以便 SCTP 协议栈填充 sctp_sndrcvinfo 结构,该结构具有 sinfo_stream 字段。事件在 sctp_event_subscribe 结构中列出
struct sctp_event_subscribe { uint8_t sctp_data_io_event; uint8_t sctp_association_event; uint8_t sctp_address_event; uint8_t sctp_send_failure_event; uint8_t sctp_peer_error_event; uint8_t sctp_shutdown_event; uint8_t sctp_partial_delivery_event; uint8_t sctp_adaptation_layer_event; uint8_t sctp_authentication_event; };
应用程序将其感兴趣的事件的字段设置为 1,将其余事件的字段设置为 0。然后,它使用 SCTP_EVENTS 调用 setsockopt()。例如
memset(&event, 0, sizeof(event)); event.sctp_data_io_event = 1; event.sctp_association_event = 1; setsockopt(fd, IPPROTO_SCTP, SCTP_EVENTS, &event, sizeof(event));
当完成读取(使用 sctp_recvmsg 或类似方法)时,事件与“普通”数据一起内联传递。如果应用程序启用事件,则读取将包含事件和数据的混合。然后,应用程序需要检查每次读取,以查看它是要处理的事件还是数据。这非常简单。如果 sctp_recvmsg() 调用中的 flags 字段设置了 MSG_NOTIFICATION 位,则读取的消息包含事件;否则,它像以前一样包含数据。伪代码如下
nread = sctp_rcvmsg(..., msg, ..., &flags); if (flags & MSG_NOTIFICATION) handle_event(msg); else handle_data(msg, nread);
事件可用于告知以下信息:新关联是否已启动或旧关联是否已终止;对等方是否已更改状态,例如,某个接口变得不可用或新接口变得可用;发送是否失败、是否发生远程错误或远程对等方是否已关闭;部分交付是否失败;以及身份验证信息是否可用。
如果在事件缓冲区中接收到事件,则首先必须找到其类型,然后可以将缓冲区强制转换为适合该事件的类型。例如,处理关闭事件的代码是
void handle_event(void *buf) { union sctp_notification *notification; struct sn_header *head; notification = buf; switch(notification->sn_header.sn_type) { case SCTP_SHUTDOWN_EVENT: { struct sctp_shutdown_event *shut; shut = (struct sctp_shutdown_event *) buf; printf("Shutdown on assoc id %d\n", shut->sse_assoc_id); break; } default: printf("Unhandled event type %d\n", notification->sn_header.sn_type); } }
一个套接字可以支持多个关联。如果您关闭一个套接字,它将关闭所有关联!有时希望仅关闭单个关联,而不关闭套接字,以便套接字可以继续用于其他关联。
SCTP 可以中止关联或正常关闭它。正常关闭将确保在关闭之前正确传递任何排队的消息,而中止则不会这样做。这些都可以通过将 sctp_sndrcvinfo 结构中的 sinfo_flags 设置为适当的值来发出信号。正常关闭通过设置 shutdown 标志并写入消息(不带数据)来发出信号
sinfo.sinfo_flags = SCTP_EOF; sctp_send(..., &sinfo, ...);
如果读者启用了该事件类型,则会向其发送 sctp_shutdown_event。上面显示了处理此类事件的代码。但这只能在多对一套接字上完成。对于一对一套接字,您仅限于使用 close()。
许多处理关联的调用都将关联 ID 作为参数。在 TCP 中,连接有效地由源和目标端点 IP 地址对表示,而在 SCTP 中,源和目标都可以是多宿主的,因此它们将由源地址集和目标地址集表示。对于多对一套接字,源地址可能由多个关联共享,因此我需要目标地址来正确标识关联。对于单个关联,这些目标地址都属于单个端点计算机。sctp_opt_info() 是 getsockopt() 的 SCTP 变体,用于从地址查找关联。我不能简单地使用 getsockopt() 的原因是,我需要传入一个套接字地址,并且返回值包括关联值。并非所有 getsockopt() 实现都支持这种输入/输出语义。代码如下
sctp_assoc_t get_associd(int sockfd, struct sockaddr *sa, socklen_t salen) { struct sctp_paddrinfo sp; int sz; sz = sizeof(struct sctp_paddrinfo); bzero(&sp, sz); memcpy(&sp.spinfo_address, sa, salen); if (sctp_opt_info(sockfd, 0, SCTP_GET_PEER_ADDR_INFO, &sp, &sz) == -1) perror("get assoc"); return (sp.spinfo_assoc_id); }
请注意,W. Richard Stevens 等人的 Unix 网络编程(第 1 卷,第 3 版)给出了不同的代码:自该书编写以来,规范已更改,上述是现在首选的方式(并且 Stevens 的代码在 Linux 下也无法工作)。
服务器可以通过多种方式处理多个客户端:TCP 服务器可以使用单个服务器套接字来侦听客户端并按顺序处理它们,或者它可以为每个新的客户端连接 fork 出一个单独的进程或线程,或者它可以有许多套接字并在它们之间进行轮询或选择。UDP 服务器通常不保留客户端状态,并将每条消息作为一个单独的实体完整地处理。SCTP 提供了另一种变体,大致介于 TCP 和 UDP 之间。
一个 SCTP 套接字可以同时处理与多个端点的多个长期存在的关联。它通过为每个关联维护一个关联 ID 来支持 TCP 的“面向连接”语义。另一方面,它类似于 UDP,因为每次读取通常都会返回来自客户端的完整消息。SCTP 应用程序通过使用我在前两篇文章中讨论的一对一套接字来使用 TCP 模型。并且,它使用一对多模型,该模型更像 UDP,通过使用一对多套接字。当您创建套接字时,您指定它是一对一还是多对一。在本系列的第一篇文章中,我通过调用创建了一个一对一套接字
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP)
要创建多对一套接字,我只需更改第二个参数
sockfd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP)
TCP 服务器通过本质上使用并发读取来同时处理多个连接。这是通过使用多个进程、线程或在许多套接字之间进行轮询/选择来完成的。UDP 服务器通常使用单个读取循环,处理到达的每条消息。SCTP 多对一服务器看起来像 UDP 服务器:它将绑定一个套接字并监听。然后,它不是阻塞在 accept() 上(这将返回一个新的点对点套接字),而是阻塞在 sctp_rcvmsg() 上,这将返回来自新关联或现有关联的消息。此类服务器的伪代码如下
sockfd = socket(...); bind(sockfd, ...); listen(sockfd, ...); while (true) { nread = sctp_rcvmsg(sockfd, ..., buf, ..., &info); assoc_id = sinfo.sinfo_assoc_id; stream = sinfo.sinfo_stream; handle_message(assoc_id, stream, buf, nread); }
客户端也可以使用多对一套接字模型。在绑定到端口(可能是临时端口)之后,它可以使用单个套接字连接到许多其他端点,并使用此单个套接字向任何端点发送消息。它甚至可以取消显式的连接操作,而只需开始向新端点发送消息(如果不存在现有关联,则会完成隐式连接)。
一对一套接字遵循 TCP 模型;多对一套接字遵循 UDP 模型。是否可以同时拥有两者?是的,在某种程度上是可以的。例如,您可能有一个服务器,您可以通过两种模式与之通信:普通用户和超级用户。来自普通用户的消息可以以 UDP 样式处理,读取并仅响应,而超级用户连接可能需要区别对待。SCTP 允许将多对一套接字上的连接“分离”并变成一对一套接字。然后,可以以 TCP 样式处理此一对一套接字,而所有其他关联仍保留在多对一套接字上。
在本节中,我将讨论如何使用 SCTP 构建简单聊天服务器的简单示例。这并非旨在成为现有众多聊天系统的竞争对手,而是为了展示 SCTP 的一些功能。
聊天服务器必须侦听可能来自瞬态客户端组的消息。当从任何一个客户端收到消息时,它应该将消息发送回所有其他客户端。
UDP 可能是这里的选择:服务器可以简单地等待在读取循环中,等待消息到达。但是,要将它们发送回去,它需要维护客户端列表,这有点困难。客户端会来来往往,因此需要某种“活跃度”测试来保持列表最新。
SCTP 是更好的选择:它也可以坐在读取循环中,但它也维护关联列表,并且更好的是,通过向对等方发送心跳消息来保持列表最新。列表管理由 SCTP 处理。
TCP 也可能是一种选择:每个客户端都将在服务器上启动一个新的客户端套接字。然后,服务器将需要维护客户端套接字列表,并在它们之间执行轮询/选择,以查看是否有人正在发送消息。同样,SCTP 是更好的选择:在多对一模式下,它将仅保留一个套接字,并且不需要轮询/选择循环。
当涉及到将消息发回所有连接的客户端时,SCTP 使其更加容易——可以在 sctp_send() 的 sctp_sndrcvinfo 字段中设置标志 SCTP_SENDALL。因此,服务器只需从任何客户端读取消息,设置 SCTP_SENDALL 位并将其写回即可。然后,SCTP 协议栈会将其发送到所有活动的对等方!只有几行代码
nread = sctp_recvmsg(sockfd, buf, SIZE, (struct sockaddr *) &client_addr, &len, &sinfo, &flags); bzero(&sinfo, sizeof(sinfo)); sinfo.sinfo_flags |= SCTP_SENDALL; sctp_send(sockfd, buf, nread, &sinfo, 0);
SCTP_SENDALL 标志只是最近才引入 SCTP,并且不在当前的内核(最高 2.6.21.1)中,但它应该会进入 2.6.22 内核。客户端和服务器的完整代码分别显示在清单 1 (chat_client.c) 和清单 2 (chat_server.c) 中。
清单 1. chat_client.c (聊天客户端代码)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/sctp.h> #define SIZE 1024 char buf[SIZE]; #define STDIN 0 char *msg = "hello\n"; #define ECHO_PORT 2013 int main(int argc, char *argv[]) { int sockfd; int nread, nsent; int flags, len; struct sockaddr_in serv_addr; struct sctp_sndrcvinfo sinfo; fd_set readfds; if (argc != 2) { fprintf(stderr, "usage: %s IPaddr\n", argv[0]); exit(1); } /* create endpoint using SCTP */ sockfd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP); 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); } printf("Connected\n"); while (1) { /* we need to select between messages FROM the user on the console and messages TO the user from the socket */ FD_CLR(sockfd, &readfds); FD_SET(sockfd, &readfds); FD_SET(STDIN, &readfds); printf("Selecting\n"); select(sockfd+1, &readfds, NULL, NULL, NULL); if (FD_ISSET(STDIN, &readfds)) { printf("reading from stdin\n"); nread = read(0, buf, SIZE); if (nread <= 0 ) break; sendto(sockfd, buf, nread, 0, (struct sockaddr *) &serv_addr, sizeof(serv_addr)); } else if (FD_ISSET(sockfd, &readfds)) { printf("Reading from socket\n"); len = sizeof(serv_addr); nread = sctp_recvmsg(sockfd, buf, SIZE, (struct sockaddr *) &serv_addr, &len, &sinfo, &flags); write(1, buf, nread); } } close(sockfd); exit(0); }
清单 2. chat_server.c (聊天服务器代码)
#include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/sctp.h> #define SIZE 1024 char buf[SIZE]; #define CHAT_PORT 2013 int main(int argc, char *argv[]) { int sockfd, client_sockfd; int nread, nsent, len; struct sockaddr_in serv_addr, client_addr; struct sctp_sndrcvinfo sinfo; int flags; struct sctp_event_subscribe events; sctp_assoc_t assoc_id; /* create endpoint */ sockfd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP); if (sockfd < 0) { perror(NULL); exit(2); } /* bind address */ serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(CHAT_PORT); if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { perror(NULL); exit(3); } bzero(&events, sizeof(events)); events.sctp_data_io_event = 1; if (setsockopt(sockfd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events))) { perror("set sock opt\n"); } /* specify queue */ listen(sockfd, 5); printf("Listening\n"); for (;;) { len = sizeof(client_addr); nread = sctp_recvmsg(sockfd, buf, SIZE, (struct sockaddr *) &client_addr, &len, &sinfo, &flags); printf("Got a read of %d\n", nread); write(1, buf, nread); /* send it back out to all associations */ bzero(&sinfo, sizeof(sinfo)); sinfo.sinfo_flags |= SCTP_SENDALL; sctp_send(sockfd, buf, nread, // (struct sockaddr *) &client_addr, 1, &sinfo, 0); } }
在这三篇文章中,我研究了如何将 TCP 应用程序迁移到 SCTP,并讨论了 SCTP 的新功能。那么,为什么现在不是每个人都在使用 SCTP 呢?好吧,存在将人们从 TCP 应用程序迁移到 SCTP 版本的惯性,只有当人们厌倦了 TCP 版本时,这种情况才会发生——但这可能永远不会发生。
寻找 SCTP 的地方是在使用旨在利用 SCTP 的新协议的新应用程序中
SS7(7 号信令系统,参见维基百科)是 PSTN(公共交换电话网络)中控制信令的标准。SS7 信令是带外完成的,这意味着 SS7 信令消息通过单独的数据连接传输。与早期使用带内信令的系统相比,这代表了显着的安全改进。SCTP 的发明基本上是为了处理 IP 上的 SS7 等协议。SS7 使用多宿主来提高可靠性,并使用流来避免 TCP 的队头阻塞问题。
Diameter(RFC 3588,www.rfc-editor.org/rfc/rfc3588.txt)是 IETF 协议,旨在为应用程序(例如网络访问或 IP 移动性)提供身份验证、授权和计费 (AAA) 框架。有关好的介绍,请访问 www.interlinknetworks.com/whitepapers/Introduction_to_Diameter.pdf。它取代了早期的协议 Radius,Radius 在 UDP 上运行。Diameter 使用 TCP 或 SCTP 来增加这些传输的可靠性。Diameter 服务器必须同时支持 TCP 和 SCTP;尽管目前,客户端可以选择其中任何一个。SCTP 是默认设置,将来,可能需要客户端支持 SCTP。SCTP 是首选,因为它可以使用流来避免 TCP 中存在的队头阻塞问题。
DLM(分布式锁管理器,sources.redhat.com/cluster/dlm)是 Red Hat 当前在内核中的项目。这可以使用 TCP 或 SCTP。SCTP 具有多宿主支持的优势。尽管 TCP 目前是默认设置,但可以通过设置内核构建配置标志来使用 SCTP。
MPI(消息传递接口,www.mpi-forum.org)是分布式内存系统上对并行程序建模的进程之间进行通信的事实标准(根据维基百科)。它没有指定应使用哪种传输协议,尽管 TCP 在过去很常见。
Humaira Kamal 在他的硕士论文中,调查了使用 SCTP 作为传输协议,并报告了有利的结果。他特别指出原因是 SCTP 的基于消息的性质以及关联内流的使用。这些示例表明,SCTP 已在各种实际应用场景中使用,以获得优于 TCP 和 UDP 传输的优势。
本系列文章涵盖了 SCTP 的基础知识。有许多选项可以细致地控制 SCTP 协议栈的行为。还在不断努力将安全模型引入 SCTP,以便例如 TLS 可以在 SCTP 上运行。还在进行不同语言绑定到 SCTP 的工作,例如 Java 语言绑定。SCTP 不会一夜之间让 TCP 和 UDP 消失,但我希望这些文章已经表明,它具有可以使编写许多应用程序更容易和更健壮的功能。
当然,SCTP 并不是设计新协议的唯一尝试。有关与其他新协议的比较,请参阅“标准 TCP 之外的传输协议调查”,网址为 www.ogf.org/Public_Comment_Docs/Documents/May-2005/draft-ggf-dtrg-survey-1.pdf。这表明 SCTP 在可能的替代方案中表现非常出色,因此您可能希望在您的下一个网络项目中考虑它!
Jan Newmarch 是莫纳什大学的名誉高级研究员。他自内核 0.98 以来一直在使用 Linux。他撰写了四本书和许多论文,并讲授了许多技术主题的课程,过去六年专注于网络编程。他的网站是 jan.newmarch.name。