理解RTCP:实时传输控制协议的简介与发送时间安排
1、简述
RTP实时传输协议,广泛应用于流媒体传输应用场景,根据rfc3550介绍,RTP协议应用场景有如下几种:
Ø 简单多播音频会议(Simple Multicast Audio Conference)
Ø 音频和视频会议(Audioand Video Conference)
Ø 混频器和转换器(MixersandTranslators)
Ø 分层编码(LayeredEncodings)
在实时音视频应用场合,考虑低延迟问题一般都使用RTP over UDP进行流媒体数据的传输,因此对于丢包、延迟、流畅性的考虑,发送端必须了解发送出去的流媒体数据到达对端的统计信息,RTP 控制协议 RTCP,就是用于监控服务质量和传达关于在一个正在进行的会议中的参与者的信息,包括对抗卡顿、网络拥塞控制扩展功能的实现,均利用RTCP报文实现,有名的是Google的GCC(拥塞控制算法(Google Congestion Control,简称GCC[1]))),RTCP的Nack、fir、pli报文都是实现抗丢包的策略,详细可查看rfc4585;
2、RFC3550中关于RTCP的介绍
2.1、基本场景介绍
RTP 控制协议(RTCP)向会议中所有成员周期性发送控制包。它使用与数据包相同的传输机制。底层协议必须提供数据包和控制包的复用,例如用不同的 UDP 端口或相同的UDP端口(采用复用模式下)。RTCP 提供以下四个功能:
Ø 基本功能是提供数据传输质量的反馈;
这是 RTP 作为一种传输协议的主要作用,它与其他协议的流量和拥塞控制相关。反馈可能对自适应编码有直接作用,并且 IP 组播的实验表明它对于从接收端得到反馈信息以诊断传输故障也有决定性作用。向所有成员发送接收反馈可以使"观察员"评估这些问题是局部的还是全局的。利用类似多点广播的传输机制,可以使某些实体,诸如没有加入会议的网络业务观察员,接收到反馈信息并作为第三方监视员来诊断网络故障。反馈功能通过 RTCP 发送者和接收者报告实现。
Ø RTCP 为每个 RTP 源传输一个固定的识别符,称为规范名(CNAME);
由于当发生冲突或程序重启时 SSRC可能改变,接收者要用 CNAME 来跟踪每个成员。接收者还要用 CNAME 来关联一系列相关 RTP 会话中来自同一个成员的多个数据流,例如同步语音和图像。
Ø 1和2功能要求参与方都发送RTCP报文,为保证会议参与方增长,必须严格控制发送包速率,避免过多占用本端带宽,导致视频质量差;
通过让每个成员向所有成员发送控制包,各个成员都可以独立地观察会议中所有成员的数目。此数目可以用来估计发包速率。
Ø 传输最少的会议控制信息;
例如在用户接口中显示参与的成员。这最可能在"松散控制"的会议中起作用,在"松散控制"会议里,成员可以不经过资格控制和参数协商而加入或退出会议。
2.2、包格类型介绍
类型
类型简称
描述
RFC文档
192
FIR
关键帧重传请求(IDR帧,无需参考帧可解码)
RFC2032
193
NACK
否定确认,NACK重传(丢包)
RFC2032
194
SMPTETC
SMPTE time-code 映射
RFC5484
195
IJ
extended inter-arrival jitter report.
RFC5450
200
SR
发送者报告,描述作为活跃发送者成员的发送和接收统计数字
RFC3550
201
RR
接收者报告,描述非活跃发送者成员的接收统计数字;
RFC3550
202
SDES
源描述项,其中包括规范名 CNAME。
RFC3550
203
BYE
表明参与者将结束会话。
RFC3550
204
APP
应用描述功能
RFC3550
205
RTPFB
通用RTP反馈
RFC4585
206
PSFB
PLI
有效载荷比
RFC4585
SLI
RPSI
207
XR
RTCP扩展
RFC3611
208
AVB
AVB RTCP数据包
IEEE1733
209
RSI
接收端汇总信息
RFC5760
注:其中206Type的PSFB中又拥有三个子项,详见rfc4585第六章
PSFB(Payload-Specific FB)消息被定义为载荷类型为PSFB的RTCP消息;
PLI:The PLI FB messageis identified by PT=PSFB and FMT=1. Picture Loss Indication,为整个图像帧丢失后发送
SLI:The SLI FB messageis identified by PT=PSFB and FMT=2. Slice Loss Indication,为帧内部分块损坏后发送
RPSI:The RPSI FB messageis identified by PT=PSFB and FMT=3. Reference Picture Selection Indication
其中各种类型报文均有用途,一般使用中各种类型有如下用途:
I 、关键帧请求
主要包括SLI/PLI/FIR,集中报文手段,目的是在关键帧丢失无法解码时,请求发送方重新生成并发送一个关键帧。本质是一种重传,但是跟传输层的重传的区别是,它重传是最新生成的帧。
PLI 是Picture LossIndication,SLI 是Slice Loss Indication。发送方接收到接收方反馈的PLI或SLI需要重新让编码器生成关键帧并发送给接收端。
FIR 是Full Intra Request,这里面Intra的含义可能很多人不知道。Intra的含义是图像内编码,不需要其他图像信息即可解码;Inter指图像间编码,解码需要参考帧。故Intra Frame其实就是指I帧,Inter Frame指P帧或B帧。
那么为什么在PLI和SLI之外还需要一个FIR呢?原因是使用场景不同,FIR更多是在一个中心化的Video Conference中,新的参与者加入,就需要发送一个FIR,其他的参与者给他发送一个关键帧这样才能解码,而PLI和SLI的含义更多是在发生丢包或解码错误时使用。
II 、重传请求
主要包括RTX/NACK/RPSI
这个重传跟关键帧请求的区别是它可以要求任意帧进行重传
III、码率控制
主要包括REMB/TMMBR/TMMBN
TMMBR是Temporal Max MediaBitrate Request,表示临时最大码率请求。表明接收端当前带宽受限,告诉发送端控制码率。
REMB是ReceiverEstimatedMax Bitrate,接收端估计的最大码率。
TMMBN是Temporal Max MediaBitrate Notification
另外,除了关键帧请求和重传,Webrtc还支持RED/FEC等冗余编码和前向纠错手段来保证视频质量。
以上报文用途,摘录自http://blog.****.net/wangruihit/article/details/47041515的博客;
2.3、RTCP传输时间间隔
为保证会议参与方人数稳定上升,必须对RTCP报文带宽进行管理控制,如一个上千人的会议,就必须对这个进行管理考虑,否则单端带宽占用严重,但对于MCU集中混屏会议来说,客户端不需要做这点,需要服务器去做。
RTP 被设计为允许应用自动适应不同的规模的会话――从几个参与者到几千个参与者的会话。对每一个会话,我们假定数据传输受到一个上限――会话带宽的限制。会话带宽分配给所有的参与者。这个带宽会被预留,并由网络所限制。如果没有预留,基于环境的其他约束将会确定合理的最大带宽供会话使用,这就是会话带宽。会话带宽在一定程度上独立于媒体编码,但媒体编码却依赖于会话带宽。
此参数由单个发送者选择的编码方式的数据带宽算出。会话管理可能会基于多播范围的规则或其他标准确定带宽限制。所有的参与者应使用相同的会话带宽值以保证计算出相同的 RTCP 间隔。
控制传输带宽应当是会话带宽的一小部分,这部分所占总的会话带宽的百分比应是已知的一小部分;
传输协议的首要功能是传输数据;已知:控制传输带可以被放进带宽描述中提供给资源预留协议,并且使每个参与者都可以独立的计算出他所占有的带宽份额。
控制传输带宽作为额外的一部分加入到会话带宽中。建议 RTCP 控制传输带宽为 RTCP 会话带宽的 5%。其中的 1/4 分配给发送者;当发送者的比例超过所有参与者的 1/4 时,其 RTCP 控制带宽相应增加。所有的会话参与者必须使用相同的常数(以上提到的百分比),以便计算出相同的发送时间间隔。这些常数应在一个特殊的描述文件中确定。
计算出的 RTCP 复合包的发送时间间隔应该有一个下限,以免参与者数量较少时大量发送 RTCP 包。这也使网络暂时断开时,发送间隔不会太小。在应用开始时,一个延迟应加到第一个的 TCP 复合包发送之前,以便从其他参与者接收 RTCP 复合包。这样,发送时间间隔能更快的收敛到正确的值。这个延迟可以设为最小时间间隔的一半。固定的时间间隔建议为 5 秒。
一个实现可能使 RTCP 最小发送时间间隔与会话带宽参数成比例,则应满足下列约束:
1、对多播会话,只有活动的数据发送者使用减小的最小化的值计算 RTCP 复合包的发送时间间隔。
2、对单播会话,减小的值也可能被不是活动的数据发送者使用,发送初始的 RTCP 复合包之前的延迟可能是 0。
3、 对所有会话,在计算参与者的离开时间时,这个固定最小值会被用到。因此,不使用减小的值进行 RTCP包的发送,就不会被其他参与者提前宣布超时。
4、减小的最小时间间隔建议为:360/sb(秒),其中 sb:会话带宽(千字节/秒)。当sb>72kb/s 时,最小时间间隔将小于 5s。
5、计算出的 RTCP 包的时间间隔与组中参与者的人数成正比。(参与者越多,发送时间间隔越长,每个参与者占有的 RTCP 带宽越小)。
6、 RTCP 包的(真实)时间间隔是计算出的时间间隔的 0.5~1.5 倍之间某个随机的值,以避免所有的参与者意外的同步。
7、RTCP 复合包的平均大小将会被动态估计,包括所有发送的包和接收的包。以自动适应携带的控制信息数量的变化。
8、由于计算出的时间间隔依赖于组中的人数。因此,当一个的用户加入一个已经存在的会话或者大量的用户几乎同时加入一个新的会话时,就会有意外的初始化效应。这些新 用户将在开始时错误的估计组中的人数(估计太小)。因此他们的 RTCP 包的发送时间间隔就会太短。如果许多用户同时加入一个会话,这个问题就很重要了。为了处理这处问题考虑了一种叫“时间重估”的算法。这个算法使得组中人数增加时,用户能够支持 RTCP 包的传输。当有用户离开会话,不管是发送 BYE 包还是超时,组中的人数会减少。计算出的时间间隔也应当减少。
因此,应用“逆向重估”算法,使组中的成员更快的减少他们的时间间隔,以对组中的人数减少做出响应。
9、BYE 包的处理和其他 RTCP 包的处理不同。BYE 包的发送用到一个“放弃支持”算法。以避免大量的 BYE包同时发送,使大量参与者同时离开会话。
这个算法适用于所有参与者都允许 RTCP 包的情况。此时,会话带宽=每个发送者的带宽×会话中参与者的总人数。
同时针对不同类型的RTCP报文,发送间隔时间不同,这样适应性更强,如语音包的反馈报文比视频包的间隔长,Tmmbr需立即发送,SSRC报文较短。
3、WebRtc中RTCP报文发送控制
Webrtc中RTCP报文发送间隔控制是相当好的基本处理流程如下:
RTCPSender::TimeToSendRTCPReport(boolsendKeyframeBeforeRTP)接口中写道
对于音频,我们使用一个固定的5秒间隔。在带宽小于360 kbit/s时,视频使用1秒的时间间隔,在视频带宽低于10kbit/s时,表面上我们打破最大5% RTCP 带宽限制,但那应该是非常罕见的。
RFC3550写道
最大的RTCP带宽应是会会话带宽的5%,一个SR报文在包含CNAME时,大约是65个字节,一个RR报文大约为28字节。
最小发送间隔的值推荐为360/会话带宽,发送间隔单位为秒,带宽单位为kbps,这个最低在带宽大于72 kb / s时,这个最小值小于5秒。如果参与者尚未发送RTCP包(包已经初始化),则常量Tmin设置为2.5秒,否则设置为5秒。
RTCP包的发送间隔是在[ 0.5,1.5 ]倍计算值之间随机变化的,避免所有参与者同步出现的意外情况
-
如果我们需要发送报文
参与者是发送者(we_sent为真),常数c设置为RTCP包大小平均值(avg_rtcp_size)除以RTCP带宽的25%(rtcp_bw),和常数n设置为发送的方数。
-
如果我们只接收报文
如果we_sent是不为真,常数c设置为RTCP包大小平均值(avg_rtcp_size)除以RTCP带宽的75%。常数n被设置为接收的方数(members - senders)。如果发件人 senders的数量大于25%,senders和members一起处理。
P2p不需要重新考虑
“timer reconsideration”是使用的.该算法实现了一种简单的回退机制,会导致用户阻止RTCP包传输如果群体规模正在增加。
N = number of members
C = avg_size /(rtcpbw / 4)
确定性计算区间Td设置为最大(Tmin,N×C)。
计算出的区间t被设置为均匀分布在确定计算区间的0.5到1.5倍之间的数。
由此产生的t值除以3 / 2 = 1.21828,弥补了定时器复议算法收敛到一个值低于预期的平均RTCP带宽
位于rtcp_sender.cc中
以下为WebRtc中计算法源码:
void RTCPSender::PrepareReport(const std::set<RTCPPacketType>& packetTypes,
const FeedbackState& feedback_state){
// Add all flags as volatile. Non volatile entries will not be overwritten
// and all new volatile flags added will be consumed by the end of this call.
SetFlags(packetTypes, true);
if (packet_type_counter_.first_packet_time_ms== -1)
packet_type_counter_.first_packet_time_ms= clock_->TimeInMilliseconds();
bool generate_report;
if (IsFlagPresent(kRtcpSr)|| IsFlagPresent(kRtcpRr)){
// Report type already explicitly set, don't automatically populate.
generate_report = true;
RTC_DCHECK(ConsumeFlag(kRtcpReport)== false);
} else {
generate_report =
(ConsumeFlag(kRtcpReport)&& method_ == RtcpMode::kReducedSize)||
method_ == RtcpMode::kCompound;
if (generate_report)
SetFlag(sending_? kRtcpSr : kRtcpRr, true);
}
if (IsFlagPresent(kRtcpSr)|| (IsFlagPresent(kRtcpRr)&& !cname_.empty()))
SetFlag(kRtcpSdes, true);
if (generate_report) {
if (!sending_ && xr_send_receiver_reference_time_enabled_)
SetFlag(kRtcpXrReceiverReferenceTime, true);
if (feedback_state.has_last_xr_rr)
SetFlag(kRtcpXrDlrrReportBlock, true);
// generate next time to send an RTCP report
uint32_t minIntervalMs = RTCP_INTERVAL_AUDIO_MS;
if (!audio_) {
if (sending_) {
// Calculate bandwidth for video; 360 / send bandwidth in kbit/s.
uint32_t send_bitrate_kbit = feedback_state.send_bitrate/ 1000;
if (send_bitrate_kbit !=0)
minIntervalMs =360000 / send_bitrate_kbit;
}
if (minIntervalMs > RTCP_INTERVAL_VIDEO_MS)
minIntervalMs = RTCP_INTERVAL_VIDEO_MS;
}
// The interval between RTCP packets is varied randomly over the
// range [1/2,3/2] times the calculated interval.
uint32_t timeToNext =
random_.Rand(minIntervalMs* 1 / 2, minIntervalMs * 3/ 2);
next_time_to_send_rtcp_ = clock_->TimeInMilliseconds()+ timeToNext;
StatisticianMap statisticians =
receive_statistics_->GetActiveStatisticians();
RTC_DCHECK(report_blocks_.empty());
for (auto& it: statisticians){
AddReportBlock(feedback_state, it.first, it.second);
}
}
}
————————————————
版权声明:本文为****博主「风清_云扬」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.****.net/DittyChen/article/details/78065974
推荐阅读
-
理解RTCP:实时传输控制协议的简介与发送时间安排
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET