Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP6 min
Modelo Cliente-Servidor
- 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.
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:
- 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.
- Qualquer estado compartilhado entre handlers precisa de sincronização. Um
mapglobal 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.
ServeMuxda stdlib (1.22+) compra zero dependências e cobra funcionalidades de roteadores maiores (grupos, middleware encadeado por rota).contextpor 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
- 2. Ciclo de uma Requisição — o que acontece entre iniciar e responder
- 7. Sessão vs JWT — onde colocar o estado que o protocolo não guarda
- 6. Hash Map — semana 2, por que map global em handler é corrida garantida
- 6. Transações e ACID — semana 9,
contextcancelado aborta a transação
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação