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.


  1. User sends HTTP request to our public RESTful HTTP API.

  2. RESTful HTTP API forwards request to internal TCP server with a TCP client.

  3. TCP server stores the data.

  4. TCP server returns response to TCP client over TCP network.

  5. RESTful HTTP API responds to user.

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

Improvements


You can do the following in terms of possible improvements.



Client structure


├── cmd
│   └── client
│   └── main.go
└── internal
├── pkg
│   ├── http
│   │   ├── router.go
│   │   └── server.go
│   ├── storage
│   │   ├── request.go
│   │   ├── response.go
│   │   └── storage.go
│   └── tcp
│   └── connection.go
└── user
└── create.go

Client files


main.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())
}

router.go


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)
}

server.go


package http

import (
"net/http"
)

func NewServer(adr string, rtr Router) http.Server {
return http.Server{
Addr: adr,
Handler: rtr.handler,
}
}

request.go


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
}

response.go


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,
)
}

storage.go


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")
}
}
}

connection.go


package tcp

import (
"net"
)

func NewConnection(address string) (net.Conn, error) {
return net.Dial("tcp", address)
}

create.go


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)
}

Server


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),
),
)
}

Test


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.