publish的流和play的流怎么连接呢?这个恐怕是最绕的地方了。看了一上午的代码,淹没于各种数据结构与流程之中后,俺终于发现了连接publish和play的关键连个类是

SrsSource

SrsConsumer

负责连接着连个类实例的是

SrsRtmpConn

下面我们详细讲解连接过程

上片我们说到。在底层客户端连接上来后,会经过一系列处理,最后绕到SrsRtmpConn类的循环函数中。就是下面的函数

[cpp] view plaincopy
  1. int SrsConnection::cycle()
  2. {
  3. int ret = ERROR_SUCCESS;
  4. _srs_context->generate_id();
  5. id = _srs_context->get_id();
  6. ip = srs_get_peer_ip(st_netfd_fileno(stfd));
  7. ret = do_cycle();
  8. // if socket io error, set to closed.
  9. if (srs_is_client_gracefully_close(ret)) {
  10. ret = ERROR_SOCKET_CLOSED;
  11. }
  12. // success.
  13. if (ret == ERROR_SUCCESS) {
  14. srs_trace("client finished.");
  15. }
  16. // client close peer.
  17. if (ret == ERROR_SOCKET_CLOSED) {
  18. srs_warn("client disconnect peer. ret=%d", ret);
  19. }
  20. return ERROR_SUCCESS;
  21. }

这个是SrsRtmpConn的基类SrsConnection的函数。在基类里,do_cycle()是个纯虚函数。具体实现完全是靠这子类来的。

那么rtmp类型的这个子类,到底有多么的变态呢,先看看我画的一个流程图,都没有画完。一张放不下,的截好几张

够长的,这里我还只是画到了播放的时候,发布流程还没有画。因为太复杂了。

下面开始一步一步的分析

首先看do_cycle()函数这个函数主要负责握手和连命令。并在成功后。获取流的配置信息。关键代码如下

[cpp] view plaincopy
  1. if ((ret = rtmp->handshake()) != ERROR_SUCCESS) {
  2. srs_error("rtmp handshake failed. ret=%d", ret);
  3. return ret;
  4. }
  5. srs_verbose("rtmp handshake success");
  6. if ((ret = rtmp->connect_app(req)) != ERROR_SUCCESS) {
  7. srs_error("rtmp connect vhost/app failed. ret=%d", ret);
  8. return ret;
  9. }
  10. srs_verbose("rtmp connect app success");

注意这里有一个比较重要的数据结构

[cpp] view plaincopy
  1. SrsRequest* req

这个主要是存储请求信息的,比如app turl streamid等等。

在各种分析后,进入下一个cycle,service_cycle()函数

service_cycly()函数在做了一些设置工作,设置比如chunk size。代码如下

[cpp] view plaincopy
  1. if ((ret = rtmp->set_window_ack_size((int)(2.5 * 1000 * 1000))) != ERROR_SUCCESS) {
  2. srs_error("set window acknowledgement size failed. ret=%d", ret);
  3. return ret;
  4. }
  5. srs_verbose("set window acknowledgement size success");
  6. if ((ret = rtmp->set_peer_bandwidth((int)(2.5 * 1000 * 1000), 2)) != ERROR_SUCCESS) {
  7. srs_error("set peer bandwidth failed. ret=%d", ret);
  8. return ret;
  9. }

下面一段代码没有看明白。这个是一个补丁打上去的,说说为了做do token traverse。这个暂时先不研究了。

[cpp] view plaincopy
  1. if (true) {
  2. bool vhost_is_edge = _srs_config->get_vhost_is_edge(req->vhost);
  3. bool edge_traverse = _srs_config->get_vhost_edge_token_traverse(req->vhost);
  4. if (vhost_is_edge && edge_traverse) {
  5. if ((ret = check_edge_token_traverse_auth()) != ERROR_SUCCESS) {
  6. srs_warn("token auth failed, ret=%d", ret);
  7. return ret;
  8. }
  9. }
  10. }

接着设置chunk 的大小

[cpp] view plaincopy
  1. int chunk_size = _srs_config->get_chunk_size(req->vhost);
  2. if ((ret = rtmp->set_chunk_size(chunk_size)) != ERROR_SUCCESS) {
  3. srs_error("set chunk_size=%d failed. ret=%d", chunk_size, ret);
  4. return ret;
  5. }

回应客户端。连接ok

[cpp] view plaincopy
  1. if ((ret = rtmp->response_connect_app(req, local_ip.c_str())) != ERROR_SUCCESS) {
  2. srs_error("response connect app failed. ret=%d", ret);
  3. return ret;
  4. }

然后连接就结束了,进入stream_service_cycle()函数,从名字上就可以看出。这个函数是开始就如流命令时代

[cpp] view plaincopy
  1. while (!disposed) {
  2. ret = stream_service_cycle();
  3. // stream service must terminated with error, never success.
  4. // when terminated with success, it's user required to stop.
  5. if (ret == ERROR_SUCCESS) {
  6. continue;
  7. }
  8. // when not system control error, fatal error, return.
  9. if (!srs_is_system_control_error(ret)) {
  10. if (ret != ERROR_SOCKET_TIMEOUT && !srs_is_client_gracefully_close(ret)) {
  11. srs_error("stream service cycle failed. ret=%d", ret);
  12. }
  13. return ret;
  14. }
  15. // for republish, continue service
  16. if (ret == ERROR_CONTROL_REPUBLISH) {
  17. // set timeout to a larger value, wait for encoder to republish.
  18. rtmp->set_send_timeout(SRS_REPUBLISH_RECV_TIMEOUT_US);
  19. rtmp->set_recv_timeout(SRS_REPUBLISH_SEND_TIMEOUT_US);
  20. srs_trace("control message(unpublish) accept, retry stream service.");
  21. continue;
  22. }
  23. // for "some" system control error,
  24. // logical accept and retry stream service.
  25. if (ret == ERROR_CONTROL_RTMP_CLOSE) {
  26. // TODO: FIXME: use ping message to anti-death of socket.
  27. // @see: https://github.com/ossrs/srs/issues/39
  28. // set timeout to a larger value, for user paused.
  29. rtmp->set_recv_timeout(SRS_PAUSED_RECV_TIMEOUT_US);
  30. rtmp->set_send_timeout(SRS_PAUSED_SEND_TIMEOUT_US);
  31. srs_trace("control message(close) accept, retry stream service.");
  32. continue;
  33. }
  34. // for other system control message, fatal error.
  35. srs_error("control message(%d) reject as error. ret=%d", ret, ret);
  36. return ret;
  37. }

stream_service_cycle()函数闪亮登场

首先进行一些安全验证

[cpp] view plaincopy
  1. f ((ret = rtmp->identify_client(res->stream_id, type, req->stream, req->duration)) != ERROR_SUCCESS) {
  2. if (!srs_is_client_gracefully_close(ret)) {
  3. srs_error("identify client failed. ret=%d", ret);
  4. }
  5. return ret;
  6. }
  7. req->strip();
  8. srs_trace("client identified, type=%s, stream_name=%s, duration=%.2f",
  9. srs_client_type_string(type).c_str(), req->stream.c_str(), req->duration);
  10. // security check
  11. if ((ret = security->check(type, ip, req)) != ERROR_SUCCESS) {
  12. srs_error("security check failed. ret=%d", ret);
  13. return ret;
  14. }
  15. srs_info("security check ok");

然后进入比较有意思的环节

[cpp] view plaincopy
  1. SrsSource* source = SrsSource::fetch(req);
  2. if (!source) {
  3. if ((ret = SrsSource::create(req, server, server, &source)) != ERROR_SUCCESS) {
  4. return ret;
  5. }
  6. }
  7. srs_assert(source != NULL);

根据req,寻找是否有这个源,如果没有,那么久创建一个。主要creat()是个静态函数。实现代码为

[cpp] view plaincopy
  1. int SrsSource::create(SrsRequest* r, ISrsSourceHandler* h, ISrsHlsHandler* hh, SrsSource** pps)
  2. {
  3. int ret = ERROR_SUCCESS;
  4. string stream_url = r->get_stream_url();
  5. string vhost = r->vhost;
  6. // should always not exists for create a source.
  7. srs_assert (pool.find(stream_url) == pool.end());
  8. SrsSource* source = new SrsSource();
  9. if ((ret = source->initialize(r, h, hh)) != ERROR_SUCCESS) {
  10. srs_freep(source);
  11. return ret;
  12. }
  13. pool[stream_url] = source;
  14. srs_info("create new source for url=%s, vhost=%s", stream_url.c_str(), vhost.c_str());
  15. *pps = source;
  16. return ret;
  17. }

创建一个新的source,并且放到poo中。pool是什么

[cpp] view plaincopy
  1. static std::map<std::string, SrsSource*> pool;

也是一个全局的静态变量,用了存储所欲的源。到此。谜底进一步解开了。

同意fetch()函数也是静态的。

[cpp] view plaincopy
  1. static SrsSource* fetch(SrsRequest* r);
  2. static SrsSource* fetch(std::string vhost, std::string app, std::string stream);

ok!在绕道循环函数看看接下来该怎么办

[cpp] view plaincopy
  1. SrsStatistic* stat = SrsStatistic::instance();
  2. if ((ret = stat->on_client(_srs_context->get_id(), req, this, type)) != ERROR_SUCCESS) {
  3. srs_error("stat client failed. ret=%d", ret);
  4. return ret;
  5. }

这个是做统计用的,没啥。

[cpp] view plaincopy
  1. bool vhost_is_edge = _srs_config->get_vhost_is_edge(req->vhost);
  2. bool enabled_cache = _srs_config->get_gop_cache(req->vhost);
  3. srs_trace("source url=%s, ip=%s, cache=%d, is_edge=%d, source_id=%d[%d]",
  4. req->get_stream_url().c_str(), ip.c_str(), enabled_cache, vhost_is_edge,
  5. source->source_id(), source->source_id());
  6. source->set_cache(enabled_cache);

判断是否是边缘节点,是否需要gop缓冲。无他

[cpp] view plaincopy
  1. switch (type) {
  2. case SrsRtmpConnPlay: {
  3. srs_verbose("start to play stream %s.", req->stream.c_str());
  4. // response connection start play
  5. if ((ret = rtmp->start_play(res->stream_id)) != ERROR_SUCCESS) {
  6. srs_error("start to play stream failed. ret=%d", ret);
  7. return ret;
  8. }
  9. if ((ret = http_hooks_on_play()) != ERROR_SUCCESS) {
  10. srs_error("http hook on_play failed. ret=%d", ret);
  11. return ret;
  12. }
  13. srs_info("start to play stream %s success", req->stream.c_str());
  14. ret = playing(source);
  15. http_hooks_on_stop();
  16. return ret;
  17. }
  18. case SrsRtmpConnFMLEPublish: {
  19. srs_verbose("FMLE start to publish stream %s.", req->stream.c_str());
  20. if ((ret = rtmp->start_fmle_publish(res->stream_id)) != ERROR_SUCCESS) {
  21. srs_error("start to publish stream failed. ret=%d", ret);
  22. return ret;
  23. }
  24. return publishing(source);
  25. }
  26. case SrsRtmpConnFlashPublish: {
  27. srs_verbose("flash start to publish stream %s.", req->stream.c_str());
  28. if ((ret = rtmp->start_flash_publish(res->stream_id)) != ERROR_SUCCESS) {
  29. srs_error("flash start to publish stream failed. ret=%d", ret);
  30. return ret;
  31. }
  32. return publishing(source);
  33. }
  34. default: {
  35. ret = ERROR_SYSTEM_CLIENT_INVALID;
  36. srs_info("invalid client type=%d. ret=%d", type, ret);
  37. return ret;
  38. }
  39. }

大流程看。好像是根据不同走到了发布或者播放流程里。但首先。这个type是从哪里来的。怎么没有发现呢?

[cpp] view plaincopy
  1. int SrsRtmpServer::identify_client(int stream_id, SrsRtmpConnType& type, string& stream_name, double& duration)

在这个函数里做确认类型的。rmptserver类不在我们这次分析。

我们分析下play的流程,函数名称为

[cpp] view plaincopy
  1. int SrsRtmpConn::playing(SrsSource* source)

关键代码

[cpp] view plaincopy
  1. SrsConsumer* consumer = NULL;
  2. if ((ret = source->create_consumer(this, consumer)) != ERROR_SUCCESS) {
  3. srs_error("create consumer failed. ret=%d", ret);
  4. return ret;
  5. }
  6. SrsAutoFree(SrsConsumer, consumer);
  7. srs_verbose("consumer created success.");

利用source创建一个consumer.创建代码为

[cpp] view plaincopy
  1. int SrsSource::create_consumer(SrsConnection* conn, SrsConsumer*& consumer, bool ds, bool dm, bool dg)
  2. {
  3. int ret = ERROR_SUCCESS;
  4. consumer = new SrsConsumer(this, conn);
  5. consumers.push_back(consumer);
  6. double queue_size = _srs_config->get_queue_length(_req->vhost);
  7. consumer->set_queue_size(queue_size);
  8. // if atc, update the sequence header to gop cache time.
  9. if (atc && !gop_cache->empty()) {
  10. if (cache_metadata) {
  11. cache_metadata->timestamp = gop_cache->start_time();
  12. }
  13. if (cache_sh_video) {
  14. cache_sh_video->timestamp = gop_cache->start_time();
  15. }
  16. if (cache_sh_audio) {
  17. cache_sh_audio->timestamp = gop_cache->start_time();
  18. }
  19. }
  20. // copy metadata.
  21. if (dm && cache_metadata && (ret = consumer->enqueue(cache_metadata, atc, jitter_algorithm)) != ERROR_SUCCESS) {
  22. srs_error("dispatch metadata failed. ret=%d", ret);
  23. return ret;
  24. }
  25. srs_info("dispatch metadata success");
  26. // copy sequence header
  27. // copy audio sequence first, for hls to fast parse the "right" audio codec.
  28. // @see https://github.com/ossrs/srs/issues/301
  29. if (ds && cache_sh_audio && (ret = consumer->enqueue(cache_sh_audio, atc, jitter_algorithm)) != ERROR_SUCCESS) {
  30. srs_error("dispatch audio sequence header failed. ret=%d", ret);
  31. return ret;
  32. }
  33. srs_info("dispatch audio sequence header success");
  34. if (ds && cache_sh_video && (ret = consumer->enqueue(cache_sh_video, atc, jitter_algorithm)) != ERROR_SUCCESS) {
  35. srs_error("dispatch video sequence header failed. ret=%d", ret);
  36. return ret;
  37. }
  38. srs_info("dispatch video sequence header success");
  39. // copy gop cache to client.
  40. if (dg && (ret = gop_cache->dump(consumer, atc, jitter_algorithm)) != ERROR_SUCCESS) {
  41. return ret;
  42. }
  43. // print status.
  44. if (dg) {
  45. srs_trace("create consumer, queue_size=%.2f, jitter=%d", queue_size, jitter_algorithm);
  46. } else {
  47. srs_trace("create consumer, ignore gop cache, jitter=%d", jitter_algorithm);
  48. }
  49. // for edge, when play edge stream, check the state
  50. if (_srs_config->get_vhost_is_edge(_req->vhost)) {
  51. // notice edge to start for the first client.
  52. if ((ret = play_edge->on_client_play()) != ERROR_SUCCESS) {
  53. srs_error("notice edge start play stream failed. ret=%d", ret);
  54. return ret;
  55. }
  56. }
  57. return ret;
  58. }

代码好长。主要是创建 放进数据结构中,并拷贝一些metadata进去,对于edge的处理,还没有看明白。

之后的动作

[cpp] view plaincopy
  1. SrsQueueRecvThread trd(consumer, rtmp, SRS_PERF_MW_SLEEP);
  2. // start isolate recv thread.
  3. if ((ret = trd.start()) != ERROR_SUCCESS) {
  4. srs_error("start isolate recv thread failed. ret=%d", ret);
  5. return ret;
  6. }

什么?单独创建了一个接受线程,实现了recv never send,send never recv,据说这样效率提高了33%

在绕回去

[cpp] view plaincopy
  1. // delivery messages for clients playing stream.
  2. wakable = consumer;
  3. ret = do_playing(source, consumer, &trd);
  4. wakable = NULL;

进入下一个循环体do_playing()

在分析下一个函数之前。让我总结下缩做的工作

1)创建或者获取了一个source

2)创建一个consumer

3) 创建一个接受线程

下面开始看函数的关键代码

[cpp] view plaincopy
  1. // setup the realtime.
  2. realtime = _srs_config->get_realtime_enabled(req->vhost);
  3. // setup the mw config.
  4. // when mw_sleep changed, resize the socket send buffer.
  5. mw_enabled = true;
  6. change_mw_sleep(_srs_config->get_mw_sleep_ms(req->vhost));
  7. // initialize the send_min_interval
  8. send_min_interval = _srs_config->get_send_min_interval(req->vhost);

做实时性 merge write 的设置

[cpp] view plaincopy
  1. while (!trd->empty()) {
  2. SrsCommonMessage* msg = trd->pump();
  3. srs_verbose("pump client message to process.");
  4. if ((ret = process_play_control_msg(consumer, msg)) != ERROR_SUCCESS) {
  5. if (!srs_is_system_control_error(ret) && !srs_is_client_gracefully_close(ret)) {
  6. srs_error("process play control message failed. ret=%d", ret);
  7. }
  8. return ret;
  9. }
  10. }

首先处理接受消息。主要是暂停的消息。

下面进入核心代码

[cpp] view plaincopy
  1. <strong><span style="color:#ff6666;"> int count = (send_min_interval > 0)? 1 : 0;
  2. if ((ret = consumer->dump_packets(&msgs, count)) != ERROR_SUCCESS) {
  3. srs_error("get messages from consumer failed. ret=%d", ret);
  4. return ret;
  5. }</span></strong>

这段代码的作用就是,把消息,从consumer里,拷贝到本地的msg队列里。当然。这个拷贝是浅拷贝,只是指针过来了。

首先看msgs的定义

[cpp] view plaincopy
  1. SrsMessageArray msgs(SRS_PERF_MW_MSGS)

这个类里有个核心变量

[cpp] view plaincopy
  1. SrsSharedPtrMessage** msgs;

可以看到它保存的是一个指向指针的指针。

那么dump_packets是怎么实现的呢?

[cpp] view plaincopy
  1. int SrsConsumer::dump_packets(SrsMessageArray* msgs, int& count)
  2. {
  3. int ret =ERROR_SUCCESS;
  4. srs_assert(count >= 0);
  5. srs_assert(msgs->max > 0);
  6. // the count used as input to reset the max if positive.
  7. int max = count? srs_min(count, msgs->max) : msgs->max;
  8. // the count specifies the max acceptable count,
  9. // here maybe 1+, and we must set to 0 when got nothing.
  10. count = 0;
  11. if (should_update_source_id) {
  12. srs_trace("update source_id=%d[%d]", source->source_id(), source->source_id());
  13. should_update_source_id = false;
  14. }
  15. // paused, return nothing.
  16. if (paused) {
  17. return ret;
  18. }
  19. // pump msgs from queue.
  20. if ((ret = queue->dump_packets(max, msgs->msgs, count)) != ERROR_SUCCESS) {
  21. return ret;
  22. }
  23. return ret;
  24. }
[cpp] view plaincopy
  1. int SrsMessageQueue::dump_packets(int max_count, SrsSharedPtrMessage** pmsgs, int& count)
  2. {
  3. int ret = ERROR_SUCCESS;
  4. int nb_msgs = (int)msgs.size();
  5. if (nb_msgs <= 0) {
  6. return ret;
  7. }
  8. srs_assert(max_count > 0);
  9. count = srs_min(max_count, nb_msgs);
  10. SrsSharedPtrMessage** omsgs = msgs.data();
  11. for (int i = 0; i < count; i++) {
  12. pmsgs[i] = omsgs[i];
  13. }
  14. SrsSharedPtrMessage* last = omsgs[count - 1];
  15. av_start_time = last->timestamp;
  16. if (count >= nb_msgs) {
  17. // the pmsgs is big enough and clear msgs at most time.
  18. msgs.clear();
  19. } else {
  20. // erase some vector elements may cause memory copy,
  21. // maybe can use more efficient vector.swap to avoid copy.
  22. // @remark for the pmsgs is big enough, for instance, SRS_PERF_MW_MSGS 128,
  23. //      the rtmp play client will get 128msgs once, so this branch rarely execute.
  24. msgs.erase(msgs.begin(), msgs.begin() + count);
  25. }
  26. return ret;
  27. }

ok代码我就不想分析了。只是个指针拷贝。

下一个问题。consumer的数据是怎么来的呢?

看source的代码,比如音频数据

[cpp] view plaincopy
  1. int SrsSource::on_audio_imp(SrsSharedPtrMessage* msg)

代码里有这么一段

[cpp] view plaincopy
  1. // copy to all consumer
  2. if (!drop_for_reduce) {
  3. for (int i = 0; i < (int)consumers.size(); i++) {
  4. SrsConsumer* consumer = consumers.at(i);
  5. if ((ret = consumer->enqueue(msg, atc, jitter_algorithm)) != ERROR_SUCCESS) {
  6. srs_error("dispatch the audio failed. ret=%d", ret);
  7. return ret;
  8. }
  9. }
  10. srs_info("dispatch audio success.");
  11. }

到此,一个循环就结束了。

srs代码学习(4)-怎么转发流相关推荐

  1. srs代码学习(2)- 线程模型

    代码阅读到现在.发现srs有两大类线程.一个是主线程的逻辑. 一个是监听线程簇.结构图如下 一定还有第三种线程模型,负责底层的多路分发.今天还没有发现. 2016.08.25--21:00 仔细阅读了 ...

  2. srs代码学习(1)--listen建立过程

    srs的服务侦听的建立过程. 以rtmp服务为例 srs服务侦听的建立依靠从上到下的三个类.分别是 SrsServer SrsStreamListener SrsTcpListener 端口侦听过程为 ...

  3. 2016年大数据Spark“蘑菇云”行动代码学习之AdClickedStreamingStats模块分析

    2016年大数据Spark"蘑菇云"行动代码学习之AdClickedStreamingStats模块分析     系统背景:用户使用终端设备(IPAD.手机.浏览器)等登录系统,系 ...

  4. SRS 代码分析【mpeg-ts解析】

    SRS 代码分析[mpeg-ts解析] 1.SrsTsContext的decode接口定义如下: int SrsTsContext::decode(SrsBuffer* stream, ISrsTsH ...

  5. VTM10.0代码学习5:coding_unit()cu_pred_data()

    此系列是为了记录自己学习VTM10.0的过程和锻炼表达能力,主要是从解码端进行入手.由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步. 上一篇博客(VTM10.0代码学习4)讲述了将语 ...

  6. VTM10.0代码学习10:EncGOP_compressGOP()

    此系列是为了记录自己学习VTM10.0的过程,目前正在看编码端.主要的参考文档有JVET-S2001-vH和JVET-S2002-v1.由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步 ...

  7. 社区圆桌分享:代码学习无止境,程序员如何规划自己的职业生涯发展?

    CloudWeGo Study Group 是由 CloudWeGo 社区发起的学习小组,开展以 30 天为一期的源码解读和学习活动,帮助新成员融入社区圈子,和社区 Committer 互动交流,并学 ...

  8. Android第一行代码学习思考笔记(碎片、广播、持久化技术和Android数据库)

    Android第一行代码学习思考笔记(碎片.广播.持久化技术和Android数据库 第四章 手机平板要兼顾--探究碎片 4.1碎片是什么(Fragment) 4.2碎片的使用方式 4.2.1碎片的简单 ...

  9. VTM10.0代码学习7:decompressCtu()xReconIntraQT()

    此系列是为了记录自己学习VTM10.0的过程和锻炼表达能力,主要是从解码端进行入手.由于本人水平有限,出现的错误恳请大家指正,欢迎与大家一起交流进步. 之前的博文(VTM10.0代码学习3)提到两个重 ...

最新文章

  1. (转)浅谈HTML5与css3画饼图!
  2. 数据挖掘十大经典算法之——C4.5 算法
  3. 高并发编程-线程生产者消费者的综合示例
  4. mysql怎么写Connection_MySQL里面的CONNECTION_ID
  5. Zoom并非端到端加密、TikTok第一季度下载量全球第一等|Decode the Week
  6. invalid substring parameter in My Opportunity account determination
  7. 一个 Safari 的 new Date() bug
  8. ajax怎样上传多张图片,多图片Ajax上传
  9. 全球电动汽车电池隔膜行业调研及趋势分析报告
  10. 俄数学天才破解庞加莱猜想拒领百万奖金
  11. Word 2003 出现 向程序发送命令时出现问题 的 解决方案
  12. DataGrid Bind Checkbox....
  13. 第2章 藏书阁签到,修为突破
  14. 2022.4.9第十三届蓝桥杯web组省赛个人题解
  15. 【转】linux下杀死进程(kill)的N种方法
  16. 鼠标背景带光圈突出显示
  17. python文本处理尝试
  18. STM32入门之LCD1602驱动
  19. 检索COM 类工厂中 CLSID 为 {000209FF-0000-0000-C000-000000000046} 的组件失败,错误: 80080005
  20. mac 查看本机 ip

热门文章

  1. iPhone系统常用文件夹位置
  2. mysql数据库引擎调优
  3. 用PHP做负载均衡指南
  4. CodeForces - 1562D2 Two Hundred Twenty One (hard version)(二分)
  5. CodeForces - 1334C Circle of Monsters(贪心)
  6. AcWing - 175 电路维修(思维建边+最短路)
  7. a律13折线pcm编码例题_a律13折线pcm编码例题
  8. python安装后无法运行任何软件_为啥我按照python安装教程,总说无法启动此程序,因为计算机中丢失?...
  9. lpt算法c语言程序,LPT算法的性能(近似).ppt
  10. 逆向工程核心原理学习笔记(十一):栈