在Golang中通过HTTP网络与TCP服务器进行通信的RESTful HTTP API

71 阅读3分钟

在这个例子中,我们将实现一个RESTful公共HTTP API,它与我们内部的TCP服务器进行通信,并有一个HTTP客户端来存储数据。这个过程非常简单,如下图所示:

  1. 用户向我们的公共RESTful HTTP API发送HTTP请求

  2. RESTful HTTP API将请求转发给内部TCP服务器的HTTP客户端

  3. TCP服务器存储数据

  4. TCP服务器通过HTTP网络向TCP客户端返回响应

  5. RESTful HTTP API向用户作出回应

User (HTTP request) -> RESTful HTTP API (HTTP request) -> TCP Server (TCP response) -> RESTful HTTP API (HTTP response) -> User

改进之处

在可能的改进方面,你可以在客户端对HTTP请求进行超时处理:

客户端结构

├── cmd
│   └── client
│       └── main.go
└── internal
    ├── pkg
    │   ├── client
    │   │   └── client.go
    │   ├── router
    │   │   └── router.go
    │   └── server
    │       └── server.go
    └── user
        └── create.go

客户端文件

main.go

package main

import (
	"log"
	"time"

	"github.com/inanzzz/client/internal/pkg/client"
	"github.com/inanzzz/client/internal/pkg/router"
	"github.com/inanzzz/client/internal/pkg/server"
)

func main() {
	clt := client.New("http://0.0.0.0:9999/api/v1", 3*time.Second)

	rtr := router.New()
	rtr.RegisterUser(clt)

	srv := server.New("0.0.0.0:8888", rtr.Handler)
	log.Fatalln(srv.ListenAndServe())
}

client.go

package client

import (
	"context"
	"net/http"
	"strings"
	"time"
)

type Client struct {
	baseURL string
	timeout time.Duration
	client  http.Client
}

func New(baseURL string, timeout time.Duration) Client {
	return Client{
		baseURL: baseURL,
		timeout: timeout,
		client:  http.Client{},
	}
}

func (c Client) Request(ctx context.Context, method, url, body string, headers map[string]string) (*http.Response, error) {
	ctx, cancel := context.WithTimeout(ctx, c.timeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+url, strings.NewReader(body))
	if err != nil {
		return nil, err
	}

	for k, v := range headers {
		req.Header.Add(k, v)
	}

	res, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}

	return res, nil
}

router.go

package router

import (
	"net/http"

	"github.com/inanzzz/client/internal/pkg/client"
	"github.com/inanzzz/client/internal/user"
	"github.com/julienschmidt/httprouter"
)

type Router struct {
	Handler *httprouter.Router
}

func New() *Router {
	rtr := httprouter.New()
	rtr.RedirectTrailingSlash = false
	rtr.RedirectFixedPath = false

	return &Router{
		Handler: rtr,
	}
}

func (r *Router) RegisterUser(clt client.Client) {
	r.Handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(clt).Handle)
}

server.go

package server

import (
	"net/http"
)

func New(adr string, hnd http.Handler) http.Server {
	return http.Server{
		Addr:    adr,
		Handler: hnd,
	}
}

创建.go

package user

import (
	"context"
	"encoding/json"
	"io/ioutil"
	"log"
	"net/http"

	"github.com/inanzzz/client/internal/pkg/client"
)

type CreateRequestBody struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Create struct {
	client client.Client
}

func NewCreate(clt client.Client) Create {
	return Create{
		client: clt,
	}
}

// POST /api/v1/users
func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
	var req CreateRequestBody

	// Map HTTP request to request model
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		log.Printf("unable to decode request: %v", err)
		return
	}

	// Convert request model to client request body
	body, err := json.Marshal(req)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		log.Printf("unable to marshal request: %v", err)
		return
	}

	// Store request model
	res, err := c.client.Request(context.Background(), http.MethodPost, "/users", string(body), nil)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		log.Printf("unable to store request: %v", err)
		return
	}
	defer res.Body.Close()

	// Check if the response code is an expected value
	if res.StatusCode != http.StatusOK {
		w.WriteHeader(http.StatusInternalServerError)
		log.Print("unable to store data")
		return
	}

	// Convert response model to HTTP response
	data, err := ioutil.ReadAll(res.Body)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		log.Print("unable to read client response")
		return
	}

	// Respond
	w.Header().Add("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(data)
}

服务器结构

├── cmd
│   └── server
│       └── main.go
└── internal
    ├── pkg
    │   ├── http
    │   │   ├── router.go
    │   │   └── server.go
    │   └── tcp
    │       └── listener.go
    └── user
        └── create.go

文件

main.go

package main

import (
	"log"

	"github.com/inanzzz/client/internal/pkg/http"
	"github.com/inanzzz/client/internal/pkg/tcp"

	nethttp "net/http"
)

func main() {
	listener, err := tcp.NewListener("0.0.0.0:9999")
	if err != nil {
		log.Fatalln(err)
	}

	rtr := http.NewRouter()
	rtr.RegisterUser(listener)

	srv := http.NewServer(*rtr)
	if err := srv.Serve(listener); err == nethttp.ErrServerClosed {
		log.Println("Shutdown")
	}
}

router.go

package http

import (
	"net"
	"net/http"

	"github.com/inanzzz/client/internal/user"
	"github.com/julienschmidt/httprouter"
)

type Router struct {
	handler *httprouter.Router
}

func NewRouter() *Router {
	rtr := httprouter.New()
	rtr.RedirectTrailingSlash = false
	rtr.RedirectFixedPath = false

	return &Router{
		handler: rtr,
	}
}

func (r *Router) RegisterUser(tcpListener net.Listener) {
	r.handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(tcpListener).Handle)
}

server.go

package http

import (
	"net/http"
)

func NewServer(router Router) *http.Server {
	return &http.Server{
		Handler: router.handler,
	}
}

监听器.go

package tcp

import (
	"net"
)

func NewListener(address string) (net.Listener, error) {
	return net.Listen("tcp", address)
}

create.go

实际上你不需要net.Listener 以下:

package user

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"time"

	"github.com/google/uuid"
)

type Create struct {
	tcpListener net.Listener
}

func NewCreate(tcpListener net.Listener) Create {
	return Create{
		tcpListener: tcpListener,
	}
}

func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
	log.Println(r.Header.Get("X-Request-ID"))

	body := fmt.Sprintf(`{"code":%d,"data":{"id":"%s","created_at":"%s"}}`,
		http.StatusOK,
		uuid.New().String(),
		time.Now().UTC().Format(time.RFC3339),
	)

	w.Header().Add("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write([]byte(body))
}

测试

假设你的服务器和客户端都在运行:

curl -i -X POST -d '{"username":"username","password":"password"}' "http://0.0.0.0:8888/api/v1/users"

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 05 Jun 2020 17:05:22 GMT
Content-Length: 101

{"code":200,"data":{"id":"7af224a1-5f23-476d-a0a3-92278b0169cc","created_at":"2020-06-05T17:05:22Z"}}

在20/22秒内提供了1000个HTTP请求,相当于每20/22毫秒一个请求。