Binance WebSocket Using CPP

표준성·2023년 11월 30일
1

컴퓨터네트워크

목록 보기
4/4

여러분은 비동기 웹소켓 클라이언트를 개발해야합니다. 여러분의 클라이언트 프로그램이 암호화폐의 정보를 상시적으로 보내는 Binance 서버(https://www.binance.com/en/markets)의 스트리밍 채널을 구독하게 하세요.

암호화폐 거래소인 바이넌스의 웹소켓 서버에 연결하여 비트코인의 매수가(Bid)와 매도가(Ask)를 받아온 후, txt 파일에 기록하는 과제이다.

Websocket Library

웹이나 자바, 파이썬과 달리 외부 라이브러리를 추가하는 것이 생각보다 매우매우 아주 어려웠다.

설치과정은 Windows를 기반으로 작성했습니다. 맥이나 리눅스는 apt를 사용하는 것을 추천합니다.

Visual Studio와 vcpkg(패키지 매니저)를 사용하는 것이 정신건강에 이롭다.

vcpkg 설치과정

git clone으로 vcpkg 파일을 가져온 다음

git clone https://github.com/Microsoft/vcpkg.git

bat 파일을 실행시켜 vcpkg를 빌드하자.

cd vcpkg
.\vcpkg\bootstrap-vcpkg.bat

Visual Studio를 사용한다는 가정 하에, 아래의 명령어를 사용하면 추가 설정없이 cpkg로 설치된 모든 라이브러리를 #include로 바로 사용 할 수 있다.

.\vcpkg integrate install

Boost.Beast 웹소켓 라이브러리 설치

이제 라이브러리를 설치해보자. 자신이 64bit 컴퓨터라면(대부분 그럴 것이고) :x64-windows 옵션을 사용하여 명시해줘야 한다.

.\vcpkg install boost:x64-windows

설치하는 데에 꽤 시간이 걸린다.

Visual Studio 연동

솔루션 탐색기에서 프로젝트 오른쪽 -> 속성 -> 구성 속성 -> vcpkg로 들어가 다음과 같이 설정해야 한다.이제 다음과 같은 코드를 작성할 때 자동완성(Ctrl+Space)이 뜨길 기도하자.

#include <boost/beast/core.hpp>

만약 자동완성이 되지 않고 such file does not exist라면, IDE를 재실행해보자. 그래도 되지 않는다면 화이팅!

Binance WebSocket

만약 오류가 뜨지 않고 여기까지 왔다면 70%는 성공이다. 코드를 작성하기 전 Binance Websocket Market Stream Docs에 따르면 기본 엔드포인트는 아래와 같다.

wss://stream.binance.com:443(or 9443)

우리는 스트림에 아래와 같은 Json 형태의 쿼리를 보내 결과를 받아올 수 있다.

{
	"method": "SUBSCRIBE",
	"params":
	[
		"btcusdt@depth<levels>" //가장 높은 bid와 ask를 levels만큼 가져온다.
	],
	"id": 1 //an identifier to uniquely identify the messages going back and forth.
}

또한 기본 프로토콜이 wss이므로 ssl 처리도 해줘야한다. 아니면 Binance 서버에서 아래와 같은 오류가 발생하니 참고!

Error: The WebSocket handshake was declined by the remote peer

소켓은 다음과 같은 흐름을 가진다.우리는 서버가 아닌 클라이언트이므로, 소켓을 만들고 -> 연결하고 -> 데이터를 보내고 받는 과정을 거치면 된다.

Headers and namespaces

구현하기 전 필요한 헤더파일과 손목 건강을 위한 namespace를 설정해줘야 한다.

#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <string>

namespace beast = boost::beast;
namespace http = beast::http;
namespace websocket = beast::websocket;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;

WebSocketClient

class WebSocketClient {
public:
    WebSocketClient(net::io_context& ioc, ssl::context& ctx)
        : resolver_(ioc), ws_(ioc, ctx) {}
    void connect(const std::string& host, const std::string& port, const std::string& target) {
        auto const results = resolver_.resolve(host, port);
        net::connect(ws_.next_layer().next_layer(), results.begin(), results.end());
        ws_.next_layer().handshake(ssl::stream_base::client);

        ws_.set_option(websocket::stream_base::decorator(
            [](websocket::request_type& req)
            {
                req.set(http::field::user_agent,
                std::string(BOOST_BEAST_VERSION_STRING) +
                " binance-websocket-client");
            }));
        ws_.handshake(host, target);
    }
        
    void send(const std::string& message, const std::string& param) {
        auto pos = message.find("\"params\": [");
        std::string subscribeMessage = message;
        subscribeMessage.insert(pos + 11, "\"" + param + "\"");
        std::cout << subscribeMessage << std::endl;
        ws_.write(net::buffer(subscribeMessage));
    }

    void receive() {
        beast::flat_buffer buffer;
        ws_.read(buffer);
        std::cout << beast::make_printable(buffer.data()) << std::endl;
    }

    void close() {
        ws_.close(websocket::close_code::normal);
    }

private:
    net::ip::tcp::resolver resolver_;
    websocket::stream<beast::ssl_stream<tcp::socket>> ws_;
};

BinanceWebSocketClient

class BinanceWebSocketClient {
public:
    BinanceWebSocketClient(net::io_context& ioc, ssl::context& ctx, const std::string& host, const std::string& port, const std::string& param)
        : wsClient(ioc, ctx), host_(host), port_(port), param_(param), target_("/ws/" + param) {}

    void run() {
        try {
            wsClient.connect(host_, port_, target_);

            const std::string subscribeMessage = R"({"method": "SUBSCRIBE", "params": [], "id": 1})";
            wsClient.send(subscribeMessage, param_);
            while (true) {
                wsClient.receive();
            }

            wsClient.close();
        }
        catch (std::exception const& e) {
            std::cerr << "Error: " << e.what() << std::endl;
        }
    }

private:
    WebSocketClient wsClient;
    std::string host_;
    std::string port_;
    std::string param_;
    std::string target_;
};

STEP A

Write a streaming client program receiving ‘btcusdt@depth5’ and a trade stream of ‘btcusdt’ information in the same stream connection. The data will be updated every 100-500 milliseconds. Your program will save the data into a text file (do not use shell redirect), called stream-databinance.txt. You will add a new line of data as they arrive. The trade stream shows “p” and “q” values only.

btcusdt@depth5와 btcusdt@trade의 reponse를 같은 스트림 내에서 받는 클라이언트 프로그램을 작성해야 한다. 그리고 그 데이터를 텍스트 파일에 저장하면 된다.

int main()
{
    const std::string host = "stream.binance.com";
    const std::string port = "443";
    const std::string param = "btcusdt@trade";

    net::io_context ioc;
    ssl::context ctx{ ssl::context::tlsv12_client };

    BinanceWebSocketClient client(ioc, ctx, host, port, param);
    client.run();
}
profile
HYU_INFOSYS 23

0개의 댓글