最近在研究LIve555實現邏輯,發現自己的愚笨至極,很多知識接觸的太少,以後準備多看開源代碼,提高自己,哈哈,看網上大神們一兩天,一兩週就搞定真神人也,本人愚笨,一切慢慢來,下面我來講述一下live555 中RTSP協議的實現,重在描述rtsp的建立過程,協議內容不會過多涉及,知道原理後,後面只是跟着規範和代碼調試就完全ok了。直接上服務器代碼
1. live555MediaServer.cpp 代碼:
int main(int argc, char** argv) {
// Begin by setting up our usage environment:
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);
UserAuthenticationDatabase* authDB = NULL;
// Create the RTSP server. Try first with the default port number (554),
// and then with the alternative port number (8554):
RTSPServer* rtspServer;
portNumBits rtspServerPortNum = 554;
rtspServer = DynamicRTSPServer::createNew(*env, rtspServerPortNum, authDB);
if (rtspServer == NULL) {
rtspServerPortNum = 8554;
rtspServer = DynamicRTSPServer::createNew(*env, rtspServerPortNum, authDB);
}
if (rtspServer == NULL) {
*env << "Failed to create RTSP server: " << env->getResultMsg() << "\n";
exit(1);
}
env->taskScheduler().doEventLoop(); // does not return
return 0; // only to prevent compiler warning
}
上述代碼除去了用戶權限部分和http部分,注重講解rtsp協議,首先熟悉一下live555封裝的類:
TaskScheduler: 調度模塊,實現整體事件的調度,可以把各種socket事件、延遲事件通過此模塊實現,可以理解爲一個事件循環,就像mfc的事件,qt的eventloop等。
UsageEnvironment:封裝的內部環境,調度對象TaskScheduler依賴於此環境,相當於包着調度對象的一個殼,統一進行日誌、錯誤處理。
此處這兩個類不是討論重點,簡單瞭解這兩個類之後就可以進行下一步的代碼講解了。
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);
- 首先創建調度對象,並把對象放入新創建的運行環境中,這屬於live555的基本框架,大部分代碼的運行都基於此調度對象,
portNumBits rtspServerPortNum = 554;
rtspServer = DynamicRTSPServer::createNew(*env, rtspServerPortNum, authDB);
- 接下來創建動態的DynamicRTSPServer對象,當554端口號被佔用時,使用8554端口,創建RTSPServer是此處的關鍵,接下來會重點描述。先把main函數講完:
env->taskScheduler().doEventLoop();
- 最後啓動環境下的調度,進入事件調度,關於調度大家可以理解爲事件循環,此調度不停的在循環詢問是否有調度事件發生,發生了就會觸發對應的函數,實際上也是這樣做的,看一下doeventloop的代碼:
void BasicTaskScheduler0::doEventLoop(char volatile* watchVariable) {
// Repeatedly loop, handling readble sockets and timed events:
while (1) {
if (watchVariable != NULL && *watchVariable != 0) break;
SingleStep();
}
}
2.DynamicRTSPServer
上面講到通過調用CreateNEW來創建了rtsp服務器,接下來通過一幅圖來描述,live555是如何一步步實現這個服務的:
不要望圖生厭,接下來會根據這四個流程詳細描述
【1】int ourSocket = setUpOurSocket(env, ourPort); 創建服務器監聽的socket,端口號就是我們傳入的554,來簡單看一下此函數,此函是在基類GenericMediaServer中實現的一個靜態函數,大家可以跳進去看一下,其實現邏輯簡單說就是:
1)創建服務器socket 2)監聽socket 3)返回socket
【2】 return new DynamicRTSPServer();到這裏才真正在構建rtspserver對象,首先將創建的socket 和 env傳到對象中,其他細節不詳細描述,在層層構造的過程中,其中其父類GenericMediaServer將此socket添加到調度中:
GenericMediaServer
::GenericMediaServer(UsageEnvironment& env, int ourSocket, Port ourPort,
unsigned reclamationSeconds)
: Medium(env),
fServerSocket(ourSocket), fServerPort(ourPort), fReclamationSeconds(reclamationSeconds),
{
//!添加監視socket調度,存在socket信號則調用回調函數(incomingConnectionHandler)
// Arrange to handle connections from others:
env.taskScheduler().turnOnBackgroundReadHandling(fServerSocket, incomingConnectionHandler, this);
}
同樣,我對代碼有一些簡化,此處將上一步創建的socket添加到創建的調度中,並將incomingConnectionHandler設置爲回調函數,加入調度的結果就是當socket收到連接通知後,就自動觸發回調函數,也就是下一步,具體怎麼實現的主要是使用了socket的select函數,不瞭解的可以搜一下,此處不是描述重點。到此服務器對象初始化完成。
【3】客戶端發起socket連接;服務器初始化完成後,調度對象就在不停的查詢是否存在新的連接產生,如果產生就自動調用回調函數,static GenericMediaServer::incomingConnectionHandler(void* objPtr),
如何將回調函數與server對象綁定呢?看代碼
//!socket新連接響應函數--通過靜態函數將對象和函數指針轉化到本對象
void GenericMediaServer::incomingConnectionHandler(void* instance, int /*mask*/) {
GenericMediaServer* server = (GenericMediaServer*)instance;
server->incomingConnectionHandler();
}
在加入調度時,會將this指針傳入調度函數,每次觸發時都將此指針作爲參數傳入靜態回調函數,以靜態函數作爲跳板進行進而綁定server對象非靜態函數,從而可以使用多態等特性,通過上面代碼可以看出來實際上調用的是server ->incomingConnectionHandler()函數,在此函數中會進一步調用函數incomingConnectionHandlerOnSocket(),接下來看此函數代碼:
void GenericMediaServer::incomingConnectionHandlerOnSocket(int serverSocket) {
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;
}
ignoreSigPipeOnSocket(clientSocket); // so that clients on the same host that are killed don't also kill us
makeSocketNonBlocking(clientSocket);
increaseSendBufferTo(envir(), clientSocket, 50*1024);
#ifdef DEBUG
envir() << "accept()ed connection from " << AddressString(clientAddr).val() << "\n";
#endif
//!創建新的連接實例(clientConnetction)
// Create a new object for handling this connection:
(void)createNewClientConnection(clientSocket, clientAddr);
}
由上述代碼可以看出,首先獲取連接的clientsocket和客戶端地址,然後根據clientsocket和客戶端地址,創建clientConnetction對象,在GenericMediaServer中此函數是純虛函數:
virtual ClientConnection* createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) = 0;
也就是其子類需要實現此函數,此處調用的是RTSPServer的函數
GenericMediaServer::ClientConnection*
RTSPServer::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) {
return new RTSPClientConnection(*this, clientSocket, clientAddr);
}
這裏就創建了一個RTSPClientConnection對象,這個類主要負責連接相關的處理,每個連接對應一個對象,接下來咱們看一下其父類GenericMediaServer::ClientConnection的構造函數:
GenericMediaServer::ClientConnection
::ClientConnection(GenericMediaServer& ourServer, int clientSocket, struct sockaddr_in clientAddr)
: fOurServer(ourServer), fOurSocket(clientSocket), fClientAddr(clientAddr) {
// Add ourself to our 'client connections' table:
fOurServer.fClientConnections->Add((char const*)this, this);
// Arrange to handle incoming requests:
resetRequestBuffer();
envir().taskScheduler()
.setBackgroundHandling(fOurSocket, SOCKET_READABLE|SOCKET_EXCEPTION, incomingRequestHandler, this);
}
可以看到在構造函數中,將客戶端socket添加到調度中,並設置當出現異常或可讀時調用回調函數,這裏與第【2】步類似,不在贅述。
【3】客戶端發起請求。客戶端與服務器建立的socket加入到調度後,就可以監視到客戶端發送的數據了,通過上面描述應該很明顯知道,回調函數是在創建的connection對象中,首先通過靜態函數跳轉到connection對象的incomingRequestHandler()函數,然後讀取數據後,調用虛函數handleRequestBytes(bytesRead)。
此函數是在子類中實現的,此處是在RTSPServer::RTSPClientConnection 中實現,在此函數中會根據rtsp協議對消息進行解析和特定的處理,代碼如下
void RTSPServer::RTSPClientConnection::handleRequestBytes(int newBytesRead) {
int numBytesRemaining = 0;
++fRecursionCount;
do {
RTSPServer::RTSPClientSession* clientSession = NULL;
if (newBytesRead < 0 || (unsigned)newBytesRead >= fRequestBufferBytesLeft) {
// Either the client socket has died, or the request was too big for us.
// Terminate this connection:
#ifdef DEBUG
fprintf(stderr, "RTSPClientConnection[%p]::handleRequestBytes() read %d new bytes (of %d); terminating connection!\n", this, newBytesRead, fRequestBufferBytesLeft);
#endif
fIsActive = False;
break;
}
Boolean endOfMsg = False;
unsigned char* ptr = &fRequestBuffer[fRequestBytesAlreadySeen];
#ifdef DEBUG
ptr[newBytesRead] = '\0';
fprintf(stderr, "RTSPClientConnection[%p]::handleRequestBytes() %s %d new bytes:%s\n",
this, numBytesRemaining > 0 ? "processing" : "read", newBytesRead, ptr);
#endif
if (fClientOutputSocket != fClientInputSocket && numBytesRemaining == 0) {
// We're doing RTSP-over-HTTP tunneling, and input commands are assumed to have been Base64-encoded.
// We therefore Base64-decode as much of this new data as we can (i.e., up to a multiple of 4 bytes).
// But first, we remove any whitespace that may be in the input data:
unsigned toIndex = 0;
for (int fromIndex = 0; fromIndex < newBytesRead; ++fromIndex) {
char c = ptr[fromIndex];
if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) { // not 'whitespace': space,tab,CR,NL
ptr[toIndex++] = c;
}
}
newBytesRead = toIndex;
unsigned numBytesToDecode = fBase64RemainderCount + newBytesRead;
unsigned newBase64RemainderCount = numBytesToDecode%4;
numBytesToDecode -= newBase64RemainderCount;
if (numBytesToDecode > 0) {
ptr[newBytesRead] = '\0';
unsigned decodedSize;
unsigned char* decodedBytes = base64Decode((char const*)(ptr-fBase64RemainderCount), numBytesToDecode, decodedSize);
#ifdef DEBUG
fprintf(stderr, "Base64-decoded %d input bytes into %d new bytes:", numBytesToDecode, decodedSize);
for (unsigned k = 0; k < decodedSize; ++k) fprintf(stderr, "%c", decodedBytes[k]);
fprintf(stderr, "\n");
#endif
// Copy the new decoded bytes in place of the old ones (we can do this because there are fewer decoded bytes than original):
unsigned char* to = ptr-fBase64RemainderCount;
for (unsigned i = 0; i < decodedSize; ++i) *to++ = decodedBytes[i];
// Then copy any remaining (undecoded) bytes to the end:
for (unsigned j = 0; j < newBase64RemainderCount; ++j) *to++ = (ptr-fBase64RemainderCount+numBytesToDecode)[j];
newBytesRead = decodedSize - fBase64RemainderCount + newBase64RemainderCount;
// adjust to allow for the size of the new decoded data (+ remainder)
delete[] decodedBytes;
}
fBase64RemainderCount = newBase64RemainderCount;
}
unsigned char* tmpPtr = fLastCRLF + 2;
if (fBase64RemainderCount == 0) { // no more Base-64 bytes remain to be read/decoded
// Look for the end of the message: <CR><LF><CR><LF>
if (tmpPtr < fRequestBuffer) tmpPtr = fRequestBuffer;
while (tmpPtr < &ptr[newBytesRead-1]) {
if (*tmpPtr == '\r' && *(tmpPtr+1) == '\n') {
if (tmpPtr - fLastCRLF == 2) { // This is it:
endOfMsg = True;
break;
}
fLastCRLF = tmpPtr;
}
++tmpPtr;
}
}
fRequestBufferBytesLeft -= newBytesRead;
fRequestBytesAlreadySeen += newBytesRead;
if (!endOfMsg) break; // subsequent reads will be needed to complete the request
// Parse the request string into command name and 'CSeq', then handle the command:
fRequestBuffer[fRequestBytesAlreadySeen] = '\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;
Boolean playAfterSetup = False;
fLastCRLF[2] = '\0'; // temporarily, for parsing
Boolean parseSucceeded = parseRTSPRequestString((char*)fRequestBuffer, fLastCRLF+2 - fRequestBuffer,
cmdName, sizeof cmdName,
urlPreSuffix, sizeof urlPreSuffix,
urlSuffix, sizeof urlSuffix,
cseq, sizeof cseq,
sessionIdStr, sizeof sessionIdStr,
contentLength);
fLastCRLF[2] = '\r'; // restore its value
// Check first for a bogus "Content-Length" value that would cause a pointer wraparound:
if (tmpPtr + 2 + contentLength < tmpPtr + 2) {
#ifdef DEBUG
fprintf(stderr, "parseRTSPRequestString() returned a bogus \"Content-Length:\" value: 0x%x (%d)\n", contentLength, (int)contentLength);
#endif
contentLength = 0;
parseSucceeded = False;
}
if (parseSucceeded) {
#ifdef DEBUG
fprintf(stderr, "parseRTSPRequestString() succeeded, returning cmdName \"%s\", urlPreSuffix \"%s\", urlSuffix \"%s\", CSeq \"%s\", Content-Length %u, with %d bytes following the message.\n", cmdName, urlPreSuffix, urlSuffix, cseq, contentLength, ptr + newBytesRead - (tmpPtr + 2));
#endif
// If there was a "Content-Length:" header, then make sure we've received all of the data that it specified:
if (ptr + newBytesRead < tmpPtr + 2 + contentLength) break; // we still need more data; subsequent reads will give it to us
// If the request included a "Session:" id, and it refers to a client session that's
// current ongoing, then use this command to indicate 'liveness' on that client session:
Boolean const requestIncludedSessionId = sessionIdStr[0] != '\0';
if (requestIncludedSessionId) {
clientSession
= (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
if (clientSession != NULL) clientSession->noteLiveness();
}
// We now have a complete RTSP request.
// Handle the specified command (beginning with commands that are session-independent):
fCurrentCSeq = cseq;
if (strcmp(cmdName, "OPTIONS") == 0) {
// If the "OPTIONS" command included a "Session:" id for a session that doesn't exist,
// then treat this as an error:
if (requestIncludedSessionId && clientSession == NULL) {
#ifdef DEBUG
fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 1)\n");
#endif
handleCmd_sessionNotFound();
} else {
// Normal case:
handleCmd_OPTIONS();
}
} else if (urlPreSuffix[0] == '\0' && urlSuffix[0] == '*' && urlSuffix[1] == '\0') {
// The special "*" URL means: an operation on the entire server. This works only for GET_PARAMETER and SET_PARAMETER:
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) {
// No session id was present in the request.
// So create a new "RTSPClientSession" object for this request.
// But first, make sure that we're authenticated to perform this command:
char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX];
// enough space for urlPreSuffix/urlSuffix'\0'
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_SETUP(this, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
playAfterSetup = clientSession->fStreamAfterSETUP;
} else if (areAuthenticated) {
#ifdef DEBUG
fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 2)\n");
#endif
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 {
#ifdef DEBUG
fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 3)\n");
#endif
handleCmd_sessionNotFound();
}
} else if (strcmp(cmdName, "REGISTER") == 0 || strcmp(cmdName, "DEREGISTER") == 0) {
// Because - unlike other commands - an implementation of this command needs
// the entire URL, we re-parse the command to get it:
char* url = strDupSize((char*)fRequestBuffer);
if (sscanf((char*)fRequestBuffer, "%*s %s", url) == 1) {
// Check for special command-specific parameters in a "Transport:" header:
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 {
// The command is one that we don't handle:
handleCmd_notSupported();
}
} else {
#ifdef DEBUG
fprintf(stderr, "parseRTSPRequestString() failed; checking now for HTTP commands (for RTSP-over-HTTP tunneling)...\n");
#endif
// The request was not (valid) RTSP, but check for a special case: HTTP commands (for setting up RTSP-over-HTTP tunneling):
char sessionCookie[RTSP_PARAM_STRING_MAX];
char acceptStr[RTSP_PARAM_STRING_MAX];
*fLastCRLF = '\0'; // temporarily, for parsing
parseSucceeded = parseHTTPRequestString(cmdName, sizeof cmdName,
urlSuffix, sizeof urlPreSuffix,
sessionCookie, sizeof sessionCookie,
acceptStr, sizeof acceptStr);
*fLastCRLF = '\r';
if (parseSucceeded) {
#ifdef DEBUG
fprintf(stderr, "parseHTTPRequestString() succeeded, returning cmdName \"%s\", urlSuffix \"%s\", sessionCookie \"%s\", acceptStr \"%s\"\n", cmdName, urlSuffix, sessionCookie, acceptStr);
#endif
// 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') {
// There was no "x-sessioncookie:" header. If there was an "Accept: application/x-rtsp-tunnelled" header,
// then this is a bad tunneling request. Otherwise, assume that it's an attempt to access the stream via HTTP.
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) {
// We might have received additional data following the HTTP "POST" command - i.e., the first Base64-encoded RTSP command.
// Check for this, and handle it if it exists:
unsigned char const* extraData = fLastCRLF+4;
unsigned extraDataSize = &fRequestBuffer[fRequestBytesAlreadySeen] - extraData;
if (handleHTTPCmd_TunnelingPOST(sessionCookie, extraData, extraDataSize)) {
// We don't respond to the "POST" command, and we go away:
fIsActive = False;
break;
}
} else {
isValidHTTPCmd = False;
}
if (!isValidHTTPCmd) {
handleHTTPCmd_notSupported();
}
} else {
#ifdef DEBUG
fprintf(stderr, "parseHTTPRequestString() failed!\n");
#endif
handleCmd_bad();
}
}
#ifdef DEBUG
fprintf(stderr, "sending response: %s", fResponseBuffer);
#endif
send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0);
if (playAfterSetup) {
// The client has asked for streaming to commence now, rather than after a
// subsequent "PLAY" command. So, simulate the effect of a "PLAY" command:
clientSession->handleCmd_withinSession(this, "PLAY", urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
}
// Check whether there are extra bytes remaining in the buffer, after the end of the request (a rare case).
// If so, move them to the front of our buffer, and keep processing it, because it might be a following, pipelined request.
unsigned requestSize = (fLastCRLF+4-fRequestBuffer) + contentLength;
numBytesRemaining = fRequestBytesAlreadySeen - requestSize;
resetRequestBuffer(); // to prepare for any subsequent request
if (numBytesRemaining > 0) {
memmove(fRequestBuffer, &fRequestBuffer[requestSize], numBytesRemaining);
newBytesRead = numBytesRemaining;
}
} while (numBytesRemaining > 0);
--fRecursionCount;
if (!fIsActive) {
if (fRecursionCount > 0) closeSockets(); else delete this;
// Note: The "fRecursionCount" test is for a pathological situation where we reenter the event loop and get called recursively
// while handling a command (e.g., while handling a "DESCRIBE", to get a SDP description).
// In such a case we don't want to actually delete ourself until we leave the outermost call.
}
}
此函數就是rtsp協議的解析函數,根據不同的命令響應不同的處理函數,處理完成後發送應答消息。
到此,rtsp的建立和響應和應答就講完了,如有不妥之處,歡迎大家拍磚,有時間我會繼續寫媒體的創建過程。