基於UDP高效能可靠傳輸協議UDT-銳英源軟體經驗
一、 概述
UDT是一個高效能的基於UDP的資料傳輸協議,它是為支援高速廣域網上海量資料傳輸而設計,為解決TCP的效率和公平問題,同時提供可靠的資料流和報文傳輸。
UDT是C++庫,幾乎類同於BSD socket APIs。
UDT是多執行緒安全的,但並不是多程序共享。
二、 原理
UDT有兩種傳輸模式:資料流模式(SOCK_STREAM)和資料報模式(SOCK_DGRAM)
資料流模式類似於傳統的BSD套接字,這種模式下不能保證任何一端一個呼叫就把所有的資料傳送了,因為在資料流中沒有邊界資訊,程序需用loop來發送和接收。
資料報模式會將資料作為整個單元來傳送,不需要迴圈來接收和傳送資料,要麼全部發送,要麼一點也不傳送。在接收端如果緩衝區不夠大,則只會接收到部分資料,其他的將被丟棄。
UDT傳送資料有阻塞與非阻塞方式,在阻塞方式下,會直到把需要傳送的資料傳送完再返回,而非阻塞方式下,會根據socket底層的可用緩衝的大小,將緩衝區中的資料拷貝過去,有多大緩衝就拷貝多少,緩衝區滿了就立即返回,這個時候的返回值只是拷貝了多少,不代表傳送了多少,同時剩下的部分需要再次呼叫send
UDT增加了rendezvous模式,這是一種連線模式,用來穿透防火牆。這種模式下,UDT不能呼叫listen和accept,而是兩端bind後同時建立連線。
UDT允許使用者自己定義擁塞控制。可以繼承DUT/CCC下的CCC類來改變一些變數,如擁塞視窗,./app/cc.h下的例項是學習的快速途徑。
三、 安裝及平臺
UDT是基於原始碼的庫,所以沒有安裝檔案工具,我們只需要根據不同的系統和CPU架構使用命令來make相應的庫即可。
UDT支援的系統:linux,BSD,OSX
UDT支援的架構:IA32,IA64,POWERPC,AMD64
命令: make –e os=XXX arch=XXX
UDT來源於BSD socket API只有一個頭檔案,一些繼續使用BSDAPI 另一些需要加標示符UDT::
庫:libudt.h udt.dll udt.dylib libudt.a udt.lib
四、 配置設定
讀取和設定選項通過getsockopt和setsockopt方法,一般不要修改預設選項除非應用不能正常執行。
UDT_MSS用來設定包的大小,一般情況下最佳的UDT包的大小是網路MTU(預設1500位元組)的大小,連線的兩端都要設定這個值,傳輸時取兩端的較小者。
UDT用不同的同步方式語義UDT_SNDSYN和UDT_RCVSYN,它可以獨立的設定傳送和接收同步,具有更多的靈活性。它不允許在連線建立和關閉的時候進行非阻塞操作。
UDT緩衝區的大小理論上越大越好,要執行的好兩端buffer至少為【頻寬*RTT】
UDT使用UDP資料通道,所以UDP緩衝大小影響程式執行,但隨著buffer變大效果也會越來越不明顯。一般來說傳送端的buffer小一點,因為包的傳送沒有限制太多,但太大會增加端到端的延時。
UDT_LINGER是設定socket關閉時是否立即停止傳送緩衝區的資料。
UDT_RENDEZVOUS設定集合點模式,在穿越防火牆時很有用。
UDT_SNDTIMEO和UDT_RCVTIMEO是timeout值
UDT_REUSEADDR設定UDP埠是否可以給其他UDT使用,預設值是true。
以下情況需設定false
1,兩個UDT socket不能在同一埠監聽。
2,兩個UDT socket繫結在同一IP同一埠而不能建立連線。
傳送發有兩種選擇:
1,TTL(預設無限)為timeout時間。
2,訊息有序到達,直到上一個訊息到達或被丟棄才發下一個。
UDT提供檔案傳輸,UDT::sendfile和UDT::recvfile這種傳送接收方式跟
UDT::send和UDT::recv是正交的。也就是說用sendfile傳送不一定要用recvfile接收。另外,sendfile和recvfile不受SNDSYN,RCVSYN,SNDTIMEO,RCVTIMEO影響。它使用C++ fstream進行檔案IO。
UDT打洞,在傳統方式下,穿越防火牆時是用SO_REUSEADDR選項去開啟兩個socket繫結同一個埠,一個監聽一個建立連線。而UDT提供直接相連的方式。
UDT允許一個程序中的所有socket繫結到同一埠但只允許一個監聽。
UDT允許繫結已經存在的UDP埠有兩個好處:
1,當應用程式向伺服器傳送一個空包去獲得它的地址(尤其是在NAT防火牆下)時,使用者會建立一個UDP包傳送個server確定繫結的埠,然後UDP埠可以順便給UDT使用。
2,一些本地防火牆在關閉“打洞”時會改變對映埠,新的UDT繫結的埠將失效,此時用UDP的是必須的。
錯誤處理:所有的UDT API在遇到錯誤時都會返回error UDT定義兩種錯誤,
UDT::INVALID_SOCK和UDT::ERROR。可以用getErrorCode和getErrorMessage方法檢視存放在ERRORINFO資料結構中的錯誤程式碼及資訊。
成功的呼叫不會清楚錯誤,所以應用程式應該利用返回值檢查呼叫結果,可以呼叫個體lasterror().clean()來清除錯誤日誌。
五、 介面描述
socket方法
方法名
socket方法
功能
用於建立一個新的socket
詳細介面
UDTSOCKET socket(
int af,
int type,
int protocol
);
返回值
成功:返回socket描述符
錯誤:返回UDT::INVALID_SOCK
描述
type:指SICK_STREAM或SOCK_DARAM
protocol:忽略
af:AF_INET(IPv4)或者AF_INET(IPv6)
UDT支援IPv4和IPv6可以通過af引數選擇。
accept方法
方法名
accept方法
功能
用於接收一個進來的連線
詳細介面
UDTSOCKET accept(
UDTSOCKET u,
struct sockaddr* addr,
int* addrlen
);
返回值
成功:返回新連線的描述符
錯誤:返回UDT::INVALID_SOCK
描述
當一個UDT socket處於監聽狀態,它將接收到的連線存放在一個佇列裡,一個accept呼叫會依次從佇列裡首先取出對頭的連線,並將其移出佇列。
當佇列裡沒有連線請求時,一個阻塞式的socket就會等待下一個連線,遇到錯誤時會有一個非阻塞式的socket立即返回。
接收到的socket會繼承監聽socket的屬性。
bind方法
方法名
bind方法
功能
用於繫結一個埠
詳細介面
int bind(
UDTSOCKET u,
struct sockaddr* name,
int* namelen
);
int bind(
#ifndef WIN32
int udpsock
#else
SOCKET udpsock
#endif
);
返回值
成功:返回0
錯誤:返回UDT::ERROR
描述
繫結內容包括IP地址和埠,如果設定了INADDR_ANY,則表示0.0.0.0任意地址(多網絡卡時),當埠使用0則會使用一個隨機的可用埠,用getsockname可以獲得埠號,另一種方式UDT允許繫結在已存在的UDP埠(前面已寫)此時需注意程式碼的健壯性,一旦UDP描述符被UDT使用,不要再隨便使用除非你對系統非常瞭解。
繫結過程是必須的,除了在監聽下,如果沒有繫結,UDT會自動隨機繫結一個地址
cleanup方法
方法名
cleanup方法
功能
用於釋放UDT庫
詳細介面
int cleanup(
);
返回值
成功:返回0
錯誤:返回UDT::ERROR
描述
當它被呼叫有如下兩個操作:
1,所有存在的開啟的連線都會被關閉
2,後臺垃圾回收也被關閉。
這個方法有效的前提是:startup方法在之前呼叫。
startup方法
方法名
startup方法
功能
用於初始化UDT庫
詳細介面
int startup(
);
返回值
成功:返回0
錯誤:返回UDT::ERROR(此版本總會成功)
描述
初始化庫,開啟垃圾回收執行緒,必須在所有UDT呼叫之前呼叫它,否則會造成記憶體洩露。
若多次呼叫則只有第一次有效。
close方法
方法名
close方法
功能
用於關閉一個UDT連線
詳細介面
int close(
UDTSOCKET u
);
返回值
成功:返回0
錯誤:返回UDT::ERROR
描述
它溫和地關閉UDT連線並釋放有關的資料結構,如果沒有連線,則只釋放與socket相關的資源。
在阻塞方式下,如果UDT_LINGER設為非0,則會等待發送快取傳送完或者時間到。非阻塞下立即關閉。
關閉socket會發送一個關閉訊息給另一端,通知其關閉連線,如果沒有成功傳送,對方也會在一個TIME_OUT的時間後關閉。
不用的socket都應該被關掉。
connect方法
方法名
connect方法
功能
用於連線到服務端socket(常規)或者peer side(集合點模式)
詳細介面
int connect(
UDTSOCKET u,
const struct sockaddr* name,
int* namelen
);
返回值
成功:返回0
錯誤:返回UDT::ERROR
描述
UDT是面向連線的。有兩種模式:SOCK_STREA,和SOCK_DARAM模式,
name是需要建立連線的服務端或者peer side。
C/S模式下服務端要呼叫bind和listen,在rendezvous模式下,兩端都要bind並同時連線。
UDT至少需要一個來回建立連線,這對於頻繁建立連線又斷開的應用是一個瓶頸。
當UDT_RCVSYN=false(接收不同步)connect會立即返回,並在後臺執行建立連線,執行緒可以用epoll來等待連線完成。
當失敗是sicket可以重連,失敗的socket也要用close來關閉。
epoll方法
方法名
epoll方法
功能
用於有效地對大量的socket輪詢IO事件
詳細介面
#ifndef WIN32
typedef int SYSSOCKET;
#else
typedef SOCKET SYSSOCKET;
#endif
int epoll_create();
int epoll_add_usock(const int eid, const UDTSOCKET usock, const int* events=NULL);
int epoll_add_ssock(const int eid, const UDTSOCKET ssock, const int* events=NULL);
int epoll_remove_usock(const int eid, const UDTSOCKET usock, const int* events=NULL);
int epoll_remove_ssock(const int eid, const UDTSOCKET ssock, const int* events=NULL);
int epoll_wait(const int eid, std::set* readfds, std::set* writefds, int64_t msTimeOut, std::set* lrfds=NULL, std::set* wrfds=NULL);
int epoll_release(const int eid);
返回值
成功:epoll_create返回epoll ID,epoll_wait返回準備好IO的socket數量
其他三個函式返回0.
錯誤:返回UDT::ERROR
描述
當執行緒需等待很多socket時,應該用epoll代替select和selectEx來查詢。
它也提供等待系統socket,這樣應用就可以同時接受UDT和TCP/UDP。
執行緒可以用epoll_create去建立一個epoll ID用epoll_add_usock和epoll_remove_usock去新增和刪除socket,如果已經存在,新增會被忽略。
新增無效的或者關閉的socket會引發錯誤。刪除不存在的則不會錯誤(忽略)
在linux上,開發者可以用EPOLLIN(read)和EPOLLOUT(write)以及EPOLLERR(異常)來檢視具體事件。可以建立多個epoll。
epoll_wait是一個timeout值
getlasterror方法
方法名
getlasterror方法
功能
用於獲得一個執行緒最近一次的UDT錯誤
詳細介面
ERRORINFO& getlasterror(
);
返回值
成功:返回0
錯誤:返回UDT::ERROR
描述
讀出執行緒中最近一次的錯誤。
getpeername方法
方法名
getpeername方法
功能
用於獲得peer side的地址
詳細介面
int getpeername(
UDTSOCKET u,
struct sockaddr* name,
int* namelen
);
返回值
成功:返回0,並將地址儲存在name變數中。
錯誤:返回UDT::ERROR
描述
前提是:UDT socket已經建立了連線。
getsockname方法
方法名
getsockname方法
功能
用於獲得相關DUT的本地地址
詳細介面
int getsockname(
UDTSOCKET u,
struct sockaddr* name,
int* namelen
);
返回值
成功:返回0,並將地址儲存在name變數中。
錯誤:返回UDT::ERROR
描述
呼叫該方法前,UDT socket必須明確地綁定了或隱式地連線了。
如果該呼叫在bind之後且在connect之前,IP地址會返回用於繫結的地址,如果在連線之後,返回的是peer side看到的地址。
例如:
有一個代理地址,連線後,將返回代理IP地址,不是本地地址,但不管哪種情況,返回的埠號是一樣的。
因為UDP是無連線的,用此呼叫返回0.0.0.0作為IP地址,而UDT是面向連線的,UDT會返回一個有效的IP地址(如果沒有代理)。
UDT暫時還沒有多重連線功能,當有多個網絡卡時會出錯。
getsockopt方法和setsockopt方法
方法名
getsockopt方法和setsockopt方法
功能
用於獲得和設定UDT的配置選項
詳細介面
int getsockopt(
UDTSOCKET u,
int level,
SOCKOPT optname,
char* optval,
int* optlen
);
int setsockopt(
UDTSOCKET u,
int level,
SOCKOPT optname,
const char* optval,
int optlen
);
返回值
成功:返回0。
錯誤:返回UDT::ERROR
描述
optname選項描述符
optval存放設定選項的指標
optlen是optval的長度
不是任何時候都可以設定這些選項
perfmon方法
方法名
perfmon方法
功能
用於取得內部協議引數和執行追蹤
詳細介面
int perfmon(
UDTSOCKET u,
TRACEINFO* perf,
bool clear=true
);
返回值
成功:返回0。並且將追蹤結果存放在trace中。
錯誤:返回UDT::ERROR
描述
追蹤最近一次的記錄,結果寫進TRACEINFO結構中。
有三種資訊可讀:
1, 自連線以來的連線總數
2, 自上次清除counts後的數
3, 立即引數值
recv方法和send方法
方法名
recv方法和send方法
功能
用於讀取資料到本地緩衝區和將執行緒緩衝區中的資料發出去。
詳細介面
int recv(
UDTSOCKET u,
char* buf,
int len,
int flags
);
int send(
UDTSOCKET u,
const char* buf,
int len,
int flags
);
返回值
成功:返回傳送或接受資料的大小。
錯誤:返回UDT::ERROR
描述
recv從協議buf讀len長度的資料,不夠的話,讀取存在的,在阻塞方式下recv等待buf中存入資料,非阻塞方式下立即返回error。
可以給recv設定UDT_RCVTIMEO
同理傳送一個大小一定的資料,如果傳送緩衝buff不夠存入一定len長度的資料,則傳送一部分。在阻塞方式下send等待buffer有空餘再發,非阻塞方式下返回錯誤。
sendfile方法和recvfile方法
方法名
sendfile方法和recvfile方法
功能
用於傳送部分或全部本地檔案資料和讀出一定數量資料到本地檔案
詳細介面
int64_t recvfile(
UDTSOCKET u,
fstream& ofs,
int64_t& offset,
int64_t size,
int block=366000
);
int64_t sendfile(
UDTSOCKET u,
fstream& ifs,
const int64_t& offset,
const int64_t size,
const int block=7320000
);
返回值
成功:返回傳送或接收的資料大小。
錯誤:返回UDT::ERROR
描述
sendfile通常是阻塞方式傳送,UDT_SNDSYN和UDTTIMEO都不影響它的傳送。
在peer side不一定非要用recvfile接收sendfile
recvfile呼叫前必須知道資料的大小,否則可能會造成死鎖。
recvmsg方法和sendmsg方法
方法名
recvmsg方法和sendmsg方法
功能
用於傳送和接收有效的資料報
詳細介面
int recvmsg(
UDTSOCKET u,
char* msg,
int len
);
int sendmsg(
UDTSOCKET u,
const char* msg,
int len,
int ttl=-1,
bool inorder=false
);
返回值
成功:返回傳送或接收的資料大小。
錯誤:返回UDT::ERROR
描述
recvmsg和sendmsg只能在SOCK_DGRAM模式下。
接收不夠的message會被丟棄。
在阻塞方式下和send跟recv一樣需等待buffer
TTL限定了送達的時間,時間到沒送達將被丟棄。
inorder引數決定了資料報的有序到達,先前的報文到達後來的才能發。
銳英源軟體開發經驗
UDT管理網路通訊,是UDP的增強版本,裡面有可靠的演算法解決了UDP得不可靠性。這些通用的介紹在這裡不詳細講,UDT原始碼特點是本段文章關注的重點。它有如下特點:
1、用C++方式實現了高效演算法,同時避免了C++的缺點。
2、合理地設計了類和類關係,是初學者學習面向物件的好目標。
3、用編譯巨集方式同時支援了Linux和Windows平臺。
4、指標的巧妙使用和C語言方式的完美結合,提供了基礎資料結構。
上述特點在銳英源研究時,印象非常深刻,我們銳英源也重視這些特點,製作了完美的剖析視訊,這些視訊對初學者適合,也對入門進階者適用,是提升能力的好選擇。