RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)
=====================================================
RTMPdump(libRTMP) 源代码分析系列文章:
RTMPDump (libRTMP) 源代码分析2:解析RTMP地址——RTMP_ParseURL()
RTMPdump (libRTMP) 源代码分析3: AMF编码
RTMPdump (libRTMP) 源代码分析4: 连接第一步——握手 (HandShake)
RTMPdump (libRTMP) 源代码分析5: 建立一个流媒体连接 (NetConnection部分)
RTMPdump (libRTMP) 源代码分析6: 建立一个流媒体连接 (NetStream部分 1)
RTMPdump (libRTMP) 源代码分析7: 建立一个流媒体连接 (NetStream部分 2)
RTMPdump (libRTMP) 源代码分析8: 发送消息 (Message)
RTMPdump (libRTMP) 源代码分析9: 接收消息 (Message) (接收视音频数据)
RTMPdump (libRTMP) 源代码分析10: 处理各种消息 (Message)
=====================================================
函数调用结构图
RTMPDump (libRTMP)的整体的函数调用结构图如下图所示。
详细分析
书接上回:RTMPdump 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
上回说到,有两个函数尤为重要:
RTMP_ReadPacket()
RTMP_ClientPacket()
而且分析了第一个函数。现在我们再来看看第二个函数吧。第二个函数的主要作用是:处理消息(Message),并做出响应。
先把带注释的代码贴上:
//处理接收到的Chunk
int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
int bHasMediaPacket = 0;
switch (packet->m_packetType)
{
//RTMP消息类型ID=1,设置块大小
case 0x01:
/* chunk size */
//----------------
r->dlg->AppendCInfo("处理收到的数据。消息 Set Chunk Size (typeID=1)。");
//-----------------------------
RTMP_LogPrintf("处理消息 Set Chunk Size (typeID=1)\n");
HandleChangeChunkSize(r, packet);
break;
//RTMP消息类型ID=3,致谢
case 0x03:
/* bytes read report */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);
break;
//RTMP消息类型ID=4,用户控制
case 0x04:
/* ctrl */
//----------------
r->dlg->AppendCInfo("处理收到的数据。消息 User Control (typeID=4)。");
//-----------------------------
RTMP_LogPrintf("处理消息 User Control (typeID=4)\n");
HandleCtrl(r, packet);
break;
//RTMP消息类型ID=5
case 0x05:
/* server bw */
//----------------
r->dlg->AppendCInfo("处理收到的数据。消息 Window Acknowledgement Size (typeID=5)。");
//-----------------------------
RTMP_LogPrintf("处理消息 Window Acknowledgement Size (typeID=5)\n");
HandleServerBW(r, packet);
break;
//RTMP消息类型ID=6
case 0x06:
/* client bw */
//----------------
r->dlg->AppendCInfo("处理收到的数据。消息 Set Peer Bandwidth (typeID=6)。");
//-----------------------------
RTMP_LogPrintf("处理消息 Set Peer Bandwidth (typeID=6)\n");
HandleClientBW(r, packet);
break;
//RTMP消息类型ID=8,音频数据
case 0x08:
/* audio data */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize); */
HandleAudio(r, packet);
bHasMediaPacket = 1;
if (!r->m_mediaChannel)
r->m_mediaChannel = packet->m_nChannel;
if (!r->m_pausing)
r->m_mediaStamp = packet->m_nTimeStamp;
break;
//RTMP消息类型ID=9,视频数据
case 0x09:
/* video data */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize); */
HandleVideo(r, packet);
bHasMediaPacket = 1;
if (!r->m_mediaChannel)
r->m_mediaChannel = packet->m_nChannel;
if (!r->m_pausing)
r->m_mediaStamp = packet->m_nTimeStamp;
break;
//RTMP消息类型ID=15,AMF3编码,忽略
case 0x0F: /* flex stream send */
RTMP_Log(RTMP_LOGDEBUG,
"%s, flex stream send, size %lu bytes, not supported, ignoring",
__FUNCTION__, packet->m_nBodySize);
break;
//RTMP消息类型ID=16,AMF3编码,忽略
case 0x10: /* flex shared object */
RTMP_Log(RTMP_LOGDEBUG,
"%s, flex shared object, size %lu bytes, not supported, ignoring",
__FUNCTION__, packet->m_nBodySize);
break;
//RTMP消息类型ID=17,AMF3编码,忽略
case 0x11: /* flex message */
{
RTMP_Log(RTMP_LOGDEBUG,
"%s, flex message, size %lu bytes, not fully supported",
__FUNCTION__, packet->m_nBodySize);
/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */
/* some DEBUG code */
#if 0
RTMP_LIB_AMFObject obj;
int nRes = obj.Decode(packet.m_body+1, packet.m_nBodySize-1);
if(nRes < 0) {
RTMP_Log(RTMP_LOGERROR, "%s, error decoding AMF3 packet", __FUNCTION__);
/*return; */
}
obj.Dump();
#endif
if (HandleInvoke(r, packet->m_body + 1, packet->m_nBodySize - 1) == 1)
bHasMediaPacket = 2;
break;
}
//RTMP消息类型ID=18,AMF0编码,数据消息
case 0x12:
/* metadata (notify) */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: notify %lu bytes", __FUNCTION__,
packet->m_nBodySize);
//处理元数据,暂时注释
/*
if (HandleMetadata(r, packet->m_body, packet->m_nBodySize))
bHasMediaPacket = 1;
break;
*/
//RTMP消息类型ID=19,AMF0编码,忽略
case 0x13:
RTMP_Log(RTMP_LOGDEBUG, "%s, shared object, not supported, ignoring",
__FUNCTION__);
break;
//RTMP消息类型ID=20,AMF0编码,命令消息
//处理命令消息!
case 0x14:
//----------------
r->dlg->AppendCInfo("处理收到的数据。消息 命令 (AMF0编码) (typeID=20)。");
//-----------------------------
/* invoke */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: invoke %lu bytes", __FUNCTION__,
packet->m_nBodySize);
RTMP_LogPrintf("处理命令消息 (typeID=20,AMF0编码)\n");
/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */
if (HandleInvoke(r, packet->m_body, packet->m_nBodySize) == 1)
bHasMediaPacket = 2;
break;
//RTMP消息类型ID=22
case 0x16:
{
/* go through FLV packets and handle metadata packets */
unsigned int pos = 0;
uint32_t nTimeStamp = packet->m_nTimeStamp;
while (pos + 11 < packet->m_nBodySize)
{
uint32_t dataSize = AMF_DecodeInt24(packet->m_body + pos + 1); /* size without header (11) and prevTagSize (4) */
if (pos + 11 + dataSize + 4 > packet->m_nBodySize)
{
RTMP_Log(RTMP_LOGWARNING, "Stream corrupt?!");
break;
}
if (packet->m_body[pos] == 0x12)
{
HandleMetadata(r, packet->m_body + pos + 11, dataSize);
}
else if (packet->m_body[pos] == 8 || packet->m_body[pos] == 9)
{
nTimeStamp = AMF_DecodeInt24(packet->m_body + pos + 4);
nTimeStamp |= (packet->m_body[pos + 7] << 24);
}
pos += (11 + dataSize + 4);
}
if (!r->m_pausing)
r->m_mediaStamp = nTimeStamp;
/* FLV tag(s) */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: FLV tag(s) %lu bytes", __FUNCTION__, packet.m_nBodySize); */
bHasMediaPacket = 1;
break;
}
default:
RTMP_Log(RTMP_LOGDEBUG, "%s, unknown packet type received: 0x%02x", __FUNCTION__,
packet->m_packetType);
#ifdef _DEBUG
RTMP_LogHex(RTMP_LOGDEBUG, (const uint8_t *)packet->m_body, packet->m_nBodySize);
#endif
}
return bHasMediaPacket;
}
里面注释的比较多,可以看出,大体的思路是,根据接收到的消息(Message)类型的不同,做出不同的响应。例如收到的消息类型为0x01,那么就是设置块(Chunk)大小的协议,那么就调用相应的函数进行处理。
因此,本函数可以说是程序的灵魂,收到的各种命令消息都要经过本函数的判断决定调用哪个函数进行相应的处理。
在这里注意一下消息类型为0x14的消息,即消息类型ID为20的消息,是AMF0编码的命令消息。这在RTMP连接中是非常常见的,比如说各种控制命令:播放,暂停,停止等等。我们来仔细看看它的调用。
可以发现它调用了HandleInvoke()函数来处理服务器发来的AMF0编码的命令,来看看细节:
/* Returns 0 for OK/Failed/error, 1 for 'Stop or Complete' */
static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
AMFObject obj;
AVal method;
int txn;
int ret = 0, nRes;
if (body[0] != 0x02) /* make sure it is a string method name we start with */
{
RTMP_Log(RTMP_LOGWARNING, "%s, Sanity failed. no string method in invoke packet",
__FUNCTION__);
return 0;
}
nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
if (nRes < 0)
{
RTMP_Log(RTMP_LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
return 0;
}
AMF_Dump(&obj);
AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
txn = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1));
RTMP_Log(RTMP_LOGDEBUG, "%s, server invoking <%s>", __FUNCTION__, method.av_val);
if (AVMATCH(&method, &av__result))
{
AVal methodInvoked = {0};
int i;
for (i=0; i<r->m_numCalls; i++) {
if (r->m_methodCalls[i].num == txn) {
methodInvoked = r->m_methodCalls[i].name;
AV_erase(r->m_methodCalls, &r->m_numCalls, i, FALSE);
break;
}
}
if (!methodInvoked.av_val) {
RTMP_Log(RTMP_LOGDEBUG, "%s, received result id %d without matching request",
__FUNCTION__, txn);
goto leave;
}
//----------------
char temp_str[100];
sprintf(temp_str,"接收数据。消息 %s 的 Result",methodInvoked.av_val);
r->dlg->AppendCInfo(temp_str);
//-----------------------------
RTMP_Log(RTMP_LOGDEBUG, "%s, received result for method call <%s>", __FUNCTION__,
methodInvoked.av_val);
if (AVMATCH(&methodInvoked, &av_connect))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","Result (Connect)");
//-----------------------------
if (r->Link.token.av_len)
{
AMFObjectProperty p;
if (RTMP_FindFirstMatchingProperty(&obj, &av_secureToken, &p))
{
DecodeTEA(&r->Link.token, &p.p_vu.p_aval);
SendSecureTokenResponse(r, &p.p_vu.p_aval);
}
}
if (r->Link.protocol & RTMP_FEATURE_WRITE)
{
SendReleaseStream(r);
SendFCPublish(r);
}
else
{
//----------------
r->dlg->AppendCInfo("发送数据。消息 Window Acknowledgement Size (typeID=5)。");
//-----------------------------
RTMP_LogPrintf("发送消息Window Acknowledgement Size(typeID=5)\n");
RTMP_SendServerBW(r);
RTMP_SendCtrl(r, 3, 0, 300);
}
//----------------
r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (CreateStream)。");
//-----------------------------
RTMP_LogPrintf("发送命令消息“CreateStream” (typeID=20)\n");
RTMP_SendCreateStream(r);
if (!(r->Link.protocol & RTMP_FEATURE_WRITE))
{
/* Send the FCSubscribe if live stream or if subscribepath is set */
if (r->Link.subscribepath.av_len)
SendFCSubscribe(r, &r->Link.subscribepath);
else if (r->Link.lFlags & RTMP_LF_LIVE)
SendFCSubscribe(r, &r->Link.playpath);
}
}
else if (AVMATCH(&methodInvoked, &av_createStream))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","Result (CreateStream)");
//-----------------------------
r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));
if (r->Link.protocol & RTMP_FEATURE_WRITE)
{
SendPublish(r);
}
else
{
if (r->Link.lFlags & RTMP_LF_PLST)
SendPlaylist(r);
//----------------
r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (Play)。");
//-----------------------------
RTMP_LogPrintf("发送命令消息“play” (typeID=20)\n");
SendPlay(r);
RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
}
}
else if (AVMATCH(&methodInvoked, &av_play) ||
AVMATCH(&methodInvoked, &av_publish))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","Result (Play or Publish)");
//-----------------------------
r->m_bPlaying = TRUE;
}
free(methodInvoked.av_val);
}
else if (AVMATCH(&method, &av_onBWDone))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","onBWDone");
//-----------------------------
if (!r->m_nBWCheckCounter)
SendCheckBW(r);
}
else if (AVMATCH(&method, &av_onFCSubscribe))
{
/* SendOnFCSubscribe(); */
}
else if (AVMATCH(&method, &av_onFCUnsubscribe))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","onFCUnsubscribe");
//-----------------------------
RTMP_Close(r);
ret = 1;
}
else if (AVMATCH(&method, &av_ping))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","Ping");
//-----------------------------
SendPong(r, txn);
}
else if (AVMATCH(&method, &av__onbwcheck))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","onBWcheck");
//-----------------------------
SendCheckBWResult(r, txn);
}
else if (AVMATCH(&method, &av__onbwdone))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","onBWdone");
//-----------------------------
int i;
for (i = 0; i < r->m_numCalls; i++)
if (AVMATCH(&r->m_methodCalls[i].name, &av__checkbw))
{
AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
break;
}
}
else if (AVMATCH(&method, &av__error))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","error");
//-----------------------------
RTMP_Log(RTMP_LOGERROR, "rtmp server sent error");
}
else if (AVMATCH(&method, &av_close))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","close");
//-----------------------------
RTMP_Log(RTMP_LOGERROR, "rtmp server requested close");
RTMP_Close(r);
}
else if (AVMATCH(&method, &av_onStatus))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","onStatus");
//-----------------------------
AMFObject obj2;
AVal code, level;
AMFProp_GetObject(AMF_GetProp(&obj, NULL, 3), &obj2);
AMFProp_GetString(AMF_GetProp(&obj2, &av_code, -1), &code);
AMFProp_GetString(AMF_GetProp(&obj2, &av_level, -1), &level);
RTMP_Log(RTMP_LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.av_val);
if (AVMATCH(&code, &av_NetStream_Failed)
|| AVMATCH(&code, &av_NetStream_Play_Failed)
|| AVMATCH(&code, &av_NetStream_Play_StreamNotFound)
|| AVMATCH(&code, &av_NetConnection_Connect_InvalidApp))
{
r->m_stream_id = -1;
RTMP_Close(r);
RTMP_Log(RTMP_LOGERROR, "Closing connection: %s", code.av_val);
}
else if (AVMATCH(&code, &av_NetStream_Play_Start))
{
int i;
r->m_bPlaying = TRUE;
for (i = 0; i < r->m_numCalls; i++)
{
if (AVMATCH(&r->m_methodCalls[i].name, &av_play))
{
AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
break;
}
}
}
else if (AVMATCH(&code, &av_NetStream_Publish_Start))
{
int i;
r->m_bPlaying = TRUE;
for (i = 0; i < r->m_numCalls; i++)
{
if (AVMATCH(&r->m_methodCalls[i].name, &av_publish))
{
AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
break;
}
}
}
/* Return 1 if this is a Play.Complete or Play.Stop */
else if (AVMATCH(&code, &av_NetStream_Play_Complete)
|| AVMATCH(&code, &av_NetStream_Play_Stop)
|| AVMATCH(&code, &av_NetStream_Play_UnpublishNotify))
{
RTMP_Close(r);
ret = 1;
}
else if (AVMATCH(&code, &av_NetStream_Seek_Notify))
{
r->m_read.flags &= ~RTMP_READ_SEEKING;
}
else if (AVMATCH(&code, &av_NetStream_Pause_Notify))
{
if (r->m_pausing == 1 || r->m_pausing == 2)
{
RTMP_SendPause(r, FALSE, r->m_pauseStamp);
r->m_pausing = 3;
}
}
}
else if (AVMATCH(&method, &av_playlist_ready))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","playlist_ready");
//-----------------------------
int i;
for (i = 0; i < r->m_numCalls; i++)
{
if (AVMATCH(&r->m_methodCalls[i].name, &av_set_playlist))
{
AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
break;
}
}
}
else
{
}
leave:
AMF_Reset(&obj);
return ret;
}
int
RTMP_FindFirstMatchingProperty(AMFObject *obj, const AVal *name,
AMFObjectProperty * p)
{
int n;
/* this is a small object search to locate the "duration" property */
for (n = 0; n < obj->o_num; n++)
{
AMFObjectProperty *prop = AMF_GetProp(obj, NULL, n);
if (AVMATCH(&prop->p_name, name))
{
*p = *prop;
return TRUE;
}
if (prop->p_type == AMF_OBJECT)
{
if (RTMP_FindFirstMatchingProperty(&prop->p_vu.p_object, name, p))
return TRUE;
}
}
return FALSE;
}
该函数主要做了以下几步:
1.调用AMF_Decode()解码AMF命令数据
2.调用AMFProp_GetString()获取具体命令的字符串
3.调用AVMATCH()比较字符串,不同的命令做不同的处理,例如以下几个:
AVMATCH(&methodInvoked, &av_connect) AVMATCH(&methodInvoked, &av_createStream) AVMATCH(&methodInvoked, &av_play) AVMATCH(&methodInvoked, &av_publish) AVMATCH(&method, &av_onBWDone)
等等,不一一例举了
具体的处理过程如下所示。在这里说一个“建立网络流”(createStream)的例子,通常发生在建立网络连接(NetConnection)之后,播放(Play)之前。
else if (AVMATCH(&methodInvoked, &av_createStream))
{
//----------------
r->dlg->AppendMLInfo(20,0,"命令消息","Result (CreateStream)");
//-----------------------------
r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));
if (r->Link.protocol & RTMP_FEATURE_WRITE)
{
SendPublish(r);
}
else
{
if (r->Link.lFlags & RTMP_LF_PLST)
SendPlaylist(r);
//----------------
r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (Play)。");
//-----------------------------
RTMP_LogPrintf("发送命令消息“play” (typeID=20)\n");
SendPlay(r);
RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
}
}
由代码可见,程序先获取了stream_id,然后发送了两个消息(Message),分别是SendPlaylist()和SendPlay(),用于获取播放列表,以及开始播放流媒体数据。
rtmpdump源代码(Linux):http://download.csdn.net/detail/leixiaohua1020/6376561
rtmpdump源代码(VC 2005 工程):http://download.csdn.net/detail/leixiaohua1020/6563163
RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)的更多相关文章
- RTMPdump(libRTMP) 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 5: 建立一个流媒体连接 (NetConnection部分)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- Spark源代码分析之中的一个:Job提交执行总流程概述
Spark是一个基于内存的分布式计算框架.执行在其上的应用程序,依照Action被划分为一个个Job.而Job提交执行的总流程.大致分为两个阶段: 1.Stage划分与提交 (1)Job依照RDD之间 ...
- RTMPdump(libRTMP)源代码分析 4: 连接第一步——握手(Hand Shake)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 10: 处理各种消息(Message)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 9: 接收消息(Message)(接收视音频数据)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 8: 发送消息(Message)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- 转:RTMPDump源代码分析
0: 主要函数调用分析 rtmpdump 是一个用来处理 RTMP 流媒体的开源工具包,支持 rtmp://, rtmpt://, rtmpe://, rtmpte://, and rtmps://. ...
- Android在如何建立一个WebServer
今天老板交待任务最终完成了,感觉收获颇多,所以写一个关于它的记录,首先,看一下.老板的需求 需求: 希望移动端的用户标识(IMEI)和HTML页面的用户标识(Cookie)连接起来,当中HTML页面可 ...
随机推荐
- Android自定义View(一、初体验自定义TextView)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51454685 本文出自:[openXu的博客] 目录: 继承View重写onDraw方法 自 ...
- 使用C++将OpenCV中Mat的数据写入二进制文件,用Matlab读出
在使用OpenCV开发程序时,如果想查看矩阵数据,比较费劲,而matlab查看数据很方便,有一种方法,是matlab和c++混合编程,可以用matlab访问c++的内存,可惜我不会这种方式,所以我就把 ...
- 有奖试读—Windows PowerShell实战指南(第2版)
为什么要学PowerShell? Windows用户都已习惯于使用图形化界面去完成工作,因为GUI总能轻易地实现很多功能,并且不需要记住很多命令.使得短时间学会一种工具成为可能. 但是不幸的是,GUI ...
- sh里的变量 $0 $1 $$ $#
$0就是该bash文件名 $?显示最后命令的退出状态.0表示没有错误,其他任何值表明有错误. $*所有位置参数的内容:就是调用调用本bash shell的参数. $@基本上与上面相同.只不过是 &qu ...
- Erlang edoc 多级目录出错
Erlang edoc 多级目录出错使用rebar doc来生成项目文档.但是当erl源文件目录src下建立子目录,并新建erlang文件后,就无法生成文档. 例如,新建 src/tttt/, 并添加 ...
- 对 jiffies 溢出、回绕及 time_after 宏的理解
原文如下: 关于jiffies变量: 全局变量jiffies用来记录自启动以来产生的节拍的总数.系统启动时会将该变量初始化为0,此后,每当时钟中断产生时就会增加该变量的值.jiffies和另外 ...
- Python爬虫! 单爬,批量爬,这都不是事!
昨天做了一个煎蛋网妹子图的爬虫,个人感觉效果不错.但是每次都得重复的敲辣么多的代码(相比于Java或者其他语言的爬虫实现,Python的代码量可谓是相当的少了),就封装了一下!可以实现对批量网址以及单 ...
- java的list几种实现方式的效率(ArrayList、LinkedList、Vector、Stack),以及 java时间戳的三种获取方式比较
一.list简介 List列表类,顺序存储任何对象(顺序不变),可重复. List是继承于Collection的接口,不能实例化.实例化可以用: ArrayList(实现动态数组),查询快(随意访问或 ...
- Android初级教程IP拨号器初识广播接受者
需求:输入ip号码并且保存在本地,监听打电话广播,如果电话号码以0开头,则加上ip区号拨打. 首先定义一个页面布局: <LinearLayout xmlns:android="http ...
- erMaster插件
需求: 在做开源项目时,了解基本业务后.试图从数据库表设计来分析项目.通过visio时绘制操作繁琐,另外不能与数据库连动.于是想找一款快速绘制er图,并且能够和数据库连动的软件工具. eclipse插 ...