SCTP是一个面向消息的协议,递送给用户的是部分的或完整的消息。只有当发送大消息时,在对端才会递送部分的消息。部分消息被递送给应用后,多个部分消息组合成单个完整消息不由SCTP负责。在SCTP应用进程看来,一个消息既可由单个输入操作接收,也可由多个连续的输入操作接收。
SCTP服务器既可以迭代运行,也可以并发运行,取决于应用开发人员选择的套接字式样。SCTP还提供了从一到多式套接字抽取某个关联,使其成为一到一式套接字的方法。
第十章中编写的一到多式SCTP回射服务器程序不保持任何关联状态,它只是像UDP一样,调用sctp_recvmsg获取消息,然后调用sctp_sendmsg回射消息,这样设计依赖于客户关闭关联,如果客户打开一个关联后不发送任何数据,服务器还是会给这些客户分配资源,虽然这些客户不会使用这些资源,这些空闲的客户会无意造成对于SCTP的拒绝服务攻击,因此SCTP增设了自动关闭特性。
自动关闭特性允许SCTP端点指定某个关联可以保持空闲的最大秒数,关联在任何方向上都没有用户数据就认为它是空闲的。如果关联的空闲时间超过它的最大允许时间,该关联就由SCTP实现自动关闭。
使用自动关闭套接字选项应仔细选择其值,若服务器选择太小的值,服务器收到消息后,处理消息的过程中关联就关闭了,然后发送时发现关联已关闭,会重新打开关联再发,这样会有重新打开关联的额外开销,且客户不太可能已经调用过listen以允许外来关联。以下是第十章中的服务器程序的改进部分,其中插入了必要的调用以避免出现长期空闲的关联,自动关闭特性默认是禁止的,需要显式通过SCTP_AUTOCLOSE套接字选项开启:
if (argc == 2) {
// 是否每次回射都把流号增加1
stream_increment = atoi(argv[1]);
}
sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
// 120秒的空闲关联自动关闭时间
close_time = 120;
Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_AUTOCLOSE, &close_time, sizeof(close_time));
bzero(&servaddr, sizeof(servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
当应用要求SCTP传输过大消息时,SCTP可能采取部分递送措施,这里的过大指的是SCTP栈认为没有足够资源专用于这样的消息。接收端SCTP的实现开启部分递送API需要考虑以下几点:
1.所接收消息的缓冲区空间使用量必须满足或超过某个门槛。
2.SCTP栈最多只能从该消息处顺序递送到首个缺失断片。
3.一旦激发,其他消息必须等到当前消息已被完整接收并递送给接收端应用后才能被递送。即过大的消息会阻塞其他所有消息的递送,包括其他流中的消息。
对于第1点,SCTP的KAME实现使用的门槛是套接字接收缓冲区的一半大小,编写本书时此SCTP栈的默认接收缓冲区大小为131072字节,因此如果不修改SO_RCVBUF套接字选项的值,单个消息必须超过65535字节才会激起部分递送API。为了将第十章中的服务器程序改为使用部分递送,我们先编写一个包裹sctp_recvmsg函数的函数,再在新服务器程序中使用该包裹函数:
#include "unp.h"
static uint8_t *sctp_pdapi_readbuf = NULL;
static int sctp_pdapi_rdbuf_sz = 0;
uint8_t *pdapi_recvmsg(int sock_fd, int *rdlen, SA *from, int *from_len,
struct sctp_sndrcvinfo *sri, int *msg_flags) {
int rdsz, left, at_in_buf;
int frmlen = 0;
// 如果接收缓冲区尚未分配,则动态分配其空间并设置与它关联的状态
if (sctp_pdapi_readbuf == NULL) {
sctp_pdapi_readbuf = (uint8_t *)Malloc(SCTP_PDAPI_INCR_SZ);
sctp_pdapi_rdbuf_sz = SCTP_PDAPI_INCR_SZ;
}
// 读入消息,它可能是某个消息的一个断片
at_in_buf = Sctp_recvmsg(sock_fd, sctp_pdapi_readbuf, sctp_pdapi_rdbuf_sz,
from, from_len, sri, msg_flags);
// 如果sctp_recvmsg函数返回错误或EOF,直接返回到调用者
if (at_in_buf < 1) {
*rdlen = at_in_buf;
return NULL;
}
// 如果收取的不是一个完整消息,则继续收集其余断片
while ((*msg_flags & MSG_EOR) == 0) {
// 首先计算接收缓冲区中剩余空间
left = sctp_pdapi_rdbuf_sz - at_in_buf;
// 当接收缓冲区中剩余空间小于某阈值时,调用realloc增长缓冲区大小
if (left < SCTP_PDAPI_NEED_MORE_THRESHOLD) {
sctp_pdapi_readbuf = realloc(sctp_pdapi_readbuf,
sctp_pdapi_rdbuf_sz + SCTP_PDAPI_INCR_SZ);
if (sctp_pdapi_readbuf == NULL) {
err_quit("sctp_pdapi ran out of memory");
}
sctp_pdapi_rdbuf_sz += SCTP_PDAPI_INCR_SZ;
left = sctp_pdapi_rdbuf_sz - at_in_buf;
}
// 继续读入本消息其余断片
rdsz = Sctp_recvmsg(sock_fd, &sctp_pdapi_readbuf[at_in_buf],
left, NULL, &frmlen, NULL, msg_flags);
// 增加缓冲区索引
at_in_buf += rdsz;
}
// 把读入的字节数复制到调用者提供的rdlen参数指针所指的整型变量中
*rdlen = at_in_buf;
// 返回指向所分配缓冲区的指针
return sctp_pdapi_readbuf;
}
使用以上函数的服务器程序例子:
for (; ; ) {
len = sizeof(struct sockaddr_in);
// 每次调用pdapi_recvmsg前,清理掉可能占据sri变量的旧数据
// sri是一个sctp_sndrcvinfo结构,如果预订了sctp_data_io_event事件(默认预订)
// 则在调用sctp_recvmsg接收消息时,会收到伴随消息的sctp_sndrcvinfo结构
bzero(&sri, sizeof(sri));
readbuf = pdapi_recvmsg(sock_fd, &rd_sz, (SA *)&cliaddr, &len, &sri, &msg_flags);
// 如果读入EOF或发生错误,则继续
if (readbuf == NULL) {
continue;
}
}
以下函数用于显示来自SCTP的通知,如果想显示所有通知,需要预定所有事件,然后当收到一个通知时调用该函数:
#include "unp.h"
void print_notification(char *notify_buf) {
union sctp_notification *snp;
struct sctp_assoc_change *sac;
struct sctp_paddr_change *spc;
struct sctp_remote_error *sre;
struct sctp_send_failed *ssf;
struct sctp_shutdown_event *sse;
struct sctp_adaption_event *ae;
struct sctp_pdapi_event *pdapi;
const char *str;
snp = (union sctp_notification *)notify_buf;
switch (snp->sn_header.sn_type) {
case SCTP_ASSOC_CHANGE:
sac = &snp->sn_assoc_change;
switch (sac->sac_state) {
case SCTP_COMM_UP:
str = "COMMUNICATION UP";
break;
case SCTP_COMM_LOST:
str = "COMMUNICATION LOST";
break;
case SCTP_RESTART:
str = "RESTART";
break;
case SCTP_SHUTDOWN_COMP:
str = "SHUTDOWN COMPLETE";
break;
case SCTP_CANT_STR_ASSOC:
str = "CAN'T START ASSOC";
break;
default:
str = "UNKNOWN";
break;
} /* end switch(sac->sac_state) */
// 如果是关联改变类通知,显示发生关联改变的类型描述
printf("SCTP_ASSOC_CHANGE: %s, assoc = 0x%x\n", str, (uint32_t)sac->sac_assoc_id);
break;
case SCTP_PEER_ADDR_CHANGE:
spc = &snp->sn_paddr_change;
switch (spc->spc_state) {
case SCTP_ADDR_AVALIABLE:
str = "ADDRESS AVAILABLE";
break;
case SCTP_ADDR_UNREACHABLE:
str = "ADDRESS UNREACHABLE";
break;
case SCTP_ADDR_REMOVED:
str = "ADDRESS REMOVED";
break;
case SCTP_ADDR_ADDED:
str = "ADDRESS ADDED";
break;
case SCTP_ADDR_MADE_PRIM:
str = "ADDRESS MADE PRIMARY";
break;
default:
str = "UNKNOWN";
break;
} /* end switch(spc->spc_state) */
// 如果是对端地址通知,显示地址的可读形式和变动后的地址
printf("SCTP_PEER_ADDR_CHANGE: %s, addr = %s, assoc = 0x%x\n", str,
Sock_ntop((SA *)&spc->spc_aaddr, sizeof(spc->spc_aaddr)),
(uint32_t)spc->spc_assoc_id);
break;
case SCTP_REMOTE_ERROR:
sre = &snp->sn_remote_error;
// 如果是远程错误,则显示错误和发生该错误的关联id
// 此处不试图获取并显示由远程对端所报告的真正错误
// 真正错误信息可从sctp_remote_error.sre_data成员获得
printf("SCTP_REMORE_ERROR: assoc = 0x%x, error = %d\n",
(uint32_t)sre->sre_assoc_id, sre->sre_error);
break;
case SCTP_SEND_FAILED:
ssf = &snp->sn_send_failed;
// 消息发送失败,这意味着:
// (1)关联正在关闭,马上会得到一个关联通知
// (2)服务器在使用部分递送扩展,并有一个消息未成功发送(由传输上设置的限制造成)
// 待发送的数据存在ssf_data成员中
printf("SCTP_SEND_FAILED: assoc = 0x%x, error = %d\n", (uint32_t)ssf->ssf_assoc_id,
ssf->ssf_error);
break;
case SCTP_ADAPTION_INDICATION:
ae = &snp->sn_adaption_event;
// 适配层指示通知,打印出在关联建立消息(INIT或INIT-ACK)中传递的32位值
printf("SCTP_ADAPTION_INDICATION: 0x%x\n", (u_int)ae->sai_adaption_ind);
break;
case SCTP_PARTIAL_DELIVERY_EVENT:
pdapi = &snp->sn_pdapi_event;
// 部分递送通知,目前唯一的事件是部分递送被取消
if (pdapi->pdapi_indication == SCTP_PARTIAL_DELIVERY_ABORTED) {
printf("SCTP_PARTIAL_DELIEVERY_ABORTED\n");
} else {
printf("UNKNOWN SCTP_PARTIAL_DELIVERY_EVENT 0x%x\n", pdapi->pdapi_indication);
}
break;
case SCTP_SHUTDOWN_EVENT:
sse = &snp->sn_shutdown_event;
// 对方已发送SHUTDOWN消息,到关联终止序列完成时,通常还会得到一个关联变动通知
printf("SCTP_SHUTDOWN_EVENT: assoc = 0x%x\n", (uint32_t)sse->sse_assoc_id);
break;
default:
printf("Unknown notification event type = 0x%x\n", snp->sn_header.sn_type);
}
}
以上代码中的sctp_notification联合的定义如下,它是所有可能事件对应结构的联合:
如上图,除了所有可能的事件外,还有一个名为sn_header的sctp_tlv结构,它用于标识当前联合中存放的事件类型,每个事件对应的结构的前3个字段都与sctp_tlv结构中的前3个字段相同。
使用以上打印事件通知函数的服务器程序:
// 接收所有通知
struct sctp_event_subscribe evnts;
bzero(&evnts, sizeof(evnts));
evnts.sctp_data_io_event = 1;
evnts.sctp_association_event = 1;
evnts.sctp_address_event = 1;
evnts.sctp_send_failure_event = 1;
evnts.sctp_peer_error_event = 1;
evnts.sctp_shutdown_event = 1;
evnts.sctp_partial_delivery_event = 1;
evnts.sctp_adaption_layer_event = 1;
Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));
Listen(sock_fd, LISTENQ);
for (; ; ) {
len = sizeof(struct sockaddr_in);
rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),
(SA *)&cliaddr, &len, &sri, &msg_flags);
// 如果读到的是通知,就显示它,然后去读下一个消息
if (msg_flags & MSG_NOTIFICATION) {
print_notification(readbuf);
continue;
}
}
如果我们启动SCTP回射客户程序,并发送一个消息:
在接收关联建立消息、用户消息、关联终止消息时,服务器输出以下事件:
SCTP通常提供可靠的有序数据传输服务,但也可提供可靠的无序数据传输服务,指定MSG_UNORDERED标志发送的消息没有顺序限制,一到达对端就能被递送,即使同一个SCTP流上早先发送的其他无序数据尚未到达。无序的数据可以在任何SCTP流中发送,不用赋予流序列号。以下是为了使用无序数据服务向回射服务器发送请求而对客户程序做的修改:
out_sz = strlen(sendline);
Sctp_sendmsg(sock_fd, sendline, out_sz, to, tolen, 0, MSG_UNORDERED, sri.sinfo_stream, 0, 0);
通常,一个给定SCTP流中的所有数据都标以序列号以便排序,MSG_UNORDERED标志使相应数据以无序方式发送,即不标以序列号。
有些应用想把主机的全体IP地址的某个子集捆绑到单个套接字,TCP和UDP只能通过bind函数捆绑到单个地址或通配地址,不能捆绑一个地址子集。由SCTP提供的sctp_bindx函数允许应用捆绑不止一个地址,这些地址必须使用相同端口,如果调用sctp_bindx前已经调用过bind,那么所用端口必须是调用bind时指定的端口,否则sctp_bindx调用将失败。以下实用函数可将参数提供的地址子集捆绑到给定套接字:
#include "unp.h"
int sctp_bind_arg_list(int sock_fd, char **argv, int argc) {
struct addrinfo *addr;
char *bindbuf, *p, portbuf[10];
int addrcnt = 0;
int i;
// 先分配sctp_bindx调用的地址列表参数所需的空间,sctp_bindx函数能混合接受IPv4和IPv6地址
// 我们为每个地址分配一个sockaddr_storage结构,它足以装下任何套接字地址结构
// 而地址列表参数是多个套接字地址结构的紧凑列表,这么做会导致一定的内存空间浪费
// 但比先处理一遍参数以计算出精确的内存空间要简单
bindbuf = (char *)Calloc(argc, sizeof(struct sockaddr_storage));
p = bindbuf;
// 把端口转换为ASCII表示形式,以便调用getaddrinfo函数的包裹函数host_serv
sprintf(portbuf, "%d", SERV_PORT);
for (i = 0; i < argc; ++i) {
// 把每个地址和端口传给host_serv函数,同时传递AF_UNSPEC作为地址族以允许IPv4和IPv6地址
// 传递SOCK_SEQPACKET作为套接字类型指明使用SCTP
addr = Host_serv(argv[i], portbuf, AF_UNSPEC, SOCK_SEQPACKET);
// 我们仅复制host_serv函数返回的第一个套接字地址结构,因为我们传给host_serv函数的是
// 一个地址而非主机名(对于主机名,getaddrinfo函数返回所有A记录和AAAA记录),因此这么做是安全的
memcpy(p, addr->ai_addr, addr->ai_addrlen);
freeaddrinfo(addr);
++addrcnt;
p += addr->ai_addrlen;
}
Sctp_bindx(sock_fd, (SA *)bindbuf, addrcnt, SCTP_BINDX_ADD_ADDR);
free(bindbuf);
return 0;
}
使用以上函数的服务器程序:
if (argc < 2) {
err_quit("Error, use %s [list of addresses to bind]\n", argv[0]);
}
// 创建IPv6套接字,因此IPv4和IPv6都能用
sock_fd = Socket(AF_INET6, SOCK_SEQPACKET, IPPROTO_SCTP);
if (sctp_bind_arg_list(sock_fd, argv + 1, argc - 1)) {
err_sys("Can't bind the address set");
}
bzero(&evnts, sizeof(evnts));
evnts.sctp_data_io_event = 1;
SCTP是一个多宿协议,找出一个关联的本地端点或远程端点所用地址需要不同于单宿协议的机制。以下我们把SCTP回射客户程序修改为接收通信开工(communication up)通知,然后使用该通知显示关联的本端和对端地址。以下是客户main函数的修改:
// 显式预定关联变动通知,通信开工通知属于该通知类型
bzero(&evnts, sizeof(evnts));
evnts.sctp_data_io_event = 1;
evnts.sctp_association_event = 1;
Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));
sctpstr_cli(stdin, sock_fd, (SA *)&servaddr, sizeof(servaddr));
以下是sctp_strcli函数的改动,它使用即将介绍的新通信处理实用函数check_notification:
do {
// 客户会设置套接字地址长度变量,用于接收函数sctp_recvmsg获取由服务器回射的应答消息
len = sizeof(peeraddr);
rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline),
(SA *)&peeraddr, &len, &sri, &msg_flags);
// 查看接收到的消息是否是一个通知
if (msg_flags & MSG_NOTIFICATION) {
check_notification(sock_fd, recvline, rd_sz);
}
// 如果读入的是一个通知,则再次循环读取
} while (msg_flags & MSG_NOTIFICATION);
// 显示读取到的真正数据
printf("From str: %d seq: %d (assoc:0x%x):",
sri.sinfo_stream, sri.sinfo_ssn, (u_int)sri.sinfo_assoc_id);
// rd_sz指定要打印的字符数量,如果不指定要打印的字符数量,会从recvline开始一直打印到首个\0字符
// 而我们收到的消息后,没有在消息最后面加上\0,因此可能会访问到其他内存区域
printf("%.*s", rd_sz, recvline);
以下是check_notification函数,它在关联通知到达后显示本地和远程两个端点的地址:
#include "unp.h"
void check_notification(int sock_fd, char *recvline, int rd_len) {
union sctp_notification *snp;
struct sctp_assoc_change *sac;
struct sockaddr_storage *sal, *sar;
int num_rem, num_loc;
snp = (union sctp_notification *)recvline;
// 如果是关联变动类通知
if (snp->sn_header.sn_type == SCTP_ASSOC_CHANGE) {
sac = &snp->sn_assoc_change;
// 如果是新关联或重新激活的关联的事件通知
if ((sac->sac_state == SCTP_COMM_UP) || (sac->sac_state == SCTP_RESTART)) {
// sctp_paddrs系统调用获取远程地址列表,并返回地址数目
num_rem = sctp_getpaddrs(sock_fd, sac->sac_assoc_id, &sar);
printf("There are %d remote addresses and they are:\n", num_rem);
// 打印各个远程地址
sctp_print_addresses(sar, num_rem);
// 释放由sctp_getpaddrs函数分配的资源
sctp_freepaddrs(sar);
// sctp_getladdrs系统调用获取本地地址列表
num_loc = sctp_getladdrs(sock_fd, sac->sac_assoc_id, &sal);
printf("There are %d local addresses and they are:\n", num_loc);
// 打印各个本地地址
sctp_print_addresses(sal, num_loc);
// 释放由sctp_getladdrs函数分配的资源
sctp_freeladdrs(sal);
}
}
}
以下是打印地址列表的sctp_print_addresses函数:
#include "unp.h"
// 用于显示由sctp_getpaddrs或sctp_getladdrs系统调用返回的地址列表
void sctp_print_addresses(struct sockaddr_storage *addrs, int num) {
struct sockaddr_storage *ss;
int i, salen;
ss = addrs;
for (i = 0; i < num; ++i) {
// 调用自编写的sock_ntop函数显示地址,使用该函数能协议无关地显示地址
printf("%s\n", Sock_ntop((SA *)ss, salen));
// 地址列表是紧凑的套接字地址结构数组,而非sockaddr_storage结构数组
// sockaddr_storage结构太大,用它在内核和进程之间传递地址过于浪费内存
// 如果套接字地址结构自带长度成员,就直接使用该值作为本结构长度
#ifdef HAVE_SOCKADDR_SA_LEN
salen = ss->sslen;
// 如果套接字地址结构没有长度成员,则根据地址族选择长度
#else
switch (ss->ss_family) {
case AF_INET:
salen = sizeof(struct sockaddr_in);
break;
#ifdef IPV6
case AF_INET6:
salen = sizeof(struct sockaddr_in6);
break;
#endif
default:
err_quit("sctp_print_addresses: unknown AF");
break;
}
#endif
// 令ss指向下一个待处理地址
ss = (struct sockaddr_storage *)((char *)ss + salen);
}
}
我们按以下方式启动SCTP回射客户并发送一个消息:
上例中,客户使用关联通知事件收到的结构中的sac_assoc_id成员获取发生事件的关联的标识,从而调用sctp_getpaddrs和sctp_getladdrs获取关联对应的IP。但如果应用没有在跟踪关联标识,只知道一个对端地址,可由以下函数获取它的关联ID:
#include "unp.h"
sctp_assoc_t sctp_address_to_associd(int sock_fd, struct sockaddr *sa, socklen_t salen) {
struct sctp_paddrparams sp;
int siz;
siz = sizeof(struct sctp_paddrparams);
bzero(&sp, siz);
memcpy(&sp.spp_address, sa, salen);
// 通过SCTP_PEER_ADDR_PARAMS套接字选项获取对端地址参数
// 该套接字选项既往内核复制数据,又需要从内核获取数据,因此不用getsockopt函数,改用sctp_opt_info函数
// 该调用返回当前心博间隔、SCTP实现认定对端地址不可达所需的最大重传次数、关联ID
sctp_opt_info(sock_fd, 0, SCTP_PEER_ADDR_PARAMS, &sp, &siz);
// 我们不检查sctp_opt_info函数是否调用成功,如果失败,此处会返回0
// 而关联ID不允许为0,就将0值关联ID作为没有关联的指示
return sp.spp_assoc_id;
}
SCTP提供类似TCP的保持存活选项的心博机制,SCTP的心博机制默认开启。应用可用SCTP_PEER_ADDR_PARAMS套接字选项设置某个对端地址的心博间隔和出错门限,出错门限是认定这个对端地址不可达前必须发生的心博遗失次数。由心博检测到该对端地址再次变为可达时,该地址重新开始活跃。
应用可以禁止心博,但如果没有心博,SCTP就无法检测一个被认定不可达的对端地址再次变为可达,没有用户干预,这些地址就不能回到活跃状态。
sctp_paddrparams结构中的心博间隔字段为spp_hbinterval,其值为SCTP_NO_HB(0值)时表示禁止心博;其值为SCTP_ISSUE_HB(0xffffffff值)时表示立即进行一次心博;其他值以毫秒为单位设置心博间隔,该值加上当前重传计时器的值(确保在发送下一个心跳之前,已经完成了可能存在的数据重传),再加上一个随机的抖动值就构成了心博的间隔时间。
以下函数可用于针对某个对端地址设置心博间隔,或请求立即心博一次,或禁止心博:
#include "unp.h"
int heartbeat_action(int sock_fd, struct sockaddr *sa, socklen_t salen, u_int value) {
struct sctp_paddrparams sp;
int siz;
// 把sctp_paddrparams.spp_pathmaxrxt设为0表示保持其当前值不变
// sctp_paddrparams.spp_pathmaxrxt表示对于某一地址的最大超时重传次数
bzero(&sp, sizeof(sp));
sp.spp_hbinterval = value;
// 把要实施心博的对端地址复制到sctp_paddrparams结构中,以指定某个对端地址
// caddr_t是一个历史遗留问题,许多早期的UNIX系统中用它表示任意类型的字符指针
// 在现代的UNIX系统中,caddr_t通常被定义为char *
// sp.spp_address的类型为通用套接字结构类型sockaddr,而memcpy函数要求参数类型为void *
// 因此需要将其转换为caddr_t类型的指针,以便传递给memcpy函数
// 这种类型转换在现代的编程实践中不再推荐使用,推荐用void *类型的指针来进行通用的指针操作
memcpy((caddr_t)&sp.spp_address, sa, salen);
Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS, &sp, sizeof(sp));
return 0;
}
SCTP一到多式接口相比一到一式接口有以下优势:
1.只需维护单个描述符。
2.允许编写简单的迭代服务器程序。
3.允许应用在四路握手的第三个和第四个分组发送数据,方法是直接用sendmsg或sctp_sendmsg函数隐式建立关联。
4.无需跟踪传输状态,即应用只需在套接字描述符上执行一个接收调用就可接收消息,之前不必执行传统的connect或accept调用。
但一到多式接口的缺陷在于难以编写并发服务器程序(用线程或子进程),该缺陷促成sctp_peeloff函数的增设,该函数参数为一个一到多式套接字描述符和一个关联ID,返回值为一个新的该关联的一到一式套接字描述符(通过该描述符可获取排队在该关联上的通知和数据)。原始的一到多式套接字继续开放,其他关联不受此影响。
剥离出的一对一式套接字随后可以递交给某个专门的线程或子进程加以处理,从而实现并发服务器。我们把SCTP回射服务器程序改为:先处理某个客户的第一个请求,再使用sctp_peeloff函数剥离出该客户的一个套接字,然后派生一个子进程,子进程中调用str_echo,派生子进程后,服务器父进程循环回去处理下一个客户的请求:
for (; ; ) {
len = sizeof(struct sockaddr_in);
// 接收并处理某客户发送的第一个消息
rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf), (SA *)&cliaddr, &len,
&sri, &msg_flags);
// sri.sinfo_ppid字段通常用于存储负载协议标识符(PPID,Payload Protocol IDentifier)
// 这个标识符可以被应用程序用来区分不同类型的消息
// sri.sinfo_stream字段标识流
Sctp_sendmsg(sock_fd, readbuf, rd_sz, (SA *)&cliaddr, len, sri.sinfo_ppid, sri.sinfo_flags,
sri.sinfo_stream, 0, 0);
// 把接收消息的源地址转换为关联id
// 此关联id也出现在sri.sinfo_assoc_id中,这么转换只为演示从IP地址确定关联ID的方法
assoc = sctp_address_to_associd(sock_fd, (SA *)&cliaddr, len);
if ((int)assoc == 0) {
err_ret("Can't get association id");
continue;
}
// 把该客户的关联剥离到一个一到一式套接字中
// 获取的一到一式套接字connfd可被传递到先前TCP版的str_echo函数中
connfd = sctp_peeloff(sock_fd, assoc);
if (connfd == -1) {
err_ret("sctp_peeloff fails");
continue;
}
// 派生一个子进程,让子进程执行新套接字上的所有后续工作
if ((childpid = fork()) == 0) {
Close(sock_fd);
str_echo(connfd);
exit(0);
} else {
Close(connfd);
}
}
SCTP有一些定时控制量,它们影响SCTP端点需要多久才能声称某关联或某对端地址已经失效。SCTP有7个确定失效检测的定时控制量:
我们先看以下两种情形:
1.一个SCTP端点试图打开与某个对端的关联,而对端主机已经断开与网络的物理连接。
2.两个多宿SCTP端点在交换数据,其中之一在通信过程中关机,由于防火墙过滤,对端没有收到任何ICMP消息。
第一种情形下,试图打开关联的系统首先把RTO定时器设为srto_initial的值(3000毫秒),发生一次超时后,它会重传INIT消息并把RTO定时器倍增成6000毫秒,这样的行为将持续到已经发送了sinit_max_attempts(8)次INIT消息,且每次传送都发生超时为止。RTO定时器的倍增以sinit_max_init_timeo(60000毫秒)为上限,因此SCTP从开始发送INIT消息到宣告潜在对端主机不可达所花的总时间为3+6+12+24+48+60+60+60=273秒。
我们可以改变一些定时控制量来缩短或延长这个时间,如果我们减少sinit_max_attempts规定的重传次数到4,则失效检测时间将缩短到45秒,约默认设置给出的时间的1/6,但这样做增加了以下情况发生的概率:对端主机可达,但由于网络丢失或对端主机过载等原因,我们声称它不可达。
我们还可以减少sinit_max_init_timeo的值,如果将其减少到20秒(最大RTO降低到20s),失效检测时间将缩短到121秒,不到原值的一半,但这样做的缺点是可能导致过密地重传INIT消息。
第二种情形下,假设一个端点有2个地址IP-A和IP-B,另一个端点也有2个地址IP-X和IP-Y。如果其中一个端点因关机变得不可达,假设数据由现未关机的端点发送,发送端对于每个对端地址将相继发生超时,开始的超时值为srto_min(1秒),以后对每个对端地址都倍增到上限的srto_max(60秒),且重传次数为sasoc_asocmaxrxt(10)次。
发送端经历的超时时间总和为1(IP-A)+1(IP-B)+2(IP-A)+2(IP-B)+4(IP-A)+4(IP-B)+8(IP-A)+8(IP-B)+16(IP-A)+16(IP-B)=62秒,srto_max没有起上限作用,因为在它起作用前关联重传次数已经达到sasoc_asocmaxrxt。如果我们把srto_max减少到10秒,检测时间就能减少12秒到50秒;如果把重传次数sasoc_asocmaxrxt减少到8,检测时间就会缩短到30秒。但这样减少检测时间同样有缺陷:持续时间较短的可恢复网络故障或远程系统过载可能导致关联被自动拆除。
对于以上定时控制量,不建议降低最小RTO(srto_min),跨因特网通信时降低该值会导致以更短的间隔重传消息,从而过度消耗因特网基础设施资源。在私用网络上降低该值尚可接受,但对大多应用来说,该值不宜减少。
在修改以上定时控制量前需要考虑以下因素:
1.应用需要以多快的速度检测失效。
2.关联的整个端对端路径是否在比因特网更稳定的私用网络上。
3.虚假的失效检测会有什么后果。
SCTP最初开发的目的是跨因特网传输电话呼叫控制信令,但在其开发过程中,其适用范围被扩展到一个通用传输协议的程度,它提供TCP的大多数特性,又增设了很多的新传输层服务。何时值得改用SCTP代替TCP,需要先看下SCTP的益处:
1.SCTP直接支持多宿。一个端点可以利用它的多个直接连接的网络获得额外的可靠性。除了移植到SCTP外,应用无需采取其他行为就能自动使用SCTP的多宿服务。
2.消除头端阻塞。应用可用单个SCTP关联并行传输多个数据队列,同一关联内,一个流中的数据丢失不影响其他流中的数据流动。
3.保持应用层消息边界。许多应用发送的并不是字节流,而是消息,SCTP保持应用进程发送的消息边界,从而略微简化了应用程序开发人员的任务。使用SCTP无需在字节流中标记消息边界,也无需提供从字节流中重构出消息的特殊处理代码。
4.提供无序消息服务。对于某些应用,消息的到达顺序无关紧要,这样的应用可能出于可靠性要求而使用TCP,但这样没有顺序要求的消息还是将按发送端发送顺序递送到接收端,其中任何一个消息的丢失都将导致头端阻塞,即后续消息即使到达也不能提前无序递送。SCTP的无序服务可用于避免该问题,使得应用需求与传输服务相匹配。
5.有些SCTP实现提供部分可靠服务。该特性允许SCTP发送端为每个消息指定一个生命期,通过sctp_sndrcvinfo.sinfo_timetolive来指定,这个生命期不同于IPv4的TTL或IPv6的跳限,它是真正的时间长度。当源端点和目的端点都支持本特性时,时间敏感的过期数据可由传输层而非应用进程丢弃,该字段用于控制消息在网络中存在的时间限制,类似IPv4的TTL,从而在面临网络阻塞时优化数据传输。
6.SCTP以一到一式接口提供了从TCP到SCTP的简易移植手段。一到一式接口类似典型的TCP接口,因此稍加修改,一个TCP应用程序就能移植成SCTP应用程序。
7.SCTP提供TCP的许多特性,如确认、重传丢失数据、重排数据、窗口式流量控制、慢启动、拥塞避免、选择性确认,但SCTP没有提供半关闭状态和紧急数据。
8.SCTP提供许多可供应用调整的配置,为应用提供了TCP难以企及的控制能力,以便匹配其需求,且SCTP提供配合良好的默认设置供不进行调整配置的应用使用。
SCTP不提供TCP的半关闭状态,当一个应用关闭了TCP连接的一半却仍然允许对端发送数据时,该TCP连接进入半关闭状态,同时还会告知对端本端已经发送完数据。使用半关闭特性的应用不是很多,因此在SCTP开发阶段,本特性被认为不值得增加到SCTP中。确实需要本特性的TCP应用移植到SCTP时必须修改应用层协议,在应用数据中提供这个告知EOF的手段,但有些程序这么修改应用层协议并非轻而易举之事。
SCTP还不提供TCP的紧急数据,用一个单独的SCTP流传输数据(将该数据当作紧急数据)类似TCP的紧急数据的语义,但该语义并不准确。
不能从SCTP中真正获益的是那些必须使用面向字节流传输服务的应用,如telnet、rlogin、rsh、ssh等,对于这样的应用,TCP比SCTP更高效地把字节流分装到TCP分节中。SCTP忠实地保持消息边界,而当每个消息的长度仅仅是1字节时,SCTP封装这样的消息到数据块会导致过多的开销,效率非常低。
许多应用可以考虑改用SCTP重新实现,但前提是SCTP能在Unix平台上得到普及。