简介
SCTP提供的服务于UDP和TCP类似。SCTP在RFC2960中详细说明,并有RFC3309加以更新。RFC 3286给出了SCTP的简要介绍1。SCTP在客户和服务器之间提供关联(association),并像TCP那样给应用提供可靠性、排序、流量控制以及全双工的数据传输。SCTP中使用“关联”一词替代“连接”是为了避免这样的内涵:一个连接只涉及两个IP地址间的通信。一个关联指代两个系统之间的一次通信,它可能因为SCTP支持多宿而涉及不止两个地址。
与TCP不同的是,SCTP是面向消息的(message-oriented)。它提供各个记录的按序递送服务。与UDP一样,由发送端写入的每一条记录的长度随数据一道传递给接收端应用。
SCTP能给在所连接的端点之间提供多个流,每个流各自可靠地按序递送消息。一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递。这种做法与TCP正好相反,就TCP而言,在单一字节流中任何位置的字节丢失都将在阻塞该连接上其后所有数据的递送,直到该丢失被修复为止。
SCTP还提供多宿特性,使得单个SCTP端点能够支持多个IP地址。该特性可以增强应对网络故障的健壮性。一个端点可能有多个冗余的连接,每个网络又可能有各自接入因特网基础设施的连接。当该端点与另一个端点建立一个关联之后,如果它的某个网络或某个跨域因特网的通路发生故障,SCTP就可以通过切换到使用已与该关联的另一个地址来避免发生的故障2。
SCTP报文格式一个SCTP分组含了一个公共的分组头(Common Header)和若干数据块(Chunk),每个数据块中既可以包含控制信息,也可以包含用户数据。
除了INIT、INIT ACK和SHUTDOWN COMPLETE数据块外,其他类型的多个数据块可以捆绑在一个SCTP分组中,以满足对MTU大小的要求。当然,这些数据块也可以不与其他数据块捆绑在一个分组中。如果一个用户消息不能放在一个SCTP分组中,这个消息可以被分成若干个数据块1。
Source Port Number:16比特的无符号整数,源端口号,识别SCTP发送端点的SCTP端口号。接收方可以使用源端口号、源IP地址、目的端口号和目的IP地址标识该SCTP分组所属的偶联。
Destination Port Number:16比特的无符号整数,目的端口号,为目的端点的SCTP端口号。接收主机可以使用目的端口号将SCTP分组复用到正确的端点或应用中。
Verification Tag:32比特的无符号整数,验证标签是偶联建立时,本端端点为这个偶联生成一个随机标识。偶联建立过程中,双方会交换这个TAG,到了数据传递时,发送端必须在公共分组头中带上对端的这个TAG,以备校验。包含INIT数据块的分组中验证标签必须为0。在包含SHUTDOWN-COMPLETE数据块且设置了T比特的分组中,验证标签必须要从包含 SHUTDOWN-ACK数据块的分组中复制。
在包含ABORT数据块的分组中,验证标签必须要从触发这个ABORT发送的分组中复制。
Checksum:32比特的无符号整数,SCTP通过对用户数据使用ADLER-32算法,计算出一个32位的校验码,带在数据报中,在接收端进行同样的运算,通过检查校验码是否相等来验证用户数据是否遭到破坏。
Chunk Type:8比特的无符号整数,块类型定义在块值(Chunk Value)中消息所属的类型。包括:INIT、INIT ACK、SACK、ABORT、ERROR、SHUTDOWN、COOKIE ACK等13种数据块类型。该参数的取值范围为0~254,255留作今后的扩展。数据块类型字段的编码分配如下:
0:净荷数据(DATA)
1:启动(INIT)
2:启动证实 (INIT ACK)
3:选择证实 (SACK)
4:Heartbeat请求(HEARTBEAT)
5:Heartbeat证实(HEARTBEAT ACK)
6:中止 (ABORT)
7:关闭(SHUTDOWN)
8:关闭证实(SHUTDOWN ACK)
9:操作差错(ERROR)
10:状态Cookie(COOKIE ECHO)
11:Cookie证实(COOKIE ACK)
12:为明确拥塞通知响应(ECNE)预留
13:为降低拥塞窗口(CWR)预留
14:关闭完成(SHUTDOWN COMPLETE)
15~62:IETF预留
63:IETF定义的数据块扩展
64~126:IETF预留
127:IETF定义的数据块扩展
128~190:IETF预留
191:IETF定义的数据块扩展
192~254:IETF预留
255:IETF定义的数据块扩展
Chunk type的高两位bit指示了收端不认识对应的chunk type的处理原则:
00:停止处理数据报并丢弃,不再处理报中的其他Chunk。
01:与00相同处理外,还要在ERROR或INIT ACK中上报,原因为不认识的参数类型。
10:忽略该Chunk ,继续处理数据报中的其他Chunk。
11:同10相同处理外,还要在ERROR中上报,原因为不认识的Chunk类型。
Chunk Flags:8比特的无符号整数,块标志位用法由块类型决定。除非被置为其他值,块标记在传送过程中会被置0而且接收端点会忽视块标记。
Chunk Length:16比特的无符号整数,块长度用来表示包括块类型、块标记、块长度和块值在内的字节数,长度使用二进制表示。
Chunk Value:变长,块值字段是在该数据块中真正传送的信息,内容由数据块类型决定。块值的长度为不定长。
SCTP特点和TCP类似,SCTP是面向连接、端到端、全双工、带有流量和拥塞控制的可靠传输协议。SCTP的连接称为关联。SCTP的关联通过4次握手建立。相对于TCP的3次握手建立连接,SCTP的关联能够抵御拒绝服务(DoS)攻击,从而提高了安全性。数据只有在关联建立之后与关联关闭之前才可发送。SCTP的关联通过3次握手关闭,不支持类似TCP的半关闭连接。也就是在任何一方关闭关联后,对方即不再发送数据3。
面向消息的传输SCTP是一种面向消息的传输协议,从上层应用传递下来的数据以消息的形式传输。SCTP提供消息的定界功能。在接收端,数据以消息的形式递交。为便于传输,SCTP提供消息的拆分和组装,以及消息的捆绑传输功能。
SCTP的数据传输基本单位是块。每个SCTP包包括一个SCTP公共头部、一个或多个块。块有两种基本类型:控制块和数据块。控制块用于SCTP的连接控制,包括连接的建立、关闭、传输路径的维护等;数据块传送应用层的用户数据。上层用户的每一个消息均被封装在一个数据块中,如果消息长度大于传输路径的最大传输单元(MTU),消息将被拆分成多个数据块传输,在接收端再组装起来向上层提交,这样每一个SCTP包封装一个数据块。如果消息长度较小,在1个MTU大小的限制下,在同一个SCTP包里可以捆绑多个消息,也即多个数据块共用一个公共头部,从而提高传输效率。数据块可以和控制块封装在同一个SCTP包里传输,这种捆绑受MTU大小的限制。
RFC2960定义了13种块类型,包括1种数据块和12种控制块。为实现新功能扩展,可以定义新的块类型。块包括块参数,用于协助完成块功能。块和块参数都是采用类型-长度-值(TLV)的结构定义的。用这种结构定义新的块类型及块参数类型来实现SCTP新功能非常方便。SCTP包括较完善的容错机制,如果通信双方的某一方不支持对端的某项扩展功能,可以通过容错和报错机制保证关联的健壮性。体现了SCTP的良好可扩展性。
多穴主机SCTP的一个主要特点是支持多穴主机。SCTP关联的每个端点都可以拥有多个网络层地址。SCTP可以支持不同的网络层协议,为描述方便,本文以IP作为网络层协议来说明,即每个SCTP端点可以拥有多个IP地址用于数据传输。
对多穴主机的支持是为了在网络级提高容错能力。如果接收端是多穴主机,那么对于发送端来说每一个接收端的IP地址代表着一条通往对端的路径,这样发送端可以选择任一条路径来发送数据。SCTP规定任何时间都有一条路径作为首选路径来发送数据,其他路径作为备份路径。如果首选路径因接口故障或者网络拥塞等原因而失效,SCTP可以自动切换到另外一条路径来发送,避免单点失效,从而提高整个关联的容错能力。多穴主机之间的SCTP关联如图所示。主机A到主机B有两条路径,A是多穴主机,这里只考虑不同的IP地址对应不同的网络接口的情况,A有两个网络接口,选择哪个接口来发送数据是A自身的路由策略问题(源地址选择问题)。
多流SCTP的另一主要特点是多流。SCTP消息在不同的流内发送,这也是流传输控制协议名称的由来。从发送端到接收端可以有多个流,在同一流内发送的消息有序,而不同流之间的消息无序,因此不同流之间的消息传输是相对独立的。在某一个流内由于数据传输失败而引起的阻塞不会影响其他流的消息递交。多流特性可以帮助解决TCP中的队头阻塞(HOL)问题。因为TCP传输是按字节严格有序的,先行传送的字节如果丢失或损坏,即使后续的字节正确地被接收到也不能向上层递交,必须在接收端缓冲起来,直到先行字节由于重传而全部正确接收到后才可以提交,并且释放缓冲区。
图描述了一个用3个流发送消息的SCTP实例。发送端有3个出流,相应的接收端就有3个入流。图2中给出了发送从1到9的9个消息实例。其中消息1、2、3在同一个流,4、5、6在同一个流,7、8、9在同一个流,分别在各自流内有序。由于消息1没有正确接收,造成消息2、3不能向上层协议(ULP)提交。然而从4到9的消息由于在不同的流中,则可以提交给ULP。这种流机制提供了无序递交功能,提高了传输效率。
此外,SCTP还定义了无序消息。如果消息带有无序标志,则不论它在哪个流中(在具体实现中,数据块中的流号不被解析),只要被正确接收,都提交给ULP,从而实现和流无关的无序递交。
流量、拥塞和错误的控制SCTP仍然采用类似TCP的流量控制和拥塞控制机制,但又有所增强。整个传输分为慢启动阶段和拥塞避免阶段。与TCP不同的是,SCTP的拥塞窗口初始值可以是2个MTU,可以比TCP获得更快的窗口增长。SCTP的拥塞控制采用了选择确认(SACK)快速重传和快速恢复机制,是TCP各种主流改进机制的集成。但是由于SCTP采用了块结构和控制块机制,可以比TCP更大地提升传输性能。例如SCTP在移动通信的切换中表现得比TCP SACK更优越[4]。 由于SCTP有多个通往对端的路径,在发送端对每一个路径都有一套拥塞控制参数和控制用的数据结构。这类似于有多个通往对端的TCP连接,SCTP为多条路径的流量控制和拥塞控制提供统一的管理机制。消息可以在不同的路径上传输,流管理和路径管理是正交的,即相对独立。
每个路径有一个错误计数器,当某一路径上的错误达到一个门限时,该路径将会被标记为不活动的(Inactive),SCTP把传输转移到另一条路径上进行。同时SCTP对整个关联设置一个错误计数器,每个路径上的错误计数时,整个关联的错误计数也要增加,只要对端返回确认,则关联错误计数器清零(不管是对哪条路径返回的确认)。如果关联错误计数器达到一个门限值,则整个关联被非正常关闭。由此可见,多路径带来比TCP更好的网络级容错机制。
SCTP工作过程建立连接不同于TCP,SCTP通过四次握手来完成连接的建立2:
连接发起者(一般为客户端)SCTP发送一个INIT消息(初始化)。该消息包括了连接发起者的IP地址清单、初始序列号、用于标识本耦联中所有报文的起始标记、客户请求的外出流的数目以及客户能够支持的外来流的数目
对端(服务器)发送一个INITACK消息确认连接发起者的INIT消息,其中含有服务器的IP地址清单、初始序列号、起始标记、服务器请求的外出流的数目、服务器能够支持的外来流的数目以及一个状态cookie,状态cookie包含服务器用于确信本耦联有效所需的所有状态,cookie是经过数字签名的,因而可以确保其有效性
客户以一个COOKIEECHO消息返回服务器的状态cookie,除COOKIEECHO外,该消息可能在同一个报文中捆绑一个用户数据
服务器以一个COOKIEACK消息确认客户返回的cookie是正确的,到此时该耦联就建立成功了。该消息也可能在同一个报文中捆绑一个用户数据。
在一次SCTP四次握手中,INIT消息的接收端不必保存任何状态信息或者分配任何资源,这样就可防范SYNFlooding等DoS攻击。它在发送INIT-ACK消息时,采用了一种机制——“状态Cookie”,该Cookie具有发送端要建立自己状态所需的全部信息。
用于建立连接的INIT ACK只能在COOKIE WATI状态收到,在其它状态收到该报文时都会直接丢弃,类似的,COOKIE ACK只能在COOKIE ECHOED状态接收。
在常规的握手中,主动发起方的本地tag在发起握手时产生,主动发起方的对端tag在收到INIT ACK时产生。而连接的被动方的本地tag和对端tag都在收到INIT时产生,但是最终要到收到了COOKIE ECO后才确定并保存下来。
SCTP产生一个状态Cookie的过程如下:
使用收到的INIT和发出的INIT-ACK块中的信息创建一个关联的TCB(传输控制块)。
在TCB中,将当前日期设为创建日期,将协议参数“有效Cookie时间”设为生存期间。
根据TCB,收集重建TCB所需的最小信息子集,将该子集和密钥产生一个MAC(信息认证编码)。
结合上述最小信息子集和MAC产生状态Cookie。
在发送完INITACK(包含状态Cookie参数)后,发送方必须删除TCB以及任何与新关联有关的本地资源。
INIT和INIT-ACK都必须包含建立初始状态所需的参数:一组IP地址,保证可靠传输的初始序列号,每个被接收的SCTP报文中必须含有的验证标签,每一端请求发出的流数目和每一端能支持接收的流数目。交换完这些消息之后,INIT的发送端以COOKIE-ECHO消息的方式发送回状态Cookie。接收端根据所接收到的COOKIE-ECHO中的状态Cookie,完整地重建自己的状态,并回送COOKIE-ACK来确认关联已建立。
因此对于SCTP,即使接收再多的INIT消息,接收端也没有任何资源的消耗:它既不分配任何系统资源,也不保存此次新关联的状态,它只是把相应重建状态所用的状态Cookie作为参数,包含在每一个回送的INIT-ACK消息中,最后该状态Cookie会被COOKIE-ECHO消息发送回来。
类似于TCP,SCTP也多由客户端执行主动打开,而服务器执行被动打开。
断开连接与TCP不同,SCTP使用三次握手来关闭一个耦联。而且SCTP不支持TCP所支持的“半关闭”状态。典型的SCTP关闭一个耦联的过程如下:
应用程序发出关闭请求,SCTP耦联进入SHUTDOWN-PENDING状态,并且不再接收应用程序的数据,只发送队列中还未发送的数据,再队列中没有待发送数据后,发送SHUTWODN并进入SHUTDOWN-SENT状态。这一方被称为主动关闭。
执行被动关闭的一方在接收到主动关闭一方的SHUTWODN消息时,进入SHUTDOWN-RECEIVED状态,此时执行被动关闭一方不再接受上层应用的数据,只发送队列中剩余的数据。在发送队列中的数据被发送完后,执行被动关闭一方发送SHUTDOWN-ACK并进入SHUTDOWN-ACK-SENT状态。
执行主动关闭一方收到SHUTDOWN-ACK后就发送SHUTDOWN-COMPLETE,并进入CLOSE状态。
执行主动关闭一端接收到SHUTDOWN-COMPLETE后就进入close状态。
同时打开连接RFC规定,如果SCTP在COOKIE-WAIT或者COOKIE-ECHOED状态接收到INIT报文。则:
INIT报文的接收者产生一个INIT-ACK,该INIT-ACK使用的本端参数和自己发送的那个INIT报文的相同
执行状态COOKIE的计算过程,产生一个状态COOKIE
不允许修改SCTP的状态
状态COOKIE相关的TCB不能删除
不关闭T1-init定时器
如果SCTP在非COOKIE-WAIT状态接收到了INIT-ACK,则丢弃它。
同时断开连接极少数情况下,耦联的双发可能同时执行主动关闭,即同时进入发送SHUTWODN并进入SHUTDOWN-SENT状态。在这种情况下关闭的流程为:
两端都发送SHUTWODN并进入SHUTDOWN-SENT状态
两端都收到对方的SHUTDOWN消息,并发送SHUTDOWN-ACK,然后进入SHUTDOWN-ACK-SENT状态
两端都收到对方的SHUTDOWN-ACK,并发送SHUTDOWN-COMPLETE,然后就进入close状态
SCTP状态变化图SCTP的状态迁移图如下所示。
C++实例下面展示一个通过C++编写的、用SCTP的一到多实现的一个回显服务器2。
1、服务端
Server.h
#pragma once#include #include #include #define SERVER_PORT 6666#define BUFFER_SIZE 1024#define LISTEN_QUEUE 100class SctpServer { public: SctpServer(); void start(void); private: //开启监听socket void listenSocket(void); //循环处理请求 void loop(void); int sockFd_; //用来接受的套接字 int messageFlags_; //消息类型 char readBuf_[BUFFER_SIZE]; //接受缓冲区 struct sockaddr_in clientAddr_; //用来保存客户端地址 struct sockaddr_in serverAddr_; //用来保存服务端地址 struct sctp_sndrcvinfo sri_; //消息相关细节信息 struct sctp_event_subscribe events_; //事件集 int streamIncrement_; //流号 socklen_t len_; //地址长度 size_t readSize_; //读到的大小};Server.cpp
#include "server.h"#include #include #include #include #include SctpServer::SctpServer() :streamIncrement_(1){}void SctpServer::listenSocket(void){ //创建SCTP套接字 sockFd_ = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP); bzero(&serverAddr_,sizeof(serverAddr_)); serverAddr_.sin_family = AF_INET; serverAddr_.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr_.sin_port = htons(SERVER_PORT); inet_pton(AF_INET,"127.0.0.1",&serverAddr_.sin_addr); //地址绑定 bind(sockFd_,(struct sockaddr *)&serverAddr_,sizeof(serverAddr_)); //设置SCTP通知事件(此处只设置了I/O通知事件) bzero(&events_,sizeof(events_)); events_.sctp_data_io_event = 1; setsockopt(sockFd_,IPPROTO_SCTP,SCTP_EVENTS,&events_,sizeof(events_)); //开始监听 listen(sockFd_,LISTEN_QUEUE);}void SctpServer::loop(void){ while(true) { len_ = sizeof(struct sockaddr_in); //从socket读取内容 readSize_ = sctp_recvmsg(sockFd_,readBuf_,BUFFER_SIZE, (struct sockaddr *)&clientAddr_,&len_,&sri_,&messageFlags_); //增长消息流号 if(streamIncrement_) { sri_.sinfo_stream++; } sctp_sendmsg(sockFd_,readBuf_,readSize_, (struct sockaddr *)&clientAddr_,len_, sri_.sinfo_ppid,sri_.sinfo_flags,sri_.sinfo_stream,0,0); }}void SctpServer::start(void){ listenSocket(); loop();}main.cpp
#include "server.h"int main(int argc,char **argv){ SctpServer server; server.start(); return 0;}2、客户端
Client.h
#pragma once#include #include #include #include #include #include #include #include #include #include #define SERVER_PORT 6666#define MAXLINE 1024void sctpstr_cli(FILE *fp,int sock_fd,struct sockaddr *to,socklen_t tolen);class SctpClient{ public: SctpClient():echoToAll_(0) { } ~SctpClient() { close(sockFd_); } //启动客户端 void start(void) { makeSocket(); } private: void makeSocket(void) { sockFd_ = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP); bzero(&serverAddr_,sizeof(serverAddr_)); serverAddr_.sin_family = AF_INET; serverAddr_.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr_.sin_port = htons(SERVER_PORT); inet_pton(AF_INET,"127.0.0.1",&serverAddr_.sin_addr); bzero(&events_,sizeof(events_)); events_.sctp_data_io_event = 1; setsockopt(sockFd_,IPPROTO_SCTP,SCTP_EVENTS,&events_,sizeof(events_)); if(echoToAll_ == 0) { sctpstr_cli(stdin,sockFd_,(struct sockaddr *)&serverAddr_,sizeof(serverAddr_)); } } int sockFd_; struct sockaddr_in serverAddr_; struct sctp_event_subscribe events_; int echoToAll_;};//循环发送并接受消息void sctpstr_cli(FILE *fp,int sock_fd,struct sockaddr *to,socklen_t tolen){ struct sockaddr_in peeraddr; struct sctp_sndrcvinfo sri; char sendline[MAXLINE]; char recvline[MAXLINE]; socklen_t len; int out_sz,rd_sz; int msg_flags; bzero(&sri,sizeof(sri)); while(fgets(sendline,MAXLINE,fp) != NULL) { if(sendline[0] != '[') { printf("ERROR\n"); continue; } sri.sinfo_stream = sendline[1] - '0'; out_sz = strlen(sendline); //发送消息 int count = sctp_sendmsg(sock_fd,sendline,out_sz,to,tolen,0,0,sri.sinfo_stream,0,0); len = sizeof(peeraddr); rd_sz = sctp_recvmsg(sock_fd,recvline,sizeof(recvline), (struct sockaddr *)&peeraddr,&len,&sri,&msg_flags); printf("From str:%d seq:%d (assoc:0x%x):", sri.sinfo_stream,sri.sinfo_ssn,(u_int)sri.sinfo_assoc_id); printf("%d %s\n",rd_sz,recvline); }}Client.cpp
#include "client.h"int main(int argc,char **argv){ SctpClient client; client.start(); return 0;}