03/06/2020 - GO
In this example we are going to implement a RESTful public HTTP API that communicates with our internal TCP server with a TCP client for storing data. The TCP client code is in our HTTP API. The process is very simple as shown below.
User (HTTP request) -> RESTful HTTP API (TCP request) -> TCP Server (TCP response) -> RESTful HTTP API (HTTP response) -> User
You can do the following in terms of possible improvements.
├── cmd
│ └── client
│ └── main.go
└── internal
├── pkg
│ ├── http
│ │ ├── router.go
│ │ └── server.go
│ ├── storage
│ │ ├── request.go
│ │ ├── response.go
│ │ └── storage.go
│ └── tcp
│ └── connection.go
└── user
└── create.go
package main
import (
"log"
"github.com/inanzzz/client/internal/pkg/http"
"github.com/inanzzz/client/internal/pkg/storage"
"github.com/inanzzz/client/internal/pkg/tcp"
)
func main() {
tcpCon, err := tcp.NewConnection("0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer tcpCon.Close()
str := storage.New(tcpCon)
rtr := http.NewRouter()
rtr.RegisterUser(str)
srv := http.NewServer("0.0.0.0:8888", *rtr)
log.Fatalln(srv.ListenAndServe())
}
package http
import (
"net/http"
"github.com/inanzzz/client/internal/pkg/storage"
"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(str storage.Storage) {
r.handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(str).Handle)
}
package http
import (
"net/http"
)
func NewServer(adr string, rtr Router) http.Server {
return http.Server{
Addr: adr,
Handler: rtr.handler,
}
}
package storage
import (
"bytes"
"encoding/json"
)
// NewRequest creates a network request from a copy of `outgoing` struct.
func NewRequest(outgoing interface{}) (*bytes.Buffer, error) {
request := bytes.NewBuffer(nil)
if err := json.NewEncoder(request).Encode(outgoing); err != nil {
return nil, err
}
return request, nil
}
package storage
import (
"encoding/json"
"strings"
)
// NewResponse updates the reference of `incoming` struct with the
// response `body` string.
func NewResponse(body string, incoming interface{}) error {
return json.Unmarshal(
[]byte(strings.TrimSpace(body)),
incoming,
)
}
package storage
import (
"bufio"
"io"
"net"
"strings"
"github.com/pkg/errors"
)
type Storage struct {
connection net.Conn
}
func New(con net.Conn) Storage {
return Storage{
connection: con,
}
}
// Store writes data to network connection for handling. It prepares a
// network request from a copy of `outgoing` struct and updates the
// reference of `incoming` struct with response body.
func (s Storage) Store(outgoing interface{}, incoming interface{}) error {
req, err := NewRequest(outgoing)
if err != nil {
return errors.Wrap(err, "create request")
}
clientReader := bufio.NewReader(req)
serverReader := bufio.NewReader(s.connection)
for {
req, err := clientReader.ReadString('\n')
switch err {
case nil:
if _, err = s.connection.Write([]byte(strings.TrimSpace(req) + "\n")); err != nil {
return errors.Wrap(err, "send request")
}
case io.EOF:
return errors.Wrap(err, "client closed the connection")
default:
return errors.Wrap(err, "client")
}
res, err := serverReader.ReadString('\n')
switch err {
case nil:
return NewResponse(res, incoming)
case io.EOF:
return errors.Wrap(err, "server closed the connection")
default:
return errors.Wrap(err, "server")
}
}
}
package tcp
import (
"net"
)
func NewConnection(address string) (net.Conn, error) {
return net.Dial("tcp", address)
}
package user
import (
"encoding/json"
"log"
"net/http"
"github.com/inanzzz/client/internal/pkg/storage"
)
type CreateRequestBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
type CreateResponseBody struct {
Code int `json:"code"`
Data struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
} `json:"data"`
}
type Create struct {
storage storage.Storage
}
func NewCreate(str storage.Storage) Create {
return Create{
storage: str,
}
}
// POST /api/v1/users
func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
var (
req CreateRequestBody
res CreateResponseBody
)
// 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
}
// Store request model and map response model
if err := c.storage.Store(req, &res); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("unable to store request: %v", err)
return
}
// Check if the response code is an expected value
if res.Code != http.StatusOK {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("unexpected storage response: %d", res.Code)
return
}
// Convert response model to HTTP response
data, err := json.Marshal(res.Data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("unable to marshal response: %v", err)
return
}
// Respond
w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(data)
}
It is a single file for now but you can implement your own structure.
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
func main() {
listener, err := net.Listen("tcp", "0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer listener.Close()
for {
con, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleClientRequest(con)
}
}
func handleClientRequest(con net.Conn) {
defer con.Close()
clientReader := bufio.NewReader(con)
for {
clientRequest, err := clientReader.ReadString('\n')
switch err {
case nil:
clientRequest := strings.TrimSpace(clientRequest)
if clientRequest == ":QUIT" {
log.Println("client requested server to close the connection so closing")
return
}
case io.EOF:
log.Println("client closed the connection by terminating the process")
return
default:
log.Printf("error: %v\n", err)
return
}
if _, err = con.Write(createResponse()); err != nil {
log.Printf("failed to respond to client: %v\n", err)
}
}
}
func createResponse() []byte {
return []byte(
fmt.Sprintf(`{"code":%d,"data":{"id":"%s","created_at":"%s"}}`+"\n",
http.StatusOK,
uuid.New().String(),
time.Now().UTC().Format(time.RFC3339),
),
)
}
Assuming that your server and client are running.
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: Wed, 03 Jun 2020 22:01:29 GMT
Content-Length: 81
{"id":"3860a39e-01d5-4c1e-bdb8-a94c8de1a6e5","created_at":"2020-06-03T22:01:29Z"}
Over a single TCP connection, 1000 requests is served within 20 seconds which equals to 1 request per 20 milliseconds.