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