1 核心流程与交互关系表
发送端 NACK 实现的核心流程与交互关系如下表所示:
| 发送端核心操作 | 媒体接收端操作 |
|---|---|
1. 发送 RTP 报文,并将报文存入 packet_history_ 缓存队列 |
- |
| 2. 接收来自接收端的 RTCP NACK 报文 | 1. 检测 RTP 报文丢失,生成并发送 RTCP NACK 报文 |
3. 触发 RTPSender::OnReceivedNack 处理 NACK 反馈 |
- |
4. 调用 RTPSender::ReSendPacket 重发丢失的 RTP 报文 |
2. 接收重发的 RTP 报文,完成丢包恢复 |

2、核心函数走读
发送端 NACK 实现分为三大核心流程:RTP 报文缓存 、RTCP NACK 处理 、RTP 报文重发,以下将逐一拆解每个流程的关键函数与源码细节。
2.1 流程1:发送 RTP 报文并缓存到 packet_history_
发送端通过 Pacer( pacing 控制器)发送 RTP 报文时,会将媒体报文(需支持重传)存入 packet_history_ 队列,为后续重发提供数据来源。同时,需通过 SetStorePacketsStatus 配置队列长度,确保缓存能覆盖合理的重传窗口。
2.1.1 关键函数调用链
plaintext
ProcessThreadImpl::Process // 线程调度入口,触发 Pacer 处理
-> PacedSender::Process // Pacing 发送器主逻辑
-> PacingController::ProcessPackets // 控制报文发送节奏
-> PacketRouter::SendPacket // 路由报文到对应模块
-> ModuleRtpRtcpImpl2::TrySendPacket // RTP/RTCP 模块发送预处理
-> RtpSenderEgress::SendPacket // 最终发送 RTP 报文,并触发缓存
2.1.2 核心函数:RtpSenderEgress::SendPacket(报文发送与缓存触发)
cpp
// 函数功能:发送 RTP 报文到网络,并将可重传的媒体报文存入 packet_history_ 缓存
// 参数说明:
// *packet: 待发送的 RTP 报文对象
// options: 发送选项(如 QoS 优先级、是否允许重传等)
// pacing_info: Pacing 相关信息(如发送时间、比特率等)
// 返回值:bool - 报文是否成功发送到网络
const bool send_success = SendPacketToNetwork(*packet, options, pacing_info);
// 关键逻辑:无论发送是否成功,均处理报文缓存(确保重传时能找到报文)
// 条件1:is_media - 是否为媒体报文(非 RTCP、非 Padding 等)
// 条件2:packet->allow_retransmission() - 报文是否允许重传(由发送端配置决定)
if (is_media && packet->allow_retransmission()) {
// 将报文存入缓存,记录当前时间(用于后续 RTT 校验)
packet_history_->PutRtpPacket(
std::make_unique<RtpPacketToSend>(*packet), // 复制 RTP 报文
now_ms // 当前时间戳(毫秒),标记报文首次发送时间
);
}
// 处理重传报文的状态更新:若当前报文是重传报文,标记原报文为"已发送"
else if (packet->retransmitted_sequence_number()) {
packet_history_->MarkPacketAsSent(*packet->retransmitted_sequence_number());
}
2.1.3 核心函数:RtpPacketHistory::PutRtpPacket(报文缓存实现)
cpp
// 函数功能:将 RTP 报文存入缓存队列,以序列号(SequenceNumber)为索引,支持重传时快速查询
// 参数说明:
// packet: 待缓存的 RTP 报文(智能指针,确保内存安全)
// send_time_ms: 报文发送时间戳(可选,首次发送时传入)
void RtpPacketHistory::PutRtpPacket(
std::unique_ptr<RtpPacketToSend> packet,
absl::optional<int64_t> send_time_ms
) {
RTC_DCHECK(packet); // WebRTC 断言:确保 packet 非空(调试用)
MutexLock lock(&lock_); // 加锁,保证多线程下缓存操作线程安全
int64_t now_ms = clock_->TimeInMilliseconds(); // 获取当前系统时间
// 若缓存模式为"禁用",直接返回(不缓存任何报文)
if (mode_ == StorageMode::kDisabled) {
return;
}
// 断言:确保当前报文允许重传(与 RtpSenderEgress::SendPacket 中的条件一致)
RTC_DCHECK(packet->allow_retransmission());
// 清理过期报文:删除缓存中超过最大缓存时间/数量的报文,避免内存泄漏
CullOldPackets(now_ms);
// 1. 获取当前报文的序列号,计算其在缓存队列中的索引
const uint16_t rtp_seq_no = packet->SequenceNumber(); // RTP 报文序列号(16位)
int packet_index = GetPacketIndex(rtp_seq_no); // 根据序列号计算索引(队列位置)
// 2. 处理重复报文:若该序列号的报文已存在,删除旧报文(避免状态不一致)
if (packet_index >= 0 &&
static_cast<size_t>(packet_index) < packet_history_.size() &&
packet_history_[packet_index].packet_ != nullptr) {
RTC_LOG(LS_WARNING) << "Duplicate packet inserted: " << rtp_seq_no; // 打印警告日志
RemovePacket(packet_index); // 删除旧报文
packet_index = GetPacketIndex(rtp_seq_no); // 重新计算索引(旧报文删除后索引可能变化)
}
// 3. 扩展缓存队列:若索引小于0(报文序列号小于队列中所有报文),在队列头部插入空元素
for (; packet_index < 0; ++packet_index) {
packet_history_.emplace_front(nullptr, absl::nullopt, 0);
}
// 4. 扩展缓存队列:若索引超过队列长度(报文序列号大于队列中所有报文),在队列尾部插入空元素
while (static_cast<int>(packet_history_.size()) <= packet_index) {
packet_history_.emplace_back(nullptr, absl::nullopt, 0);
}
// 5. 断言:确保索引合法(防止越界访问)
RTC_DCHECK_GE(packet_index, 0); // 索引 >= 0
RTC_DCHECK_LT(packet_index, packet_history_.size()); // 索引 < 队列长度
RTC_DCHECK(packet_history_[packet_index].packet_ == nullptr); // 目标位置为空(无重复)
// 6. 存入缓存:创建 StoredPacket 对象,保存报文、发送时间和插入顺序
packet_history_[packet_index] = StoredPacket(
std::move(packet), // 转移报文所有权到缓存
send_time_ms, // 发送时间戳
packets_inserted_++ // 插入计数器(用于排序或清理优先级)
);
// 7. (可选)Padding 优先级处理:若启用 Padding 优先级,将报文加入优先级集合
if (enable_padding_prio_) {
// 若优先级集合超过最大长度,删除最后一个元素(LRU 策略)
if (padding_priority_.size() >= kMaxPaddingHistory - 1) {
padding_priority_.erase(std::prev(padding_priority_.end()));
}
// 将当前缓存的报文加入优先级集合
auto prio_it = padding_priority_.insert(&packet_history_[packet_index]);
RTC_DCHECK(prio_it.second) << "Failed to insert packet into prio set."; // 断言插入成功
}
}
2.1.4 缓存队列配置:SetStorePacketsStatus
packet_history_ 的缓存长度需根据媒体类型(视频/音频)分别配置,确保能覆盖典型的 RTT 窗口(避免重传时报文已被清理)。
-
视频缓存配置 :在
CreateRtpStreamSenders函数中初始化cpp// 创建 ModuleRtpRtcpImpl2 实例(RTP/RTCP 核心模块) std::unique_ptr<ModuleRtpRtcpImpl2> rtp_rtcp( ModuleRtpRtcpImpl2::Create(configuration) ); rtp_rtcp->SetSendingStatus(false); // 初始关闭发送状态 rtp_rtcp->SetSendingMediaStatus(false); // 初始关闭媒体发送状态 rtp_rtcp->SetRTCPStatus(RtcpMode::kCompound); // 启用复合 RTCP 模式(支持 NACK) // 启用缓存,设置最小缓存长度(kMinSendSidePacketHistorySize 为预定义常量,通常为 1000+) rtp_rtcp->SetStorePacketsStatus(true, kMinSendSidePacketHistorySize); -
音频缓存配置 :在
ChannelSend::RegisterSenderCongestionControlObjects函数中初始化cppvoid ChannelSend::RegisterSenderCongestionControlObjects( RtpTransportControllerSendInterface* transport, RtcpBandwidthObserver* bandwidth_observer ) { RTC_DCHECK_RUN_ON(&worker_thread_checker_); // 断言在工作线程执行 RtpPacketSender* rtp_packet_pacer = transport->packet_sender(); // 获取 Pacer PacketRouter* packet_router = transport->packet_router(); // 获取报文路由 RTC_DCHECK(rtp_packet_pacer); RTC_DCHECK(packet_router); RTC_DCHECK(!packet_router_); rtcp_observer_->SetBandwidthObserver(bandwidth_observer); // 设置带宽观察者 rtp_packet_pacer_proxy_->SetPacketPacer(rtp_packet_pacer); // 绑定 Pacer 代理 // 启用音频缓存,固定设置缓存长度为 600(音频报文小,缓存更多以应对高丢包) rtp_rtcp_->SetStorePacketsStatus(true, 600); constexpr bool remb_candidate = false; // 不作为 REMB(带宽估计)候选 packet_router->AddSendRtpModule(rtp_rtcp_.get(), remb_candidate); // 注册 RTP 模块到路由 packet_router_ = packet_router; }
2.2 流程2:处理接收端的 RTCP NACK 报文
接收端检测到 RTP 丢包后,会发送 RTCP NACK 报文(携带丢包序列号列表)。发送端通过 RTCP 接收模块解析该报文,提取丢包序列,并传递给 RTPSender 准备重传。

2.2.1 关键函数调用链
plaintext
RTCPReceiver::IncomingPacket // 接收 RTCP 报文(从网络层获取)
-> RTCPReceiver::ParseCompoundPacket // 解析复合 RTCP 报文(NACK 属于复合报文的一部分)
-> RTCPReceiver::TriggerCallbacksFromRtcpPacket // 触发 RTCP 报文回调(分发到对应处理器)
-> RTCPReceiver::HandleNack // 专门处理 NACK 报文,提取丢包序列号
-> ModuleRtpRtcpImpl::OnReceivedNack // 转发 NACK 信息到 RTPSender
-> RTPSender::OnReceivedNack // 最终处理 NACK,准备重传
2.2.2 核心函数:RTCPReceiver::HandleNack(解析 NACK 报文)
cpp
// 函数功能:解析 RTCP NACK 报文,提取丢包序列号,存入 packet_information 供后续处理
// 参数说明:
// rtcp_block: RTCP 报文头部(包含 NACK 报文的类型、长度等信息)
// packet_information: 输出参数,存储 NACK 相关信息(丢包序列、报文类型标记等)
void RTCPReceiver::HandleNack(
const CommonHeader& rtcp_block,
PacketInformation* packet_information
) {
rtcp::Nack nack; // NACK 报文解析对象
// 第一步:解析 RTCP 报文块,若解析失败(格式错误),跳过该报文并计数
if (!nack.Parse(rtcp_block)) {
++num_skipped_packets_; // 统计跳过的无效报文数
return;
}
// 第二步:校验 NACK 报文的目标 SSRC 是否匹配当前发送端的媒体 SSRC
// receiver_only_: 若当前是纯接收端(不发送媒体),忽略 NACK
// main_ssrc_: 当前发送端的媒体 SSRC(NACK 报文中的 media_ssrc 需与之匹配)
if (receiver_only_ || main_ssrc_ != nack.media_ssrc()) {
return; // 不是发给当前发送端的 NACK,忽略
}
// 第三步:提取 NACK 报文中的丢包序列号,存入 packet_information
// nack.packet_ids(): 返回丢包序列号列表(std::vector<uint16_t>)
packet_information->nack_sequence_numbers.insert(
packet_information->nack_sequence_numbers.end(),
nack.packet_ids().begin(),
nack.packet_ids().end()
);
// 第四步:更新 NACK 统计信息(用于监控和调试)
for (uint16_t packet_id : nack.packet_ids()) {
nack_stats_.ReportRequest(packet_id); // 记录每个丢包序列号的请求次数
}
// 第五步:标记 packet_information 的报文类型为"NACK",供后续回调识别
if (!nack.packet_ids().empty()) {
packet_information->packet_type_flags |= kRtcpNack; // 置位 NACK 标记
++packet_type_counter_.nack_packets; // 统计接收的 NACK 报文总数
packet_type_counter_.nack_requests = nack_stats_.requests(); // 统计总 NACK 请求数
packet_type_counter_.unique_nack_requests = nack_stats_.unique_requests(); // 统计唯一丢包数
}
}
2.2.3 核心函数:ModuleRtpRtcpImpl::OnReceivedNack(NACK 信息转发)
cpp
// 函数功能:将解析后的 NACK 丢包序列和 RTT 信息转发给 RTPSender,触发重传准备
// 参数说明:
// nack_sequence_numbers: 丢包序列号列表(从 NACK 报文中提取)
void ModuleRtpRtcpImpl::OnReceivedNack(
const std::vector<uint16_t>& nack_sequence_numbers
) {
// 检查 RTPSender 是否存在(若未初始化,忽略 NACK)
if (!rtp_sender_) {
return;
}
// 检查缓存是否启用且丢包列表非空(无缓存则无法重传,空列表无需处理)
if (!StorePackets() || nack_sequence_numbers.empty()) {
return;
}
// 计算 RTT(往返时间):用于后续重传频率控制(避免短时间内重复重传)
int64_t rtt = rtt_ms(); // 优先从 RtcpRttStats 获取 RTT(若已统计)
if (rtt == 0) { // 若 RTT 未统计,从 RTCPReceiver 中获取远程 SSRC 的 RTT
rtcp_receiver_.RTT(
rtcp_receiver_.RemoteSSRC(), // 接收端的 SSRC
NULL, // 忽略发送端到接收端的延迟
&rtt, // 输出 RTT(毫秒)
NULL, // 忽略抖动
NULL // 忽略延迟偏差
);
}
// 将 NACK 丢包序列和 RTT 传递给 RTPSender,触发重传逻辑
rtp_sender_->packet_generator.OnReceivedNack(nack_sequence_numbers, rtt);
}
2.3 流程3:重发 NACK 反馈的 RTP 报文
RTPSender 接收 NACK 丢包序列后,从 packet_history_ 中提取对应报文,校验重传条件(如 RTT 间隔、是否已在重传队列等),并通过 Pacer 以高优先级重发报文。
2.3.1 核心函数:RTPSender::OnReceivedNack(重传触发入口)
cpp
// 函数功能:处理 NACK 丢包序列,逐个触发报文重传
// 参数说明:
// nack_sequence_numbers: 丢包序列号列表
// avg_rtt: 平均 RTT(用于重传频率控制)
void RTPSender::OnReceivedNack(
const std::vector<uint16_t>& nack_sequence_numbers,
const int32_t avg_rtt
) {
// 设置 RTT 到缓存:缓存中用于校验重传间隔(避免短时间内重复重传)
// 5 + avg_rtt:增加 5ms 偏移,应对网络抖动
packet_history_->SetRtt(5 + avg_rtt);
// 遍历丢包序列号列表,逐个尝试重传
for (uint16_t seq_no : nack_sequence_numbers) {
// 调用 ReSendPacket 重传当前序列号的报文,返回重发的字节数
const int32_t bytes_sent = ReSendPacket(seq_no);
// 若重传失败(bytes_sent < 0),放弃后续所有丢包的重传(避免连锁失败)
if (bytes_sent < 0) {
RTC_LOG(LS_WARNING) << "Failed resending RTP packet " << seq_no
<< ", Discard rest of packets.";
break;
}
}
}
2.3.2 核心函数:RTPSender::ReSendPacket(报文重发实现)
该函数是重传逻辑的核心,包含 缓存校验 、重传通道选择 、优先级配置 三大关键逻辑。
cpp
// 函数功能:重发指定序列号的 RTP 报文,处理重传通道、优先级和速率限制
// 参数说明:
// packet_id: 待重传报文的序列号
// 返回值:int32_t - 重发的字节数(<0 表示重传失败)
int32_t RTPSender::ReSendPacket(uint16_t packet_id) {
// 第一步:查询报文在缓存中的状态(是否存在、是否已在重传队列)
absl::optional<RtpPacketHistory::PacketState> stored_packet =
packet_history_->GetPacketState(packet_id);
// 若报文不存在或已在重传队列,返回 0(无需处理)
if (!stored_packet || stored_packet->pending_transmission) {
return 0;
}
// 第二步:获取报文大小(用于速率限制和统计)
const int32_t packet_size = static_cast<int32_t>(stored_packet->packet_size);
// 第三步:判断是否使用 RTX 通道重传(RTX 是专门的重传通道)
// RtxStatus() & kRtxRetransmitted:检查 RTX 重传模式是否启用
const bool rtx = (RtxStatus() & kRtxRetransmitted) > 0;
// 第四步:从缓存中提取报文并标记为"待重传"(防止重复重传)
std::unique_ptr<RtpPacketToSend> packet =
packet_history_->GetPacketAndMarkAsPending(
packet_id,
// 匿名函数:处理报文封装(RTX 或普通通道)
[&](const RtpPacketToSend& stored_packet) -> std::unique_ptr<RtpPacketToSend> {
// 子步骤1:速率限制校验(避免重传占用过多带宽)
if (retransmission_rate_limiter_ &&
!retransmission_rate_limiter_->TryUseRate(packet_size)) {
// 若速率超限,返回空指针(重传失败)
return nullptr;
}
// 子步骤2:选择重传通道并封装报文
std::unique_ptr<RtpPacketToSend> retransmit_packet;
if (rtx) {
// 方案1:使用 RTX 通道重传(推荐)
// BuildRtxPacket:将原报文封装为 RTX 格式(携带原序列号和 SSRC)
retransmit_packet = BuildRtxPacket(stored_packet);
} else {
// 方案2:与普通媒体报文混传(不推荐,会影响丢包率统计)
retransmit_packet = std::make_unique<RtpPacketToSend>(stored_packet);
}
// 子步骤3:标记重传报文的原序列号(供接收端识别)
if (retransmit_packet) {
retransmit_packet->set_retransmitted_sequence_number(
stored_packet.SequenceNumber()
);
}
return retransmit_packet;
}
);
// 若提取报文失败(如速率超限、报文已过期),返回 -1(重传失败)
if (!packet) {
return -1;
}
// 第五步:配置重传报文的属性
packet->set_packet_type(RtpPacketMediaType::kRetransmission); // 标记为"重传报文"(用于优先级)
packet->set_fec_protect_packet(false); // 重传报文无需再做 FEC 保护(原报文已做)
// 第六步:将重传报文加入 Pacer 队列(按高优先级发送)
std::vector<std::unique_ptr<RtpPacketToSend>> packets;
packets.emplace_back(std::move(packet)); // 转移报文所有权到队列
paced_sender_->EnqueuePackets(std::move(packets)); // 加入 Pacer 发送队列
// 返回重发的字节数(成功)
return packet_size;
}
2.3.3 重传关键校验:RtpPacketHistory::GetPacketAndMarkAsPending(RTT 与状态校验)
cpp
// 函数功能:从缓存中提取报文,校验重传条件(RTT 间隔、是否已在重传),并标记状态
// 参数说明:
// sequence_number: 待提取报文的序列号
// encapsulate: 封装函数(用于 RTX 或普通通道处理)
// 返回值:std::unique_ptr<RtpPacketToSend> - 提取的报文(空指针表示失败)
std::unique_ptr<RtpPacketToSend> RtpPacketHistory::GetPacketAndMarkAsPending(
uint16_t sequence_number,
rtc::FunctionView<std::unique_ptr<RtpPacketToSend>(const RtpPacketToSend&)> encapsulate
) {
MutexLock lock(&lock_); // 线程安全锁
// 条件1:缓存禁用,返回空
if (mode_ == StorageMode::kDisabled) {
return nullptr;
}
// 条件2:获取缓存中的报文,不存在则返回空
StoredPacket* packet = GetStoredPacket(sequence_number);
if (packet == nullptr) {
return nullptr;
}
// 条件3:报文已在重传队列(pending_transmission_ 为 true),返回空(避免重复重传)
if (packet->pending_transmission_) {
return nullptr;
}
// 条件4:RTT 校验(避免短时间内重复重传,减轻网络负担)
int64_t now_ms = clock_->TimeInMilliseconds();
if (!VerifyRtt(*packet, now_ms)) {
// 校验失败:距离上次重传时间小于 RTT,返回空
return nullptr;
}
// 封装报文(RTX 或普通通道)
std::unique_ptr<RtpPacketToSend> encapsulated_packet = encapsulate(*packet->packet_);
// 若封装成功,标记报文为"待重传"
if (encapsulated_packet) {
packet->pending_transmission_ = true;
}
return encapsulated_packet;
}
// 辅助函数:RTT 校验逻辑
bool RtpPacketHistory::VerifyRtt(
const RtpPacketHistory::StoredPacket& packet,
int64_t now_ms
) const {
// 仅对已发送过的重传报文进行校验(首次重传无需校验)
if (packet.send_time_ms_ && // 报文有发送时间戳
packet.times_retransmitted() > 0 && // 已重传过至少一次
now_ms < *packet.send_time_ms_ + rtt_ms_) { // 当前时间 - 上次发送时间 < RTT
// 校验失败:短时间内重复重传,可能报文仍在网络中
return false;
}
return true;
}
2.3.4 重传优先级配置:GetPriorityForType(确保重传报文优先发送)
重传报文需按高优先级发送,以减少延迟。WebRTC 通过 RtpPacketMediaType 定义报文类型,再通过 GetPriorityForType 映射优先级。
cpp
// 函数功能:根据报文类型获取发送优先级(数值越小,优先级越高)
// 参数说明:
// type: 报文类型(音频、视频、重传、FEC、Padding 等)
// 返回值:int - 优先级数值
int GetPriorityForType(RtpPacketMediaType type) {
switch (type) {
case RtpPacketMediaType::kAudio:
// 音频优先级最高(实时性要求最高)
return kFirstPriority + 1;
case RtpPacketMediaType::kRetransmission:
// 重传报文优先级次之(需尽快恢复丢包,避免卡顿)
return kFirstPriority + 2;
case RtpPacketMediaType::kVideo:
// 普通视频报文优先级中等
return kFirstPriority + 3;
case RtpPacketMediaType::kForwardErrorCorrection:
// FEC(前向纠错)报文优先级低于普通视频
return kFirstPriority + 3;
case RtpPacketMediaType::kPadding:
// Padding(填充)报文优先级最低(无实际数据,仅用于带宽探测)
return kFirstPriority + 4;
default:
RTC_CHECK_NOTREACHED(); // 断言:无其他类型
}
}
// Pacer 队列中使用优先级:EnqueuePacket 时传入优先级,确保高优先级报文先发送
void PacingController::EnqueuePacket(std::unique_ptr<RtpPacketToSend> packet) {
RTC_DCHECK(pacing_bitrate_ > DataRate::Zero())
<< "SetPacingRate must be called before InsertPacket."; // 断言 Pacing 速率已配置
// 先获取报文优先级,再存入队列(避免报文移动后无法访问类型)
const int priority = GetPriorityForType(*packet->packet_type());
// 按优先级加入内部队列(Pacer 会优先发送低数值优先级的报文)
EnqueuePacketInternal(std::move(packet), priority);
}
2.3.5 RTX 通道配置(推荐)
RTX(Retransmission)是专门的重传通道,与普通媒体通道分离,可避免重传报文影响正常媒体的丢包率统计。启用 RTX 需配置以下三个核心参数:
cpp
// 1. 配置 NACK 缓存历史时间(确保重传时报文未被清理)
rtp_config_.nack.rtp_history_ms = kNackHistoryMs; // kNackHistoryMs 通常为 1000ms(1秒)
// 2. 配置 RTX 通道的 payload type(与普通媒体的 payload type 区分)
rtp_config_.rtx.payload_type = payload_type; // 如 96(需与接收端协商一致)
// 3. 配置 RTX 通道的 SSRC(与普通媒体的 SSRC 区分,避免混淆)
rtp_config_.rtx.ssrcs.push_back(rtx_ssrc); // 如 0x12345678(需唯一)
3、核心总结
发送端 NACK 是 WebRTC 保障实时媒体传输质量的关键机制,其核心逻辑可概括为以下三点:
-
报文缓存 :通过
packet_history_队列·34缓存可重传的 RTP 报文,按序列号索引,支持快速查询;同时根据媒体类型(视频/音频)配置合理的缓存长度,平衡内存占用与重传覆盖率。 -
NACK 处理:接收端通过 RTCP NACK 报文反馈丢包序列,发送端解析后提取丢包序列号,结合 RTT 信息控制重传节奏,避免短时间内重复重传。
-
优先级重传 :重传报文通过
RtpPacketMediaType::kRetransmission标记为高优先级,确保 Pacer 优先发送;推荐使用 RTX 专用通道重传,避免影响正常媒体的丢包率统计和带宽估计(GCC)。