Live555源码解析(4) - 鱼儿上钩来

上一篇Live555源码解析(3) - 服务开启,愿者上钩中我们讲到RTSPServer创建后,带来了两项重要支持(旁注类比):

  • incomingConnectionHandler服务 - 挂着鱼饵的钓钩
  • 哈希表
    • ServerMediaSessions - 钓竿与手之间所有互动
    • ClientConnections - 钓线
    • ClientSessions - 钓钩与鱼之间所有互动

整理一下,其实服务器已经摆好姿势,严阵以待第一个吃鱼饵的鱼上钩了,那么接下来,我们就看鱼儿是怎么一步一步上钩来。

1. 鱼

钓鱼要有鱼,服务要有被服务者,Live555媒体服务器的服务对象就是支持RTSP/RTP协议的客户端。从官网对客户端介绍中我们可以看到,目前支持如下主流客户端:

要钓就钓大鱼,本篇就采用VLC播放器作为客户端,来探测一下咬钩引起的连锁反应。

2. 钩

先看一下服务器准备好后的命令行提示界面,如下图所示:


关键几个信息,说明如下:

  • 入口 Play URL

    Play streams from this server using the URL
    rtsp://192.168.56.1/<filename>
    where <filename> is a file present in the current directory.
    

    从中我们可以得到几个信息:

    • rtsp 代表使用的是TCP作为传输层
    • 192.168.53.1 表示的是服务器所在主机IP地址,未显式给出端口号8554,说明使用了知名端口554
    • <filename> 文件必须与存放在进程当前目录
  • 支持文件类型

    • .264 / .265
    • .aac / .ac3 / .amr / .mp3 / .ogg / .wav
    • .dv / .m4e / .mkv / .mpg / .ts / .vob / .webm

因此,本篇中我们以ts文件类型为例,将bipbop-gear1-all.ts文件置于live555MediaServer可执行文件同一路径下。对于VLC而言,想要播放(点播)该文件,则其入口为:

rtsp://192.168.56.1/bipbop-gear1-all.ts

这也就是鱼所看到的钩,而同时,服务器正处于doEventLoop()的循环等待中,正如河边静气凝神握着钓竿的手。

3. 来

如图所示,VLC客户端打开网络串流rtsp://192.168.56.1/bipbop-gear1-all.ts,开始咬钩。

果不其然,这触发了doEventLoop()所调用的BasicTaskScheduler::SingleStep()中的如下代码。

int selectResult = select(fMaxNumSockets, &readSet, &writeSet, &exceptionSet, &tv_timeToDelay);
if(selectResult <0)
{
    if( GetLastError() != EINTR )
    {
        // 异常错误,视为严重故障;打印错误信息后退出
        print_Set_info();
        abort();                
    }
}
else //if(selectResult <0)
{
    HandlerIterator iter(*fHandlers);
    HandlerDescriptor* handler;
    if(fLastHandledSocketNum >= 0)
    {   
        // 如已处理过socket读写,则找到前次socket读写的下一个链表节点
        while((handler = iter.next()) != NULL)
        {
            if(handler->socketNum == fLastHandledSocketNum) break;
        }
        if(handler == NULL)
        {
            // 未找到,重置相关值
            fLastHandlerSocketNum = -1;
            iter.reset();
        }
    }
    while((handler = iter.next()) != NULL)
    {
        // 找到链表中合法节点,开始处理
        int sock = handler->socketNum;
        int resultConditionSet = 0;
        if(FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet)) resultConditionSet |= SOCKET_READABLE;
        if(FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet) resultConditionSet |= SOCKET_WRITEABLE;
        if(FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet) resultConditionSet |= SOCKET_EXCEPTION;
        if((resultConditionSet&handler->conditionSet) != 0 && handler->handlerProc != NULL)
        {
            // 保存当前处理节点socketNum
            fLastHandledSocketNum = sock;
            (*handler->handlerProc)(handler->clientData, resultConditionSet);
            break;
        }
    } // while((handler = iter.next()) != NULL)
    ...
}

代码已于 Live555源码解析(1) - Main 寻根问祖,留其筋骨中Section 3进行了详细说明,这里不再赘述。总之要注意的是,incomingConnectionHandler服务已经注册好,存放位置就是HandlerSet中(详见Live555源码解析(3) - 服务开启,愿者上钩
Section 2.1.1.1.4)。

3.1 incomingConnectionHandler

有朋自远方来,GenericMediaServer::incomingConnectionHandler终于粉墨登场。

void GenericMediaServer::incomingConnectionHandler(void* instance, int /*mask*/)
{
    GenericMediaServer* server = (GenericMediaServer*)instance;
    server->incomingConnectionHandler();
}

这里的instance归根结底注册时是在GenericMediaServer构造函数中用的this指针,因此调用的也就依然是自身无参的incomingConnectionHandler()。

void GenericMediaServer::incomingConnectionHandler()
{
    incomingConnectionHandlerOnSocket(fServerSocket);
}

还是一层封装,为了类接口的隐藏。

void GenericMediaServer::incomingConnectionHandlerOnSocket(int serverSocket)
{
    //@3.1.1 socket accept 
    struct sockaddr_in clientAddr;
    SOCKLEN_T clientAddrLen = sizeof clientAddr;
    int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
    if(clientSocket < 0)
    {
        int err = envir().getErrno();
        if(err != EWOULDBLOCK)
            envir().setResultErrMsg("accept() failed: ");
        return;
    }
    
    //@3.1.2 socket revise
    ignoreSigPipeOnSocket(clientSocket);
    makeSocketBlocking(clientSocket);
    increaseSendBufferTo(envir(), clientSocket, 50*1024);

    //@3.1.3 createNewClientConnection
    (void)createNewClientConnection(clientSocket, clientAddr);
}

@3.1.1 socket accept

这段代码其实并无多少好说的,如果你看过关于socket编程的书,那么这些就只是基础的socket accept套路。甚至,如果有需要的,比如显示客户端地址、端口信息,你也可以在套路上加上一些输出操作。

@3.1.2 socket revise

Live555源码解析(3) - 服务开启,愿者上钩 中介绍过的一样,忽略SIGPIPE是为了防止退出,非阻塞模式是为了支持同时多Socket,调整Buffer是为了配合重传。有兴趣的话可以详细阅读Live555源码解析(3) - 服务开启,愿者上钩
Section @1.1部分。

@3.1.3 createNewClientConnection

到了这里,才是真正的重头戏。这里实际调用的是RTSPServerSupportHTTPStreaming中的createNewClientConnection(),其代码如下:

GenericMediaServer::ClientConnection*
RTSPServerSupportingHTTPStreaming::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr)
{
        return new RTSPClientConnectionSupportingHTTPStreaming(*this, clientSocket, clientAddr);
}

其调用了RTSPClientConnectionSupportingHTTPStreaming构造函数。

RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming
::RTSPClientConnectionSupportingHTTPStreaming(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
    : RTSPClientConnection(ourServer, clientSocket, clientAddr)
    , fClientSessionId(0), fStreamSource(NULL), fPlaylistSource(NULL), fTCPSink(NULL) 
{}

进一步调用了RTSPClientConnection()构造函数。

RTSPServer::RTSPClientConnection
::RTSPClientConnection(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
    : GenericMediaServer::ClientConnection(ourServer, clientSocket, clientAddr)
    , fOurRTSPServer(ourServer), fClientInputSocket(fOurSocket)
    , fClientOutputSocket(fOurSocket), fIsActive(True)
    , fRecursionCount(0), fOurSessionCookie(NULL) 
{
        resetRequestBuffer();
}

resetRequestBuffer()真的就只是重设了请求Buffer(),没有其他操作。我们需要关注的是这里调用了GenericMediaServer::ClientConnection()函数。

GenericMediaServer::ClientConnection
::ClientConnection(GenericMediaServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
    : fOurServer(ourServer), fOurSocket(clientSocket), fClientAddr(clientAddr) 
{
    //@3.1.3.1 哈希表
    fOurServer.fClientConnections->Add((char const*)this, this);
    resetRequestBuffer();
    
    //@3.1.3.2 新服务incomingRequestHandler
    envir().taskScheduler().setBackgroundHandling(fOurSocket,   
                                    SOCKET_READABLE|SOCKET_EXCEPTION, 
                                    incomingRequestHandler, this);
}

@3.1.3.1 哈希表

如果你还记得Live555源码解析(3) - 服务开启,愿者上钩 中有提到,可修改ClientConnections哈希表的API之一就是ClientConnection()构造函数,那么这里就可以推出,咬钩的动作引起了数据的变化。该变化将永存于服务器生命周期内,直到有人将其从表中抹去。而抹去也只能由哈希表的另一API,~ClientConnection()析构函数完成。

也就是说,该连接,将从连接创建开始存在,将于连接销毁而逝去

@3.1.3.2 新服务incomingRequestHandler

程序是指令加上数据,数据固然重要,但必须指令将其盘活。代码到这里,开启了新的服务incomingRequestHandler,从名称上来看,应该是服务于客户端发出的RTSP请求,那么究竟是不是呢?就在下一小节继续跟踪进去。

3.2 新服务incomingRequestHandler

还是SingleStep()中那段调度代码,换了个主角,戏照样唱。这次轮到incomingRequestHandler。

void GenericMediaServer::ClientConnection::incomingRequestHandler(void* instance, int /*mask*/) 
{
    ClientConnection* connection = (ClientConnection*)instance;
    connection->incomingRequestHandler();
}

一层封装。

void GenericMediaServer::ClientConnection::incomingRequestHandler() 
{
    struct sockaddr_in dummy; 
    //@3.2.1 readSocket
    int bytesRead = readSocket(envir(), fOurSocket,                 &fRequestBuffer[fRequestBytesAlreadySeen], fRequestBufferBytesLeft, dummy);
    //@3.2.2 handleRequestBytes
    handleRequestBytes(bytesRead);
}

@3.2.1 readSocket

依然调用的是GroupsockHelper提供的帮助函数,其内部代码如下。

int readSocket(UsageEnvironment& env, int socket, 
        unsigned char* buffer, unsigned bufferSize, 
        struct sockaddr_in& fromAddress) 
{
    SOCKLEN_T addressSize = sizeof fromAddress;
    int bytesRead = recvfrom(socket, (char*)buffer, bufferSize, 0,
                            (struct sockaddr*)&fromAddress, &addressSize);
    if(bytesRead < 0)
    {
        int err = env.getErrno();
        if( err == 0 || err == EWOULDBLOCK // Windows
         || err == EAGAIN || err == 111 || err == 113 )// ECONNREFUSED(linux)
        {
            fromAddress.sin_addr.s_addr = 0;
            return 0;
        }
        socketErr(env, "recvfrom() error: ");
    }
    else if(bytesRead == 0)
        return -1;
    
    return bytesRead;
}

标准read套路,调用了winsock的recvfrom函数,对读取字节数进行校验。要么错误了清场报错,要么正确了返回。
稍加注意的是最后一个参数,也就是incomingRequestHandler调用中的dummy结构体,其用于存放请求发出者,也就是说客户端的地址,这里并没有实际用处。

@3.2.2 handleRequestBytes

void RTSPServer::RTSPClientConnection::handleRequestBytes(int newBytesRead)
{
    int numBytesRemaining = 0;
    ++fRecursionCount;
    
    do{
        RTSPServer::RTSPClientSession* clientSession = NULL;
        if(newBytesRead < 0 || (unsigned)newBytesRead>= RequestBufferBytesLeft) {
            //读取失败,或读取到错误信息,关闭连接
            fIsActive = False;
            break;
        }
        
        Boolean endOfMsg = False;
        unsigned char* ptr = &fRequestBuffer[fRequestBytesAlreadySeen];
        if(fClientOutputSocket != fClientInputSocket && numBytesRemaining == 0) {
            //去除空白字符
            unsigned toIndex = 0;
            for(int fromIndex = 0; fromIndex < newBytesRead; ++fromIndex) {
                char c = ptr[fromIndex];
                if(!(c == '' || c == '\t' || c == '\r' || c == '\n'))
                    ptr[toIndex++] = c;
            }
            newBytesRead = toIndex;
            
            //判定为RTSP-over-HTTP tunneling,其中内容可能使用Base64编码,
            //所以此处尽可能使用Base64解码
            unsigned numBytesToDecode = fBase64RemainderCount + newBytesRead;
            unsigned numBase64RemainderCount = numBytesToDecode % 4;
            numBytesToDecode -= newBase64RemainderCount;
            if(numBytesToDecode > 0) {
                ptr[newBytesRead] = '\0';
                unsigned decodedSize;
                unsigned char* decodedBytes = base64Decode((char const*)(ptr-fBase64RemainderCount), numBytesToDecode, decodedSize);
                
                unsigned char* to = ptr - fBase64RemainderCount;\
                for(unsigned i = 0; i < decodedSize; ++i)
                    *to++ = decodedBytes[i];
                    
                for(unsigned j=0; j < newBase64RemainderCount; ++j)
                    *to++ = (ptr-fBase64RemainderCount + numBytesToDecode)[j];
                
                newBytesRead = decodedSize - fBase64RemainderCount + newBase64RemainderCount;
                delete[] decodedBytes;
            }
            fBase64RemainderCount = newBase64RemainderCount;
        }       
        
        //@3.2.2.1 确保Request消息完整性
        unsigned char* tmpPtr = fLastCRLF + 2;
        if(fBase64RemainderCount == 0)
        {
            if(tmpPtr < fRequestBuffer)
                tmpPtr = fRequestBuffer;
            while(tmpPtr < &ptr[newBytesRead - 1])
            {
                //查找消息结尾标识符 <CR><LF><CR><LF>
                if(*tmpPtr == '\r' && *(tmpPtr + 1) == '\n')
                {
                    if(tmpPtr - fLastCRLF == 2)
                    {
                        endOfMsg = True;
                        break;
                    }
                    fLastCRLF = tmpPtr;
                }
                ++tmpPtr;
            }
        }
        
        fRequestBufferBytesLeft -= newBytesRead;
        fRequestBufferAlreadySeen += newBytesRead;
        // 确保Request完整性
        if(!endOfMsg) break;
        
        fRequestBuffer[fRequstBytesAlreadySeen] = '\0';
        char cmdName[RTSP_PARAM_STRING_MAX];
        char urlPreSuffix[RTSP_PARAM_STRING_MAX];
        char urlSuffix[RTSP_PARAM_STRING_MAX];
        char cseq[RTSP_PARAM_STRING_MAX];
        char sessionIdStr[RTSP_PARAM_STRING_MAX];
        unsigned contentLength = 0;
        
        fLastCRLF[2] = '\0';
        
        //@3.2.2.2 解析RTSP请求
        Boolean parseSucceeded = parseRTSPRequstString((char*)fRequestBuffer, fLastCRLF+2 - fRequestBuffer, cmdName, sizeof cmdName, urlPreSuffix, sizeof urlPreSuffix, urlSuffix, sizeof urlSuffix, cseq, sizeof cseq, sessionIdStr, sizeof sessionIdStr, contentLength);
        fLastCRLF[2] = '\r';
        
        Boolean playAfterSetup = False;
        if(parseSucceeded){
            //如头中存在Content-Length,则再次校验消息完整性
            if(ptr + newBytesRead < tmpPtr + 2 + contentLength) break;
            
            Boolean const requestIncludedSessionId = sessionIdStr[0] != '\0'; 
            if(requestIncludedSessionId){
                //如头中存在SessionID,则验证该会话是否存在,并确认其状态
                clientSession = (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
                if(clientSession != NULL) clientSession->noteLiveness();
            }
            
            //@3.2.2.3 处理RTSP请求中方法
            fCurrentCSeq = cseq;
            if(strcmp(cmdName, "OPTIONS") == 0){
                if(requestIncludedSessionId && clientSession == NULL)
                    handleCmd_sessionNotFound();
                else
                    handleCmd_OPTIONS();
            }
            else if(urlPreSuffix[0] == '\0' && rlSuffix[0] == '*' && urlSuffix[1] == '\0'){
                if(strcmp(cmdName, "GET_PARAMETER") == 0)
                    handleCmd_GET_PARAMETER((char const*)fRequestBuffer);
                else  if(strcmp(cmdName, "SET_PARAMETER") == 0)
                    handleCmd_SET_PARAMETER((char const*)fRequestBuffer);
                else
                    handleCmd_notSupported();
            }
            else if(strcmp(cmdName, "DESCRIBE") == 0){
                handleCmd_DESCRIBE(urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
            }
            else if(strcmp(cmdName, "SETUP") == 0){
                Boolean areAuthenticated = True;
                if(!requestIncludedSessionId){
                    // 创建会话
                    char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX];
                    urlTotalSuffix[0] = '\0';
                    if(urlPreSuffix[0] != '\0'){
                        strcat(urlTotalSuffix, urlPreSuffix);
                        strcat(urlTotalSuffix, "/");
                    }
                    strcat(urlTotalSuffix, urlSuffix);
                    if(authenticationOK("SETUP", urlTotalSuffix, (char const*)fRequestBuffer))
                        clientSession = (RTSPServer::RTSPClientSession*)fOurRTSPServer.createNewClientSessionWithId();
                    else 
                        areAuthenticated = False;
                }
                
                if (clientSession != NULL) 
                    clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix,urlSuffix, (char const*)fRequestBuffer);
                else
                    handleCmd_sessionNotFound();
            }
            else if (strcmp(cmdName, "TEARDOWN") == 0
                    || strcmp(cmdName, "PLAY") == 0
                    || strcmp(cmdName, "PAUSE") == 0
                    || strcmp(cmdName, "GET_PARAMETER") == 0
                    || strcmp(cmdName, "SET_PARAMETER") == 0) {
                if (clientSession != NULL)
                    clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
                else
                    handleCmd_sessionNotFound();
            }
            else if(strcmp(cmdName, "REGISTER") == 0 || strcmp(cmdName, "DEREGISTER") == 0) {
                char* url = strDupSize((char*)fRequestBuffer);
                if (sscanf((char*)fRequestBuffer, "%*s %s", url) == 1) {
                    Boolean reuseConnection, deliverViaTCP;
                    char* proxyURLSuffix;
                    parseTransportHeaderForREGISTER((const char*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);
                    handleCmd_REGISTER(cmdName, url, urlSuffix, (char const*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix);
                    delete[] proxyURLSuffix;
                } else {
                    handleCmd_bad();
                }
                delete[] url;
            } else {
                handleCmd_notSupported();
            }
        } else {
            // RTSP-over-HTTP tunnel
            char sessionCookie[RTSP_PARAM_STRING_MAX];
            char acceptStr[RTSP_PARAM_STRING_MAX];
            *fLastCRLF = '\0';
            parseSucceeded = parseHTTPRequestString(cmdName, sizeof cmdName,urlSuffix, sizeof urlPreSuffix,  sessionCookie, sizeof sessionCookie, acceptStr, sizeof acceptStr);
            *fLastCRLF = '\r';
            if (parseSucceeded) {
                // Check that the HTTP command is valid for RTSP-over-HTTP tunneling: There must be a 'session cookie'.
                Boolean isValidHTTPCmd = True;
                if (strcmp(cmdName, "OPTIONS") == 0) {
                    handleHTTPCmd_OPTIONS();
                } else if (sessionCookie[0] == '\0') {
                    if (strcmp(acceptStr, "application/x-rtsp-tunnelled") == 0) 
                        isValidHTTPCmd = False;
                    else
                        handleHTTPCmd_StreamingGET(urlSuffix, (char const*)fRequestBuffer);
                } else if (strcmp(cmdName, "GET") == 0){
                    handleHTTPCmd_TunnelingGET(sessionCookie);
                } else if (strcmp(cmdName, "POST") == 0) {
                    unsigned char const* extraData = fLastCRLF+4;
                    unsigned extraDataSize = &fRequestBuffer[fRequestBytesAlreadySeen] - extraData;
                    if (handleHTTPCmd_TunnelingPOST(sessionCookie, extraData, extraDataSize)) {
                        fIsActive = False;
                        break;
                    }
                }
                else 
                    isValidHTTPCmd = False;
                
                if (!isValidHTTPCmd)
                    handleHTTPCmd_notSupported();
                else
                    handleCmd_bad();
                
                send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0);
                
                if (playAfterSetup) 
                    clientSession->handleCmd_withinSession(this, "PLAY", urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
                                
                unsigned requestSize = (fLastCRLF+4-fRequestBuffer) + contentLength;
                numBytesRemaining = fRequestBytesAlreadySeen - requestSize;
                resetRequestBuffer(); 
                
                if (numBytesRemaining > 0) {
                    memmove(fRequestBuffer, &fRequestBuffer[requestSize], numBytesRemaining);
                    newBytesRead = numBytesRemaining;
                }
            } while (numBytesRemaining > 0);

            --fRecursionCount;
            if(!fIsActive) {
                if(fRecursionCount > 0)
                    closeSockets();
                else
                    delete this;
            }
        }   
    }
}

@3.2.2.1 确保Request消息完整性

代码用于确保已完整接收Request消息,判断标注为是否能检测到消息结尾标志CRLF CRLF\r\n\r\n。如未检测到,退出循环,继续接收,直到完整为止。

@3.2.2.2 解析RTSP请求

函数parseRTSPRequestString()实现位置在RTSPCommon中,同样以全局函数形式存在。由于3.2.2中处理函数众多,如均一一展开,篇幅将过长过臭。因此这里仅列出其步骤及示例Request,如有兴趣,可自行阅读相关代码。

OPTIONS rtsp://192.168.56.1/bipbop-gear1-all.ts RTSP/1.0
CSeq : 2
User-Agent : LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
  • 跳过request开始处的任何空白字符
  • 读取至下一空白符,所读取到的内容作为命令名称,此处为OPTIONS
  • 跳过前缀为rtsp://或rtsp:/的URL,获取URL指定的文件名,此处为bipbop-gear1-all.ts
  • 查找'CSeq:'头,如存在,获取序号值。此处为2
  • 查找'Session:'头,如有,获取其值。此处空缺
  • 查找'Content-Length:'头,如有,获取其值。此处空缺

补充说明 User-Agent
User-Agent用于标识应用类型、操作系统、软件版本、开发商等信息。例如

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1
Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0

此时如服务器端对不同类型客户端有做更优适配,如针对手机、电脑制作不同的网页布局,就可以更好地提升用户体验。

成功解析RTSP请求后,如其中存在SessionID,则需在哈希表中查找该ID值。如查找成功,进一步确认其状态。

if(requestIncludedSessionId)
{
    clientSession = (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
    if(clientSession != NULL) clientSession->noteLiveness();                
}

lookupClientSession的源码就不放了,纯粹的查找HashTable而已,有兴趣的话可以阅读GenericMediaServer::lookupClientSession()并进一步跟踪。

关于noteLiveness()要稍微说明下,因为其可能引申出一个新的延时任务。

void GenericMediaServer::ClientSession::noteLiveness()
{
    // 使用默认实现,无其他操作,属虚张声势
    if(fOurServerMediaSession != NULL)
        fOurServerMediaSession->noteLiveness();
        
    // fReclamationSeconds>0时开启延时任务livenessTimeoutTask,延时时长为fReclamationSeconds
    if(fOurServer.fReclamationSeconds > 0)
        envir().taskScheduler().rescheduleDelayedTask(fLivenessCheckTask, 
                                fOurServer.fReclamationSeconds*1000000,
                                (TaskFunc*)livenessTimeoutTask, this));
}

fReclamationSeconds是由main函数中DynamicRTSPServer创建时传递参数而来,其值为0。因此此处并不会开启,至于什么时候会开启,只能说,本程序中不会开启。如果开启,且到达指定时长,则会删除clientSession。

@3.2.2.3 处理RTSP请求中方法

如对RTSP请求、回复不太熟悉,可先阅读Live555源码解析(2) - RTSP协议概述
RTSP请求中会存在几种方法,这里列出了所有支持的方法,各方法及相应处理如下:

  • OPTIONS
    如果存在会话ID但并未找到相应clientSession,则转至handleCmd_sessionNotFound(),也就是说回复"454 未找到会话"消息。其他方法也有类似处理,就不再一一说明。
    如无异常,handleCmd_OPTIONS。

    void RTSPServer::RTSPCLientConnection::handleCmd_OPTIONS()
    {
        snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,
        "RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n",
        fCurrentCSeq, dateHeader(), fOurRTSPServer.allowedCommandNames());
    }
    

    标准应答,返回服务器所支持的所有方法名。示例如下:

    RTSP/1.0 200 OK
    CSeq: 2
    Date: Fri, May 26 2017 13:06:44 GMT
    Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
    
  • DESCRIBE
    请求示例:

    DESCRIBE rtsp://192.168.56.1/bipbop-gear1-all.ts RTSP/1.0
    CSeq: 3
    User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
    Accept: application/sdp
    

    handleCmd_DESCRIBE,长话短说:

    • 认证检测,由于默认未启用认证机制,所以肯定认证通过
    • 根据URL中文件名bipbop-gear1-all.ts(如有子目录,则为完整,如/dir/bipbop-gear1-all.ts)查找ServerMediaSession。如找到,增加引用计数,如未找到,handleCmd_notFound()也就是回复"404 未找到流"错误后退出
    • 生成SDP描述信息 generateSDPDescription()
      回复示例:
      RTSP/1.0 200 OK
      CSeq: 3
      Date: Fri, May 26 2017 13:06:44 GMT
      Content-Base: rtsp://192.168.56.1/bipbop-gear1-all.ts/
      Content-Type: application/sdp
      Content-Length: 416
      
      v=0
      o=- 1495855965038741 1 IN IP4 192.168.56.1
      s=MPEG Transport Stream, streamed by the LIVE555 Media Server
      i=bipbop-gear1-all.ts
      t=0 0
      a=tool:LIVE555 Streaming Media v2017.04.10
      a=type:broadcast
      a=control:*
      a=range:npt=0-
      a=x-qt-text-nam:MPEG Transport Stream, streamed by the LIVE555 Media Server
      a=x-qt-text-inf:bipbop-gear1-all.ts
      m=video 0 RTP/AVP 33
      c=IN IP4 0.0.0.0
      b=AS:5000
      a=control:track1
      
  • SETUP
    请求示例:

    SETUP rtsp://192.168.56.1/bipbop-gear1-all.ts/track1 RTSP/1.0
    CSeq: 4
    User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
    Transport: RTP/AVP;unicast;client_port=56136-56137
    

    handleCmd_SETUP,主线如下:

    • 根据URL中文件名bipbop-gear1-all.ts(如有子目录,则为完整,如/dir/bipbop-gear1-all.ts)查找ServerMediaSession。如找到,增加引用计数,如未找到,handleCmd_notFound()也就是回复"404 未找到流"错误后退出。
    • 如指定流已存在,则先停止
    • 根据Transport头内容,确定串流模式及其他参数。如RTP/AVP/TCP对应TCP模式,而RAW/RAW/UDP或/MP2T/H2221/UDP对应UDP模式。本例中使用TCP模式进行传输,且可进一步确认RTP端口为56136,RTCP端口为56137
    • 检查是否带Range或x-playNoew头,以判断是否要在SETUP后立即PLAY
    • getStreamParameters

    主要是生成serverRTPPort、serverRTCPPort及如下重要组件:

    • createNewStreamSource
    • createNewRTPSink
      后两者要着重注意,将是下一篇的入口之一。
      SETUP操作最大的变化是创建了ServerMediaSession,在后续PLAY/PAUSE/TEARDOWN等操作中均会使用到。

    回复示例:

    RTSP/1.0 200 OK
    CSeq: 4
    Date: Fri, May 26 2017 13:06:44 GMT
    Transport:RTP/AVP;unicast;destination=192.168.56.1;source=192.168.56.1;
    client_port=55436-55437;server_port=6970-6971
    Session: 050BAAB9;timeout=65
    
  • PLAY
    请求示例:

    PLAY rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0
    CSeq: 5
    User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
    Session: 050BAAB9
    Range: npt=0.000-
    

    handleCmd_withinSession -> handleCmd_PLAY()思路如下:

    • 检测是否存在Scale头,如存在,更新为指定值,否则为默认值1.0
    • 测试Scale值是否可行
    • 检测是否存在Range头,根据值情况设定duration
    • 播放前设置为指定Scale、Range
    • 开始Streaming,默认操作为调用其startPlaying()。需注意参数handleAlternativeRequestByte,这说明又开启了一项新服务,至于PLAY中发生的具体操作,将单独成篇说明。
    fStreamStates[i].subsession->startStream(fOurSessionId,  
          fStreamStates[i].streamToken, (TaskFunc*)noteClientLiveness, this, 
          rtpSeqNum, rtpTimestamp,  
    RTSPServer::RTSPClientConnection::handleAlternativeRequestByte,                                      ourClientConnection);
    

    回复示例:

    RTSP/1.0 200 OK
    CSeq: 5
    Date: Fri, May 26 2017 13:06:44 GMT
    Range: npt=0.000-
    Session: 050BAAB9
    RTP-Info: url=rtsp://192.168.56.1/bipbop-gear1-all.ts/track1;seq=39939;rtptime=3398276543
    
  • GET_PARAMETER
    请求示例:

    GET_PARAMETER rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0
    CSeq: 6
    User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
    Session: 050BAAB9
    
    0.000-
    

    handleCmd_GET_PARAMETER直接生成回复内容,无其他操作。

    回复示例:

    RTSP/1.0 200 OK
    CSeq: 6
    Date: Fri, May 26 2017 13:06:44 GMT
    Session: 050BAAB9
    Content-Length: 10
    
    2017.04.10
    
  • TEARDOWN
    请求示例:

    TEARDOWN rtsp://192.168.56.1/bipbop-gear1-all.ts/ RTSP/1.0
    CSeq: 7
    User-Agent: LibVLC/2.2.6 (LIVE555 Streaming Media v2016.02.22)
    Session: 050BAAB9
    

    handleCmd_TEARDOWN中释放Socket、删除流等资源,并回复"200"结果。

    回复示例:

    RTSP/1.0 200 OK
    CSeq: 7
    Date: Fri, May 26 2017 13:06:44 GMT
    
    • SET_PARAMETER
      handleCmd_SET_PARAMETER直接生成回复内容,无其他操作。
  • PAUSE
    handleCmd_PAUSE最终调用了RTPSink/UDPSink上的StopPlaying()接口。

4. 总结

综上所述,客户端与服务器进行连接过程实际上就是为RTSP会话交互过程,而其中会进一步产生连锁反应的步骤主要有:

  • SETUP

    • createNewStreamSource
    • createNewRTPSink
  • PLAY

    • handleAlternativeRequestByte

篇幅所限,将根据这些线索展开下一篇。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • RTSP SDP RTP/RTCP 介绍应用层 RTSP、SDP; 传输层 RTP、TCP、UDP; 网络层 IP...
    Atom_Woo阅读 3,801评论 0 7
  • 本文整理了在实践过程中使用的Linux网络工具,这些工具提供的功能非常强大,我们平时使用的只是冰山一角,比如lso...
    老夫刘某阅读 3,496评论 0 7
  • 上一篇Live555源码解析(1) - Main 寻根问祖,留其筋骨将main()函数脉络做了整体分析,通常来讲本...
    SniperPan阅读 4,440评论 0 14
  • host Copyright (c) 2014-2017, racaljk. https://github.com...
    JasonStack阅读 3,082评论 0 4
  • 广播里的文化诗词鉴赏伴着王菲的水调歌头; 推销人员高腔调的唱和着高钙内蒙古奶粉; 旁人哈欠...
    张零呀阅读 280评论 0 2