IronsDu / brynet

A Header-Only cross-platform C++ TCP network library . We can use vcpkg(https://github.com/Microsoft/vcpkg/tree/master/ports/brynet) install brynet.
MIT License
1.05k stars 241 forks source link

multipart/form-data support #111

Open hrnozel opened 2 years ago

hrnozel commented 2 years ago

I am planning to provide mjpeg stream over HTTP in my project. Briefly, I need to send each frame as jpeg over http but did not see anything about setting body to multipart or there is not a setBody function or its overloads that take byte(unsigned char) as the argument. All of them want std::string as the argument.

IronsDu commented 2 years ago
  1. you can set content type by this:
    HttpResponse response;
    response.setContentType("multipart/form-data");
  2. and you can write the jpeg to response by this:
    std::string content;
    readJpeg(content);
    response.setBody(content);

in fact, std::string is binary, so it can represent the unsigned char array;

hrnozel commented 2 years ago

Thank you for your answer. I will let you know when I try this. After, maybe add an example related with this topic.

hrnozel commented 2 years ago

I tried something, but it is not working as multipart stream. If I send request for each frame working perfectly, but if I run as below it does not work.

// Callback for http request
    auto httpEnterCallback = [&, this](const HTTPParser& httpParser,
        const HttpSession::Ptr& session)
    {
        HttpResponse firstResponse;

        firstResponse.setContentType("multipart/x-mixed-replace; boundary=mjpegstream");
        firstResponse.addHeadValue("Connection", "close");
        firstResponse.addHeadValue("max-ages", "0");
        firstResponse.addHeadValue("expires", "0");
        firstResponse.addHeadValue("Cache-Control", "no-cache, private");   
        firstResponse.addHeadValue("Pragma", "no-cache");

        session->send(firstResponse.getResult());

        while(true)
        {
            std::this_thread::sleep_for(100ms);
            std::lock_guard<std::mutex> lock(m_mutex);
            HttpResponse httpResponse;
            std::string response{};

            // ...
            // Prepare frame to client
            if (!m_buffer.empty())
            {
                spdlog::info(m_buffer.size());
                httpResponse.setBody(std::move(m_buffer.front()));
                m_buffer.pop();
            }

            // ...
            httpResponse.setContentType("image/jpeg");

            session->send(httpResponse.getResult("--mjpegstream"));

        }
    };

Also my mini html snippet:

<html>
  <body>
    <img src="http://127.0.0.1:8082">
  </body>
</html>

BTW I needed to little change on HttpResponse::getResult, because should be added "boundary" attribute to stream part after sending header. Also browser only waits in pending status meanwhile.

Addition, this repo is working mjpegwriter as well.

IronsDu commented 2 years ago
firstResponse.addHeadValue("Connection", "close");

maby you can't setting close Connection.

BTW , you can use chrome F12 network for debug HTTP response.

hrnozel commented 2 years ago

firstResponse.addHeadValue("Connection", "close"); I have removed this header but does not work.

I have debugged mjpegwriter and your library with wireshark. mjpegwriter sends the header;

firstResponse.setContentType("multipart/x-mixed-replace; boundary=mjpegstream"); firstResponse.addHeadValue("Connection", "close"); firstResponse.addHeadValue("max-ages", "0"); firstResponse.addHeadValue("expires", "0"); firstResponse.addHeadValue("Cache-Control", "no-cache, private"); firstResponse.addHeadValue("Pragma", "no-cache");

So the browser passes from pending status to stream. But in your library, If keep to stream in while loop, httpSession did not send either this first header or stream. So if the browser does not get this header firstly which stays in pending status.

If I remove this while stream loop;

`while(true) { std::this_thread::sleep_for(100ms); std::lock_guard lock(m_mutex); HttpResponse httpResponse; std::string response{};

        // ...
        // Prepare frame to client
        if (!m_buffer.empty())
        {
            spdlog::info(m_buffer.size());
            httpResponse.setBody(std::move(m_buffer.front()));
            m_buffer.pop();
        }

        // ...
        httpResponse.setContentType("image/jpeg");

        session->send(httpResponse.getResult("--mjpegstream"));

    }`

Your library sends header to browser and it passes from pending status to stream like mjpegwriter. But I could send stream to browser :/

IronsDu commented 2 years ago

Is that the question? Only the first response can carry the HTTP header, and the subsequent responses should not carry headers.

hrnozel commented 2 years ago

subsequent responses already do not have a header they begin with boundary attribute and remained part is payload(jpeg).

The issue is that the first response could not be sent to the browser.

IronsDu commented 2 years ago

Oh, This httpEnterCallback is called in network thread, so this callback can't be while loop;

hrnozel commented 2 years ago

How can I send my stream? Is there a way?

Or can I use the tcp part of your library? Is it's callback call from another thread too?

IronsDu commented 2 years ago

Send data in another thread;

IronsDu commented 2 years ago

Of course, we can send data in the callback, but the callback can't do something while loop;

IronsDu commented 2 years ago

For use another thread, we can start up one main EventLoop;

hrnozel commented 2 years ago

I have looked your examples but I could not figure out how use mainloop in that my case.

IronsDu commented 2 years ago

Or, you can start up one std::thread;

hrnozel commented 2 years ago

I guess you mention this example; https://github.com/IronsDu/brynet/blob/master/examples/BroadCastServer.cpp

If I implement by using this example, I guess I will be rewrapped the http part. So, but I still could not figure out that how to use it with main loop or std::thread. Can you share the code snippet or example?

BTW I am trying to use only http part of your library. I do not want to use other parts as far as possible. Of course, I can use it If you say that should use tcp part, the http part is not able to do this.

IronsDu commented 2 years ago

Don't need rewrapped the http part; You must send data in another thread, it's only because your logic code is while loop;

hrnozel commented 2 years ago

What do you mean another thread? How can explain with a code snippet?

IronsDu commented 2 years ago
auto httpEnterCallback = [&, this](const HTTPParser& httpParser,
    const HttpSession::Ptr& session)
{
    HttpResponse firstResponse;

    firstResponse.setContentType("multipart/x-mixed-replace; boundary=mjpegstream");
    firstResponse.addHeadValue("Connection", "close");
    firstResponse.addHeadValue("max-ages", "0");
    firstResponse.addHeadValue("expires", "0");
    firstResponse.addHeadValue("Cache-Control", "no-cache, private");   
    firstResponse.addHeadValue("Pragma", "no-cache");

    session->send(firstResponse.getResult());

    std::thread([=]() {
                    while(true)
            {
                std::this_thread::sleep_for(100ms);
                std::lock_guard<std::mutex> lock(m_mutex);
                HttpResponse httpResponse;
                std::string response{};

                // ...
                // Prepare frame to client
                if (!m_buffer.empty())
                {
                    spdlog::info(m_buffer.size());
                    httpResponse.setBody(std::move(m_buffer.front()));
                    m_buffer.pop();
                }

                // ...
                httpResponse.setContentType("image/jpeg");

                session->send(httpResponse.getResult("--mjpegstream"));

            }
            );
};
hrnozel commented 2 years ago

That your example code not working, it is crashing when creating the thread in the callback. Do you have an idea about that?

BTW, Library still does not send the first header which is before while loop.

hrnozel commented 2 years ago

From what I've debugged, it looks like httpsession is trying to destroy the thread created in the callback.

hrnozel commented 2 years ago

I have found another issue; httpSession cant send sequential responses for a request. I think this is due to running in event loop.

IronsDu commented 2 years ago

You can give me your project, I have a try.

hrnozel commented 2 years ago

I viewed your broadcast server example, and I changed my code like this. And now it is working as properly.

void MyClass::addClient(const brynet::net::http::HttpSession::Ptr& client)
{
    m_sessions.push_back(client);
}

void MyClass::dispatchFrame()
{
    while (true)
    {
            std::this_thread::sleep_for(10ms);
            HttpResponse httpResponse;

            // ...
            // Prepare frame to client
            if (!m_buffer.empty())
            {
                {
                    std::lock_guard<std::mutex> lock(m_mutex);
                    spdlog::info(m_buffer.size());
                    httpResponse.setBody(std::move(m_buffer.front()));
                    m_buffer.pop();
                }

                // ...
                httpResponse.setContentType("image/jpeg");
                for(auto i : m_sessions)
                {
                        i->send(httpResponse.getResult("--mjpegstream"));
                }
        }

    }
}

void MyClass::listenHTTP()
{
    // ...
    auto service = TcpService::Create();
    service->startWorkerThread(std::thread::hardware_concurrency());

    auto mainLoop = std::make_shared<EventLoop>();

    // Callback for http request
    auto httpEnterCallback = [&, this](const HTTPParser& httpParser,
        const HttpSession::Ptr& session)
    {
        HttpResponse firstResponse;

        firstResponse.setContentType("multipart/x-mixed-replace; boundary=mjpegstream");
        firstResponse.addHeadValue("Connection", "close");
        firstResponse.addHeadValue("max-ages", "0");
        firstResponse.addHeadValue("expires", "0");
        firstResponse.addHeadValue("Cache-Control", "no-cache, private");   
        firstResponse.addHeadValue("Pragma", "no-cache");

        session->send(firstResponse.getResult());

        mainLoop->runAsyncFunctor([&, session]() {
            addClient(session);
            });
    };

    wrapper::HttpListenerBuilder httpListenBuilder;

    unsigned int httpServerPort{ 8082 };

    try
    {
        httpListenBuilder
            .WithService(service)
            .AddSocketProcess([](TcpSocket& socket) {
            socket.setNodelay();
                })
            .WithMaxRecvBufferSize(8192)
                    .WithAddr(false, "127.0.0.1", httpServerPort)
                    .WithReusePort()
                    .WithEnterCallback([httpEnterCallback]
                    (const HttpSession::Ptr& httpSession, HttpSessionHandlers& handlers) {
                            handlers.setHttpCallback(httpEnterCallback);
                        })
                    .asyncRun();
    }
    catch (const brynet::net::BrynetCommonException& e)
    {
        // ...
        std::string exceptionMessage("HTTP Server Error: could not bind to given port. ");
    }

    // Infinite loop for connection
    while (true)
    {
        mainLoop->loop(10);
        std::this_thread::sleep_for(1s);
        if (brynet::base::app_kbhit())
        {
            break;
        }
    }
}

BTW I had to make changes on HttpResponse::getResult like below;

 std::string getResult(const std::string& firstLine = {}) const
    {
        std::string ret;

        if(firstLine.empty())
        {
            ret = "HTTP/1.1 ";
            ret += std::to_string(static_cast<int>(mStatus));
            switch (mStatus)
            {
                case HTTP_RESPONSE_STATUS::OK:
                    ret += " OK";
                    break;
                default:
                    ret += "UNKNOWN";
                    break;
            }
        }
        else
        {
          ret = firstLine;

        }

        ret += "\r\n";

        for (auto& v : mHeadField)
        {
            ret += v.first;
            ret += ": ";
            ret += v.second;
            ret += "\r\n";
        }

        ret += "\r\n";

        if (!mBody.empty())
        {
            ret += mBody;
        }

        return ret;
    }

I made those changes, you know that I need send boundary attribute as header for sequential responses.

So if it is suitable I can send PR with an example.