Herkese merhaba!

Uzun yıllardır bol miktarda kişisel zaman ve enerji harcayarak bilgimizi hepinizle paylaşıyoruz. Ancak şu andan itibaren bu blogu çalışır durumda tutabilmek için yardımınıza ihtiyacımız var. Yapmanız gereken tek şey, sitedeki reklamlardan birine tıklamak olacaktır, aksi takdirde hosting vb. masraflar nedeniyle maalesef yayından kaldırılacaktır. Teşekkürler.

Bu örnekte, veri depolamak için dahili TCP sunucumuzla bir TCP istemcisi kullanarak iletişim kuran RESTful HTTP API'sini yapacağız. TCP istemci kodu HTTP API'mızdadır. İşlem aşağıda gösterildiği gibi çok basittir.


  1. Kullanıcı, genel RESTful HTTP API'mıza HTTP isteği gönderir.

  2. RESTful HTTP API, isteği bir TCP istemcisi ile dahili TCP sunucusuna iletir.

  3. TCP sunucusu verileri depolar.

  4. TCP sunucusu TCP istemcisine TCP ağı üzerinden yanıt verir.

  5. RESTful HTTP API kullanıcıya yanıt verir.

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

İyileştirmeler


Olası iyileştirmeler açısından aşağıdakileri yapabilirsiniz.



İstemci yapısı


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

İstemci dosyaları


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

Sunucu


Şimdilik tek bir dosya ama kendi yapınızı uygulayabilirsiniz.


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


Sunucunuzun ve istemcinizin çalıştığını varsayıyorum.


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

Tek bir TCP bağlantısı üzerinden, 20 saniye içinde 1000 istek sunulur ve bu da 20 milisaniyede 1 isteğe eşittir.