ortp使用详解1

一: 关于 oRTP

oRTP 是一款开源软件,实现了 RTP 与 RTCP 协议。目前使用 oRTP 库的软件主要是linphone(一款基于IP 进行视频和语音通话的软件)。

oRTP作为 linphone 的 RTP 库,为基于 RTP 协议传输语音和视频数据提供保障。

二: 源代码的构建框架

类似于 mediastream2 中的 filter,在RTP 中也有比较重要的一个结构,就是 payload type,该结构用于指定编码类型,以及与其相关的时钟速率、采样率等一些参数,参见下图。

图 2-1 实际上在 RTP 的包头就有专门的域用来定义当前传输的数据是什么编码类型的。在代码中,不同的媒体类型有不同的 payloadtype 结构体与之对应,像 h263,g729,MPEG4等。因为每种编码都有其独有的特点,而且许多参数也不一样,所以 RTP 包头中使用 payload 域标记负载的类型,一方面接收端可以就此判断负载的类型,从而选择对应的解码器进行解码播放;另一方面,代码在进行时间戳等方面的计算时可以更加方便一点。

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文件中实现。

除了 payloadtype 结构体外,一个更重要的结构体是 rtpsession。该结构体即是一个会话的抽象,与会话相关的各种信息都定义在该结构体上或者能够通过该结构体找到。要使用oRTP 进行媒体数据的传输,需要先创建一个会话,之后所有数据的传输都在会话上完成或基于会话完成。rtpsession结构体的定义如下:

图 2-3 可以看到,这是一个非常大的结构体,从侧面说明了要维护的与会话相关的量还是比较多的。

关于该结构体的比较详细的说明会在后面给出。Session 的初始化通过接口 rtp_session_init 完成,外部获得一个新的 session是通过调用接口rtp_session_new 完成。关于 session 的其他有关配置和获取信息的操作都可以在文件 rtpsession.c 中找到定义。

使用 oRTP 进行数据传输时,可以在一个任务上完成多个会话流的接收和发送。这得益于 oRTP 中调度模块的支持。要使用调度模块,应用需要在进行 oRTP 的初始化时对调度进行初始化,将需要调度管理的会话注册到调度模块中,这样当进行接收和发送操作时,先向调度询问当前会话是否可以进行发送和接收,如果不能进行收发操作,则处理下一个会话。 这有点类似I/O 接口上的 select 操作。调度模块使用的数据结构主要为 rtpscheduler,如下图所示:

图 2-4 List 保存了所有要处理的会话,rwe 的意义类似于 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,唤醒到时间的接收流并设置掩码位。应用检查掩码位得到接收流是否被唤醒,然后进行接收处理,在接收处理中会清掉调度器设置的掩码位。

原文地址:https://www.cnblogs.com/elisha-blogs/p/4029412.html