TIL 22 - Golang으로 블록체인 만들기(RESTful하게 수정하기) 6

프동프동·2023년 2월 10일
0

TIL

목록 보기
22/46

RESTful하게 수정하기

  • POST
    • 블록 생성 및 추가하기
  • GET
    • 모든 블록 가져오기
  • 소스 코드
    • main.go

      package main
      
      import (
      	"coin/exam16/blockchain"
      	"coin/exam16/utils"
      	"encoding/json"
      	"fmt"
      	"log"
      	"net/http"
      )
      
      const port string = ":4000"
      
      type URL string
      
      func (u URL) MarshalText() ([]byte, error) {
      	url := fmt.Sprintf("http://localhost%s%s", port, u)
      	return []byte(url), nil
      }
      
      type URLDescription struct {
      	URL         URL    `json:"url"`
      	Method      string `json:"method"`
      	Description string `json:"description"`
      	Payload     string `json:"payload,omitempty"`
      }
      
      type AddBlockBody struct {
      	Message string `json:"message"`
      }
      
      func (u URLDescription) String() string {
      	return "Hello I'm the URL Description"
      }
      
      func documentation(rw http.ResponseWriter, r *http.Request) {
      	data := []URLDescription{
      		{
      			URL:         URL("/"),
      			Method:      "GET",
      			Description: "See Documentation",
      		},
      		{
      			URL:         URL("/blocks"),
      			Method:      "GET",
      			Description: "See All Block",
      		},
      		{
      			URL:         URL("/blocks"),
      			Method:      "POST",
      			Description: "Add A Block",
      			Payload:     "data:string",
      		},
      		{
      			URL:         URL("/blocks/{id}"),
      			Method:      "GET",
      			Description: "See A Block",
      		},
      	}
      
      	rw.Header().Add("Content-Type", "application/json")
      	json.NewEncoder(rw).Encode(data)
      
      }
      func blocks(rw http.ResponseWriter, r *http.Request) {
      	switch r.Method {
      	case "GET":
      		rw.Header().Add("Content-Type", "application/json")
      		json.NewEncoder(rw).Encode(blockchain.GetBlockchain().AllBlocks())
      	case "POST":
      		var addBlockBody AddBlockBody
      
      		utils.HandleErr(json.NewDecoder(r.Body).Decode(&addBlockBody))
      		blockchain.GetBlockchain().AddBlock(addBlockBody.Message)
      		rw.WriteHeader(http.StatusCreated)
      	}
      }
      func main() {
      	http.HandleFunc("/", documentation)
      	http.HandleFunc("/blocks", blocks)
      	fmt.Printf("Listening on http://localhost%s\n", port)
      	log.Fatal(http.ListenAndServe(port, nil))
      }
  • 실행 결과
    • 모든 블록 가져오기

    • GET

    • URL : /blocks

      HTTP/1.1 200 OK
      Content-Type: application/json
      Date: Wed, 28 Dec 2022 04:34:24 GMT
      Content-Length: 115
      Connection: close
      
      [
        {
          "Data": "Genesis Block",
          "Hash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
          "PrevHash": ""
        }
      ]
    • 블록 생성 및 추가하기

    • POST

    • URL : /blocks

      ```go
      HTTP/1.1 201 Created
      Date: Wed, 28 Dec 2022 04:34:51 GMT
      Content-Length: 0
      Connection: close
      ```
    • 추가된 블록 확인하기

    • GET

    • URL : /Blocks

      ```go
      HTTP/1.1 200 OK
      Content-Type: application/json
      Date: Wed, 28 Dec 2022 04:35:43 GMT
      Content-Length: 477
      Connection: close
      
      [
        {
          "Data": "Genesis Block",
          "Hash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
          "PrevHash": ""
        },
        {
          "Data": "Data for my block",
          "Hash": "6de940f3a7ead5008e358bdda0ac9b0234a4e8dbc94c31ca1dd91b8798607182",
          "PrevHash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3"
        },
      ]
      ```

상기 코드 Refactoring 진행

  • NewServeMux로 새로운 Mux를 만들어 explorer과 rest를 나눠줬다
  • 소스 코드
    • main.go
      package main
      
      import (
      	"coin/exam17/explorer"
      	"coin/exam17/rest"
      )
      
      func main() {
      	go explorer.Start(3000)
      	rest.Start(4000)
      }
    • rest/rest.go
      package rest
      
      import (
      	"coin/exam17/blockchain"
      	"coin/exam17/utils"
      	"encoding/json"
      	"fmt"
      	"log"
      	"net/http"
      )
      
      var port string
      
      type url string
      
      func (u url) MarshalText() ([]byte, error) {
      	url := fmt.Sprintf("http://localhost%s%s", port, u)
      	return []byte(url), nil
      }
      
      type urlDescription struct {
      	URL         url    `json:"url"`
      	Method      string `json:"method"`
      	Description string `json:"description"`
      	Payload     string `json:"payload,omitempty"`
      }
      
      type addBlockBody struct {
      	Message string `json:"message"`
      }
      
      func (u urlDescription) String() string {
      	return "Hello I'm the URL Description"
      }
      
      func documentation(rw http.ResponseWriter, r *http.Request) {
      	data := []urlDescription{
      		{
      			URL:         url("/"),
      			Method:      "GET",
      			Description: "See Documentation",
      		},
      		{
      			URL:         url("/blocks"),
      			Method:      "GET",
      			Description: "See All Block",
      		},
      		{
      			URL:         url("/blocks"),
      			Method:      "POST",
      			Description: "Add A Block",
      			Payload:     "data:string",
      		},
      		{
      			URL:         url("/blocks/{id}"),
      			Method:      "GET",
      			Description: "See A Block",
      		},
      	}
      
      	rw.Header().Add("Content-Type", "application/json")
      	json.NewEncoder(rw).Encode(data)
      
      }
      func blocks(rw http.ResponseWriter, r *http.Request) {
      	switch r.Method {
      	case "GET":
      		rw.Header().Add("Content-Type", "application/json")
      		json.NewEncoder(rw).Encode(blockchain.GetBlockchain().AllBlocks())
      	case "POST":
      		var addBlockBody addBlockBody
      
      		utils.HandleErr(json.NewDecoder(r.Body).Decode(&addBlockBody))
      		blockchain.GetBlockchain().AddBlock(addBlockBody.Message)
      		rw.WriteHeader(http.StatusCreated)
      	}
      }
      func Start(aPort int) {
      	handler := http.NewServeMux()
      	port = fmt.Sprintf(":%d", aPort)
      	handler.HandleFunc("/", documentation)
      	handler.HandleFunc("/blocks", blocks)
      	fmt.Printf("Listening on http://localhost%s\n", port)
      	log.Fatal(http.ListenAndServe(port, handler))
      }
    • erplorer/explorer.go
      package explorer
      
      import (
      	"coin/exam17/blockchain"
      	"fmt"
      	"log"
      	"net/http"
      	"text/template"
      )
      
      const (
      	templateDir string = "explorer/templates/"
      )
      
      var templates *template.Template
      
      type homeData struct {
      	PageTitle string
      	Blocks    []*blockchain.Block
      }
      
      func home(rw http.ResponseWriter, r *http.Request) {
      	data := homeData{"Home", blockchain.GetBlockchain().AllBlocks()}
      	templates.ExecuteTemplate(rw, "home", data)
      }
      func add(rw http.ResponseWriter, r *http.Request) {
      	switch r.Method {
      	case "GET":
      		templates.ExecuteTemplate(rw, "add", nil)
      	case "POST":
      		r.ParseForm()
      		data := r.FormValue("blockData")
      		fmt.Println(data)
      		blockchain.GetBlockchain().AddBlock(data)
      		http.Redirect(rw, r, "/", http.StatusPermanentRedirect)
      
      	}
      }
      func Start(port int) {
      	handle := http.NewServeMux()
      	templates = template.Must(template.ParseGlob(templateDir + "pages/*.gohtml"))
      	templates = template.Must(templates.ParseGlob(templateDir + "partials/*.gohtml"))
      	handle.HandleFunc("/", home)
      	handle.HandleFunc("/add", add)
      	fmt.Printf("Listening on http://localhost%d\n", port)
      	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), handle))
      }

Gorilla/mux 패키지 사용하기

  • mux 변경
  • GET메서드 URL에서 파라미터 가져오기
  • 소스 코드
    • main.go
      package main
      
      import (
      	"coin/exam18/rest"
      )
      
      func main() {
      
      	rest.Start(4000)
      }
    • rest.go
      package rest
      
      import (
      	"coin/exam18/blockchain"
      	"coin/exam18/utils"
      	"encoding/json"
      	"fmt"
      	"log"
      	"net/http"
      
      	"github.com/gorilla/mux"
      )
      
      //
      // ...
      //
      
      func block(rw http.ResponseWriter, r *http.Request) {
      	vars := mux.Vars(r)
      	fmt.Println(vars)
      	id := vars["id"]
      }
      func Start(aPort int) {
      	router := mux.NewRouter()
      	port = fmt.Sprintf(":%d", aPort)
      	router.HandleFunc("/", documentation).Methods("GET")
      	router.HandleFunc("/blocks", blocks).Methods("GET", "POST")
      	router.HandleFunc("/blocks/{id:[0-9]+}", block).Methods("GET")
      	fmt.Printf("Listening on http://localhost%s\n", port)
      	log.Fatal(http.ListenAndServe(port, router))
      }
  • 실행 결과
    Listening on http://localhost:4000
    map[id:1]

블록의 Height 값을 받아 해당하는 블록정보 가져오기

  • 소스 코드
    • main.go
      package main
      
      import (
      	"coin/exam19/rest"
      )
      
      func main() {
      
      	rest.Start(4000)
      }
    • rest/rest.go
      package rest
      
      import (
      	"coin/exam19/blockchain"
      	"coin/exam19/utils"
      	"encoding/json"
      	"fmt"
      	"log"
      	"net/http"
      	"strconv"
      
      	"github.com/gorilla/mux"
      )
      
      // ...
      
      func documentation(rw http.ResponseWriter, r *http.Request) {
      	data := []urlDescription{
      		// ... 
      		// 블록의 height값을 받는다
      		{
      			URL:         url("/blocks/{height}"),
      			Method:      "GET",
      			Description: "See A Block",
      		},
      	}
      }
      
      // URL의 파라미터를 받아서 해당하는 블록을 찾아 json형태로 출력한다.
      func block(rw http.ResponseWriter, r *http.Request) {
      	vars := mux.Vars(r)
      	id, err := strconv.Atoi(vars["height"])
      	utils.HandleErr(err)
      
      	block := blockchain.GetBlockchain().GetBlock(id)
      	json.NewEncoder(rw).Encode(block)
      
      }
      
      func Start(aPort int) {
      	router := mux.NewRouter()
      	// ...
      	router.HandleFunc("/blocks/{height:[0-9]+}", block).Methods("GET")
      	fmt.Printf("Listening on http://localhost%s\n", port)
      	log.Fatal(http.ListenAndServe(port, router))
      }
    • blockchain/blockchain.go
      package blockchain
      
      // 블록의 데이터에 height 값을 넣어준다.
      
      type Block struct {
      	Data     string `json:"data"`
      	Hash     string `json:"hash"`
      	PrevHash string `json:"prevhash,omitempty"`
      	Height   int    `json:"height"`
      }
      
      // ...
      // 블록을 생성할 때 height 값도 함께 넣어준다.
      
      func createBlock(data string) *Block {
      	newBlock := Block{data, "", getLastHash(), len(GetBlockchain().blocks) + 1}
      	newBlock.calculateHash()
      	return &newBlock
      }
      
      // ...
      
      // height 값을 받아 하나의 블록을 반환한다
      func (b *blockchain) GetBlock(height int) *Block {
      	return b.blocks[height-1]
      }
  • 실행 결과
    1. 기존에 저장된 블록체인 정보를 가져온다.

      [
        {
          "data": "Genesis Block",
          "hash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
          "height": 1
        }
      ]
    2. 하나의 블록을 생성한다.

      HTTP/1.1 201 Created
      Date: Wed, 28 Dec 2022 08:17:07 GMT
      Content-Length: 0
      Connection: close
    3. 생성된 블록을 확인한다.

      [
        {
          "data": "Genesis Block",
          "hash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
          "height": 1
        },
        {
          "data": "Data for my block",
          "hash": "6de940f3a7ead5008e358bdda0ac9b0234a4e8dbc94c31ca1dd91b8798607182",
          "prevhash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
          "height": 2
        }
      ]
    4. height가 2번인 블록의 정보를 가져온다

      {
        "data": "Data for my block",
        "hash": "6de940f3a7ead5008e358bdda0ac9b0234a4e8dbc94c31ca1dd91b8798607182",
        "prevhash": "89eb0ac031a63d2421cd05a2fbe41f3ea35f5c3712ca839cbf6b85c4ee07b7a3",
        "height": 2
      }

에러 처리하기

  • 소스 코드
    • main.go
      package main
      
      import (
      	"coin/exam20/rest"
      )
      
      func main() {
      
      	rest.Start(4000)
      }
    • rest/rest.go
      package rest
      
      // 에러 메세지를 저장하기 위한 구조체 선언
      type errorResponse struct {
      	ErrorMessage string `json:"errormessage"`
      }
      
      // ...
      
      func block(rw http.ResponseWriter, r *http.Request) {
      	vars := mux.Vars(r)
      	id, err := strconv.Atoi(vars["height"])
      	utils.HandleErr(err)
      
      	block, err := blockchain.GetBlockchain().GetBlock(id)
      	encoder := json.NewEncoder(rw)
      	if err == blockchain.ErrNotFound {
      		encoder.Encode(errorResponse{fmt.Sprint(err)})
      	} else {
      		encoder.Encode(block)
      	}
      
      }
      
      // ...
    • blockchain/blockchain.go
      // 에러 메세지를 만든다.
      var ErrNotFound = errors.New("block not found")
      
      // Client가 잘못된 인덱스에 접근하면 에러 메세지를 리턴한다.
      func (b *blockchain) GetBlock(height int) (*Block, error) {
      	if height > len(b.blocks) {
      		return nil, ErrNotFound
      	}
      	return b.blocks[height-1], nil
      }
  • 실행 결과
    HTTP/1.1 200 OK
    Date: Wed, 28 Dec 2022 08:41:00 GMT
    Content-Length: 35
    Content-Type: text/plain; charset=utf-8
    Connection: close
    
    {
      "errormessage": "block not found"
    }

MiddleWare 적용

  • Header에 Content-Type, application/json을 넣기 위해 미들웨어형태로 구현
  • 소스 코드
    • rest/rest.go
      package rest
      
      // ...
      
      func jsonContentTypeMiddleware(next http.Handler) http.Handler {
      	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
      		rw.Header().Add("Content-Type", "application/json")
      		next.ServeHTTP(rw, r)
      	})
      }
      func Start(aPort int) {
      	router := mux.NewRouter()
      	router.Use(jsonContentTypeMiddleware)
      	port = fmt.Sprintf(":%d", aPort)
      	router.HandleFunc("/", documentation).Methods("GET")
      	//...
      }
  • 실행 결과
    HTTP/1.1 200 OK
    Content-Type: application/json
    Date: Wed, 28 Dec 2022 15:11:12 GMT
    Content-Length: 366
    Connection: close
profile
좋은 개발자가 되고싶은

0개의 댓글