'server'에 해당되는 글 3건

  1. 2019.05.01 C# WebSocket Server 6
  2. 2019.04.14 node.js를 이용한 iOS Push 구현하기 (requests to APNs)
  3. 2019.04.09 Android 결제 서버 개발

C# WebSocket Server

C# 2019. 5. 1. 13:17

웹은 기본적으로, TCP/UDP 등의 소켓 통신을 할 수 없습니다. 그로인해, 웹 온라인 게임을 제작하는 것은 불가능했었습니다. 하지만, HTML5와 웹소켓의 등장으로, 웹에서도 게임을 제작할 수 있게 되었습니다. 이 문서는 c#으로 웹소켓을 구현하는 방법을 설명합니다.

 

[선행작업]

에코서버 만들기 : https://nicgoon.tistory.com/227 .

 

[공식문서]

Writing a WebSocket server in c# : https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server .

Writing WebSocket servers : https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers .

웹소켓 클라이언트 공식 문서 : https://www.websocket.org/echo.html .

C# 및 Visual Studio Code 시작 : https://docs.microsoft.com/ko-kr/dotnet/core/tutorials/with-visual-studio-code .

 

[참고문서]

WebSocket Server in C# : https://www.codeproject.com/Articles/1063910/WebSocket-Server-in-Csharp .
TCP & UDP 참고 영상 : https://www.youtube.com/watch?v=MW91_l2dnnU&t=661s .

 

1. 검증된 클라이언트 사용.

서버를 만들기 전 검증된 클라이언트를 만들어 서버를 만드는 데, 잘 동작하는지 확인할 필요가 있습니다. 위의 에코서버 만들기 문서를 먼저 읽어 보시고 클라이언트에 대한 지식을 습득해 두세요.

이 문서에서는 웹소켓 클라이언트 공식 문서 ( 링크 ) 에 나오는 소스를 그대로 사용하도록 하겠습니다. 링크로가 아래처럼 페이지를 띄웁니다.

스크롤을 내리면 아래에 처럼 소스가 나올 텐데 소스를 복사해 websocketclient.html 파일을 만들고, 붙여 넣습니다. 제가 이 문서를 만들 당시의 소스는 아래와 같습니다.

<!DOCTYPE html>
  <meta charset="utf-8" />
  <title>WebSocket Test</title>
  <script language="javascript" type="text/javascript">

  var wsUri = "wss://echo.websocket.org/";
  var output;

  function init()
  {
    output = document.getElementById("output");
    testWebSocket();
  }

  function testWebSocket()
  {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }

  function onOpen(evt)
  {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
  }

  function onClose(evt)
  {
    writeToScreen("DISCONNECTED");
  }

  function onMessage(evt)
  {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
    websocket.close();
  }

  function onError(evt)
  {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }

  function doSend(message)
  {
    writeToScreen("SENT: " + message);
    websocket.send(message);
  }

  function writeToScreen(message)
  {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }

  window.addEventListener("load", init, false);

  </script>

  <h2>WebSocket Test</h2>

  <div id="output"></div>

이 파일을 한 번 실행해 보면, 에코 서버로 데이터를 보내고, 응답을 받아, 출력하고, 종료하도록 되어 있습니다. 한 번실행해 보도록 합니다. 이상이 없다면 아래와 같이 실행됩니다.

서버를 만든는 동안 계속 사용해야 하므로, 창을 열어 두도록 합니다.

 

 

2. c# 프로젝트 만들기.

평소에 잘 사용하시던 IDE 에디터를 사용하시면 될 것 같습니다. Visual Studio를 쓰면 가장 좋겠지만, 저는 맥이라, Visual Studio를 사용할 수가 없습니다. 그래서 Visual Studio Code를 통해 c# 코딩을 하겠습니다. 비주얼 코드에서 c#사용법은 https://docs.microsoft.com/ko-kr/dotnet/core/tutorials/with-visual-studio-code 에서 확인할 수 있습니다. 읽는데 3분 걸리단고 되어 있네요.

 

C# 및 Visual Studio Code 시작 - .NET Core

Visual Studio Code를 사용하여 C#에서 첫 번째 .NET Core 애플리케이션을 만들고 디버그하는 방법을 알아봅니다.

docs.microsoft.com

먼저 웹소켓 프로젝트를 관리할 디렉토리를 만들어 두도록 합니다.

그리고 비주얼 스튜디오를 열어, 위에서 생성해둔 폴더를 지정합니다.

그럼 아래와 같이 해당 폴더가 선택된 것을 확인할 수 있습니다.

그리고 주 메뉴에서, 터미널을 열어 줍니다. 맥 미리 보기지만, 공식 문서를 보니 메뉴가 같은 위치에 있습니다. (즉, 윈도우도 같은 위치에 있습니다.)

그럼 아래와 같이 화면 하단에 터미널이 나타 납니다. 저는 맥이라, 저렇게 경로가 나타나지만, 윈도우는 또 윈도우 경로가 표시됩니다.

터미널에 아래와 같이 doetnet new consol 을 입력합니다. 그럼 아래와 같이 프로젝트가 생성됩니다.

doetnet new consol

이렇게 정상적으로 되었다면, c# 프로젝트가 생성된 것을 확인할 수 있습니다.

터미널에서 동작하는 콘솔 프로그램의 프로젝트가 생성된 것인데, 프로젝트의 처음 시작 부분은 Program.cs 파일의  Main 메소드 입니다. 왼쪽의 Program.cs 파일을 눌러 파일의 내용을 확인해 보면 다음과 같습니다.

이제 프로젝트가 잘 생성되었는지 확인할 차례인데, 터미널 내용을 먼저 지워 줍니다. (여기서는 새 터미널을 열었습니다.)

터미널에 dotnet run 을 입력하면, 로그가 실행되고 종료합니다. (컴파일 시간이 조금 걸릴 수 있습니다.).

dotnet run

디버깅을 위해서는, 디버그 > 시작 을 눌러 주면됩니다.

처음에는 디버그 구성이 되어있지 않아, 구성을 위해 개발환경을 묻는데, .net core 를 눌러 줍니다.

그리고 다시 디버그 시작을 눌러 주면 아래와 같이  DEBUG CONSOLE 에 로그가 표시되는 것을 확인할 수 있습니다.

 

3. ReadLine 이용 바로 종료되지 않게 하기.

위에서 실행을 해 보면, 콘솔앱이 시작되자 마자 바로 종료되는 것을 확인할 수 있을 것 입니다. 우리는 클라이언트의 응답을 기다리기 위해, 계속 대기 상태로 들어가야하는데, 이렇게 되어 있다면, 대기가 되기 전 메인 쓰레드가 종료되어 바로 종료되거나, 좀비 프로세스가 되어, 이전 좀비 프로세스로가 물고 있는 포트를 사용하지 못할 가능성이 있습니다.

이 것을 막기 위해, 입력값을 받을 수 있도록 해 두면, 입력값을 줄 때까지, 콘솔이 대기 상태가 됩니다. 터미널에서 실행하면, 터미널에 키를 입력하면 종료되고, 디버그로 실행하면 입력을 받지 않아, 계속 실행상태가 될 수 있습니다.

아래와 같이 코드를 약간 수정하고, 디버그를 실행시켜 봅니다.

    class Program
    {
        static void Main(string[] args)
        {
            
            Console.WriteLine("웹 소켓 서버를 시작합니다.");

            // 엔터를 입력하면 앱이 종료됩니다.
            Console.ReadLine();

        }
    }

그럼 메시지를 뱉고 종료는 되지 않고 계속 대기 상태에 빠집니다.

상단의 디버그 메뉴에서 정지를 시켜줍니다.

자 그럼 서버를 구축하기 위한 준비가 완료되었습니다.

 

4. 코드 작성하기.

아래와 같이 파일 추가 버튼을 눌러줍니다. (마우스를 해당 위치로 가져가야 나타 납니다.)

그리고, wsServer.cs 라고 wsServer 클래스용 파일을 만들어 줍니다.

생성자 부분은 소켓을 생성하고, 클라이언트 접속을 기다리는 코드를 추가합니다.

그리고, 클라이언트가 접속했을 때 처리하는 메소드를 정의 합니다. 현재는 클라이언트 소켓을 따로 저장하거나 하지 않습니다. 이유는, 클라이언트에서 단발성으로 데이터를 보내고 끊어지도록되어 있기 때문입니다. 여러분은 필요에 따라, 구현하시면되겠습니다.

*현재의 wsServer.cs 파일 코드.

using System;
using System.Net;
using System.Net.Sockets;




namespace vsWebSocketServer
{


    class wsServer
    {


        // 초기화 시 입력 받는 값들.
        public string addr = "";
        public int port = 0;


        // 동작에 사용되는 멤버 변수들.
        TcpListener listner = null;
        TcpClient client = null;



        // 생성자를 만들어 주도록 합니다.
        public wsServer( string _addr, int _port )
        {

            // 입력 받은 값들을 저장해 줍니다.
            addr = _addr;
            port = _port;



            listner = new TcpListener( IPAddress.Parse( addr ), port );


            listner.Start();
            Console.WriteLine( "웹소켓 서버를 오픈하도록 합니다." );


            listner.BeginAcceptTcpClient( OnServerConnect, null);
            Console.WriteLine( "클라이언트와의 접속을 기다립니다." );



        }


        // 서버의 접속이 들어 왔을 때 처리하는 메소드 입니다.
        void OnServerConnect( IAsyncResult ar )
        {

            client = listner.EndAcceptTcpClient( ar );
            Console.WriteLine( "한 클라이언트가 접속했습니다." );

            // 클라이언트의 접속을 다시 대기.
            listner.BeginAcceptTcpClient( OnServerConnect, null);
        
        }


    }


}

그리고, Program.cs 파일도 127.0.0.1, 80 번 포트로 wsServer 클래스 파일을 호출 할 수 있도록 해당 내용을 추가합니다. 아래 코드는 Program.cs 파일 전체지만, 실제,  wsSever ws = new wsServer("127.0.0.1", 80); 한줄만 추가하였습니다.

using System;

namespace vsWebSocketServer
{
    class Program
    {
        static void Main(string[] args)
        {


            Console.WriteLine("웹 소켓 서버를 시작합니다.");

            wsServer ws = new wsServer( "127.0.0.1", 8181 );

            // 엔터를 입력하면 앱이 종료됩니다.
            Console.ReadLine();


        }
    }
}

그리고, websocketclient.html 파일도, 우리의 로컬 웹소켓 서버를 호출할 수 있도록, 주소를 변경해 줍니다.

이제 서버 부부분을 디버깅 합니다.

그럼 코드가 디버깅 되고, 아래처럼 클라이언트의 응답을 기다립니다.

그럼 websocketclient.html 파일을 실행해 봅시다. 그럼 미처 다 실행되지 않고, 아래 처럼 서버의 응답을 기다립니다.

우리 서버쪽을 보면, 클라이언트가 접속했음을 확인할 수 있는 로그가 뜹니다.

클라이언트를 몇 번 새로고침 해 보면, 클라이언트가 계속 접속을 시도하는 것을 확인할 수 있습니다.

여기 까지 왔다면 일단 웹소켓으로 접속이 된 것으로 이후는 큰 문제를 발생시키지 않습니다. 이제 서버를 종료 시켜 줍니다.

그럼 서버가 종료되고, 서버와 접속을 유지하던 클라이언트도, 접속이 끊어저 접속이 끊어졌음을 표시해 줍니다.

연결을 처리하기 위한 코드가 필요한데, wsServer 클래스에, onAcceptReader( IAsyncResult ) 메소들를 추가해 줍니다. 응답을 돌려주는 부분은 RFC 6455 규격이 정해져 있습니다.

        void onAcceptReader( IAsyncResult ar )
        {

            // 받은 데이터의 길이를 확인합니다.
            int receiveLength = clientStream.EndRead( ar );


            // 받은 데이터가 없는 경우는 접속이 끊어진 경우 입니다.
            if( receiveLength <= 0 )
            {
                Console.WriteLine( "접속이 끊어졌습니다." );
                return;
            }


            // 받은 메시지를 출력합니다.
            string newMessage = Encoding.UTF8.GetString( readBuffer, 0, receiveLength );
            Console.WriteLine(
                string.Format("받은 메시지:{0}\n", newMessage)
            );


            // 첫 3문자가 GET으로 시작하지 않는 경우, 잘못된 접속이므로 종료합니다.
            if( !Regex.IsMatch( newMessage, "^GET" ) )
            {
                Console.WriteLine( "잘못된 접속 입니다." );
                client.Close();
                return;
            }

            // 클라이언트로 응답을 돌려 줍니다.
            const string eol = "\r\n"; // HTTP/1.1 defines the sequence CR LF as the end-of-line marker



            // 보낼 메시지.
            string resMessage = "HTTP/1.1 101 Switching Protocols" + eol
                + "Connection: Upgrade" + eol
                + "Upgrade: websocket" + eol
                + "Sec-WebSocket-Accept: " + Convert.ToBase64String(
                    System.Security.Cryptography.SHA1.Create().ComputeHash(
                        Encoding.UTF8.GetBytes(
                            new Regex("Sec-WebSocket-Key: (.*)").Match(newMessage).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                        )
                    )
                ) + eol
                + eol
                ;



            // 보낸 메시지를 출력해 봅니다.
            Console.WriteLine(
                string.Format("보낸 메시지:{0}\n", resMessage)
            );



            // 메시지를 보내 줍니다.
            Byte[] response = Encoding.UTF8.GetBytes( resMessage );
            clientStream.Write(response, 0, response.Length);



        }

그리고, 기존의 클라이언트가 접속했을 때, 코드를 각각확인할 수 있습니다. 접속처리를 하는 부분을 아래와 같이 처리하도록 합니다.

        void OnServerConnect( IAsyncResult ar )
        {

            client = listner.EndAcceptTcpClient( ar );
            Console.WriteLine( "한 클라이언트가 접속했습니다." );

            // 클라이언트의 접속을 다시 대기.
            listner.BeginAcceptTcpClient( OnServerConnect, null);

            // 현재의 클라이언트로 부터 데이터를 받아 옵니다.
            clientStream = client.GetStream();
            clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onAcceptReader, null );

        
        }

서버를 다시 디버깅해 아래처럼 대기 상태로 놓습니다.

그리고, websocketclient.html 파일을 새로고침 하면, 아래 처럼, 접속이 완료 되고 메시지를 보내는 것 까지 확인이 됩니다.

받은 로그를 보면, 또 아래와 같은 메시지가 나옵니다. 중요한 부분은  Sec-WebSocket-Key 부분입니다. 클라이언트가 매번 바꿔 보내 줍니다. 이 값을 활용해 데이터 보낼때 사용합니다.

이제 데이터 받는 부분을 추가해 봅니다. 종료는 한번에 처리 했습니다.

        void onEchoReader( IAsyncResult ar )
        {

            // 받은 데이터의 길이를 확인합니다.
            int receiveLength = clientStream.EndRead( ar );



            // 받은 데이터가 6인 경우는 종료 상태 일 뿐이므로, 종료 데이터를 보내고 우리도 접속을 종료합니다.
            if( receiveLength == 6 )
            {
                Console.WriteLine( "접속 해제 요청이 와 접속을 종료합니다." );
                client.Close();
                return;
            }



            // 받은 데이터가 없는 경우는 접속이 끊어진 경우 입니다.
            if( receiveLength <= 0 )
            {
                Console.WriteLine( "접속이 끊어졌습니다." );
                return;
            }




            BitArray maskingCheck = new BitArray( new byte[]{ readBuffer[1] } );
            int receivedSize = (int)readBuffer[1];
            
            byte[] mask = new byte[]{readBuffer[2], readBuffer[3], readBuffer[4], readBuffer[5]};

            if( maskingCheck.Get(0) )
            {
                Console.WriteLine( "마스킹 되어 있습니다." );
                receivedSize -= 128;            // 마스킹으로 인해 추가된 값을 빼 줍니다.
            }
            else
            {
                Console.WriteLine( "마스킹 되어 있지 않습니다." );
            }
            

            // 문자열을 길이를 파악합니다.
            Console.WriteLine( "받은 데이터 길이 비트 : {0}", receivedSize );
            Console.WriteLine( "받은 데이터 길이 : {0}", receiveLength );




            // 받은 메시지를 디코딩해 줍니다.
            byte[] decodedByte = new byte[ receivedSize ];
            for( int _i = 0; _i < receivedSize; _i++ )
            {

                int curIndex = _i + 6;
                decodedByte[_i] = (byte)( readBuffer[curIndex] ^ mask[_i % 4] );

            }


            // 받은 메시지를 출력합니다.
            // string newMessage = Encoding.UTF8.GetString( readBuffer, 6, receiveLength - 6 );
            string newMessage = Encoding.UTF8.GetString( decodedByte, 0, receivedSize );
            Console.WriteLine(
                string.Format("받은 메시지:{0}", newMessage)
            );



            string sendSource = "이 문자열이 보이면 성공!";
            byte[] sendMessage = Encoding.UTF8.GetBytes( sendSource );


            // 보낼 메시지를 만들어 줍니다.
            List< byte > sendByteList = new List<byte>();


            // 첫 데이터의 정보를 만들어 추가합니다.
            BitArray firstInfor = new BitArray(
                new bool[]{

                    true                // FIN
                    , false             // RSV1
                    , false             // RSV2
                    , false             // RSV3

                    // opcode (0x01: 텍스트)
                    , false
                    , false
                    , false
                    , true

                }
            );
            byte[] inforByte = new byte[1];
            firstInfor.CopyTo( inforByte, 0 );
            sendByteList.Add( inforByte[0] );

            // 문자열의 길이를 추가합니다.
            sendByteList.Add( (byte)sendMessage.Length );

            // 실제 데이터를 추가합니다.
            sendByteList.AddRange( sendMessage );
            
            









            // 보낸 메시지를 출력해 봅니다.
            Console.WriteLine(
                string.Format("보낸 메시지:\n{0}", sendSource)
            );



            // 받은 메시지를 그대로 보내 줍니다.
            clientStream.Write(sendByteList.ToArray(), 0, sendByteList.Count);
            Console.WriteLine( string.Format( "보낸 메시지 길이:{0}", sendByteList.Count ) );




            // 또 다음 메시지를 받을 수 있도록 대기 합니다.
            clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onEchoReader, null );


        }

그리고, 클라이언트 접속 처리를 하던, onAcceptReader 메소드 끝에, 아래와 같이 한줄을 추가해, 클라이언트의 응답을 대기하도록 합니다.

clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onEchoReader, null );

그리고, 클라이언트를 시작하고, 웹페이지를 리로드하면, 다음과 같은 화면을 볼 수 있습니다.

이 코드에서 중요한 점은 메시지를 보낼 때의 서식 입니다. 아래와 같은데, 비트 다위로 표시되어 있습니다.

0번 비트는 이게 메시지 끝인지 여부인데, 1이면, 끝 0 이면, 문서를 나눠 보냈으며, 다 오려면 기다려야 함을 표시합니다.

1~3번 비트는 사용하지 않습니다.

4~7번 비트는 이 데이터의 속성을 나타냅니다. 이 4비트를 부호없는 정수로 읽어, text =1 , Binary = 2, Disconnect = 8, Ping = 9, Pong = 10 입니다.

핑은 현재 상대가 연결중인지 확인하기 위한 데이터이며, 받은 쪽은 받은 데이터 그대로 퐁을 보내야 합니다.

8 번 비트는 마스킹 되어 있는지 여부, 9~15 번 비트는 부호없는 정수로 값을 읽어 데이터 길이 입니다. 125인 경우 그대로 길이이며, 126인경우, 뒤에 2바이트를 부호없는 정수로 읽어 문자열의 길이를 나타내며, 127인 경우, 뒤에 4바이트를 부호없는 정수로 읽어 문자열의 길이를 나타냅니다.

앞서 8번째 비트가 마스킹 되어 있는지 의 비트라고 했는데, 만약 마스킹되어 있다면, 길이 이후 4바이트는 마스크 값, 이 후가 데이터 입니다. 마스킹 되어 있지않다면, 바로 데이터 입니다.

마스크 되어 있는 경우는 마스크를 해제 해야 하며 아래의 공식으로 마스크를 해제 할수 있습니다.

for (int i = 0; i < encoded.Length; i++) {
    decoded[i] = (Byte)(encoded[i] ^ mask[i % 4]);
}

 

5. 모든 코드.

* Program.cs

    class Program
    {
        static void Main(string[] args)
        {


            Console.WriteLine("웹 소켓 서버를 시작합니다.");

            wsServer ws = new wsServer( "127.0.0.1", 8181 );

            // 엔터를 입력하면 앱이 종료됩니다.
            Console.ReadLine();


        }
    }

* wsServer.cs

    class wsServer
    {


        // 초기화 시 입력 받는 값들.
        public string addr = "";
        public int port = 0;


        // 읽기에 사용되는 값들.
        NetworkStream clientStream = null;
        byte[] readBuffer = new byte[500000];
        


        // 동작에 사용되는 멤버 변수들.
        TcpListener listner = null;
        TcpClient client = null;



        // 생성자를 만들어 주도록 합니다.
        public wsServer( string _addr, int _port )
        {

            // 입력 받은 값들을 저장해 줍니다.
            addr = _addr;
            port = _port;



            listner = new TcpListener( IPAddress.Parse( addr ), port );


            listner.Start();
            Console.WriteLine( "웹소켓 서버를 오픈하도록 합니다." );


            listner.BeginAcceptTcpClient( OnServerConnect, null);
            Console.WriteLine( "클라이언트와의 접속을 기다립니다." );



        }


        // 서버의 접속이 들어 왔을 때 처리하는 메소드 입니다.
        void OnServerConnect( IAsyncResult ar )
        {

            client = listner.EndAcceptTcpClient( ar );
            Console.WriteLine( "한 클라이언트가 접속했습니다." );

            // 클라이언트의 접속을 다시 대기.
            listner.BeginAcceptTcpClient( OnServerConnect, null);

            // 현재의 클라이언트로 부터 데이터를 받아 옵니다.
            clientStream = client.GetStream();
            clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onAcceptReader, null );




        
        }


        // 클라이언트의 데이터를 읽어 오는 메소드 입니다.
        void onAcceptReader( IAsyncResult ar )
        {

            // 받은 데이터의 길이를 확인합니다.
            int receiveLength = clientStream.EndRead( ar );


            // 받은 데이터가 없는 경우는 접속이 끊어진 경우 입니다.
            if( receiveLength <= 0 )
            {
                Console.WriteLine( "접속이 끊어졌습니다." );
                return;
            }


            // 받은 메시지를 출력합니다.
            string newMessage = Encoding.UTF8.GetString( readBuffer, 0, receiveLength );
            Console.WriteLine(
                string.Format("받은 메시지:\n{0}", newMessage)
            );


            // 첫 3문자가 GET으로 시작하지 않는 경우, 잘못된 접속이므로 종료합니다.
            if( !Regex.IsMatch( newMessage, "^GET" ) )
            {
                Console.WriteLine( "잘못된 접속 입니다." );
                client.Close();
                return;
            }

            // 클라이언트로 응답을 돌려 줍니다.
            const string eol = "\r\n"; // HTTP/1.1 defines the sequence CR LF as the end-of-line marker



            // 보낼 메시지.
            string resMessage = "HTTP/1.1 101 Switching Protocols" + eol
                + "Connection: Upgrade" + eol
                + "Upgrade: websocket" + eol
                + "Sec-WebSocket-Accept: " + Convert.ToBase64String(
                    System.Security.Cryptography.SHA1.Create().ComputeHash(
                        Encoding.UTF8.GetBytes(
                            new Regex("Sec-WebSocket-Key: (.*)").Match(newMessage).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                        )
                    )
                ) + eol
                + eol
                ;



            // 보낸 메시지를 출력해 봅니다.
            Console.WriteLine(
                string.Format("보낸 메시지:\n{0}", resMessage)
            );



            // 메시지를 보내 줍니다.
            Byte[] response = Encoding.UTF8.GetBytes( resMessage );
            clientStream.Write(response, 0, response.Length);



            // 에코 메시지 받기 시작.
            clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onEchoReader, null );


        }



        // 에코 메시지를 받아 오는 부분 입니다.
        void onEchoReader( IAsyncResult ar )
        {

            // 받은 데이터의 길이를 확인합니다.
            int receiveLength = clientStream.EndRead( ar );



            // 받은 데이터가 6인 경우는 종료 상태 일 뿐이므로, 종료 데이터를 보내고 우리도 접속을 종료합니다.
            if( receiveLength == 6 )
            {
                Console.WriteLine( "접속 해제 요청이 와 접속을 종료합니다." );
                client.Close();
                return;
            }



            // 받은 데이터가 없는 경우는 접속이 끊어진 경우 입니다.
            if( receiveLength <= 0 )
            {
                Console.WriteLine( "접속이 끊어졌습니다." );
                return;
            }




            BitArray maskingCheck = new BitArray( new byte[]{ readBuffer[1] } );
            int receivedSize = (int)readBuffer[1];
            
            byte[] mask = new byte[]{readBuffer[2], readBuffer[3], readBuffer[4], readBuffer[5]};

            if( maskingCheck.Get(0) )
            {
                Console.WriteLine( "마스킹 되어 있습니다." );
                receivedSize -= 128;            // 마스킹으로 인해 추가된 값을 빼 줍니다.
            }
            else
            {
                Console.WriteLine( "마스킹 되어 있지 않습니다." );
            }
            

            // 문자열을 길이를 파악합니다.
            Console.WriteLine( "받은 데이터 길이 비트 : {0}", receivedSize );
            Console.WriteLine( "받은 데이터 길이 : {0}", receiveLength );




            // 받은 메시지를 디코딩해 줍니다.
            byte[] decodedByte = new byte[ receivedSize ];
            for( int _i = 0; _i < receivedSize; _i++ )
            {

                int curIndex = _i + 6;
                decodedByte[_i] = (byte)( readBuffer[curIndex] ^ mask[_i % 4] );

            }


            // 받은 메시지를 출력합니다.
            // string newMessage = Encoding.UTF8.GetString( readBuffer, 6, receiveLength - 6 );
            string newMessage = Encoding.UTF8.GetString( decodedByte, 0, receivedSize );
            Console.WriteLine(
                string.Format("받은 메시지:{0}", newMessage)
            );



            string sendSource = "이 문자열이 보이면 성공!";
            byte[] sendMessage = Encoding.UTF8.GetBytes( sendSource );


            // 보낼 메시지를 만들어 줍니다.
            List< byte > sendByteList = new List<byte>();


            // 첫 데이터의 정보를 만들어 추가합니다.
            BitArray firstInfor = new BitArray(
                new bool[]{

                    true                // FIN
                    , false             // RSV1
                    , false             // RSV2
                    , false             // RSV3

                    // opcode (0x01: 텍스트)
                    , false
                    , false
                    , false
                    , true

                }
            );
            byte[] inforByte = new byte[1];
            firstInfor.CopyTo( inforByte, 0 );
            sendByteList.Add( inforByte[0] );

            // 문자열의 길이를 추가합니다.
            sendByteList.Add( (byte)sendMessage.Length );

            // 실제 데이터를 추가합니다.
            sendByteList.AddRange( sendMessage );
            
            









            // 보낸 메시지를 출력해 봅니다.
            Console.WriteLine(
                string.Format("보낸 메시지:\n{0}", sendSource)
            );



            // 받은 메시지를 그대로 보내 줍니다.
            clientStream.Write(sendByteList.ToArray(), 0, sendByteList.Count);
            Console.WriteLine( string.Format( "보낸 메시지 길이:{0}", sendByteList.Count ) );




            // 또 다음 메시지를 받을 수 있도록 대기 합니다.
            clientStream.BeginRead( readBuffer, 0, readBuffer.Length, onEchoReader, null );


        }




    }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'C#' 카테고리의 다른 글

c# Reflection  (0) 2019.05.06
Posted by 창업닉군
,

 Push Notification 서버를 기본 REST API 형태로 구현하려고 했으나, 애플 공식 문서를 보는 순간, 아, 그냥 node js의 모듈을 이용해야 겠다는 생각이 들었습니다. iOS 서버를 따로 만들어 구동 방식을 이해하고, Fire base 에 통합할 예정이라, 별 고민 없이 node의 모듈을 이용하게 되었습니다.

node js의 apn 이라는 모듈을 이용할 예정이며, 선행작업으로는 프로비저닝 프로파일 및 푸쉬 인증서, 클라이언트 푸쉬구현이 되어 있어야 합니다.

 

선행 작업.

프로비저닝 프로파일 및 푸쉬 인증서 생성 : https://nicgoon.tistory.com/202 (필수)
클라이언트 Push 구현 : https://nicgoon.tistory.com/203 (필수)

 

공식 문서.

푸쉬서버 설치 : https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server .
APNs로 알림요청 : https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns .
푸쉬 메시지 구조 : https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification .
node-apn 문서 : https://www.npmjs.com/package/apn .

 

참고 문서.

참고 1. https://g-y-e-o-m.tistory.com/72 .
참고 2. http://blog.naver.com/PostView.nhn?blogId=jipmouse&logNo=220829918858 .

 

1. apn 설치.

npm i apn --save

 

2. 인증서 복사.

위의 선행 작업 문서 중, [프로비저닝 프로파일 및 푸쉬 인증서 생성] 링크를 눌러 나타나는 문서에, 푸쉬 인증서 .pem 을 만드는 방법이 있습니다. 여기서 푸쉬 인증서 pem 파일과, 푸쉬 인증서의 key pem 파일(암호를 풀어 놓은 것) 을 만들어 이것을 복사해 둡니다.

저는 인증서 파일의 이름은 [yeslife_ap_cer.pem] 이며, 키 파일의 이름은 [yeslife_ap_key.unencrypted.pem] 입니다.

 

3. apn 프로바이더 만들기.

옵션을 만들어 프로바이더를 생성하는 구조이며, 옵션은 토큰을 넣는 방법, 인증서를 넣는 방법 2가지가 있습니다. 저는 인증서를 넣는 방법을 선택했습니다. 아래 2가지 방법 중 하나로 옵션을 만들면 되겠습니다.

1) 푸쉬 인증서로 보내기.

gateway는 개발단계에서는  gateway.sandbox.push.apple.com 이며, 실제 서비스 할 때는 gateway.push.apple.com 입니다. 해당 내용은 애플 개발자 공식문서 (링크) 에 나와 있습니다.

var option = {
    gateway:"gateway.sandbox.push.apple.com",
    cert:'yeslife_ap_cer.pem',
    key:'yeslife_ap_key.unencrypted.pem'
};

let apn_provider = new apn.Provider( option );

 

2) 토큰 으로 보내기.

먼저, 애플 개발자 사이트의 Certificates, Identifiers & Profiles 부분페이지에서 토큰을 생성하고 다운로드 받도록 하겠습니다. Certificates, Identifiers & Profiles 페이지로 에서 Keys, All 을 선택하고, + 버튼을 눌러 주도록 합니다.

나온 페이지에서, Name 과 APNs 항목을 클릭하고, Continue 버튼을 눌러 주도록 합니다.

키를 만들기 전 입력사항이 맞는지 확인하는 페이지에서 Confirm을 눌러 생성하도록 합니다.

키가 만들어 졌습니다. Key ID는 중요하므로, 꼭 적어 두도록 합니다. 그리고 키는 한 번만 다운로드 가능합니다. 옵션은 아래와 같은 요령으로 만들어 주면되겠습니다. Production은 개발인지 실제 서비스하는 지 여부 입니다. (호출 주소가 틀림).

var option = {
    token : {
        key : '.p8 파일 경로',
        keyId:'키를 만들 때 표시된 아이디',
        teamId: "팀명"
    },
    production: false
};

 

4. 보낼 내용 및 장치 토큰 저장해 두기.

앱이 켜지면,  APNs 에 앱이 등록되는 데, 성공하면, 토큰을 받아 옵니다. 이 값을 deviceToken 값에 저장해 줍니다.

보낼 내용은 apn.Notification 객체에 저장이 되는 데, 이 부분은 공식 문서의 payload 부분을 참고 하면 되겠습니다.

// 앱에서 APNs에 앱을 등록하고, 얻은 값.
let deviceToken = 'A89B3B5731AD3821D0AA316CFBC6E54E750C216ED6308112CE437BAC3730005F';



// 보낼 데이터를 만들어 줍니다.
var note = new apn.Notification();

// 보내기 실패할 경우 언제까지 재시돌 할 것인지 여부.
note.expiry = Math.floor( Date.now() / 1000 ) + 3600;

// 앱의 아이콘에 표시될 숫자. ( 우리가 흔히 몇 개의 메시지가 있다고 인식하는 )
note.badge = 3;

// 메시지가 도착했을 때 나는 소리.
note.sound = "ping.aiff";

// 메시지 내용.
note.alert = "왓쇼이 왓쇼이";

// 누가 보냈는지 여부.
note.payload = {"messageFrom" : "메시지"};

// ios app 번들 명.
note.topic = "com.tistory.yeslife.hello";

 

 

5. 메시지 보내기.

apn.provider의 send 메소드를 이용해 보내면 됩니다.

// 실제 메시지를 보내도록 합니다.
apn_provider.send(note, deviceToken).then( function( result ){

    console.log("결과 : " + result);
    console.log( result );

}).catch( function (err){

    throw( err );
    
});

 

별다른 문제가 없다면, 메시지가 성공적으로 도착합니다.

 

 

 

 

 

 

 

'Node.js' 카테고리의 다른 글

node.js nodemailer gmail  (0) 2019.05.24
node js OAuth 2.0 Login  (0) 2019.04.16
Android 결제 서버 개발  (0) 2019.04.09
node.js FCM (Push Notification) 서버 환경 구축하기  (1) 2019.04.06
express 사용시 주의 사항.  (0) 2019.03.04
Posted by 창업닉군
,

안드로이드 결제를 하면, 이를 검증하고 DB에 반영할 서버가 필요합니다. 이 node.js로 구현해 볼 생각 입니다. Android 개발자 사이트 공식 문서에는 결제 관련 데이터 접근을 Google Play Developer API로 하도록 하고 있습니다. 이 기능은 앱의 새 버전을 업로드하고 출시 하는 기능과 인앱 상품의 구매 이력을 관리할 수 있는 기능을 구현할 수 있습니다. 저는 이 중 인앱 상품 관련 API 를 이용해 볼 생각 입니다.

[관련 사이트]

구글 플레이 개발자 API : https://developers.google.com/android-publisher
구글 클라이언트 라이브러리 : https://developers.google.com/api-client-library/

 

[관련문서]

공식문서 : https://developer.android.com/google/play/developer-api

 

[선행작업]

출시용 APK 파일 만들기 : https://nicgoon.tistory.com/198
베타 버전으로 앱 등록하기 : https://nicgoon.tistory.com/199
안드로이드 클라이언트 인앱 결제 구현 : https://nicgoon.tistory.com/200

 

1. 참고 문서보기.

구글 플레이 개발자 API의 안내속 시작 하기 문서(링크) 속에는 구글 플레이 개발자 API기능에 대한 설명이 들어 있습니다. 여러분이 보는 시기에 이 문서가 어떻게 표시되는지 모르겠지만, 제가 보고 있는 문서는 한글로 번역이 되어 있습니다. 이 문서를 읽어 보면, 개발 진행사항에서 API를 통해, 개발 버전을 관리할 수 있고, 상품에 달려 있는 리플들을 가지고와 관리할 수 있는 기능, 구매 관련 기능이 있음을 설명하고 있습니다. 저는 이 중 구매 관련, (지정한 상품이 유효한지 여부)를 확인하는 기능을 구현할 생각 입니다. 이 기능을 이용해 유저가 앱에서 실제 구매 했는지 검증을 할 것 입니다.

 

2. 새 API 프로젝트 생성.

구글 플레이 콘솔에서, 가장 아래 톱니바퀴 모양의 설정 버튼을 눌러 줍니다.

설정 페이지가 나오면, 개발자 계정 > API 액세스 를 눌러 해당 페이지를 호출 합니다. 페이지가 나오면, 새 프로젝트를 눌러 API 프로젝트를 만들어 API 생성 페이지를 만들어 줍니다.

 

그렇게  API 프로젝트를 만들고 나면,  Google Play Android Develope, Game Service Publishing API 두개의 프로젝트가 설정되어 있습니다. 이 것은 Goolge Play Android Developer 만 연결된 상태로 되어 있는데 그대로 두면 됩니다. 

2. Autho 클라이언트 만들기.

결제등의 데이터를 확인할 때, 구글 플레이 서버는 서버라 칭하고, 구글 플레이 서버에 데이터를 요청하는 쪽을 클라이언트라고 합니다. 우리는 node js를 통해, 구글 플레이 서버에 데이터를 요청하지만, 요청자 이므로, 클라이언트라 불립니다. node js를 통해 접근할 때 사용할 클라이언트 계정을 만드는 단계 입니다.

 

그럼 자동으로 OAuth 클라이언트가 생성됩니다. 아이디는 바로 표시되지만, 비밀번호는 표시되지 않고, 구글 APIs로 이동한 뒤 확인하도록 되어 있습니다. Google Play Console에서 보기 버튼을 눌러 사용자 인증 페이지 창을 부릅니다.

 

그냥 만들어진 사용자는 지워 버립니다. 이 계정은 구글에서 사용하는 클라이언트 라이브러리를 사용할 때, 사용하게 되는 계정입니다. 우리는, nodejs REST FULL API 바식으로 Autho 2.0 인증을 사용할 것 입니다.

 

그럼 구글 APIs의 사용자 인증 정보 페이지가 표시되고, 클라이언트 아이디 리스트가 나타납니다. 여기서 비밀 번호를 바로 확인할 수는 없고, 연필버튼을 눌러 수정 창으로 들어가서 확인이 가능합니다. 참고로 말씀 드리면, 우리가 사용할 Google Play Android Developer는 구글 APIs 사이트의 하나의 API 입니다. (즉 바로 오셔도 됩니다.).

 

자 새로운 페이지에서 사용자 인증 정보 만들기 버튼을 눌러 풀다운 메뉴가 표시되면, OAuth 클라이언트 ID 항목을 선택해 계정 생성 창을 띄워 줍니다.

 

그럼 OAuth 클라이언트 ID 만들기 창이 생성되고, [애플리케이션 유형]은 [웹 애플리케이션]으로 선택 [이름]은 적당히, 승인된 리디렉션 URI 는 http://localhost/androidpublisher/exchange_token로 입력하고 (엔터 꼭,), 입력된 것을 확인 후 생성 버튼을 누릅니다.

 

3. 서비스 계정 만들기.

Autho2 로그인의 가장 큰 특징은 서버에 유저 패스워드나 비밀번호를 두지 않는 다는 점 입니다. 바로 SNS 등의 계정을 통해 로그인을 할 수 있는 데, 구글은 구글 계정을 통해 로그인을 하도록 되어 있습니다. 이 로그인을 할 때 사용하는 서비스 계정을 만들어 보도록 하겠습니다.

서비스 계정 만들기를 눌러 호출된 팝업에서, Google API 콘솔을 눌러 줍니디다.

여기서, [서비스 계정 만들기]를 눌러, 서비스 계만들기 창을 호출하도록 합니다.

적당한 값을 입력하고, [만들기] 버튼을 눌러 주도록 합니다.

이후 역할을 지정하도록 되어 있는데, 별다른 역할 없이 계속 계속을 눌러주면 되겠습니다.

 

4. node js로 영수증 검증 페이지를 만들기 위한 사전 정보 조사.

   1) 클라이언트 아이디, 시크릿 키, 리딕렉션 URI, 서비스 계정.

   이 값들은 위에서, 모두 보셨거나 직접 입력한 값들 입니다. 하지만, 언제든 다시 확인할 필요성이 있으므로, 다시 확인하는 방법을 알려 드립니다. 먼저 플레이 콘솔로 접속 합니다. ( 링크 ). 그리고, 나온 페이지 좌측에 설정을 눌러 줍니다.

   나온 페이지에서, [개발자 계정] > [API 액세스] 를 선택해 나온 페이지에서, 서비스 계정을 확인해 둡니다.

그리고, 구글 플레이 콘솔에서, 보기를 눌러, 사용자 인증 정보 창이 뜨면 클라이언트 아이디의 이름 부분을 클릭해 정보 페이지를 호출합니다.

클라이언트 ID, 클라이언트 보안 비밀, 승인된 리디렉션 URI 를 확인해 둡니다.

 

5. node js 사전 준비.

이 내용은 express framework를 이용해, router 다루는 방법을 알아야 합니다. nodejs로 웹서버 구축까지 싣기에는 양이 너무 많습니다.

express 서버를 포트 80번으로 설치해 localhost를 기본적으로 사용할 수 있도록 합니다.

 

1) request 모듈 설치.

$ npm i request

서버에서 다른 웹사이트의 내용을 읽어 올 때 사용됩니다.

 

2) querystring 모듈 설치.

$ npm i query

GET으로 리디렉션 할 때, URL 인코딩 (특수 문자들을 주로소 넣어도 문제가 없도록) 하도록 합니다.

 

3) androidpublisher.js 라우트 만들어 연결 시키기. 소스는 아래와 같습 니다. 일단 동작을 하는 모듈은 넣지 않고 가장 기본적인 상태로 두었습니다.

var express = require('express');
var router = express.Router();


// 필요한 모듈을 가지고 옵니다.
var request = require('request');
var qs = require('querystring');



router.get('/:name', function( req, res, next ){


    // 파라메터 값을 가지고 옵니다.
    let filename = req.params.name;


    // 처리 방법이 저정되지 않으면 다음으로 넘겨 버립니다.
    next();


});



module.exports = router;

아래와 같이 익스프레스 서버에 연결하도록 합니다.

var androidpublisher = require('./androidpublisher');


app.use('/androidpublisher', androidpublisher);

 

5. node js 코드

   1) 이 항목을 작성하는데, 읽어야할 기본 문서들.

이 페이지는 제가 필요한 부분들만 다루고 있습니다. 빠진 정보가 있을 수 있으므로, 공식 문서를 확인하도록 합니다. android 서버 부분은 android 개발 부분 뿐 이니라, 구글 APIs 부분도 봐야 합니다.

안드로이드 개발자 페이지에서, 서버 부분을 언급한 페이지 : https://developer.android.com/google/play/developer-api (소개수준)
구글 아이덴티티 플랫폼 OAuth 2.0 개요 : https://developers.google.com/identity/protocols/OAuth2 (많은 도움이 됩니다.)
웹서버 응용프로그램 (OAuth 2.0 구현) : https://developers.google.com/identity/protocols/OAuth2WebServer (실제 사용문서)
권한 범위 : https://developers.google.com/identity/protocols/googlescopes (OAuth 2.0 에서 권한을 요청할 때 사용되는 값들.)

위 문서들 중 별다은 언급이 없이 공식 문서를 참조 했다고 하면, 웹서버 응용프로그램 (OAuth 2.0 구현) 문서라 생각하시면되겠습니다.

 

   2) 로그인 페이지 호출 하게 하기.

OAuth 2.0의 핵심은, API를 가진 서버에, 자원을 활용하기 위해 웹브라우저를 이용하고, 우리 서버는 이 계정의 정보를 처리 하지 않음으로써, 안정성을 높이고, 우리는 계정 관리의 부담을 더는 것입니다. 즉, 우리서버를 아무리 해킹해도, 계정 정보는 남아 있지 않습니다.

이러한 작업을 할 수 있는 페이지를 호출 하고, 입력했던 리디렉션 주소로 결과를 받아 이 것이 올바르게 로그인 되었는지 한 번더 확인을 위한 토큰 교환을 시도하고, 성공하면 받은 토큰으로 API를 가진 서버의 자원에 접근할 수 있게 됩니다.

일단 저는 이 작업을 할 수 있는 페이지를 호출 하기위해 get_code 라는 페이지를 통해 페이지를 호출 할 수 있도록 하였습니다.

위 라우터에 http://localhost/androidpublisher/get_code 라고 브라우저 주소창에 입력하면, 구글 로그인 페이지가 호출 될 수 있도록 하기위해 다음과 같이 코드를 변경합니다. 앞으로 사용할 get_code와 exchange_token 메소드를 분기를 통해 처리할 수 있도록 하였습니다.
중요한 점은 recently_token 변수 인데, 잊지 않고 선언해 주도록 합니다.

router.get('/:name', function( req, res, next ){


    // 파라메터 값을 가지고 옵니다.
    let filename = req.params.name;


    if( filename == "get_code" )
    {
        get_code( req, res, next );
        return;             // 리턴 중요.
    }

    else if( filename == "exchange_token" )
    {
        exchange_token( req, res, next );
        return;             // 리턴 중요.
    }

    // 처리 방법이 저정되지 않으면 다음으로 넘겨 버립니다.
    next();


});



// 처음 코드를 가지고 오는 메소드 입니다.
function get_code( req, res, next )
{

}


// 토큰을 받고, 저장한 값.
var recently_token = "";


// 코드를 가지고 오는 메소드 입니다.
function exchange_token( req, res, next )
{

}

이렇게 입력하고, 코드를 가지고 오는 부분에서 공식 문서에서 지정한 로그인 페지를 불러 올 수 있도록, 주소를 입력합니다. 공식문서의 HTTP/REST 부분을 봐야 합니다.

코드로 다음과 같이 하면 될 것 같습니다. 여기서 사용되는 클라이언트 아이디, 클라이언트 시크릿 키, 리디게션 url은 위에서 모두 확인했던 값입니다. socpe (권한 범위는), 권한 범위 문서 (위에 링크 있음)을 참고 하시면됩니다.

// 결제 관리를 위한 상수값들 입니다. (앞서 확인했던 값들, 적당한 곳에서 상수로 선언).
var inn_client_id = "769707945781-kqe7j3bk8j3kbq3drliqv5josu74ukgn.apps.googleusercontent.com";
var inn_client_secret = "BfVV8gCKVlvGR_7G7E3IlWtN";
var inn_redirect_url = "http://localhost/androidpublisher/exchange_token";




// 처음 코드를 가지고 오는 메소드 입니다.
function get_code( req, res, next )
{


    // 코드를 요청하기 위한 주소를 만들어 줍니다.
    const url = "https://accounts.google.com/o/oauth2/v2/auth";
    const scope = "https://www.googleapis.com/auth/androidpublisher";


    // 코드를 획득하기 위한 요청을 만들어 줍니다.
    let code_url = url
    + `?scope=${ qs.escape(scope) }`
    + `&access_type=offline`
    // + `&include_granted_scopes=true`
    + `&redirect_uri=${ qs.escape(inn_redirect_url) }`
    + `&response_type=code`
    + `&client_id=${inn_client_id}`
    ;


    // 리디렉션 페이지를 호출 합니다.
    res.redirect( code_url );


}

이렇게 입력하면, 계정으로 로그인 창이 뜰 것 입니다.

 

   3) 토큰 교환.

그럼 토큰을 교환하는 페이지를 만들도록 합시다. 위 로그인을 시도하면, 성공시, 입력한 리디렉션 (http://localhost/androidpublisher/exchange_token) 페이지를 마지막으로 호출 하며, 코드를 get 타입으로 서버에 넘겨 줍니다. 받은 코드로 토큰 교환을 시도할 것이며, 보안을 위해, POST 메소드, 그리고, 서버 내에서 교환할 것 입니다. POST로 다른 서버의 REST API를 호출하기 위해서는 request 모듈이 필요합니다. (그냥 참고).

exchange_token 부분을 아래와 같이 코드를 작성하면, 토큰 교환이 완료됩니다.

// 토큰을 받고, 저장한 값.
var recently_token = "";


// 코드를 가지고 오는 메소드 입니다.
function exchange_token( req, res, next )
{


    let postForm = {
        grant_type: "authorization_code"
        , code: req.query.code
        , client_id: inn_client_id
        , client_secret: inn_client_secret
        , redirect_uri: inn_redirect_url
    };


    // form 데이터 형태로 토큰을 교환할 것 이므로, 폼데이터를 만들어 줍니다.
    request.post(
        "https://www.googleapis.com/oauth2/v4/token"
        , {form: postForm}, function( error, response, body ){

            // 오류가 난 경우 오류를 출력하도록 합니다.
            if( error )
            {
                res.end('Error!!');
                return;
            }


            let parsed_body = JSON.parse( body );
            recently_token = parsed_body.access_token;

            // 오류가 없을 경우 token을 그대로 받아 화면에 출력합니다.
            res.end( body );

        });

}

자 그럼, nodejs를 실행하고, 웹브라우저를 통해, http://localhost/androidpublisher/get_code 라고 입력합니다.

별문제가 없다면, 아래와 같은 창을 보게 될 것 입니다. 다른 계정 사용을 클릭 합니다.

그럼 로그인을 하라고 하는데, 여기서, 구글 플레이 개발자 계정으로 사용하는 계정(등록 비용을 지불하고 등록된) 이나, 서비스로 입력했던, 계정을 입력하면 됩니다. 여기서는 위에서 만들어 두었던 서비스 계정을 사용하도록 하겠습니다. 여기서는 편의를 위해 구글 개발자 계정(결제후 등록된) 계정으로 로그인하겠습니다. (이 부분은 그냥 넘어 가는 부분이므로, 화면 첨부를 화지 않겠습니다.).

성공했다면, 대게 이런 화면을 보게 될 것 입니다. access_token만 사용할 것이므로, 일단 이 것을 내부적으로,  recently_token에 저장되도록 하였습니다.

{
  "access_token": "ya29.GlvmBgcLk7RhR4EmBuKfPgxfiQVmQk9z57phE6kxEOu30yvM9kdYqW9Cn5zHMHmEPJISC7VdWaJMvScOFISq1A2I_-38YI861xRzwtmgJYacRnbxvVDhhomOHcEq",
  "expires_in": 3600,
  "refresh_token": "1/99m47DPXz7NVekvFGgULmpJv_jNSG7H6j0fFKeNR7j4JGiH2y8Xq53Nqp-67_XO3",
  "scope": "https://www.googleapis.com/auth/androidpublisher",
  "token_type": "Bearer"
}

 

   4) 리프레쉬 토큰을 이용한 토큰 다시 받기.

    이 부분은 다음에 시간 날 때 채워 넣도록 하겠습니다. (어렵지는 않고, 공식 문서만 참고 하셔도 충분합니다.).

 

6. 구매 검증.

구매 검증은 또, 아래 문서들을 참고해야 합니다.

Google Play Developer API : https://developers.google.com/android-publisher/getting_started ( 이 작업의 개요 부분 입니다.).
검증 작업 API : https://developers.google.com/android-publisher/api-ref/purchases/products/get ( 실제 작업 페이지 )

 

1) Android 앱에서 구매 후 구매 토큰 획득하기.

상품 구매는 Android 앱에서 일어나고, 결과로 토큰을 받아 옵니다. 상품구매후 토큰을 서버로 날려 주면 서버에서 그 토큰이 유효한지 확인하면 됩니다.
이 문서에서는 안드로이드 부분은 다루고 있지 않습니다. 아래 링크에서, 토큰을 획득하시면되겠습니다.

 

안드로이드 인앱 결제 (Android inapp billing)

Android는 내부적으로 결제를 할 수 있도록, API를 제공하고 있습니다. 이 문서는 인앱 결제에 대한 설명만을 포함하고 있습니다. 우선 앱이 프로덕션 혹은 베타테스트 출시 상태에서 보시면 좋으리라 생각합니다...

nicgoon.tistory.com

 

2) nodejs 에서 토큰 검증하기.

get을 통해 구매토큰만 입력하면, 처리가 될 수 있도록 하겠습니다. 일단 검증 함수를 라우터 속에 만들어 줍니다.

// 구입 상품 검증하는 메소드.
function validate_purchase( req, res, next )
{


    const _packageName = 'com.tistory.nicgoon.android.inapptest';
    const _productId = 'gold_1000';
    let _token = req.query.token;



    // 주소를 확인합니다.
    url = `https://www.googleapis.com/androidpublisher/v3/applications/${_packageName}/purchases/products/${_productId}/tokens/${_token}`;
    reqUrl = url + `?access_token=${recently_token}`;


    let logSTR = '받은토큰:' + req.query.token;
    console.log( logSTR );

    // res.end( reqUrl );
    res.redirect( reqUrl );

}

그리고, 라우트 메인에 검증을 처리 코드의 분기를 작성해 줍니다.

router.get('/:name', function( req, res, next ){


    // 파라메터 값을 가지고 옵니다.
    let filename = req.params.name;


    if( filename == "get_code" )
    {
        get_code( req, res, next );
        return;             // 리턴 중요.
    }

    else if( filename == "exchange_token" )
    {
        exchange_token( req, res, next );
        return;             // 리턴 중요.
    }

    // 구입 상품 검증.
    else if( filename == "validate_purchase" )
    {
        validate_purchase( req, res, next );
        return;             // 리턴 중요.
    }

    // 처리 방법이 저정되지 않으면 다음으로 넘겨 버립니다.
    next();


});

 

3) 검증 시도.

   코드를 수정했으므로, node js를 재 시작 합니다. 그리고, access_token이 없을 것이므로( 재 시작으로 인해 ), OAuth 2.0로그인 과정을 다시 해 줍니다. 

그리고, 브라우저 창에, 아래와 같이 입력하면, 상품 검증이 끝납니다. (당연히 여려분이 실 구현할 때는 내부에서 처리되도록 해야 합니다. request 모듈을 사용해, 지금은 간단히 해 보기 위해서, 브라우저를 사용합니다.

http://localhost/androidpublisher/validate_purchase?token=구매토큰

저는 앱으로 상품을 구매하고, 아래의 키를 받았습니다.

mjfiodoogjkoklolhkglhpbc.AO-J1Oy39MXZpJdwlbbMuUYB33ezvLnQRSckTNOkyZy4BRj12szzaz-z5C_WFdQGPidlPgPgEwAbJorPIvlL9hBmnJG8STaKt5BUCeBYxdduxqVE2mRo9FdRq87WbWeTZ_TBiAqVYfLUAdEt_LLnrwBCqpaI8lxGCg

그래서 요청 주소는 아래와 같이 되었습니다.

http://localhost/androidpublisher/validate_purchase?token=mjfiodoogjkoklolhkglhpbc.AO-J1Oy39MXZpJdwlbbMuUYB33ezvLnQRSckTNOkyZy4BRj12szzaz-z5C_WFdQGPidlPgPgEwAbJorPIvlL9hBmnJG8STaKt5BUCeBYxdduxqVE2mRo9FdRq87WbWeTZ_TBiAqVYfLUAdEt_LLnrwBCqpaI8lxGCg

 

아래와 같은 값을 확인했다면, 검증에 성공했고, purchaseState 값이 0 이라면, 취소되지 않은 구매 상품 입니다.

{
 "kind": "androidpublisher#productPurchase",
 "purchaseTimeMillis": "1554870248765",
 "purchaseState": 0,
 "consumptionState": 0,
 "developerPayload": "",
 "orderId": "GPA.3305-1217-4902-80622",
 "purchaseType": 0
}

 

수고하셨습니다.

 

 

Posted by 창업닉군
,