IT

[안드로이드/웹소켓] 바이낸스 가격 조회

햄과함께 2023. 1. 8. 19:03
320x100

한 번만 조회하는게 아닌 지속적인 가격을 불러와서 폰으로 확인하고 싶다.

 

binance API 호출

공식 문서 : https://www.binance.com/en/binance-api

가격, 그중에서 선물 가격을 조회 할 것이기 때문에 https://binance-docs.github.io/apidocs/futures/en/#testnet 에서 확인.

 

https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams

위 주소로 조회해보자.

 

 

https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=ko 

simple-websocket-client 크롬 확장프로그램으로 웹소켓 테스트

 

구분 value
Server Location wss://fstream.binance.com/stream?btcusdt@markPrice
구독 {
"method": "SUBSCRIBE",
"params":["btcusdt@markPrice"],
"id": 1
}
구독해지 {
"method": "UNSUBSCRIBE",
"params":["btcusdt@markPrice"],
"id": 1
}

 

3초 혹은 매초마다 데이터를 전송한다.

 

https://chrome.google.com/webstore/detail/simple-websocket-client/pfdhoblngboilpfeibdedpjgfnlcodoo?hl=ko 

simple-websocket-client 크롬 확장프로그램으로 웹소켓 테스트 결과

 

p가 market price 이걸 써야한다.

 

Android Studio websocket 통신

// BinanceWebSocketListener.kt

class BinanceWebSocketListener(
    private val textView: TextView,
): WebSocketListener() {

    override fun onOpen(webSocket: WebSocket, response: Response?) {
        Log.i(TAG, "onOpen: ${response?.toString()}")
        webSocket.send("""|
            |{
            |"method": "SUBSCRIBE",
            |"params": ["btcusdt@markPrice"],
            |"id": 1
            |}
        """.trimMargin())
    }

    override fun onMessage(webSocket: WebSocket?, text: String) {
        Log.i(TAG,"onMessage: $text")

        textView.findFragment<Fragment>()
        text.marketPrice?.also { textView.text = it }
    }

    override fun onMessage(webSocket: WebSocket?, bytes: ByteString) {
        Log.i(TAG, "onMessage: $bytes")
        bytes.toString().marketPrice?.also { textView.text = it }
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        Log.i(TAG,"onClosing: $code $reason")
        webSocket.send("""|
            |{
            |"method": "UNSUBSCRIBE",
            |"params": ["btcusdt@markPrice"],
            |"id": 1
            |}
        """.trimMargin())
        webSocket.close(NORMAL_CLOSURE_STATUS, null)
        webSocket.cancel()
    }

    override fun onFailure(webSocket: WebSocket?, t: Throwable, response: Response?) {
        Log.i(TAG,"onFailure: " + t.message)
    }

    private val String.marketPrice: String?
        get() = try {
            JSONObject(this).getJSONObject("data")
                .getString(MARKET_PRICE_NAME)
        } catch (e: JSONException) {
            null
        }

    companion object {
        private const val URL = "wss://fstream.binance.com/stream?btcusdt@markPrice"
        private const val TAG = "BinanceWebSocketListener"
        private const val NORMAL_CLOSURE_STATUS = 1000
        private const val MARKET_PRICE_NAME = "p"

        val request: Request
            get() = Request.Builder().url(URL).build()
    }

}

WebSocketListener를 상속받고 binance와 통신하는 BinanceWebSocketListener를 만든다.

onMessage에서 textView의 text를 전달받은 가격으로 변경한다.

 

// FirstFragment.kt

class FirstFragment : Fragment() {

    private var _binding: FragmentFirstBinding? = null

    private lateinit var client: OkHttpClient // add #1
    private val binding get() = _binding!!

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentFirstBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ... 
        
        // add #2
        client = OkHttpClient()
        val listener = BinanceWebSocketListener(binding.textviewFirst)
        client.newWebSocket(BinanceWebSocketListener.request, listener)
        
        // ... 
        
    }

    override fun onDestroyView() {
        super.onDestroyView()
        
        client.dispatcher().executorService().shutdown() // add #3 
        
        _binding = null
    }
}

#2에서 view가 생성될 때 webSocket을 만들어 등록한다.

#3에서 해당 view가 삭제될 때 등록한 websocket도 종료되게 한다.

 

 

이렇게 하면

"only the original thread that created a view hierarchy can touch its views."

이런 에러가 발생하면서 text를 변경할 수 없었다.

 

구글링 해보니 메인 쓰레드(original thread)에서만 ui를 변경할 수 있어서 해당 에러가 발생하는 거였다.

 

// BinanceWebSocketListener.kt

class BinanceWebSocketListener(
    private val textView: TextView,
): WebSocketListener() {

    // ...

    override fun onMessage(webSocket: WebSocket?, text: String) {
        Log.i(TAG,"onMessage: $text")

        textView.findFragment<Fragment>()
        
        // change
        text.marketPrice?.also { textView.runOnUiThread {  textView.text = it }}
    }

    override fun onMessage(webSocket: WebSocket?, bytes: ByteString) {
        Log.i(TAG, "onMessage: $bytes")
        
        // change
        bytes.toString().marketPrice?.also { textView.runOnUiThread {  textView.text = it }}
    }

    // add
    private fun TextView.runOnUiThread(action: () -> Unit) {
        val fragment = try {
            this.findFragment<Fragment>()
        } catch (e: IllegalStateException) {
            null
        } ?: return

        if (!fragment.isAdded) return
        fragment.activity?.runOnUiThread(action)
    }

}

그래서 위와 같이 파라미터로 전달받은 TextView의 Fragment > Activity를 거슬로 올라가 찾아서 runonUiThread안에서 text 변경 코드를 추가하면 정상 작동하였다.

 

 

결과물

위젯으로 만들고 싶었는데 안드로이드 잘 모르겠어서 생각보다 작업하는데 오래걸렸다. 

다른 날에 마저 해야겠다.


 

부록) Binance RSA Key Pair 생성

필요할 줄 알고 먼저 발급받았던 rsa key pair 관련 정리

 

https://www.binance.com/en/support/faq/how-to-generate-an-rsa-key-pair-to-send-api-requests-on-binance-2b79728f331e43079b27440d9d15c5db

공식 문서.

 

1. https://github.com/binance/rsa-key-generator/releases 접속해서 os에 맞는 generator 다운로드

2. generate key pair 클릭하여 공개키, 비밀키 생성

3. binance API 생성

https://www.binance.com/en/my/settings/api-management

 

자체 생성 API Key 클릭.

아까 생성했던 공개키 복붙.

API Key 라벨 이름 적고 완료.

이메일, 모바일 인증을 모두 받아야 된다.

 

API Key가 생성되었다.

 

설정을 조금 보면, 일단은 가격 조회만 할 것이기 때문에 API restrictions는 수정할 필요가 없고.

IP 제한은 빨간색 글자에 의하면 제한이 없는 경우엔 권장하지 않는다. 나머지는 읽기 권한 이외의 권한을 선택한 경우기 때문에 일단 무시.

백엔드단 서버를 한 대 두고 거기서만 API로 바이낸스랑 통신을 해서 데이터를 가져오면서 백엔드 서버의 IP를 화이트리스트로 관리. 앱에서 백엔드 사이엔 별도로 인증. 을 해야겠지만 지금은 다 패스.

 


참고

 

https://stackoverflow.com/questions/16425146/runonuithread-in-fragment

https://ku-hug.tistory.com/92

320x100