SRS学习 - rtc转rtmp流程分析_webrtc推流到rtmp,十年Golang编程开发生涯

73 阅读6分钟
local_sdp.set\_fingerprint\_algo("sha-256");
local_sdp.set\_fingerprint(_srs_rtc_dtls_certificate->get\_fingerprint());

// We allows to mock the eip of server.
if (true) {
    int listen_port = _srs_config->get\_rtc\_server\_listen();
    set<string> candidates = discover\_candidates(ruc);
    for (set<string>::iterator it = candidates.begin(); it != candidates.end(); ++it) {
        string hostname; int port = listen_port;
        srs\_parse\_hostport(\*it, hostname,port);
        local_sdp.add\_candidate(hostname, port, "host");
    }

    vector<string> v = vector<string>(candidates.begin(), candidates.end());
    srs\_trace("RTC: Use candidates %s", srs\_join\_vector\_string(v, ", ").c\_str());
}

// Setup the negotiate DTLS by config.
local_sdp.session_negotiate_ = local_sdp.session_config_;

// Setup the negotiate DTLS role.
if (ruc->remote_sdp_.get\_dtls\_role() == "active") {
    local_sdp.session_negotiate_.dtls_role = "passive";
} else if (ruc->remote_sdp_.get\_dtls\_role() == "passive") {
    local_sdp.session_negotiate_.dtls_role = "active";
} else if (ruc->remote_sdp_.get\_dtls\_role() == "actpass") {
    local_sdp.session_negotiate_.dtls_role = local_sdp.session_config_.dtls_role;
} else {
    // @see: https://tools.ietf.org/html/rfc4145#section-4.1
    // The default value of the setup attribute in an offer/answer exchange
    // is 'active' in the offer and 'passive' in the answer.
    local_sdp.session_negotiate_.dtls_role = "passive";
}
local_sdp.set\_dtls\_role(local_sdp.session_negotiate_.dtls_role);

session->set\_remote\_sdp(ruc->remote_sdp_);
// We must setup the local SDP, then initialize the session object.
session->set\_local\_sdp(local_sdp);
session->set\_state(WAITING_STUN);

// Before session initialize, we must setup the local SDP.
if ((err = session->initialize(req, ruc->dtls_, ruc->srtp_, username)) != srs_success) {
    return srs\_error\_wrap(err, "init");
}

// We allows username is optional, but it never empty here.
_srs_rtc_manager->add\_with\_name(username, session);

return err;

}


src/app/srs\_app\_rtc\_conn.cpp大约1970行 SrsRtcConnection::add\_publisher 进行媒体协商,并创建SrsRtcSource对象和SrsRtcPublisherStream对象



srs_error_t SrsRtcConnection::add_publisher(SrsRtcUserConfig* ruc, SrsSdp& local_sdp) { srs_error_t err = srs_success;

SrsRequest\* req = ruc->req_;

SrsRtcSourceDescription\* stream_desc = new SrsRtcSourceDescription();
SrsAutoFree(SrsRtcSourceDescription, stream_desc);

// 媒体协商
// TODO: FIXME: Change to api of stream desc.
if ((err = negotiate\_publish\_capability(ruc, stream_desc)) != srs_success) {
    return srs\_error\_wrap(err, "publish negotiate");
}

//生成响应SDP
if ((err = generate\_publish\_local\_sdp(req, local_sdp, stream_desc, ruc->remote_sdp_.is\_unified())) != srs_success) {
    return srs\_error\_wrap(err, "generate local sdp");
}

SrsRtcSource\* source = NULL;
//创建SrsRtcSource对象
if ((err = _srs_rtc_sources->fetch\_or\_create(req, &source)) != srs_success) {
    return srs\_error\_wrap(err, "create source");
}

// When SDP is done, we set the stream to create state, to prevent multiple publisher.
// @note Here, we check the stream again.
if (!source->can\_publish()) {
    return srs\_error\_new(ERROR_RTC_SOURCE_BUSY, "stream %s busy", req->get\_stream\_url().c\_str());
}
source->set\_stream\_created();

// Apply the SDP to source.
source->set\_stream\_desc(stream_desc);

// 创建推流对象用于接收流
// TODO: FIXME: What happends when error?
if ((err = create\_publisher(req, stream_desc)) != srs_success) {
    return srs\_error\_wrap(err, "create publish");
}

return err;

}


src/app/srs\_app\_rtc\_conn.cpp大约3569行 SrsRtcConnection::create\_publisher 创建推流对象,用于接收流



srs_error_t SrsRtcConnection::create_publisher(SrsRequest* req, SrsRtcSourceDescription* stream_desc) { srs_error_t err = srs_success;

srs\_assert(stream_desc);

// Ignore if exists.
if(publishers_.end() != publishers_.find(req->get\_stream\_url())) {
    return err;
}

// 创建推流对象并初始化
SrsRtcPublishStream\* publisher = new SrsRtcPublishStream(this, _srs_context->get\_id());
if ((err = publisher->initialize(req, stream_desc)) != srs_success) {
    srs\_freep(publisher);
    return srs\_error\_wrap(err, "rtc publisher init");
}
publishers_[req->get\_stream\_url()] = publisher;

...

// If DTLS done, start the publisher. Because maybe create some publishers after DTLS done.
// For example, for single PC, we maybe start publisher when create it, because DTLS is done.
if(ESTABLISHED == state()) {
    if(srs_success != (err = publisher->start())) { //启动流接收
        return srs\_error\_wrap(err, "start publisher");
    }
}

return err;

}


src/app/srs\_app\_rtc\_conn.cpp大约1217行 SrsRtcPublishStream::start



srs_error_t SrsRtcPublishStream::start() { srs_error_t err = srs_success;

if (is_started) {
    return err;
}

if ((err = source->on\_publish()) != srs_success) {
    return srs\_error\_wrap(err, "on publish");
}

if ((err = pli_worker_->start()) != srs_success) {
    return srs\_error\_wrap(err, "start pli worker");
}

if (_srs_rtc_hijacker) {
    if ((err = _srs_rtc_hijacker->on\_start\_publish(session_, this, req_)) != srs_success) {
        return srs\_error\_wrap(err, "on start publish");
    }
}

// update the statistic when client discoveried.
SrsStatistic\* stat = SrsStatistic::instance();
if ((err = stat->on\_client(cid_.c\_str(), req_, session_, SrsRtcConnPublish)) != srs_success) {
    return srs\_error\_wrap(err, "rtc: stat client");
}

is_started = true;

return err;

}


src/app/srs\_app\_rtc\_source.cpp大约在518行SrsRtcSource::on\_publish



srs_error_t SrsRtcSource::on_publish() { srs_error_t err = srs_success;

// update the request object.
srs\_assert(req);

// For RTC, DTLS is done, and we are ready to deliver packets.
// @note For compatible with RTMP, we also set the is\_created\_, it MUST be created here.
is_created_ = true;
is_delivering_packets_ = true;

// Notify the consumers about stream change event.
if ((err = on\_source\_changed()) != srs_success) {
    return srs\_error\_wrap(err, "source id change");
}

// If bridge to other source, handle event and start timer to request PLI.
if (bridger_) {
    if ((err = bridger_->on\_publish()) != srs_success) {
        return srs\_error\_wrap(err, "bridger on publish");
    }

    // The PLI interval for RTC2RTMP.
    pli_for_rtmp_ = _srs_config->get\_rtc\_pli\_for\_rtmp(req->vhost);

    // @see SrsRtcSource::on\_timer()
    _srs_hybrid->timer100ms()->subscribe(this);
}

SrsStatistic\* stat = SrsStatistic::instance();
stat->on\_stream\_publish(req, _source_id.c\_str());

return err;

}


### WebRTC转RTMP


断点:


1. srs\_app\_source.cpp:1847 SrsLiveSource::SrsLiveSource()



SrsLiveSource::SrsLiveSource srs_app_source.cpp:1847 SrsLiveSourceManager::fetch_or_create srs_app_source.cpp:1734 SrsRtcPublishStream::initialize srs_app_rtc_conn.cpp:1196 SrsRtcConnection::create_publisher srs_app_rtc_conn.cpp:3582 SrsRtcConnection::add_publisher srs_app_rtc_conn.cpp:2008 SrsRtcServer::do_create_session srs_app_rtc_server.cpp:545 SrsRtcServer::create_session srs_app_rtc_server.cpp:526 SrsGoApiRtcPublish::do_serve_http srs_app_rtc_api.cpp:457 SrsGoApiRtcPublish::serve_http srs_app_rtc_api.cpp:323 SrsHttpServeMux::serve_http srs_http_stack.cpp:727 SrsHttpCorsMux::serve_http srs_http_stack.cpp:875 SrsHttpConn::process_request srs_app_http_conn.cpp:233 SrsHttpConn::process_requests srs_app_http_conn.cpp:206 SrsHttpConn::do_cycle srs_app_http_conn.cpp:160 SrsHttpConn::cycle srs_app_http_conn.cpp:105 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694


2. srs\_app\_souce.cpp:1273 SrsRtmpFromRtcBridger::SrsRtmpFromRtcBridger



SrsRtmpFromRtcBridger::SrsRtmpFromRtcBridger srs_app_rtc_source.cpp:1273 SrsRtcPublishStream::initialize srs_app_rtc_conn.cpp:1204 SrsRtcConnection::create_publisher srs_app_rtc_conn.cpp:3582 SrsRtcConnection::add_publisher srs_app_rtc_conn.cpp:2008 SrsRtcServer::do_create_session srs_app_rtc_server.cpp:545 SrsRtcServer::create_session srs_app_rtc_server.cpp:526 SrsGoApiRtcPublish::do_serve_http srs_app_rtc_api.cpp:457 SrsGoApiRtcPublish::serve_http srs_app_rtc_api.cpp:323 SrsHttpServeMux::serve_http srs_http_stack.cpp:727 SrsHttpCorsMux::serve_http srs_http_stack.cpp:875 SrsHttpConn::process_request srs_app_http_conn.cpp:233 SrsHttpConn::process_requests srs_app_http_conn.cpp:206 SrsHttpConn::do_cycle srs_app_http_conn.cpp:160 SrsHttpConn::cycle srs_app_http_conn.cpp:105 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694


fetch\_or\_create创建了rtmp对象,SrsRtmpFromRtcBridger将其从rtc转化成rtmp,其转化过程为  
 SrsRtcSource -> SrsRtmpFromRtcBridger -> SrsLiveSource



bool rtc_to_rtmp = _srs_config->get\_rtc\_to\_rtmp(req_->vhost);
if (rtc_to_rtmp) {
    if ((err = _srs_sources->fetch\_or\_create(r, _srs_hybrid->srs()->instance(), &rtmp)) != srs_success) {
        return srs\_error\_wrap(err, "create source");
    }

    // Disable GOP cache for RTC2RTMP bridger, to keep the streams in sync,
    // especially for stream merging.
    rtmp->set\_cache(false);

    SrsRtmpFromRtcBridger \*bridger = new SrsRtmpFromRtcBridger(rtmp);
    if ((err = bridger->initialize(r)) != srs_success) {
        srs\_freep(bridger);
        return srs\_error\_wrap(err, "create bridger");
    }
	
    //设置桥接器
    source->set\_bridger(bridger);
}

然后我们再在下面打下断点


1. srs\_app\_rtc\_source.cpp大约1560 SrsRtmpFromRtcBridger::packet\_video\_rtmp



SrsRtmpFromRtcBridger::packet_video_rtmp srs_app_rtc_source.cpp:1561 SrsRtmpFromRtcBridger::packet_video srs_app_rtc_source.cpp:1451 SrsRtmpFromRtcBridger::on_rtp srs_app_rtc_source.cpp:1347 SrsRtcSource::on_rtp srs_app_rtc_source.cpp:632 SrsRtcVideoRecvTrack::on_rtp srs_app_rtc_source.cpp:2520 SrsRtcPublishStream::do_on_rtp_plaintext srs_app_rtc_conn.cpp:1451 SrsRtcPublishStream::on_rtp_plaintext srs_app_rtc_conn.cpp:1419 SrsRtcPublishStream::on_rtp srs_app_rtc_conn.cpp:1386 SrsRtcConnection::on_rtp srs_app_rtc_conn.cpp:2276 SrsRtcServer::on_udp_packet srs_app_rtc_server.cpp:462 SrsUdpMuxListener::cycle srs_app_listener.cpp:620 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694 SrsFileLog::info srs_app_log.cpp:118 0x00005555560ed6f0 __libc_calloc 0x00007ffff7af3d15 st_cond_new sync.c:157 st_thread_create sched.c:702 SrsFastCoroutine::start srs_app_st.cpp:180 SrsSTCoroutine::start srs_app_st.cpp:95 SrsResourceManager::start srs_app_conn.cpp:72 0x000055555613c0c0 0x00007fffffffe060 SrsWaitGroup::wait srs_app_st.cpp:328 SrsHybridServer::run srs_app_hybrid.cpp:281 run_hybrid_server srs_main_server.cpp:477 run_directly_or_daemon srs_main_server.cpp:407 do_main srs_main_server.cpp:198 main srs_main_server.cpp:207 __libc_start_main 0x00007ffff7a7c0b3 _start 0x0000555555809a1e


2. srs\_app\_rtc\_source.cpp大约1465 SrsRtmpFromRtcBridger::packet\_video\_key\_frame



SrsRtmpFromRtcBridger::packet_video_key_frame srs_app_rtc_source.cpp:1466 SrsRtmpFromRtcBridger::packet_video srs_app_rtc_source.cpp:1433 SrsRtmpFromRtcBridger::on_rtp srs_app_rtc_source.cpp:1347 SrsRtcSource::on_rtp srs_app_rtc_source.cpp:632 SrsRtcVideoRecvTrack::on_rtp srs_app_rtc_source.cpp:2520 SrsRtcPublishStream::do_on_rtp_plaintext srs_app_rtc_conn.cpp:1451 SrsRtcPublishStream::on_rtp_plaintext srs_app_rtc_conn.cpp:1419 SrsRtcPublishStream::on_rtp srs_app_rtc_conn.cpp:1386 SrsRtcConnection::on_rtp srs_app_rtc_conn.cpp:2276 SrsRtcServer::on_udp_packet srs_app_rtc_server.cpp:462 SrsUdpMuxListener::cycle srs_app_listener.cpp:620 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694 SrsFileLog::info srs_app_log.cpp:118 0x00005555560ed6f0 __libc_calloc 0x00007ffff7af3d15 st_cond_new sync.c:157 st_thread_create sched.c:702 SrsFastCoroutine::start srs_app_st.cpp:180 SrsSTCoroutine::start srs_app_st.cpp:95 SrsResourceManager::start srs_app_conn.cpp:72 0x000055555613c0c0 0x00007fffffffe060 SrsWaitGroup::wait srs_app_st.cpp:328 SrsHybridServer::run srs_app_hybrid.cpp:281 run_hybrid_server srs_main_server.cpp:477 run_directly_or_daemon srs_main_server.cpp:407 do_main srs_main_server.cpp:198 main srs_main_server.cpp:207 __libc_start_main 0x00007ffff7a7c0b3 _start 0x0000555555809a1e



srs_error_t SrsRtmpFromRtcBridger::packet_video_key_frame(SrsRtpPacket* pkt) { srs_error_t err = srs_success;

// TODO: handle sps and pps in 2 rtp packets
SrsRtpSTAPPayload\* stap_payload = dynamic\_cast<SrsRtpSTAPPayload\*>(pkt->payload());
if (stap_payload) {
    SrsSample\* sps = stap_payload->get\_sps();
    SrsSample\* pps = stap_payload->get\_pps();
    if (NULL == sps || NULL == pps) {
        return srs\_error\_new(ERROR_RTC_RTP_MUXER, "no sps or pps in stap-a rtp. sps: %p, pps:%p", sps, pps);
    } else {
        //type\_codec1 + avc\_type + composition time + fix header + count of sps + len of sps + sps + count of pps + len of pps + pps
        int nb_payload = 1 + 1 + 3 + 5 + 1 + 2 + sps->size + 1 + 2 + pps->size;
        SrsCommonMessage rtmp;
        rtmp.header.initialize\_video(nb_payload, pkt->get\_avsync\_time(), 1);
        rtmp.create\_payload(nb_payload);
        rtmp.size = nb_payload;
        SrsBuffer payload(rtmp.payload, rtmp.size);
        //TODO: call api
        payload.write\_1bytes(0x17);// type(4 bits): key frame; code(4bits): avc
        payload.write\_1bytes(0x0); // avc\_type: sequence header
        payload.write\_1bytes(0x0); // composition time
        payload.write\_1bytes(0x0);
        payload.write\_1bytes(0x0);
        payload.write\_1bytes(0x01); // version
        payload.write\_1bytes(sps->bytes[1]);
        payload.write\_1bytes(sps->bytes[2]);
        payload.write\_1bytes(sps->bytes[3]);
        payload.write\_1bytes(0xff);
        payload.write\_1bytes(0xe1);
        payload.write\_2bytes(sps->size);
        payload.write\_bytes(sps->bytes, sps->size);
        payload.write\_1bytes(0x01);
        payload.write\_2bytes(pps->size);
        payload.write\_bytes(pps->bytes, pps->size);
        if ((err = source_->on\_video(&rtmp)) != srs_success) { 
            return err;
        }
    }
}

if (-1 == rtp_key_frame_ts_) {
    rtp_key_frame_ts_ = pkt->header.get\_timestamp();
    header_sn_ = pkt->header.get\_sequence();
    lost_sn_ = header_sn_ + 1;
    // Received key frame and clean cache of old p frame pkts
    clear\_cached\_video();
    srs\_trace("set ts=%u, header=%hu, lost=%hu", (uint32\_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
} else if (rtp_key_frame_ts_ != pkt->header.get\_timestamp()) {
    //new key frame, clean cache
    int64\_t old_ts = rtp_key_frame_ts_;
    uint16\_t old_header_sn = header_sn_;
    uint16\_t old_lost_sn = lost_sn_;
    rtp_key_frame_ts_ = pkt->header.get\_timestamp();
    header_sn_ = pkt->header.get\_sequence();
    lost_sn_ = header_sn_ + 1;
    clear\_cached\_video();
    srs\_warn("drop old ts=%u, header=%hu, lost=%hu, set new ts=%u, header=%hu, lost=%hu",
        (uint32\_t)old_ts, old_header_sn, old_lost_sn, (uint32\_t)rtp_key_frame_ts_, header_sn_, lost_sn_);
}

uint16\_t index = cache\_index(pkt->header.get\_sequence());
cache_video_pkts_[index].in_use = true;
srs\_freep(cache_video_pkts_[index].pkt);
cache_video_pkts_[index].pkt = pkt;
cache_video_pkts_[index].sn = pkt->header.get\_sequence();
cache_video_pkts_[index].ts = pkt->get\_avsync\_time();
cache_video_pkts_[index].rtp_ts = pkt->header.get\_timestamp();

int32\_t sn = lost_sn_;
uint16\_t tail_sn = 0;
if (srs\_rtp\_seq\_distance(header_sn_, pkt->header.get\_sequence()) < 0){
    // When receive previous pkt in the same frame, update header sn;
    header_sn_ = pkt->header.get\_sequence();
    sn = find\_next\_lost\_sn(header_sn_, tail_sn);
} else if (lost_sn_ == pkt->header.get\_sequence()) {
    sn = find\_next\_lost\_sn(lost_sn_, tail_sn);
}

if (-1 == sn) {
    if (check\_frame\_complete(header_sn_, tail_sn)) {
        if ((err = packet\_video\_rtmp(header_sn_, tail_sn)) != srs_success) {
            err = srs\_error\_wrap(err, "fail to packet key frame");
        }
    }
} else if (-2 == sn) {
    return srs\_error\_new(ERROR_RTC_RTP_MUXER, "video cache is overflow");
} else {
    lost_sn_ = (uint16\_t)sn;
}

return err;

}


3. 音频转码 srs\_app\_rtc\_codec.cpp 大约156行 SrsAudioTranscoder::transcode



SrsAudioTranscoder::transcode srs_app_rtc_codec.cpp:157 SrsRtmpFromRtcBridger::transcode_audio srs_app_rtc_source.cpp:1388 SrsRtmpFromRtcBridger::on_rtp srs_app_rtc_source.cpp:1345 SrsRtcSource::on_rtp srs_app_rtc_source.cpp:632 SrsRtcAudioRecvTrack::on_rtp srs_app_rtc_source.cpp:2462 SrsRtcPublishStream::do_on_rtp_plaintext srs_app_rtc_conn.cpp:1446 SrsRtcPublishStream::on_rtp_plaintext srs_app_rtc_conn.cpp:1419 SrsRtcPublishStream::on_rtp srs_app_rtc_conn.cpp:1386 SrsRtcConnection::on_rtp srs_app_rtc_conn.cpp:2276 SrsRtcServer::on_udp_packet srs_app_rtc_server.cpp:462 SrsUdpMuxListener::cycle srs_app_listener.cpp:620 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694 SrsFileLog::info srs_app_log.cpp:118 0x00005555560ed6f0 __libc_calloc 0x00007ffff7af3d15 st_cond_new sync.c:157 st_thread_create sched.c:702 SrsFastCoroutine::start srs_app_st.cpp:180 SrsSTCoroutine::start srs_app_st.cpp:95 SrsResourceManager::start srs_app_conn.cpp:72 0x000055555613c0c0 0x00007fffffffe060 SrsWaitGroup::wait srs_app_st.cpp:328 SrsHybridServer::run srs_app_hybrid.cpp:281 run_hybrid_server srs_main_server.cpp:477 run_directly_or_daemon srs_main_server.cpp:407 do_main srs_main_server.cpp:198 main srs_main_server.cpp:207 __libc_start_main 0x00007ffff7a7c0b3 _start 0x0000555555809a1e


4. 音频包分发 srs\_app\_rtc\_source.cpp:1360 SrsRtmpFromRtcBridger::transcode\_audio



SrsRtmpFromRtcBridger::transcode_audio srs_app_rtc_source.cpp:1360 SrsRtmpFromRtcBridger::on_rtp srs_app_rtc_source.cpp:1345 SrsRtcSource::on_rtp srs_app_rtc_source.cpp:632 SrsRtcAudioRecvTrack::on_rtp srs_app_rtc_source.cpp:2462 SrsRtcPublishStream::do_on_rtp_plaintext srs_app_rtc_conn.cpp:1446 SrsRtcPublishStream::on_rtp_plaintext srs_app_rtc_conn.cpp:1419 SrsRtcPublishStream::on_rtp srs_app_rtc_conn.cpp:1386 SrsRtcConnection::on_rtp srs_app_rtc_conn.cpp:2276 SrsRtcServer::on_udp_packet srs_app_rtc_server.cpp:462 SrsUdpMuxListener::cycle srs_app_listener.cpp:620 SrsFastCoroutine::cycle srs_app_st.cpp:272 SrsFastCoroutine::pfn srs_app_st.cpp:287 _st_thread_main sched.c:363 st_thread_create sched.c:694 SrsFileLog::info srs_app_log.cpp:118 0x00005555560ed6f0 __libc_calloc 0x00007ffff7af3d15 st_cond_new sync.c:157 st_thread_create sched.c:702 SrsFastCoroutine::start srs_app_st.cpp:180 SrsSTCoroutine::start srs_app_st.cpp:95 SrsResourceManager::start srs_app_conn.cpp:72 0x000055555613c0c0 0x00007fffffffe060 SrsWaitGroup::wait srs_app_st.cpp:328 SrsHybridServer::run srs_app_hybrid.cpp:281 run_hybrid_server srs_main_server.cpp:477 run_directly_or_daemon srs_main_server.cpp:407 do_main srs_main_server.cpp:198 main srs_main_server.cpp:207 __libc_start_main 0x00007ffff7a7c0b3 _start 0x0000555555809a1e



srs_error_t SrsRtmpFromRtcBridger::transcode_audio(SrsRtpPacket *pkt) {

img img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!