一: 關於 oRTP linux
oRTP 是一款開源軟件,實現了 RTP 與 RTCP 協議。目前使用 oRTP 庫的軟件主要是linphone(一款基於IP 進行視頻和語音通話的軟件)。數組
oRTP做爲 linphone 的 RTP 庫,爲基於 RTP 協議傳輸語音和視頻數據提供保障。網絡
二: 源代碼的構建框架session
相似於 mediastream2 中的 filter,在RTP 中也有比較重要的一個結構,就是 payload type,該結構用於指定編碼類型,以及與其相關的時鐘速率、採樣率等一些參數,參見下圖。數據結構
圖 2-1 實際上在 RTP 的包頭就有專門的域用來定義當前傳輸的數據是什麼編碼類型的。在代碼中,不一樣的媒體類型有不一樣的 payloadtype 結構體與之對應,像 h263,g729,MPEG4等。由於每種編碼都有其獨有的特色,並且許多參數也不同,因此 RTP 包頭中使用 payload 域標記負載的類型,一方面接收端能夠就此判斷負載的類型,從而選擇對應的解碼器進行解碼播放;另外一方面,代碼在進行時間戳等方面的計算時能夠更加方便一點。app
Payloadtype結構體定義了 payload 的許多屬性,好比是音頻仍是視頻數據,時鐘採樣率, 每次採樣的比特數,正常的比特率,MIME類型,通道等等。代碼中已有常見音視頻編解碼器對應的 payloadtype結構體實現,應用程序在初始化 oRTP 庫時,能夠根據本身的需求, 選擇其中的一部分添加到系統中。全部系統當前支持的payload 類型都被放在一個數組中, 由全局變量 av_profile 這個結構體實例統領,以下圖所示:框架
圖 2-2 這些 payloadtype 結構體在payload 數組中的位置就是以編碼類型的定義爲索引的。編碼類型值的定義在RFC3551 第六部分「payload type definitions」進行了描述。Avprofile.c 文件定義了全部的payload type。而有關payload type 和 profile 的操做在文件payloadtype.c文件中實現。socket
除了 payloadtype 結構體外,一個更重要的結構體是 rtpsession。該結構體便是一個會話的抽象,與會話相關的各類信息都定義在該結構體上或者可以經過該結構體找到。要使用oRTP 進行媒體數據的傳輸,須要先建立一個會話,以後全部數據的傳輸都在會話上完成或基於會話完成。rtpsession結構體的定義以下:tcp
圖 2-3 能夠看到,這是一個很是大的結構體,從側面說明了要維護的與會話相關的量仍是比較多的。函數
關於該結構體的比較詳細的說明會在後面給出。Session 的初始化經過接口 rtp_session_init 完成,外部得到一個新的 session是經過調用接口rtp_session_new 完成。關於 session 的其餘有關配置和獲取信息的操做均可以在文件 rtpsession.c 中找到定義。
使用 oRTP 進行數據傳輸時,能夠在一個任務上完成多個會話流的接收和發送。這得益於 oRTP 中調度模塊的支持。要使用調度模塊,應用須要在進行 oRTP 的初始化時對調度進行初始化,將須要調度管理的會話註冊到調度模塊中,這樣當進行接收和發送操做時,先向調度詢問當前會話是否能夠進行發送和接收,若是不能進行收發操做,則處理下一個會話。 這有點相似I/O 接口上的 select 操做。調度模塊使用的數據結構主要爲 rtpscheduler,以下圖所示:
圖 2-4 List 保存了全部要處理的會話,r\w\e 的意義相似於 select,在這裏分別表明接收、發送以及異常。posixtimer.c,rtptimer.c,scheduler.c,sessionset.c等文件實現了調度模塊。數據在底層實際的接收和發送是經過 socket 接口完成的,這些實如今 rtpsession_inet.c 文件中。爲了方便將 oRTP 移植到不一樣平臺上,oRTP 實現了對操做系統接口的封裝,包括經常使用的任務的建立及銷燬,條件變量及互斥鎖,進程間的管道通訊機制等。這些在port.c 文件中實現。
除了操做系統相關的接口外,oRTP爲了便於內部操做,實現了部分數據結構,一個是雙向鏈表,在文件utils.c 中;一個是隊列,在文件 str_utilis.c文件中。鏈表的實現比較簡單, 隊列的實現相對較複雜一點。首先,隊列數據結構由三部分組成:隊列頭、消息塊以及數據 塊,圖示以下:
圖 2-5 上圖中從左到右依次爲隊列頭,消息塊和數據塊。隊列頭指向消息塊,消息塊之間能夠構成雙向鏈表,這是隊列的基本要素。消息塊自己不帶buffer,數據是由專門的數據塊來保存的, 並被消息塊指向。上圖是一個初始化後的狀態,消息塊的讀寫指針都指向數據塊的buffer 的開始位置。數據塊的 base 和lim 指針則分別指向 buffer 空間的開始地址和結束地址處。向 buffer 中寫入和讀出數據後的狀態變化以下圖:
圖 2-6 除了向隊列添加消息塊外,上述數據結構設計還支持向一個消息塊添加新的消息塊,這樣能夠支持一個消息塊保存較大塊的數據,以下圖所示:
圖 2-7 消息塊的 b_cont 指針用於鏈接新的消息塊。
在發送上層應用的 payload 數據以前,oRTP 會構造一個消息塊,數據指針會指向payload, 這避免了數據拷貝。較低層的接口處理數據時依賴於消息塊結構。接收後的數據從消息塊中 拷貝到用戶buffer。接收的 rtp和 rtcp 包的解析處理函數在文件 rtpparse.c 和 rtcpparse.c 文件中實現。另外,rtcp.c 文件實現了 rtcp 數據包的構造處理。
在基於 ip 的音視頻流傳輸中,防抖動能力是一個重要的特性,這在必定程度上可以保證用戶有良好的體驗。在 oRTP 中,是經過 jitter 模塊完成這部分工做的。相關數據結構以下圖所示:
圖 2-8 要使用 jitter 功能,須要使能 enabled 變量,若是要支持自適應補償,則須要使能 adaptive變量。對於數據傳輸過程當中產生的一些事件(好比ssrc 發生改變,數據爲 dtmf 數據等),在 oRTP中是經過signaltable(信號表)來處理的。signaltable 關聯了事件類型與其上的回調函數。 oRTP 使用signaltable處理以下一些事件:ssrc_changed(ssrc發生改變),payload_type_changed (payload type 發生改變),telephone-event_packet(telephone event包到達),telephone-event (telephone 事件),timestamp_jump(timestamp jump事件),network_error(網絡錯誤事件), 以及rtcp_bye(rtcp bye 包事件)。用戶可針對這些事件註冊回調處理函數,當底層接收函數接收到 rtp 包後,會對包進行檢查,發現是上述某些事件的話,則觸發回調函數的執行。rtpsignaltable.c 文件實現了對該表的操做,包括初始化,添加 callback 刪除 callback 以及執行 callback。
oRTP中對於事件的處理是基於事件結構體和事件隊列的。隊列用於存放事件結構體,結構體用於存放事件的數據。相關的處理在文件 event.c 中定義。特別的,對於 telephone 事件的處理放在 telephone_event.c 文件中,其中包括瞭如何構造用於傳輸telephone_event 的 rtp 包,如何將 telephone 事件添加到包中,如何發送dtmf 數據,以及接收到對應數據包後該如何處理。關於 telephone_event 的構成以下圖所示:
圖 2-9 最左邊的結構體是 rtp 包中存放的有關telephone event 的數據,經過 packet 指針能夠找到telephone event的詳細信息。最終放入事件隊列的也是 packet 指向的內容。
在使用oRTP 提供的 rtp 庫以前,須要先對其進行初始化,這部分的實如今 oRTP.c 文件中。oRTP的初始化主要調用兩個接口:ortp_init 和 ortp_scheduler_init。其中 ortp_init完成了 payload 的註冊,ortp_scheduler_init完成了調度任務的初始化。
三: 有關時間戳的說明
1 關於 RTP 傳輸中時間戳的說明(這部分來自於網絡)
時間戳單位:RTP協議中使用的時間戳,其單位不是秒之類的,而是以採樣頻率爲基礎的。這樣作的目的就是爲了使時間戳單位更爲精準。好比說一個音頻的採樣頻率爲 8000Hz, 那麼咱們能夠把時間戳單位設爲 1 / 8000。
時間戳增量:相鄰兩個 RTP 包之間的時間差(以時間戳單位爲基準)。採樣頻率: 每秒鐘抽取樣本的次數,例如音頻的採樣率通常爲8000Hz幀率:每秒傳輸或者顯示幀數,例如 25f/s 在 RTP 協議中並無規定時間戳的粒度,這取決於有效載荷的類型。所以RTP 的時間戳又稱爲媒體時間戳,以強調這種時間戳的粒度取決於信號的類型。例如,對於8kHz 採樣的話音信號,若每隔20ms 構成一個數據塊,則一個數據塊中包含有 160 個樣本(0.02× 8000=160)。所以每發送一個 RTP 分組,其時間戳的值就增長160。
若是採樣頻率爲 90000Hz,則由上面討論可知,時間戳單位爲 1/90000,咱們就假設1s 鐘被劃分了 90000 個時間塊,若是每秒發送 25 幀,那麼,每個幀的發送佔多少個時間塊呢?固然是90000/25 = 3600。所以,咱們根據定義「時間戳增量是發送第二個RTP 包相距發送第一個 RTP 包時的時間間隔」,故時間戳增量應該爲 3600。
關於 RTCP 中 NTP 時間戳的計算問題:從 1900 年到如今的通過的秒數賦值給 NTP 時間戳的高 32 位,這個時間的低 32 位經過當前獲取的納秒時間值計算獲得。將 1 秒劃分爲 2 的 32 次方來表示,則一份子持續的時間大約位 232 皮秒。若是當前時間爲 x 秒 232 毫秒, 則232毫秒爲232000微妙,232000000納秒,232000 000 000皮秒,即1000 000 000多個 232皮秒。也就是說在NTP時間戳的低32位劃分的2的32次方個232皮秒塊中佔用了1000 000 000個塊,轉換爲16進製表示爲3b9aca00,也就是說噹噹前時間的低位爲232毫秒的 話,NTP 時間戳的低 32 位就設置爲 3b9aca00。
在 linux 系統中,咱們經常使用的一個時間是 1970 年 1 月 1 日以來的時間所通過的秒數。在 RTCP 中,咱們能夠將當前所得到的上述時間加上83AA7E80(十六進制)就是 1900 年 1 月 1 日以來所通過的秒數了。換爲十進制,則爲 2208988800。計算方法爲(70 * 365 + 17) * 24 * 60 * 60。
2 代碼中有關時間戳變量的說明在數據的接收和發送過程當中,用到了許多記錄時間的變量。經過這些時間變量,oRTP完成對 rtp 數據的流控功能。全部這些變量都定義在 rtpstream結構體中,以下圖所示:(這裏只是截取了時間相關的變量)
圖 3-1 下面對這些變量的含義進行集中的說明:
uint32_t snd_time_offset;應用程序發送其第一個時間戳時的調度器時間
uint32_t snd_ts_offset;被應用程序發送的第一個應用程序時間戳
uint32_t snd_rand_offset; 添加到用戶offset 上的一個隨機數,用來產生流的時間戳
uint32_t snd_last_ts; 流上最後發送的時間戳
前述三個時間變量是 offset 結尾的,分別標記了第一個時間戳,包括調度器的時間偏移, 在應用開始發送數據時,應用發送數據的時間偏移,也便是本身的時間戳,還有一個隨機數用來添加到偏移上的,而第四個纔是真正標記流裏面當前最新發送的數據的時間戳。
uint32_t rcv_time_offset; 應用程序詢問其第一個時間戳時的調度時間,這裏詢問意指獲取接收到的數據包—此應該指開始接收數據時的調度器時間
uint32_t rcv_ts_offset;第一個流的時間戳----此應該指第一個rtp 包到來時其流上帶的時間戳值
uint32_t rcv_query_ts_offset;被應用程序詢問的第一個user時間戳—此應該指應用接收數據流時的時間
uint32_t rcv_last_ts; 應用程序獲得的流的最後一個時間戳—此應該指應用程序收到的最後一個rtp 包的時間戳,是包裏的時間戳值, 而非應用本身的時間。
uint32_t rcv_last_app_ts; 被應用程序詢問的最後一個應用程序時間戳—此處應該指應用收最後一個包時的應用時間,是應用按照 payload 類型及其採樣率增加的時間戳記錄,不是系統時間,也不是包裏的時間
uint32_t rcv_last_ret_ts; 最後一個返回的採樣的時間戳,僅僅對於連續的音頻而言
接收相對於發送來說存在一個問題,就是接收數據包時當前系統有個時間,數據包裏面也有時間戳記錄的時間,調度器也有記錄時間。而對於發送,當前應用的時間就是給包的時間戳時間,這兩個值對於發送來說是同樣的。
uint32_t hwrcv_extseq; 在socket 上最後接收的擴展的序列號
uint32_t hwrcv_seq_at_last_SR;每次發送報告包後,該變量更新爲hwrcv_extseq,所以是最近發送rtcp 報告包時的最高擴展序列號。
uint32_t hwrcv_since_last_SR;每收到一個 rtp 包,該變量加 1,在 rtcp 報告報構造好後, 該變量就清爲零,所以說明這個變量計數的是從上一個報告包以來接收的rtp 包數目。
根據上面三個變量就能夠計算出丟包率。首先,最近一次丟失包數(就是自從上一次sr 或者rr發送以來)經過hwrcv_extseq – hwrcv_seq_at_last_SR – hwrcv_since_last_SR計算獲得。 可是丟包率爲啥要除以hwrcv_since_last_SR 比較奇怪。這個值是自從上一次發送報告包以來累計接收的包數。這個值不該該就是指望接收的包數。(最高序列號減去最初序列號)
累計包丟失數經過每次的丟包數累加獲得。uint32_t last_rcv_SR_ts;最後一個接收到的 sr 的 NTP 時間戳,取的是中間的 32bit。這個值也是報告包中上 LSR 值的來源。
struct timeval last_rcv_SR_time;最後一個 sr 被接收到的時間,這個時間是用系統當前的時間來表示的。這個值記錄了接收到最後一個SR時的系統時間,再發送當前報告包時,再次獲取系統當前時間,而後兩者相減,獲得的值乘以65536 獲得以1/65536 爲單位的時間值。
uint16_t snd_seq; 發送序列號。累加變量,保存會話的序列號的 增加。
uint32_t last_rtcp_report_snt_r;最後一個rtcp 報告發送的時間,按照接收時間戳單位。程序中這個值是用 rcv_last_app_ts變量的值來更新的。就是應用最後一次進行 rtp 接收時其時間戳增加到的值。無論收沒收到就是這個值了?
uint32_t last_rtcp_report_snt_s;最後一個rtcp報告發送的時間,按照發送時間戳單位。程序中這個值是用snd_last_ts變量的值來更新的,就是應用最後一次進行rtp 發送操做時其時間戳增加到的值。無論有沒有發送 rtcp 報告包出去?
uint32_t rtcp_report_snt_interval; 按照時間戳單位表示的 rtcp 報告發送的間隔。這個值程序中使用默認時間值 5 秒與 payload的 clockrate 的乘積來表示。是否是計算過於簡單了?
uint32_t last_rtcp_packet_count; 在最後發送的一個rtcp sr包中記錄的發送者發送的 rtp 包總數。這個變量把這個值記錄了下來。記錄這個值是爲了實現協議中規定的:若是以前的rtcp 包發送以後到當前再次發送 rtcp 包, 這期間若是發送了rtp 包,則發送rtcp SR 報告包,不然只需發送 rtcp RR 包就能夠了。
uint32_t sent_payload_bytes; 用於rtcp 發送者報告的 payload 字節數,數據來源。這個變量保存了從開始發送到發送這個 rtcp 報告包時發送的字節總數,但不包括頭部和填充。
上面這些時間相關變量都是用於rtcp 包的。
unsigned int sent_bytes; 用於帶寬評估
struct timeval send_bw_start; 同上上面兩個變量用於計算髮送帶寬,start記錄的開始時間,sent_bytes 記錄了發送的字節數,該值沒調用 rtp 接口發送數據後都會進行累加更新。記錄一次帶寬值後,清爲零,以後進行下一次帶寬估計的計算。
unsigned int recv_bytes; 同上struct timeval recv_bw_start; 同上做用和處理邏輯都同上面發送部分。
四:調度的實現 要使用 oRTP 的調度功能,須要在初始化 oRTP 庫時調用接口 ortp_scheduler_init 對調度模塊進行初始化。在該接口中建立一個RtpScheduler 類型的結構體__ortp_scheduler(參見圖2--4),並調用rtp_scheduler_init 初始化它。
在 rtp_scheduler_init 中,分配定時器 posix_timer(rtptimer類型結構體,參見圖 2-4)掛載到調度結構體上。(定時器初始間隔設置爲POSIXTIMER_INTERVAL)。接着初始化 __ortp_scheduler 的其餘部分,包括初始化互斥鎖、條件變量等。在調度模塊運行的整個過程當中,相關操做都圍繞該結構體,__ortp_scheduler被定義爲全局變量。
初始化完後調用rtp_scheduler_start 啓動調度任務。調度任務的執行體爲 rtp_scheduler_schedule,參數爲調度結構體自身。
調度任務執行後,首先初始化 timer。在這過程當中將 timer 設置爲運行狀態,保存系統當前時間值。接着進入任務的while 循環,遍歷 scheduler 上註冊的全部會話。若是不爲空, 說明應用有會話須要調度管理。此時會調用 rtp_session_process 進行處理。全部須要調度管理的會話按上述邏輯處理完以後,廣播信號量unblock_select_cond 喚醒全部因等待 select而睡眠的任務,意即讓這些任務去檢查本身的會話是否須要進行處理了,這塊後續還會說明。此時調度器完成了本身當前的工做開始準備進入睡眠狀態,而其餘的任務開始檢查掩碼結果以決定是須要進行數據的收發仍是等待下次調度。
調度的睡眠是經過調用 timer 的 timer_do 接口來完成的,這裏就是posix_timer_do 接口。在該接口中,計算系統當前的時間,並和初始啓動的時間(調度器初始化時保存)作差運算, 結果轉換爲毫秒單位。posix_timer_time記錄了下一次調度器超時到達的時間,每次就讓 posix_timer_time減去系統當前時間與啓動時間的差值,若是大於零,說明調度時間尚未到達,就調用 select 等待(posix_timer_time-差值)時間,而後從新獲取系統當前時間,計算新的差值。流程圖以下:
圖 4-1 直觀一點來講就是,調度器的調度精度由 POSIXTIMER_INTERVAL肯定,每次調度器運行,若是處理會話集合(session set)的時間超過該間隔,就會接着處理下次調度,若是沒有用完,即剩餘diff時間,這點時間就經過 select 系統調用耗掉。所以,調度器每次進行調度的時間點基本是肯定的,diff時間根據處理會話集合消耗時間的不一樣,每次的大小都是 不同的。
調度任務每次都基本上會在固定點檢查全部須要由它來管理的會話,也就是應用添加到會話集合中的全部會話。若是在處理這些會話的過程當中,時間超過了調度器設置的默認間隔, 那麼調度器處理完本次循環後會接着進行下一輪的循環,不然,會等待,直到下一個調度點 時間到來。
調度器檢查每一個會話是經過 rtp_session_process 接口完成的。對於某一個會話,調用該接口將按以下邏輯進行處理:首先檢查會話的發送部分的 waitpoint 結構體,將其時間與調度器當前時間進行比較(上述結構體中的時間是收發接口設置的須要喚醒的時間點)。若是該會話須要進行喚醒,也就是在等待喚醒,並且其等待的喚醒點也到了,(就是當前調度器時間已經超過了喚醒點)則清除須要進行喚醒的標識,而後在調度器結構體(調度器初始化時建立的全局變量)的w_session 集合上將該會話的掩碼位置置位,並經過條件變量喚醒該任務。一樣的邏輯檢查r_session 集合。總的來看,調度器就是檢查各個會話設置的喚醒點是否到了。若是到了則喚醒並設置其在集合中的掩碼標誌位。這樣收發任務經過檢查掩碼標識位就知道是否能夠繼續進行收發了。一旦能夠收發,應用會再次將這些掩碼位置從新清除掉, 這樣在下次收發前就須要再次等待調度器進行檢查並設置。
上層應用經過調用接口 rtp_session_set_scheduling_mode 將一個 session 添加到調度器中。添加過程爲先得到調度器全局數據結構,給會話的 sched 指針,即該會話的 sched 指針指向全局調度器數據結構;會話flags添加 RTP_SESSION_SCHEDULED,意即讓調度器管理會話;最後調用 rtp_scheduler_add_session 接口將會話添加註冊到調度器管理的會話集合上。 rtp_scheduler_add_session 接口中,先將會話掛到調度器數據結構的會話鏈表上(調度器每次循環時就從該鏈表上獲取要處理的會話),而後在all_sessions 集合中找到一個空閒位置,記錄該掩碼位置,將當前會話在該集合中的掩碼位置進行置位操做。這樣調度器經過會話鏈表就能夠找到要調度的會話,進而找到會話上記錄的掩碼位置,從而在集合中對會話進行設置。相似的,將會話從集合中移除的接口爲rtp_scheduler_remove_session,基本處理邏輯就是找到會話列表中的該會話,將其從鏈表中移除,並把它在集合中的位置清零。
上層應用檢查是否須要收發數據是經過檢查會話集合來完成。首先,應用調用session_set_new 接口建立一個新的集合。在該接口中咱們建立一個SessionSet 結構體並將其初始化,後續的操做就在該結構體上完成。對於須要調度的會話,調用接口session_set_set將其在該集合中的掩碼位設置爲 1,也就是打上標識。應用在每次接收或者發送前,調用接口session_set_select檢查是否能夠發送或者接收。該接口會將 caller 掛起直到有事件到達。 session_set_select相似咱們經常使用的系統調用 select,其使用方式也是相似的。
Session_set_select是應用與調度器打交道比較重要的一個接口,下面看看它的實現:
首先調用ortp_get_scheduler 獲取到調度器全局結構體進入 while(1)循環
若是接收集合不爲空,(也就是要檢查是否有接收事件), 調用session_set_init 初始化一個臨時存放結果的集合調用session_set_and 檢查會話集合。處理基於三個量,一個是初始化時添加到調度中進行接收檢測的會話集合r_sessions(這個集合表明調度器能夠處理那些會話), 一個是用戶調用select 時進行檢查的會話集合,也就是應用要處理的集合(這個集合表明用戶要處理那些會話),一個就是當前調度處理的會話集合的最大值all_max (調度器從小到大檢查到 all_max 位置就檢查了其須要檢查的全部會話掩碼位)。在處理中,集合就是一個數組,數組每個元素的每個 bit 位表明了一個會話。這樣,以 all_max 爲上限,檢查每個會話對應的 bit 位,將調度器結構體上的接收集合和用戶集合進行與運算(注意:這裏接收集合是調度器處理完的,其中被設置的會話代表有接收事件到達。),獲得的結果既是調度器處理後能夠接收的會話, 也是在應用環境中添加了的要處理的會話,記爲result set。同時將接收集合中被添加到 result 集合中的位清除(由於已經獲取了)。最終 session_set_and接口返回 result 集合中被設置的 bit 位數,也就是實際能夠處理的會話個數。若是有會話的事件到達,即返回值大於零,將新的結果拷貝回用戶集合,告知用戶 那些會話有事件到達了。
對於發送和 error 集合作一樣相似的處理若是最終三個集合處理完後有事件(無論是接收仍是發送仍是error),則直接返回, 不然在條件變量上等待,直到調度器返回有事件到達。
跳到 While(1)進行下次循環處理
除了session_set_select 接口供用戶調用外,oRTP 還提供了帶有超時處理的 select 接口:session_set_timedselect,該接口能夠設置跳出時間,而不是像session_set_select 那樣爲死等模式。
綜合應用和調度器兩部分處理,能夠看出,調度器的精度(調度間隔)在必定程度上能夠影響數據接收速度。由於若是本次檢查會話上不能進行收發數據操做,那麼下次的檢查就必須等到下個調度點,即便在當前檢查剛過數據就到來了,應用也得等到下次調度點,由調度器檢查後應用才能知道,而這段時間數據就必須等待。這樣的話,若是調度間隔過大,那 麼接收速度必然減慢。
應用在收發數據時,除了可使用調度器管理會話外,還能夠設置阻塞與非阻塞模式。關於調度器與阻塞模式的關係:若是使用調度器,能夠不用管阻塞模式,即調度器能夠工做在阻塞模式下,也能夠工做在非阻塞模式下。若是要使用阻塞模式,則須要啓動調度器,這是必須的,即阻塞模式必須工做在調度器使用的狀況下。(由於阻塞功能的實現自己就依賴於調度器)。對於調度器啓動而且爲非阻塞模式,當數據不能收發時,上層任務能夠在應用層作其餘操做來等待。對於調度器啓動並設置爲阻塞模式,當數據不能收發時,上層應用任務會等待條件變量,該條件變量只有等到調度器 signal 以後,上層任務才能繼續運行。因此, 若是上層應用啓動了多個發送或者接收端口,那麼非阻塞模式下有一個或多個端口不能發送或者接收時,會嘗試其餘端口是否能夠發送,若是都不能使用,則能夠空循環。而阻塞模式下,若是有一個端口被阻塞了,那麼其餘端口都沒法進行數據的收發了,即必須等待該端口有事件並被調度器觸發後纔有機會進行其餘端口的發送或者接收。因此,在多接收發送應用 狀況下不該使用阻塞模式。
在非阻塞模式下,應用的等待時間消耗在 session_set_select 接口中了。阻塞模式下,應用可能就阻塞在發送接收接口中了。
使用目前的庫,存在一個問題,在使用調度的狀況下打開阻塞模式,則會致使程序掛住。具體緣由分析來在於,阻塞模式下,包發送時其喚醒時間點packet time在調度器scheduler time後面了,這樣調度器檢查時就認爲不須要進行喚醒,由於此時已經比調度器 old 了。根本緣由在於阻塞時等待調度器運行,致使調度器時間超過了 packet time。而非阻塞模式下, 包會直接發送出去,這樣其實包的暫緩發送是在下次,也便是下次select 等待時,調度器遇上包的發送時間,而後喚醒包發送,而阻塞模式下下次 select 時調度器已經遇上了並超過了包的發送時間。
關於調度器與應用的關係以下圖所示:
圖 4-2 調度器檢查 session set,喚醒到時間的接收流並設置掩碼位。應用檢查掩碼位獲得接收流是否被喚醒,而後進行接收處理,在接收處理中會清掉調度器設置的掩碼位。