본문 바로가기
경일/Block-chain

[Blockchain/블록체인] P2P 네트워크 이용하여 블록정보 주고 받기

by dev_kong 2022. 6. 14.
728x90
728x90

0. 목차

1. 개요

2. 기능 구현

1. 개요

저번 포스팅에서 websocket을 이용해서 기초적인 P2P네트워크를 구현했다.
이번에는 저번에 구현한 네트워크를 이용해,
노드간에 블록을 주고받는 기능을 구현해보려한다.


원래라면 이 과정에 검증과정이 필수 이지만,
우선 오늘은 코드의 흐름을 이해하는게 우선이기 때문에 검증에 대한 부분은 제외하고 구현해보려 한다.

 

P2P 네트워크에서는 클라이언트와 서버의 역할을 각각의 노드가 동시 수행하기 때문에,
각각에 역할에 대한 코드가 나눠져 있지않고 하나의 문서에서 작성된다.

 

그렇다보니 흐름이 조금 괴랄하긴 한데 최대한 하나하나 풀어서 기술해보려 한다.

 

2. 기능 구현

index.ts

import { BlockChain } from '@src/core';
import { P2PServer } from '@src/serve/p2p';

import express, { Request, Response } from 'express';

const app = express();
const bc = new BlockChain();
const ws = new P2PServer();

app.use(express.json());

app.post('/addToPeer', (req: Request, res: Response) => {
  const { peer } = req.body;
  ws.connectToPeer(peer);
});

app.listen(3000, () => {
  console.log('서버 시작 \n PORT : #3000');
  ws.listen();
});

 

p2p.ts

import { WebSocket } from 'ws';
import { Chain } from '@src/core/blockchain/chain';

export class P2PServer extends Chain {
  sockets: WebSocket[];

  constructor() {
    super();
    this.sockets = [];
  }

  // 서버 시작하는 실행코드
  listen() {
    const server = new WebSocket.Server({ port: 7545 });
    server.on('connection', (socket) => {
      console.log('websocket connection');
    });
  }

  // client 연결코드
  connectToPeer(newPeer: string) {
    const socket = new WebSocket(newPeer);
  }
}

 

우선 위의 두 코드가 저번 포스팅에서 작성했던 내용이다.
여기서 오늘 작업할 부분은 p2p.ts 이다.

 

우선 connectToPeer를 보자.
노드가 클라이언트로 역할을 수행할 때의 코드이다.

 

해당 함수가 실행되면 new WebSocket(newPeer) 실행된다.


위의 함수로 인해 hadnshake에 대한 요청이 전달된다.


그리고 handshake가 정상적으로 이뤄졌을 때, 어떠한 함수를 동작하게끔 만들어 주면된다.

그리고 handshake가 정상적으로 일어났을 때의 시점을 캐치하는 event type은 open 이다.

 

  connectToPeer(newPeer: string) {
    const socket = new WebSocket(newPeer);
    socket.on('open', () => {
      this.connectSocket(socket);
    });
  }

 

위처럼 코드를 추가해주었다.

 

못보던 함수가 하나 생겼다. connectSocket이란 함순데,
socket을 sockets라는 배열에 추가하고,
handshake가 이뤄진 노드로부터 데이터를 전달 받을 때의 이벤트를 등록하는 함수이다.


그리고 동시에, handshake가 이뤄진 노드로부터 데이터를 달라고 요청하는 메세지를 보내기도 한다.

...코드를 보는게 더 쉬울지도 모르겠다.

 

이 함수에 대한 내용을 작성해보자.

 

// (import..)

enum MessageType {
  latest_block = 0,
  all_block = 1,
  receiveChain,
}

interface Message {
  type: MessageType;
  payload: any;
}
  // (...)

  connectSocket(socket: WebSocket) {
    // 배열에 추가
    this.sockets.push(socket);

    // 이벤트 등록
    this.messageHandler(socket);

    const data: Message = {
      type: MessageType.latest_block,
      payload: {},
    };

    // 노드에 데이터(빈객체) 전달
    // 메세지를 받은 노드는 데이터의 type의 value에 따라 다른 코드를 동작하게끔 만들어준다
    // 이것에 대한 코드는 messageHandler에 작성될거임
    const send = this.send(socket);
    send(data);
  }

   messageHandler(socket: WebSocket) {
   //TODO: 메세지 등록하는 함수 작성
  }

  send(_socket: WebSocket) {
    return (_data: Message) => {
      _socket.send(JSON.stringify(_data));
    };
  }

우선은 ts로 작성을 하고 있기 때문에, enum과 interface를 만들어주었다.

 

그리고 connectSocket 을 작성해주었다.


위에 기술한 대로 배열에 socket을 추가하고, 이벤트를 등록하는 메서드를 호출하고,
data를 만들어 준 뒤, 최초로 해당 데이터를 handshake가 이뤄진 노드에 메세지를 전달한다.

 

send함수는 그냥 send 편하게 쓰기위해 함수를 따로 뺀거 뿐이다.

이번엔 messageHandler를 작성해보자.


해당 함수는 handshake가 이뤄진 node로부터 메세지를 받았을 때 실행될 코드를 작성해줄거다.
정확하게는 메세지를 받음 에 대한 이벤트를 등록하고, 이벤트가 발생했을 때 실행될 콜백 함수를 작성한다.

 

messageHandler(socket: WebSocket) {
    const send = this.send(socket);

    const messageCallback = (data: string) => {}

    socket.on('message', messageCallback);
  }

 

우선은 이렇게 작성하면 이벤트는 등록을 한거다.

이제 messageCallback 즉 이벤트가 발생했을 때 실행될 콜백함수를 작성해보자.

 

message를 받으면 message에 포함된 데이터를 콜백함수의 인자값으로 받는다.
해당 값에 data라는 변수를 지정해주었다.

 

socket 통신으로 받은 데이터는 기본적으로 Buffer의 형식을 지닌다.
해당 data를 다시 객체의 형태로변경해주는 함수..가 필요하다.

 

  static dataParser<T>(data: string): Failable<T, string> {
    const result = JSON.parse(Buffer.from(data).toString());

    if (result === undefined) {
      return { isError: true, error: '변환실패' };
    }

    return { isError: false, value: result };
  }

 

리턴값을 Failable로 만들었다.
이 덕에, 혹시나 변환에 실패했을 때의 에러처리가 좀더 편해진다.

 

방금 만든 따끈따끈한 dataParser 함수를 이용해 data를 다시 객체 형태로 변환 시켜주자.

 

객체형태로 변환된 데이터는 value프로퍼티의 밸류에 들어갈거다.
그러면 value의 데이터 타입은 Message가 될거고,
Message에는 type이라는 프로퍼티가 있는데 이 값에 따라,
이후 진행이 달라지는 코드를 작성할거다.

 

messageHandler(socket: WebSocket) {
    const send = this.send(socket);

    const messageCallback = (data: string) => {
      const result: Failable<Message, string> =
        P2PServer.dataParser<Message>(data);

      if (result.isError) {
        console.log(result.error);
        return;
      }

      if (!result.isError) {
        switch (result.value.type) {
           case MessageType.latest_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: [this.getLatestBlock()],
            };

            send(message);
            break;
          }
    };

    socket.on('message', messageCallback);
  }

 

dataParser 함수의 결과에 따른 예외처리를 가장 상단에 작성했다.

connectSocket 메서드로 인해,
최초로 메세지를 보냈다.


어디로? handshake가 일어난 노드로!

그럼 그 노드에선 해당 메세지를 받았을 거고,
그 메세지의 type 이 MessageType.latest_block 이면,
동작할 함수 까지 작성한거다.

 

즉, 현재 작성된 1번 case문은 서버 역할을 하는 노드에서 실행될 부분이다.
이 부분에서 data를 만들어서 다시 클라이언트 노드로 메세지를 전달하는데
이때 data의 payload에 chain의 마지막 블록을 전달해준다.

 

그럼 이젠 다시 클라이언트 입장에서 type이 Message.all_block 인 데이터가 포함된 메세지를 받았을 때의 코드를 작성해보자

 

 const messageCallback = (data: string) => {
      const result: Failable<Message, string> =
        P2PServer.dataParser<Message>(data);

      if (result.isError) {
        console.log(result.error);
        return;
      }

      if (!result.isError) {
        switch (result.value.type) {
          case MessageType.latest_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: [this.getLatestBlock()],
            };

            send(message);
            break;
          }
          //추가된 부분
          case MessageType.all_block: {
            const message: Message = {
              type: MessageType.receiveChain,
              payload: this.getChain(),
            };

            send(message);
            break;
          }
        }
      }
    };

    socket.on('message', messageCallback);
  }

 

data의 type이 MessageType.all_block 일때,
data를 만들어서 메세지를 전달하는데
해당하는 data의 payload 에는 chain전체를 전달해준다.

 

서로 주고받는 데이터는 검증에 대한 부분때문인데 이부분은 다음 포스팅에 작성될 예정이다.

그럼 이제 마지막으로 전체 체인을 전달 받았을 때,
즉, 메세지에 포함된 데이터의 type의 밸류가 MessageType.receiveChain일때의 처리만 해주면 된다!

 

  messageHandler(socket: WebSocket) {
    const send = this.send(socket);

    const messageCallback = (data: string) => {
      const result: Failable<Message, string> =
        P2PServer.dataParser<Message>(data);

      if (result.isError) {
        console.log(result.error);
        return;
      }

      if (!result.isError) {
        switch (result.value.type) {
          case MessageType.latest_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: [this.getLatestBlock()],
            };

            send(message);
            break;
          }

          case MessageType.all_block: {
            const message: Message = {
              type: MessageType.all_block,
              payload: this.getChain(),
            };

            send(message);
            break;
          }
          //추가된 부분
          case MessageType.receiveChain: {
            const receivedChain: IBlock[] = result.value.payload;

            console.log(receivedChain);
            break;
          }
        }
      }
    };

    socket.on('message', messageCallback);
  }

 

검증에 대한 부분은 아직 작성하지 않기로 했으니 깔끔하게 console.log를 찍어 봤다.

이렇게하면 최종적으로 server 노드에서 client 노드의 전체 chain이 console.log로 찍힌다.

 

그런데, 블록체인에서는 블록에대한 상호검증이 이뤄져야 한다.
그렇기 때문에 server 노드는 client노드의 체인을 받아야 하고,
client 노드 역시 server 노드의 체인을 받아야한다.

 

그렇기 때문에 listen 코드를 살짝만 아주 살짝만 수정해주면 된다.

 

  listen() {
    const server = new WebSocket.Server({ port: 7545 });
    server.on('connection', (socket) => {
      console.log('websocket connection');
      // 추가된 부분
      this.connectSocket(socket);
    });
  }

 

핸드셰이크 요청을 받고 핸드셰이크가 정상적으로 이뤄졌을 때,
server 노드에서도 connectSocket메서드를 실행시켜서,
client 노드와 동일한 작업을하게끔 만들어준다.

 

이렇게 하면 client 노드에서는 server 노드의 전체 체인을
server 노드에서는 client 노드의 전체 체인을 받을 수 있다!

 

서버측 코드와 클라이언트측 코드가 같은 문서에
심지어는 같은 함수에 작성되기 때문에 드럽게 헷갈린다. 진짜로...


그래도 하나하나 절차적으로 코드를 작성및 블로그 작성을 해보니,

머릿속에 정리가 된 것 같은 느낌이다.

728x90
728x90

댓글