블록체인p2p와 socket으로 구현하기

이무헌·2023년 9월 8일
0

blcokchain

목록 보기
4/10
post-thumbnail

1.chain을 p2p로 관리하여 처리하기


import Block from "@coreP2P/block/block";
import Chain from "@coreP2P/block/chain";
import { WebSocket, WebSocketServer } from "ws";

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

// 상태를 지정할 때 사용
// run state work
enum MessageType {
  // 알기 쉽게 사용하려고
  // 0,1,2 상태를 지정한다 했을 때
  lastBlock = 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 = [];
  }

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

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

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

export default P2P;

// ip 주소 연결해서 data를 받을거임

1.MessageType

enum 타입으로 객체의 role을 정해준다.

2.IMessage

socket으로 보내줄 message의 interface이다.

3.P2P

p2p기능을 구현할 함수들과 속성들을 담은 클래스이다.

4.private sockets: Array;

socket주소를 담을 배열이다. 이 배열을 활용하여 다양한 ip와 포트에서 들어온 유저와 통신한다.

5.getSocket

소켓을 반환하는 함수이다.

6.connectSocket

소켓과 메세지를 매개변수로 받는 메서드이다.
소켓에 연결하여 본 함수를 실행시키면 일단 입장한 유저의 socket의 remoteAddress와 remotePort를 클래스의 속성(sockets)에 저장하고


    socket.send(JSON.stringify(message));

구문에서 send로 message 이벤트를 실행시킨다. message 이벤트는
socket.send 함수가 실행되어야만 실행된다.
replaceChain을 통해 longestrule을 적용시켜 접속한 유저들이 체인을 갖게 만들고 이를sockets.forEach로 모든 유저들에게 전달한다.

7.listen

소켓 서버를 여는 함수이다

8.addToPeer

상대방이 내 ip에 접속했을 때 소켓을 생성하고 연결해주는 함수이다.
성공한다면 open이벤트가 실행되고 connectSocket함수가 실행되며 나의 socket이 클래스 속성 sockets에 psuh된다.

2.server


import Block from "@coreP2P/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(express.json());

app.use(express.urlencoded({ extended: false }));
app.use(
  cors({
    // 도메인 혀용 옵션
    // 접근을 허용할 도메인
    // 여러개의 도메인을 허용하고 싶다면 배열의 형태로 넣어주면 된다.
    origin: "http://127.0.0.1:5501",
    // 클라이언트의 요청에 쿠키를 포함할지의 속성
    credentials: true,
  })
);
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 newBlock is null");
  }

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

// post 작성을 했지만 get으로 바꿈=> 오타 문제 때문에 v4를 자신의 os에서 가지고 올거임
app.get("/peer/add", (req: Request, res: Response) => {
  const networkInterface = os.networkInterfaces();
  let v4: string;
  for (const key in networkInterface) {
    const Array = networkInterface[key];

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

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



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

// http://172.25.176.1:8080/peer/add

1./chains

ws는 P2P클래스를 동적으로 할당 받았고, 이는 Chain을 상속받는다. 그러므로 get함수로 chain을 가져올 수 있다.

2./block/mine

블록을 채굴한다. generateBlock으로 새로운 블록을 채굴한 후 addToChain으로 새로운 블록을 체인에 추가한다.

3./peer/add

현재 os의 ipv4 주소를 받아오는 함수이다. 또한 이 주소를 addToPeer의 파라미터로 보냄으로서 ip를 상대 sockets에 추가함으로서 메세지를 주고받을 수 있다.

4. /peer

sockets배열을 반환하는 api다.

3. 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
      src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.5.0/axios.min.js"
      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>

4.결과

블록이 주어진 데이터에 따라 추가된 것을 볼 수 있다.

5.느낀점

소켓을 오랜만에 사용해보니 이해가 안가는 부분이 많았다.특히 send와 message의 실행조건이 매우 헷갈렸다. 실행순서를 올바르게 잡고 다시 코드를 분석하니 훨씬 수월했다. 블록체인을 점점 깊게 이해해가고 있어 하루하루가 많은 의의가 있다.

profile
개발당시에 직면한 이슈를 정리하는 곳

0개의 댓글