Les systèmes distribués modernes exigent une application stricte des contrats. Bien que REST ait sa place, JSON-RPC offre une alternative légère et efficace pour la communication inter-services, notamment dans les architectures microservices. Cependant, les appels RPC bruts peuvent être dangereux sans validation. Cet article explore comment construire un serveur JSON-RPC robuste et haute performance en Go, en intégrant une validation stricte du JSON Schema pour garantir l'intégrité et la sécurité des données.
Pourquoi JSON-RPC et la validation de schéma ?
JSON-RPC est un protocole d'appel de procédure à distance (RPC) sans état et léger, similaire à JSON-RPC 1.0 mais sans les objets imbriqués. Il utilise la méthode POST, permettant au client de transmettre les paramètres encodés en JSON dans le corps de la requête. L'avantage principal est sa simplicité et son efficacité. Cependant, l'inconvénient de la philosophie « tout est permis » dans les corps de requête est le risque de données mal formées provoquant des plantages à l'exécution ou des erreurs de logique.
L'intégration de la validation JSON Schema agit comme une garde-fou. En validant le payload JSON entrant par rapport à un schéma prédéfini avant le traitement, nous nous assurons que :
- Les champs requis sont présents.
- Les types de données correspondent aux attentes.
- Les contraintes (telles que la longueur de la chaîne ou les plages de nombres) sont appliquées.
Sélection des bonnes bibliothèques
Pour cette implémentation, nous utiliserons gorilla/rpc pour le framework RPC, car il est stable et largement utilisé. Pour la validation, go-playground/validator est la norme de l'industrie. Cependant, pour une conformité stricte au JSON Schema (RFC 7159), nous utiliserons github.com/xeipuuv/gojsonschema ou une alternative moderne comme github.com/santhosh-tekuri/jsonschema. Pour des performances élevées, nous tirerons parti du package jsonschema qui compile les schémas pour une validation rapide.
Implémentation du serveur
Construisons un serveur qui accepte une requête de création d'utilisateur. Nous définirons un JSON Schema qui dicte la structure des données utilisateur.
Assurez-vous d'abord d'avoir les dépendances nécessaires :
go get github.com/gorilla/rpc/v2
go get github.com/gorilla/rpc/v2/json2
go get github.com/santhosh-tekuri/jsonschema/v3
Définissons maintenant le handler. La clé de la haute performance ici est de compiler le schéma JSON une seule fois au démarrage, plutôt que de le compiler à chaque requête.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/rpc/v2"
"github.com/gorilla/rpc/v2/json2"
"github.com/santhosh-tekuri/jsonschema/v3"
)
// UserRequest représente les paramètres JSON-RPC entrants
type UserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Schéma JSON pour la validation
const userSchema = `{
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 0, "maximum": 120}
},
"required": ["name", "email", "age"],
"additionalProperties": false
}`
func main() {
// Compiler le schéma une fois pour la performance
compiler := jsonschema.NewCompiler()
if err := compiler.AddResource("schema.json", strings.NewReader(userSchema)); err != nil {
log.Fatal(err)
}
schema, err := compiler.Compile("schema.json")
if err != nil {
log.Fatal(err)
}
// Créer le serveur RPC
server := rpc.NewServer()
server.RegisterCodec(json2.NewCodec(), "application/json")
server.RegisterService(new(UserService), "")
// Définir le service
type UserService struct{}
// Gérer la méthode
userService := &UserService{}
// Wrapper de handler personnalisé pour la validation
http.Handle("/rpc", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var params json.RawMessage
// Dans un scénario réel, analyser l'enveloppe JSON-RPC pour extraire les paramètres
// Ceci est un exemple simplifié se concentrant sur la logique de validation
// Supposons que nous ayons le payload JSON brut dans le corps
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Valider par rapport au schéma
var loadedSchema jsonschema.Loader
loadedSchema = jsonschema.NewGoLoader(map[string]interface{}{}) // Chargement simplifié
// Note : En production, utilisez gojsonschema pour un mappage plus facile des structs Go ou une validation JSON stricte
// Ici, nous démontrons le concept de l'application du schéma
service := new(UserService)
server.ServeHTTP(w, r)
}))
log.Println("Serveur écoutant sur :8080")
http.ListenAndServe(":8080", nil)
}
Meilleures pratiques pour la performance
Pour maximiser le débit, envisagez les optimisations suivantes :
- Compiler les schémas une seule fois : La compilation des schémas JSON peut être intensive en CPU. Compilez toujours les schémas lors de l'initialisation du serveur.
- Utiliser json.RawMessage : Lors de l'analyse des requêtes RPC, utilisez
json.RawMessagepour éviter le marshaling/unmarshaling inutile avant la validation. - Pools de connexions : Si vos clients RPC sont des services internes, utilisez le pool de connexions HTTP/2 pour réduire la surcharge de la poignée de main.
- Pools de tampons : Pour les systèmes à très haut débit, envisagez d'utiliser
sync.Poolpour les tampons de requête/réponse afin de réduire la pression sur le Garbage Collector (GC).
Conclusion
Construire un serveur JSON-RPC en Go est simple, mais l'ajout de la validation JSON Schema l'élève du statut de projet d'exemple à celui de service de qualité production. En validant les entrées à la frontière, vous protégez votre logique métier contre les états invalides et réduisez la charge cognitive de vos handlers. Avec une attention particulière à la compilation des schémas et à la gestion de la mémoire, vous pouvez atteindre à la fois la rigueur et des performances élevées.