複製鏈接
請複製以下鏈接發送給好友

流控制傳輸協議

鎖定
流控制傳輸協議(SCTP,Stream Control Transmission Protocol)是一種在網絡連接兩端之間同時傳輸多個數據流的協議。SCTP提供的服務與UDP和TCP類似。
中文名
流控制傳輸協議
外文名
Stream Control Transmission Protocol

流控制傳輸協議介紹

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報文格式
一個SCTP分組含了一個公共的分組頭(Common Header)和若干數據塊(Chunk),每個數據塊中既可以包含控制信息,也可以包含用户數據。
除了INIT、INIT ACK和SHUTDOWN COMPLETE數據塊外,其他類型的多個數據塊可以捆綁在一個SCTP分組中,以滿足對MTU大小的要求。當然,這些數據塊也可以不與其他數據塊捆綁在一個分組中。如果一個用户消息不能放在一個SCTP分組中,這個消息可以被分成若干個數據塊 [1] 
  1. Source Port Number:16比特的無符號整數,源端口號,識別SCTP發送端點的SCTP端口號。接收方可以使用源端口號、源IP地址、目的端口號和目的IP地址標識該SCTP分組所屬的偶聯。
  2. Destination Port Number:16比特的無符號整數,目的端口號,為目的端點的SCTP端口號。接收主機可以使用目的端口號將SCTP分組複用到正確的端點或應用中。
  3. Verification Tag:32比特的無符號整數,驗證標籤是偶聯建立時,本端端點為這個偶聯生成一個隨機標識。偶聯建立過程中,雙方會交換這個TAG,到了數據傳遞時,發送端必須在公共分組頭中帶上對端的這個TAG,以備校驗。包含INIT數據塊的分組中驗證標籤必須為0。在包含SHUTDOWN-COMPLETE數據塊且設置了T比特的分組中,驗證標籤必須要從包含 SHUTDOWN-ACK數據塊的分組中複製。在包含ABORT數據塊的分組中,驗證標籤必須要從觸發這個ABORT發送的分組中複製。
  4. Checksum:32比特的無符號整數,SCTP通過對用户數據使用ADLER-32算法,計算出一個32位的校驗碼,帶在數據報中,在接收端進行同樣的運算,通過檢查校驗碼是否相等來驗證用户數據是否遭到破壞。
  5. 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類型。
  6. Chunk Flags:8比特的無符號整數,塊標誌位用法由塊類型決定。除非被置為其他值,塊標記在傳送過程中會被置0而且接收端點會忽視塊標記。
  7. Chunk Length:16比特的無符號整數,塊長度用來表示包括塊類型、塊標記、塊長度和塊值在內的字節數,長度使用二進制表示。
  8. Chunk Value:變長,塊值字段是在該數據塊中真正傳送的信息,內容由數據塊類型決定。塊值的長度為不定長。

流控制傳輸協議特點

和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地址用於數據傳輸。
圖1 圖1
對多穴主機的支持是為了在網絡級提高容錯能力。如果接收端是多穴主機,那麼對於發送端來説每一個接收端的IP地址代表着一條通往對端的路徑,這樣發送端可以選擇任一條路徑來發送數據。SCTP規定任何時間都有一條路徑作為首選路徑來發送數據,其他路徑作為備份路徑。如果首選路徑因接口故障或者網絡擁塞等原因而失效,SCTP可以自動切換到另外一條路徑來發送,避免單點失效,從而提高整個關聯的容錯能力。多穴主機之間的SCTP關聯如圖1所示。主機A到主機B有兩條路徑,A是多穴主機,這裏只考慮不同的IP地址對應不同的網絡接口的情況,A有兩個網絡接口,選擇哪個接口來發送數據是A自身的路由策略問題(源地址選擇問題)。
多流
圖2 圖2
SCTP的另一主要特點是多流。SCTP消息在不同的流內發送,這也是流傳輸控制協議名稱的由來。從發送端到接收端可以有多個流,在同一流內發送的消息有序,而不同流之間的消息無序,因此不同流之間的消息傳輸是相對獨立的。在某一個流內由於數據傳輸失敗而引起的阻塞不會影響其他流的消息遞交。多流特性可以幫助解決TCP中的隊頭阻塞(HOL)問題。因為TCP傳輸是按字節嚴格有序的,先行傳送的字節如果丟失或損壞,即使後續的字節正確地被接收到也不能向上層遞交,必須在接收端緩衝起來,直到先行字節由於重傳而全部正確接收到後才可以提交,並且釋放緩衝區。
圖2描述了一個用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更好的網絡級容錯機制。

流控制傳輸協議工作過程

流控制傳輸協議建立連接

不同於TCP,SCTP通過四次握手來完成連接的建立 [2] 
  1. 連接發起者(一般為客户端)SCTP發送一個INIT消息(初始化)。該消息包括了連接發起者的IP地址清單、初始序列號、用於標識本耦聯中所有報文的起始標記、客户請求的外出流的數目以及客户能夠支持的外來流的數目
  2. 對端(服務器)發送一個INITACK消息確認連接發起者的INIT消息,其中含有服務器的IP地址清單、初始序列號、起始標記、服務器請求的外出流的數目、服務器能夠支持的外來流的數目以及一個狀態cookie,狀態cookie包含服務器用於確信本耦聯有效所需的所有狀態,cookie是經過數字簽名的,因而可以確保其有效性
  3. 客户以一個COOKIEECHO消息返回服務器的狀態cookie,除COOKIEECHO外,該消息可能在同一個報文中捆綁一個用户數據
  4. 服務器以一個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的過程如下:
  1. 使用收到的INIT和發出的INIT-ACK塊中的信息創建一個關聯的TCB(傳輸控制塊)。
  2. 在TCB中,將當前日期設為創建日期,將協議參數“有效Cookie時間”設為生存期間。
  3. 根據TCB,收集重建TCB所需的最小信息子集,將該子集和密鑰產生一個MAC(信息認證編碼)。
  4. 結合上述最小信息子集和MAC產生狀態Cookie。
  5. 在發送完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關閉一個耦聯的過程如下:
  1. 應用程序發出關閉請求,SCTP耦聯進入SHUTDOWN-PENDING狀態,並且不再接收應用程序的數據,只發送隊列中還未發送的數據,再隊列中沒有待發送數據後,發送SHUTWODN並進入SHUTDOWN-SENT狀態。這一方被稱為主動關閉。
  2. 執行被動關閉的一方在接收到主動關閉一方的SHUTWODN消息時,進入SHUTDOWN-RECEIVED狀態,此時執行被動關閉一方不再接受上層應用的數據,只發送隊列中剩餘的數據。在發送隊列中的數據被髮送完後,執行被動關閉一方發送SHUTDOWN-ACK並進入SHUTDOWN-ACK-SENT狀態。
  3. 執行主動關閉一方收到SHUTDOWN-ACK後就發送SHUTDOWN-COMPLETE,並進入CLOSE狀態。
  4. 執行主動關閉一端接收到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狀態。在這種情況下關閉的流程為:
  1. 兩端都發送SHUTWODN並進入SHUTDOWN-SENT狀態
  2. 兩端都收到對方的SHUTDOWN消息,併發送SHUTDOWN-ACK,然後進入SHUTDOWN-ACK-SENT狀態
  3. 兩端都收到對方的SHUTDOWN-ACK,併發送SHUTDOWN-COMPLETE,然後就進入close狀態

流控制傳輸協議狀態變化圖

圖3 圖3
SCTP的狀態遷移圖如圖3所示。

流控制傳輸協議C實例

下面展示一個通過類Unix下C編寫的、用SCTP的一到多實現的一個回顯服務器 [2] 
1、服務端
Server.h
#pragma once

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/sctp.h>

#define SERVER_PORT 6666
#define BUFFER_SIZE 1024
#define LISTEN_QUEUE 100

class 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 <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>

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 <sys/socket.h>
#include <netinet/in.h>
#include <netinet/sctp.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_PORT 6666
#define MAXLINE 1024

void 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;
}

參考資料