본문 바로가기
Blockchain

[블록체인 만들기 #6] P2P 네트워크

by 개발자 염상진 2022. 10. 12.

블록체인에 참여하는 노드들은 스스로 클라이언트이자 서버가 됩니다. 모든 노드들이 동일한 체인을 보유해야 하기 때문에 broadcasting을 하게 되는데, P2P 네트워크를 WS와 HTTP 프로토콜을 이용해 구현할 수 있습니다. HTTP 프로토콜은 블록데이터를 반환하는 용도 / WS는 P2P 네트워크를 담당합니다.

 

 

HTTP/WS 환경 구성

 

터미널에서 필요한 모듈을 먼저 설치합니다.

$ npm install express ws
$ npm install -D @types/express @types/ws

 

P2P 서버 구성

 

블록체인에서는 노드가 클라이언트가 되면서 동시에 서버가 됩니다. 즉, 서버 쪽 코드와 클라이언트 코드를 동시에 작성해줘야 합니다.

./src/server/p2p.ts 파일을 작성하고 아래 코드를 입력합니다.

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

enum MessageType{
    latest_block = 0,
    all_block = 1,
    receivedChain = 2,
}

interface Message{
    type : MessageType;
    payload : any;
}

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

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

    /**
     * listen()
     * 클라이언트가 연결을 시도할 때 코드 실행
     */
    listen(){
        const server = new WebSocket.Server({port : 7545});

        // Server 기준 Connection
        server.on('connection', (socket)=>{
            console.log('WebSocket connected');

            this.connectSocket(socket);
        })        
    }
    /**
     * connectToPeer()
     * Client가 P2P 네트워크에 입장함
     * 서버로 연결을 요청할 때 코드 실행
     */
     connectToPeer(newPeer:string){
        const socket = new WebSocket(newPeer);
        
        // Client 기준 open
        socket.on('open', ()=>{
            this.connectSocket(socket);
        })
    }

    /**
     * connectSocket()
     * Client가 Server가 되서 Broadcasting 하게 됨
     */
    connectSocket(socket:WebSocket){
        this.sockets.push(socket);

        this.messageHandler(socket);

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

        this.errorHandler(socket);

        const send = P2PServer.send(socket);
        send(data);

        // socket.on('message', (data:string)=>{   
        //     console.log(data);
        // })

        socket.send('message from server');
    }

    /**
     * messageHandler()
     * @params socket
     * @desc Client A노드는 B노드로 부터 데이터를 전달받을 준비함
     */
    messageHandler(socket:WebSocket){
        const callback = (_data:string)=>{
            const result:Message = P2PServer.dataParse<Message>(_data);
            const send = P2PServer.send(socket);

            switch(result.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.receivedChain,
                        payload : this.getChain(),
                    }
                    // chain에 블록을 추가할지 결정함
                    const [receivedBlock] = result.payload;

                    const isValid = this.addToChain(receivedBlock);

                    if(!isValid.isError) break;

                    send(message);
                    break;
                }

                case MessageType.receivedChain : {
                    const receivedChain : IBlock[] = result.payload;

                    // chain을 교체함
                    this.handleChainResponse(receivedChain);
                    break;
                }
            }
        };

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

    errorHandler(socket : WebSocket){
        const close = () => {
            // Error 발생 시 해당 socket을 제거함
            this.sockets.splice(this.sockets.indexOf(socket), 1);
        }

        // socket 통신이 끊긴 경우
        socket.on('close', close);
    
        // Error 발생하는 경우
        socket.on('error', close);
    }

    /**
     * handleChainResponse()
     */
    handleChainResponse(receivedChain:IBlock[]):Failable<Message | undefined, string>{
        const isValidChain = this.isValidChain(receivedChain)

        if(isValidChain.isError) return {isError:true, error:isValidChain.error}

        const isValid = this.replaceChain(receivedChain);
        if(isValid.isError) return {isError : true, error : isValid.error};

        // Broadcasting
        const message : Message = {
            type : MessageType.receivedChain,
            payload : receivedChain,
        }
        this.broadcast(message);

        return{isError:false, value: undefined}
    }

    /**
     * broadcast()
     */

    broadcast(message:Message):void{
        this.sockets.forEach((socket)=>P2PServer.send(socket)(message))
    }
    /**
     * 전체 socket 반환 메소드
     */

    getSockets():WebSocket[]{
        return this.sockets;
    }

    /**
     * 
     * @param _socket 
     * @returns _function
     * @desc WebSocket.send()로 통신함
     */
    static send(_socket:WebSocket){
        return (_data:Message)=>{
            _socket.send(JSON.stringify(_data))
        }
    }

    static dataParse<T>(_data:string): T {
        return JSON.parse(Buffer.from(_data).toString());
    }
}
  • P2PServer 클래스는 Chain 클래스를 상속받고 있습니다. 
  • listen() 메소드는 최초 클라이언트가 접속했을 때 connectSocket() 메소드를 실행합니다.
  • connetToPeer() 메소드는 클라이언트로써 다른 노드에 접속합니다. 이 때 connectSocket() 메소드를 실행합니다.
  • connectSocket() 메소드는 socket을 인자로 받고,  messageHandler 메소드를 실행합니다.
  • messageHandler() 메소드는 최초 접속시 서버가 되는 노드가 커넥션을 만들고, 클라이언트에게 데이터를 전달합니다. 데이터를 전달받은 클라이언트는 데이터를 다시 파싱해서 서버노드로 전달합니다. 즉, 클라이언트 노드는 서버와 연결을 만들면서 동시에 본인도 서버로써 역할을 하도록 'message' 이벤트를 대기하게 됩니다.
  • 본인이 가진 체인보다 더 긴 체인을 받은 노드들은 Broadcasting을 하게 됩니다. 

 

 

 

HTTP 서버 구성

 

express 모듈을 import 하고, P2PServer 클래스를 import 합니다. 라우터들은 블록체인의 정보를 반환하는 용도로 사용됩니다. 주목할 점은 app.listen() 메소드 안에서 WS(P2P 서버)가 함께 작동된다는 점이죠. 블록체인에 참여하는 노드를 기다리고 있는 P2P 서버입니다. 노드가 한개 추가될 때 마다 서버 노드는 점점 많이질 것이고, 블록체인을 공유하는 노드가 증가하게 됩니다. 

import { P2PServer } from "./src/server/p2p";
import express, { Request, Response } from "express";

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

app.use(express.json());

app.get("/", (req, res) => {
  res.send("bit-chain");
});

// 블록 조회
app.post("/chains", (req, res) => {
  const { data } = req.body;
  const newBlock = ws.addBlock(data);

  if (newBlock.isError) return res.status(500).json(newBlock.error);

  res.json(newBlock.value);
});

// ws 연결 요청
app.post("/add-to-peer", (req, res) => {
  const { peer } = req.body;
  ws.connectToPeer(peer);
});

// 연결된 sockets 전체 조회
app.get('/peers', (req:Request, res:Response)=>{
    const sockets = ws.getSockets().map((s:any)=>s._socket.remoteAddress + ':' + s._socket.remotePort);
    res.json(sockets);
})

app.listen(3000, () => {
  console.log("server is listening on #3000");
  ws.listen();
});

 

블록체인 구현 정리

 

Typescript를 이용해 간단한 블록체인을 구현해봤습니다. Block을 생성하고, 채굴 노드들이 nonce 값을 찾으면 연결되는 PoW 합의 알고리즘을 사용했고, WS를 통해 P2P 네트워크까지 구현해봤습니다. 

 

Reference

 


 

🚀️ 도움이 되셨다면 구독좋아요 부탁드립니다 👍️

 

 

 

 

 

 

댓글