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를 받을거임
enum 타입으로 객체의 role을 정해준다.
socket으로 보내줄 message의 interface이다.
p2p기능을 구현할 함수들과 속성들을 담은 클래스이다.
socket주소를 담을 배열이다. 이 배열을 활용하여 다양한 ip와 포트에서 들어온 유저와 통신한다.
소켓을 반환하는 함수이다.
소켓과 메세지를 매개변수로 받는 메서드이다.
소켓에 연결하여 본 함수를 실행시키면 일단 입장한 유저의 socket의 remoteAddress와 remotePort를 클래스의 속성(sockets)에 저장하고
socket.send(JSON.stringify(message));
구문에서 send로 message 이벤트를 실행시킨다. message 이벤트는
socket.send 함수가 실행되어야만 실행된다. replaceChain을 통해 longestrule을 적용시켜 접속한 유저들이 체인을 갖게 만들고 이를sockets.forEach로 모든 유저들에게 전달한다.
소켓 서버를 여는 함수이다
상대방이 내 ip에 접속했을 때 소켓을 생성하고 연결해주는 함수이다.
성공한다면 open이벤트가 실행되고 connectSocket함수가 실행되며 나의 socket이 클래스 속성 sockets에 psuh된다.
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
ws는 P2P클래스를 동적으로 할당 받았고, 이는 Chain을 상속받는다. 그러므로 get함수로 chain을 가져올 수 있다.
블록을 채굴한다. generateBlock으로 새로운 블록을 채굴한 후 addToChain으로 새로운 블록을 체인에 추가한다.
현재 os의 ipv4 주소를 받아오는 함수이다. 또한 이 주소를 addToPeer의 파라미터로 보냄으로서 ip를 상대 sockets에 추가함으로서 메세지를 주고받을 수 있다.
sockets배열을 반환하는 api다.
간단하게 버튼만 구현하였다.
<!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>
블록이 주어진 데이터에 따라 추가된 것을 볼 수 있다.
소켓을 오랜만에 사용해보니 이해가 안가는 부분이 많았다.특히 send와 message의 실행조건이 매우 헷갈렸다. 실행순서를 올바르게 잡고 다시 코드를 분석하니 훨씬 수월했다. 블록체인을 점점 깊게 이해해가고 있어 하루하루가 많은 의의가 있다.