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 HTTP istemcisi kullanarak iletişim kuran RESTful HTTP API'sini yapacağız. İş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 HTTP istemcisi ile dahili TCP sunucusuna iletir.

  3. TCP sunucusu verileri depolar.

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

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

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

İyileştirmeler


Olası bir iyileştirme olarak istemcinin HTTP isteklerine zaman aşımı ekleyebilirsiniz.


İstemci yapısı


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

İstemci dosyaları


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

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

Sunucu yapısı


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

Dosyalar


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

listener.go


package tcp

import (
"net"
)

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

create.go


Aslında aşağıda net.Listener'e ihtiyacınız yok.


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

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: 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 saniye içinde 1000 HTTP istek sunulur ve bu da 20/22 milisaniyede 1 isteğe eşittir.