• SRS流媒体服务器:服务器读取RTMP推流数据


    目录

    1. 处理RTMP推流video message
    2. 处理RTMP推流audio message
    3. 处理RTMP推流onMetaData message

    1. 服务器读取RTMP推流数据

    RTMP推流、拉流创建连接说到,SrsRtmpConn::publishing会创建SrsPublishRecvThread协程来接收数据,然后在SrsRtmpConn::do_publishing开启接收协程。

    而最终会执行到SrsRecvThread::do_cycle,负责读取RTMP推流数据。

    srs_error_t SrsRecvThread::do_cycle() { srs_error_t err = srs_success; while (true) { if ((err = trd->pull()) != srs_success) { return srs_error_wrap(err, "recv thread"); } // When the pumper is interrupted, wait then retry. if (pumper->interrupted()) { srs_usleep(timeout); continue; } SrsCommonMessage *msg = NULL; // Process the received message. 读取msg并进行消费 if ((err = rtmp->recv_message(&msg)) == srs_success) { err = pumper->consume(msg); } if (err != srs_success) { // Interrupt the receive thread for any error. trd->interrupt(); // Notify the pumper to quit for error. pumper->interrupt(err); return srs_error_wrap(err, "recv thread"); } } return err; }

    3.SrsRtmpServer::recv_message会获取message,如果获取的是协议控制消息或者用户控制消息会进行解码成packet然后进行响应处理,比如设置ackowledgement_window_size或者chunk_size。

    如果是Aduio message,Video message或者Data message,不进行解码成packet,仅获取message。

    TODO:后续写一篇关于如何接收message的分析,详细代码见SrsProtocol::recv_interlaced_message

    1. srs_error_t SrsProtocol::recv_message(SrsCommonMessage** pmsg)
    2. {
    3. *pmsg = NULL;
    4. srs_error_t err = srs_success;
    5. while (true) {
    6. SrsCommonMessage* msg = NULL;
    7. if ((err = recv_interlaced_message(&msg)) != srs_success) { //获取message
    8. srs_freep(msg);
    9. return srs_error_wrap(err, "recv interlaced message");
    10. }
    11. if (!msg) {
    12. continue;
    13. }
    14. if (msg->size <= 0 || msg->header.payload_length <= 0) {
    15. srs_trace("ignore empty message(type=%d, size=%d, time=%" PRId64 ", sid=%d).",
    16. msg->header.message_type, msg->header.payload_length,
    17. msg->header.timestamp, msg->header.stream_id);
    18. srs_freep(msg);
    19. continue;
    20. }
    21. if ((err = on_recv_message(msg)) != srs_success) { //对获取的message进行判断类型并进行相应处理
    22. srs_freep(msg);
    23. return srs_error_wrap(err, "on recv message");
    24. }
    25. *pmsg = msg;
    26. break;
    27. }
    28. return err;
    29. }

    4.SrsRtmpServer::recv_message会获取message后,在SrsPublishRecvThread::consume中进行处理,最后交由SrsRtmpConn::handle_publish_message进行处理。

    1. srs_error_t SrsPublishRecvThread::consume(SrsCommonMessage *msg) {
    2. srs_error_t err = srs_success;
    3. // when cid changed, change it.
    4. if (ncid.compare(cid)) {
    5. _srs_context->set_id(ncid);
    6. cid = ncid;
    7. }
    8. _nb_msgs++;
    9. if (msg->header.is_video()) {
    10. video_frames++;
    11. }
    12. if (msg->header.is_audio()) {
    13. audio_frames++;
    14. }
    15. // log to show the time of recv thread.
    16. srs_verbose("recv thread now=%" PRId64 "us, got msg time=%" PRId64 "ms, size=%d",
    17. srs_update_system_time(), msg->header.timestamp, msg->size);
    18. // the rtmp connection will handle this message 由RTMP连接处理message
    19. err = _conn->handle_publish_message(_source, msg);
    20. // must always free it,
    21. // the source will copy it if need to use.
    22. srs_freep(msg);
    23. if (err != srs_success) {
    24. return srs_error_wrap(err, "handle publish message");
    25. }
    26. // Yield to another coroutines.
    27. // @see https://github.com/ossrs/srs/issues/2194#issuecomment-777463768
    28. if (++nn_msgs_for_yield_ >= 15) {
    29. nn_msgs_for_yield_ = 0;
    30. srs_thread_yield();
    31. }
    32. return err;
    33. }

    5.SrsRtmpConn::handle_publish_message会先判断message type是否是命令消息,如果是进行解码并进行处理,推流不需要这些事件数据。

    6.audio、video以及data数据由SrsRtmpConn::process_publish_message进行处理。

    1. srs_error_t SrsRtmpConn::handle_publish_message(SrsLiveSource* source, SrsCommonMessage* msg)
    2. {
    3. srs_error_t err = srs_success;
    4. // process publish event. 处理推流事件,因为推流不需要这些事件数据,audio、video以及data数据不会进入此逻辑
    5. if (msg->header.is_amf0_command() || msg->header.is_amf3_command()) {
    6. SrsPacket* pkt = NULL;
    7. if ((err = rtmp->decode_message(msg, &pkt)) != srs_success) {
    8. return srs_error_wrap(err, "rtmp: decode message");
    9. }
    10. SrsAutoFree(SrsPacket, pkt);
    11. // for flash, any packet is republish.
    12. if (info->type == SrsRtmpConnFlashPublish) {
    13. // flash unpublish.
    14. // TODO: maybe need to support republish.
    15. srs_trace("flash flash publish finished.");
    16. return srs_error_new(ERROR_CONTROL_REPUBLISH, "rtmp: republish");
    17. }
    18. // for fmle, drop others except the fmle start packet.
    19. if (dynamic_cast<SrsFMLEStartPacket*>(pkt)) {
    20. SrsFMLEStartPacket* unpublish = dynamic_cast<SrsFMLEStartPacket*>(pkt);
    21. if ((err = rtmp->fmle_unpublish(info->res->stream_id, unpublish->transaction_id)) != srs_success) {
    22. return srs_error_wrap(err, "rtmp: republish");
    23. }
    24. return srs_error_new(ERROR_CONTROL_REPUBLISH, "rtmp: republish");
    25. }
    26. srs_trace("fmle ignore AMF0/AMF3 command message.");
    27. return err;
    28. }
    29. // video, audio, data message
    30. if ((err = process_publish_message(source, msg)) != srs_success) {
    31. return srs_error_wrap(err, "rtmp: consume message");
    32. }
    33. return err;
    34. }

    7.SrsRtmpConn::process_publish_message会判断message_type来区别audio、video和onMetaData来进行不同处理。

    SrsLiveSource::on_audio负责处理音频message。

    SrsLiveSource::on_video负责处理视频message。

    SrsLiveSource::on_meta_data负责处理onMetaData message。

    1. srs_error_t SrsRtmpConn::process_publish_message(SrsLiveSource* source, SrsCommonMessage* msg)
    2. {
    3. srs_error_t err = srs_success;
    4. // for edge, directly proxy message to origin.
    5. if (info->edge) {
    6. if ((err = source->on_edge_proxy_publish(msg)) != srs_success) {
    7. return srs_error_wrap(err, "rtmp: proxy publish");
    8. }
    9. return err;
    10. }
    11. // process audio packet
    12. if (msg->header.is_audio()) {
    13. if ((err = source->on_audio(msg)) != srs_success) {
    14. return srs_error_wrap(err, "rtmp: consume audio");
    15. }
    16. return err;
    17. }
    18. // process video packet
    19. if (msg->header.is_video()) {
    20. if ((err = source->on_video(msg)) != srs_success) {
    21. return srs_error_wrap(err, "rtmp: consume video");
    22. }
    23. return err;
    24. }
    25. // process aggregate packet
    26. if (msg->header.is_aggregate()) {
    27. if ((err = source->on_aggregate(msg)) != srs_success) {
    28. return srs_error_wrap(err, "rtmp: consume aggregate");
    29. }
    30. return err;
    31. }
    32. // process onMetaData
    33. if (msg->header.is_amf0_data() || msg->header.is_amf3_data()) {
    34. SrsPacket* pkt = NULL;
    35. if ((err = rtmp->decode_message(msg, &pkt)) != srs_success) {
    36. return srs_error_wrap(err, "rtmp: decode message");
    37. }
    38. SrsAutoFree(SrsPacket, pkt);
    39. if (dynamic_cast<SrsOnMetaDataPacket*>(pkt)) {
    40. SrsOnMetaDataPacket* metadata = dynamic_cast<SrsOnMetaDataPacket*>(pkt);
    41. if ((err = source->on_meta_data(msg, metadata)) != srs_success) {
    42. return srs_error_wrap(err, "rtmp: consume metadata");
    43. }
    44. return err;
    45. }
    46. return err;
    47. }
    48. return err;
    49. }
    1. 处理RTMP推流video message
      SrsLiveSource::on_video负责处理视频message。主要的功能为:
      将SrsCommonMessage转换成SrsSharedPtrMessage,用于更少内存拷贝的音频/视频/数据message,SrsSharedPtrMessage使用共享计数方法。
      将SrsSharedPtrMessage交给SrsLiveSource::on_video_imp处理。
    1. srs_error_t SrsLiveSource::on_video(SrsCommonMessage* shared_video)
    2. {
    3. srs_error_t err = srs_success;
    4. // monotically increase detect.
    5. if (!mix_correct && is_monotonically_increase) {
    6. if (last_packet_time > 0 && shared_video->header.timestamp < last_packet_time) {
    7. is_monotonically_increase = false;
    8. srs_warn("VIDEO: stream not monotonically increase, please open mix_correct.");
    9. }
    10. }
    11. last_packet_time = shared_video->header.timestamp;
    12. // drop any unknown header video.
    13. // @see https://github.com/ossrs/srs/issues/421
    14. if (!SrsFlvVideo::acceptable(shared_video->payload, shared_video->size)) {
    15. char b0 = 0x00;
    16. if (shared_video->size > 0) {
    17. b0 = shared_video->payload[0];
    18. }
    19. srs_warn("drop unknown header video, size=%d, bytes[0]=%#x", shared_video->size, b0);
    20. return err;
    21. }
    22. // convert shared_video to msg, user should not use shared_video again.
    23. // the payload is transfer to msg, and set to NULL in shared_video.
    24. SrsSharedPtrMessage msg; //创建共享message ptr,将SrsCommonMessage转换成SrsSharedPtrMessage,用于更少内存拷贝的音频/视频/数据message
    25. if ((err = msg.create(shared_video)) != srs_success) { //SrsSharedPtrMessage使用引用计数
    26. return srs_error_wrap(err, "create message");
    27. }
    28. // directly process the video message. mix_correct为false,直接处理video message
    29. if (!mix_correct) {
    30. return on_video_imp(&msg);
    31. }
    32. // insert msg to the queue. 如果mix_correct为true,先将msg加入按时间戳排序map,然后从中取出第一个msg进行处理
    33. mix_queue->push(msg.copy());
    34. // fetch someone from mix queue. 取出的msg可能是音频或视频,所以需要进行判断
    35. SrsSharedPtrMessage* m = mix_queue->pop();
    36. if (!m) {
    37. return err;
    38. }
    39. // consume the monotonically increase message.
    40. if (m->is_audio()) {
    41. err = on_audio_imp(m);
    42. } else {
    43. err = on_video_imp(m);
    44. }
    45. srs_freep(m);
    46. return err;
    47. }

    2.SrsLiveSource::on_video_imp处理SrsSharedPtrMessage主要过程包括:

    1. 遍历对应source拉流客户端,将msg发送给每个客户端。
    2. 如果不是sequence header,加入gop缓存中,gop会缓存最后一个gop的packet。
    1. srs_error_t SrsLiveSource::on_video_imp(SrsSharedPtrMessage* msg)
    2. {
    3. srs_error_t err = srs_success;
    4. bool is_sequence_header = SrsFlvVideo::sh(msg->payload, msg->size);
    5. // whether consumer should drop for the duplicated sequence header.
    6. bool drop_for_reduce = false;
    7. if (is_sequence_header && meta->previous_vsh() && _srs_config->get_reduce_sequence_header(req->vhost)) {
    8. if (meta->previous_vsh()->size == msg->size) {
    9. drop_for_reduce = srs_bytes_equals(meta->previous_vsh()->payload, msg->payload, msg->size);
    10. srs_warn("drop for reduce sh video, size=%d", msg->size);
    11. }
    12. }
    13. // cache the sequence header if h264
    14. // donot cache the sequence header to gop_cache, return here.
    15. if (is_sequence_header && (err = meta->update_vsh(msg)) != srs_success) {
    16. return srs_error_wrap(err, "meta update video");
    17. }
    18. // Copy to hub to all utilities.
    19. if ((err = hub->on_video(msg, is_sequence_header)) != srs_success) {
    20. return srs_error_wrap(err, "hub consume video");
    21. }
    22. // For bridger to consume the message.
    23. if (bridger_ && (err = bridger_->on_video(msg)) != srs_success) {
    24. return srs_error_wrap(err, "bridger consume video");
    25. }
    26. // copy to all consumer 遍历对应source拉流客户端,将msg发送给每个客户端
    27. if (!drop_for_reduce) {
    28. for (int i = 0; i < (int)consumers.size(); i++) {
    29. SrsLiveConsumer* consumer = consumers.at(i);
    30. if ((err = consumer->enqueue(msg, atc, jitter_algorithm)) != srs_success) {
    31. return srs_error_wrap(err, "consume video");
    32. }
    33. }
    34. }
    35. // when sequence header, donot push to gop cache and adjust the timestamp.
    36. if (is_sequence_header) {
    37. return err;
    38. }
    39. // cache the last gop packets
    40. if ((err = gop_cache->cache(msg)) != srs_success) {
    41. return srs_error_wrap(err, "gop cache consume vdieo");
    42. }
    43. // if atc, update the sequence header to abs time.
    44. if (atc) {
    45. if (meta->vsh()) {
    46. meta->vsh()->timestamp = msg->timestamp;
    47. }
    48. if (meta->data()) {
    49. meta->data()->timestamp = msg->timestamp;
    50. }
    51. }
    52. return err;
    53. }

    3.每个拉流客户端是一个SrsLiveConsumer,会将msg加入SrsMessageQueue对象中,实际保存的是SrsMessageQueue对象中的SrsFastVector对象中的SrsSharedPtrMessage数组。

    1. srs_error_t SrsLiveConsumer::enqueue(SrsSharedPtrMessage* shared_msg, bool atc, SrsRtmpJitterAlgorithm ag)
    2. {
    3. srs_error_t err = srs_success;
    4. SrsSharedPtrMessage* msg = shared_msg->copy();
    5. if (!atc) {
    6. if ((err = jitter->correct(msg, ag)) != srs_success) {
    7. return srs_error_wrap(err, "consume message");
    8. }
    9. }
    10. if ((err = queue->enqueue(msg, NULL)) != srs_success) {
    11. return srs_error_wrap(err, "enqueue message");
    12. }
    13. ...
    14. return err;
    15. }
    1. srs_error_t SrsMessageQueue::enqueue(SrsSharedPtrMessage* msg, bool* is_overflow)
    2. {
    3. srs_error_t err = srs_success;
    4. msgs.push_back(msg);
    5. // If jitter is off, the timestamp of first sequence header is zero, which wll cause SRS to shrink and drop the
    6. // keyframes even if there is not overflow packets in queue, so we must ignore the zero timestamps, please
    7. // @see https://github.com/ossrs/srs/pull/2186#issuecomment-953383063
    8. if (msg->is_av() && msg->timestamp != 0) {
    9. if (av_start_time == -1) {
    10. av_start_time = srs_utime_t(msg->timestamp * SRS_UTIME_MILLISECONDS);
    11. }
    12. av_end_time = srs_utime_t(msg->timestamp * SRS_UTIME_MILLISECONDS);
    13. }
    14. if (max_queue_size <= 0) {
    15. return err;
    16. }
    17. while (av_end_time - av_start_time > max_queue_size) {
    18. // notice the caller queue already overflow and shrinked.
    19. if (is_overflow) {
    20. *is_overflow = true;
    21. }
    22. shrink();
    23. }
    24. return err;
    25. }
    1. void SrsFastVector::push_back(SrsSharedPtrMessage* msg)
    2. {
    3. // increase vector.
    4. if (count >= nb_msgs) {
    5. int size = srs_max(SRS_PERF_MW_MSGS * 8, nb_msgs * 2);
    6. SrsSharedPtrMessage** buf = new SrsSharedPtrMessage*[size];
    7. for (int i = 0; i < nb_msgs; i++) {
    8. buf[i] = msgs[i];
    9. }
    10. srs_info("fast vector incrase %d=>%d", nb_msgs, size);
    11. // use new array.
    12. srs_freepa(msgs);
    13. msgs = buf;
    14. nb_msgs = size;
    15. }
    16. msgs[count++] = msg;
    17. }

    2. 处理RTMP推流audio message

    SrsLiveSource::on_audio负责处理音频message。主要的功能跟处理video message一样,为:
    将SrsCommonMessage转换成SrsSharedPtrMessage,用于更少内存拷贝的音频/视频/数据message,SrsSharedPtrMessage使用共享计数方法。
    将SrsSharedPtrMessage交给SrsLiveSource::on_audio_imp处理。

    1. srs_error_t SrsLiveSource::on_audio(SrsCommonMessage* shared_audio)
    2. {
    3. srs_error_t err = srs_success;
    4. // monotically increase detect.
    5. if (!mix_correct && is_monotonically_increase) {
    6. if (last_packet_time > 0 && shared_audio->header.timestamp < last_packet_time) {
    7. is_monotonically_increase = false;
    8. srs_warn("AUDIO: stream not monotonically increase, please open mix_correct.");
    9. }
    10. }
    11. last_packet_time = shared_audio->header.timestamp;
    12. // convert shared_audio to msg, user should not use shared_audio again.
    13. // the payload is transfer to msg, and set to NULL in shared_audio.
    14. SrsSharedPtrMessage msg;
    15. if ((err = msg.create(shared_audio)) != srs_success) {
    16. return srs_error_wrap(err, "create message");
    17. }
    18. // directly process the audio message.
    19. if (!mix_correct) {
    20. return on_audio_imp(&msg);
    21. }
    22. // insert msg to the queue.
    23. mix_queue->push(msg.copy());
    24. // fetch someone from mix queue.
    25. SrsSharedPtrMessage* m = mix_queue->pop();
    26. if (!m) {
    27. return err;
    28. }
    29. // consume the monotonically increase message.
    30. if (m->is_audio()) {
    31. err = on_audio_imp(m);
    32. } else {
    33. err = on_video_imp(m);
    34. }
    35. srs_freep(m);
    36. return err;
    37. }

    2.SrsLiveSource::on_audio_imp处理SrsSharedPtrMessage主要过程包括:

    1. 遍历对应source拉流客户端,将msg发送给每个客户端。
    2. 如果不是sequence header,加入gop缓存中,gop会缓存最后一个gop的packet。
    1. srs_error_t SrsLiveSource::on_audio_imp(SrsSharedPtrMessage* msg)
    2. {
    3. srs_error_t err = srs_success;
    4. bool is_aac_sequence_header = SrsFlvAudio::sh(msg->payload, msg->size);
    5. bool is_sequence_header = is_aac_sequence_header;
    6. // whether consumer should drop for the duplicated sequence header.
    7. bool drop_for_reduce = false;
    8. if (is_sequence_header && meta->previous_ash() && _srs_config->get_reduce_sequence_header(req->vhost)) {
    9. if (meta->previous_ash()->size == msg->size) {
    10. drop_for_reduce = srs_bytes_equals(meta->previous_ash()->payload, msg->payload, msg->size);
    11. srs_warn("drop for reduce sh audio, size=%d", msg->size);
    12. }
    13. }
    14. // Copy to hub to all utilities.
    15. if ((err = hub->on_audio(msg)) != srs_success) {
    16. return srs_error_wrap(err, "consume audio");
    17. }
    18. // For bridger to consume the message.
    19. if (bridger_ && (err = bridger_->on_audio(msg)) != srs_success) {
    20. return srs_error_wrap(err, "bridger consume audio");
    21. }
    22. // copy to all consumer 遍历对应source拉流客户端,将msg发送给每个客户端
    23. if (!drop_for_reduce) {
    24. for (int i = 0; i < (int)consumers.size(); i++) {
    25. SrsLiveConsumer* consumer = consumers.at(i);
    26. if ((err = consumer->enqueue(msg, atc, jitter_algorithm)) != srs_success) {
    27. return srs_error_wrap(err, "consume message");
    28. }
    29. }
    30. }
    31. // cache the sequence header of aac, or first packet of mp3.
    32. // for example, the mp3 is used for hls to write the "right" audio codec.
    33. // TODO: FIXME: to refine the stream info system.
    34. if (is_aac_sequence_header || !meta->ash()) {
    35. if ((err = meta->update_ash(msg)) != srs_success) {
    36. return srs_error_wrap(err, "meta consume audio");
    37. }
    38. }
    39. // when sequence header, donot push to gop cache and adjust the timestamp.
    40. if (is_sequence_header) {
    41. return err;
    42. }
    43. // cache the last gop packets
    44. if ((err = gop_cache->cache(msg)) != srs_success) {
    45. return srs_error_wrap(err, "gop cache consume audio");
    46. }
    47. // if atc, update the sequence header to abs time.
    48. if (atc) {
    49. if (meta->ash()) {
    50. meta->ash()->timestamp = msg->timestamp;
    51. }
    52. if (meta->data()) {
    53. meta->data()->timestamp = msg->timestamp;
    54. }
    55. }
    56. return err;
    57. }

    3.每个拉流客户端是一个SrsLiveConsumer,会将msg加入SrsMessageQueue对象中,实际保存的是SrsMessageQueue对象中的SrsFastVector对象中的SrsSharedPtrMessage数组。跟video存储逻辑一样。

    3. 处理RTMP推流onMetaData message

    对于onMetaData message,SrsRtmpConn::process_publish_message会先解码成SrsOnMetaDataPacket,然后和onMetaData message一起作为参数传入SrsLiveSource::on_meta_data。主要的功能有:

    更新onMetaData message。即更新SrsMetaCache的meta(SrsSharedPtrMessage)

    遍历对应source拉流客户端,将共享meta发送给每个客户端。

    1. srs_error_t SrsLiveSource::on_meta_data(SrsCommonMessage* msg, SrsOnMetaDataPacket* metadata)
    2. {
    3. srs_error_t err = srs_success;
    4. // if allow atc_auto and bravo-atc detected, open atc for vhost.
    5. SrsAmf0Any* prop = NULL;
    6. atc = _srs_config->get_atc(req->vhost);
    7. if (_srs_config->get_atc_auto(req->vhost)) {
    8. if ((prop = metadata->metadata->get_property("bravo_atc")) != NULL) {
    9. if (prop->is_string() && prop->to_str() == "true") {
    10. atc = true;
    11. }
    12. }
    13. }
    14. // Update the meta cache. 如果需要更新MetaData,会更新共享SrsSharedPtrMessage* meta
    15. bool updated = false;
    16. if ((err = meta->update_data(&msg->header, metadata, updated)) != srs_success) {
    17. return srs_error_wrap(err, "update metadata");
    18. }
    19. if (!updated) {
    20. return err;
    21. }
    22. // when already got metadata, drop when reduce sequence header.
    23. bool drop_for_reduce = false;
    24. if (meta->data() && _srs_config->get_reduce_sequence_header(req->vhost)) {
    25. drop_for_reduce = true;
    26. srs_warn("drop for reduce sh metadata, size=%d", msg->size);
    27. }
    28. // copy to all consumer 遍历对应source拉流客户端,将共享meta发送给每个客户端。
    29. if (!drop_for_reduce) {
    30. std::vector<SrsLiveConsumer*>::iterator it;
    31. for (it = consumers.begin(); it != consumers.end(); ++it) {
    32. SrsLiveConsumer* consumer = *it;
    33. if ((err = consumer->enqueue(meta->data(), atc, jitter_algorithm)) != srs_success) {
    34. return srs_error_wrap(err, "consume metadata");
    35. }
    36. }
    37. }
    38. // Copy to hub to all utilities.
    39. return hub->on_meta_data(meta->data(), metadata);
    40. }

    2.每个拉流客户端是一个SrsLiveConsumer,会将msg加入SrsMessageQueue对象中,实际保存的是SrsMessageQueue对象中的SrsFastVector对象中的SrsSharedPtrMessage数组。跟video和audio的存储逻辑一样。

  • 相关阅读:
    某今日头条_signature解析
    解析Collection接口中的常用的被实现子类重写的方法
    Dubbo—Admin 整体架构与安装步骤
    【我的机器学习之旅】打造个性化手写汉字识别系统
    Docker指定容器使用内存
    RabbitMQ有什么优缺点
    thinkphp使用dompdf导出pdf(html转pdf)
    软件评测师之逻辑运算
    HTML网页期末作业:基于Html+Css+javascript的网页制作(化妆品企业官网设计20页)
    使用 DFS 轻松求解数独难题(C++ 的一个简单实现)
  • 原文地址:https://blog.csdn.net/irainsa/article/details/127941373