1. 程式人生 > >SCTP協議詳解與例項

SCTP協議詳解與例項

1.SCTP是什麼?

只要是接觸過程式設計的人,當你問他傳輸層都有哪些協議?我想幾乎很多人會說TCP,IP協議而很少有人知道SCTP(流控制傳輸協議)這個和上述倆個協議具有相同地位的協議。
SCTP提供的服務與TCP,UDP類似,或者甚至可以理解為其是TCP與UDP協議各自優點的組合後的產物。

2.SCTP的特點

(1)SCTP連線的建立

SCTP協議建立連線可呼叫

int sctp_connectx(int sd, struct sockaddr *addrs, int addrcnt);
//或者直接傳送訊息就可建立連線
int sctp_sendmsg(int s, const
void *msg, size_t len, struct sockaddr *to, socklen_t tolen, uint32_t ppid, uint32_t flags, uint16_t stream_no, uint32_t timetolive, uint32_t context);

介紹完建立連線的介面,那麼就來談談其連線建立的具體過程,廢話不多說,先給出其建立連線過程的示意圖
這裡寫圖片描述
SCTP連線建立的過程如上圖所示:

(1)客戶端給伺服器傳送一個INIT初始化訊息,訊息中包含有客戶端要告訴伺服器自己的IP地址的清單,初始化列號(可以和TCP初始化序列一樣來理解),用於表示本關聯中的所有分組的其實標誌,客戶請求以及能夠支援流的數目等
(2)伺服器給客戶端回一個INIT ACK來確認剛收到的訊息,並且還有和剛才客戶端給伺服器發的所有種類的伺服器自己的資訊,除此之外還會還有一個狀態cookie
(3)客戶端收到狀態cookie之後,給伺服器回一個COOKIE ECHO 並且此時還可以在此資料包中包含使用者資料
(4)伺服器收到客戶端給其發的COOKIE ECHO之後在給其發個COOKIE ACK 此時同樣也可以攜帶資料

至此一個SCTP關聯就建立成功

(2)SCTP斷開連線

SCTP斷開連線和TCP的介面相同都是呼叫shutdown(),但是語義卻不同其how引數對SCTP來說都是讀寫都禁掉
其具體流程如下圖
這裡寫圖片描述

SCTP不像TCP那樣允許”半關閉的關聯”。當一端關閉某個關聯時,另一段必須停止傳送資料。

(3)多宿主

TCP為客戶與伺服器之間提供連線,從而可以使雙方安全的傳送資料,而SCTP將連線改為了”關聯”。

要解釋清楚為什麼SCTP將TCP的連線改為”關聯”我打算從他們的bind(繫結介面)來說明原因,先以伺服器呼叫bind為例,
TCP的bind介面如下

int
bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen);

SCTP的bind介面如下

int sctp_bindx(int sockfd, struct sockaddr *my_addrs, int addrcnt, int flags);

上面給出了TCP和STCP對應的繫結套接字的介面,我們逐一的對比其引數
(1)首先二者的第一個引數sockfd含義相同,都是指待繫結的套接字
(2)bind中的引數my_addr指的是要把哪個插座(IP地址和埠號)繫結到sockfd上,這相當與給了sockfd一個身份,之後網路中就可以唯一的標識該sockfd了,而sctp_bindx中對應位置的引數名變為my_addrs,細心的讀者會發現這個引數變為了複數了。沒錯該引數並不是單一的某個sockaddr結構體的地址,而是sockaddr結構體陣列的首地址,引數addrcnt表示該陣列中元素的個數。既然相比TCP的bind介面,此處為多個地址,我們也不難理解sctp_bindx所做的工作是將這些地址都繫結到sockfd上。這樣其和TCP的第一個不同點就出來了。即TCP的bind之後的socket只有一個身份(只綁定了一個地址),而SCTP bind之後socket有多個身份(綁定了多個地址)。關於flags引數我們之後在補充

上述中我們的倆種協議的伺服器呼叫bind介面有所區別,那麼SCTP其對應的客戶端建立連線時呼叫的connect介面也類似稍有不同
sctpconnect介面

int sctp_connectx(int sd, struct sockaddr *addrs, int addrcnt);

可以看出SCTP在建立連線時addrs也為一個sockaddr結構體陣列

那麼SCTP相較TCP的繫結埠和連線時使用了地址陣列有什麼用意呢?沒錯這就是我們這裡最終想要說明的其多宿主的特性。伺服器呼叫多個地址來標識自己,客戶端可以和每一個地址建立一個路徑。這樣當客戶端的哪條路勁失效時,可以自動切換到另一條路徑上,應用程式甚至都不必知道發生了故障,這樣是不是比起TCP的單一路徑安全保障性更好一點呢?

(4)多流解決頭端阻塞

還是與TCP做比較,TCP雖然是個全雙工協議,但是其讀寫方向各自只有一個流。一個流很多情況下會發生頭端阻塞,其大概意思如下圖
這裡寫圖片描述
如上圖所示,因為TCP每一個方向上只有一個流,那麼當該流中的某個部分的報文丟失之後,其後的所有報文都只能等待丟失部分重傳之後,並在重新排序之後方可被送入應用的接受緩衝區。這種協議設定對傳送資料有嚴格順序要求時還可以接受,但如果對傳送資料沒有這麼強的順序限制時就顯的不那麼友好了。
一個具體的場景

當我們的一個web頁面要展示好多張圖片時,就像上圖所示。在這種情況下其實圖片1到4之間到達的先後順序是完全不影響的。此時如果你用TCP這種單項流協議,那麼圖片一有資料丟失,導致你因此也看不了其實已經發來的圖二到圖4。如果你改用SCTP這種支援多流的協議,那麼你的每張圖片傳送都使用不同的流號,到時候,當圖一對應流號的資料有丟失,那麼只有這一個流會等待重傳,其他幾張圖片都不會被其影響,這樣我們就可以先看到圖2到圖4了,完全不受圖1丟失的影響。不知道讀者此時會不會感覺出來點多流在此種場景下的巨大優勢

(5) 面向訊息

SCTP是面向訊息的協議,不像TCP那樣沒有包的概念,也意味著訊息沒有邊界,邊界只能由應用層來設計和劃分,而SCTP一個包就是一個訊息,這就大大降低了程式設計者的難度。其次它的傳送接受介面還可以傳遞訊息型別,介面具體如下

int sctp_sendmsg(int s, const void *msg, size_t len, struct sockaddr *to,
         socklen_t tolen, uint32_t ppid, uint32_t flags,
         uint16_t stream_no, uint32_t timetolive, uint32_t context);

上述的傳送介面中flags引數就是用來標識訊息型別的引數。通過這樣直接獲取訊息型別由可以減少我們不必要的拆包操作,真的是方便之極

(6)一到多特性

SCTP的一到多特性其實和UDP的套接字一樣,都能通過一個套接字接受多個訊息,這意味著程式設計師可以不用向TCP那樣管理大量的套接字了

3.SCTP常用介面

一下介面我們都是針對一到多形式的SCTP

(1)sctp_bindx埠繫結

該介面用來命名一個套接字

int sctp_bindx(int sd, struct sockaddr *addrs, int addrcnt, int flags);
//成功返回0,出錯返回1

該介面用來給sd繫結一組地址,或從addrs地址組中新增,刪除某個地址。具體操作由flags來控制,由於之前我們以介紹過其他引數,這裡值介紹flags

flags 說明
SCTP_BINDX_ADD_ADDR 往套接字裡新增地址
SCTP_BINDX_REM_ADDR 從套接字中刪除地址

(2)sctp_connectx函式

該介面用於與伺服器建立連線

int sctp_connectx(int sd, struct sockaddr *addrs, int addrcnt);

引數含義上文有介紹這裡不在贅餘

(3)sctp_sendmsg

如果之前沒有呼叫sctp_connectx那麼第一次呼叫該介面既負責建立連線也負責傳送資料

int sctp_sendmsg(int s, const void *msg, size_t len, struct sockaddr *to,
         socklen_t tolen, uint32_t ppid, uint32_t flags,
         uint16_t stream_no, uint32_t timetolive, uint32_t context);

前5個引數與UDP的sendto引數含義相同,我們在此只討論後4個

ppid引數指定將隨資料塊傳遞的淨荷協議
flags引數標識訊息型別
stream_no引數標識具體的流號
timetolive指定訊息的生命期
context儲存訊息傳輸過程中可能產生的上下文

(4)sctp_recvmsg

該介面負責接受資料

int sctp_recvmsg(int s, void *msg, size_t len, struct sockaddr *from,
         socklen_t *fromlen, struct sctp_sndrcvinfo *sinfo,
         int *msg_flags);

由於前5個引數與UDP的recvfrom相同,在此我們同樣也只討論後幾個引數

sinfo儲存了訊息相關的細節
msg_flags和sctp_sendmsg中的flags相對應

4.SCTP例項程式碼

下面是用SCTP的一到多實現的一個回顯伺服器

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

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