블록체인 P2P 구현

이재영·2023년 9월 12일
0

BlockChain

목록 보기
4/13
post-thumbnail

마이닝풀에 접속하여 마이닝 하는 과정을 가정하여, 사이트 접속시 웹 소켓이 연결되는것을 peer접속버튼을 통해 구현했고, 생성 버튼을 통해 블록이 생성되는 것을 구현하고, 갱신버튼을 통해 갱신이 된 상태를 볼 수 있다.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script
      integrity="sha512-aoTNnqZcT8B4AmeCFmiSnDlc4Nj/KPaZyB5G7JnOnUEkdNpCZs1LCankiYi01sLTyWy+m2P+W4XM+BuQ3Q4/Dg=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
</head>
<body>
    <div>
        <button id="peer">peer 접속</button>
    </div>
    <div>
        <label for="">peer</label>
        <button id="peerViewBtn">갱신</button>
    </div>
    <div id="peerView">
    </div>
    <div>
        <label for="">block</label>
        <button id="blockViewBtn">갱신</button>
    </div>
    <div id="blockView">
    </div>
    <div>
        <label for="">블록 생성</label>
        <input type="text" id ="blockData">
        <!-- 블록 바디 내용 -->
        <button id="blockCreate">생성</button>
    </div>
</body>
<script>
    peer.onclick = () =>{
        axios.get("http://localhost:8080/peer/add")
    }

    const render =async()=>{
        const {data:peer} = await axios.get("http://localhost:8080/peer");
        peerView.innerHTML = peer.join("|");
    }

    peerViewBtn.onclick = render;

    const blockRender = async()=>{
        const {data : block} = await axios.get("http://localhost:8080/chains");
        blockView.innerHTML = JSON.stringify(block);
    }

    blockViewBtn.onclick = blockRender;

    const _blockCreate = async()=>{
        const _blockData = [blockData.value];
        const {data : block } = await axios.post("http://localhost:8080/block/mine",{data : _blockData});
        console.log(block);
    }

    blockCreate.onclick = _blockCreate;
</script>
</html>

p2p.ts

import Block from "@core/block/block";
import Chain from "@core/chain/chain";

import {WebSocket,WebSocketServer} from "ws";

// 기본적인 연결 관련된것만 있는 모듈 Ws

enum MessageType{
// 알기 쉽게 사용하려고 
//0,1,2 상태를 지정한다했을 때
// 마지막 블록을 요청할 때
    latestBlock = 0, // string 문자로 해도 되는데 오타가 발생할수 있어서 number로 오류가 최대한 없게
    // 전체 체인을 요청할 때
    allBlock = 1,
    // 블록이 추가되서 알려줄 때
    addBlock = 2,
}

interface IMessage {
    // 메시지의 타입
    type : MessageType;
    // 메시지에 대한 값 데이터
    payload : any;
}

class P2P extends Chain{
    // Chain 상속받아서 Chain에 있는 메서드를 사용하려고
    private sockets : Array<any> // 연결된 socket들 확인.

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

    getSockets() : Array<WebSocket> {
        return this.sockets;
    }

    connectSocket(socket : any, type? : MessageType) : void {
        // 소켓을 연결하면
        // 하나의 포트가 동적으로 생기고 그 포트에서 소켓을 들고 있는데.
        // socket에는 고유의 포트가 들어있는 상태 충돌방지를 위해 애플리케이션 or 서비스 연결을 하면
        // 동적으로 포트를 지정해준다. (고유 포트)
        console.log("+++++++++socket",socket);
        this.sockets.push(
            `${socket._socket.remoteAddress} : ${socket._socket.remotePort}`
        );
        console.log("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^")
        console.log(this.sockets);
        // socket.send() 메서드를 호출하면 이벤트가 실행된다.
        // message이벤트 실행
        socket.on("message", (_data : string)=>{
            const data = JSON.parse(_data.toString());
            console.log("+++++++++++++++++data",data)
            switch (data.type) {
                case MessageType.latestBlock:
                    // 0이 들어오면 여기               
                    const message : IMessage = {
                        // type
                        type : MessageType.latestBlock, // 모든블록 타입이 실행되는지 확인
                        // 마지막 블록은 payload에 담아서
                        payload : [this.latestBlock()],
                    }
                    // 완성한 객체를 문자열로 치환해서 보낸다.
                    socket.send(JSON.stringify(message))
                    break;
                    
                case MessageType.allBlock:
                    // 1이 들어오면 여기  
                    break;

                case MessageType.addBlock:
                    console.log("2에 들어옴");
                    // 2이 들어오면 여기
                    // 검증 로직은 여기에
                    const isValid = this.replaceChain(data.payload);
                    if(isValid.isError) break;
                    // 문제가 있으면 종료
                    
                    //
                    const message2: IMessage = {
                        type : MessageType.addBlock,
                        payload : data.payload

                    }
                    this.sockets.forEach((item)=>{

                        // 현재 접속한 유저들에게 메시지 전송
                        item.send(JSON.stringify(message2));
                    })
                    break;
            
                default:
                    break;
            }
        })

        const msg : IMessage = {
            type : MessageType.addBlock,
            payload : new Chain().get()
        }
        socket.send(JSON.stringify(msg));
    }

    listen(port : number) : void{
        // 현재 로컬에서 서버 생성
        // 웹소켓 포트 오픈 대기상태
        const server : WebSocketServer = new WebSocket.Server({port});

        server.on("connection", (socket : WebSocket) =>{
            // 소켓 연결 시도하면
            console.log("new socket connection");
            // 연결한 소켓을 배열에도 추가해주고 message 이벤트도 등록
            this.connectSocket(socket);
        })
    }

    addToPeer(peer : string) : void {
        // 상대방이 내 ip에 접속 했을 때
        // 소켓을 생성하고 연결한다.
        // console.log("-----------peer",peer);
        const socket : WebSocket = new WebSocket(peer);
        // console.log("-----------socket",socket);
        // 상대 소켓 서버 주소를 받아서 연결을 시도한다.
        socket.on("open",()=>{
            // 연결이 성공하면 open 이벤트가 실행된다
            console.log("연결 성공");
            this.connectSocket(socket, MessageType.addBlock);
            //
        })
    }
}

export default P2P;

index.ts

import Block from "@core/block/block";
import P2P from "./p2p";
import express, {Express, Request, Response} from "express" ;
import os from "os";
import cors from "cors";
const app : Express = express();
const ws : P2P = new P2P();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({extended : false}));

app.get("/chains",(req : Request, res : Response)=>{
    res.json(ws.get());
})

app.post("/block/mine",(req : Request, res: Response)=>{
    // 블록에 기록할 내용을 받고
    const {data} : {data : Array<string>} = req.body;
    const newBlock : Block | null = Block.generateBlock(ws.latestBlock(),data, ws.getAdjustmentBlock());
    if(newBlock === null) res.send("error");

    ws.addToChain(newBlock);
    res.json(newBlock);
})

// post 작성을 했었는데 get으로 바꿀거고 오타 이슈 본인 v4확인도 귀찮
app.get("/peer/add",(req: Request, res: Response)=>{
    const networkinterface = os.networkInterfaces();
    // console.log(networkinterface);
    let v4 : string;

    for (const key in networkinterface) {
            const Array = networkinterface[key];

            for (const value of Array) {
                    // value.internal이 false 일때 && value.family 이 IPv4일 때
                    if(!value.internal && value.family === "IPv4")
                    v4 = value.address
                    // v4 ip 주소
                }
        }
        ws.addToPeer(`ws://${v4}:7545`);
        res.end();
})

app.get("/peer", (req : Request, res : Response) =>{
    const sockets = ws.getSockets();
    res.json(sockets);
});

app.listen(8080, ()=>{
    console.log("server on");
    ws.listen(7545);
})

1. peer접속 버튼을 눌렀을 때 동작순서

//🚩index.html의 peer접속 버튼으로 아래 코드 실행.
axios.get("http://localhost:8080/peer/add")} 로 요청이 가고,

//🚩index.ts의 해당 코드가 실행된다.

app.get("/peer/add",(req: Request, res: Response)=>{
  
  // 내 os의 네트워크 인터페이스를 가져온다.
    const networkinterface = os.networkInterfaces();
    let v4 : string;
  // for in : 순차적으로 키값으로 객체의 값을 Array에 담음.
    for (const key in networkinterface) {
            const Array = networkinterface[key];
  // for of : 순차적으로 배열의 값을 value에 담음.
            for (const value of Array) {
  // value.internal이 false 일때 && value.family 이 IPv4일 때
                if(!value.internal && value.family === "IPv4")
                
                  // v4 =  ip 주소
                  v4 = value.address
                }
        }
  // const ws : P2P = new P2P(); 로 생성된
  // ws에 addToPeer 메서드가 실행된다.
  // ⭐⭐ ws:// 는 웹소켓의 url은 앞에 ws://가 붙어야하는 규칙이고, 웹소켓 서버 포트를 7545로 열어서 포트번호는 7545로 설정.
        ws.addToPeer(`ws://${v4}:7545`);
  
  //🚩p2p.ts의 addToPeer 메서드가 실행된다.
  addToPeer(peer : string) : void {
	
    // peer의 주소값으로 WebSocket 을 생성
    // ⭐⭐ 웹소켓을 생성할 땐, 대개 서버의 주소및 포트를 지정해야 하기때문에 
    // ⭐⭐ 매개변수로 받은 peer로 생성자 매개변수로 넣어 생성.
    // ⭐⭐ 아래코드까지 다 읽고 웹 소켓 연결을 설정하면 이 연결 시도가 
    // ⭐⭐ server.on("connection",...) 의 이벤트 핸들러로 이어지고 코드가 실행된다. 
        const socket : WebSocket = new WebSocket(peer);

        socket.on("open",()=>{
            console.log("연결 성공");
            this.connectSocket(socket, MessageType.addBlock);
        })
    }
        
  //🚩p2p.ts의 server.on("connection",...) 메서드가 실행된다.    
        // ⭐⭐ 소켓이 연결을 시도했을 때 실행되는 메서드 : server.on("connection",...)
        // ⭐⭐ 연결 시도했을 때  socket에는 
        // ⭐⭐ WebSocket 클라이언트와 서버 간의 연결을 나타내는 WebSocket 객체가 전달된다.
        server.on("connection", (socket : WebSocket) =>{
            // 연결한 소켓을 배열에도 추가해주고 message 이벤트도 등록
            this.connectSocket(socket);
        })
//🚩p2p.ts의 connectSocket 메서드가 실행된다.    

connectSocket(socket : any, type? : MessageType) : void {
 
        this.sockets.push(
            `${socket._socket.remoteAddress} : ${socket._socket.remotePort에는 }`);

// ⭐⭐ remoteAddress 에는 ::ffff:192.168.0.28
// ⭐⭐ remotePort에는 51070 의 값이 담기는데,
// ⭐⭐ ::ffff:192.168.0.28 는 IPv6 주소 표기법에서 IPv4 주소를 나타내기 위한 특별한 표기, 클라이언트 ip
// ⭐⭐ 51070 는 클라이언트에서 연결 시도한 포트 번호
        
// socket.send() 했을때 실행되는 핸들러
        socket.on("message", (_data : string)=>{
            const data = JSON.parse(_data.toString());
            switch (data.type) {
                case MessageType.latestBlock:
                    const message : IMessage = {
                        type : MessageType.latestBlock, 
                        payload : [this.latestBlock()],
                    }
                    socket.send(JSON.stringify(message))
                    break;
                    
                case MessageType.allBlock:
                    break;
                case MessageType.addBlock:
                    
                    const isValid = this.replaceChain(data.payload);
                    if(isValid.isError) break;
                    
                    const message2: IMessage = {
                        type : MessageType.addBlock,
                        payload : data.payload

                    }
                    this.sockets.forEach((item)=>{
                        item.send(JSON.stringify(message2));
                    })
                    break;
            
                default:
                    break;
            }
        })

    }
// ⭐⭐ 서버에 연결이 되었으니 socket.on("open",...)이 실행된다.
// 🚩p2p.ts 의 socket.on("open",...)

socket.on("open",()=>{
            // 연결이 성공하면 open 이벤트가 실행된다
            console.log("연결 성공");

// ⭐⭐ 매개변수 socket에는 클라이언트의 소켓 객체가 전달된다.
// remoteAddress : 서버의 ip주소, remoteport : 서버의 포트번호 
		// 위의 connectSocket 메소드가 실행되어 sockets 배열에 push 된다.
            this.connectSocket(socket, MessageType.addBlock);
        })
  
  
        res.end();
})

2. peer갱신 버튼을 눌렀을 때 동작순서

// 🚩indext.html
peerViewBtn.onclick = render;

const render =async()=>{
  // ⭐⭐ const {data:peer} 구조분해 할당,
  // ⭐⭐ const {peer} = data ; 랑 같음.
        const {data:peer} = await axios.get("http://localhost:8080/peer");
  
// 🚩 index.ts의 /peer 로 요청이 간다.
  app.get("/peer", (req : Request, res : Response) =>{
    const sockets = ws.getSockets();
    
// 🚩 p2p.ts의 getSockets() 가 실행된다.
    getSockets() : Array<WebSocket> {
        return this.sockets;
    }
    //-----------------------------------
    // 리턴 받은 값을 res.json 으로 전달.
    res.json(sockets);
});
  	// 구조분해한 peer의 값들을 join으로 문자열로 표시.
        peerView.innerHTML = peer.join("|");
    }

3. block 갱신 버튼을 눌렀을 때 동작순서

// 🚩 index.html
blockViewBtn.onclick = blockRender;

const blockRender = async()=>{
        const {data : block} = await axios.get("http://localhost:8080/chains");
        
    }
// 🚩 index.ts 의 /chains 로 요청이 간다.
app.get("/chains",(req : Request, res : Response)=>{
  // get() 메서드를 실행하고 그 값을 res.json 으로 전달
    res.json(ws.get());
})
// 🚩 chain.ts
// ws는 new p2p로 만들어진 클래스인데 p2p는 chain 클래스를 
// 상속받았기때문에  chain클래스의 get() 메서드 접근이 가능.
// 현재 체인 반환
get(){
        return this.chain;
    }
// ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 객체를 문자열로 표시.
blockView.innerHTML = JSON.stringify(block);

4. block 생성 버튼을 눌렀을 때 동작순서

blockCreate.onclick = _blockCreate;

const _blockCreate = async()=>{
  // 블록의 data 부분이 string[]; 로 선언되어 있기때문에 []를 씌워 저장.
        const _blockData = [blockData.value];
        const {data : block } = await axios.post("http://localhost:8080/block/mine",{data : _blockData});
    }
//  /block/mine 으로 요청이 가서 새로운 블록을 생성
app.post("/block/mine",(req : Request, res: Response)=>{
    // 블록에 기록할 내용을 받고
    const {data} : {data : Array<string>} = req.body;
    const newBlock : Block | null = Block.generateBlock(ws.latestBlock(),data, ws.getAdjustmentBlock());
    if(newBlock === null) res.send("error");
	// 새 블록이 생성되면 추가.
    ws.addToChain(newBlock);
})
profile
한걸음씩

0개의 댓글