From 2c511ec9040345a8ed81e0335124dff0ee0d8744 Mon Sep 17 00:00:00 2001 From: Alex Hultman Date: Thu, 27 Sep 2018 23:44:46 +0200 Subject: [PATCH] More work on new streams --- 15.pro | 7 +- main.cpp | 88 +++++++++++++----- src/AsyncSocket.h | 91 +++++++++--------- src/HttpContext.h | 68 +++++--------- src/HttpResponse.h | 206 ++++++++++++++++------------------------- src/HttpResponseData.h | 6 +- src/Utilities.h | 49 ++++++++++ 7 files changed, 266 insertions(+), 249 deletions(-) create mode 100644 src/Utilities.h diff --git a/15.pro b/15.pro index 283f0e6..ab6ef9e 100644 --- a/15.pro +++ b/15.pro @@ -28,8 +28,9 @@ HEADERS += \ src/AsyncSocket.h \ src/AsyncSocketData.h \ src/Loop.h \ - src/App.h + src/App.h \ + src/Utilities.h INCLUDEPATH += uSockets/src src -QMAKE_CXXFLAGS += -fsanitize=address -LIBS += -lasan -lssl -lcrypto +#QMAKE_CXXFLAGS += -fsanitize=address +LIBS += -pthread -lssl -lcrypto diff --git a/main.cpp b/main.cpp index 852f7ef..d1f91f8 100644 --- a/main.cpp +++ b/main.cpp @@ -2,45 +2,85 @@ #include "examples/helpers/AsyncFileReader.h" -AsyncFileReader asyncFileReader("/home/alexhultman/v0.15/sintel.mkv"); +AsyncFileReader asyncFileReader("/home/alexhultman/sintel_small.mp4"); + + +inline std::string slurp(const std::string &path) { + std::ostringstream buf; + std::ifstream input (path.c_str()); + buf << input.rdbuf(); + return buf.str(); +} + +std::string sintelMovie = slurp("/home/alexhultman/sintel_small.mp4"); int main(int argc, char **argv) { + std::cout << "Sintel movie is " << sintelMovie.length() << " bytes" << std::endl; + + // läs in hela sintel-filmen här, testa strömmarna med den sen! + uWS::/*SSL*/App(/*{ .key_file_name = "/home/alexhultman/uWebSockets/misc/ssl/key.pem", .cert_file_name = "/home/alexhultman/uWebSockets/misc/ssl/cert.pem", .dh_params_file_name = "/home/alexhultman/dhparams.pem", .passphrase = "1234" - }*/).get("/", [](auto *res, auto *req) { + }*/).get("/*", [](auto *res, auto *req) { - res->writeStatus(uWS::HTTP_200_OK); - res->writeHeader("Content-Type", "text/html;charset=utf-8"); + //res->writeStatus(uWS::HTTP_200_OK); + //res->writeHeader("Content-Type", "text/html;charset=utf-8"); - res->write("

Hallå!

Din user-agent är: "); - res->end(req->getHeader("user-agent")); + std::cout << "Endar nu" << std::endl; + // buffer it up and end by draining it + res->end(sintelMovie); + + // stream it here + /*if (!res->tryEnd(sintelMovie)) { + res->onWritable([res](int offset) { + std::cout << "Streaming data at offset " << offset << std::endl; + res->tryEnd(std::string_view(sintelMovie).substr(offset)); + }); + }*/ + + // tryWrite / tryEnd + + // vad om man skriver tryEnd("

Hallå!

Din user-agent är: ", totalLength) + + //res->write("

Hallå!

Din user-agent är: "); + //res->end(req->getHeader("user-agent")); }).get("/async/sintel.mkv", [](auto *res, auto *req) { - // asyncFileReader.getFileSize() - - /* Peek from cache */ - std::string_view chunk = asyncFileReader.peek(/*offset*/ 0); - if (chunk.length()) { - /* We had parts of this file cached already */ - res->write(chunk); - } else { - /* We had nothing readily available right now, request async chunk and pause the stream until we have */ - asyncFileReader.request(/*offset*/ 0, [res](std::string_view chunk) { - /* We were aborted */ - if (!chunk.length()) { - std::cout << "Async File Read request was aborted!" << std::endl; - // close the socket here? - // we need a way to NOT resume a paused socket! essentially close! + // I guess it should return wherer or not it wants to be called again? + auto streamIt = [res](int offset) { + while(true) { + /* Peek from cache */ + std::string_view chunk = asyncFileReader.peek(offset); + if (chunk.length()) { + /* We had parts of this file cached already */ + res->tryEnd(chunk, asyncFileReader.getFileSize()); } else { - /* We finally got the data, resume stream with this chunk */ - res->write(chunk); + /* We had nothing readily available right now, request async chunk and pause the stream until we have */ + asyncFileReader.request(offset, [res](std::string_view chunk) { + /* We were aborted */ + if (!chunk.length()) { + std::cout << "Async File Read request was aborted!" << std::endl; + // close the socket here? + // we need a way to NOT resume a paused socket! essentially close! + } else { + /* We finally got the data, resume stream with this chunk */ + res->tryEnd(chunk, asyncFileReader.getFileSize()); + } + }); } - }); + return true; + } + }; + + // basically do this + if (!streamIt(0)) { + // what should it return? will onWritable be called again depending on what it returns? + res->onWritable(streamIt); } }).listen(3000, [](auto *token) { diff --git a/src/AsyncSocket.h b/src/AsyncSocket.h index 5cbd3d6..07f4ebf 100644 --- a/src/AsyncSocket.h +++ b/src/AsyncSocket.h @@ -11,6 +11,7 @@ namespace uWS { template struct AsyncSocket : StaticDispatch { + template friend struct HttpContext; protected: using SOCKET_TYPE = typename StaticDispatch::SOCKET_TYPE; using StaticDispatch::static_dispatch; @@ -23,39 +24,68 @@ protected: } } -public: void *getExt() { return static_dispatch(us_ssl_socket_ext, us_socket_ext)((SOCKET_TYPE *) this); } + void timeout(unsigned int seconds) { + static_dispatch(us_ssl_socket_timeout, us_socket_timeout)((SOCKET_TYPE *) this, seconds); + } + + void shutdown() { + static_dispatch(us_ssl_socket_shutdown, us_socket_shutdown)((SOCKET_TYPE *) this); + } + + SOCKET_TYPE *close() { + return static_dispatch(us_ssl_socket_close, us_socket_close)((SOCKET_TYPE *) this); + } + /* Cork this socket. Only one socket may ever be corked per-loop at any given time */ void cork() { + std::cout << "Cork called" << std::endl; + LoopData *loopData = getLoopData(); loopData->corked = true; } - /* Write in three levels of prioritization: cork-buffer, syscall, socket-buffer */ + /* Write in three levels of prioritization: cork-buffer, syscall, socket-buffer. Always drain if possible. */ // todo: consider supporting nextLength = UNKNOWN as -1 (more but unknown size) int write(const char *src, int length, bool optionally = false, int nextLength = 0) { LoopData *loopData = getLoopData(); - //std::cout << "Write called with length: " << length << ", optionally: " << optionally << std::endl; + std::cout << "Write called with length: " << length << ", optionally: " << optionally << std::endl; + + AsyncSocketData *asyncSocketData = (AsyncSocketData *) getExt(); /* Do nothing for a null sized chunk */ - if (length == 0) { + if (length == 0 && !asyncSocketData->buffer.length()) { //std::cout << "Write returned: 0" << std::endl; return 0; } - AsyncSocketData *asyncSocketData = (AsyncSocketData *) getExt(); - /* Do not write anything if we have a per-socket buffer */ if (asyncSocketData->buffer.length()) { if (optionally) { - //std::cout << "Write returned: 0" << std::endl; + + + // we have buffer and we are optionally, if drain then drain else quit + + // drain here + std::cout << "Drain path" << std::endl; + + // will just end up in a loop! + int written = static_dispatch(us_ssl_socket_write, us_socket_write)((SOCKET_TYPE *) this, asyncSocketData->buffer.data(), asyncSocketData->buffer.length(), nextLength != 0);//write(asyncSocketData->buffer.data(), asyncSocketData->buffer.length(), optionally, 0, true); + + // removeBuffer + asyncSocketData->buffer = asyncSocketData->buffer.substr(written); + + // should we really return this here? should be 0 as we took 0 new data! return 0; + + + } else { - std::cout << "Buffering at top of write!" << std::endl; + std::cout << "Buffering at top of write (really bad)!" << std::endl; /* At least we can reserve room for next chunk if we know it up front */ if (nextLength) { @@ -104,7 +134,7 @@ public: return written; } - std::cout << "Buffering at bottom of write!" << std::endl; + std::cout << "Buffering at bottom of write (okay)!" << std::endl; /* Fall back to worst possible case (should be very rare for HTTP) */ /* At least we can reserve room for next chunk if we know it up front */ @@ -124,6 +154,9 @@ public: /* Uncork this socket and flush or buffer any corked and/or passed data. It is essential to remember doing this. */ /* It does NOT count bytes written from cork buffer (they are already accounted for in the write call responsible for its corking)! */ int uncork(const char *src = nullptr, int length = 0, bool optionally = false) { + + std::cout << "Uncork called with length: " << length << std::endl; + LoopData *loopData = getLoopData(); if (loopData->corked) { @@ -137,48 +170,12 @@ public: /* We should only return with new writes, not things written to cork already */ return write(src, length, optionally, 0); + } else { + std::cout << "Not even corked!" << std::endl; } return 0; } - - /* Check if this socket has buffered data */ - bool hasBuffer() { - return ((AsyncSocketData *) getExt())->buffer.length() > 0; - } - - /* Drain any socket-buffer while also optionally sending a chunk */ - int mergeDrain(std::string_view optionalChunk = {}) { - - // strategy: if we have two parts and both will fit in cork buffer then cork them and recursively send them off - - // write any per-socket buffer and optionally more - - AsyncSocketData *asyncSocketData = (AsyncSocketData *) getExt(); - - // not handled yet - if (asyncSocketData->buffer.length()) { - std::cout << "ERROR! has socket buffer!" << std::endl; - exit(0); - } - - - /* Write the optional part */ - return write(optionalChunk.data(), optionalChunk.length(), true, 0); - } - - /* These should not be public to the user! */ - void timeout(unsigned int seconds) { - static_dispatch(us_ssl_socket_timeout, us_socket_timeout)((SOCKET_TYPE *) this, seconds); - } - - void shutdown() { - static_dispatch(us_ssl_socket_shutdown, us_socket_shutdown)((SOCKET_TYPE *) this); - } - - SOCKET_TYPE *close() { - return static_dispatch(us_ssl_socket_close, us_socket_close)((SOCKET_TYPE *) this); - } }; } diff --git a/src/HttpContext.h b/src/HttpContext.h index e0fc479..554973e 100644 --- a/src/HttpContext.h +++ b/src/HttpContext.h @@ -135,60 +135,38 @@ private: /* Handle HTTP write out */ static_dispatch(us_ssl_socket_context_on_writable, us_socket_context_on_writable)(getSocketContext(), [](auto *s) { - //std::cout << "Writable event!" << std::endl; + std::cout << "Writable event!" << std::endl; - /* Silence any spurious writable events due to SSL_read failing to write */ - AsyncSocket *asyncSocket = (AsyncSocket *) s; - HttpResponseData *httpResponseData = (HttpResponseData *) asyncSocket->getExt(); - if (httpResponseData->state & HttpResponseData::HTTP_PAUSED_STREAM_OUT) { - return s; - } /* Writing data should reset the timeout */ static_dispatch(us_ssl_socket_timeout, us_socket_timeout)(s, HTTP_IDLE_TIMEOUT_S); - /* Are we already ended and just waiting for a drain / shutdown? */ - if (httpResponseData->state & HttpResponseData::HTTP_ENDED_STREAM_OUT) { + AsyncSocket *asyncSocket = (AsyncSocket *) s; + HttpResponseData *httpResponseData = (HttpResponseData *) asyncSocket->getExt(); - /* Try and send everything buffered up */ - asyncSocket->mergeDrain(); - - /* If we succeed with drainage we can finally shut down */ - if (!asyncSocket->hasBuffer()) { - asyncSocket->shutdown(); - } - - /* Nothing here for us */ - return s; - } - - if (httpResponseData->outStream) { - /* Regular path, request more data */ - - // todo: share this path with HttpResponse::write (it is exatly the same logic!) - while (true) { - auto [msg_more, chunk] = httpResponseData->outStream(httpResponseData->offset); - - if (chunk.length() == 0) { - //std::cout << "onwritable paused!" << std::endl; - httpResponseData->state |= HttpResponseData::HTTP_PAUSED_STREAM_OUT; - break; - } - - int written = asyncSocket->mergeDrain(chunk); - httpResponseData->offset += written; - // this is not correct, we can reach the end! - if (written < chunk.length()) { - break; - } - } - - // todo: we should loop until we cannot send anymore just like we do in HttpResponse::write(stream)! + // if this, then it means it finished with no issues so we need to empty any buffers? + if (httpResponseData->onWritable) { + httpResponseData->onWritable(httpResponseData->offset); } else { - /* We can come here if we only have socket buffers to drain yet no attached stream */ - asyncSocket->mergeDrain(); + // lets drain here + std::cout << "LEts drain!" << std::endl; + + // mergeDrain + asyncSocket->write(nullptr, 0, true, 0); } + + + // bascially just empty the buffer and if successful also call onWritable (bad strategy!) + //asyncSocket->mergeDrain(); + + // what we want is to immediately call onWritable and have AsyncSocket::write calls always try and empty any buffers at the same time? + // AsyncSocket::write can take boolean drain = true to know it should try and drain the buffers according to whatever strategy + // mergeDrain is basically AsyncSocket::write with boolean drain = true! + + + // on writable should return whether it wants more data or not? + return s; }); diff --git a/src/HttpResponse.h b/src/HttpResponse.h index 8c81712..2409394 100644 --- a/src/HttpResponse.h +++ b/src/HttpResponse.h @@ -5,6 +5,7 @@ #include "AsyncSocket.h" #include "HttpResponseData.h" +#include "Utilities.h" namespace uWS { @@ -13,62 +14,28 @@ const char *HTTP_200_OK = "200 OK"; template struct HttpResponse : public AsyncSocket { + typedef AsyncSocket Super; private: HttpResponseData *getHttpResponseData() { - return (HttpResponseData *) AsyncSocket::getExt(); - } - - int u32toaHex(uint32_t value, char *dst) { - char palette[] = "0123456789abcdef"; - char temp[10]; - char *p = temp; - do { - *p++ = palette[value % 16]; - value /= 16; - } while (value > 0); - - int ret = p - temp; - - do { - *dst++ = *--p; - } while (p != temp); - - return ret; + return (HttpResponseData *) Super::getExt(); } /* Write an unsigned 32-bit integer in hex */ void writeUnsignedHex(unsigned int value) { char buf[10]; - int length = u32toaHex(value, buf); + int length = utils::u32toaHex(value, buf); /* For now we do this copy */ - AsyncSocket::write(buf, length); - } - - int u32toa(uint32_t value, char *dst) { - char temp[10]; - char *p = temp; - do { - *p++ = (char) (value % 10) + '0'; - value /= 10; - } while (value > 0); - - int ret = p - temp; - - do { - *dst++ = *--p; - } while (p != temp); - - return ret; + Super::write(buf, length); } /* Write an unsigned 32-bit integer */ void writeUnsigned(unsigned int value) { char buf[10]; - int length = u32toa(value, buf); + int length = utils::u32toa(value, buf); /* For now we do this copy */ - AsyncSocket::write(buf, length); + Super::write(buf, length); } public: @@ -77,59 +44,61 @@ public: HttpResponseData *httpResponseData = getHttpResponseData(); /* Do not allow writing more than one status */ - if (httpResponseData->state & HttpResponseData::HTTP_STATUS_SENT) { + if (httpResponseData->state & HttpResponseData::HTTP_STATUS_CALLED) { return this; } /* Update status */ - httpResponseData->state |= HttpResponseData::HTTP_STATUS_SENT; + httpResponseData->state |= HttpResponseData::HTTP_STATUS_CALLED; - AsyncSocket::write("HTTP/1.1 ", 9); - AsyncSocket::write(status.data(), status.length()); - AsyncSocket::write("\r\n", 2); + Super::write("HTTP/1.1 ", 9); + Super::write(status.data(), status.length()); + Super::write("\r\n", 2); return this; } /* Write an HTTP header with string value */ HttpResponse *writeHeader(std::string_view key, std::string_view value) { - AsyncSocket::write(key.data(), key.length()); - AsyncSocket::write(": ", 2); - AsyncSocket::write(value.data(), value.length()); - AsyncSocket::write("\r\n", 2); + Super::write(key.data(), key.length()); + Super::write(": ", 2); + Super::write(value.data(), value.length()); + Super::write("\r\n", 2); return this; } /* Write an HTTP header with unsigned int value */ HttpResponse *writeHeader(std::string_view key, unsigned int value) { - AsyncSocket::write(key.data(), key.length()); - AsyncSocket::write(": ", 2); + Super::write(key.data(), key.length()); + Super::write(": ", 2); writeUnsigned(value); - AsyncSocket::write("\r\n", 2); + Super::write("\r\n", 2); return this; } /* End the response with an optional data chunk */ void end(std::string_view data = {}) { + writeStatus(HTTP_200_OK); + HttpResponseData *httpResponseData = getHttpResponseData(); if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { /* Do not allow sending 0 chunk here */ if (data.length()) { - AsyncSocket::write("\r\n", 2); + Super::write("\r\n", 2); writeUnsignedHex(data.length()); - AsyncSocket::write("\r\n", 2); - AsyncSocket::write(data.data(), data.length()); + Super::write("\r\n", 2); + Super::write(data.data(), data.length()); } /* Terminating 0 chunk */ - AsyncSocket::write("\r\n0\r\n\r\n", 7); + Super::write("\r\n0\r\n\r\n", 7); } else { /* We have a known send size */ - AsyncSocket::write("Content-Length: ", 16); + Super::write("Content-Length: ", 16); writeUnsigned(data.length()); - AsyncSocket::write("\r\n\r\n", 4); + Super::write("\r\n\r\n", 4); - AsyncSocket::write(data.data(), data.length()); + Super::write(data.data(), data.length()); } } @@ -147,94 +116,77 @@ public: httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; } - AsyncSocket::write("\r\n", 2); + Super::write("\r\n", 2); writeUnsignedHex(data.length()); - AsyncSocket::write("\r\n", 2); - AsyncSocket::write(data.data(), data.length()); + Super::write("\r\n", 2); + Super::write(data.data(), data.length()); // are we corked still? return true; } - // we really want tryEnd(data) integer to try and stream something with known size - - // write/tryWrite called first should enter into chunked? - - // tryWrite(char *, length) int - - // write(char *, length) bool - - - - /* Attach an output stream function. Chunks may be read more than once. Negative offset mean broken stream */ - void writeOldRemoveMe(std::function(int)> cb, int length = 0) { - HttpResponseData *httpResponseData = getHttpResponseData(); - - /* Do not allow write if already called */ - if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { - return; - } - - /* Write 200 OK if not already written any status */ + // todo: share this code in a function + bool tryEnd(std::string_view data, int totalSize = 0) { + /* Write status if not already done */ writeStatus(HTTP_200_OK); - /* Update status */ - httpResponseData->state |= HttpResponseData::HTTP_WRITE_CALLED; - if (length) { - httpResponseData->state |= HttpResponseData::HTTP_KNOWN_STREAM_OUT_SIZE; - - /* Rely on FIN to signal end if we do not pass any length */ - AsyncSocket::write("Content-Length: ", 16); - writeUnsigned(length); - AsyncSocket::write("\r\n", 2); + /* If no total size given then assume this chunk is everything */ + if (!totalSize) { + totalSize = data.length(); } - /* HTTP body separator */ - AsyncSocket::write("\r\n", 2); + HttpResponseData *httpResponseData = getHttpResponseData(); - for (int offset = 0; length == 0 || offset < length; ) { - /* Pull a chunk from stream */ - auto [msg_more, chunk] = cb(offset); + if (httpResponseData->state & HttpResponseData::HTTP_WRITE_CALLED) { + /* Do not allow sending 0 chunk here */ + if (data.length()) { + Super::write("\r\n", 2); + writeUnsignedHex(data.length()); + Super::write("\r\n", 2); - /* Handle PAUSE and FIN */ - if (chunk.length() == 0) { - /* FIN */ - if (chunk.data()) { - /* Try flush and shut down */ - AsyncSocket::uncork(); - if (!AsyncSocket::hasBuffer()) { - /* Uncork finished with no buffered data */ - AsyncSocket::shutdown(); - } else { - /* Let it shut down when drained */ - httpResponseData->state |= HttpResponseData::HTTP_ENDED_STREAM_OUT; - } - } else { - /* Disable timeout and mark this stream as paused (important to silence spurious onWritable events) */ - AsyncSocket::timeout(0); - httpResponseData->state |= HttpResponseData::HTTP_PAUSED_STREAM_OUT; - - // forgot about this path, if we sent things off and then ended up pausing! - httpResponseData->offset = offset; - httpResponseData->outStream = cb; - } - return; + // should be optional + Super::write(data.data(), data.length()); } - /* Send off the chunk */ - int written = AsyncSocket::write(chunk.data(), chunk.length(), true); + /* Terminating 0 chunk */ + Super::write("\r\n0\r\n\r\n", 7); + } else { + // if not already ended! should we call tryWrite after this? we need an extra flag! end called! - /* If we failed to send everything, exit */ - if (written < chunk.length()) { - httpResponseData->offset = offset + written; - httpResponseData->outStream = cb; - return; + if (!(httpResponseData->state & HttpResponseData::HTTP_END_CALLED)) { + /* We have a known send size */ + Super::write("Content-Length: ", 16); + writeUnsigned(data.length()); + Super::write("\r\n\r\n", 4); + + /* Mark end called */ + httpResponseData->state |= HttpResponseData::HTTP_END_CALLED; } - offset += written; + /* Write as much as possible without causing backpressure */ + httpResponseData->offset += Super::write(data.data(), data.length(), true); } + + return httpResponseData->offset == totalSize; } + /* Attach handler for writable HTTP response */ + HttpResponse *onWritable(std::function handler) { + HttpResponseData *httpResponseData = getHttpResponseData(); + + httpResponseData->onWritable = handler; + return this; + } + + /* Attach handler for aborted HTTP request */ + HttpResponse *onAborted(std::function handler) { + HttpResponseData *httpResponseData = getHttpResponseData(); + + httpResponseData->onAborted = handler; + return this; + } + + // onData(chunk, remaining == -1 or 0 or actual remaining)? /* Attach a read handler for data sent. Will be called with a chunk of size 0 when FIN */ void read(std::function handler) { HttpResponseData *data = getHttpResponseData(); diff --git a/src/HttpResponseData.h b/src/HttpResponseData.h index 1a4be6a..949e675 100644 --- a/src/HttpResponseData.h +++ b/src/HttpResponseData.h @@ -16,15 +16,15 @@ struct HttpResponseData : HttpParser, AsyncSocketData { private: /* Bits of status */ enum { - HTTP_STATUS_SENT = 1, // used + HTTP_STATUS_CALLED = 1, // used HTTP_WRITE_CALLED = 2, // used - HTTP_KNOWN_STREAM_OUT_SIZE = 4, // not used + HTTP_END_CALLED = 4, // used HTTP_PAUSED_STREAM_OUT = 8, // not used HTTP_ENDED_STREAM_OUT = 16 // not used }; /* Per socket event handlers */ - std::function onWritable; + std::function onWritable; //std::function onData; std::function inStream; diff --git a/src/Utilities.h b/src/Utilities.h new file mode 100644 index 0000000..0dd0390 --- /dev/null +++ b/src/Utilities.h @@ -0,0 +1,49 @@ +#ifndef UTILITIES_H +#define UTILITIES_H + +/* Various common utilities */ + +#include + +namespace uWS { +namespace utils { + +int u32toaHex(uint32_t value, char *dst) { + char palette[] = "0123456789abcdef"; + char temp[10]; + char *p = temp; + do { + *p++ = palette[value % 16]; + value /= 16; + } while (value > 0); + + int ret = p - temp; + + do { + *dst++ = *--p; + } while (p != temp); + + return ret; +} + +int u32toa(uint32_t value, char *dst) { + char temp[10]; + char *p = temp; + do { + *p++ = (char) (value % 10) + '0'; + value /= 10; + } while (value > 0); + + int ret = p - temp; + + do { + *dst++ = *--p; + } while (p != temp); + + return ret; +} + +} +} + +#endif // UTILITIES_H