arduino-libraries / Ethernet

Ethernet Library for Arduino
http://arduino.cc/
259 stars 264 forks source link

EthernetClient - Closed socket with data to be read is cleared by client.connect() #249

Open yngndrw opened 10 months ago

yngndrw commented 10 months ago

Hello,

I have a program where I need to connect to multiple servers and want to handle requests asynchronously.

I've created a NetworkRequest class (See below) which is essentially a state machine with its own EthernetClient, responsible for handling a request and its response. I have a NetworkRequest instance for each server which I need to connect to. (In this case, two)

I call the Update() method of each NetworkRequest instance every frame.

Right now the first of the servers doesn't exist so it's expected to fail to connect to that IP. My issue is that the connection attempt of the first NetworkRequest instance causes the second EthernetClient's received data buffer to be cleared before it gets read.

Essentially: (Assume both NetworkRequest instances have outstanding requests and are therefore in State::SendingRequest)

My workaround for this is to do two separate Update() loops - The first updates any NetworkRequest instances which are in the State::AwaitingResponse state and the second then updates any NetworkRequest instances which are in the State::SendingRequest state.

Am I missing something here? It would be nice if Ethernet.socketBegin() would rotate through the unused sockets, or even if we could reserve a specific socket index for an EthernetClient instance.

#pragma once

#include <Arduino.h>
#include <Base64.h>
#include <EthernetClient.h>
#include "..\constants.h"
#include "key-helper.h"

// Example Requests:
//
// GET /path HTTP/1.1
// Authorization: Basic ...
//
// POST /path HTTP/1.1
// Authorization: Basic ...
// Content-Type: text/plain
// Content-Length: 4
//
// data
//
class NetworkRequest
{
public:
    enum class State
    {
        Idle,
        SendingRequest,
        AwaitingResponse,
        ReceivedResponse
    };

    enum class HttpMethod
    {
        Get,
        Post
    };

public:
    NetworkRequest()
    {
        _authorisation.reserve(70);

        _path.reserve(32);
        _queryString.reserve(32);
        _body.reserve(64);
        _response.reserve(512);
        Reset();
    }

    ~NetworkRequest()
    {
        _client.stop();
    }

    void Reset()
    {
        _state = State::Idle;

        _httpMethod = HttpMethod::Get;
        _path = "";
        _queryString = "";
        _body = "";

        _ignoreResponse = false;
        _requestStartTimeMilliseconds = 0;
        _success = false;
        _response = "";
    }

    void SetServer(IPAddress ipAddress, uint16_t port = 80)
    {
        _ipAddress = ipAddress;
        _port = port;

        _client.stop();
        Reset();
    }

    void SetCredentials(String username, String password)
    {
        _authorisation = BuildBasicAuthorisation(username, password);

        _client.stop();
        Reset();
    }

    void SetMethod(HttpMethod httpMethod)
    {
        if (IsRequestInFlight())
        {
            return;
        }

        _httpMethod = httpMethod;
    }

    void SetPath(String path)
    {
        if (IsRequestInFlight())
        {
            return;
        }

        _path = path;
    }

    void SetQueryString(String queryString)
    {
        if (IsRequestInFlight())
        {
            return;
        }

        _queryString = queryString;
    }

    void SetTextBody(String body)
    {
        if (IsRequestInFlight())
        {
            return;
        }

        _body = body;
    }

    bool Send(bool ignoreResponse = false)
    {
        if (!IsClientSetup() || IsRequestInFlight())
        {
            return false;
        }

        _state = State::SendingRequest;
        _ignoreResponse = ignoreResponse;

        return true;
    }

    State GetState() const
    {
        return _state;
    }

    void Update(uint32_t currentTimeMilliseconds)
    {
        bool noHardware = Ethernet.hardwareStatus() == EthernetNoHardware;
        bool noLink = Ethernet.linkStatus() != LinkON;
        if (noHardware || noLink)
        {
            return;
        }

        if (!_ipAddress.isSet())
        {
            return;
        }

        switch (_state)
        {
        case State::Idle:
        case State::ReceivedResponse:
        default:
            return;

        case State::SendingRequest:
            if (!_client.connected())
            {
                if (_client.connect(_ipAddress, _port) < 1)
                {
                    return;
                }
            }

            switch (_httpMethod)
            {
            case HttpMethod::Get:
            default:
                _client.print("GET");
                break;

            case HttpMethod::Post:
                _client.print("POST");
                break;
            }
            _client.print(" ");
            if (_path.length() > 0)
            {
                if (_path[0] != '/')
                {
                    _client.print("/");
                }
                _client.print(_path);
            }
            else
            {
                _client.print("/");
            }
            _client.println(" HTTP/1.1");

            if (_authorisation.length() > 0)
            {
                _client.print("Authorization: ");
                _client.println(_authorisation);
            }

            switch (_httpMethod)
            {
            case HttpMethod::Get:
            default:
                break;

            case HttpMethod::Post:
                _client.print("Content-Type: ");
                _client.println(_contentType);
                _client.print("Content-Length: ");
                _client.println(_body.length());
                break;
            }

            _client.println();

            if (_body.length() > 0)
            {
                _client.println(_body);
            }

            _client.flush();

            _state = _ignoreResponse ? State::Idle : State::AwaitingResponse;
            if (_ignoreResponse)
            {
                _success = true;
            }
            _requestStartTimeMilliseconds = currentTimeMilliseconds;

            return;

        case State::AwaitingResponse:
            if (_ignoreResponse)
            {
                _success = true;
                _state = State::Idle;
                return;
            }

            uint8_t buffer[EthernetBufferLength];
            int bytesRead = _client.read(buffer, EthernetBufferLength);
            if (bytesRead == 0)
            {
                // Connection closed
                _success = false;

                _state = State::Idle;
                return;
            }
            if (bytesRead == -1)
            {
                // No data available

                if (currentTimeMilliseconds > _requestStartTimeMilliseconds + _requestTimeoutMilliseconds)
                {
                    // Timeout
                    _success = false;

                    _state = State::Idle;
                    return;
                }

                if (!_client.connected())
                {
                    // Connection closed
                    _success = false;

                    _state = State::Idle;
                    return;
                }

                return;
            }

            _response += String(buffer, bytesRead);

            // todo - temp

            int endOfHeadersIndex = _response.indexOf("\r\n\r\n");
            _responseBody = _response.substring(endOfHeadersIndex + 4);

            // todo - check for the end of the response
            _success = true;
            _state = State::ReceivedResponse;
            // todo - temp

            return;
        }
    }

    bool IsClientSetup() const
    {
        return _ipAddress.isSet();
    }

    bool IsRequestInFlight() const
    {
        switch (_state)
        {
        case State::SendingRequest:
        case State::AwaitingResponse:
            return true;
        }

        return false;
    }

    bool IsResponseReady() const
    {
        return _state == State::ReceivedResponse;
    }

    bool WasRequestSuccessful() const
    {
        return _success;
    }

    String GetResponse() const
    {
        return _response;
    }

    String GetResponseBody() const
    {
        return _responseBody;
    }

private:
    String BuildBasicAuthorisation(String username, String password) const
    {
        if (username.length() < 1 || password.length() < 1)
        {
            return "";
        }

        unsigned int decodedBufferSize = username.length() + password.length() + 2;
        char decodedBasicAuthorisationValue[decodedBufferSize];
        snprintf(decodedBasicAuthorisationValue, decodedBufferSize, "%s:%s", username.c_str(), password.c_str());

        unsigned int encodedBufferSize = Base64.encodedLength(decodedBufferSize - 1) + 1;
        char encodedBasicAuthorisationValue[encodedBufferSize];
        Base64.encode(encodedBasicAuthorisationValue, decodedBasicAuthorisationValue, decodedBufferSize);

        return _authorisationScheme + " " + encodedBasicAuthorisationValue;
    }

private:
    const uint32_t _requestTimeoutMilliseconds = 5000;

    const String _authorisationScheme = "Basic";
    const String _contentType = "text/plain";

    IPAddress _ipAddress;
    uint16_t _port = 80;
    String _authorisation = "";

    EthernetClient _client;
    State _state = State::Idle;

    HttpMethod _httpMethod = HttpMethod::Get;
    String _path;
    String _queryString;
    String _body;

    bool _ignoreResponse = false;
    uint32_t _requestStartTimeMilliseconds = 0;
    bool _success = false;
    String _response;
    String _responseBody;
};