流控制傳輸協定

流控制傳輸協定

流控制傳輸協定(SCTP,Stream Control Transmission Protocol)是一種在網路連線兩端之間同時傳輸多個數據流的協定。SCTP提供的服務於UDPTCP類似。

基本介紹

  • 中文名:流控制傳輸協定
  • 外文名:Stream Control Transmission Protocol
介紹,報文格式,特點,工作過程,建立連線,下線,同時打開連線,同時下線,狀態變化圖,C++實例,

介紹

SCTP提供的服務於UDPTCP類似。SCTP在RFC2960中詳細說明,並有RFC3309加以更新。RFC 3286給出了SCTP的簡要介紹。SCTP在客戶和伺服器之間提供關聯(association),並像TCP那樣給套用提供可靠性、排序、流量控制以及全雙工的數據傳輸。SCTP中使用“關聯”一詞替代“連線”是為了避免這樣的內涵:一個連線只涉及兩個IP位址間的通信。一個關聯指代兩個系統之間的一次通信,它可能因為SCTP支持多宿而涉及不止兩個地址。
與TCP不同的是,SCTP是面向訊息的(message-oriented)。它提供各個記錄的按序遞送服務。與UDP一樣,由傳送端寫入的每一條記錄的長度隨數據一道傳遞給接收端套用。
SCTP能給在所連線的端點之間提供多個流,每個流各自可靠地按序遞送訊息。一個流上某個訊息的丟失不會阻塞同一關聯其他流上訊息的投遞。這種做法與TCP正好相反,就TCP而言,在單一位元組流中任何位置的位元組丟失都將在阻塞該連線上其後所有數據的遞送,直到該丟失被修復為止。
SCTP還提供多宿特性,使得單個SCTP端點能夠支持多個IP位址。該特性可以增強應對網路故障的健壯性。一個端點可能有多個冗餘的連線,每個網路又可能有各自接入網際網路基礎設施的連線。當該端點與另一個端點建立一個關聯之後,如果它的某個網路或某個跨域網際網路的通路發生故障,SCTP就可以通過切換到使用已與該關聯的另一個地址來避免發生的故障。

報文格式

一個SCTP分組含了一個公共的分組頭(Common Header)和若干數據塊(Chunk),每個數據塊中既可以包含控制信息,也可以包含用戶數據。
SCTP報文格式SCTP報文格式
除了INIT、INIT ACK和SHUTDOWN COMPLETE數據塊外,其他類型的多個數據塊可以捆綁在一個SCTP分組中,以滿足對MTU大小的要求。當然,這些數據塊也可以不與其他數據塊捆綁在一個分組中。如果一個用戶訊息不能放在一個SCTP分組中,這個訊息可以被分成若干個數據塊。
  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的半關閉連線。也就是在任何一方關閉關聯後,對方即不再傳送數據。
面向訊息的傳輸
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更好的網路級容錯機制。

工作過程

建立連線

不同於TCP,SCTP通過四次握手來完成連線的建立:
流控制傳輸協定
  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狀態

狀態變化圖

SCTP的狀態遷移圖如下所示。
流控制傳輸協定

C++實例

下面展示一個通過C++編寫的、用SCTP的一到多實現的一個回顯伺服器。
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 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 <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 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;}

相關詞條

熱門詞條

聯絡我們