P2P 네트워크 연결 코드 리뷰
웹소켓 서버 열기
app.listen(3000, () => {
console.log('server on');
ws.listen();
});
listen() {
const server = new WebSocket.Server({ port: 7545 });
server.on('connection', (_socket) => {
this.connectSocket(_socket);
});
}
- 먼저 서버를 실행하면 ws.listen()에 의해 p2p.ts에 있는 listen()함수가 실행되며 7545 port로 웹소켓 서버가 열립니다.
- 이렇게 열어둔 웹소켓 서버로 누군가가 접속하게 된다면 저는 서버가 되는것이고 요청보낸 컴퓨터는 클라이언트가 되는것입니다.
- 이러한 네트워크 구조를 P2P 네트워크라고 합니다.
웹소켓 서버에 연결하기 ( 클라이언트 )
app.post('/addPeers', (req, res) => {
peers.forEach((peer) => {
ws.connectToPeer(peer);
});
});
[
"ws://192.168.0.123:7545",
"ws://192.168.0.143:7545",
"ws://192.168.0.234:7545",
"ws://192.168.0.256:7545",
"ws://192.168.0.187:7545",
"ws://192.168.0.165:7545",
"ws://192.168.0.162:7545"
]
/addPeers
로 post
요청을 보내면 peer.json
에 배열안에 있는 모든 IP
들에게 웹소켓 요청을 보냅니다.
- 이런식으로 서버에 웹소켓 연결을 요청하게 되면 저는 클라이언트의 역할이 되는것이고 제 요청을 받은 컴퓨터는 서버의 역할이 되는것 입니다.
웹소켓 연결후 데이터 주고 받기 ( 1 )
private sockets: WebSocket[];
constructor() {
super();
this.sockets = [];
}
connectSocket(_socket: WebSocket) {
this.sockets.push(_socket);
this.messageHandler(_socket);
const data: Message = {
type: MessageType.latest_block,
payload: {},
};
this.errorHandler(_socket);
this.send(_socket)(data);
}
errorHandler(_socket: WebSocket) {
const close = () => {
this.sockets.splice(this.sockets.indexOf(_socket), 1);
};
_socket.on('close', close);
_socket.on('error', close);
}
send(_socket: WebSocket) {
return (_data: Message) => {
_socket.send(JSON.stringify(_data));
};
}
- 이제 서버, 클라이언트가 정상적으로 핸드쉐이킹이 되면
connectSocket
함수가 실행됩니다.
함수를 살펴보면 sockets.push
로 현재 연결되어 있는 소켓의 정보를 배열에 담아줍니다, 이때 서버측에서는 클라이언트의 정보를 배열에 담게되고 클라이언트측은 서버의 정보를 배열에 담게됩니다.
errorHandler
함수는 웹소켓 연결이 끊겼을 경우나 error
가 발생했을 경우에만 해당 노드를 배열에서 삭제하여 후에 연결되어 있는 노드들에게 broadcast
를 할 때 연결이 끊긴 노드에게는 broadcast
를 하지 않도록 해주었습니다.
send
함수가 실행되며 현재 연결되어 있는 노드에게 type: MessageType.latest_block
의 데이터를 보냅니다. 이때도 마찬가지로 서버일경우 클라이언트에게, 클라이언트일경우 서버에게 데이터를 보내줍니다.
웹소켓 연결후 데이터 주고 받기 ( 2 )
messageHandler(_socket: WebSocket) {
const callback = (data: string) => {
const result: Message = P2PServer.dataParse<Message>(data);
const send = this.send(_socket);
switch (result.type) {
case MessageType.latest_block: {
const message: Message = {
type: MessageType.all_block,
payload: [this.getLastestBlock()],
};
send(message);
break;
}
case MessageType.all_block: {
const message: Message = {
type: MessageType.receivedChain,
payload: this.getChain(),
};
const [receivedBlock] = result.payload;
const isVaild = this.addToChain(receivedBlock);
if (!isVaild.isError) {
const message: Message = {
type: MessageType.all_block,
payload: [this.getLastestBlock()],
};
this.broadcast(message);
break;
}
send(message);
break;
}
case MessageType.receivedChain: {
const receivedChain: IBlock[] = result.payload;
console.log('체인 통째로 바꿔끼기', receivedChain);
this.handleChainResponse(receivedChain);
break;
}
}
};
_socket.on('message', callback);
}
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 };
const message: Message = {
type: MessageType.receivedChain,
payload: receivedChain,
};
this.broadcast(message);
return { isError: false, value: undefined };
}
public addToChain(_receviedBlock: Block): Failable<undefined, string> {
const isValid = Block.isValidNewBlock(_receviedBlock, this.getLastestBlock());
if (isValid.isError) return { isError: true, error: isValid.error };
this.blockchain.push(_receviedBlock);
return { isError: false, value: undefined };
}
public isValidChain(_chain: Block[]): Failable<undefined, string> {
for (let i = 1; i < _chain.length; i++) {
const newBlock = _chain[i];
const previousBlock = _chain[i - 1];
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
if (isValid.isError === true) return { isError: true, error: isValid.error };
}
return { isError: false, value: undefined };
}
replaceChain(receivedChain: Block[]): Failable<undefined, string> {
const latestReceivedBlock: Block = receivedChain[receivedChain.length - 1];
const latestBlock: Block = this.getLastestBlock();
if (latestReceivedBlock.height === 0) {
return { isError: true, error: '받은 최신블록이 제네시스 블록입니다. ' };
}
if (latestReceivedBlock.height <= latestBlock.height) {
return { isError: true, error: '자신의 체인이 더 길거나 같습니다. ' };
}
if (latestReceivedBlock.previousHash === latestBlock.hash) {
return { isError: true, error: '블록이 하나 모자랍니다. ' };
}
this.blockchain = receivedChain;
return { isError: false, value: undefined };
}
public static isValidNewBlock(_newBlock: Block, _previousBlock: Block): Failable<Block, string> {
if (_previousBlock.height + 1 !== _newBlock.height)
return { isError: true, error: '블록 높이가 맞지않습니다.' };
if (_previousBlock.hash !== _newBlock.previousHash)
return { isError: true, error: '이전 블록 해시가 맞지않습니다' };
if (Block.createBlockHash(_newBlock) !== _newBlock.hash)
return { isError: true, error: '블록해시가 올바르지 않습니다' };
return { isError: false, value: _newBlock };
}
send
함수에서 보낸 data
를 messageHandler
의 _socket.on('message', callback)
메서드에서 받고 callback
함수를 실행합니다.
- 함수의 내용을 보면 맨 처음 웹소켓 통신이 완료되고
switch
문으로 case MessageType.latest_block
으로 빠져 내가 가지고 있는 블록체인의 가장 마지막 블록을 payload
에 담아 case MessageType.all_block
으로 보내줍니다.
- 이번에는
switch
문의 case MessageType.all_block
로 빠져 받은 payload
로 받은 마지막 블록을 addToChain()
함수 받아온 마지막 블록을 인자값으로 넣어 실행합니다.
addToChain
함수를 보면 받은 블록을 본인의 가장 마지막 블록과 같이 isValidNewBlock()
함수의 인자값으로 넣어 블록을 검증하여 블록 높이, 이전 블록해시, 블록해시를 비교하여 검증하고 에러가 없을경우 받은 블록을 리턴합니다.
- 다시
addToChain
함수로 돌아와서 isValid
의 리턴값이 에러이면 에러를 리턴하고 아니면 본인의 블록체인에 추가하고난 후 return
해줍니다.
addToChain
함수에서 에러가 발생하지 않았다면 broadcast
로 sockets
에 있는 모든 노드들에게 새로운 블록을 추가하도록 message
를 보냅니다.
addToChain
함수에서 만약 에러가 발생했다면 type: MessageType.receivedChain
으로 다시 message
를 보내서 case MessageType.receivedChain
으로 빠지게 됩니다. 이때는 블록의 높이가 2 이상 차이났을 경우이니 받은 블록체인을 통째로 갈아끼워주면 됩니다.
receivedChain
에 받은 블록체인을 담아주고, handleChainResponse
함수를 실행시키고 isValidChain
함수로 받은 블록체인을 검증합니다.`isValidChain 함수를 살펴보면 반복문으로 받은 블록체인에 있는 모든 블록들을 검증합니다.
isValidChain
함수에서 에러가 나지 않았으면 replaceChain
함수에 받은 블록체인을 넣어 서로의 블록체인을 비교하여 내가 가지고 있는 체인이 더 짧으면 내 블록체인을 받아온 블록체인으로 통째로 변경해주고 return
해줍니다.
- 마지막으로 받아온 블록체인을
payload
에 담아 broadcast
로 연결되어있는 모든 노드들에게 message
를 보내줍니다.
블록 생성시 broadcast 추가
app.post('/mineBlock', (req, res) => {
const { data } = req.body;
const newBlock = ws.addBlcok(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
const message: Message = {
type: MessageType.all_block,
payload: [newBlock.value],
};
ws.broadcast(message);
res.json(newBlock.value);
});
- 마지막으로 블록을 생성하고 나서도 broadcast로 message를 보내주어 다른 노드들도 똑같이 블록을 추가하도록 해주었습니다.