Error:headers already sent ->output buffer

本文详细解释了PHP中Header的工作原理及如何避免在输出任何内容前使用Header所导致的问题。介绍了两种解决方案:一是通过配置php.ini文件启用output_buffering;二是使用ob_start()开启输出缓存,配合ob_end_flush()来确保在输出内容前可以正确设置Header。

早就听说," header()之前不能有任何输出"(html文本、echo 、include其它编码格式的文件)


原理:

对于使用HTTP 协议建立的每个请求,Web服务器产生的响应通常包括两个部分 – 标题和主体。

脚本开始执行时,它可以同时发送header(标题)信息和主体信息。 Header信息(来自 header() 或 SetCookie() 函数)并不会立即发送,相反,它被保存到一个列表中。 这样就可以允许你修改标题信息,包括缺省的标题(例如 Content-Type 标题)。但是,一旦脚本发送了任何非标题的输出(例如,使用 HTML 代码块或 print() 调用),那么PHP就必须先发送所有的标题,然后再送出空行,终止 HTTP header,而在此之后才会继续发送主体数据。从这时开始,任何添加或修改标题信息的试图都是不允许的,并会发送上述的错误消息之一。


解决办法:

1、php.ini配置文件里面output_buffering=on

启用output buffering时,在脚本发送输出时,PHP并不发送HTTP header。相反,它将此输出通过管道(pipe)输入到动态增加的缓存中(只能在PHP 4.0中使用,它具有中央化的输出机制)。你仍然可以修改,添加标题行,或者设置cookie,因为标题实际上并没有发送。最简单的情况是,当脚本终止时,PHP将自动发送HTTP header到浏览器,然后再发送输出缓冲中的内容。


2、在整个页面之前,也就是HTTP信息之后,把<?php ob_start(); ?>打开缓存,在脚本结束的时候再用ob_end_flush()关闭缓存。

<?php

ob_start();
echo "Hello\n";

setcookie("cookiename", "cookiedata");

ob_end_flush();

?>
In the above example, the output from echo() would be stored in the output buffer until ob_end_flush() was called. In the mean time, the call to setcookie() successfully stored a cookie without causing an error. (You can not normally send headers to the browser after data has already been sent.)

详情见手册:http://php.net/manual/en/book.outcontrol.php【output buffer部分】


这是arduinohtpclient库中的HttpClient.cpp的内容// Class to simplify HTTP fetching on Arduino // (c) Copyright 2010-2011 MCQN Ltd // Released under Apache License, version 2.0 #include "HttpClient.h" #include "b64.h" // Initialize constants const char* HttpClient::kUserAgent = "Arduino/2.2.0"; const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": "; const char* HttpClient::kTransferEncodingChunked = HTTP_HEADER_TRANSFER_ENCODING ": " HTTP_HEADER_VALUE_CHUNKED; HttpClient::HttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort) : iClient(&aClient), iServerName(aServerName), iServerAddress(), iServerPort(aServerPort), iConnectionClose(true), iSendDefaultRequestHeaders(true) { resetState(); } HttpClient::HttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort) : HttpClient(aClient, aServerName.c_str(), aServerPort) { } HttpClient::HttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort) : iClient(&aClient), iServerName(NULL), iServerAddress(aServerAddress), iServerPort(aServerPort), iConnectionClose(true), iSendDefaultRequestHeaders(true) { resetState(); } void HttpClient::resetState() { iState = eIdle; iStatusCode = 0; iContentLength = kNoContentLengthHeader; iBodyLengthConsumed = 0; iContentLengthPtr = kContentLengthPrefix; iTransferEncodingChunkedPtr = kTransferEncodingChunked; iIsChunked = false; iChunkLength = 0; iHttpResponseTimeout = kHttpResponseTimeout; iHttpWaitForDataDelay = kHttpWaitForDataDelay; } void HttpClient::stop() { iClient->stop(); resetState(); } void HttpClient::connectionKeepAlive() { iConnectionClose = false; } void HttpClient::noDefaultRequestHeaders() { iSendDefaultRequestHeaders = false; } void HttpClient::beginRequest() { iState = eRequestStarted; } int HttpClient::startRequest(const char* aURLPath, const char* aHttpMethod, const char* aContentType, int aContentLength, const byte aBody[]) { if (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk) { flushClientRx(); resetState(); } tHttpState initialState = iState; if ((eIdle != iState) && (eRequestStarted != iState)) { return HTTP_ERROR_API; } if (iConnectionClose || !iClient->connected()) { if (iServerName) { if (!(iClient->connect(iServerName, iServerPort) > 0)) { #ifdef LOGGING Serial.println("Connection failed"); #endif return HTTP_ERROR_CONNECTION_FAILED; } } else { if (!(iClient->connect(iServerAddress, iServerPort) > 0)) { #ifdef LOGGING Serial.println("Connection failed"); #endif return HTTP_ERROR_CONNECTION_FAILED; } } } else { #ifdef LOGGING Serial.println("Connection already open"); #endif } // Now we're connected, send the first part of the request int ret = sendInitialHeaders(aURLPath, aHttpMethod); if (HTTP_SUCCESS == ret) { if (aContentType) { sendHeader(HTTP_HEADER_CONTENT_TYPE, aContentType); } if (aContentLength > 0) { sendHeader(HTTP_HEADER_CONTENT_LENGTH, aContentLength); } bool hasBody = (aBody && aContentLength > 0); if (initialState == eIdle || hasBody) { // This was a simple version of the API, so terminate the headers now finishHeaders(); } // else we'll call it in endRequest or in the first call to print, etc. if (hasBody) { write(aBody, aContentLength); } } return ret; } int HttpClient::sendInitialHeaders(const char* aURLPath, const char* aHttpMethod) { #ifdef LOGGING Serial.println("Connected"); #endif // Send the HTTP command, i.e. "GET /somepath/ HTTP/1.0" iClient->print(aHttpMethod); iClient->print(" "); iClient->print(aURLPath); iClient->println(" HTTP/1.1"); if (iSendDefaultRequestHeaders) { // The host header, if required if (iServerName) { iClient->print("Host: "); iClient->print(iServerName); if (iServerPort != kHttpPort && iServerPort != kHttpsPort) { iClient->print(":"); iClient->print(iServerPort); } iClient->println(); } // And user-agent string sendHeader(HTTP_HEADER_USER_AGENT, kUserAgent); } if (iConnectionClose) { // Tell the server to // close this connection after we're done sendHeader(HTTP_HEADER_CONNECTION, "close"); } // Everything has gone well iState = eRequestStarted; return HTTP_SUCCESS; } void HttpClient::sendHeader(const char* aHeader) { iClient->println(aHeader); } void HttpClient::sendHeader(const char* aHeaderName, const char* aHeaderValue) { iClient->print(aHeaderName); iClient->print(": "); iClient->println(aHeaderValue); } void HttpClient::sendHeader(const char* aHeaderName, const int aHeaderValue) { iClient->print(aHeaderName); iClient->print(": "); iClient->println(aHeaderValue); } void HttpClient::sendBasicAuth(const char* aUser, const char* aPassword) { // Send the initial part of this header line iClient->print("Authorization: Basic "); // Now Base64 encode "aUser:aPassword" and send that // This seems trickier than it should be but it's mostly to avoid either // (a) some arbitrarily sized buffer which hopes to be big enough, or // (b) allocating and freeing memory // ...so we'll loop through 3 bytes at a time, outputting the results as we // go. // In Base64, each 3 bytes of unencoded data become 4 bytes of encoded data unsigned char input[3]; unsigned char output[5]; // Leave space for a '\0' terminator so we can easily print int userLen = strlen(aUser); int passwordLen = strlen(aPassword); int inputOffset = 0; for (int i = 0; i < (userLen+1+passwordLen); i++) { // Copy the relevant input byte into the input if (i < userLen) { input[inputOffset++] = aUser[i]; } else if (i == userLen) { input[inputOffset++] = ':'; } else { input[inputOffset++] = aPassword[i-(userLen+1)]; } // See if we've got a chunk to encode if ( (inputOffset == 3) || (i == userLen+passwordLen) ) { // We've either got to a 3-byte boundary, or we've reached then end b64_encode(input, inputOffset, output, 4); // NUL-terminate the output string output[4] = '\0'; // And write it out iClient->print((char*)output); // FIXME We might want to fill output with '=' characters if b64_encode doesn't // FIXME do it for us when we're encoding the final chunk inputOffset = 0; } } // And end the header we've sent iClient->println(); } void HttpClient::finishHeaders() { iClient->println(); iState = eRequestSent; } void HttpClient::flushClientRx() { while (iClient->available()) { iClient->read(); } } void HttpClient::endRequest() { beginBody(); } void HttpClient::beginBody() { if (iState < eRequestSent) { // We still need to finish off the headers finishHeaders(); } // else the end of headers has already been sent, so nothing to do here } int HttpClient::get(const char* aURLPath) { return startRequest(aURLPath, HTTP_METHOD_GET); } int HttpClient::get(const String& aURLPath) { return get(aURLPath.c_str()); } int HttpClient::post(const char* aURLPath) { return startRequest(aURLPath, HTTP_METHOD_POST); } int HttpClient::post(const String& aURLPath) { return post(aURLPath.c_str()); } int HttpClient::post(const char* aURLPath, const char* aContentType, const char* aBody) { return post(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); } int HttpClient::post(const String& aURLPath, const String& aContentType, const String& aBody) { return post(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); } int HttpClient::post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) { return startRequest(aURLPath, HTTP_METHOD_POST, aContentType, aContentLength, aBody); } int HttpClient::put(const char* aURLPath) { return startRequest(aURLPath, HTTP_METHOD_PUT); } int HttpClient::put(const String& aURLPath) { return put(aURLPath.c_str()); } int HttpClient::put(const char* aURLPath, const char* aContentType, const char* aBody) { return put(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); } int HttpClient::put(const String& aURLPath, const String& aContentType, const String& aBody) { return put(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); } int HttpClient::put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) { return startRequest(aURLPath, HTTP_METHOD_PUT, aContentType, aContentLength, aBody); } int HttpClient::patch(const char* aURLPath) { return startRequest(aURLPath, HTTP_METHOD_PATCH); } int HttpClient::patch(const String& aURLPath) { return patch(aURLPath.c_str()); } int HttpClient::patch(const char* aURLPath, const char* aContentType, const char* aBody) { return patch(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); } int HttpClient::patch(const String& aURLPath, const String& aContentType, const String& aBody) { return patch(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); } int HttpClient::patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) { return startRequest(aURLPath, HTTP_METHOD_PATCH, aContentType, aContentLength, aBody); } int HttpClient::del(const char* aURLPath) { return startRequest(aURLPath, HTTP_METHOD_DELETE); } int HttpClient::del(const String& aURLPath) { return del(aURLPath.c_str()); } int HttpClient::del(const char* aURLPath, const char* aContentType, const char* aBody) { return del(aURLPath, aContentType, strlen(aBody), (const byte*)aBody); } int HttpClient::del(const String& aURLPath, const String& aContentType, const String& aBody) { return del(aURLPath.c_str(), aContentType.c_str(), aBody.length(), (const byte*)aBody.c_str()); } int HttpClient::del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]) { return startRequest(aURLPath, HTTP_METHOD_DELETE, aContentType, aContentLength, aBody); } int HttpClient::responseStatusCode() { if (iState < eRequestSent) { return HTTP_ERROR_API; } // The first line will be of the form Status-Line: // HTTP-Version SP Status-Code SP Reason-Phrase CRLF // Where HTTP-Version is of the form: // HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT int c = '\0'; do { // Make sure the status code is reset, and likewise the state. This // lets us easily cope with 1xx informational responses by just // ignoring them really, and reading the next line for a proper response iStatusCode = 0; iState = eRequestSent; unsigned long timeoutStart = millis(); // Psuedo-regexp we're expecting before the status-code const char* statusPrefix = "HTTP/*.* "; const char* statusPtr = statusPrefix; // Whilst we haven't timed out & haven't reached the end of the headers while ((c != '\n') && ( (millis() - timeoutStart) < iHttpResponseTimeout )) { if (available()) { c = HttpClient::read(); if (c != -1) { switch(iState) { case eRequestSent: // We haven't reached the status code yet if ( (*statusPtr == '*') || (*statusPtr == c) ) { // This character matches, just move along statusPtr++; if (*statusPtr == '\0') { // We've reached the end of the prefix iState = eReadingStatusCode; } } else { return HTTP_ERROR_INVALID_RESPONSE; } break; case eReadingStatusCode: if (isdigit(c)) { // This assumes we won't get more than the 3 digits we // want iStatusCode = iStatusCode*10 + (c - '0'); } else { // We've reached the end of the status code // We could sanity check it here or double-check for ' ' // rather than anything else, but let's be lenient iState = eStatusCodeRead; } break; case eStatusCodeRead: // We're just waiting for the end of the line now break; default: break; }; // We read something, reset the timeout counter timeoutStart = millis(); } } else { // We haven't got any data, so let's pause to allow some to // arrive delay(iHttpWaitForDataDelay); } } if ( (c == '\n') && (iStatusCode < 200 && iStatusCode != 101) ) { // We've reached the end of an informational status line c = '\0'; // Clear c so we'll go back into the data reading loop } } // If we've read a status code successfully but it's informational (1xx) // loop back to the start while ( (iState == eStatusCodeRead) && (iStatusCode < 200 && iStatusCode != 101) ); if ( (c == '\n') && (iState == eStatusCodeRead) ) { // We've read the status-line successfully return iStatusCode; } else if (c != '\n') { // We must've timed out before we reached the end of the line return HTTP_ERROR_TIMED_OUT; } else { // This wasn't a properly formed status line, or at least not one we // could understand return HTTP_ERROR_INVALID_RESPONSE; } } int HttpClient::skipResponseHeaders() { // Just keep reading until we finish reading the headers or time out unsigned long timeoutStart = millis(); // Whilst we haven't timed out & haven't reached the end of the headers while ((!endOfHeadersReached()) && ( (millis() - timeoutStart) < iHttpResponseTimeout )) { if (available()) { (void)readHeader(); // We read something, reset the timeout counter timeoutStart = millis(); } else { // We haven't got any data, so let's pause to allow some to // arrive delay(iHttpWaitForDataDelay); } } if (endOfHeadersReached()) { // Success return HTTP_SUCCESS; } else { // We must've timed out return HTTP_ERROR_TIMED_OUT; } } bool HttpClient::endOfHeadersReached() { return (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk); }; long HttpClient::contentLength() { // skip the response headers, if they haven't been read already if (!endOfHeadersReached()) { skipResponseHeaders(); } return iContentLength; } String HttpClient::responseBody() { int bodyLength = contentLength(); String response; if (bodyLength > 0) { // try to reserve bodyLength bytes if (response.reserve(bodyLength) == 0) { // String reserve failed return String((const char*)NULL); } } // keep on timedRead'ing, until: // - we have a content length: body length equals consumed or no bytes // available // - no content length: no bytes are available while (iBodyLengthConsumed != bodyLength) { int c = timedRead(); if (c == -1) { // read timed out, done break; } if (!response.concat((char)c)) { // adding char failed return String((const char*)NULL); } } if (bodyLength > 0 && (unsigned int)bodyLength != response.length()) { // failure, we did not read in response content length bytes return String((const char*)NULL); } return response; } bool HttpClient::endOfBodyReached() { if (endOfHeadersReached() && (contentLength() != kNoContentLengthHeader)) { // We've got to the body and we know how long it will be return (iBodyLengthConsumed >= contentLength()); } return false; } int HttpClient::available() { if (iState == eReadingChunkLength) { while (iClient->available()) { char c = iClient->read(); if (c == '\n') { iState = eReadingBodyChunk; break; } else if (c == '\r') { // no-op } else if (isHexadecimalDigit(c)) { char digit[2] = {c, '\0'}; iChunkLength = (iChunkLength * 16) + strtol(digit, NULL, 16); } } } if (iState == eReadingBodyChunk && iChunkLength == 0) { iState = eReadingChunkLength; } if (iState == eReadingChunkLength) { return 0; } int clientAvailable = iClient->available(); if (iState == eReadingBodyChunk) { return min(clientAvailable, iChunkLength); } else { return clientAvailable; } } int HttpClient::read() { if (iIsChunked && !available()) { return -1; } int ret = iClient->read(); if (ret >= 0) { if (endOfHeadersReached() && iContentLength > 0) { // We're outputting the body now and we've seen a Content-Length header // So keep track of how many bytes are left iBodyLengthConsumed++; } if (iState == eReadingBodyChunk) { iChunkLength--; if (iChunkLength == 0) { iState = eReadingChunkLength; } } } return ret; } bool HttpClient::headerAvailable() { // clear the currently stored header line iHeaderLine = ""; while (!endOfHeadersReached()) { // read a byte from the header int c = readHeader(); if (c == '\r' || c == '\n') { if (iHeaderLine.length()) { // end of the line, all done break; } else { // ignore any CR or LF characters continue; } } // append byte to header line iHeaderLine += (char)c; } return (iHeaderLine.length() > 0); } String HttpClient::readHeaderName() { int colonIndex = iHeaderLine.indexOf(':'); if (colonIndex == -1) { return ""; } return iHeaderLine.substring(0, colonIndex); } String HttpClient::readHeaderValue() { int colonIndex = iHeaderLine.indexOf(':'); int startIndex = colonIndex + 1; if (colonIndex == -1) { return ""; } // trim any leading whitespace while (startIndex < (int)iHeaderLine.length() && isSpace(iHeaderLine[startIndex])) { startIndex++; } return iHeaderLine.substring(startIndex); } int HttpClient::read(uint8_t *buf, size_t size) { int ret =iClient->read(buf, size); if (endOfHeadersReached() && iContentLength > 0) { // We're outputting the body now and we've seen a Content-Length header // So keep track of how many bytes are left if (ret >= 0) { iBodyLengthConsumed += ret; } } return ret; } int HttpClient::readHeader() { char c = HttpClient::read(); if (endOfHeadersReached()) { // We've passed the headers, but rather than return an error, we'll just // act as a slightly less efficient version of read() return c; } // Whilst reading out the headers to whoever wants them, we'll keep an // eye out for the "Content-Length" header switch(iState) { case eStatusCodeRead: // We're at the start of a line, or somewhere in the middle of reading // the Content-Length prefix if (*iContentLengthPtr == c) { // This character matches, just move along iContentLengthPtr++; if (*iContentLengthPtr == '\0') { // We've reached the end of the prefix iState = eReadingContentLength; // Just in case we get multiple Content-Length headers, this // will ensure we just get the value of the last one iContentLength = 0; iBodyLengthConsumed = 0; } } else if (*iTransferEncodingChunkedPtr == c) { // This character matches, just move along iTransferEncodingChunkedPtr++; if (*iTransferEncodingChunkedPtr == '\0') { // We've reached the end of the Transfer Encoding: chunked header iIsChunked = true; iState = eSkipToEndOfHeader; } } else if (((iContentLengthPtr == kContentLengthPrefix) && (iTransferEncodingChunkedPtr == kTransferEncodingChunked)) && (c == '\r')) { // We've found a '\r' at the start of a line, so this is probably // the end of the headers iState = eLineStartingCRFound; } else { // This isn't the Content-Length or Transfer Encoding chunked header, skip to the end of the line iState = eSkipToEndOfHeader; } break; case eReadingContentLength: if (isdigit(c)) { long _iContentLength = iContentLength*10 + (c - '0'); // Only apply if the value didn't wrap around if (_iContentLength > iContentLength) { iContentLength = _iContentLength; } } else { // We've reached the end of the content length // We could sanity check it here or double-check for "\r\n" // rather than anything else, but let's be lenient iState = eSkipToEndOfHeader; } break; case eLineStartingCRFound: if (c == '\n') { if (iIsChunked) { iState = eReadingChunkLength; iChunkLength = 0; } else { iState = eReadingBody; } } break; default: // We're just waiting for the end of the line now break; }; if ( (c == '\n') && !endOfHeadersReached() ) { // We've got to the end of this line, start processing again iState = eStatusCodeRead; iContentLengthPtr = kContentLengthPrefix; iTransferEncodingChunkedPtr = kTransferEncodingChunked; } // And return the character read to whoever wants it return c; } 这是HttpClient.h头文件的内容// Class to simplify HTTP fetching on Arduino // (c) Copyright MCQN Ltd. 2010-2012 // Released under Apache License, version 2.0 #ifndef HttpClient_h #define HttpClient_h #include <Arduino.h> #include <IPAddress.h> #include "Client.h" static const int HTTP_SUCCESS =0; // The end of the headers has been reached. This consumes the '\n' // Could not connect to the server static const int HTTP_ERROR_CONNECTION_FAILED =-1; // This call was made when the HttpClient class wasn't expecting it // to be called. Usually indicates your code is using the class // incorrectly static const int HTTP_ERROR_API =-2; // Spent too long waiting for a reply static const int HTTP_ERROR_TIMED_OUT =-3; // The response from the server is invalid, is it definitely an HTTP // server? static const int HTTP_ERROR_INVALID_RESPONSE =-4; // Define some of the common methods and headers here // That lets other code reuse them without having to declare another copy // of them, so saves code space and RAM #define HTTP_METHOD_GET "GET" #define HTTP_METHOD_POST "POST" #define HTTP_METHOD_PUT "PUT" #define HTTP_METHOD_PATCH "PATCH" #define HTTP_METHOD_DELETE "DELETE" #define HTTP_HEADER_CONTENT_LENGTH "Content-Length" #define HTTP_HEADER_CONTENT_TYPE "Content-Type" #define HTTP_HEADER_CONNECTION "Connection" #define HTTP_HEADER_TRANSFER_ENCODING "Transfer-Encoding" #define HTTP_HEADER_USER_AGENT "User-Agent" #define HTTP_HEADER_VALUE_CHUNKED "chunked" class HttpClient : public Client { public: static const int kNoContentLengthHeader =-1; static const int kHttpPort =80; static const int kHttpsPort =443; static const char* kUserAgent; // FIXME Write longer API request, using port and user-agent, example // FIXME Update tempToPachube example to calculate Content-Length correctly HttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort = kHttpPort); HttpClient(Client& aClient, const String& aServerName, uint16_t aServerPort = kHttpPort); HttpClient(Client& aClient, const IPAddress& aServerAddress, uint16_t aServerPort = kHttpPort); /** Start a more complex request. Use this when you need to send additional headers in the request, but you will also need to call endRequest() when you are finished. */ void beginRequest(); /** End a more complex request. Use this when you need to have sent additional headers in the request, but you will also need to call beginRequest() at the start. */ void endRequest(); /** Start the body of a more complex request. Use this when you need to send the body after additional headers in the request, but can optionally call endRequest() when you are finished. */ void beginBody(); /** Connect to the server and start to send a GET request. @param aURLPath Url to request @return 0 if successful, else error */ int get(const char* aURLPath); int get(const String& aURLPath); /** Connect to the server and start to send a POST request. @param aURLPath Url to request @return 0 if successful, else error */ int post(const char* aURLPath); int post(const String& aURLPath); /** Connect to the server and send a POST request with body and content type @param aURLPath Url to request @param aContentType Content type of request body @param aBody Body of the request @return 0 if successful, else error */ int post(const char* aURLPath, const char* aContentType, const char* aBody); int post(const String& aURLPath, const String& aContentType, const String& aBody); int post(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); /** Connect to the server and start to send a PUT request. @param aURLPath Url to request @return 0 if successful, else error */ int put(const char* aURLPath); int put(const String& aURLPath); /** Connect to the server and send a PUT request with body and content type @param aURLPath Url to request @param aContentType Content type of request body @param aBody Body of the request @return 0 if successful, else error */ int put(const char* aURLPath, const char* aContentType, const char* aBody); int put(const String& aURLPath, const String& aContentType, const String& aBody); int put(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); /** Connect to the server and start to send a PATCH request. @param aURLPath Url to request @return 0 if successful, else error */ int patch(const char* aURLPath); int patch(const String& aURLPath); /** Connect to the server and send a PATCH request with body and content type @param aURLPath Url to request @param aContentType Content type of request body @param aBody Body of the request @return 0 if successful, else error */ int patch(const char* aURLPath, const char* aContentType, const char* aBody); int patch(const String& aURLPath, const String& aContentType, const String& aBody); int patch(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); /** Connect to the server and start to send a DELETE request. @param aURLPath Url to request @return 0 if successful, else error */ int del(const char* aURLPath); int del(const String& aURLPath); /** Connect to the server and send a DELETE request with body and content type @param aURLPath Url to request @param aContentType Content type of request body @param aBody Body of the request @return 0 if successful, else error */ int del(const char* aURLPath, const char* aContentType, const char* aBody); int del(const String& aURLPath, const String& aContentType, const String& aBody); int del(const char* aURLPath, const char* aContentType, int aContentLength, const byte aBody[]); /** Connect to the server and start to send the request. If a body is provided, the entire request (including headers and body) will be sent @param aURLPath Url to request @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. @param aContentType Content type of request body (optional) @param aContentLength Length of request body (optional) @param aBody Body of request (optional) @return 0 if successful, else error */ int startRequest(const char* aURLPath, const char* aHttpMethod, const char* aContentType = NULL, int aContentLength = -1, const byte aBody[] = NULL); /** Send an additional header line. This can only be called in between the calls to beginRequest and endRequest. @param aHeader Header line to send, in its entirety (but without the trailing CRLF. E.g. "Authorization: Basic YQDDCAIGES" */ void sendHeader(const char* aHeader); void sendHeader(const String& aHeader) { sendHeader(aHeader.c_str()); } /** Send an additional header line. This is an alternate form of sendHeader() which takes the header name and content as separate strings. The call will add the ": " to separate the header, so for example, to send a XXXXXX header call sendHeader("XXXXX", "Something") @param aHeaderName Type of header being sent @param aHeaderValue Value for that header */ void sendHeader(const char* aHeaderName, const char* aHeaderValue); void sendHeader(const String& aHeaderName, const String& aHeaderValue) { sendHeader(aHeaderName.c_str(), aHeaderValue.c_str()); } /** Send an additional header line. This is an alternate form of sendHeader() which takes the header name and content separately but where the value is provided as an integer. The call will add the ": " to separate the header, so for example, to send a XXXXXX header call sendHeader("XXXXX", 123) @param aHeaderName Type of header being sent @param aHeaderValue Value for that header */ void sendHeader(const char* aHeaderName, const int aHeaderValue); void sendHeader(const String& aHeaderName, const int aHeaderValue) { sendHeader(aHeaderName.c_str(), aHeaderValue); } /** Send a basic authentication header. This will encode the given username and password, and send them in suitable header line for doing Basic Authentication. @param aUser Username for the authorization @param aPassword Password for the user aUser */ void sendBasicAuth(const char* aUser, const char* aPassword); void sendBasicAuth(const String& aUser, const String& aPassword) { sendBasicAuth(aUser.c_str(), aPassword.c_str()); } /** Get the HTTP status code contained in the response. For example, 200 for successful request, 404 for file not found, etc. */ int responseStatusCode(); /** Check if a header is available to be read. Use readHeaderName() to read header name, and readHeaderValue() to read the header value MUST be called after responseStatusCode() and before contentLength() */ bool headerAvailable(); /** Read the name of the current response header. Returns empty string if a header is not available. */ String readHeaderName(); /** Read the value of the current response header. Returns empty string if a header is not available. */ String readHeaderValue(); /** Read the next character of the response headers. This functions in the same way as read() but to be used when reading through the headers. Check whether or not the end of the headers has been reached by calling endOfHeadersReached(), although after that point this will still return data as read() would, but slightly less efficiently MUST be called after responseStatusCode() and before contentLength() @return The next character of the response headers */ int readHeader(); /** Skip any response headers to get to the body. Use this if you don't want to do any special processing of the headers returned in the response. You can also use it after you've found all of the headers you're interested in, and just want to get on with processing the body. MUST be called after responseStatusCode() @return HTTP_SUCCESS if successful, else an error code */ int skipResponseHeaders(); /** Test whether all of the response headers have been consumed. @return true if we are now processing the response body, else false */ bool endOfHeadersReached(); /** Test whether the end of the body has been reached. Only works if the Content-Length header was returned by the server @return true if we are now at the end of the body, else false */ bool endOfBodyReached(); virtual bool endOfStream() { return endOfBodyReached(); }; virtual bool completed() { return endOfBodyReached(); }; /** Return the length of the body. Also skips response headers if they have not been read already MUST be called after responseStatusCode() @return Length of the body, in bytes, or kNoContentLengthHeader if no Content-Length header was returned by the server */ long contentLength(); /** Returns if the response body is chunked @return true if response body is chunked, false otherwise */ int isResponseChunked() { return iIsChunked; } /** Return the response body as a String Also skips response headers if they have not been read already MUST be called after responseStatusCode() @return response body of request as a String */ String responseBody(); /** Enables connection keep-alive mode */ void connectionKeepAlive(); /** Disables sending the default request headers (Host and User Agent) */ void noDefaultRequestHeaders(); // Inherited from Print // Note: 1st call to these indicates the user is sending the body, so if need // Note: be we should finish the header first virtual size_t write(uint8_t aByte) { if (iState < eRequestSent) { finishHeaders(); }; return iClient-> write(aByte); }; virtual size_t write(const uint8_t *aBuffer, size_t aSize) { if (iState < eRequestSent) { finishHeaders(); }; return iClient->write(aBuffer, aSize); }; // Inherited from Stream virtual int available(); /** Read the next byte from the server. @return Byte read or -1 if there are no bytes available. */ virtual int read(); virtual int read(uint8_t *buf, size_t size); virtual int peek() { return iClient->peek(); }; virtual void flush() { iClient->flush(); }; // Inherited from Client virtual int connect(IPAddress ip, uint16_t port) { return iClient->connect(ip, port); }; virtual int connect(const char *host, uint16_t port) { return iClient->connect(host, port); }; virtual void stop(); virtual uint8_t connected() { return iClient->connected(); }; virtual operator bool() { return bool(iClient); }; virtual uint32_t httpResponseTimeout() { return iHttpResponseTimeout; }; virtual void setHttpResponseTimeout(uint32_t timeout) { iHttpResponseTimeout = timeout; }; virtual uint32_t httpWaitForDataDelay() { return iHttpWaitForDataDelay; }; virtual void setHttpWaitForDataDelay(uint32_t delay) { iHttpWaitForDataDelay = delay; }; protected: /** Reset internal state data back to the "just initialised" state */ void resetState(); /** Send the first part of the request and the initial headers. @param aURLPath Url to request @param aHttpMethod Type of HTTP request to make, e.g. "GET", "POST", etc. @return 0 if successful, else error */ int sendInitialHeaders(const char* aURLPath, const char* aHttpMethod); /* Let the server know that we've reached the end of the headers */ void finishHeaders(); /** Reading any pending data from the client (used in connection keep alive mode) */ void flushClientRx(); // Number of milliseconds that we wait each time there isn't any data // available to be read (during status code and header processing) static const int kHttpWaitForDataDelay = 100; // Number of milliseconds that we'll wait in total without receiving any // data before returning HTTP_ERROR_TIMED_OUT (during status code and header // processing) static const int kHttpResponseTimeout = 30*1000; static const char* kContentLengthPrefix; static const char* kTransferEncodingChunked; typedef enum { eIdle, eRequestStarted, eRequestSent, eReadingStatusCode, eStatusCodeRead, eReadingContentLength, eSkipToEndOfHeader, eLineStartingCRFound, eReadingBody, eReadingChunkLength, eReadingBodyChunk } tHttpState; // Client we're using Client* iClient; // Server we are connecting to const char* iServerName; IPAddress iServerAddress; // Port of server we are connecting to uint16_t iServerPort; // Current state of the finite-state-machine tHttpState iState; // Stores the status code for the response, once known int iStatusCode; // Stores the value of the Content-Length header, if present long iContentLength; // How many bytes of the response body have been read by the user int iBodyLengthConsumed; // How far through a Content-Length header prefix we are const char* iContentLengthPtr; // How far through a Transfer-Encoding chunked header we are const char* iTransferEncodingChunkedPtr; // Stores if the response body is chunked bool iIsChunked; // Stores the value of the current chunk length, if present int iChunkLength; uint32_t iHttpResponseTimeout; uint32_t iHttpWaitForDataDelay; bool iConnectionClose; bool iSendDefaultRequestHeaders; String iHeaderLine; }; #endif 能否基于https://docs.arduino.cc/libraries/ethernet/#Client%20Class中的EthernetClient帮我仿照实现下所有对应的功能
09-03
@Slf4j @Component public class BidirectionRequestHeaderProcessor extends AuthRequiredHeaderProcessor<BidirectionRequestChannel, BidirectionRequestChannelInformation> { private ChannelManager channelManager; private ContainerManager containerManager; private CloudAccessChannelHelper cloudAccessChannelHelper; @Autowired public BidirectionRequestHeaderProcessor(AuthClient authClient, BidirectionRequestHeaderProcessorProp prop, ChannelManager channelManager, ContainerManager containerManager, CloudAccessChannelHelper cloudAccessChannelHelper) { super(authClient, prop); this.channelManager = channelManager; this.containerManager = containerManager; this.cloudAccessChannelHelper = cloudAccessChannelHelper; } @Override protected BidirectionRequestChannel doBeforeAddExecutor(BidirectionRequestChannel bidirectionChannel) { if (log.isDebugEnabled()) { log.debug("[sid:{}] Disable auto read of Bidirection channel:", bidirectionChannel.getSid()); } bidirectionChannel.disableAutoRead(); return bidirectionChannel; } @Override public void doHeaderProcess(BidirectionRequestChannel bidirectionChannel) throws Exception { // Add into channel manager String channelId = bidirectionChannel.getChannelId(); channelManager.addRequestChannel(channelId, bidirectionChannel); // Add life time event if necessary. As the bidirection has sink channel characteristic, the life time event // should be created before channel adding into container. if (bidirectionChannel instanceof ILifeTimeControl) { ((ILifeTimeControl) bidirectionChannel).createLifeTimeEvent(ILifeTimeControl.LifeTimeType.CON_IDLE); } // Obtain the container. BidirectionContainer container = containerManager.obtainBidirectionContainer(bidirectionChannel); if (container == null) { log.info("[sid:{}] Bidirection channel is kicked off by others when obtaining container.", bidirectionChannel.getSid()); bidirectionChannel.sendHttpResponseAndClose(503, "Kicked off", RelayConsts.CloseReason.BIDIRECTION_KICKED_OFF); return; } if (bidirectionChannel instanceof CloudAccessBidirectionRequestChannel) { doCloudAccessHeaderProcess(bidirectionChannel, container); } // Initialize the container. if (log.isDebugEnabled()) { log.debug("[sid:{}] Bidirection channel obtain container successfully: container={}", bidirectionChannel.getSid(), container.toString()); } boolean hasPeerChannel = bidirectionChannel.initContainer(container); if (hasPeerChannel) { if (log.isDebugEnabled()) { log.debug("[sid:{}] Initialize Bidirection channel after container initialization", bidirectionChannel.getSid()); } bidirectionChannel.startDataAcceptanceAfterSinkChannelAccess(); } } public void doCloudAccessHeaderProcess(final BidirectionRequestChannel bidirectionChannel, BidirectionContainer container) throws Exception { RequestChannel cloudAccessChannel = cloudAccessChannelHelper .createCloudAccessChannel(bidirectionChannel.getBindingId(), bidirectionChannel.getInformation().getOrignalRequest(), bidirectionChannel.getNettyChannel()); if (cloudAccessChannel instanceof CloudAccessBidirectionRequestChannel) { CloudAccessBidirectionRequestChannel cloudRelayChannel = (CloudAccessBidirectionRequestChannel) cloudAccessChannel; channelManager.addRequestChannel(cloudRelayChannel.getChannelId(), cloudRelayChannel); cloudRelayChannel.createLifeTimeEvent(ILifeTimeControl.LifeTimeType.CON_IDLE); containerManager.obtainBidirectionContainer(cloudRelayChannel); cloudRelayChannel.initContainer(container); } else { log.error("failed to create cloud access channel for {}", bidirectionChannel.getBindingId()); } } } @Slf4j @Component public class PassthroughGetRequestDataProcessor extends AbstractProcessor { private ChannelManager channelManager; @Autowired public PassthroughGetRequestDataProcessor(ChannelManager channelManager) { this.channelManager = channelManager; } @Override protected PassthroughGetRequestChannel doProcess(PassthroughGetRequestChannel passthroughGetChannel) throws Exception { String bindingId = passthroughGetChannel.getBindingId(); PostRequestChannel postChannel = (PostRequestChannel) channelManager.getRequestChannel(bindingId); if (postChannel == null) { log.info("[sid:{}] POST channel is missing when passthrough GET data arrives.", passthroughGetChannel.getSid()); return passthroughGetChannel; } // Start Get channel transmission: only for 1.3 multiple mapping request passthroughGetChannel.startTransmission(postChannel); List<IHttpData> dataList = passthroughGetChannel.getDataList(); IHttpResponseGenerator generator = postChannel.getSuccessResponseGenerator(); while (!dataList.isEmpty()) { IHttpData data = dataList.remove(0); if (generator != null && !data.isLastData()) { postChannel.sendData(generator.getHttpData(data)); } else { // Maybe POST channel is Version 1.2, this should not happen. data.release(); } } return passthroughGetChannel; } @Override public void exceptionCaught(PassthroughGetRequestChannel passthroughGetChannel, Throwable cause) { if (cause instanceof IllegalArgumentException || cause instanceof IllegalRequestException) { log.error("[sid:{}] Illegal request data:", passthroughGetChannel.getSid(), cause); passthroughGetChannel.close(RelayConsts.CloseReason.ILLEGAL_REQUEST_DATA_FORMAT); } else { log.error("[sid:{}] PassthroughGetRequestDataProcessor failed:", passthroughGetChannel.getSid(), cause); passthroughGetChannel.close(RelayConsts.CloseReason.SERVER_INTERNAL_ERROR); } } } @Slf4j @Component public class BufferedRequestDataProcessor extends RelayPostRequestDataProcessor { @Override protected DataProcessState processData(BufferedPostRequestChannel bufferedPostChannel, DataProcessState dataProcessState, IHttpData data) { if (dataProcessState == DataProcessState.TRANSMIT_DIRECT) { return super.transmitDataDirectly(bufferedPostChannel, DataProcessState.TRANSMIT_DIRECT, data); } else if (dataProcessState == DataProcessState.WANT_BUFFERED_DATA) { if (log.isDebugEnabled()) { log.debug("[sid:{}] Process buffered data: index={}", bufferedPostChannel.getSid(), data.getIndex()); } // store buffered data int left = bufferedPostChannel.addBufferedHttpData(data); if (left > 0) { // transmit buffered data to bound channels bufferedPostChannel.transmitData(data); // keep state return DataProcessState.WANT_BUFFERED_DATA; } else if (left == 0) { // transmit buffered data to bound channels bufferedPostChannel.transmitData(data); // change data process stat to DataProcessStat. return bufferedPostChannel.changeToNextState(); } else { // This will not happen super.transmitDataDirectly(bufferedPostChannel, DataProcessState.TRANSMIT_DIRECT, data); return bufferedPostChannel.changeToNextState(); } } else if (dataProcessState == DataProcessState.WANT_RESPONSE_MOULD) { return super.processHttpResponseMould(bufferedPostChannel, data); } else { // Use direct transmission as default action. This case should not happen. return super.transmitDataDirectly(bufferedPostChannel, DataProcessState.TRANSMIT_DIRECT, data); } } } @Slf4j @Component(“relayPostRequestDataProcessor”) public class RelayPostRequestDataProcessor extends PostRequestDataProcessor { @Override protected DataProcessState processData(T postChannel, DataProcessState dataProcessState, IHttpData data) { if (dataProcessState == DataProcessState.TRANSMIT_DIRECT) { return super.transmitDataDirectly(postChannel, DataProcessState.TRANSMIT_DIRECT, data); } else if (dataProcessState == DataProcessState.WANT_RESPONSE_MOULD) { return processHttpResponseMould(postChannel, data); } else { // Use direct transmission as default action. This case should not happen. return super.transmitDataDirectly(postChannel, DataProcessState.TRANSMIT_DIRECT, data); } } /** * Parse the response mould into response header, set it into POST request channel and transmit it to all GET * request channels related with the POST request channel. * * @param postChannel The POST request channel which received the response mould data. * @param data The response mould data */ protected DataProcessState processHttpResponseMould(T postChannel, IHttpData data) throws IllegalRequestException { log.debug("[sid:{}] Process first data: index={}", postChannel.getSid(), data.getIndex()); try { String content = null; if (data instanceof MultipartHttpData) { content = ((MultipartHttpData) data).addedContents().toString(data.getContentCharset()); } else { content = data.toString(); } HttpResponse httpResponse = parseHttpResponse(content); // If the Transfer-Encoding: chunked is set, do not modified as Chunked is used to measure the message // length of the HTTP response even if the Content-Length is set at the same time. if (!HttpUtil.isTransferEncodingChunked(httpResponse)) { String type = postChannel.getInformation().getParams().get(RelayConsts.ParamKey.TYPE); if (RelayConsts.Type.FILE.equals(type)) { // If the type is file, the Content-Length field in response should be the file size and multipart // should not be used. if (postChannel.getInformation().getContentType().isMultiPart() && httpResponse.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) { throw new IllegalRequestException( "file type with multipart package should use Transfer-Encoding: chunked."); } } else { // If the type is other value, only when the Content-Length field in response does not exist or is // negtive, new Content-Length will be set. if (HttpUtil.getContentLength(httpResponse, -1L) < 0) { long contentLength = HttpUtil.getContentLength(postChannel.getInformation().getOrignalRequest(), -1L); httpResponse.headers().set(RelayConsts.HttpNames.CONTENT_LENGTH, contentLength > 0 ? contentLength : Long.MAX_VALUE); } } } // if status is 429, it shows device has too many relay connections at the same time // if (postChannel.hasSegmenter() && // httpResponse.status().code() == HttpResponseStatus.TOO_MANY_REQUESTS.code()) { // postChannel.stopSegmenterWhenCanNotGetVideoStream(RelayConsts.SegmenterStopReason.RELAY_CONNECTION_EXCEEDED); // } postChannel.setResponseHeader(httpResponse); postChannel.transmitHttpResponse(httpResponse); return postChannel.changeToNextState(); } finally { data.release(); } } /** * Parse response header from {@link String} format to {@link HttpResponse} format. * * @param stringValue {@link String} format of response header. * * @return {@link HttpResponse} format of response header. */ public HttpResponse parseHttpResponse(String stringValue) throws IllegalRequestException { String[] parts = stringValue.split("\r\n", 2); if (parts.length != 2 || parts[1].isEmpty()) { throw new IllegalRequestException("Invalid first data as a response header: " + stringValue); } String[] statusLine = parts[0].split(" ", 3); if (statusLine.length != 3 || statusLine[1].isEmpty() || statusLine[2].isEmpty()) { throw new IllegalRequestException("Invalid first data with an invalid response line: " + parts[0]); } HttpResponse httpResponse = null; try { HttpResponseStatus status = new SpecificCodeHttpResponseStatus(statusLine[1], statusLine[2]); httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); } catch (NumberFormatException e) { throw new IllegalRequestException("Invalid first data with an unresovled status code: " + parts[0]); } String[] entries = parts[1].split("\r\n"); for (String entry : entries) { String[] content = entry.split(":", 2); if (content.length != 2 || content[1].isEmpty()) { throw new IllegalRequestException("Invalid first data with an unresovled header: " + entry); } String name = content[0].trim(); String value = content[1].trim(); if (name.isEmpty()) { throw new IllegalRequestException("Invalid first data with an empty key of header:" + entry); } httpResponse.headers().add(name, value); } // Netty channel of GET could not be reused. httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); httpResponse.headers().set(HttpHeaderNames.PRAGMA, HttpHeaderValues.NO_CACHE); httpResponse.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE); return httpResponse; } } @Slf4j @Component(“postRequestDataProcessor”) public class PostRequestDataProcessor extends AbstractProcessor { @Override protected T doProcess(T sourceChannel) throws IllegalRequestException { DataProcessState dataProcessStat = sourceChannel.getDataProcessState(); List<IHttpData> dataList = sourceChannel.getDataList(); while (!dataList.isEmpty()) { IHttpData data = dataList.remove(0); dataProcessStat = processData(sourceChannel, dataProcessStat, data); } return sourceChannel; } protected DataProcessState processData(T sourceChannel, DataProcessState dataProcessState, IHttpData data) { return transmitDataDirectly(sourceChannel, dataProcessState, data); } protected DataProcessState transmitDataDirectly(T sourceChannel, DataProcessState dataProcessStat, IHttpData data) { assert dataProcessStat == DataProcessState.TRANSMIT_DIRECT; // Transmit the data sourceChannel.transmitData(data); // Release data original reference data.release(); return DataProcessState.TRANSMIT_DIRECT; } @Override public void exceptionCaught(T sourceChannel, Throwable cause) { if (cause instanceof IllegalArgumentException || cause instanceof IllegalRequestException) { log.error("[sid:{}] Illegal request data format:", sourceChannel.getSid(), cause); sourceChannel.close(RelayConsts.CloseReason.ILLEGAL_REQUEST_DATA_FORMAT); } else { log.error("[sid:{}] PostRequestDataProcessor failed:", sourceChannel.getSid(), cause); sourceChannel.close(RelayConsts.CloseReason.SERVER_INTERNAL_ERROR); } } } @Slf4j public class DefaultGetRequestChannel extends GetRequestChannel implements ILifeTimeControl, IHeartBeatControl { private static final RuntimeException HEART_BEAT_TIMEOUT_EXCEPTION = new RuntimeException("Heart beat timeout"); protected ILifeTimeControl lifeTimeController; protected AtomicLong closedTimeStamp; public DefaultGetRequestChannel(String channelId, Channel nettyChannel, String bindingId, int basicLifeTime, GetRequestChannelInformation information) { super(channelId, nettyChannel, bindingId, information); this.lifeTimeController = buildLifeTimeController(nettyChannel, basicLifeTime); this.closedTimeStamp = new AtomicLong(System.currentTimeMillis() + IHeartBeatControl.PROLONGED_TIME_MS); } protected ILifeTimeControl buildLifeTimeController(Channel nettyChannel, int basicLifeTime) { IEventCallBack callback = new IEventCallBack() { @Override public void handleEvent() { if (log.isDebugEnabled()) { log.debug("[sid:{}] Channel is closing as timeout: channelType={}", getSid(), DefaultGetRequestChannel.this.getClass().getSimpleName()); } sendHttpResponseAndClose(HttpResponseStatus.NOT_FOUND, RelayConsts.CloseReason.GET_LIFETIME_EXPIRE); } }; return new LifeTimeController(getSid(), ILifeTimeControl.GET_IDLE_TIME_S, basicLifeTime, EventTimer.getLifeTimeEventKey(nettyChannel), getClass().getSimpleName(), callback); } @Override protected ISinkHandler buildBasicSinkHandler(GetRequestChannelInformation information) { assert !information.isFFmpegChannel(); if (isAudio(information)) { return new AudioSinkHandler(this); } else { return new DefaultSinkHandler<>(this); } } private boolean isAudio(GetRequestChannelInformation information) { String type = information.getParams().get(RelayConsts.ParamKey.TYPE); switch (type) { case RelayConsts.Type.AUDIO: return true; case RelayConsts.Type.NVR: case RelayConsts.Type.SMART_NVR: String resolution = information.getParams().get(RelayConsts.ParamKey.RESOLUTION); if (resolution == null) { return false; } switch (resolution) { case RelayConsts.AudioResolution.AAC: case RelayConsts.AudioResolution.MP2: case RelayConsts.AudioResolution.PCM: return true; case RelayConsts.VideoResolution.HD: case RelayConsts.VideoResolution.QVGA: case RelayConsts.VideoResolution.VGA: default: return false; } default: return false; } } @Override public long getClosedTimeStamp() { return closedTimeStamp.get(); } @Override public void setClosedTimeStamp(long timeStamp) { closedTimeStamp.lazySet(timeStamp); information.addHeartBeatNum(); } @Override public String getLifeTimeEventKey() { return lifeTimeController.getLifeTimeEventKey(); } @Override public int getBasicLifeTime() { return lifeTimeController.getBasicLifeTime(); } @Override public int createLifeTimeEvent(LifeTimeType type) throws TimerEventException { return lifeTimeController.createLifeTimeEvent(type); } @Override public void prolongLeftTimeTo(int newLifeTime) throws TimerEventException { lifeTimeController.prolongLeftTimeTo(newLifeTime); } @Override public void shortenLeftTimeTo(int newLifeTime) { lifeTimeController.shortenLeftTimeTo(newLifeTime); } @Override public void removeLifeTimeEvent() { lifeTimeController.removeLifeTimeEvent(); } /** * {@inheritDoc} */ @Override public boolean updateAfterContainerInitialization(ISourceRequestChannel postRequestChannel) { try { boolean isSuccess = super.updateAfterContainerInitialization(postRequestChannel); if (isSuccess) { lifeTimeController.prolongLeftTimeTo(lifeTimeController.getBasicLifeTime()); } return isSuccess; } catch (Exception e) { log.error("[sid:{}] Exception caught when updating Get channel after container initialization", getSid(), e); return false; } } /** * {@inheritDoc} */ @Override public boolean updateWhenBinding(PostRequestChannel postRequestChannel) { try { boolean isSuccess = super.updateWhenBinding(postRequestChannel); if (isSuccess) { createLifeTimeEvent(LifeTimeType.BASIC); } return isSuccess; } catch (Exception e) { log.error("[sid:{}] Exception caught when binding Get channel", getSid(), e); return false; } } /** * If the heart beat is expired, the Get channel will be closed instead of sending template data. At this time, the * template data reference will not be increased. * * @param templateData The template data */ @Override public ChannelFuture sendTemplateData(IHttpData templateData) { if (System.currentTimeMillis() > getClosedTimeStamp()) { if (log.isDebugEnabled()) { log.debug("[sid:{}] Close GET channel as losing heartbeat:", getSid()); } close(RelayConsts.CloseReason.GET_LACK_HEART_BEAT); DefaultChannelPromise promise = new DefaultChannelPromise(getNettyChannel()); promise.setFailure(HEART_BEAT_TIMEOUT_EXCEPTION); return promise; } else { return super.sendTemplateData(templateData); } } @Override protected void releaseResourceAfterNettyChannelClosure() { super.releaseResourceAfterNettyChannelClosure(); removeLifeTimeEvent(); } } public class PassthroughGetRequestChannel extends DefaultGetRequestChannel implements IDataFollowedRelayRequestChannel { private RelayServiceDataController<PassthroughGetRequestChannel> dataController; private volatile boolean isStartDataProcess; public PassthroughGetRequestChannel(String channelId, Channel nettyChannel, String bindingId, // ClientInfoController clientInfoController, int basicLifeTime, GetRequestChannelInformation information) { super(channelId, nettyChannel, bindingId, // clientInfoController, basicLifeTime, information); this.dataController = new RelayServiceDataController<>(this, false); } @Override public boolean isStartDataProcess() { return isStartDataProcess; } @Override public void setStartDataProcess() { this.isStartDataProcess = true; } @Override public boolean updateAfterContainerInitialization(ISourceRequestChannel postRequestChannel) { if (super.updateAfterContainerInitialization(postRequestChannel)) { setStartDataProcess(); enableAutoRead(); addPushDataTask(); return true; } else { return false; } } /** * {@inheritDoc} */ @Override public boolean updateWhenBinding(PostRequestChannel postRequestChannel) { if (super.updateWhenBinding(postRequestChannel)) { setStartDataProcess(); enableAutoRead(); addPushDataTask(); return true; } else { return false; } } @Override public void disableAutoRead() { dataController.disableAutoRead(); } @Override public void enableAutoRead() { dataController.enableAutoRead(); } @Override public List<IHttpData> getDataList() { return dataController.getDataList(); } @Override public void addPushDataTask() { dataController.addPushDataTask(); } @Override protected void releaseResourceAfterNettyChannelClosure() { super.releaseResourceAfterNettyChannelClosure(); dataController.releaseDataList(); } } @Slf4j public class BidirectionRequestChannel extends SourceRequestChannel<BidirectionContainer, BidirectionRequestChannelInformation> implements ISinkRequestChannel { private DefaultSinkHandler<BidirectionRequestChannel> sinkHandler; public BidirectionRequestChannel(String channelId, Channel nettyChannel, String bindingId, BidirectionRequestChannelInformation information) { super(channelId, nettyChannel, bindingId, information); this.sinkHandler = new DefaultSinkHandler<>(this); } @Override public String getRequestType() { if (isDeviceChannel()) { return "relayservice-deviceStream"; } else { return "relayservice-appStream"; } } @Override public boolean initContainer(BidirectionContainer container) { assert this.container == null : "initContainer() should only be called once."; this.container = container; BidirectionRequestChannel peerChannel = getPeerDirectionChannel(container); if (peerChannel == null) { return false; } if (peerChannel.updateAfterContainerInitialization(this)) { return true; } else { // As peer channel is closing, this bidirection channel will be closing soon. close(RelayConsts.CloseReason.BIDIRECTION_CLOSED_BY_PEER); return false; } } public BidirectionRequestChannel getPeerDirectionChannel() { BidirectionContainer container = this.container; if (container != null) { return getPeerDirectionChannel(container); } else { return null; } } private BidirectionRequestChannel getPeerDirectionChannel(BidirectionContainer container) { if (isDeviceChannel()) { return container.getAppChannel(); } else { return container.getDeviceChannel(); } } /** * Only contains peer channel if it exists. */ @Override protected List<? extends ISinkRequestChannel> getTransmittedSinkRequestChannels() { BidirectionContainer container = this.container; if (container == null) { return new ArrayList<>(); } List<BidirectionRequestChannel> list = new ArrayList<>(); BidirectionRequestChannel peerChannel = getPeerDirectionChannel(container); if (peerChannel != null) { list.add(peerChannel); } return list; } /** * Do nothing, waiting terminal to close the request channel. */ @Override protected void doProcessBeforeTransmitData(IHttpData templateData) { // do nothing } @Override protected void transmitDataToSinkRequestChannel(ISinkRequestChannel sinkChannel, IHttpData templateData) { // 当channel来自回放请求时,channel为长连接,此时为了适配web无法发起正常http长连接,手动将lastData的空包丢掉 String type = this.getInformation().getParamValue(RelayConsts.ParamKey.TYPE); if ((Objects.equals(type, RelayConsts.Type.SDVOD) || Objects.equals(type, RelayConsts.Type.DOWNLOAD)) && templateData.isLastData()) { return; } sinkChannel.sendTemplateData(templateData); } @Override public void startDataAcceptanceAfterSinkChannelAccess() { assert container != null : "initContainer should be called first"; BidirectionRequestChannel peerChannel = getPeerDirectionChannel(container); if (peerChannel == null) { log.warn("[sid:{}] Peer channel is missing, may be closed?", getSid()); return; } if (information.getStatisticInformation().getIsWs()) { // Start data process setStartDataProcess(); // Enable auto read enableAutoRead(); return; } doBindInitialization(peerChannel); } @Override public DataProcessState getDataProcessState() { return DataProcessState.TRANSMIT_DIRECT; } @Override public DataProcessState changeToNextState() { return DataProcessState.TRANSMIT_DIRECT; } @Override public boolean updateAfterContainerInitialization(ISourceRequestChannel sourceRequestChannel) { if (log.isDebugEnabled()) { log.debug("[sid:{}] Do bind initialization after container initialization", getSid()); } doBindInitialization(sourceRequestChannel); return true; } protected void doBindInitialization(ISourceRequestChannel sourceRequestChannel) { // Update sid String sourceSid = sourceRequestChannel.getSid(); if (sourceSid.indexOf('-') == -1) { String oldSid = getSid(); if (oldSid.indexOf('-') == -1) { updateSid(sourceSid + '-' + oldSid); } } // Try to send response header HttpResponse responseHeader = sourceRequestChannel.getResponseHeader(); if (responseHeader != null) { sendTemplateHttpResponse(responseHeader); } // Start data process setStartDataProcess(); // Enable auto read enableAutoRead(); // Add push data task addPushDataTask(); } @Override public ChannelFuture sendTemplateHttpResponse(HttpResponse templateHttpResponse) { return sinkHandler.sendTemplateHttpResponse(templateHttpResponse); } @Override public ChannelFuture sendTemplateHttpResponseAndClose(HttpResponse templateHttpResponse, RelayConsts.CloseReason closeReason) { return sinkHandler.sendTemplateHttpResponseAndClose(templateHttpResponse, closeReason); } @Override public ChannelFuture sendTemplateData(IHttpData templateData) { return sinkHandler.sendTemplateData(templateData); } @Override public ChannelFuture sendTemplateDataAndClose(IHttpData templateData, RelayConsts.CloseReason closeReason) { return sinkHandler.sendTemplateDataAndClose(templateData, closeReason); } @Override protected void releaseResourceBeforeNettyChannelClosure() { if (information.getCloseReason() != RelayConsts.CloseReason.BIDIRECTION_CLOSED_BY_PEER) { ContainerManager.getInstance().removeBidirectionContainer(this); } super.releaseResourceBeforeNettyChannelClosure(); } } @Slf4j public abstract class SourceRequestChannel<T extends IRequestChannelContainer<? extends ISinkRequestChannel>, I extends AbstractTransactionRequestChannelInformation> extends TransactionRequestChannel implements ISourceRequestChannel { private String bindingId; private ChannelTrafficShapingHandler trafficHandler; private RelayServiceDataController<SourceRequestChannel<T, I>> dataController; private volatile boolean isStartDataProcess; private HttpResponse responseHeader; protected T container; public SourceRequestChannel(String channelId, Channel nettyChannel, String bindingId, I information) { super(channelId, nettyChannel, information); this.bindingId = bindingId; this.dataController = new RelayServiceDataController<>(this, false); } @Override public String getBindingId() { return bindingId; } public ChannelTrafficShapingHandler getTrafficHandler() { return trafficHandler; } public void setTrafficHandler(ChannelTrafficShapingHandler trafficHandler) { this.trafficHandler = trafficHandler; } @Override public void disableAutoRead() { dataController.disableAutoRead(); } @Override public void enableAutoRead() { dataController.enableAutoRead(); } @Override public List<IHttpData> getDataList() { return dataController.getDataList(); } @Override public void addPushDataTask() { dataController.addPushDataTask(); } @Override public HttpResponse getResponseHeader() { return responseHeader; } @Override public void setResponseHeader(HttpResponse responseHeader) { this.responseHeader = responseHeader; } @Override public boolean isStartDataProcess() { return isStartDataProcess; } @Override public void setStartDataProcess() { this.isStartDataProcess = true; } /** * Initialize the source channel with a container in which the sink channel is put into. * * @param container The container * * @return True if the container has at least one sink channel. */ public abstract boolean initContainer(T container); public boolean isInitContainer() { return container != null; } public T getContainer() { return container; } /** * Only used for Junit test. */ void setContainer(T container) { this.container = container; } /** * Start source channel data acceptance after sink channel access. */ public abstract void startDataAcceptanceAfterSinkChannelAccess(); /** * {@inheritDoc} */ @Override public void transmitHttpResponse(HttpResponse templateHttpResponse) { List<? extends ISinkRequestChannel> currentList = getTransmittedSinkRequestChannels(); for (ISinkRequestChannel sinkChannel : currentList) { try { sinkChannel.sendTemplateHttpResponse(templateHttpResponse); } catch (Exception e) { log.error("[sid:{}] Failed to transmit response to GET channel: channelId={}:", getSid(), sinkChannel.getChannelId(), e); sinkChannel.close(RelayConsts.CloseReason.SERVER_INTERNAL_ERROR); } } } /** * {@inheritDoc} */ @Override public void transmitData(IHttpData templateData) { List<? extends ISinkRequestChannel> currentList = templateData.isLastData() ? getTransmittedSinkRequestChannels() : null; doProcessBeforeTransmitData(templateData); currentList = currentList == null ? getTransmittedSinkRequestChannels() : currentList; for (ISinkRequestChannel sinkChannel : currentList) { try { transmitDataToSinkRequestChannel(sinkChannel, templateData); } catch (Exception e) { log.error("[sid:{}] Failed to transmit data to GET channel: channelId={}, index={}", getSid(), sinkChannel.getChannelId(), templateData.getIndex(), e); sinkChannel.close(RelayConsts.CloseReason.SERVER_INTERNAL_ERROR); } } } /** * Obtain all sink channel which can be transmitted response and data to. * * @return The sink channel which can be transmitted response and data to. */ protected abstract List<? extends ISinkRequestChannel> getTransmittedSinkRequestChannels(); /** * Process before transmit template data. * * @param templateData The template data */ protected abstract void doProcessBeforeTransmitData(IHttpData templateData); /** * Transmit template data to sink channel. * * @param sinkChannel The sink channel * @param templateData The template data. */ protected abstract void transmitDataToSinkRequestChannel(ISinkRequestChannel sinkChannel, IHttpData templateData); @Override protected void releaseResourceAfterNettyChannelClosure() { super.releaseResourceAfterNettyChannelClosure(); dataController.releaseDataList(); } } @Slf4j public abstract class TransactionRequestChannel extends RequestChannel { protected String channelId; @Getter @Setter private String content; public TransactionRequestChannel(String channelId, Channel nettyChannel, I information) { super(nettyChannel, information); this.channelId = channelId; } public String getChannelId() { return channelId; } public ChannelVersion getChannelVersion() { return information.getChannelVersion(); } public boolean isDeviceChannel() { return information.isFromDevice(); } public AbstractAuthContext getAuthContext() { return information.getAuthContext(); } public RequestLevel getRequestLevel() { return getAuthContext().getLevel(); } @Override protected void releaseResourceBeforeNettyChannelClosure() { // Call super method super.releaseResourceBeforeNettyChannelClosure(); // Reomve itself from ChannelManager immediately if (isRequestChannelRemovedImmediately()) { removeFromChannelManager(); } } @Override protected void releaseResourceAfterNettyChannelClosure() { // Call super method super.releaseResourceAfterNettyChannelClosure(); // Reomve itself from ChannelManager after 1s. if (!isRequestChannelRemovedImmediately()) { getNettyChannel().eventLoop().schedule(new Runnable() { @Override public void run() { removeFromChannelManager(); } }, 1000, TimeUnit.MILLISECONDS); } } protected boolean isRequestChannelRemovedImmediately() { return true; } ; protected void removeFromChannelManager() { if (log.isDebugEnabled()) { log.debug("[sid:{}] Remove request channel {} channelId={} from channel manager", getSid(), this.getClass().getSimpleName(), getChannelId()); } ChannelManager.getInstance().removeRequestChannel(channelId, this); } } @Slf4j public class GetRequestChannel extends TransactionRequestChannel implements ISinkRequestChannel { protected String bindingId; protected AtomicBoolean isAllowTransmission; protected ISinkHandler sinkHandler; public GetRequestChannel(String channelId, Channel nettyChannel, String bindingId, GetRequestChannelInformation information) { super(channelId, nettyChannel, information); this.bindingId = bindingId; this.isAllowTransmission = new AtomicBoolean(false); this.sinkHandler = buildSinkHandler(information); } /** * Build the sink hanlder according to the sink channel. When any template response and data are sent to this Get * channel, the sending action will be processed by using sink hanlder. * * @param information The Get channel information * * @return The sink hanlder. */ protected ISinkHandler buildSinkHandler(GetRequestChannelInformation information) { if (information.isNeedMultipleMappingSource()) { // Flow control is only used in Preview type(such as video or nvr) return new FlowControlHandler(buildBasicSinkHandler(information)); } else { return buildBasicSinkHandler(information); } } /** * The basic sink handler which is used to do actual sending action. * * @param information The Get channel information * * @return The basic sink hanlder. */ protected ISinkHandler buildBasicSinkHandler(GetRequestChannelInformation information) { return new DefaultSinkHandler<>(this); } @Override public String getRequestType() { if (getChannelVersion() == ChannelVersion.VERSION_1_2) { return "relayservice-getStream"; } else if (isDeviceChannel()) { return "relayservice-deviceStream"; } else { return "relayservice-appStream"; } } @Override public String getBindingId() { return bindingId; } public void updateBindingId(String newBindingId) { this.bindingId = newBindingId; } public boolean isAllowTransmission() { return isAllowTransmission.get(); } public boolean setAllowTransmission() { return this.isAllowTransmission.compareAndSet(false, true); } /** * Whether the type is a multiple mapping type, such as video, audio, mixed and nvr. * * @return True if the type is a multiple mapping type. */ public boolean isNeedMultipleMappingSource() { return information.isNeedMultipleMappingSource(); } /** * If the Get channel arrives before the corresponding Post channel, the Get channel will be put into a container. * When the Post channel arrives, the Post channel will find and initialize the container to check whether any Get * channel is bound with it. At this time, using this method the notify the Get channel of binding action after * container initialization. * <p> * <p> * This method will be called in following cases: * <ul> * <li>After the Post channel initializes the container, any Get channel in the container will be notified of the * initialization by calling this.</li> * <li>If the container has been initialized by Post channel when the Get channel is added into container directly. * The Get channel should be notified of the initialization by calling this.</li> * </ul> * </p> * * @param postRequestChannel The source channel which this sink channel is bound with. * * @return Whether the sink channel is successfully updated. If false, the sink channel may be closed and the source * channel should remove it. */ @Override public boolean updateAfterContainerInitialization(final ISourceRequestChannel postRequestChannel) { doBindInitialization(postRequestChannel); return true; } /** * Update Get channel when the Get channel is bound with Post channel. * * @param postRequestChannel The corresponding Post channel. * * @return True if the updating operation is successful. */ public boolean updateWhenBinding(final PostRequestChannel postRequestChannel) { doBindInitialization(postRequestChannel); return true; } private void doBindInitialization(final ISourceRequestChannel requestChannel) { EventLoop eventLoop = getNettyChannel().eventLoop(); if (eventLoop.inEventLoop()) { doBindInitializationInEventLoop(requestChannel); } else { eventLoop.execute(() -> doBindInitializationInEventLoop(requestChannel)); } } /** * The initialization of Get channel when bound with Post channel * * @param postRequestChannel The corresponding Post channel. */ protected void doBindInitializationInEventLoop(ISourceRequestChannel postRequestChannel) { // Update sid updateSidByPrefix(postRequestChannel.getSid()); // Try to send response header HttpResponse responseHeader = postRequestChannel.getResponseHeader(); if (responseHeader != null) { sendTemplateHttpResponse(responseHeader); } } /** * To initialize the Get request channel transmission. * * @param postRequestChannel The corresponding Post request channel. */ public void startTransmission(final PostRequestChannel postRequestChannel) { EventLoop eventLoop = getNettyChannel().eventLoop(); if (eventLoop.inEventLoop()) { doStartTransmission(postRequestChannel); } else { eventLoop.execute(() -> doStartTransmission(postRequestChannel)); } } private void doStartTransmission(PostRequestChannel postRequestChannel) { assert getNettyChannel().eventLoop().inEventLoop(); // This is helpful when the 2nd App for smart codec stream. initSmartCodec(postRequestChannel); if (setAllowTransmission()) { int num = postRequestChannel.updateTransmittedGetRequestChannels(); if (log.isDebugEnabled()) { log.debug("[sid:{}] Start transmission of GET channel: transmittedGetChannelNum={}", getSid(), num); } // This is helpful when the 1st App and smart codec notification may be comming soon. initSmartCodec(postRequestChannel); /* List<IHttpData> templateDataList = getDataListToStartTransmission(postRequestChannel); for (IHttpData templateData : templateDataList) { sendTemplateData(templateData); }*/ } } private void initSmartCodec(PostRequestChannel postRequestChannel) { if (postRequestChannel.isSmartCodec()) { startSmartCodec(); } else { stopSmartCodec(); } } /** * If the corresponding channel is {@link BufferedPostRequestChannel}, the buffered data will be returned. * Otherwise, empty array will be returned. * * @param postRequestChannel The corresponding Post request channel. * * @return The data list for initialization of transmission */ protected List<IHttpData> getDataListToStartTransmission(PostRequestChannel postRequestChannel) { List<IHttpData> dataList; if (postRequestChannel instanceof BufferedPostRequestChannel) { dataList = ((BufferedPostRequestChannel) postRequestChannel).getAllBufferedData(); if (log.isDebugEnabled()) { log.debug("[sid:{}] Add buffered HTTP data: size={}", getSid(), dataList.size()); } } else { dataList = new ArrayList<>(); } IHttpData iFrame = postRequestChannel.getIFrame(); if (iFrame != null) { dataList.add(iFrame); } return dataList; } @Override public ChannelFuture sendTemplateHttpResponse(HttpResponse templateHttpResponse) { return sinkHandler.sendTemplateHttpResponse(templateHttpResponse); } @Override public ChannelFuture sendTemplateHttpResponseAndClose(HttpResponse templateHttpResponse, RelayConsts.CloseReason closeReason) { return sinkHandler.sendTemplateHttpResponseAndClose(templateHttpResponse, closeReason); } @Override public ChannelFuture sendTemplateData(IHttpData templateData) { return sinkHandler.sendTemplateData(templateData); } @Override public ChannelFuture sendTemplateDataAndClose(IHttpData templateData, RelayConsts.CloseReason closeReason) { return sinkHandler.sendTemplateDataAndClose(templateData, closeReason); } public void startSmartCodec() { ISinkHandler handler = sinkHandler; FlowControlHandler flowControlHandler = findFlowControlHandler(handler); if (flowControlHandler != null) { flowControlHandler.setSmartCodec(true); } if (!(handler instanceof SmartCodecSinkHandler)) { sinkHandler = new SmartCodecSinkHandler(handler); } } public void stopSmartCodec() { ISinkHandler handler = sinkHandler; FlowControlHandler flowControlHandler = findFlowControlHandler(handler); if (flowControlHandler != null) { flowControlHandler.setSmartCodec(false); } if (handler instanceof SmartCodecSinkHandler) { sinkHandler = ((SmartCodecSinkHandler) handler).unwrap(); } } public void updateKeyFrameUTCTime(long utcTime) { FlowControlHandler h = findFlowControlHandler(sinkHandler); if (h != null) { h.updateUTCTime(utcTime); } } public void updateWritabilityEvent(boolean isWritable) { FlowControlHandler h = findFlowControlHandler(sinkHandler); if (h != null) { h.setSinkChannelWriterable(isWritable); } } private FlowControlHandler findFlowControlHandler(ISinkHandler h) { while (h instanceof FilterSinkHandler) { if (h instanceof FlowControlHandler) { return (FlowControlHandler) h; } else { h = ((FilterSinkHandler) h).unwrap(); } } return null; } @Override protected void doClose() { if (RelayConsts.Type.FILE.equals(information.getParams().get(RelayConsts.ParamKey.TYPE))) { // When the last data is sent, netty channel may be closed immediately and the elb will not discard the last // data. The delay added here is like SO_LINGER configuration. if (log.isDebugEnabled()) { log.debug("[sid={}] Delay close netty channel when file type", getSid()); } getNettyChannel().eventLoop().schedule(GetRequestChannel.super::doClose, 100, TimeUnit.MILLISECONDS); } else { super.doClose(); } } @Override protected void releaseResourceBeforeNettyChannelClosure() { // Call super method super.releaseResourceBeforeNettyChannelClosure(); // As this channel has unbounded, unbindGetRequestChannel() should be avoided. RelayConsts.CloseReason closeReason = information.getCloseReason(); if (closeReason == RelayConsts.CloseReason.GET_CLOSED_BY_POST || closeReason == RelayConsts.CloseReason.GET_CLOSED_AFTER_LAST_DATA_SENT || closeReason == RelayConsts.CloseReason.GET_CLOSED_BY_KILL_SHARER_CMD) { return; } ChannelManager manager = ChannelManager.getInstance(); PostRequestChannel postRequestChannel = (PostRequestChannel) manager.getRequestChannel(bindingId); if (postRequestChannel != null) { postRequestChannel.unbindGetRequestChannel(this); } else { ContainerManager.getInstance().removeFromSingleDirectionContainer(this); } } } @Slf4j public class BufferedPostRequestChannel extends DefaultPostRequestChannel { private volatile IHttpData[] buffedDataList; public BufferedPostRequestChannel(String channelId, Channel nettyChannel, String shortBindingId, DataProcessState initDataProcessState, int basicLifeTime, int bufferedNum, PostRequestChannelInformation information) { super(channelId, nettyChannel, shortBindingId, initDataProcessState, basicLifeTime, information); this.buffedDataList = new IHttpData[bufferedNum]; } /** * Try to add data into buffer. The return value represents the left number in the buffer after adding this data. If * the data is added successfully, the return value is non-negtive, especially 0 indicates the buffer is full after * adding this data. If the data is failed to be added into buffer when the buffer is already full, -1 will be * return. No matter the data is added successfully or not, the data reference will not be modified. * * @return left number in the buffer after adding this data. * <ul> * <li>If the value is positive, the buffer still have capcacity after adding this data</li> * <li>If the value is 0, the buffer is full after adding this data</li> * <li>If the value is -1, the data is not added into this data as the buffer is already full</li> * </ul> */ public int addBufferedHttpData(IHttpData bufferedHttpData) { IHttpData[] oldList = this.buffedDataList; int bufferedNum = oldList.length; int index = 0; for (; index < bufferedNum; index++) { if (oldList[index] == null) { break; } } if (index < bufferedNum) { IHttpData[] newList = Arrays.copyOf(oldList, bufferedNum); newList[index] = bufferedHttpData; this.buffedDataList = newList; return bufferedNum - (index + 1); } else { return -1; } } /** * Obtain all buffered data at this time. The return list will be instantiated each time this method is called. * * @return The buffered data list. */ public List<IHttpData> getAllBufferedData() { IHttpData[] current = this.buffedDataList; ArrayList<IHttpData> ret = new ArrayList<>(current.length); for (int index = 0; index < current.length; index++) { if (current[index] != null) { ret.add(current[index]); } else { break; } } return ret; } @Override protected DataProcessState getNextState(DataProcessState oldState) { if (oldState == DataProcessState.WANT_RESPONSE_MOULD) { return DataProcessState.WANT_BUFFERED_DATA; } else if (oldState == DataProcessState.WANT_BUFFERED_DATA) { return DataProcessState.TRANSMIT_DIRECT; } else if (oldState == DataProcessState.TRANSMIT_DIRECT) { return DataProcessState.TRANSMIT_DIRECT; } else { throw new RuntimeException("Bugs in data process state change:" + oldState); } } @Override protected void releaseResourceAfterNettyChannelClosure() { // release wave header if (log.isDebugEnabled()) { log.debug("[sid:{}] Release buffered HTTP data", getSid()); } IHttpData[] current = this.buffedDataList; this.buffedDataList = new IHttpData[0]; for (IHttpData data : current) { ReferenceCountUtil.safeRelease(data); } if (information.isFromDevice()) { log.debug("cloud access channel for {} from device is closing", getBindingId()); CloudStreamInfoContainer.getInstance().removeInfo(getBindingId()); } super.releaseResourceAfterNettyChannelClosure(); } } @Slf4j public class PostRequestChannel extends SourceRequestChannel<SingleDirectionContainer, PostRequestChannelInformation> { /* It is used for multiple mapping type when social share starts and the preview of single stream output IPC */ private String shortBindingId; private DataProcessState dataProcessState; private boolean isNeedResponseAfterGetAccess; private IHttpResponseGenerator successResponseGenerator; private IFrameHolder iFrameHolder; public PostRequestChannel(String channelId, Channel nettyChannel, String shortBindingId, DataProcessState initDataProcessState, PostRequestChannelInformation information) { super(channelId, nettyChannel, channelId, information); this.shortBindingId = shortBindingId; this.dataProcessState = initDataProcessState; if (this.getChannelVersion() == ChannelVersion.VERSION_1_2) { Boolean isNeedSuccessResponse = information.isNeedSuccessResponse(); if (isNeedSuccessResponse == null) { this.isNeedResponseAfterGetAccess = false; this.successResponseGenerator = buildSuccessResponseGenerator(nettyChannel.alloc()); } else { this.isNeedResponseAfterGetAccess = isNeedSuccessResponse; this.successResponseGenerator = isNeedSuccessResponse ? buildSuccessResponseGenerator(nettyChannel.alloc()) : null; } } else { this.isNeedResponseAfterGetAccess = true; this.successResponseGenerator = buildSuccessResponseGenerator(nettyChannel.alloc()); } } private IHttpResponseGenerator buildSuccessResponseGenerator(ByteBufAllocator alloc) { String boundary = ContentType.generateBoundary(); ContentType contentType = new ContentType("multipart/mixed", boundary, ContentType.DEFAULT_DATA_CHARSET); return new MultiPartResponseGenerator(alloc, contentType, HttpUtil.isTransferEncodingChunked(information .getOrignalRequest())); } @Override public String getRequestType() { if (getChannelVersion() == ChannelVersion.VERSION_1_2) { return "relayservice-postStream"; } else if (isDeviceChannel()) { return "relayservice-deviceStream"; } else { return "relayservice-appStream"; } } public boolean isMultipleMapping() { return information.isMultipleMapping(); } public boolean isNeedResponseAfterGetAccess() { return isNeedResponseAfterGetAccess; } public boolean isSingleStream() { return information.isSingleStream(); } public IHttpResponseGenerator getSuccessResponseGenerator() { return successResponseGenerator; } public String getShortBindingId() { return shortBindingId; } @Override public DataProcessState getDataProcessState() { return dataProcessState; } /** * Change to next state and return the new state. As this method is called in data processor only, the state field * only uses <code>volitale</code> instead of <code>AtomicReference</code> * * @return The next data process state. */ @Override public DataProcessState changeToNextState() { assert getNettyChannel().eventLoop().inEventLoop(); DataProcessState state = getNextState(this.dataProcessState); this.dataProcessState = state; return state; } protected DataProcessState getNextState(DataProcessState oldState) { if (oldState == DataProcessState.WANT_RESPONSE_MOULD) { return DataProcessState.TRANSMIT_DIRECT; } else if (oldState == DataProcessState.TRANSMIT_DIRECT) { return DataProcessState.TRANSMIT_DIRECT; } else { throw new RuntimeException("Bugs in data process state change:" + oldState); } } /** * Initialize the Post channel with a container. The Post channel only transmit data to the Get channel which has * the same version(except ffmpeg Get channel). If the Get channel version does not match with the Post version, the * Get channel will be response 404/400 and closed. Especially, when the Get channel is 1.2 version and Post channel * is 1.3 version, Get channel will be shown hint video whe next access. * * @param container The allocated container * * @return Whether the container has contained any Get channels, if <code>true</code>, the container contains GET * channels. */ @Override public boolean initContainer(SingleDirectionContainer container) { assert this.container == null : "initContainer() should only be called once."; synchronized (PostRequestChannel.class) { this.container = container; return doInitContainer(container); } } protected boolean doInitContainer(SingleDirectionContainer container) { boolean hasGetChannels = false; Iterator<GetRequestChannel> iter = container.boundIterator(); while (iter.hasNext()) { GetRequestChannel getChannel = iter.next(); if (getChannel.getChannelVersion() != getChannelVersion() // && !getChannel.isFFmpegChannel() ) { iter.remove(); information.addVersionUnmatchNum(); getChannel.getInformation().setVersionUnmatch(true); if (getChannel.getChannelVersion() == ChannelVersion.VERSION_1_2) { getChannel.sendHttpResponseAndClose(HttpResponseStatus.NOT_FOUND, CloseReason.GET_CLOSED_WITH_LOW_VERSION); } else { getChannel.sendHttpResponseAndClose(HttpResponseStatus.BAD_REQUEST, CloseReason.GET_CLOSED_WITH_HIGHT_VERSION); } continue; } if (getChannel.getChannelVersion() == ChannelVersion.VERSION_1_2) { getChannel.setAllowTransmission(); } boolean isSuccess = getChannel.updateAfterContainerInitialization(this); if (isSuccess) { isSuccess = doUpdateBindingInformation(getChannel); } if (isSuccess) { hasGetChannels = true; } else { iter.remove(); getChannel.sendHttpResponseAndClose(HttpResponseStatus.NOT_FOUND, CloseReason.GET_CLOSED_WHEN_BIND_FAILURE); } } if (hasGetChannels) { // As Post channel data doesnot arrive or is buffered in decoder, startTransmission of Get channel does // not need to be called. container.synchronizeTransmittedSinkRequestChannelList(); } return hasGetChannels; } /** * Update Get and Post channel after container initialization. This is called when Get channel is added into * container which has been initialized by this Post channel. * <ul> * <li>For Get channel, using {@link GetRequestChannel#updateAfterContainerInitialization(ISourceRequestChannel)} to * update Get channel.</li> * <li>For Post channel, updating binding information</li> * </ul> * * @param getRequestChannel The Get channel which is added into an initialized container. * * @return True if the updating operation is successfully executed. */ public boolean updateAfterContainerInitialization(GetRequestChannel getRequestChannel) { // As this method is called after isInitContainer() checking, the container has been initialized. assert container != null : "initContainer() or isInitContainer() should be called first"; SingleDirectionContainer container = this.container; synchronized (container) { if (getRequestChannel.getChannelVersion() == ChannelVersion.VERSION_1_2) { getRequestChannel.startTransmission(this); } boolean isSuccess = getRequestChannel.updateAfterContainerInitialization(this); if (isSuccess) { isSuccess = doUpdateBindingInformation(getRequestChannel); } if (!isSuccess) { container.removeBoundSinkRequestChannel(getRequestChannel); container.synchronizeTransmittedSinkRequestChannelList(); } return isSuccess; } } /** * Call it after Post channel is added into channel manager. This method is used to obtain and process data which is * stored in decoder, which is useful to sticky package for version 1.3. */ public void callAfterAddIntoChannelManager() { for (GetRequestChannel getChannel : getBoundGetRequestChannels()) { if (getChannel instanceof IDataFollowedRelayRequestChannel) { ((IDataFollowedRelayRequestChannel) getChannel).setStartDataProcess(); ((IDataFollowedRelayRequestChannel) getChannel).enableAutoRead(); ((IDataFollowedRelayRequestChannel) getChannel).addPushDataTask(); } } } /** * Bind the given Get channel with this Post channel and return the binding result. If the Post channel is closed, * false will be returned.The Post channel only transmit data to the Get channel which has the same version(except * ffmpeg Get channel). If the Get channel version does not match with the Post version, the binding result will be * false. * * @param getRequestChannel The Get channel needs to be bound with this Post channel * * @return The binding action is sucessful or not. True if the Get channel is successfully bound with this Post * channel. False if the Post channel is closed and fails to bind the Get channel. */ public BindingResult bindGetRequestChannel(GetRequestChannel getRequestChannel) { // As this method is called after PostRequestChannel being put into channel manager. the container has been // initialized. assert container != null : "initContainer() or isInitContainer() should be called first"; if (isClosed()) { return BindingResult.SOURCE_CHANNEL_CLOSURE; } SingleDirectionContainer container = this.container; synchronized (container) { return doBindGetRequestChannel(container, getRequestChannel); } } protected BindingResult doBindGetRequestChannel(SingleDirectionContainer container, GetRequestChannel getRequestChannel) { BindingResult result = container.addBoundSinkRequestChannel(getRequestChannel); if (result != BindingResult.SUCCESS) { return result; } if (getRequestChannel.getChannelVersion() == ChannelVersion.VERSION_1_2) { getRequestChannel.startTransmission(this); } if (BooleanUtils.isTrue(getRequestChannel.getWs())) { if (getRequestChannel instanceof IDataFollowedRelayRequestChannel) { ((IDataFollowedRelayRequestChannel) getRequestChannel).setStartDataProcess(); ((IDataFollowedRelayRequestChannel) getRequestChannel).enableAutoRead(); } return BindingResult.SUCCESS; } boolean isSuccess = getRequestChannel.updateWhenBinding(this); if (isSuccess) { isSuccess = doUpdateBindingInformation(getRequestChannel); } if (!isSuccess) { container.removeBoundSinkRequestChannel(getRequestChannel); container.synchronizeTransmittedSinkRequestChannelList(); } return isSuccess ? BindingResult.SUCCESS : BindingResult.UPDATE_INFORMATION_FAILURE; } private boolean doUpdateBindingInformation(GetRequestChannel getRequestChannel) { switch (getRequestChannel.getRequestLevel()) { case OWNER: information.addOwnerNum(); break; case SLAVE: information.addSlaveNum(); break; case SHARED: information.addSharedNum(); break; case SOCIAL: information.addSocialNum(); break; default: break; } return true; } /** * Send pull-stream command when ffmpeg channel arrives and the Post channel version is 1.3 */ public void sendPullStreamCommandIfNecessary() { if (getChannelVersion() == ChannelVersion.VERSION_1_3) { assert successResponseGenerator != null; HttpResponse response = successResponseGenerator.getHttpResponse(HttpResponseStatus.OK); if (response != null) { sendHttpResponse(response); } JSONObject command = buildPullStreamCommand(getInformation().getParams()); sendData(successResponseGenerator.getHttpData(command)); } } /** * <p> * Build pull-stream command. * </p> * <p> * In 1.3 version, channel of video type is 0, and channel of nvr type starts from 0 and has following mapping. * * <pre> * 'channel' in relay request url ----> 'channel' in JSON command * 0 ----> 0 * 1 ----> 1 * ... ----> ... * </pre> * * </p> * * @param params request url parameters. * * @return The pull-stream command */ private JSONObject buildPullStreamCommand(Map<String, String> params) { // 实际上设备使用此信息控制推流RESOLUTION int channel = 0; if (Type.NVR.equals(params.get(ParamKey.TYPE)) || Type.SMART_NVR.equals(params.get(ParamKey.TYPE))) { channel = Integer.parseInt(params.get(ParamKey.CHANNEL)); } JSONObject preview = new JSONObject(); preview.put("channels", new JSONArray().put(channel)); String resolution = params.get(ParamKey.RESOLUTION); if (resolution != null) { preview.put("resolutions", new JSONArray().put(resolution)); } JSONObject command = new JSONObject(); command.put("type", "request"); command.put("seq", "0"); command.put("params", new JSONObject().put("method", "get").put("preview", preview)); return command; } /** * {@inheritDoc} * * <ul> * <li>For 1.3 version Post channel, as the terminal will not send data before receiving pull-data command, the * response and auto read COULD be sent and enabled after Get access.</li> * <li>For 1.2 version Post channel, the response and auto read SHOULD be sent and enabled after Get access.</li> * </ul> */ @Override public void startDataAcceptanceAfterSinkChannelAccess() { // Send success response header if necessary if (isNeedResponseAfterGetAccess()) { HttpResponse httpResponse = successResponseGenerator.getHttpResponse(HttpResponseStatus.OK); if (httpResponse != null) { sendHttpResponse(httpResponse); } } // Start data process setStartDataProcess(); // Try to enable auto read of POST channel enableAutoRead(); // Add pull data task addPushDataTask(); } /** * Unbind the given GET channel. If the GET channel exists in the binding list, true will be returned. * * @param getRequestChannel The GET channel needs to be unbound. * * @return True if the GET channel exists in the binding list. */ public boolean unbindGetRequestChannel(GetRequestChannel getRequestChannel) { // As this method is called after PostRequestChannel being put into channel manager. the container has been // initialized. assert container != null : "Should call initContainer() first."; SingleDirectionContainer container = this.container; synchronized (container) { boolean isExisted = container.removeBoundSinkRequestChannel(getRequestChannel); if (isExisted) { // If the ffmpeg channel is closed explicitly, the channel will not exist in list. container.synchronizeTransmittedSinkRequestChannelList(); // 取消FFmpeg重试 // if (getRequestChannel.isFFmpegChannel()) { // doRestartSegmenter(container); // } else { doTryToClose(container, CloseReason.POST_CLOSED_AFTER_GET_EXIT); // } } return isExisted; } } /** * Try to close the Post channel. The closure strategy is shown in {@link #doTryToClose(SingleDirectionContainer, * CloseReason)} * * @param closeReason The specific close reason. */ public void tryToClose(CloseReason closeReason) { SingleDirectionContainer container = this.container; if (container == null) { return; } synchronized (container) { doTryToClose(container, closeReason); } } /** * Try to close current POST channel after GET channel is unbound. For single mapping channel, the POST channel will * be closed immediately if no GET channel is bound any more. For multiple mapping channel, the POST channel will be * closed in following cases: * <ul> * <li>When the bound channel number is 0, the segmenter doesn't exist or is in Segmenter.MAX_RESTART_RETRIES state, * or doesn't have consumer.</li> * <li>When the bound channel number is 1, the segmenter is in Segmenter.SEGMENTING state and doesn't have consumer</li> * </ul> * * @param closeReason The reason of closing action */ protected void doTryToClose(SingleDirectionContainer container, CloseReason closeReason) { // Before doTryToClose, container has been checked and could not be null. if (isMultipleMapping()) { // int bindedChannelNum = container.getBoundSinkRequestChannelNum(); // if (bindedChannelNum == 0) { // Segmenter s = segmenter; // if (s == null || s.getState() == Segmenter.MAX_RESTART_RETRIES || !s.hasConsumer()) { // close(closeReason); // } // } else if (bindedChannelNum == 1) { // Segmenter s = segmenter; // if (s != null && s.isSegmenting() && !s.hasConsumer()) { // close(closeReason); // } // } //log.error("Is multiple mapping"); close(closeReason); } else { if (container.hasNoBoundSinkRequestChannel()) { close(closeReason); } } } /** * Close all GET channels bound with this POST channel. The method is only used in {@link * #releaseResourceAfterNettyChannelClosure()} */ private void closeAllGetRequestChannels() { SingleDirectionContainer container = this.container; if (container == null) { return; } synchronized (container) { Iterator<GetRequestChannel> iter = container.boundIterator(); while (iter.hasNext()) { GetRequestChannel getChannel = iter.next(); if (log.isDebugEnabled()) { log.debug("[sid:{}] Close GET Channel: channelId={}", getSid(), getChannel.getChannelId()); } getChannel.close(CloseReason.GET_CLOSED_BY_POST); } container.removeAllBoundSinkeRequestChannels(); container.synchronizeTransmittedSinkRequestChannelList(); } } public List<GetRequestChannel> getBoundGetRequestChannels() { SingleDirectionContainer container = this.container; if (container == null) { return new ArrayList<>(); } synchronized (container) { return container.getBoundSinkRequestChannels(); } } public int getBoundGetRequestChannelNum() { SingleDirectionContainer container = this.container; if (container == null) { return 0; } synchronized (container) { return container.getBoundSinkRequestChannelNum(); } } @SuppressWarnings("unchecked") public List<GetRequestChannel> getTransmittedGetRequestChannels() { return (List<GetRequestChannel>) getTransmittedSinkRequestChannels(); } public int getTransmittedGetRequestChannelNum() { SingleDirectionContainer container = this.container; if (container == null) { return 0; } return container.getTransmittedSinkRequestChannelNum(); } public int updateTransmittedGetRequestChannels() { SingleDirectionContainer container = this.container; if (container == null) { return 0; } synchronized (container) { return container.synchronizeTransmittedSinkRequestChannelList(); } } public class ControlRequestChannel extends BaseHttpRequestChannel { public ControlRequestChannel(String channelId, Channel nettyChannel, ControlRequestChannelInformation information) { super(channelId, nettyChannel, ILifeTimeControl.IDLE_LIFE_TIME_S, information); } @Override public String getRequestType() { String cmdMethod = information.getCmdMethod(); return cmdMethod != null ? RelayConsts.Service.CONTROL + '-' + cmdMethod : RelayConsts.Service.CONTROL; } }
09-11
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值