상세 컨텐츠

본문 제목

Golang - libp2p를 이용한 chat 실습

Programming Language/Go

by Yongari 2023. 2. 23. 21:52

본문

 

Go 언어를 이용한 p2p 채팅 실습

 

이것은 libp2p 라이브러리를 사용한 p2p(peer-to-peer) 통신의 사용을 보여주는 간단한 채팅 애플리케이션이다. 중앙 서버 없이도 두 사용자가 직접 네트워크 연결을 통해 서로 통신할 수 있습니다

 

환경설정

 

go get "package" 로 환경설정 

go mod tidy

 

 

 

go 소스코드를 구성하는 내용

 

  • "flag" 패키지를 사용하여 소스 포트와  추가 문자열을 지정하는 일부 플래그를 정의하는 것으로 시작합니다. 또한 "help" 및 "debug" 플래그는 각각 도움말 정보를 표시하고 디버깅을 활성화하도록 정의됩니다. (터미널을 참조하면 됩니다.)

  • "makeHost" 함수는 새 RSA 키 쌍을 생성하고 지정된 포트 번호와 ID를 사용하여 새 호스트를 생성합니다.
    프로세스 실행 중에 발생하는 모든 오류와 함께 호스트가 반환됩니다.

  • "StartPeer" 함수는 들어오는 연결을 수신하도록 스트림 핸들러를 설정하고 사용자가 다른 노드에 연결하도록 지시사항을 표시합니다. 연결이 설정되면 함수는 입력 스트림에서 읽을 새로운 고루틴과 출력 스트림에 쓸 다른 고루틴을 생성합니다. 

  • 새 스트림이 설정되면 "handleStream" 함수를 호출됩니다. 스트림을 사용하여 새로운 ReadWriter 개체를 생성하고 두 개의 고루틴을 생성합니다. 하나는 스트림에서 읽을 것이고 다른 하나는 스트림에 쓸 것입니다.

  • "ReadData" 함수는 입력 스트림에서 읽고 수신된 데이터를 콘솔로 인쇄합니다.

  • "writeData" 함수는 콘솔에서 데이터를 읽고 출력 스트림에 데이터를 씁니다. 두 기능 모두 오류가 발생할 때까지 무한 루프에서 실행됩니다.
  • 마지막으로, "메인" 함수는 플래그(flag)를 구문 분석하고, 지정된 포트와 ID를 사용하여 호스트를 만들고, 대상 주소가 제공되었는지 여부에 따라 "startPeer" 또는 "startPeerAndConnect" 함수를 호출하여 채팅 프로그램을 시작합니다. 프로그램은 "select {}" 문을 사용하여 무기한 실행됩니다.
 
 

go 소스코드

/*
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Juan Batiz-Benet
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * This program demonstrate a simple chat application using p2p communication.
 *
 */
package main

import (
	"bufio"
	"context"
	"crypto/rand"
	"flag"
	"fmt"
	"io"
	"log"
	mrand "math/rand"
	"os"

	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/host"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/core/peerstore"

	"github.com/multiformats/go-multiaddr"
)

func handleStream(s network.Stream) {
	log.Println("Got a new stream!")

	// Create a buffer stream for non blocking read and write.
	//읽기 및 쓰기를 차단하지 않는 버퍼 스트림을 만듭니다.
	rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))

	//고루틴 readData(rw) 호출
	//고루틴 writeData(rw) 호출
	go readData(rw)
	go writeData(rw)

	// stream 's' will stay open until you close it (or the other side closes it).
	//스트림 's'는 사용자가 닫을 때까지(또는 다른 쪽이 닫을 때까지) 열려 있습니다.
}

func readData(rw *bufio.ReadWriter) {
	for {
		//str변수가 데이터를 읽음 
		str, _ := rw.ReadString('\n')

		if str == "" {
			return
		}
		if str != "\n" {
			// Green console colour: 	\x1b[32m
			// Reset console colour: 	\x1b[0m
			// 녹색 콘솔 색상 : \x1b[32m]
			// 콘솔 색상 재설정: \x1b[0m]
			fmt.Printf("\x1b[32m%s\x1b[0m> ", str)
		}

	}
}

func writeData(rw *bufio.ReadWriter) {
	stdReader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("> ")
		sendData, err := stdReader.ReadString('\n')
		if err != nil {
			log.Println(err)
			return
		}

		//쓰기함수를 통해 sendData를 보냄?
		rw.WriteString(fmt.Sprintf("%s\n", sendData))
		rw.Flush()
	}
}

func main() {
	//ctx 변수에 context 변수 저장 
	ctx, cancel := context.WithCancel(context.Background())
	//예약어 defer 사용
	defer cancel()

	//flag는 명령줄 인자 읽는 함수 
	sourcePort := flag.Int("sp", 0, "Source port number")
	dest := flag.String("d", "", "Destination multiaddr string")
	help := flag.Bool("help", false, "Display help")
	debug := flag.Bool("debug", false, "Debug generates the same node ID on every execution")

	flag.Parse()

	if *help {
		fmt.Printf("This program demonstrates a simple p2p chat application using libp2p\n\n")
		fmt.Println("Usage: Run './chat -sp <SOURCE_PORT>' where <SOURCE_PORT> can be any port number.")
		fmt.Println("Now run './chat -d <MULTIADDR>' where <MULTIADDR> is multiaddress of previous listener host.")

		os.Exit(0)
	}

	// If debug is enabled, use a constant random source to generate the peer ID. Only useful for debugging,
	// off by default. Otherwise, it uses rand.Reader.

	// 디버그를 사용할 수 있는 경우 일정한 랜덤 소스를 사용하여 피어 ID를 생성합니다. 디버깅에만 유용합니다,
	// 그렇지 않으면 랜덤.reader 를 사용합니다.

	var r io.Reader
	if *debug {
		// Use the port number as the randomness source.
		// This will always generate the same host ID on multiple executions, if the same port number is used.
		// Never do this in production code.
		// 포트 번호를 랜덤 소스로 사용합니다.
		// 이렇게 하면 동일한 포트 번호가 사용되는 경우 여러 실행에서 항상 동일한 호스트 ID가 생성됩니다.
		// 프로덕션 코드에서 이 작업을 수행하지 마십시오.
		r = mrand.New(mrand.NewSource(int64(*sourcePort)))
	} else {
		r = rand.Reader
	}

	h, err := makeHost(*sourcePort, r)
	if err != nil {
		log.Println(err)
		return
	}

	if *dest == "" {
		startPeer(ctx, h, handleStream)
	} else {
		rw, err := startPeerAndConnect(ctx, h, *dest)
		if err != nil {
			log.Println(err)
			return
		}

		// Create a thread to read and write data.
		go writeData(rw)
		go readData(rw)

	}

	// Wait forever
	select {}
}

func makeHost(port int, randomness io.Reader) (host.Host, error) {
	// Creates a new RSA key pair for this host.
	// 이 호스트에 대한 새 RSA 키 쌍을 생성합니다.
	prvKey, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, randomness)
	if err != nil {
		log.Println(err)
		return nil, err
	}

	// 0.0.0.0 will listen on any interface device.
	// 0.0.0.0은 모든 인터페이스 장치에서 수신합니다.
	sourceMultiAddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))

	// libp2p.New constructs a new libp2p Host.
	// Other options can be added here.
	// libp2p.New는 새 libp2p Host를 구성합니다.
	// 다른 옵션을 여기에 추가할 수 있습니다.
	return libp2p.New(
		libp2p.ListenAddrs(sourceMultiAddr),
		libp2p.Identity(prvKey),
	)
}

func startPeer(ctx context.Context, h host.Host, streamHandler network.StreamHandler) {
	// Set a function as stream handler.
	// This function is called when a peer connects, and starts a stream with this protocol.
	// Only applies on the receiving side.
	// 함수를 스트림 처리기로 설정합니다.
	// 이 기능은 피어가 연결하고 이 프로토콜로 스트림을 시작할 때 호출됩니다.
	// 수신측에만 적용됩니다.
	h.SetStreamHandler("/chat/1.0.0", streamHandler)

	// Let's get the actual TCP port from our listen multiaddr, in case we're using 0 (default; random available port).
	// 0(기본값; 임의 사용 가능한 포트)을 사용하는 경우를 대비하여 수신이 되는 멀티addr 에서 실제 TCP 포트를 가져옵니다.
	var port string
	for _, la := range h.Network().ListenAddresses() {
		if p, err := la.ValueForProtocol(multiaddr.P_TCP); err == nil {
			port = p
			break
		}
	}

	if port == "" {
		log.Println("was not able to find actual local port")
		return
	}

	log.Printf("Run './chat -d /ip4/127.0.0.1/tcp/%v/p2p/%s' on another console.\n", port, h.ID().Pretty())
	log.Println("You can replace 127.0.0.1 with public IP as well.")
	log.Println("Waiting for incoming connection")
	log.Println()
}

func startPeerAndConnect(ctx context.Context, h host.Host, destination string) (*bufio.ReadWriter, error) {
	log.Println("This node's multiaddresses:")
	for _, la := range h.Addrs() {
		log.Printf(" - %v\n", la)
	}
	log.Println()

	// Turn the destination into a multiaddr.
	// 대상을 다중 addr로 변경합니다.
	maddr, err := multiaddr.NewMultiaddr(destination)
	if err != nil {
		log.Println(err)
		return nil, err
	}

	// Extract the peer ID from the multiaddr.
	// 다중 addr 기능에서 피어 ID를 추출합니다.
	info, err := peer.AddrInfoFromP2pAddr(maddr)
	if err != nil {
		log.Println(err)
		return nil, err
	}

	// Add the destination's peer multiaddress in the peerstore.
	// This will be used during connection and stream creation by libp2p.
	// 피어 store에 목적지의 피어  multiaddress를 추가합니다.
	// 이것은 libp2p에 의한 연결 및 스트림 생성 중에 사용될 것이다.
	h.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL)

	// Start a stream with the destination.
	// Multiaddress of the destination peer is fetched from the peerstore using 'peerId'.
	// 목적지로 스트림을 시작합니다.
	// 'peerId'를 사용하여 목적지 피어의 다중 주소를 피어 저장소에서 가져옵니다.
	s, err := h.NewStream(context.Background(), info.ID, "/chat/1.0.0")
	if err != nil {
		log.Println(err)
		return nil, err
	}
	log.Println("Established connection to destination")

	// Create a buffered stream so that read and writes are non blocking.
	// 읽기 및 쓰기가 차단되지 않도록 버퍼링된 스트림을 만듭니다.
	rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))

	return rw, nil
}

 

 

Go 빌드 후 실행

cd "go 소스가 있는 경로"

go build . 

./chat

 

 

스크린샷 참조
터미널1과 터미널2에서 실습한 사진입니다. 

관련글 더보기