上一篇BLOG已經介紹了revolver RUDP的傳輸性能、基本的框架和接口,這篇文章我重點講述RUDP的實現細節。在RUDP的模塊中最爲重要的是其收發緩衝控制和CCC發送窗口控制、CCC發送慢啓動控制、CCC快恢復控制等幾個過程。(關於RUDP源代碼實現在revolver開源項目的RUDP目錄:點擊打開鏈接)
數據塊定義
//發送數據片
typedef struct tagRUDPSendSegment
{
uint64_t seq_; //塊序號
uint64_t push_ts_; //進入發送隊列的時刻
uint64_t last_send_ts_; //最後一次發送的時刻
uint16_t send_count_; //發送的次數
uint8_t data_[MAX_SEGMENT_SIZE]; //塊數據
uint16_t data_size_; //塊數據長度
}RUDPSendSegment;
typedef struct tagRUDPRecvSegment
{
uint64_t seq_; //塊序號
uint8_t data_[MAX_SEGMENT_SIZE]; //塊數據
uint16_t data_size_; //塊數據長度
}RUDPRecvSegment;
塊的最大尺寸爲MAX_SEGMENT_SIZE = 1408(不能大於MTU,一般MTU是1492)。爲了加快內存分配的速度,RUDP模塊中使用了對象池來保證塊對象的快速申請,對象池定義:ObjectPool<RUDPSendSegment, RUDP_SEGMENT_POOL_SIZE> SENDSEGPOOL;
ObjectPool<RUDPRecvSegment, RUDP_SEGMENT_POOL_SIZE> RECVSEGPOOL;
#define GAIN_SEND_SEG(seg) \
RUDPSendSegment* seg = SENDSEGPOOL.pop_obj();\
seg->reset()
#define RETURN_SEND_SEG(seg) \
if(seg != NULL)\
SENDSEGPOOL.push_obj(seg)
#define GAIN_RECV_SEG(seg) \
RUDPRecvSegment* seg = RECVSEGPOOL.pop_obj(); \
seg->reset()
#define RETURN_RECV_SEG(seg) \
if(seg != NULL)\
RECVSEGPOOL.push_obj(seg)
發送緩衝區
class RUDPSendBuffer
{
public:
...
//發送數據接口
int32_t send(const uint8_t* data, int32_t data_size);
//ACK處理
void on_ack(uint64_t ack_seq);
//NACK處理
void on_nack(uint64_t base_seq, const LossIDArray& loss_ids);
//定時器接口
void on_timer(uint64_t now_ts);
//檢查BUFFER是否可以寫入數據
void check_buffer();
...
public:
uint64_t get_buffer_seq() {return buffer_seq_;};
//設置NAGLE算法
void set_nagle(bool nagle = true){nagle_ = nagle;};
bool get_nagle() const {return nagle_;};
//設置發送緩衝區的大小
void set_buffer_size(int32_t buffer_size){buffer_size_ = buffer_size;};
int32_t get_buffer_size() const {return buffer_size_;};
...
protected:
IRUDPNetChannel* net_channel_;
//正在發送的數據片
SendWindowMap send_window_;
//正在發送的報文的丟包集合
LossIDSet loss_set_;
//等待發送的數據片
SendDataList send_data_;
//發送緩衝區的大小
int32_t buffer_size_;
//當前緩衝數據的大小
int32_t buffer_data_size_;
//當前BUFFER中最大的SEQ
uint64_t buffer_seq_;
//當前WINDOW中最大的SEQ
uint64_t cwnd_max_seq_;
//接收端最大的SEQ
uint64_t dest_max_seq_;
//速度控制器
RUDPCCCObject* ccc_;
//是否啓動NAGLE算法
bool nagle_;
}
其中send函數是數據寫入函數,在這個函數裏面,緩衝區對象先會對寫入的數據進行報文拼接成發送塊,讓發送數據儘量接近MAX_SEGMENT_SIZE,如果發送的數據大於MAX_SEGMENT_SIZE,也會進行MAX_SEGMENT_SIZE爲單元的分片。然後寫入到對應的發送緩衝列表send_data_當中。最後嘗試進行網絡發送。僞代碼如下:
int32_t RUDPSendBuffer::send(const uint8_t* data, int32_t data_size)
{
int32_t copy_pos = 0;
int32_t copy_size = 0;
uint8_t* pos = (uint8_t *)data;
uint64_t now_timer = CBaseTimeValue::get_time_value().msec();
if(!send_data_.empty()) //拼接報文,讓其接近MAX_SEGMENT_SIZE
{
//取出send_data_中的最後一片,如果它沒有達到MAX_SEGMENT_SIZE,數據追加到MAX_SEGMENT_SIZE大小爲止。
RUDPSendSegment* last_seg = send_data_.back();
if(last_seg != NULL && last_seg->data_size_ < MAX_SEGMENT_SIZE)
{
copy_size = MAX_SEGMENT_SIZE - last_seg->data_size_;
if( copy_size > data_size)
copy_size = data_size;
memcpy(last_seg->data_ + last_seg->data_size_, pos, copy_size);
copy_pos += copy_size;
pos += copy_size;
last_seg->data_size_ += copy_size;
}
}
//剩餘數據分成MAX_SEGMENT_SIZE爲單位的若干分片
while(copy_pos < data_size)
{
GAIN_SEND_SEG(last_seg);
//設置初始化的的時刻
last_seg->push_ts_ = now_timer; //記錄壓入時間戳
last_seg->seq_ = buffer_seq_;
buffer_seq_ ++;
//確定拷貝的塊長度
copy_size = (data_size - copy_pos);
if(copy_size > MAX_SEGMENT_SIZE)
copy_size = MAX_SEGMENT_SIZE;
memcpy(last_seg->data_, pos, copy_size);
copy_pos += copy_size;
pos += copy_size;
last_seg->data_size_ = copy_size;
//壓入發送隊列
send_data_.push_back(last_seg);
}
//記錄緩衝區的數據長度
buffer_data_size_ += copy_pos;
//嘗試發送,立即發送
attempt_send(now_timer);
return copy_pos;
}
這裏會觸發attempt_send()函數。這個函數是嘗試發送的核心函數。在後面的幾個過程裏面也會調用到這個函數。以上就是發送函數的過程。attempt_send函數僞代碼如下:
void RUDPSendBuffer::attempt_send(uint64_t now_timer)
{
uint32_t cwnd_size = send_window_.size();
uint32_t rtt = ccc_->get_rtt();
uint32_t ccc_cwnd_size = ccc_->get_send_window_size();
RUDPSendSegment* seg = NULL;
uint32_t send_packet_number = 0;
if(!loss_set_.empty()) //重發丟失的片段
{
//發送丟包隊列中的報文
uint64_t loss_last_ts = 0;
uint64_t loss_last_seq = 0;
for(LossIDSet::iterator it = loss_set_.begin(); it != loss_set_.end();) //檢查丟失報文是否要重發
{
if(send_packet_number >= ccc_cwnd_size) //超過發送窗口
break;
SendWindowMap::iterator cwnd_it = send_window_.find(*it);
if(cwnd_it != send_window_.end() && cwnd_it->second->last_send_ts_ + rtt < now_timer) //丟失報文必須在窗口中
{
seg = cwnd_it->second;
//UDP網絡發送
net_channel_->send_data(0, seg->seq_, seg->data_, seg->data_size_, now_timer);
if(cwnd_max_seq_ < seg->seq_)
cwnd_max_seq_ = seg->seq_;
//判斷是否可以更改TS
if(loss_last_ts < seg->last_send_ts_)
{
loss_last_ts = seg->last_send_ts_;
if(loss_last_seq < *it)
loss_last_seq = *it;
}
seg->last_send_ts_ = now_timer;
seg->send_count_ ++;
send_packet_number ++;
loss_set_.erase(it ++);
//報告CCC有重發
ccc_->add_resend();
}
else
++ it;
}
//更新重發包範圍內未重發報文的時刻,防止下一次定時器到來時重複發送
for(SendWindowMap::iterator it = send_window_.begin(); it != send_window_.end(); ++it)
{
if(it->second->push_ts_ < loss_last_ts && loss_last_seq >= it->first)
it->second->last_send_ts_ = now_timer;
else if(loss_last_seq < it->first)
break;
}
}
else if(send_window_.size() > 0)//丟包隊列爲空,重發所有窗口中超時的分片
{
//發送間時間隔閾值
uint32_t rtt_threshold = (uint32_t)ceil(rtt * 1.25);
rtt_threshold = (core_max(rtt_threshold, 30));
SendWindowMap::iterator end_it = send_window_.end();
for(SendWindowMap::iterator it = send_window_.begin(); it != end_it; ++it)
{
if(send_packet_number >= ccc_cwnd_size || (it->second->push_ts_ + rtt_threshold > now_timer))
break;
seg = it->second;
//重發塊的觸發條件是上一次發送的時間距離現在大於特定的閾值或者壓入時間很長並且是屬於發送緩衝區靠前的塊
if(seg->last_send_ts_ + rtt_threshold < now_timer
|| (seg->push_ts_ + rtt_threshold * 5 < now_timer && seg->seq_ < dest_max_seq_ + 3 && seg->last_send_ts_ + rtt_threshold / 2 < now_timer))
{
net_channel_->send_data(0, seg->seq_, seg->data_, seg->data_size_, now_timer);
if(cwnd_max_seq_ < seg->seq_)
cwnd_max_seq_ = seg->seq_;
seg->last_send_ts_ = now_timer;
seg->send_count_ ++;
send_packet_number ++;
//報告CCC有重發塊
ccc_->add_resend();
}
}
}
//判斷是否可以發送新的報文
if(ccc_cwnd_size > send_packet_number)
{
while(!send_data_.empty())
{
RUDPSendSegment* seg = send_data_.front();
//判斷NAGLE算法,NAGLE最少需要在100MS湊1024個字節報文
if(cwnd_size > 0 && nagle_ && seg->push_ts_ + NAGLE_DELAY > now_timer && seg->data_size_ < MAX_SEGMENT_SIZE - 256)
break;
//判斷髮送窗口
if(cwnd_size < ccc_cwnd_size)
{
send_data_.pop_front();
send_window_.insert(SendWindowMap::value_type(seg->seq_, seg));
cwnd_size ++;
seg->push_ts_ = now_timer;
seg->last_send_ts_ = now_timer;
seg->send_count_ = 1;
//UDP網絡發送
net_channel_->send_data(0, seg->seq_, seg->data_, seg->data_size_, now_timer);
if(cwnd_max_seq_ < seg->seq_)
cwnd_max_seq_ = seg->seq_;
}
else //發送窗口滿,則停止發送
break;
}
}
}
從上可得知,attempt_send是首先檢查是否可以發送丟失的報文,然後再檢查窗口中太老的報文是否要重發,最後才加入新的發送報文。所有的前提約束是不超過發送窗口。這個函數裏CCC決定的發送窗口大小和RTT直接控制着發送速度和發送策略。在這裏值得一提的是NAGLE的實現,RUDP爲了防止小包過多,實現了一個nagle算法,如果設置了此開關,假如只有1個塊在緩衝隊列中,會等數據達到1024的長度才進行發送。如果等100MS沒到1024長度也會發送,也就是最大等100MS.開關可以通過rudp
interface設置的。接收緩衝區
class RUDPRecvBuffer
{
public:
...
//來自網絡中的數據
int32_t on_data(uint64_t seq, const uint8_t* data, int32_t data_size);
//定時事件
void on_timer(uint64_t now_timer, uint32_t rtc);
//讀取BUFFER中的數據
int32_t read(uint8_t* data, int32_t data_size);
//檢查緩衝區是否可讀
void check_buffer();
//檢查丟包
bool check_loss(uint64_t now_timer, uint32_t rtc);
...
protected:
IRUDPNetChannel* net_channel_;
//接收窗口
RecvWindowMap recv_window_;
//已完成的連續數據片
RecvDataList recv_data_;
//丟包序列
LossIDTSMap loss_map_;
//當前BUFFER中最大連續數據片的SEQ
uint64_t first_seq_;
//當期BUFFER中受到的最大的數據片ID
uint64_t max_seq_;
//最後一次發送ACK的時刻
uint64_t last_ack_ts_;
//在上次發送ACK到現在,受到新的連續報文的標誌
bool recv_new_packet_;
...
};
在上面定義中,核心的函數主要是on_data和on_timer。on_data是接收來自發送端的RUDP數據報文,在這個函數裏面首先會進行接收到報文和緩衝去裏面的報文進行比較判斷是否丟包和重複包。如果有丟包,記錄到loss_map中。如果是重複包,則丟棄。如果接收到的包和緩衝區裏的報文可以組成連續的塊序列。則對上層觸發on_read讀事件。一下是這個函數的僞代碼:int32_t RUDPRecvBuffer::on_data(uint64_t seq, const uint8_t* data, int32_t data_size)
{
//報文合法性檢測
if(seq > first_seq_ + MAX_SEQ_INTNAL || data_size > MAX_SEGMENT_SIZE)
{
//報告異常
RUDP_RECV_DEBUG("on data exception!!");
net_channel_->on_exception();
return -1;
}
RUDPRecvSegment* seg = NULL;
if(first_seq_ + 1 == seq)//連續報文
{
recv_new_packet_= true;
//將數據緩衝到隊列中
GAIN_RECV_SEG(seg);
seg->seq_ = seq;
seg->data_size_ = data_size;
memcpy(seg->data_, data, data_size);
recv_data_.push_back(seg);
first_seq_ = seq;
//判斷緩衝區中的塊是否連續,並進行排序
check_recv_window();
//觸發可讀事件
net_channel_->on_read();
//刪除丟包
loss_map_.erase(seq);
}
else if(seq > first_seq_ + 1) //非連續報文
{
RecvWindowMap::iterator it = recv_window_.find(seq);
if(it == recv_window_.end()) //記錄到接收窗口中
{
//將數據緩衝到隊列中
GAIN_RECV_SEG(seg);
seg->seq_ = seq;
seg->data_size_ = data_size;
memcpy(seg->data_, data, data_size);
recv_window_[seq] = seg;
}
//判斷丟包
if(seq > max_seq_ + 1)
{
uint64_t ts = CBaseTimeValue::get_time_value().msec();
for(uint64_t i = max_seq_ + 1; i < seq; ++ i) //緩衝區中最大的報文和收到的報文之間的報文全部列入丟包範圍中,並記錄丟包時刻
loss_map_[i] = ts;
}
else
{
//刪除丟包
loss_map_.erase(seq);
}
}
//更新緩衝區最大SEQ
if(max_seq_ < seq)
max_seq_ = seq;
return 0;
}
on_timer是定時觸發的,一般是5MS觸發一次。主要是向發送端發送報告消息(ack/nack)、檢查緩衝區是否可讀兩個操作。發送ack狀態消息的條件是
if(last_ack_ts_ + rtc_threshold <= now_timer && recv_new_packet_){
void RUDPRecvBuffer::on_timer(uint64_t now_timer, uint32_t rtc)
{ //檢查丟包
if(check_loss(now_timer, rtc))
recv_new_packet_ = false;
//檢查是否需要發送ack
uint32_t rtc_threshold = core_min(20, rtc / 2);
if(last_ack_ts_ + rtc_threshold <= now_timer && recv_new_packet_)
send_ack();
//檢查緩衝區是否可讀
if(!recv_data_.empty() && net_channel_ != NULL)
net_channel_->on_read();
}
CCC核心控制
void RUDPCCCObject::set_rtt(uint32_t keep_live_rtt)
{
...
//第一次計算rtt和rtt修正
if(rtt_first_)
{
rtt_first_ = false;
rtt_ = keep_live_rtt;
rtt_var_ = rtt_ / 2;
}
else //參考了tcp的rtt計算
{
rtt_var_ = (rtt_var_ * 3 + core_abs(rtt_, keep_live_rtt)) / 4;
rtt_ = (7 * rtt_ + keep_live_rtt) / 8;
}
rtt_ = core_max(5, rtt_);
rtt_var_ = core_max(3, rtt_var_);
...
}