Clients
Der Server stellt HTTP auf Port 8000 und gRPC auf Port 50051 aus einem einzigen Binary bereit. HTTP ist der Standard und das, was 95 % der Nutzer brauchen – die folgenden Rezepte decken Persistent Connections und Fan-out ab. gRPC liegt im Abschnitt Fortgeschritten ganz unten.
Jeden Endpoint, jeden Query-Parameter und jedes Response-Schema finden Sie in der API-Referenz. Diese Seite ergänzt das, was die Referenz nicht abdeckt: Keep-Alive-Setup, paralleles Fan-out und Protobuf-Codegen.
HTTP
Verwenden Sie ein langlebiges Client-Objekt. Eine Schleife frischer Requests bezahlt jedes Mal den TCP-Handshake und kann den Server überlasten. Alle Standard-HTTP-Bibliotheken halten Verbindungen offen, sofern Sie denselben Client wiederverwenden.
Python
Bauen Sie eine requests.Session beim Start auf und teilen Sie sie. Die Session hält einen TCP-Verbindungspool und nutzt ihn über alle Aufrufe hinweg, sodass eine enge Schleife den Handshake genau einmal zahlt.
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"))Für Fan-out reichen Sie dieselbe SESSION durch einen ThreadPoolExecutor. Für das einfache POST-Muster unten ist die Session thread-safe, und die Worker-Anzahl bestimmt, wie viele Requests der Server gleichzeitig sieht.
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
Ein HttpClient pro Prozess. Er hält intern einen Verbindungspool, ist thread-safe und handelt HTTP/1.1-Keep-Alive transparent aus. Wenn Sie pro Request HttpClient.newHttpClient() aufrufen, werfen Sie den Pool weg und öffnen einen neuen Socket. Zielversion: Java 21+; das Fan-out-Beispiel nutzt 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"))); }}Für Fan-out reichen Sie pro Bild eine Aufgabe an einen Virtual-Thread-Executor. Der gemeinsame HttpClient macht parallele blockierende Aufrufe billig; Tausende laufende Requests kosten keine zusätzlichen Plattform-Threads.
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++
Halten Sie pro Worker-Thread einen CURL*-Easy-Handle für die gesamte Prozesslaufzeit. Solange der Handle lebt, hält libcurl die zugrunde liegende TCP-Verbindung offen. CURLOPT_TCP_KEEPALIVE verhindert, dass NAT-Gateways inaktive Sockets stillschweigend abräumen. Kompilieren mit -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());}Für Fan-out: pro Thread ein Handle, gesteuert über std::async. In einer Single-Thread-Eventschleife treibt stattdessen curl_multi_* viele Übertragungen auf einem 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;}Fortgeschritten – gRPC
Greifen Sie zu gRPC, wenn Sie Streaming brauchen, bei sehr hohem QPS einen kleineren Wire-Footprint wollen oder ohnehin auf Protobuf in Ihren Services standardisiert sind. Für die meisten Workloads ist HTTP einfacher und genauso schnell. Die Service-Definition liegt in ocr.proto – herunterladen und den Codegen für Ihre Sprache laufen lassen.
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.protoDas erzeugt ocr_pb2.py und ocr_pb2_grpc.py. Verwenden Sie pro Prozess einen einzigen Channel; er pooled HTTP/2-Streams intern.
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)
Legen Sie ocr.proto unter src/main/proto/ ab; das Protobuf-Gradle-Plugin erledigt die Codegenerierung bei jedem 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 {} } } }}Ein ManagedChannel pro Prozess, geteilt über Stubs und 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.protoBauen mit g++ -std=c++20 ... -lgrpc++ -lprotobuf. Verwenden Sie pro Prozess einen einzigen std::shared_ptr<grpc::Channel> wieder.
// 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());}Details zu Request und Response je Endpoint finden Sie in der API-Referenz.