live555中ts流详细解析
live555中ts流详细解析
该文档主要是对live555源码下testProgs中testMPEG2TransportStreamer服务器端的详细分析。主要分析ts流实现的总体调用流程。(重新整理下,当时有些 代码、图片复制到CSDN出了点问题)
testMPEG2TransportStreamer中主要涉及的类视图如下,其中这些类主要实现都在liveMedia库下,最原始基类为Medium,下面这些类都是从这个类继承而来。
1.主要是对于ts流文件信息的获取,可以理解为获取数据的对象。
2.主要是对于ts流文件信息的传输,可以理解为消费数据的对象。
testMPEG2TransportStreamer服务器端main函数主要流程为:
1)创建我们需要使用的环境(UsageEnvironment* env该类主要是对于一些出错信息,输入、输出信息等的封装)。
2)创建组播地址、端口号(其中端口号和组播地址被传入到一个叫Groupsock类中,该类主要是封装了socket创建udp通信,对于端口信息的封装)。
3)创建sink类用于数据的发送,创建RTSP服务器、媒体会话等(被一个宏定义IMPLEMENT_RTSP_SERVER所控制了,源码中,这个宏是被注释掉的),其中对sink类入口函数如下:
// Create an appropriate 'RTP sink' from the RTP 'groupsock'://创建一个RTPsink Sink就是消费数据的对象,比如把接收到的数据存储到文件,这个文件就是一个Sink。RTPSink* videoSink
videoSink = SimpleRTPSink::createNew(*env,&rtpGroupsock,33,90000,"video","MP2T",1,True,False/*no 'M' bit*/);//33:RTP有效负荷格式 90000:时间戳频率 "video":媒体类型字符串 "MP2T":RTP有效负载格式名字 1:数字通道? True:允许每个数据包的多个帧 False:做正常的兆位规则
最后是最重要的一个传输数据入口函数play()。
play()该函数的封装如下:
void play() {
unsignedconstinputDataChunkSize = TRANSPORT_PACKETS_PER_NETWORK_PACKET*TRANSPORT_PACKET_SIZE;
//Open the input file as a 'byte-stream file source':
//将输入文件作为“字节流文件源”打开
/*主要工作的内容是打开流文件,获取文件的大小信息,获取文件描述符,文件所处的状态以及调整文件指针位置*/
ByteStreamFileSource* fileSource
= ByteStreamFileSource::createNew(*env,inputFileName,inputDataChunkSize); //创建一个流文件源
if (fileSource==NULL){
*env<< "Unable to open file\"" << inputFileName
<< "\"as a byte-stream file source\n";
exit(1);
}
// Create a 'framer' for the input source(to give us proper inter-packet gaps): /*为输入文件创建一个成帧器*/
videoSource= MPEG2TransportStreamFramer::createNew(*env,fileSource); //Finally, start playing://最后开始播放
*env<< "Beginning to read fromfile...\n";
videoSink->startPlaying(*videoSource,afterPlaying,videoSink);//获取数据源videoSource,FramedSource* videoSource,RTPSink* videoSink;//消费数据的对象
}
其中TRANSPORT_PACKET_SIZE为传输的ts包,大小为188字节,TRANSPORT_PACKETS_PER_NETWORK_PACKET大小为7,所以每次传输的最大ts包为188x7。前面所说的source类中关于获取数据的信息,是由下面函数所创建,同时传入我们的ts流文件(inputFileName即ts流文件,env为使用环境,inputDataChunSize为传输的ts包大小)。
ByteStreamFileSource* fileSource
= ByteStreamFileSource::createNew(*env,inputFileName,inputDataChunkSize);
另外还有一个函数videoSource = MPEG2TransportStreamFramer::createNew(*env,fileSource);可以理解为一个成帧器,意义不是很大。
接下来是play()函数中最重要的一个函数startPlaying(),该函数是一个发送数据的入口函数。
// Finally, start playing://最后开始播放
*env << "Beginningto read from file...\n";
videoSink->startPlaying(*videoSource,afterPlaying,videoSink);//获取数据源videoSource,FramedSource* videoSource,RTPSink* videoSink;//消费数据的对象
该函数的实现是在MediaSink类中,所以和前面所理解的sink为发送数据的对象是一致的。该函数如下:
Boolean MediaSink::startPlaying(MediaSource&source, /* source = videoSource*/ afterPlayingFunc*afterFunc, /*afterFunc= afterPlaying*/void*afterClientData) {/*afterClientData = videosink*/
// Make sure we're not already being played: 确保我们还没有播放
if (fSource!=NULL){
envir().setResultMsg("Thissink is already being played");
returnFalse;
}// Makesure our source is compatible: 确保我们的源是兼容的
if (!sourceIsCompatibleWithUs(source)) {
envir().setResultMsg("MediaSink::startPlaying():source is not compatible!");
returnFalse;
} //记下一些要使用的对象,保存数据
fSource= (FramedSource*)&source;
fAfterFunc= afterFunc;
fAfterClientData=afterClientData;
returncontinuePlaying(); //在继承类MultiFrameRTPSink中
}
从上面代码中可以看到,其实还没有进行真正的数据发送,只是在为数据发送做准备,比如有没有源,是否兼容等,接着调用continuePlaying()函数,该函数是在MultiFramedRTPSink类中实现,如下:
Boolean MultiFramedRTPSink::continuePlaying() {
// Send the first packet. 发送第一个包 // (This will also schedule any futuresends.) 这样将安排未来的任何发送
buildAndSendPacket(True);
returnTrue;
}
从上面函数中看到该函数并没有做什么事情,而是buildAndSendPacket(True)
函数的一个封装,buildAndSendPacket(True)函数仍然在该类中实现,如下:
void MultiFramedRTPSink::buildAndSendPacket(BooleanisFirstPacket) {
fIsFirstPacket=isFirstPacket; //isFirstPacket = true;
//Set up the RTP header: 设置RTP包头
unsignedrtpHdr=0x80000000;// RTP version 2; marker ('M') bit not set (by default; it can be setlater) RTP版本2,标记M位而不设置(默认情况下,它可以之后设置)
rtpHdr |=(fRTPPayloadType<<16); //有效负荷
rtpHdr |=fSeqNo;// sequence number 序列号
fOutBuf->enqueueWord(rtpHdr); //向包头加入一个字,此函数在MediaSink中
// Note where the RTP timestamp willgo. RTP时间戳将去哪里。(我们可以不填写直到我们开始包装负载帧。)
// (We can't fill this inuntil we start packing payload frames.)
fTimestampPosition=fOutBuf->curPacketSize();
fOutBuf->skipBytes(4);// leave a hole for the timestamp 离开时间戳 在缓冲中空出时间戳的位置
fOutBuf->enqueueWord(SSRC());
// Allow for a special,payload-format-specific header following the // RTP header:
//允许一个特殊的、具体的RTP报头报头负载格式如下:
fSpecialHeaderPosition=fOutBuf->curPacketSize(); //这些函数在MediaSink里面
fSpecialHeaderSize= specialHeaderSize();
fOutBuf->skipBytes(fSpecialHeaderSize);
// Begin packing as many (complete) framesinto the packet as we can: 因为我们可以开始打包尽可能多的数据包
fTotalFrameSpecificHeaderSizes=0;
fNoFramesLeft= False;
fNumFramesUsedSoFar=0; //一个包中已打入的帧数。
//头准备好了,再打包帧数据
packFrame();
}
从上面的函数可以看出该函数主要是对ts的一个封包,将其封包为rtp包,其中主要是对rtp包头的设置,当其准备好包头以后,就准备在打包帧数据了,接着调用packFrame()函数,该函数同样在MultiFramedRTPSink类中实现,如下:
void MultiFramedRTPSink::packFrame(){
// Get the next frame. 得到下一个帧 // First, skip over thespace we'll use for any frame-specific header:首先,跳过我们将使用的任何特定帧的空间:
fCurFrameSpecificHeaderPosition=fOutBuf->curPacketSize();
fCurFrameSpecificHeaderSize=frameSpecificHeaderSize();
fOutBuf->skipBytes(fCurFrameSpecificHeaderSize);
fTotalFrameSpecificHeaderSizes+=fCurFrameSpecificHeaderSize;
// See if we have an overflow frame that wastoo big for the last pkt 看看我们是否有溢出的帧,由于最后的包太大
if (fOutBuf->haveOverflowData()){
// Use this frame before reading a new onefrom the source
//如果有帧数据,在使用之。OverflowData是指上次打包时,剩下的帧数据,因为一个包可能容纳不了一个帧。
unsignedframeSize=fOutBuf->overflowDataSize();
structtimeval presentationTime = fOutBuf->overflowPresentationTime();
unsigneddurationInMicroseconds=fOutBuf->overflowDurationInMicroseconds();
fOutBuf->useOverflowData();
afterGettingFrame1(frameSize,0,presentationTime,durationInMicroseconds);
} else{
// Normal case: we need to read a new framefrom the source
//当发送的帧数据没有时,我们需要从源中读取一个新的帧
if (fSource== NULL) return;
/*fOutBuf->curPtr():新数据存放开始的位置;
*fOutBuf->totalBytesAvailable():缓冲中空余的空间大小;
*afterGettingFrame:因为可能source中的读数据函数会被放在任务调度中,所以把获取帧后应调用的函数传授给source;
*ourHandleClosure:这个是source结束时(比如文件读完了)要调用的函数; */
fSource->getNextFrame(fOutBuf->curPtr(),fOutBuf->totalBytesAvailable(), //fOutBuf输出包缓冲 getNextFrame在FrameSource类里面afterGettingFrame,this,ourHandleClosure,this);
}
}
该函数主要实现的功能是:对上一个包帧数据的一个判断,如果还有数据,那么调用afterGettingFrame1()函数进行数据发送,该函数是一个重点,实现如下:
void MultiFramedRTPSink
::afterGettingFrame1(unsignedframeSize,unsignednumTruncatedBytes, //数据截断numTruncatedBytes= 0,没有发生数据截断;structtimevalpresentationTime,unsigneddurationInMicroseconds) {
if (fIsFirstPacket) {
// Record the fact that we're starting toplay now: 记录事件,我们准备开始播放
gettimeofday(&fNextSendTime,NULL);
}
fMostRecentPresentationTime = presentationTime;
if (fInitialPresentationTime.tv_sec==0&& fInitialPresentationTime.tv_usec==0) {
fInitialPresentationTime = presentationTime;
}
if (numTruncatedBytes>0) {//如果给予一帧的缓冲不够大,就会发生截断一帧数据的现象。但也只能提示一下用户
unsignedconstbufferSize=fOutBuf->totalBytesAvailable();
envir()<< "MultiFramedRTPSink::afterGettingFrame1():The input frame data was too large for our buffer size ("
<< bufferSize << "). "
<< numTruncatedBytes<<"bytes of trailing data was dropped! Correct this by increasing \"OutPacketBuffer::maxSize\" to atleast "
<< OutPacketBuffer::maxSize+numTruncatedBytes<<", *before* creating this'RTPSink'. (Current value is "
<< OutPacketBuffer::maxSize<<".)\n";
}
unsignedcurFragmentationOffset=fCurFragmentationOffset; //当前段偏移
unsignednumFrameBytesToUse=frameSize;//帧字节大小
unsignedoverflowBytes=0;//上一个包的剩余字节数
//If we have already packed one or more frames into this packet,
//check whether this new frame is eligible to be packed after them.
//(This is independent of whether the packet has enough room for this
//new frame; that check comes later.)
//如果我们已经将一个或多个帧封装到这个数据包中,检查这个新的帧是否有资格被打包。(这是独立于这个数据包是否有足够的空间来进行这个新的帧。)
if (fNumFramesUsedSoFar>0) { //fNumFramesUsedSoFar= 0 一个包中已打入的字节
//如果包中已有了一个帧,并且不允许再打入新的帧了,则只记录下新的帧
if ((fPreviousFrameEndedFragmentation
&& !allowOtherFramesAfterLastFragment())
|| !frameCanAppearAfterPacketStart(fOutBuf->curPtr(),frameSize)) {
// Save away this frame for next time: 为下一次保存这个帧
numFrameBytesToUse= 0;
fOutBuf->setOverflowData(fOutBuf->curPacketSize(),frameSize, presentationTime,durationInMicroseconds);
}
}//表示当前打入的是否是上一个帧的最后一块数据。
fPreviousFrameEndedFragmentation=False;
//下面是计算获取的帧中有多少数据可以打到当前包中,剩下的数据就作为overflow数据保存下来
if (numFrameBytesToUse>0) { //Check whether this frame overflows the packet 检查这个帧是否在数据包溢出
if (fOutBuf->wouldOverflow(frameSize)) {
// Don't use this frame now; instead, saveit as overflow data, and
// send it in the next packetinstead. However, if the frame is too
// big to fit in a packet by itself,then we need to fragment it (and
// use some of it in this packet, ifthe payload format permits this.)
if(isTooBigForAPacket(frameSize)
&& (fNumFramesUsedSoFar==0 || allowFragmentationAfterStart())){
// We need to fragment this frame, and usesome of it now:
overflowBytes= computeOverflowForNewFrame(frameSize);
numFrameBytesToUse-=overflowBytes;
fCurFragmentationOffset+=numFrameBytesToUse;
} else{
// We don't use any of this frame now:
overflowBytes= frameSize;
numFrameBytesToUse=0;
}
fOutBuf->setOverflowData(fOutBuf->curPacketSize()+numFrameBytesToUse,
overflowBytes,presentationTime,durationInMicroseconds);
} elseif (fCurFragmentationOffset> 0) {
//This is the last fragment of a frame that was fragmented over
//more than one packet. Do any specialhandling for this case:
//这是一个帧的最后一个片段,在一个以上的数据包被碎片化。对这种情况做任何特殊处理:
fCurFragmentationOffset=0;
fPreviousFrameEndedFragmentation=True;
}
}
if (numFrameBytesToUse==0 && frameSize> 0) {
// Send our packet now, because we havefilled it up:
//如果包中有数据并且没有新数据了,则发送之。(这种情况好像很难发生啊!)
//现在/发送我们的数据包,因为我们已经填补了它:
sendPacketIfNecessary();
} else{
//需要向包中打入数据。
// Use this frame in our outgoing packet:
unsignedchar*frameStart=fOutBuf->curPtr(); //获取新数据存放开始的位置
fOutBuf->increment(numFrameBytesToUse);
// do this now, in case"doSpecialFrameHandling()" calls "setFramePadding()" toappend padding bytes
//Here's where any payload format specific processing gets done:
doSpecialFrameHandling(curFragmentationOffset,frameStart,
numFrameBytesToUse,presentationTime,
overflowBytes);
++fNumFramesUsedSoFar;
// Update the time at which the next packetshould be sent, based
//on the duration of the frame that we just packed into it.
//However, if this frame has overflow data remaining, then don't
//count its duration yet。 //根据我们刚装入的帧的持续时间,更新下一个数据包的时间。但是,如果这个帧有溢出的数据,那么就不要计算它的持续时间。
if (overflowBytes==0){
fNextSendTime.tv_usec+=durationInMicroseconds;
fNextSendTime.tv_sec+=fNextSendTime.tv_usec/1000000;
fNextSendTime.tv_usec%=1000000;
}
//如果需要,就发出包,否则继续打入数据。
//Send our packet now if (i) it's already at our preferred size, or
//(ii) (heuristic) another frame of the same size as the one we just
// read would overflow the packet, or
//(iii) it contains the last fragment of a fragmented frame, and we
// don't allow anything else to follow thisor
//(iv) one frame per packet is allowed:
//如果(我)它已经在我们的首选大小,或发送我们的数据包
//(二)(启发式)另一个帧相同大小的一个我们只是读会溢出数据包,或
//(III)它包含一个支离破碎的框架的最后一个片段,我们
//不允许任何其他东西遵循这个或
//每包一个帧是允许的:
if (fOutBuf->isPreferredSize()
|| fOutBuf->wouldOverflow(numFrameBytesToUse)
|| (fPreviousFrameEndedFragmentation&&
!allowOtherFramesAfterLastFragment())
|| !frameCanAppearAfterPacketStart(fOutBuf->curPtr()-frameSize, frameSize)) {
// The packet is ready to be sent now // 数据包已经准备好
sendPacketIfNecessary();
} else{
// There's room for more frames; try gettinganother: 有更多的帧的空间,尝试得到另一个:
packFrame();
}
}
}
从上面函数可以看到该函数主要实现了检查该帧数据是否有资格打包,保存下一个帧,计算有多少数据可以打包等,如果数据包已经准备好了就调用sendPacketIfNecessary()函数进行数据的发送,否则调用packFrame()函数再次进行判断,接下来主要介绍发送数据函数sendPacketIfNecessary(),主要实现如下:
void MultiFramedRTPSink::sendPacketIfNecessary(){
if (fNumFramesUsedSoFar>0) { //Send the packet:发送数据包
#ifdef TEST_LOSS
if ((our_random()%10)!=0)//simulate 10% packet loss #####
#endif
if(!fRTPInterface.sendPacket(fOutBuf->packet(),fOutBuf->curPacketSize())) { //在RTPInterface里
// if failure handler has been specified,call it 如果指定了故障处理程序,则调用它
if (fOnSendErrorFunc!=NULL) (*fOnSendErrorFunc)(fOnSendErrorData);
}
++fPacketCount;
fTotalOctetCount += fOutBuf->curPacketSize();
fOctetCount += fOutBuf->curPacketSize()
- rtpHeaderSize- fSpecialHeaderSize - fTotalFrameSpecificHeaderSizes;
++fSeqNo; // for next time 序列号加1
} //如果还有剩余数据,则调整缓冲区
if (fOutBuf->haveOverflowData()
&& fOutBuf->totalBytesAvailable()>fOutBuf->totalBufferSize()/2) {
//Efficiency hack: Reset the packet start pointer to just in front of
//the overflow data (allowing for the RTP header and special headers),
//so that we probably don't have to "memmove()" the overflow data
//into place when building the next packet:
//包开始指针在溢出数据前(允许RTP报头和特殊的头),所以,我们可能不需要“memmove()“溢出数据当建立的下一个数据包时:
unsignednewPacketStart=fOutBuf->curPacketSize()
- (rtpHeaderSize+ fSpecialHeaderSize + frameSpecificHeaderSize());
fOutBuf->adjustPacketStart(newPacketStart);
} else{
// Normal case: Reset the packet startpointer back to the start:
//正常情况:重置数据包开始指针回到开始:
fOutBuf->resetPacketStart();
}
fOutBuf->resetOffset();
fNumFramesUsedSoFar=0;
if (fNoFramesLeft) { //如果再没有数据了,则结束之
//We're done:
onSourceClosure();
} else{
//如果还有数据,则在下一次需要发送的时间再次打包发送。
//We have more frames left to send. Figureout when the next frame
//is due to start playing, then make sure that we wait this long before
//sending the next packet.
//我们有更多的帧发送。确定下一个帧是何时开始播放的,然后请确保我们在发送下一个数据包之前先等上一段时间。
structtimeval timeNow;
gettimeofday(&timeNow,NULL);
int secsDiff = fNextSendTime.tv_sec-timeNow.tv_sec;
int64_t uSecondsToGo=secsDiff*1000000+ (fNextSendTime.tv_usec-timeNow.tv_usec);
if (uSecondsToGo<0|| secsDiff < 0) {// sanitycheck: Make sure that the time-to-delay is non-negative:完整性检查:确保时间延迟是非负的
uSecondsToGo= 0;
} //Delay this amount of time: 延迟一段时间
nextTask()= envir().taskScheduler().scheduleDelayedTask(uSecondsToGo, (TaskFunc*)sendNext,this);
}
}
sendPacketIfNecessary()该函数主要是发送数据,主要有确定下一个帧数据何时发送,调用延迟程序,重置数据包开始指针等。
以上情况均属于上一个包中有数据剩余,接着回到packFrame()函数,如果没有数据,我们将调用getNextFrame()函数获得帧数据
fSource->getNextFrame(fOutBuf->curPtr(),fOutBuf->totalBytesAvailable(),//fOutBuf输出包缓冲 getNextFrame在FrameSource类里面afterGettingFrame,this, ourHandleClosure,this);
getNextFrame()函数是在FrameSource类里面实现,主要是获取帧数据,该函数如下:
void FramedSource::getNextFrame(unsignedchar*to,unsignedmaxSize,afterGettingFunc*afterGettingFunc,void*afterGettingClientData,onCloseFunc*onCloseFunc,void*onCloseClientData){
// Make sure we're notalready being read 确保我们没有被读
if (fIsCurrentlyAwaitingData) {
envir()<< "FramedSource[" <<this <<"]::getNextFrame(): attempting to read more thanonce at the same time!\n";
envir().internalError();
}
fTo= to;//存放读取到数据的内存指针
fMaxSize= maxSize;//允许的最大数据量,即fto指针指向的内存区间的大小
fNumTruncatedBytes=0;// bydefault; could be changed by doGetNextFrame()
fDurationInMicroseconds=0;// by default; could be changed by doGetNextFrame()
fAfterGettingFunc=afterGettingFunc;
fAfterGettingClientData=afterGettingClientData;
fOnCloseFunc= onCloseFunc;
fOnCloseClientData=onCloseClientData;
fIsCurrentlyAwaitingData=True;
doGetNextFrame();//从文件中获取数据,只是获取数据,在ByteStreamFileSource中
}
从上面可以看到其实该函数并没有做什么很多事,主要是对参数进行了一些设置,接着调用doGetNextFrame()函数,但是该函数却没有在该类中进行实现,而是在继承类ByteStreamFileSource中进行实现,函数如下:
void ByteStreamFileSource::doGetNextFrame(){//判断是否已经到文件尾部,feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为非零值,否则为0。
if (feof(fFid)||ferror(fFid) || (fLimitNumBytesToStream&&fNumBytesToStream== 0)) {
handleClosure();
return;
}
#ifdef READ_FROM_FILES_SYNCHRONOUSLY
doReadFromFile();
#else
if (!fHaveStartedReading) {
// Await readable data from the file:
envir().taskScheduler().turnOnBackgroundReadHandling(fileno(fFid), (TaskScheduler::BackgroundHandlerProc*)&fileReadableHandler,this);
fHaveStartedReading= True;
}
#endif
}
从doGetNextFrame()函数可以看到主要是对ts文件流的一个判断,看是否已经读取完,而真正对文件的读取操作是在doReadFromFile()函数中的。同时会调用任务调度进行处理。所以这就是testMPEG2TransportStreamer服务器端传输ts流的总体实现过程。
live555中ts流详细解析的更多相关文章
- 关于TS流的解析
字节.在TS流里可以填入很多类型的数据,如视频.音频.自定义信息等.他的包的结构为,包头为4个字节,负载为184个字节(这184个字节不一定都是有效数据,有一些可能为填充数据). 工作形式: 因为在T ...
- TS流的解析
个字节不一定都是有效数据,有一些可能为填充数据). 工作形式: 因为在TS流里可以填入很多种东西,所以有必要有一种机制来确定怎么来标识这些数据.制定TS流标准的机构就规定了一些数据结构来定义.比如: ...
- 【转】python中的闭包详细解析
一.什么是闭包? 如果一个内嵌函数访问外部嵌套函数作用域的变量,并返回这个函数,则这个函数就是闭包 闭包必须满足三个条件: 1. 必须有一个内嵌函数 2. 内嵌函数必须引用外部嵌套函数中的变量 ...
- JavaScript中依赖注入详细解析
计算机编程的世界其实就是一个将简单的部分不断抽象,并将这些抽象组织起来的过程.JavaScript也不例外,在我们使用JavaScript编写应用时,我们是不是都会使用到别人编写的代码,例如一些著名的 ...
- Java中的初始化详细解析
今天所要详细讲解的是Java中的初始化,也就是new对象的过程中,其程序的行走流程. 先说没有静态成员变量和静态代码块的情况. public class NormalInit { public sta ...
- R语言中的遗传算法详细解析
前言 人类总是在生活中摸索规律,把规律总结为经验,再把经验传给后人,让后人发现更多的规规律,每一次知识的传递都是一次进化的过程,最终会形成了人类的智慧.自然界规律,让人类适者生存地活了下来,聪明的科学 ...
- Django模版中的过滤器详细解析 Django filter大全
就象本章前面提到的一样,模板过滤器是在变量被显示前修改它的值的一个简单方法. 过滤器看起来是这样的: {{ name|lower }} 显示的内容是变量 {{ name }} 被过滤器 lower 处 ...
- JS中的闭包 详细解析大全(面试避必考题)
JS中闭包的介绍 闭包的概念 闭包就是能够读取其他函数内部变量的函数. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变 ...
- MySQL中EXPLAIN命令详细解析
很多情况下我们需要知道某条SQL语句的性能,都会通过EXPLAIN命令来查看查询优化器是如何执行的. 如何使用 使用EXPLAIN很简单,只需要在执行的SQL前面加上EXPLAIN即可 explain ...
- Java中泛型的详细解析,深入分析泛型的使用方式
泛型的基本概念 泛型: 参数化类型 参数: 定义方法时有形参 调用方法时传递实参 参数化类型: 将类型由原来的具体的类型参数化,类似方法中的变量参数 类型定义成参数形式, 可以称为类型形参 在使用或者 ...
随机推荐
- What is RSS
What is RSS?RSS (Rich Site Summary) is a format for delivering regularly changing web content. Many ...
- 01 ansible的基本介绍
1.现有的企业服务器环境 在现在的企业中,特别是互联网公司,他们的业务量众多:比如负载均衡服务器.web服务器.动态解析(php)服务器.数据库(mysql)服务器以及网站缓存服务器,等等: 例如:一 ...
- 02 docker的基本用法
本章内容 1.OCI 2.docker核心组件--Cgroup与runC 3.docker的架构 4.docker的基本操作 5.安装docker环境 6.创建第一个容器 6.docker容器的状态变 ...
- C语言中字符数组的赋值和复制
/*C中,字符串,即字符数组的赋值与字符变量.常量.变量的赋值是不同的.初学者总会犯错误. 常见错误如下: 1.定义的时候直接用字符串赋值 char a[10]; char a[10]="h ...
- Vue-router 中hash模式和history模式的关系
Vue-router 中hash模式和history模式的关系 众所周知,vue+vue-router能够构建一个SPA单页面应用, 打包后只会生成一个index.html文件,而项目内页面的切换其实 ...
- arpspoof、driftnet工具使用
一.arpspoof.driftnet工具安装: 在kali liux中: 安装命令:apt install dsniff apt install driftnet 二.使用arpspoof ...
- Tcp网络模型
要摸清网络,那么第一步肯定是要清楚网络协议的分层结构,用上帝视角来看网络. 对于同一台设备上的进程间通信,有很多种方式,比如有管道.消息队列.共享内存.信号等方式,而对于不同设备上的进程间通信,就需要 ...
- windows 安装mysql57
1. 配置my.ini文件 在根目录下新建 "my.ini" 文件: 添加配置: [mysql] # 设置mysql客户端默认字符集 default-character-set=u ...
- Linux & 标准C语言学习 <DAY10>
一.函数递归 函数自己调用自己的行为,叫做函数递归 递归是分治思想的一种具体实现,就是把一个复杂而庞大的问题,分解成若干个相似的小问题,解决所有小问题以解决大问题 如果函数递归 ...
- 使用nsis美化安装向导后,安装时实现浏览器自定义协议打开
1. electron官方提供api,支持向注册表中写入协议,可通过浏览器打开 app.setAsDefaultProtocolClient('open-electron') 问题:1. 因为该方法时 ...