aws node js SMS 보내기

Node.js 2019. 5. 26. 02:37

aws를 통해 문자를 보내는 기능은 꽤나 복잡한 절차를 따릅니다. 문서는 잘 되어 있지만 선행해야할 작업이 많습니다. 구글 클라우드든 aws 든 사용방법은 매우 간단합니다. 하지만 설정을 하는데 매우 복잡합니다. 그리고 문자를 보내는 것은 무료는 아닙니다. 건당 대략 1~20원 가량 소요됩니다. 테스트를 위해 1달러 쯤 사용할 각오는 해야 하실 것 같습니다.

그럼 이제 부터 한 단계씩 차근 찬근 진행해 보도록 하겠습니다.

 

공식문서들:

인증자격받기 : https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html .
인증자격증명로드 : https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html .
aws 리전 : https://docs.aws.amazon.com/ko_kr/sns/latest/dg/sms_supported-countries.html .
기본 설정 지정 : https://docs.aws.amazon.com/ko_kr/sns/latest/dg/sms_preferences.html .
Amazon SNS를 통한 SMS 메시지 전송 : https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/sns-examples-sending-sms.html .

 

1. aws 계정만들기.

여기서 계정 만드는 것에 대해서는 다루지 않고, 외국에서 결제되는 신용카드는 꼭 필요합니다. 잘 못하면 계정이 잘 만들어 지지 않을 수 있지만, 같은 카드로 계정을 여러개 만드는 것은 가능하기 때문에, 크게 걱정하실 필요는 없습니다.

 

2. 액세스 키 만들기

누구의 권한으로 SMS를 보낼 것인지 확인해야 하므로, 액세스 키를 만들어 권한 설정을 해야 합니다. 하는 방법은 클릭 몇 번으로 가능합니다. 이 작업은 공식 문서 중 인증 자격 받기를 참고 합니다.

먼저 AWS Management Console에 접근합니다. ( 링크 ). 로그인 되어 있다면, 아래와 같은 페이지가 표시될 것 입니다. 이 페이지에서 IAM을 클릭해 주도록 합니다.

Identity and Access Management 소개 페이지가 호출되면, 사용자를 선택해 줍니다.

사용자 페이지에서, 사용자를 추가할 수도 있고, 기존의 사용자를 선택할수 도 있습니다. 여기에 꼭 1명의 사용자는 존재하는 데 루트 사용자 입니다. 저는 사용자를 추가하지 않고 aws 가입시 생성된 루트 사용자를 선택했습니다. 여러분은 상황에 맞게 적당한 사용자를 클릭해 주세요.

요약 페이지가 호출되면, 보안자격증명 > 액세스 키 만들기를 차례로 눌러 줍니다.

그럼 에세스 키가 자동으로 만들어 지는데, 비밀 액세스 키는 단 1회만 확인 가능 하므로, 잊지 말고 적어두고, .csv 파일 다운로드를 눌러 다운로드 받아 주도록 합니다.

 

3. 공유 인증 파일에서 node.js에 인증 자격 증명 로드.

에세스 키를 공유인증 자격에 넣는 방법을 설정합니다. 공식 문서는, [인증자격증명로드] 을 참고하면 됩니다.

공유인증 자격 증명이 있어야할 위치는 윈도우는 C:\Users\USER_NAME\.aws\credentials 맥 또는 리눅스, 유닉스는 ~/.aws/credentials 입니다. 유닉스 시스템에서 . 문자로 시작하는 파일은 감춰 지기 때문에, 주의해야 하며, ~ 디렉터리는 로그인한 유저의 홈디렉터리를 의미합니다.

그리고 앞서 받았던 엑스키와 시크릿 액세스 키를 credentials 파일에 작성합니다.

[default]
aws_access_key_id = <YOUR_ACCESS_KEY_ID>
aws_secret_access_key = <YOUR_SECRET_ACCESS_KEY>

 

4. SNS 콜솔 열기.

SNS 콘솔을 열려면, (링크) 를 눌러도 바로 열 수 있지만, 아래와 같이 열 수도 있습니다. 콘솔 페이지에서, Simple Notification Service를 선택해 주도록 합니다.

Amazon Simple Notification Service 페이지가 열리면 개요로 시작을 선택해 콘솔로 들어 갑니다.

그리고, 대시 보드 페이지가 표시되면, 문자 보내기가 없을 것 입니다. (있을 수도 있음). 이유는 몇몇 리전만 지원을 하기 때문입니다. 리전을 한국과 가까운 일본으로 선택합니다.

그럼 문자 메시지(SMS) 항목이 나타날 텐데 눌러서 문자 메시지 대쉬 보드가 표시 될 수 있도록 합니다.

 

5. 기본 설정 지정

여기 까지 왔다면, 문자 메시지 기본 설정의 편집 버튼을 눌러 주도록 합니다.

여기서 주요한 사항은 메시지 유형과 지출 한도 입니다. 일단 트랜잭션이 고급진 보내기 인데, 저는 테스트용도 이므로 이렇게 했습니다. 여러분은 상황에 맞게 해 주시기 바랍니다.

모두 설정했다면 변경 사항 저장 버튼을 눌러 주도록 합니다.

 

6. 권한 설정.

SMS을 보낼 때, 계정에 권한을 주지 않으면, not authorized to perform: SNS:Publish 라는 오류를 만납니다. 권한만 추가해 주면 간단히 해결 되므로, 권한을 주도록 합니다.

콘솔에서 IAM 을 선택합니다. (위치는 다를 수 있습니다. 자주 사용하는 항목이 위에 표시되도록 되어 있습니다.).

사용자 탭을 눌러 위에서, 엑세스 키를 만들었던 유저를 선택해 줍니다.

여기서 권한 추가 버튼을 눌러 줍니다.

권한 추가 창이 뜨면 기존 정책 직접 연결을 선택합니다.

권한 부여 창이 뜨면 정책 필터에 sns 를 입력해 줍니다. 그럼 관련 권한 3개가 뜨는데 모두 선택해 줍니다. (하나씩 권한을 주고, 문자를 보내봤는데, 모두 오류가 났습니다. 아마 3개 다 필요한 듯 합니다.) 그 다음 검토 버튼을 눌러 줍니다.

다음 창이 뜨면 권한 추가 버튼을 눌러 권한을 추가하도록 합니다.

 

7. node js 파일 생성 및  aws-sdk 설치.

여기서 부터는 공식 문서, [Amazon SNS를 통한 SMS 메시지 전송] 을 참고해 주시면되겠습니다.

적당한 위치에 node.js를 위한 send_message.js 파일을 만들어 줍니다. 그리고, 아래와 같이 aws-sdk를 설치합니다.

$ npm i aws-sdk

 

 

8. 문자 보내기.

드디어 대망의 문자 보내기 입니다. 공식 문서의 가장 마지막의 문자 보내기를 참고해서 보내면 됩니다. (공식 문서의 옵트인 확인은, 문자를 받지 않겠다고 해당 스마트폰 사용자가 설정한 경우 입니다.)

소스는 아래와 같고, params 의 Message 항목에 보낼 메시지를 넣고, PhoneNumber에 보내는 전화 번호를 넣으면 됩니다. 주의할 점은, 전화번호는 국제 전화 번오를 넣어야 합니다. 한국같은 경우, 전화번호가 010-9999-8888 이라면, +8201099998888을 입력해야 합니다.

AWS.config.update 의 region 값은 이 문서 상단의 공식문서 [aws 리전] 을 참고해 넣으시면 되며, 이 문서는 일본에 설정하였으므로, 일본으로 넣어 테스트 했습니다.

// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set region
AWS.config.update({region: 'REGION'});

// Create publish parameters
var params = {
  Message: 'TEXT_MESSAGE', /* required */
  PhoneNumber: 'E.164_PHONE_NUMBER',
};

// Create promise and SNS service object
var publishTextPromise = new AWS.SNS({apiVersion: '2010-03-31'}).publish(params).promise();

// Handle promise's fulfilled/rejected states
publishTextPromise.then(
  function(data) {
    console.log("MessageID is " + data.MessageId);
  }).catch(
    function(err) {
    console.error(err, err.stack);
  });

저는 이 소스를 이용해 값을 알맞게 고쳐 제 폰으로 문자를 보내어 보니 아주 잘 왔습니다. 여러분은 어떠신가요?

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

Express-generator로 express 간단히 셋팅하기.  (0) 2019.06.02
node js 파일 업로드 multer (field)  (0) 2019.06.01
node js 파일 업로드 multer  (0) 2019.05.24
node.js nodemailer gmail  (0) 2019.05.24
node js OAuth 2.0 Login  (0) 2019.04.16
Posted by 창업닉군
,

node.js로 파일을 업로드 하려면 multer 라는 모듈을 다운로드 받아 사용하면 됩니다.

1. multer 설치.

$ npm i multer

 

2. multer 로드.

multer를 아래와 같이 모듈 로드해 줍니다. path는 파일 경로를 만들어 줍니다.

var multer = require('multer');
var path = require('path');

 

3. multer 초기화

아래 코드를 참고해 multer를 생성합니다. 별 내용은 없고, 저장 위치와, 파일명을 지정하는 메소드, 옵션 정도 설정해 초기화 합니다.

  const uploader = multer({

    // 저장 위치 설정.
    storage: multer.diskStorage({
      
      destination(req, file, cb){
        cb(null, "upload/");
      },

      filename( req, file, cb )
      {
        const ext = path.extname( file.originalname );
        cb( null, path.basename( file.originalname, ext ) + new Date().valueOf() + ext );
      }

    })


    // 저장 용량설정.
    , limits: { filesize: 5 * 1024 * 1024 }

  });

 

4. 업로드 라우터 설정.

위에서 설정했던 multer를 라우터에 설정하는 것 인데, single('img')는 html에서 업로드하는 필드중 img 가 파일을 업로드하는 것 임을 알려 줍니다. single은 하나의 파일만 업로드했음을 설정하는 메소드 입니다.

  app.post( '/img', uploader.single('img'), function( req, res, next ){

    // 파일 정보.
    console.log( req.file );

    // POST의 다른 정보.
    res.json( req.body );

  });

 

5. 업로드하는 파일 폼의 html은 다음과 같습니다.

<html>

    <head>
        <title>이미지 업로드</title>
    </head>


    <body>

        <form action="/img" method="POST" enctype="multipart/form-data" name="file_F" id="file_F" >

            <input type="file" name="img" />
            <input type="text" name="kkon" />

            <input type="submit" value="보내기" />

        </form>

    </body>


</html>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

node js 파일 업로드 multer (field)  (0) 2019.06.01
aws node js SMS 보내기  (0) 2019.05.26
node.js nodemailer gmail  (0) 2019.05.24
node js OAuth 2.0 Login  (0) 2019.04.16
node.js를 이용한 iOS Push 구현하기 (requests to APNs)  (0) 2019.04.14
Posted by 창업닉군
,

node.js nodemailer gmail

Node.js 2019. 5. 24. 03:19

노드 메일러로 gmail 보내는 내용입니다. 웹상에 많이 있어 그냥 제가 해보고 되었던 웹페이지의 주소를 링크해 둡니다.

https://jizard.tistory.com/150

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

aws node js SMS 보내기  (0) 2019.05.26
node js 파일 업로드 multer  (0) 2019.05.24
node js OAuth 2.0 Login  (0) 2019.04.16
node.js를 이용한 iOS Push 구현하기 (requests to APNs)  (0) 2019.04.14
Android 결제 서버 개발  (0) 2019.04.09
Posted by 창업닉군
,

c# Reflection

C# 2019. 5. 6. 05:34

c#의 장점은 클래스 형태, 변수 타입등이 정확히 일치하지 않으면, 컴파일 자체가 일치하지 않으면, 명확히 오류를 발생 시키므로, javascript 등과 같이 데이터형 및 오타 등으로인한 오류가 숨어있을 가능성이 매우 적습니다. 하지만, 이를 위해 치르는 비용 또한 만만치 않은데, 약간만 틀려도 다른 클래스를 정의하거나, 추상 클래스, 상속등 코드가 커지는 단점 또한 발생합니다. 특히 완전히 다른 클래스 중 일부만 같지도 않고, 비슷하면, 모두 다른 클래스로, 만들어 관리해야 할지 모릅니다.

그렇게 된다면, 파일이 너무 크고, 관리해야할 포인트, 업데이트 및 유지 보수 비용에 막대하게 늘어 날 것 입니다. 이를 간단히 해결하는 방법으로, 어트리뷰트와 리플렉션을 사용하는 방법이 있습니다. 사실 c#의 매우 이른시기에 포함되었지만, 그렇게 흥미를 끄는 키워드는 아닌듯 합니다. 하지만, 이 기능을 잘 사용한다면, 분명 많은 노력과 시간을 줄일 수 있습니다.

저는 맥에서 비주얼 스튜디오 코드를 사용하지만, 윈도우에서 비주얼 스튜디오를 사용하시는 분들도 무리없이 따라 오실 수 있을 것 이라 봅니다.

 

[개발환경 및 환경구축]

개발환경 : mac, visual studio code
프로젝트 생성방법 : https://docs.microsoft.com/ko-kr/dotnet/core/tutorials/with-visual-studio-code . (C# 및 Visual Studio Code)

 

[공식문서]

Reflection : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/reflection .
attribute에 저장된 정보 검색 : https://docs.microsoft.com/ko-kr/dotnet/standard/attributes/retrieving-information-stored-in-attributes .

 

1. 프로젝트 만들기.

프로젝트 생성 방법 문서를 참고 하도록 합니다.

프로젝트를 생성할 폴더를 파인더 (윈도우는 탐색기)를 통해 만들어 줍니다.

비주얼 스튜디오를 열어 Open Folder를 눌러 앞서 만든 폴더를 선택해 주도록 합니다.

주메뉴에서 보기 > 통합 터미널을 선택하여, 터미널을 열어 주도록 합니다.

콘솔에 dotnet new console 이라고 입력합니다. 그려면 아래와 같이 프로젝트가 초기화 되며, 처음 호출되는 메소드인 program.cs 파일도 자동으로 생성되어 집니다.

그냥 컴파일 후 시작하려면, 터미널에 dotnet run 이라고 입력하면됩니다.

오류를 찾기 위한 구성을 만들어 주려면 디버그 버튼과 화살표 버튼을 누른 후 .NET Core를 선택해 주도록 합니다.

그러면 구성이 추가되었습니다. 한 번 더, 디버그 > 삼각형 버튼을 눌러 주도록 합니다. 그러면 아래와 같이 DEBUG CONSOLE에 Hello World! 문자열이 찍히는 것을 확인할 수 있습니다.

 

 

2. Attribute 만들기.

Attribute를 만드는 것은 간단합니다. 클래스를 만들고, Attribute를 상속하기만 하면됩니다. 주의할 점은 클래스 명에, Attribute를 붙여 끝내야한다는 것입니다. 그렇게 하지 않아도 된다고 하는 데, 공식문서에서는 그렇게 하라고 합니다.

소스는 아래와 같습니다.

using System;

public class myAgeAttribute : Attribute
{


    // 초기화 시 입력받은 값들을 저장합니다.
    public int age = 0;


    // 생성자 입니다.
    public myAgeAttribute( int _age )
    {

        // 입력받은 값들을 저장합니다.
        age = _age;

    }

}

Attribute를 적용할 때는, Attribute클래스 명에서, Attribute를 제외하고 사용하면 됩니다. 저는 테스트를 위해 ex1, ex2라는 클래스를 만들었고, Attribute를 적용한 메소드가 적용하지 않은 메소드를 적절히 섞어 두었습니다.

*ex1

using System;

class ex1
{


    [myAge(16)]
    static public void myAge( int _age )
    {
        Console.WriteLine( string.Format("my Age : {0}", _age) );
    }

    static public void noAge( int _age )
    {
        Console.WriteLine( "호출 안되는 메소드" );
    }

}

*ex2

using System;

class ex2
{

    [myAge(18)]
    static public void yourAge( int _age )
    {
        Console.WriteLine( string.Format("Your Age : {0}", _age) );
    }

    [myAge(3)]
    static public void horseAge( int _age )
    {
        Console.WriteLine( string.Format("Horse Age : {0}", _age) );
    }

    static public void hideAge( int _age )
    {
        Console.WriteLine( "호출 안되는 메소드." );
    }


}

그리고, 테스트를 위해 program.cs 파일을 약간 고쳐 줍니다. 어셈블리는 모듈 (.dll) 단위가 프로그램 상에 로드될 때, 어셈블리를 형성하게 됩니다. c#에서는 주로 프로젝트 단위가 되는데, 스스로 만든 클래스는 어떤 프로젝트에 포함되어 있는지 우리 스스로가 알고 있습니다. 대게의 경우 자신이 만든 클래스는 자신이 생성한 하나 혹은 2개의 프로젝트에 들어 있을 것 이므로, 자신이 생성한 클래스 중 하나의 어셈블리를 가지고 오면, 자신이 만든 모든 어셈블리 일 것 입니다. (프로젝트가 2개인 경우라도 적절히 찾으실수 있을 것 입니다.)

아래 소스는 이런식으로 어셈블리를 찾아 와 모든 타입(클래스, 인터페이스등)에 myAgeAttribute가 붙어 있는 메소드를 찾아 Attribute에 설정된 값으로, 해당 메소드를 실행시켜 줍니다. 대게 실행하려면, 인스턴스화 되어야 하지만, 우리는  static 메소드만 만들었으므로, 인스턴스화 할 필요 없이 바로 실행해 주는 것이 가능했습니다.

using System;
using System.Reflection;

namespace reflection
{
    class Program
    {

        static void Main(string[] args)
        {


            // 어셈블리를 찾아 옵니다.
            Assembly myAssem = typeof(myAgeAttribute).Assembly;


            // 어셈블리에 포함된 모든 타입 (인터페이스, 클래스 등)을 찾아 옵니다.
            var types = myAssem.GetTypes();


            foreach( var _abc in types )
            {

                // 해당 타입의 메소드를 찾아 옵니다.
                var methods = _abc.GetMethods();

                foreach( var _curMethodinfor in methods )
                {

                    // myAgeAttribute가 있는 객체를 모두 찾아 옵니다.
                    var attributes = _curMethodinfor.GetCustomAttributes<myAgeAttribute>();
                    foreach( var nn in attributes )
                    {
                        // 해당 어트리 뷰트가 있는 경우만, 메소드를 실행해 주도록 합니다.
                        _curMethodinfor.Invoke( null, new object[]{ nn.age } );
                    }

                }

            }

        }

    }
}

실행해 보면 아래와 같은 결과를 만날 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

C# WebSocket Server  (6) 2019.05.01
Posted by 창업닉군
,

Visual Studio는 코딩을 할 때, 매우 강력한 IDE 프로그램 입니다. 하지만, 맥등에서는 사용할 수 없는 불편함을 가지고 있습니다. 몇 해 전 부터, 마이크로 소프트에서는 Visual Studio Code를 만들어 매우 간단한 기능이 지만, OS에 상관없이 쉽게 사용할 수 있도록 해 주었습니다. 저 같이 맥북을 사용하는 유저에게는, Visual Studio Code는 거의 축복에 가까운 IDE 프로그램입니다.

몇 달 전, Jetbrain의 IntelliJ도 괜찮아 보여 한 번써 볼까라고 생각을 했지만, 가격을 보고, 감사하는 마음으로 Visual Studio Code를 쭉 사용하고 있습니다.

처음에는 Visual Studio Code 공식 문서를 보고 블로그에 나며둬야 겠다 생각을 했는데, 공식 문서를 쭉 읽어 보니 너무 잘되어 있어서, 제가 따로 적을 말이 없네요. 공식 문서 그대로 따라 하시면될 것 같습니다.

 

[공식 문서]

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'IDE Editor 사용법' 카테고리의 다른 글

Eclipse IDE 사용법  (0) 2019.04.18
Visual Studio Code IDE 사용법  (0) 2019.04.17
Atom IDE 사용법 (링크들)  (0) 2019.04.17
Posted by 창업닉군
,

HTML5와 웹소켓이 나오기 전까지, 홈페이지는 그저 요청에 대한 서버 결과를 돌려 주는 형태로만 제작이 가능했습니다. 물론 여러가지 방법을 통해, 비슷한 형태로 제작이 가능했지만, 근본적인 대책은 아니였습니다. TCP/IP 통신을 하지 못해, 챗팅 게임등의 서버로 사용하기 힘들었지만, 웹소켓이 나온 이후로는 매우 편리해 졌습니다.

거기다, 쉬운 방식으로 구현히 가능해, 적어도 한 시간 이내에 기능을 구현하는 것이 가능합니다. 제가 TCP, UDP통신을 모두 구현해 보았지만, 웹소켓만큼 구현히 쉽지는 않았습니다.

이 문서에는 javascript를 통한, 클라이언트를 구현해 보도록 하겠습니다. websocket.org 라는 사이트에서 웹소켓 에코 서버를 지원해 주므로, 클라이언트 부터 만들고, 이 클라이언트를 바탕으로 서버를 만들면 서버 또한 깔금하게 만들수 있습니다.

 

[공식문서]

공식 사이트 : websocket org : https://www.websocket.org .

 

 

1. 샘플 보기 및 샘플 테스트.

공식 사이트에 가면, 샘플에 대한 구현 방법과 에코서버를 어떻게 테스트 하는지 잘 설명하고 있습니다.

먼저, 공식 사이트로 이동합니다. ( 링크 ) 그럼, 아래와 같이 페이지가 나오는데 화면 중앙의 GET STARTED 버튼을 눌러 주도록 합니다.

그럼 에코 테스트 페이지가 나옵니다. 화면 중앙의 녹색 This browser supports WebSocket 이라는 문구는, 여러분이 사용하는 브라우저가 웹소켓을 지원하고 있음을 알려주고 있습니다. 요즘은 거의 모든 브라우저가 웹소켓을 지원하지만, 얼마전까지만 해도 그렇지 않았습니다.

원할한 서비스를 위해 여러분도 여려분의 홈페이지에 방문자의 브라우저가 웹소켓을 지원하는지 여부를 알려줄 필요가 있을 지 모릅니다. (구현은 간단합니다.) location: 부분에 입력된 값은 테스트를 위한 에코서버의 주소 입니다. 일단 그대로 두고, 컨넥트 버튼을 눌러 보도록 하겠습니다.

그럼 에코 서버와 연결이 되어 Log에, CONNECTED라고 표시됩니다.

메시지 부분에 적당한 메시지를 입력하고, Send를 눌러 보도록 하겠습니다.

그럼 아래와 같이 보낸 메시지를 서버가 그대로 돌려 줍니다.

마지막으로 서버와의 접속을 끊어주는 Disconnect 를 눌러 주도록 합니다.

그럼 아래와 같이 DISCONNECTED 라는 로그가 남기며 접속이 종료됩니다.

간단한 기능이지만, 챗팅 및 게임을 구현하기에 충분한 역할을 하고 있는 것을 확인할 수 있습니다. 하지만, 이렇게 구현하는데, 코드 몇 줄으면 끝이난다는 것이 더 매력적입니다.

 

 

 

2. 소스 보기.

위의 페이지에서 아래로 스크로를 해 보면, 위 코드에 대해서 간단히 구현해 놓은 코드를 제공합니다.

워낙 소스가 잘되어 있어서, 따로 무언가를 할 필요가 없어서, 공식예제에 주석만 달아 아래에 소스를 공개합니다.

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

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

  // DOM 이 형성되었을 때 가장 먼저 호출되는 메소드 입니다.
  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);
  }

  // 브라우저의, DOM 구성이 완료되면, 가장 먼저 실행할 메소드를 지정하는 메소드 입니다.
  // (즉, 가장 먼저 실행될 메소드 지정).
  window.addEventListener("load", init, false);

  </script>

  <h2>WebSocket Test</h2>

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

 

 

 

 

 

 

 

 

 

 

 

Posted by 창업닉군
,

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 창업닉군
,