trilha

Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP6 min

Modelo Cliente-Servidor

Perguntas-guia
  • O que significa dizer que HTTP é sem estado, se aplicações têm sessão?
  • Onde mora o estado então — cliente, servidor, banco, token?
  • O que muda quando o cliente é browser, mobile ou outro serviço?
  • Por que "o servidor é uma máquina" é um modelo mental que quebra rápido?

Conceito

Cliente-servidor é uma arquitetura de papéis assimétricos: o cliente inicia, o servidor escuta e responde. O servidor nunca fala primeiro.

Isso contrasta com peer-to-peer, onde qualquer nó inicia. A assimetria é o que permite firewall, NAT e o modelo de segurança da web — e é também por que push do servidor exige gambiarra (polling, long-polling, SSE, WebSocket): o protocolo não previa isso.

"HTTP é sem estado" significa uma coisa específica

Significa que cada requisição carrega tudo o que é necessário para ser processada, e o servidor não guarda memória daquele cliente entre requisições.

É uma propriedade do protocolo, não da aplicação. Aplicações têm estado — o que muda é onde ele mora.

Onde o estado pode morar, e cada escolha tem um custo diferente:

Onde Como Custo
no cliente cookie, token (7. Sessão vs JWT) vai e volta em toda requisição; cliente pode manipular
memória do servidor sessão em RAM quebra com mais de uma instância
store compartilhado Redis, banco uma consulta por requisição
na URL query string, path vaza em log, histórico, Referer

Por que a ausência de estado é o que permite escalar horizontalmente: se qualquer servidor pode atender qualquer requisição, você adiciona máquinas e um balanceador. No momento em que o servidor guarda sessão em memória, você precisa de sticky sessions — e aí perder uma instância significa derrubar as sessões dela.

"O servidor é uma máquina" quebra rápido. Na prática há balanceador, N instâncias, contêineres reagendados a qualquer momento, e deploy rolando. A consequência mais concreta: qualquer estado em memória local é perdido no próximo deploy — e deploy acontece toda semana.

O tipo de cliente muda o desenho:

Cliente Consequências
navegador cookies automáticos, CORS, same-origin, CSRF
mobile sem cookie jar convencional, token de vida longa, atualização lenta do app
outro serviço sem usuário, mTLS ou credencial de máquina, retry agressivo

Em Go

O modelo de concorrência do net/http é o fato mais importante: o servidor aceita a conexão e serve cada requisição numa goroutine própria.

Duas consequências diretas:

  1. Você escreve código sequencial — o handler parece single-threaded, e o paralelismo é do servidor. É por isso que servidor HTTP em Go é tão simples comparado a modelos de callback ou event loop.
  2. Qualquer estado compartilhado entre handlers precisa de sincronização. Um map global acessado por handlers é escrita concorrente garantida — fatal error: concurrent map writes (6. Hash Map). Mutex, sync.Map, ou canal.

A interface http.Handler tem um método, e é a abstração central da linguagem para HTTP (2. Abstração):

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

Middleware é apenas um Handler que envolve outro. Não há framework necessário.

http.ServeMux com padrões de método (Go 1.22+) tornou roteador externo dispensável na maioria dos casos:

mux.HandleFunc("GET /pedidos/{id}", buscar)
mux.HandleFunc("POST /pedidos", criar)
id := r.PathValue("id")

O context por requisição é como Go expressa "o cliente foi embora": ele é cancelado quando a conexão cai. Passar r.Context() para as consultas ao banco significa que uma query de 30 segundos é abortada quando o usuário fecha a aba — economia real (6. Transações e ACID).

Respostas às perguntas-guia

1. O que significa dizer que HTTP é sem estado, se aplicações têm sessão?

Que o protocolo não guarda contexto entre requisições — cada uma é autocontida. A aplicação tem estado; ele mora em outro lugar (cookie, store, token).

Em Go: o *http.Request é criado e descartado por requisição. Nada em net/http persiste entre elas, o que torna a ausência de estado o default e a presença dela uma decisão explícita.

2. Onde mora o estado então — cliente, servidor, banco, token?

Nas quatro possibilidades da tabela acima, e a escolha define escalabilidade e revogação.

Em Go: estado em memória de processo (map global) é o caminho mais curto e o que quebra primeiro — na segunda instância. Se precisar mesmo, sync.Map ou mutex, e assuma que é cache descartável, não fonte da verdade.

3. O que muda quando o cliente é browser, mobile ou outro serviço?

Navegador traz CORS, cookies automáticos e CSRF. Mobile traz token longo e app que não atualiza. Serviço traz ausência de usuário e retry agressivo.

Em Go: para navegador você configura SameSite e CORS; para serviço, tls.Config com ClientCAs (mTLS) e http.Client com timeout e retry controlados.

4. Por que "o servidor é uma máquina" é um modelo mental que quebra rápido?

Porque há N instâncias atrás de um balanceador, reagendadas e reiniciadas continuamente. Todo estado local é temporário por definição.

Em Go, o sintoma clássico: sessão em map funciona em dev (uma instância) e desloga usuários aleatoriamente em produção (duas instâncias, sem sticky).

Trade-offs

Do conceito:

  • Sem estado compra escala horizontal e cobra que o estado viaje (token) ou seja consultado (store).
  • Estado em memória compra latência zero e cobra a impossibilidade de escalar.
  • Cliente-servidor compra simplicidade e cobra que o servidor não possa iniciar comunicação.

Em Go:

  • Goroutine por requisição compra código sequencial legível e cobra atenção com estado compartilhado.
  • ServeMux da stdlib (1.22+) compra zero dependências e cobra funcionalidades de roteadores maiores (grupos, middleware encadeado por rota).
  • context por requisição compra cancelamento propagado e cobra passá-lo por toda a cadeia.

Exemplo prático

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
)

// ERRADO: map global sem sincronização, acessado por handlers concorrentes
var contadorErrado = map[string]int{}

// CERTO: estado compartilhado protegido
type contador struct {
	mu sync.Mutex
	n  map[string]int
}

func (c *contador) inc(k string) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.n == nil {
		c.n = map[string]int{}
	}
	c.n[k]++
	return c.n[k]
}

func main() {
	c := &contador{}

	// ServeMux com padrão de método e wildcard (Go 1.22+)
	mux := http.NewServeMux()
	mux.HandleFunc("GET /pedidos/{id}", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		n := c.inc(id)
		fmt.Fprintf(w, "pedido %s, acesso nº %d", id, n)
	})
	mux.HandleFunc("POST /pedidos", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusCreated)
		w.Header().Set("Location", "/pedidos/9") // tarde demais: ver Headers HTTP
		fmt.Fprint(w, "criado")
	})

	srv := httptest.NewServer(mux)
	defer srv.Close()

	// 1. roteamento por método e por path
	for _, c := range []struct{ metodo, path string }{
		{"GET", "/pedidos/42"},
		{"GET", "/pedidos/42"},
		{"POST", "/pedidos"},
		{"DELETE", "/pedidos/42"}, // método não registrado
		{"GET", "/inexistente"},
	} {
		req, _ := http.NewRequest(c.metodo, srv.URL+c.path, nil)
		resp, _ := http.DefaultClient.Do(req)
		corpo := make([]byte, 60)
		n, _ := resp.Body.Read(corpo)
		resp.Body.Close()
		fmt.Printf("%-7s %-14s -> %d %s\n", c.metodo, c.path, resp.StatusCode, corpo[:n])
	}

	// 2. goroutine por requisição: 100 requisições concorrentes no mesmo estado
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			r, err := http.Get(srv.URL + "/pedidos/concorrente")
			if err == nil {
				r.Body.Close()
			}
		}()
	}
	wg.Wait()
	fmt.Printf("\napós 100 requisições concorrentes, contador = %d (sem corrida, por causa do mutex)\n",
		c.n["concorrente"])
}

Rode com go test -race num teste equivalente e troque o contador pelo map global: o detector de corrida aponta imediatamente. É a demonstração mais rápida de por que goroutine-por-requisição exige disciplina com estado.

Relacionado


Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas