Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yngndrw opened this issue Jan 25, 2024 · 0 comments
Labels
topic: code Related to content of the project itself type: imperfection Perceived defect in any part of project

Comments

@yngndrw
Copy link

yngndrw commented Jan 25, 2024

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)

  • Update 1
    • Network request 1 - Connect times out (Socket index = max)
    • Network request 2 - Connect succeeds (Socket index = 0) and state is progressed to State::AwaitingResponse
  • Internally:
    • Socket index 0 request has now been completed, the response data is now in its receive buffer and the connection is closed by the server
  • Update 2
    • Network request 1 - Connect times out (Socket index = max) - During this Ethernet.socketBegin() will try to use the first closed socket (Socket index 0) and appears to clear the receive buffer)
    • Network request 2 - Tries to read its data but the buffer is empty :(

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;
};
@per1234 per1234 added topic: code Related to content of the project itself type: imperfection Perceived defect in any part of project labels Jan 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: code Related to content of the project itself type: imperfection Perceived defect in any part of project
Projects
None yet
Development

No branches or pull requests

2 participants