Clients
Le serveur expose HTTP sur le port 8000 et gRPC sur le port 50051 depuis un seul binaire. HTTP est le choix par défaut, ce dont 95 % des utilisateurs ont besoin — les recettes ci-dessous couvrent les connexions persistantes et la parallélisation. gRPC se trouve dans la section Avancé en bas de page.
Tous les endpoints, paramètres de requête et schémas de réponse sont dans la Référence API. Cette page complète ce que la référence ne peut pas couvrir : configuration keep-alive, parallélisation et génération de code protobuf.
HTTP
Utilisez un objet client à longue durée de vie. Une boucle de requêtes neuves paie le handshake TCP à chaque appel et peut saturer le serveur. Toutes les bibliothèques HTTP standard réutilisent les connexions par défaut, à condition de réutiliser le client.
Python
Construisez une requests.Session au démarrage et partagez-la. La session maintient un pool de connexions TCP et le réutilise d’un appel à l’autre, donc une boucle serrée ne paie le handshake qu’une seule fois.
pip install "requests>=2.32"import requests
SESSION = requests.Session()SESSION.headers.update({"Connection": "keep-alive"})BASE_URL = "http://localhost:8000"
def ocr_raw(path: str, layout: bool = False) -> dict: with open(path, "rb") as f: data = f.read() r = SESSION.post( f"{BASE_URL}/ocr/raw", data=data, headers={"Content-Type": "image/png"}, params={"layout": 1} if layout else None, timeout=30, ) r.raise_for_status() return r.json()
def ocr_pdf(path: str, mode: str = "ocr", dpi: int = 100) -> dict: with open(path, "rb") as f: data = f.read() r = SESSION.post( f"{BASE_URL}/ocr/pdf", data=data, params={"mode": mode, "dpi": dpi}, timeout=120, ) r.raise_for_status() return r.json()
print(ocr_raw("invoice.png"))Pour la parallélisation, faites passer la même SESSION dans un ThreadPoolExecutor. Pour le motif POST simple ci-dessous, la session est thread-safe, et le nombre de workers détermine combien de requêtes le serveur voit simultanément.
from concurrent.futures import ThreadPoolExecutor
def ocr_many(paths: list[str], workers: int = 8) -> list[dict]: with ThreadPoolExecutor(max_workers=workers) as pool: return list(pool.map(ocr_raw, paths))
results = ocr_many(["a.png", "b.png", "c.png", "d.png"])Java
Un seul HttpClient par processus. Il maintient en interne un pool de connexions, est thread-safe, et négocie HTTP/1.1 keep-alive de manière transparente. Appeler HttpClient.newHttpClient() à chaque requête jette le pool et ouvre un nouveau socket. Cible Java 21+ ; l’exemple de parallélisation utilise les virtual threads.
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.file.Files;import java.nio.file.Path;import java.time.Duration;
public final class TurboOcr { public static final HttpClient HTTP = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build();
public static String ocrRaw(Path image) throws Exception { var req = HttpRequest.newBuilder(URI.create("http://localhost:8000/ocr/raw")) .header("Content-Type", "image/png") .timeout(Duration.ofSeconds(30)) .POST(HttpRequest.BodyPublishers.ofByteArray(Files.readAllBytes(image))) .build(); return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body(); }
public static void main(String[] args) throws Exception { System.out.println(ocrRaw(Path.of("invoice.png"))); }}Pour la parallélisation, soumettez une tâche par image à un executor de virtual threads. Le HttpClient partagé rend les appels bloquants concurrents bon marché ; des milliers de requêtes en vol ne consomment aucun thread plate-forme supplémentaire.
import java.nio.file.Path;import java.util.List;import java.util.concurrent.Executors;import java.util.concurrent.Future;
public class FanOut { public static void main(String[] args) throws Exception { var paths = List.of(Path.of("a.png"), Path.of("b.png"), Path.of("c.png")); try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<String>> futures = paths.stream() .map(p -> pool.submit(() -> TurboOcr.ocrRaw(p))) .toList(); for (var f : futures) System.out.println(f.get()); } }}C++
Conservez un handle easy CURL* par thread worker pendant toute la durée de vie du processus. Tant que le handle est vivant, libcurl réutilise la connexion TCP sous-jacente. CURLOPT_TCP_KEEPALIVE empêche les passerelles NAT de fermer silencieusement les sockets inactifs. Compiler avec -std=c++20.
apt install libcurl4-openssl-dev// Compile: g++ -std=c++20 client.cc -lcurl#include <curl/curl.h>#include <fstream>#include <sstream>#include <stdexcept>#include <string>
static size_t write_cb(char* ptr, size_t size, size_t nmemb, void* ud) { static_cast<std::string*>(ud)->append(ptr, size * nmemb); return size * nmemb;}
class Client {public: Client() : curl_(curl_easy_init()) { if (!curl_) throw std::runtime_error("curl_easy_init failed"); curl_easy_setopt(curl_, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(curl_, CURLOPT_TCP_KEEPIDLE, 30L); curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, write_cb); } ~Client() { if (curl_) curl_easy_cleanup(curl_); } Client(const Client&) = delete; Client& operator=(const Client&) = delete;
std::string ocr_raw(const std::string& path) { std::ifstream f(path, std::ios::binary); std::ostringstream ss; ss << f.rdbuf(); std::string body = ss.str();
std::string response; curl_slist* hdrs = curl_slist_append(nullptr, "Content-Type: image/png");
curl_easy_setopt(curl_, CURLOPT_URL, "http://localhost:8000/ocr/raw"); curl_easy_setopt(curl_, CURLOPT_POST, 1L); curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, body.data()); curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, (long)body.size()); curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 30L);
CURLcode rc = curl_easy_perform(curl_); curl_slist_free_all(hdrs); if (rc != CURLE_OK) throw std::runtime_error(curl_easy_strerror(rc)); return response; }
private: CURL* curl_ = nullptr;};
int main() { Client c; std::printf("%s\n", c.ocr_raw("invoice.png").c_str());}Pour la parallélisation, un handle par thread, piloté par std::async. Pour une boucle d’événements mono-thread, curl_multi_* pilote au contraire plusieurs transferts depuis un seul thread.
#include <future>#include <string>#include <vector>
std::vector<std::string> ocr_many(const std::vector<std::string>& paths) { std::vector<std::future<std::string>> futures; for (const auto& p : paths) { futures.push_back(std::async(std::launch::async, [p] { thread_local Client c; // one handle per worker thread return c.ocr_raw(p); })); } std::vector<std::string> out; out.reserve(futures.size()); for (auto& f : futures) out.push_back(f.get()); return out;}Avancé — gRPC
Choisissez gRPC si vous avez besoin de streaming, d’un format filaire plus compact à très haut QPS, ou si vos services sont déjà standardisés sur protobuf. Pour la plupart des charges de travail, HTTP est plus simple et tout aussi rapide. La définition du service vit dans ocr.proto — téléchargez-le et lancez le générateur de code de votre langage.
Python (gRPC)
pip install "grpcio>=1.68" "grpcio-tools>=1.68" "protobuf>=5.28"python -m grpc_tools.protoc -I proto \ --python_out=. --grpc_python_out=. \ proto/ocr.protoCela génère ocr_pb2.py et ocr_pb2_grpc.py. Réutilisez un seul channel par processus ; il met en pool les flux HTTP/2 en interne.
import grpcfrom concurrent.futures import ThreadPoolExecutorimport ocr_pb2import ocr_pb2_grpc
CHANNEL = grpc.insecure_channel( "localhost:50051", options=[ ("grpc.keepalive_time_ms", 30_000), ("grpc.max_receive_message_length", 50 * 1024 * 1024), ],)STUB = ocr_pb2_grpc.OCRServiceStub(CHANNEL)
def recognize(path: str) -> ocr_pb2.OCRResponse: with open(path, "rb") as f: return STUB.Recognize( ocr_pb2.OCRRequest(image=f.read(), layout=False), timeout=30, )
def recognize_many(paths: list[str], workers: int = 8): with ThreadPoolExecutor(max_workers=workers) as pool: return list(pool.map(recognize, paths))
results = recognize_many(["a.png", "b.png", "c.png"])Java (gRPC)
Placez ocr.proto sous src/main/proto/ ; le plugin Gradle protobuf gère la génération de code à chaque build.
# build.gradleplugins { id 'com.google.protobuf' version '0.9.4' }
dependencies { implementation 'io.grpc:grpc-netty-shaded:1.70.0' implementation 'io.grpc:grpc-protobuf:1.70.0' implementation 'io.grpc:grpc-stub:1.70.0' implementation 'com.google.protobuf:protobuf-java:4.28.3'}
protobuf { protoc { artifact = 'com.google.protobuf:protoc:4.28.3' } plugins { grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.70.0' } } generateProtoTasks { all().each { task -> task.plugins { grpc {} } } }}Un seul ManagedChannel par processus, multiplexé entre stubs et threads.
import com.google.protobuf.ByteString;import io.grpc.ManagedChannel;import io.grpc.ManagedChannelBuilder;import ocr.Ocr.OCRRequest;import ocr.Ocr.OCRResponse;import ocr.OCRServiceGrpc;
import java.nio.file.Files;import java.nio.file.Path;import java.util.List;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;
public class GrpcFanOut { public static void main(String[] args) throws Exception { ManagedChannel channel = ManagedChannelBuilder .forAddress("localhost", 50051) .usePlaintext() .keepAliveTime(30, TimeUnit.SECONDS) .maxInboundMessageSize(50 * 1024 * 1024) .build(); var stub = OCRServiceGrpc.newBlockingStub(channel);
var paths = List.of(Path.of("a.png"), Path.of("b.png"), Path.of("c.png")); try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<OCRResponse>> futures = paths.stream() .map(p -> pool.submit(() -> { var req = OCRRequest.newBuilder() .setImage(ByteString.copyFrom(Files.readAllBytes(p))) .setLayout(false) .build(); return stub.recognize(req); })) .toList(); for (var f : futures) System.out.println(f.get().getNumDetections()); } channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); }}C++ (gRPC)
apt install protobuf-compiler-grpc libgrpc++-devprotoc -I proto \ --cpp_out=. --grpc_out=. \ --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \ proto/ocr.protoCompiler avec g++ -std=c++20 ... -lgrpc++ -lprotobuf. Réutilisez un seul std::shared_ptr<grpc::Channel> par processus.
// Compile: g++ -std=c++20 grpc_client.cc ocr.pb.cc ocr.grpc.pb.cc -lgrpc++ -lprotobuf -lpthread#include <grpcpp/grpcpp.h>#include "ocr.grpc.pb.h"#include <chrono>#include <fstream>#include <future>#include <sstream>#include <vector>
static std::string slurp(const std::string& p) { std::ifstream f(p, std::ios::binary); std::ostringstream ss; ss << f.rdbuf(); return ss.str();}
int main() { grpc::ChannelArguments args; args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30'000); args.SetInt(GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH, 50 * 1024 * 1024); auto channel = grpc::CreateCustomChannel( "localhost:50051", grpc::InsecureChannelCredentials(), args); auto stub = ocr::OCRService::NewStub(channel);
auto recognize = [&](const std::string& path) { ocr::OCRRequest req; req.set_image(slurp(path)); ocr::OCRResponse resp; grpc::ClientContext ctx; ctx.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30)); stub->Recognize(&ctx, req, &resp); return resp.num_detections(); };
std::vector<std::string> paths = {"a.png", "b.png", "c.png"}; std::vector<std::future<int>> futures; for (const auto& p : paths) { futures.push_back(std::async(std::launch::async, recognize, p)); } for (auto& f : futures) std::printf("%d\n", f.get());}Pour les détails de requête et de réponse par endpoint, voir la Référence API.