이번 포스팅에서는 저번 포스팅에 이어서 블록체인 P2P 네트워크를 만들기 위한 기초 작업을 진행해보고자 한다.
이전 글)
2022.06.14 - [BlockChain] - BlockChain - 블록체인 P2P 네트워크 만들기 (1)
현재 p2p.ts 파일은 다음과 같다.
// p2p.ts 파일
import { WebSocket } from "ws";
import { Chain } from "@core/blockchain/chain";
export class P2PServer extends Chain {
private sockets: WebSocket[];
constructor() {
super();
this.sockets = [];
}
listen() {
const server = new WebSocket.Server({ port: 7545 });
// 서버 기준 "connection"
server.on("connection", (socket) => {
console.log("webSocket connected");
this.connectSocket(socket);
});
}
connectToPeer(newPeer: string) {
const socket = new WebSocket(newPeer);
// 클라이언트 기준 "open"
socket.on("open", () => {
this.connectSocket(socket);
});
}
connectSocket(socket: WebSocket) {
this.sockets.push(socket);
socket.on("message", (data: string) => {
console.log(data);
});
socket.send("msg from server");
}
}
현재 connectSocket( ) 함수 안에서 작동하는 코드는 P2P 네트워크에 대한 이해를 돕기 위해 작성했던 예시 코드이다. 이제 해당 내용들을 실제 블록체인 P2P 네트워크 코드로 바꿔주는 작업을 진행해보고자 한다. 중간 완성본을 먼저 살펴본 다음 각각의 코드들이 어떻게 구현되어 있는지 하나하나 알아보도록 하자.
// 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() {
const server = new WebSocket.Server({ port: 7545 });
// 서버 기준 "connection"
server.on("connection", (socket) => {
console.log("webSocket connected");
this.connectSocket(socket);
});
}
connectToPeer(newPeer: string) {
const socket = new WebSocket(newPeer);
// 클라이언트 기준 "open"
socket.on("open", () => {
this.connectSocket(socket);
});
}
connectSocket(socket: WebSocket) {
// 향후 broadcasting을 하기 위한 용도
this.sockets.push(socket);
this.messageHandler(socket);
const data: Message = {
type: MessageType.latest_block,
payload: {},
};
const send = P2PServer.send(socket);
send(data);
}
messageHandler(socket: WebSocket) {
const callback = (_data: string) => {
const result: Message = P2PServer.dataParse<Message>(_data);
const send = P2PServer.send(socket);
switch (result.type) {
// 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(),
};
// ToDo : 체인에 블록을 추가할지 말지 결정
send(message);
break;
}
case MessageType.receivedChain: {
const receivedChain: IBlock[] = result.payload;
// ToDo : 체인을 교체하는 코드 (보다 긴 체인 선택하기)
console.log(receivedChain);
break;
}
}
};
socket.on("message", callback);
}
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());
}
}
작성된 코드를 살펴보면 클라이언트 쪽 노드에서 connectToPeer( ) 메소드를 실행시킨 후 소켓이 열렸을 때 connectSocket( ) 메소드가 실행되고 있는 상황이다. 이해를 돕기 위해 앞으로 클라이언트 입장으로 동작하는 노드를 A 노드, 서버 입장으로 동작하는 노드를 B 노드라고 하겠다.
connectSocket(socket: WebSocket) {
// 향후 broadcasting을 하기 위한 용도
this.sockets.push(socket);
this.messageHandler(socket);
const data: Message = {
type: MessageType.latest_block,
payload: {},
};
const send = P2PServer.send(socket);
send(data);
}
connectSocket( ) 함수 안에서 this.sockets.push(socket) 을 통해 P2PServer 클래스의 속성으로 만들어 주었던 sockets 배열 안에 socket 을 담아주었다. 클라이언트 입장에서는 연결된 서버쪽 소켓 정보가, 서버 입장에서는 연결된 클라이언트 쪽 소켓 정보를 배열 안에 넣어주게 된다. 연결된 소켓 정보를 배열에 담아 저장하는 이유는 향후 broadcasting 을 해주기 위함이다. A 노드 입장에서는 자신과 연결된 모든 노드들에게 업데이트 된 자신의 체인 정보를 broadcasting 해줘야 하기 때문이다. broadcasting 부분은 나중에 구현할 것이기 때문에 일단은 넘어가도록 하겠다.
이후 messageHandler( ) 함수를 실행시키면서 인자값으로 socket을 전달해준다.
messageHandler(socket: WebSocket) {
const callback = (_data: string) => {
const result: Message = P2PServer.dataParse<Message>(_data);
const send = P2PServer.send(socket);
switch (result.type) {
// 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(),
};
// ToDo : 체인에 블록을 추가할지 말지 결정
send(message);
break;
}
case MessageType.receivedChain: {
const receivedChain: IBlock[] = result.payload;
// ToDo : 체인을 교체하는 코드 (보다 긴 체인 선택하기)
console.log(receivedChain);
break;
}
}
};
socket.on("message", callback);
}
messageHandler( ) 메소드 안에서 우선적으로 봐야할 것은 가장 아래에 위치한 socket.on( "message", callback ) 부분이다. socket.on( ) 메소드에 의해 "message" 이벤트가 발생했을 때 (데이터를 전달 받았을 때) 두번째 인자값으로 들어간 callback 함수가 실행되게 된다. 즉 , 클라이언트 입장의 A 노드는 B 노드가 전달하는 데이터를 받을 수 있는 준비상태에 들어가게 된 것이다.
callback 함수를 살펴보면, _data 를 인자값으로 받고 있으며 전달 받은 데이터를 dataParse( ) 함수를 통해 Message 타입의 객체로 변환하고 있다. P2PServer.dataParse( ) 메소드는 string 타입으로 들어온 데이터를 JSON.parse( )를 이용해 객체 형태로 변환하기 위해 따로 빼놓은 함수이다.
static dataParse<T>(_data: string): T {
return JSON.parse(Buffer.from(_data).toString());
}
const result: Message = P2PServer.dataParse<Message>(_data);
Message 타입의 객체는 소켓 통신을 할때 발생하는 이벤트들을 구분해서 처리해주기 위해 interface로 사전에 만들어 놓은 객체 타입이다. Message 타입은 다음과 같다.
enum MessageType {
latest_block = 0,
all_block = 1,
receivedChain = 2,
}
interface Message {
type: MessageType;
payload: any;
}
소켓 통신을 할때 socket.send( ) 메소드에 의해 데이터를 전달하게 되는데 데이터를 전달 받는 쪽 입장에서는 어떤 종류의 데이터가 왔는지에 따라 각각 다른 처리를 해줘야만 한다. 따라서 위와 같이 interface Message를 만들어서 type에 따라 다른 처리를 해줄 수 있게끔 하였다.
다시 callback 함수로 돌아가서 const send = P2PServer.send(socket) 부분을 살펴보자.
static send(_socket: WebSocket) {
return (_data: Message) => {
_socket.send(JSON.stringify(_data));
};
}
P2PServer 클래스 안에서 static 메소드로 send( ) 함수를 만들어 주었는데 해당 함수 역시 데이터를 전달할 때 보다 손쉽게 전달하기 위해 만들어 놓은 함수이다. 고차함수 형태로 구현되어 있는 것을 확인할 수 있는데 send( ) 메소드는 결과적으로 ( _data: Message ) => { } 함수를 return 하게 된다. 고차함수 형태로 send( ) 함수를 정의하였기 때문에 arrow 함수 ( _data: Message ) => { } 안에서 send( ) 메소드의 인자값으로 받은 socket을 사용해 socket.send( ) 메소드를 실행할 수 있다. 실제로 send( ) 메소드가 사용되는 형태를 살펴보면 send( message ) 와 같은 형태로 사용되고 있으며 인자값으로 데이터만을 받고 있기 때문에 보다 직관적으로 코드를 작성할 수 있다.
지금까지의 내용을 정리해보자. 클라이언트 쪽 역할을 하는 A 노드에서 connectToPeer( ) 메소드를 실행시켜 서버쪽 노드와 연결이 이루어진다. 이 때 connectSocket( ) 메소드가 실행되고 connectSocket( ) 메소드 안에서 messageHandler( ) 함수가 실행된다. messageHandler( ) 함수 안에서는 socket.on( "message", callback ) 에 의해 클라이언트 쪽 노드는 데이터를 전달 받을 수 있는 준비상태에 들어간다.
반대로 서버쪽 역할을 하는 B 노드에서는 다음과 같은 과정이 진행된다. 클라이언트 쪽으로부터 연결 요청이 들어오고 웹 소켓이 연결 되었을 때 listen( ) 메소드 안에 있는 socket.on( "connection" , (socket) => { } ) 메소드의 콜백함수가 실행되면서 B 노드에서도 A 노드와 마찬가지로 connectSocket( socket ) 함수가 실행된다.
listen() {
const server = new WebSocket.Server({ port: 7545 });
// 서버 기준 "connection"
server.on("connection", (socket) => {
console.log("webSocket connected");
this.connectSocket(socket);
});
}
connectSocket(socket: WebSocket) {
// 향후 broadcasting을 하기 위한 용도
this.sockets.push(socket);
this.messageHandler(socket);
const data: Message = {
type: MessageType.latest_block,
payload: {},
};
const send = P2PServer.send(socket);
send(data);
}
connectSocket( ) 함수를 살펴보면 위에서 언급했던 messageHandler( ) 함수 호출 이외에 send( ) 함수를 통해 연결된 소켓에 Message 타입으로 정의된 데이터를 전송하고 있는 것을 확인할 수 있다. 즉, 서버 쪽 B노드에서도 messageHandler( ) 함수에 의해 데이터를 전달받을 수 있는 준비 상태가 만들어지고 연결된 클라이언트 쪽 A 노드에게 send( data )로 데이터를 전송하게 되는 것이다. 해당 과정은 클라이언트 쪽 A 노드에서도 마찬가지이다. A 노드에서도 데이터를 전달받을 수 있는 상태가 만들어지고 send( data ) 로 서버 쪽 B 노드에게 데이터를 전달한다.
요약하면, A 노드와 B 노드 모두 socket.on( "message" , callback ) 이 실행되고 A 노드는 B 노드에게 , B 노드는 A노드에게 데이터를 전송하게 된다. 이후의 과정은 callback 함수 내에서 작성된 switch 문을 통해 처리가 되는데 서로가 데이터를 주고 받으면서 블록 체인 상에 블록을 추가할지 , 체인 자체를 교체할지를 결정하게 된다. 해당 부분은 다음번 포스팅에서 좀 더 자세히 다뤄보기로 하자.
다음 글)
2022.06.16 - [BlockChain] - BlockChain - 블록체인 P2P 네트워크 만들기 (3)
'BlockChain' 카테고리의 다른 글
BlockChain - 개인키, 공개키, 서명, 지갑/계정 (1) | 2022.06.17 |
---|---|
BlockChain - 블록체인 P2P 네트워크 만들기 (3) (2) | 2022.06.16 |
BlockChain - 블록체인 P2P 네트워크 만들기 (1) (1) | 2022.06.14 |
BlockChain - TypeScript로 블록체인 만들기 (4) PoW (0) | 2022.06.14 |
BlockChain - TypeScript로 블록체인 만들기 (3) (0) | 2022.06.12 |