English Русский Deutsch
preview
Metatrader 5のWebsockets — Windows APIを使用した非同期クライアント接続

Metatrader 5のWebsockets — Windows APIを使用した非同期クライアント接続

MetaTrader 5 |
68 9
Francis Dube
Francis Dube

はじめに

MetaTrader 5のWebSocket — WindowsAPIの使用」では、Windows APIを利用してMetaTrader 5アプリケーション内にWebSocketクライアントを実装する方法を紹介しました。しかし、その実装は同期的な動作モードに制限されていました。

本記事では、Windows APIの活用を再検討し、MetaTrader 5プログラム向けに非同期クライアント機能を実現するWebSocketクライアントの構築を目的としています。この目標を達成するための実用的な方法として、MetaTrader 5アプリケーションと連携可能な関数をエクスポートするカスタムDLL(ダイナミックリンクライブラリ)の作成が挙げられます。

そのため、本記事ではDLLの開発プロセスについて説明し、最後にMetaTrader 5プログラムを用いた適用例を紹介します。


WinHTTP非同期モード

ドキュメントに明記されているように、WinHTTPライブラリにおける非同期操作の前提条件は2つあります。まず第一に、WinHttpOpen関数を呼び出す際、セッションハンドルをWINHTTP_FLAG_ASYNCまたはWINHTTP_FLAG_SECURE_DEFAULTSフラグのいずれかに設定しなければなりません。

// Set hSession
hSession = WinHttpOpen(L"MyApp", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
if (hSession == NULL)
        ErrorCode = ERROR_INVALID_HANDLE;
// Return error code
return ErrorCode;

有効なセッションハンドルが確立された後、ユーザーは特定のWinHTTP関数呼び出しに関連する各種イベントに関する通知を受け取るために、コールバック関数を登録する必要があります。このステータスコールバック関数は、通知フラグを通じて非同期操作の進行状況を通知します。

コールバック関数の登録はWinHttpSetStatusCallback関数によっておこなわれ、コールバックが処理する通知フラグの指定も可能です。ユーザーは、すべての通知を受け取るよう設定することも、一部の通知のみに限定することもできます。さらに、セッション、リクエスト、WebSocketの各ハンドルごとに、異なるコールバック関数を割り当てることも可能です。

なお、セッションハンドルの作成直後にコールバック関数を登録する必要は必ずしもありません。WinHttpSetStatusCallback関数は、任意の有効なHINTERNETハンドルに対して、WebSocket接続の初期化前または初期化中の任意の段階で呼び出すことができます。

if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
{
        // Handle error
        ErrorCode = GetLastError();
        return ErrorCode;
}

if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
{
        ErrorCode = GetLastError();
        return ErrorCode;
}

ユーザー定義のコールバック関数のシグネチャには、ユーザー定義のデータ構造体を指すパラメータ(コンテキスト値と呼ばれることが多い)が含まれています。この仕組みにより、コールバック関数からデータを渡すことが可能になります。

このコンテキスト値は、WinHttpSetOption関数をWINHTTP_OPTION_CONTEXT_VALUEオプションフラグとともに使用して、コールバック関数の登録前に設定する必要があります。しかしながら、実際のテストでは、この方法によって登録したコンテキスト値を安定して取得するのが困難であることが判明しました。

実装ミスの可能性を完全に否定することはできませんが、繰り返される失敗のため、代替手段としてグローバル変数の使用を採用しました。この詳細については、DLL実装の説明で後述します。

最後に、ユーザー定義のコールバック関数に関して重要な考慮点として、スレッドセーフである必要があるという点があります。ただし、本稿で扱うのはMetaTrader 5環境で使用されるDLLの作成であるため、この制約はある程度緩和されます。というのも、MetaTrader 5プログラムは基本的にシングルスレッドで動作する性質を持っており、DLLコードはロード元プロセスのスレッドプール内で実行されるものの、MetaTrader 5側ではアクティブなスレッドは常に1つのみであるためです。

void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)


DLLの実装

このDLLは、C++プログラミング言語を用いてVisual Studio環境内で構築されます。このプロセスには、Visual Studioにおける「C++デスクトップ開発」ワークロードのインストールが必要です。また、Windows 10またはWindows 11のソフトウェア開発キット(SDK)のいずれかもインストールしておく必要があります。Windows SDKは必須であり、DLLのコンパイル時にリンクされるWinHTTPライブラリファイル(.lib)を提供します。生成されるDLLは、少なくとも以下の3つの基本的なコンポーネントで構成されます。

1つ目は、WinHTTP WebSocketクライアントの基本機能をカプセル化するクラスです。2つ目は、グローバル変数と連携して動作する単一のコールバック関数(この構成により、コールバック関数のスコープ内および外部の両方からWebSocket接続を操作可能になります)です。3つ目は、MetaTrader 5プログラムで使用される簡易な関数ラッパー群で、これらはDLLによってエクスポートされます。実装は、asyncwebsocketclient.hヘッダーファイルに定義されたコードから始まります。

このヘッダーファイルでは、まずWebSocketClientクラスが宣言されており、その各インスタンスが個別のクライアント接続を表します。

// WebSocket client class
class WebSocketClient {
private:
        // Application session handle to use with this connection
        HINTERNET hSession;
        // Windows connect handle
        HINTERNET hConnect;
        // The initial HTTP request handle to start the WebSocket handshake
        HINTERNET hRequest;
        // Windows WebSocket handle
        HINTERNET hWebSocket;
        //initialization flag
        DWORD initialized;
        //sent bytes
        DWORD bytesTX;
        //last error code
        DWORD ErrorCode;
        //last completed websocket operation as indicated by callback function
        DWORD completed_websocket_operation;
        //internal queue of frames sent from a server
        std::queue<Frame>* frames;
        //client state;
        ENUM_WEBSOCKET_STATE status;
        //sets an hSession handle
        DWORD Initialize(VOID);
        // reset state of object
        /*
         reset_error: boolean flag indicating whether to rest the
         internal error buffers.
        */
        VOID  Reset(bool reset_error = true);
        
public:
        //constructor(s)
        WebSocketClient(VOID);
        WebSocketClient(const WebSocketClient&) = delete;
        WebSocketClient(WebSocketClient&&) = delete;
        WebSocketClient& operator=(const WebSocketClient&) = delete;
        WebSocketClient& operator=(WebSocketClient&&) = delete;
        //destructor
        ~WebSocketClient(VOID);
        //received bytes;
        DWORD bytesRX;
        // receive buffer
        std::vector<BYTE> rxBuffer;
        // received frame type;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE rxBufferType;
        // Get the winhttp websocket handle
        /*
        return: returns the hWebSocket handle which is used to 
        identify a websocket connection instance
        */
        HINTERNET WebSocketHandle(VOID);

        // Connect to a server
        /* 
           hsession: HINTERNET session handle
           host: is the url
           port: prefered port number to use
           secure: 0 is false, non-zero is true
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure);
        
        // Send data to the WebSocket server
        /*
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           pBuffer: pointer to the data to be sent
           dwLength: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Send(WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType, void* pBuffer, DWORD dwLength);

        // Close the connection to the server
        /*
           status: WINHTTP_WEB_SOCKET_CLOSE_STATUS enumeration of the close notification to be sent
           reason: character string of extra data sent with the close notification
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS status, CHAR* reason = NULL);

        // Retrieve the close status sent by a server
        /*
           pusStatus: pointer to a close status code that will be filled upon return.
           pvReason: pointer to a buffer that will receive a close reason
           dwReasonLength: The length of the pvReason buffer,
           pdwReasonLengthConsumed:The number of bytes consumed. If pvReason is NULL and dwReasonLength is 0,
       pdwReasonLengthConsumed will contain the size of the buffer that needs to be allocated
       by the calling application.
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD QueryCloseStatus(USHORT* pusStatus, PVOID pvReason, DWORD dwReasonLength, DWORD* pdwReasonLengthConsumed);

        // read from the server
        /*
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           pBuffer: pointer to the data to be sent
           pLength: size of pBuffer
           bytesRead: pointer to number bytes read from the server
           pBufferType: pointer to type of frame sent from the server
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Receive(PVOID pBuffer, DWORD pLength, DWORD* bytesRead, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);

        // Check client state
        /*
           return: ENUM_WEBSOCKET_STATE enumeration
        */
        ENUM_WEBSOCKET_STATE Status(VOID);

        // get frames cached in the internal queue
        /*
           pBuffer: User supplied container to which data is written to
           pLength: size of pBuffer
           pBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type 
        */
        VOID Read(BYTE* pBuffer, DWORD pLength, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);

        // get bytes received
        /*
           return: Size of most recently cached frame sent from a server
        */
        DWORD ReadAvailable(VOID);

        // get the last error
        /*
           return: returns the last error code
        */
        DWORD LastError(VOID);

        // activate callback function
        /*
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD EnableCallBack(VOID);
        // set error 
        /*
           message: Error description to be captured
           errorcode: new user defined error code
        */
        VOID SetError(const DWORD errorcode);
         
        // get the last completed operation
        /*
          returns: DWORD constant of last websocket operation
        */
        DWORD LastOperation(VOID);

        //deinitialize the session handle and free up resources
        VOID Free(VOID);

        //the following methods define handlers meant to be triggered by the callback function//
        
        // on error 
        /*
           result: pointer to WINHTTP_ASYNC_RESULT structure that 
           encapsulates the specific event that triggered the error
        */
        VOID OnError(const WINHTTP_ASYNC_RESULT* result);

        // read completion handler
        /*
           read: Number of bytes of data successfully read from the server
           buffertype: type of frame read-in.
           Called when successfull read is completed
        */
        VOID OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype);

        // websocket close handler
        /*
           Handles the a successfull close request
        */
        VOID OnClose(VOID);

        // Send operation handler
        /*
           sent: the number of bytes successfully sent to the server if any
           This is a handler for an asynchronous send that interacts with the
           callback function
        */
        VOID OnSendComplete(const DWORD sent);

        //set the last completed websocket operation 
        /*
          operation : constant defining the operation flagged as completed by callback function
        */
        VOID OnCallBack(const DWORD operation);
};

また、クラスと併せて、メッセージフレームを表す構造体「Frame」も定義されています。

struct Frame
{
        std::vector<BYTE>frame_buffer;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type;
        DWORD frame_size;
};

さらに、Websocket接続のさまざまな状態を記述するために、列挙体ENUM_WEBSOCKET_STATEが宣言されています。

// client state
enum ENUM_WEBSOCKET_STATE
{
        CLOSED = 0,
        CLOSING = 1,
        CONNECTING = 2,
        CONNECTED = 3,
        SENDING = 4,
        POLLING = 5
};

続いて、asyncwebsocketclient.hでは、clientsという名前のグローバル変数が宣言されます。この変数はアクティブなWebSocket接続を保持するためのコンテナであり、具体的にはmapコンテナです。このmapコンテナがグローバルスコープで宣言されていることで、ライブラリ内で定義された任意のコールバック関数からアクセス可能になります。

// container for  websocket objects accessible to callback function
extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;

asyncwebsocketclient.hファイルの最後では、WEBSOCK_API修飾子が付与された一連の関数が定義されています。この修飾子は、これらの関数をDLLからエクスポートするための目印として機能します。これらの関数は前述の関数ラッパーに該当し、MetaTrader 5アプリケーション内で開発者がDLLとやり取りをおこなう際のインターフェイスとなります。

// deinitializes a session handle
/*
   websocket_handle: HINTERNET the websocket handle to close
*/
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle);

//creates a client connection to a server
/*   
           url: the url of the server
           port: port
           secure: use secure connection(non-zero) or not (zero)
           websocket_handle: in-out,HINTERNET non NULL session handle
           return: returns DWORD, zero if successful or non-zero on failure
           
*/

DWORD  WEBSOCK_API client_connect(const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocket_handle);

//destroys a client connection to a server
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
*/

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle);

//writes data to a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           message: pointer to the data to be sent
           length: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length);

//reads data sent from a server cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           out: User supplied container to which data is written to
           out_size: size of out buffer
           buffertype: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type 
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype);

//listens for a response from a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle);

//gets the last generated error
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           lasterror: container that will hold the error description
           length: the size of lasterror container
           lasterrornum: reference to which the last error code is written
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle);

//checks whether there is any data cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: returns the size of the last received frame in bytes;
*/

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle);

//return the state of a websocket connection
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: ENUM_WEBSOCKET_STATE enumeration of the state of a client
*/

ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle);

//return the last websocket operation
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : DWORD constant corresponding to a unique callback status value as defined in API    
*/
DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle);

//return the websocket handle
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : HINTERNET returns the websocket handle for a client connection 
*/
HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle);

WebSocketCallback関数の定義を確認すると、通知の管理にグローバル変数「clients」が利用されていることがわかります。現在の実装は、WinHTTPのドキュメントで「完了通知」と呼ばれるものを処理するように設定されています。これらの通知は、非同期操作が正常に完了した際に発生します。たとえば、WINHTTP_CALLBACK_STATUS_WRITE_COMPLETEは送信操作の完了を、WINHTTP_CALLBACK_STATUS_READ_COMPLETEは受信操作の完了を示します。

void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
        if (WinHttpWebSocketClient::clients.find(hInternet)!= WinHttpWebSocketClient::clients.end())
        {
                WinHttpWebSocketClient::clients[hInternet]->OnCallBack(dwInternetStatus);
                switch (dwInternetStatus)
                {
                case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnClose();
                        break;

                case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnSendComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred);
                        break;

                case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnReadComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred, ((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->eBufferType);
                        break;

                case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
                        WinHttpWebSocketClient::clients[hInternet]->OnError((WINHTTP_ASYNC_RESULT*)lpvStatusInformation);
                        break;

                default:
                        break;
                }
        }

}

コールバック関数のhInternet引数は、コールバックが最初に登録されたHINTERNETハンドルを表します。このハンドルを使って、グローバルのmapコンテナであるclientsから対応するメンバーを検索し、WebSocketClientインスタンスへのポインタを取得します。具体的なコールバック通知はdwInternetStatus引数によって伝えられます。なお、lpvStatusInformationおよびdwStatusInformationLength引数に含まれるデータは、dwInternetStatusの値に応じて異なります。

たとえば、dwInternetStatusがWINHTTP_CALLBACK_STATUS_READ_COMPLETEまたはWINHTTP_CALLBACK_STATUS_WRITE_COMPLETEの場合、lpvStatusInformationにはWINHTTP_WEB_SOCKET_STATUS構造体へのポインタが格納されており、dwStatusInformationLengthはそのデータのサイズを示します。

本実装では、このコールバック関数は、提供される通知の一部を選択的に処理し、それに応じて対応するWebSocketClientインスタンスの状態を変更します。具体的には、OnCallBackメソッドがこれら通知に関連するステータスコードを取得し、その情報はWebSocketClientインスタンス内に保持され、ラッパー関数を通じてユーザーに公開されます。

VOID WebSocketClient::OnCallBack(const DWORD operation)
{
        completed_websocket_operation = operation;
}

WebSocketClientクラス内のOnReadCompleteメソッドは、生データをフレームのキューバッファに転送する役割を担っており、ユーザーはそこから後にデータの取得可能性を問い合わせることができます。

VOID WebSocketClient::OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype)
{
        bytesRX = read;
        rxBufferType = buffertype;
        status = ENUM_WEBSOCKET_STATE::CONNECTED;
        Frame frame;
        frame.frame_buffer.insert(frame.frame_buffer.begin(), rxBuffer.data(), rxBuffer.data() + read);
        frame.frame_type = buffertype;
        frame.frame_size = read;
        frames->push(frame);
}

OnSendCompleteメソッドは、送信操作が成功したことを示す内部フィールドを更新し、それに伴いWebSocketクライアントの状態変化を引き起こします。

VOID WebSocketClient::OnSendComplete(const DWORD sent)
{
        bytesTX = sent;
        status = ENUM_WEBSOCKET_STATE::CONNECTED;
        return;
}

最後に、OnErrorメソッドは、lpvStatusInformation引数を通じて提供されるエラー関連の情報を受け取って処理します。

VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result)
{
        SetError(result->dwError);
        Reset(false);
}

サーバーへの接続は、client_connect関数によっておこなわれます。この関数は、サーバーアドレス(文字列)、ポート番号(整数)、接続を安全にするかどうかを示すブール値、そしてHINTERNET型のポインタを引数として呼び出されます。関数はDWORD型の値を返し、同時にHINTERNET引数に値を設定します。接続処理中にエラーが発生した場合は、HINTERNET引数がNULLに設定され、ゼロ以外のエラーコードが返されます。

内部では、client_connect関数はWebSocketClientクラスのインスタンスを初期化し、それを用いて接続を確立します。接続が正常に完了すると、このWebSocketClientインスタンスへのポインタがグローバルのclientsコンテナに格納され、インスタンスのWebSocketハンドルが一意のキーとして使われます。このWebSocketハンドルは、DLL内部の処理および外部の呼び出し元であるMetaTrader 5プログラム双方で、特定のWebSocket接続を一意に識別するために利用されます。

DWORD  WEBSOCK_API client_connect( const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocketp_handle)
{
        DWORD errorCode = 0;
        auto client = std::make_shared<WebSocketClient>();

        if (client->Connect(url, port, secure) != NO_ERROR)
                errorCode = client->LastError();
        else
        {
                HINTERNET handle = client->WebSocketHandle();
                if (client->EnableCallBack())
                {
                        errorCode = client->LastError();
                        client->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
                        client->Free();
                        handle = NULL;
                }
                else
                {
                        clients[handle] = client;
                        *websocketp_handle = handle;
                }
        }
        return errorCode;
}

WebSocketClientクラスのConnectメソッドがWebSocket接続の初期化と確立を管理します。接続処理は最初に同期的に実行され、その後、EnableCallBackメソッドを呼び出してWebSocketハンドルにコールバック関数を登録することで、非同期のイベント通知が可能になります。

DWORD WebSocketClient::Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure)
  {

   if((status != ENUM_WEBSOCKET_STATE::CLOSED))
     {
      ErrorCode = Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,NULL);
      return WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTING;


// Return 0 for success
   if(hSession == NULL)
     {
      ErrorCode = Initialize();
      if(ErrorCode)
        {
         Reset(false);
         return ErrorCode;
        }
     }

// Cracked URL variable pointers
   URL_COMPONENTS UrlComponents;

// Create cracked URL buffer variables
   std::unique_ptr <WCHAR> scheme(new WCHAR[0x20]);
   std::unique_ptr <WCHAR> hostName(new WCHAR[0x100]);
   std::unique_ptr <WCHAR> urlPath(new WCHAR[0x1000]);

   DWORD dwFlags = 0;
   if(secure)
      dwFlags |= WINHTTP_FLAG_SECURE;

   if(scheme == NULL || hostName == NULL || urlPath == NULL)
     {
      ErrorCode = ERROR_NOT_ENOUGH_MEMORY;
      Reset();
      return ErrorCode;
     }

// Clear error's
   ErrorCode = 0;

// Setup UrlComponents structure
   memset(&UrlComponents, 0, sizeof(URL_COMPONENTS));
   UrlComponents.dwStructSize = sizeof(URL_COMPONENTS);
   UrlComponents.dwSchemeLength = -1;
   UrlComponents.dwHostNameLength = -1;
   UrlComponents.dwUserNameLength = -1;
   UrlComponents.dwPasswordLength = -1;
   UrlComponents.dwUrlPathLength = -1;
   UrlComponents.dwExtraInfoLength = -1;

// Get the individual parts of the url
   if(!WinHttpCrackUrl(host, NULL, 0, &UrlComponents))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset();
      return ErrorCode;
     }

// Copy cracked URL hostName & UrlPath to buffers so they are separated
   if(wcsncpy_s(scheme.get(), 0x20, UrlComponents.lpszScheme, UrlComponents.dwSchemeLength) != 0 ||
      wcsncpy_s(hostName.get(), 0x100, UrlComponents.lpszHostName, UrlComponents.dwHostNameLength) != 0 ||
      wcsncpy_s(urlPath.get(), 0x1000, UrlComponents.lpszUrlPath, UrlComponents.dwUrlPathLength) != 0)
     {
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   if(port == 0)
     {
      if((_wcsicmp(scheme.get(), L"wss") == 0) || (_wcsicmp(scheme.get(), L"https") == 0))
        {
         UrlComponents.nPort = INTERNET_DEFAULT_HTTPS_PORT;
        }
      else
         if((_wcsicmp(scheme.get(), L"ws") == 0) || (_wcsicmp(scheme.get(), L"http")) == 0)
           {
            UrlComponents.nPort = INTERNET_DEFAULT_HTTP_PORT;
           }
         else
           {
            ErrorCode = ERROR_INVALID_PARAMETER;
            Reset(false);
            return ErrorCode;
           }
     }
   else
      UrlComponents.nPort = port;

// Call the WinHttp Connect method
   hConnect = WinHttpConnect(hSession, hostName.get(), UrlComponents.nPort, 0);
   if(!hConnect)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Create a HTTP request
   hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.get(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
   if(!hRequest)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Set option for client certificate

   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Add WebSocket upgrade to our HTTP request
#pragma prefast(suppress:6387, "WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET does not take any arguments.")
   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Send the WebSocket upgrade request.
   if(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 0, 0, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Receive response from the server
   if(!WinHttpReceiveResponse(hRequest, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Finally complete the upgrade
   hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL);
   if(hWebSocket == 0)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTED;

// Return should be zero
   return ErrorCode;
  }

DWORD WebSocketClient::EnableCallBack(VOID)
  {
   if(!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
     {
      // Handle error
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   if(WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
     {
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   return ErrorCode;
  }

サーバーへの接続が確立され有効なハンドルが取得された後、ユーザーはリモートエンドポイントとの通信を開始できます。サーバーへのデータ送信はclient_send関数の呼び出しによっておこなわれます。この関数には、有効なWebSocketハンドル、送信するWebSocketフレームの種類を指定するWINHTTP_WEB_SOCKET_BUFFER_TYPE列挙値、データペイロードを含むBYTE配列、およびデータ配列のサイズを示すulong引数が必要です。関数は即時のエラーがなければゼロを返し、エラーがある場合は特定のエラーコードを返します。

内部的には、WinHttpWebSocketSend関数が非同期的に呼び出されます。そのためclient_sendの返り値は送信操作のセットアップにおける初期的なエラーの有無を示す中間的なステータスであり、実際のデータ送信の結果は同期的に返されません。送信結果は登録されたコールバック関数を通じて非同期に通知されます。送信操作が成功した場合はWINHTTP_CALLBACK_STATUS_WRITE_COMPLETE通知が想定されます。一方、送信や受信のいずれかの操作中にエラーが発生した場合は、通常WINHTTP_CALLBACK_STATUS_REQUEST_ERROR通知がコールバック関数に伝達されます。

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->Send(buffertype, message, length);

   return out;
  }

内部コールバック関数が受け取った通知は、client_lastcallback_notification関数を使って取得できます。この関数は、唯一の引数として渡されたWebSocketハンドルで識別される特定の接続に対して、コールバックが最後に受信した通知を返します。以下のコードスニペットは、MetaTrader 5プログラム内でこれらの通知を処理する一例を示しています。これらの通知に対応するシンボリック定数は、元のwinhttp.hヘッダーファイルから派生したasyncwinhttp.mqhファイルに定義されています。

DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastOperation();

   return out;
  }

サーバーから送信されたデータを受信するには、まずclient_poll関数を呼び出してクライアントをポーリング状態に置く必要があります。この操作は内部的にWebSocketClientクラスのWinHttpWebSocketReceive関数を呼び出します。送信操作と同様に、このWinHTTP関数は非同期で呼び出されるため、中間的なステータスが即時に返されます。

WebSocketClientクラスは、到着した生データを格納するための内部バッファを備えています。読み取り操作が正常に完了すると、このデータは内部のデータ構造でキューに入れられます。この処理はWebSocketClientクラスのOnReadCompleteメソッドによって管理されます。読み取り完了後、WebSocket接続の状態は変化し、受信メッセージの待機を一時的に停止します。

つまり、非同期読み取り要求は連続的ではなく、持続的なポーリングを意味しません。サーバーからの次のメッセージを受け取るには、再度client_poll関数を呼び出す必要があります。基本的にclient_pollを呼ぶことでWebSocketクライアントは一時的かつノンブロッキングなポーリング状態になり、データが利用可能になると取得し、WINHTTP_CALLBACK_STATUS_READ_COMPLETE通知を発生させます。

現在のWebSocketクライアントの状態は、client_status関数を呼び出すことで問い合わせ可能であり、この関数はENUM_WEBSOCKET_STATE列挙型の値を返します。

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->Receive(clients[websocket_handle]->rxBuffer.data(), (DWORD)clients[websocket_handle]->rxBuffer.size(), &clients[websocket_handle]->bytesRX, &clients[websocket_handle]->rxBufferType);

   return out;
  }

ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle)
  {
   ENUM_WEBSOCKET_STATE out = {};
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = {};
   else
      out = clients[websocket_handle]->Status();

   return out;
  }

サーバーから受信した生データの取得は、client_read関数によっておこなわれます。この関数は以下の引数を受け取ります。

  • 有効なHINTERNET型のWebSocketハンドル
  • 事前に確保されたBYTE配列への参照、上記配列のサイズを示すulong値 
  • WINHTTP_WEB_SOCKET_BUFFER_TYPE型の値への参照
受信データは指定されたBYTE配列に書き込まれ、WebSocketフレームの種類はbuffertype引数にコピーされます。重要なのは、この関数はネットワークソケットと直接やり取りするのではなく、内部キューに格納された受信フレームから読み取ることで動作する点です。したがって、client_read関数はWinHTTPライブラリの非同期機構とは独立した同期的な操作となっています。戻り値がゼロでない場合は、内部キューからデータのコピーに失敗したことを示します。正常にフレームを取得した場合、そのフレームは内部キューから取り除かれます。また、client_readable関数を呼び出すことで、サーバーから受信したフレームキューの先頭にあるデータフレームのサイズを取得することができます。
DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype)
  {
   DWORD rout = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      rout = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      clients[websocket_handle]->Read(out, out_size, buffertype);

   return rout;
  }

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = 0;
   else
      out = clients[websocket_handle]->ReadAvailable();

   return out;
  }

エラーコードはclient_lasterror関数を呼び出すことで取得できます。この関数は直近に発生したエラーのDWORD値を返します。また、現在のWebSocketハンドルの値はclient_websocket_handle関数で取得可能です。これは、ハンドルが閉じられているかどうかを確認する際に役立ちます。

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastError();

   return out;
  }

HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle)
  {
   HINTERNET out = NULL;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = NULL;
   else
      out = clients[websocket_handle]->WebSocketHandle();
   return out;
  }

サーバーへの接続を正常に終了するにはclient_disconnect関数を呼び出します。この関数は値を返しません。ただし、呼び出すとすぐにWebSocketクライアントの状態が「closing」に遷移します。その後、サーバーから対応するクローズフレームが受信されると、WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE通知がトリガーされ、WebSocketの状態は「closed」に変更されます。

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle)
  {

   if(clients.find(websocket_handle) != clients.end())
     {
      if(clients[websocket_handle]->WebSocketHandle() != NULL)
        {
         clients[websocket_handle]->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
        }
     }

   return;
  }

DLLによってエクスポートされる最後の関数はclient_resetです。理想的には、この関数はサーバーから切断した後に呼び出すべきです。この関数の目的は、切断された接続に関連付けられた内部メモリバッファを解放することです。必須というわけではありませんが、この関数を呼び出すことで、プログラム実行中に他の用途で必要となる可能性のあるメモリリソースを回収するのに役立ちます。client_resetを呼び出すと、指定されたWebSocketハンドルに関連するすべてのデータ(エラーコード、エラーメッセージ、内部フレームキュー内に残っている未読データなど)が無効化されます。

VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
  {
   if(clients.find(websocket_handle) != clients.end())
     {
      clients[websocket_handle]->Free();
      clients.erase(websocket_handle);
     }
  }


CWebsocketクラスの再定義

前のセクションで説明した関数を活用するMetaTrader 5アプリケーションを検証する前に、前述の記事で取り上げたCWebsocketクラスの再定義をおこないます。この再定義では、新たに開発された非同期WebSocketクライアントを活用するようにクラスを再構築します。この適応版のソースコードはasyncwebsocket.mqhファイルに含まれています。

ENUM_WEBSOCKET_STATE列挙型は、クライアントの非同期的な特性を反映するために、追加の状態を取り込む形で拡張されています。非同期の読み取り操作が開始されると、POLLING状態が有効になります。基盤となるソケットがデータを受信し、それが取得可能になると、コールバック関数が非同期読み取り操作の完了を通知し、WebSocketクライアントの状態はデフォルトのCONNECTEDに遷移します。同様に、非同期送信操同様に、非同期送信操作が開始されると状態はSENDINGに移行します。この操作の結果もコールバック関数を通じて非同期に通知され、送信が正常に完了すると、状態は再びデフォルトのCONNECTEDに戻ります。

//+------------------------------------------------------------------+
//|   client state enumeration                                       |
//+------------------------------------------------------------------+

// client state
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = 0,
   CLOSING = 1,
   CONNECTING = 2,
   CONNECTED = 3,
   SENDING = 4,
   POLLING = 5
  };

CWebsocketクラスには、その機能強化に対応するため、いくつかの新しいメソッドが統合されています。残りのメソッドは元のシグネチャを維持したまま、内部実装のみが新しいDLL依存関係に対応するよう変更されています。これらのメソッドは以下の通りです。

Connect:サーバーへの接続を確立するための最初のインターフェイスとして機能します。以下のパラメータを受け取ります。

  •  _serveraddress:サーバーの完全なアドレス(string型)
  •  _port:サーバーのポート番号(ushort型)
  •  _secure:セキュア接続を確立するかどうかを示すブール値(boolean型)

このメソッドの実装は大幅に簡素化されており、接続確立の大部分の処理は、基盤となるDLLによっておこなわれます。

//+------------------------------------------------------------------------------------------------------+
//|Connect method used to set server parameters and establish client connection                          |
//+------------------------------------------------------------------------------------------------------+
bool CWebsocket::Connect(const string _serveraddress,const INTERNET_PORT port=443, bool secure = true)
  {
   if(initialized)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
         Close();
      else
         return(true);
     }

   serveraddress = _serveraddress;

   int dot=StringFind(serveraddress,".");

   int ss=(dot>0)?StringFind(serveraddress,"/",dot):-1;

   serverPath=(ss>0)?StringSubstr(serveraddress,ss+1):"/";

   int sss=StringFind(serveraddress,"://");

   if(sss<0)
      sss=-3;

   serverName=StringSubstr(serveraddress,sss+3,ss-(sss+3));
   serverPort=port;


   DWORD connect_error = client_connect(serveraddress,port,ulong(secure),hWebSocket);

   if(hWebSocket<=0)
     {
      Print(__FUNCTION__," Connection to ", serveraddress, " failed. \n", GetErrorDescription(connect_error));
      return(false);
     }
   else
      initialized = true;

   return(true);
  }

Connectメソッドがboolean型のtrueを返した場合は接続が正常に確立されたことを意味するので、WebSocketクライアントを通じたデータ送信を開始することができます。この目的のために、以下の2つのメソッドが用意されています。

  •  SendString:文字列を引数として受け取ります
  •  Send:符号なし文字配列を唯一のパラメータとして受け取ります

どちらのメソッドも、送信操作の開始に成功した場合はboolean型のtrueを返します。これらのメソッドは内部的に、すべての送信処理を管理するprivateメソッド「clientsend」を呼び出します。

//+------------------------------------------------------------------+
//|public method for sending raw string messages                     |
//+------------------------------------------------------------------+
bool CWebsocket::SendString(const string msg)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   if(StringLen(msg)<=0)
     {
      Print(__FUNCTION__, " Message buffer is empty ");
      return(false);
     }

   BYTE msg_array[];

   StringToCharArray(msg,msg_array,0,WHOLE_ARRAY);

   ArrayRemove(msg_array,ArraySize(msg_array)-1,1);

   DWORD len=(ArraySize(msg_array));

   return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

//+------------------------------------------------------------------+
//|Public method for sending data prepackaged in an array            |
//+------------------------------------------------------------------+
bool CWebsocket::Send(BYTE &buffer[])
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

Pollメソッドを呼び出すことで、下位レベルで非同期の読み取り操作が開始され、WebSocketクライアントはPOLLING状態に遷移します。この状態は、クライアントがサーバーからの応答を待機していることを示します。

//+------------------------------------------------------------------+
//|  asynchronous read operation (polls for server response)         |
//+------------------------------------------------------------------+
ulong CWebsocket::Poll(void)
  {
   if(hWebSocket!=NULL)
      return client_poll(hWebSocket);
   else
      return WEBSOCKET_ERROR_INVALID_HANDLE;
  }

クライアントがデータを受信し、正常に読み取ったかどうかを確認するために、ユーザーは以下の2つの方法を利用できます。

  • CallBackResult:コールバック関数から最後に受け取った通知を確認します。読み取り操作が正常に完了した場合、「読み取り完了」の通知が返されるはずです。
  • ReadAvailable:内部バッファ内で現在取得可能なデータのサイズ(バイト単位)を返します。

//+------------------------------------------------------------------+
//| call back notification                                           |
//+------------------------------------------------------------------+
ulong CWebsocket::CallBackResult(void)
  {
   if(hWebSocket!=NULL)
      return client_lastcallback_notification(hWebSocket);
   else
      return WINHTTP_CALLBACK_STATUS_DEFAULT;
  }
//+------------------------------------------------------------------+
//|  check if any data has read from the server                      |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadAvailable(void)
  {
   if(hWebSocket!=NULL)
      return(client_readable(hWebSocket));
   else
      return 0;
  }

サーバーから送信された生データは、ReadまたはReadStringメソッドのいずれかを使用して取得することができます。どちらのメソッドも、受信したデータのサイズを返します。ReadStringは、受信データを書き込むための文字列変数を参照渡しで受け取ります。一方、Readはデータを符号なし文字(unsigned char)配列に書き込みます。

//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   clientread(buffer,buffertype,bytes_read_from_socket);

   return(bytes_read_from_socket);

  }
//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadString(string &_response)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   ZeroMemory(rxbuffer);

   WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype;

   clientread(rxbuffer,rbuffertype,bytes_read_from_socket);

   _response=(bytes_read_from_socket)?CharArrayToString(rxbuffer):"";

   return(bytes_read_from_socket);
  }

WebSocketクライアントが不要になった場合、Closeメソッドを使用してサーバーとの接続を終了することができます。AbortメソッドはCloseとは異なり、基盤となるハンドルを強制的に閉じることでWebSocket接続を終了させます。この操作により、クラス内部のプロパティ値もデフォルト状態にリセットされます。このメソッドは明示的に呼び出すことで、リソースのクリーンアップをおこなうことができます。

最後に、WebSocketHandleメソッドは、基盤となるHINTERNET型のWebSocketハンドルを返します。

//+------------------------------------------------------------------+
//| Closes a websocket client connection                             |
//+------------------------------------------------------------------+
void CWebsocket::Close(void)
  {
   if(!initialized || hWebSocket == NULL)
      return;
   else
      client_disconnect(hWebSocket);
  }
//+--------------------------------------------------------------------------+
//|method for abandoning a client connection. All previous server connection |
//|   parameters are reset to their default state                            |
//+--------------------------------------------------------------------------+
void CWebsocket::Abort(void)
  {
   client_reset(hWebSocket);
   reset();
  }
//+------------------------------------------------------------------+
//|   websocket handle                                               |
//+------------------------------------------------------------------+
HINTERNET CWebsocket::WebSocketHandle(void)
  {
   if(hWebSocket!=NULL)
      return client_websocket_handle(hWebSocket);
   else
      return NULL;
  }


DLLの使用

このセクションでは、asyncwinhttpwebsockets.dllの利用方法を示すためのサンプルプログラムを紹介します。このプログラムは、GUI(グラフィカルユーザーインターフェイス)を備えており、WebSocketクライアントの実装テスト用に提供されているhttps://echo.websocket.orgのエコーサービスに接続します。このアプリケーションの構築には、無料で利用可能なEasy And Fast GUI MQL5ライブラリが必要です。プログラムはMetaTrader 5環境内のエキスパートアドバイザー(EA)として実装されています。ユーザーインターフェイスには、指定サーバーへの接続開始と終了をおこなうための2つのボタンがあり、さらにサーバーに送信するメッセージを入力できるテキスト入力フィールドが備わっています。

WebSocketクライアントの操作はすべて記録され、各ログエントリには発生時刻がタイムスタンプとして付与されて表示されます。以下に、このプログラムのソースコードを示します。

//+------------------------------------------------------------------+
//|                                                         Echo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <EasyAndFastGUI\WndCreate.mqh>
#include <asyncwebsocket.mqh>
//+------------------------------------------------------------------+
//| Gui application class                                            |
//+------------------------------------------------------------------+
class CApp:public CWndCreate
  {
protected:

   CWindow           m_window;                   //main window

   CTextEdit         m_rx;                       //text input to specify messages to be sent

   CTable            m_tx;                       //text box displaying received messages from the server

   CButton           m_connect;                  //connect button

   CButton           m_disconnect;               //disconnect button

   CTimeCounter      m_timer_counter;                //On timer objects

   CWebsocket*        m_websocket;               //websocket connection
public:
                     CApp(void);                 //constructor
                    ~CApp(void);                 //destructor

   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);

   virtual void      OnEvent(const int id, const long &lparam, const double &dparam,const string &sparam);
   void              OnTimerEvent(void);
   bool              CreateGUI(void);

protected:

private:
   uint              m_row_index;
   void              EditTable(const string newtext);
  };
//+------------------------------------------------------------------+
//|  constructor                                                     |
//+------------------------------------------------------------------+
CApp::CApp(void)
  {
   m_row_index = 0;
   m_timer_counter.SetParameters(10,50);
   m_websocket = new CWebsocket();
  }
//+------------------------------------------------------------------+
//|   destructor                                                     |
//+------------------------------------------------------------------+
CApp::~CApp(void)
  {
   if(CheckPointer(m_websocket) == POINTER_DYNAMIC)
      delete m_websocket;
  }
//+------------------------------------------------------------------+
//| On initialization                                                |
//+------------------------------------------------------------------+
void CApp::OnInitEvent(void)
  {
  }
//+------------------------------------------------------------------+
//| On DeInitilization                                               |
//+------------------------------------------------------------------+
void CApp::OnDeinitEvent(const int reason)
  {
   CWndEvents::Destroy();
  }
//+------------------------------------------------------------------+
//| on timer event                                                   |
//+------------------------------------------------------------------+
void CApp::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();

   if(m_timer_counter.CheckTimeCounter())
     {

      ENUM_WEBSOCKET_STATE client_state = m_websocket.ClientState();
      ulong operation = m_websocket.CallBackResult();

      switch((int)operation)
        {
         case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
            if(client_state == CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnected]");
               m_websocket.Abort();
              }
            break;
         case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
            if(client_state!=POLLING)
              {
               m_websocket.Poll();
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Send Complete]");
              }
            break;

         case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
            if(m_websocket.ReadAvailable())
              {
               string response;
               m_websocket.ReadString(response);
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Received]-> "+response);
              }
            break;
         case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
            EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Error]-> "+m_websocket.LastErrorMessage());
            m_websocket.Abort();
            break;
         default:
            break;
        }
     }
  }
//+------------------------------------------------------------------+
//| create the gui                                                   |
//+------------------------------------------------------------------+
bool CApp::CreateGUI(void)
  {
//---check the websocket object
   if(CheckPointer(m_websocket) == POINTER_INVALID)
     {
      Print(__FUNCTION__," Failed to create websocket client object ", GetLastError());
      return false;
     }
//---initialize window creation
   if(!CWndCreate::CreateWindow(m_window,"Connect to https://echo.websocket.org Echo Server ",1,1,750,300,true,false,true,false))
      return(false);
//---
   if(!CWndCreate::CreateTextEdit(m_rx,"",m_window,0,false,0,25,750,750,"Click connect button below, input your message here, then press enter key to send"))
      return(false);
//---
   if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,0,5,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for width in frequency units
   if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,0,500,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for amount of padding
   string tableheader[1] = {"Client Operations Log"};
   if(!CWndCreate::CreateTable(m_tx,m_window,0,1,10,tableheader,5,75,0,0,true,true,5))
      return(false);
//---
   m_tx.TextAlign(0,ALIGN_LEFT);
   m_tx.ShowTooltip(false);
   m_tx.DataType(0,TYPE_STRING);
   m_tx.IsDropdown(false);
   m_tx.SelectableRow(false);
   int cwidth[1] = {740};
   m_tx.ColumnsWidth(cwidth);
//---init events
   CWndEvents::CompletedGUI();
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| edit the table                                                   |
//+------------------------------------------------------------------+
void CApp::EditTable(const string newtext)
  {
   if(newtext==NULL)
      return;

   if((m_row_index+1)==m_tx.RowsTotal())
     {
      m_tx.AddRow(m_row_index+1,true);
      m_tx.Update();
     }

   m_tx.SetValue(0,m_row_index++,newtext,0,true);
   m_tx.Update(true);
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CApp::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      if(lparam==m_rx.Id())
        {
         if(m_websocket.ClientState() == CONNECTED)
           {
            string textinput = m_rx.GetValue();
            if(StringLen(textinput)>0)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Sending]-> "+textinput);
               m_websocket.SendString(textinput);
              }
           }
        }
      return;
     }
   else
      if(id == CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
        {
         if(lparam==m_connect.Id())
           {
            if(m_websocket.ClientState() != CONNECTED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connecting]");
               if(m_websocket.Connect("https://echo.websocket.org/"))
                 {
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connected]");
                  m_websocket.Poll();
                 }
               else
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[FailedToConnect]");
              }
            return;
           }
         if(lparam==m_disconnect.Id())
           {
            if(m_websocket.ClientState() != CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnecting]");
               m_websocket.Close();
              }
           }
         return;
        }
  }
//+------------------------------------------------------------------+
CApp app;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   ulong tick_counter=::GetTickCount();
//---
   app.OnInitEvent();
//---
   if(!app.CreateGUI())
     {
      ::Print(__FUNCTION__," > error");
      return(INIT_FAILED);
     }

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   app.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   app.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   app.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

下の図はプログラムがどのように動作するかを示しています。

Echo EAデモ


結論

本記事では、WinHTTPライブラリを非同期モードで利用したMetaTrader 5向けWebSocketクライアントの開発について詳細に解説しました。この機能をカプセル化する専用クラスを構築し、その実装をecho.websocket.orgでホストされているechoサーバーと連携するEA内で示しました。完全なソースコードは付録資料にて提供しており、DLLのソースファイルおよびCMakeビルド設定ファイル(CMakeLists.txt)は指定されたC++ディレクトリにあります。また、MQL5ディレクトリには即時展開可能なプリコンパイル済みのasyncwinhttpwebsockets.dllファイルが含まれています。

提供されたソースコードからクライアントライブラリをビルドしたい場合は、CMakeビルドシステムが必要です。CMakeがインストールされている場合、GUI版(cmake-gui)を起動できます。まずソースコードのディレクトリ(CMakeLists.txtファイルが存在する場所、すなわちMqlWebsocketsClientDLL\Source\C++)と、任意の場所に作成した別のビルドディレクトリを指定します。

その後、[Configure]ボタンをクリックすると設定処理が開始されます。ダイアログが表示され、[Specify the generator for this project]と求められるので、システムにインストールされている適切なVisual Studioのバージョンを選択します。[Optional platform for generator]の設定では、32ビット版DLLをコンパイルする場合はWin32を指定します。空欄のままにするとデフォルトで64ビット版がビルドされます。[Finish]をクリックすると初期設定がCMakeによって処理されます。

次にエラーメッセージが表示され、「CMakeLists.txt内の特定のエントリの設定が必要」と通知されます。これを解決するには、[ADDITIONAL_LIBRARY_DEPENDENCIES]とラベル付けされた項目を見つけ、その隣の入力欄をクリックしてwinhttp.libファイルがあるディレクトリを指定します。

続いて、[OUTPUT_DIRECTORY_Xxx_RELEASE](ここで「Xxx」はアーキテクチャのX64またはX86)とラベル付けされた項目を見つけ、対応するパスをMetaTraderのLibrariesフォルダに設定します。

これらの設定をおこなった後、[Configure]を再度クリックするとエラーは表示されず設定が完了します。[Generate]ボタンをクリックするとビルドファイルが生成され、正常に生成されると[Open Project]ボタンが有効になります。これをクリックするとVisual Studioのプロジェクトファイルが開きます。

DLLをビルドするには、Visual Studio内で[Build]>[Build Solution]を選択すると、数秒でDLLが生成されます。

ファイル名またはフォルダ名
説明
MqlWebsocketsClientDLL\Source\C++
asycnwinhttpwebsockets.dllの完全なソースコードファイルが含まれているフォルダ
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh asycnwinhttpwebsockets.dllによって公開されるすべての関数をリストするインポートディレクティブ
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh 基礎となるDLL関数によって提供される機能をラップするCWebsocketクラスの定義
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 DLLの適用を示すEAのコードファイル
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 DLLの適用を示すコンパイル済みEA
MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll 非同期Winhttp Websocket機能を提供するコンパイル済みDLL

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17877

添付されたファイル |
asyncwinhttp.mqh (13.42 KB)
asyncwebsocket.mqh (19.09 KB)
Echo.mq5 (10.84 KB)
Echo.ex5 (306.48 KB)
最後のコメント | ディスカッションに移動 (9)
Shephard Mukachi
Shephard Mukachi | 2 5月 2025 において 21:17
Ryan L Johnson #:

でコード例をご覧ください:

同じ大前提がEAにも当てはまります。

それは素晴らしいアイデアです。

CapeCoddah
CapeCoddah | 15 5月 2025 において 23:47

私の質問に対するお二人の返答に感謝します。 オーバーロードされた関数の定義を見落としていたようで、最初の方しか読んでいませんでした。 28のペアごとにシンボルパラメータを変え、Brooky Trend Strengthのように複数のiCustomコールを使用する予定なので、プロセッサの使用率を最大化するためにiCustomコールを並列処理するほどターミナルが賢いかどうかご存知ですか?

また、MQ5のバグについてコメントを投稿できる場所や、Mq管理者への提案も教えてください。 いくつか見つけたのですが、最近ではターミナルとストラテジーテスターの Barsの 違いについてです。 また、メインディスプレイが左端にある3スクリーンのセットアップを使用しています。ナビゲーターやマーケットパネルなどのパネルを左から右へ移動させるのは非常に面倒です。 ドラッグマウスポインターは一番左の画面にありますが、ドラッグパネルは真ん中にあります。 マウスが1ピクセル移動し、パネルを1ピクセル移動させるためにディスプレイが切り替わり、また元に戻るとき、ターミナルかWindowsのどちらかがおかしくなっているのだと思います。

Silk Road Trading LLC
Ryan L Johnson | 16 5月 2025 において 00:13
CapeCoddah #:
28のペアごとにシンボルパラメータを変化させ、Brooky Trend Strengthのように複数のiCustomコールを使用する予定なので、TerminalがiCustomコールを並列処理し、プロセッサの使用率を最大化できるかどうかご存知ですか?

問題ありません。インジケータ・ハンドルとCopyBuffer()のインスタンスが別々に必要なだけです。すべてのインジケーターが同じスレッドで実行されるとしても、100ほどのインジケーター・インスタンスを実行することができます。

CapeCoddah#:
また、MQ5のバグに関するコメントや、Mq管理者への提案を投稿できる場所を教えてください。

Bars()は、価格データが欠落している場合、しゃっくりを起こします。過去に読んだ本の記憶が正しければ、Bars()はタイムスタンプを参照することで修正できます。検索してみる価値はあるかもしれない。

CapeCoddah#:
メインディスプレイが左端にある3スクリーンのセットアップをしています。 NavigatorやMarketパネルのようなパネルを左から右に移動させようとすると、とても面倒です。 ドラッグマウスポインタは一番左のスクリーンにありますが、ドラッグパネルは真ん中にあります。 マウスが1ピクセル移動し、ディスプレイを切り替えてパネルを1ピクセル移動させ、また戻るときに、ターミナルかWindowsのどちらかがおかしくなっているのだと思います。

これについては本当にわからない。私は3台のコンピューターを持っていて、それぞれにモニターとターミナルがあります。Windowsには一般的に、回避策としてピクチャー・イン・ピクチャーを含むマルチモニター表示設定があることは知っている。

どなたか、1台のマシンに複数のモニターを実際に使用している方がいらっしゃいましたら、ご意見をお聞かせください。

CapeCoddah
CapeCoddah | 16 5月 2025 において 09:42

素晴らしい情報です!

ありがとうライアン。 barsとrates_totalに関するコメントは適切です。 私の問題は、ターミナルではこの2つは同じですが、STrategy Tester VisualizeではBarsの方が大きいことです。

また、Strategy TesterでEAが画面にテキストを表示する方法はありますか? Mq4では自動的に表示されましたが、今は表示されません。 情報を表示するために多くのクラスオブジェクトを使用しており、テンプレートに2つ目のコピーを入れるとStrategy Testerの動作がさらに遅くなります。

3パネル・ディスプレイについては、マウスが画面2から画面1に移動したときに、端末がモニターの位置を適切に更新しないのが問題だと思います。

私はそれぞれ3つのモニターをサポートする2台のミニPCを持っているので、3つのスクリーンを両方のミニPCに接続し、1台のPCにはHDMI1を、もう1台のPCにはHDMI2を使用しています。 リモコンが1つのモニターだけをコントロールするように適切に設定されていることを確認する必要がありますが(amazonのサポートに連絡してください)、43インチのFire Tvでうまく動作しています。 唯一の欠点は、オン・オフボタンがすべてのモニターをシャットダウンすることで、電源を同期させるためにプラグを抜く必要があることがあります。


ケープコッダ

Silk Road Trading LLC
Ryan L Johnson | 16 5月 2025 において 19:58
CapeCoddah STrategy Tester VisualizeではBarsが1つ大きいことで、ドキュメントを最後まで読まずにボツにしました。 あなたの意見をiCustomに使おうと思います。 シンボルと時間の指定の組み合わせごとに別々のiCustomアドレスが必要だと推測します。

  1. 1つのディレクトリにある1つのインジケーター・ファイルを、複数のiCustom()インスタンスで再利用できます。
  2. 1つのインジケータ・ハンドルを複数のCopyBuffer()インスタンスで再利用できます。
  3. rate_total だけでは単一のタイムフレームに制限されるため、Bars() を使用している理由がわかりました。おそらく、各タイムフレームに対して別のループで Bars() を使用しているのでしょう。

CapeCoddah#:
また、Strategy TesterでEAが画面にテキストを表示する方法はありますか? Mq4では自動的に表示されましたが、今は表示されません。 情報を表示するために多くのクラスオブジェクトを使用しており、テンプレートに2つ目のコピーを置くとStrategy Testerの動作がさらに遅くなります。
私の知る限りではそうではありません。ビジュアライゼーションのテストMT5ヘルプページで私が知っている唯一の方法をすでに使用しています。
CapeCoddah#:
3パネルディスプレイについては、マウスが画面2から画面1に移動したときに、端末がモニターの位置を適切に更新しないことが問題だと思います。
残念ながら、私自身のセットアップでこれをテストする方法はありません。単一のMT5ターミナル画面をすべてのモニターに引き伸ばしていますか?他の人がその方法で問題を解決しているのを見たことがあります。
プライスアクション分析ツールキットの開発(第21回):Market Structure Flip Detector Tool プライスアクション分析ツールキットの開発(第21回):Market Structure Flip Detector Tool
The Market Structure Flip Detectorエキスパートアドバイザー(EA)は、市場センチメントの変化を常に監視する頼れるパートナーとして機能します。ATR (Average True Range)に基づく閾値を活用することで、構造の反転を的確に検出し、各高値切り下げおよび安値切り上げを明確なインジケーターで表示します。MQL5の高速な実行性能と柔軟なAPIにより、このツールはリアルタイム分析を可能にし、最適な視認性を保つよう表示を調整しながら、反転の回数やタイミングをモニターできるライブダッシュボードも提供します。さらに、カスタマイズ可能なサウンド通知やプッシュ通知により、重要なシグナルを確実に受け取ることができ、シンプルな入力と補助ルーチンがどのように価格変動を実用的な戦略へと変換するかを実感できます。
取引チャート上で双三次補間を用いたリソース駆動型画像スケーリングによる動的MQL5グラフィカルインターフェイスの作成 取引チャート上で双三次補間を用いたリソース駆動型画像スケーリングによる動的MQL5グラフィカルインターフェイスの作成
本記事では、取引チャート上で高品質な画像スケーリングを実現するために、双三次補間(バイキュービック補間)を使用した動的なMQL5グラフィカルインターフェイスについて解説します。カスタムオフセットによる動的な中央配置やコーナーアンカーなど、柔軟なポジショニングオプションも紹介します。
取引におけるニューラルネットワーク:制御されたセグメンテーション 取引におけるニューラルネットワーク:制御されたセグメンテーション
この記事では、複雑なマルチモーダルインタラクション分析と特徴量理解の方法について説明します。
MQL5での取引戦略の自動化(第16回):ミッドナイトレンジブレイクアウト+Break of Structure (BoS)のプライスアクション MQL5での取引戦略の自動化(第16回):ミッドナイトレンジブレイクアウト+Break of Structure (BoS)のプライスアクション
本記事では、MQL5を用いて「ミッドナイトレンジブレイクアウト + Break of Structure (BoS)」戦略を自動化し、ブレイクアウトの検出および取引実行のコードを詳細に解説します。エントリー、ストップ、利益確定のためのリスクパラメータを正確に定義し、実際の取引に活用できるようバックテストおよび最適化もおこないます。