Comienza a usar gRPC-Rust: transmisión

1. Introducción

En este codelab, usarás gRPC-Rust para crear un cliente y un servidor que formen la base de una aplicación de asignación de rutas escrita en Rust.

Al final del instructivo, tendrás un cliente que se conectará a un servidor remoto con gRPC para obtener información sobre las funciones de la ruta de un cliente, crear un resumen de la ruta de un cliente e intercambiar información de la ruta, como actualizaciones de tráfico, con el servidor y otros clientes.

El servicio se define en un archivo de búferes de protocolo, que se usará para generar código estándar para el cliente y el servidor, de modo que puedan comunicarse entre sí, lo que te ahorrará tiempo y esfuerzo en la implementación de esa funcionalidad.

Este código generado se encarga no solo de las complejidades de la comunicación entre el servidor y el cliente, sino también de la serialización y deserialización de datos.

Qué aprenderás

  • Cómo usar búferes de protocolo para definir una API de servicio
  • Cómo compilar un cliente y un servidor basados en gRPC a partir de una definición de búferes de protocolo con la generación de código automatizada
  • Cómo comprender la comunicación de transmisión cliente-servidor con gRPC

Este codelab está dirigido a desarrolladores de Rust que no tienen experiencia con gRPC o que desean repasar gRPC, o a cualquier otra persona interesada en compilar sistemas distribuidos. No se requiere experiencia previa con gRPC.

2. Antes de comenzar

Requisitos previos

Asegúrate de haber instalado lo siguiente:

  • GCC. Sigue las instrucciones que aparecen aquí.
  • Git: Puedes encontrar las instrucciones de instalación aquí.
  • Rust, versión 1.88.0. Sigue las instrucciones de instalación que aparecen aquí.

Obtén el código

Para que no tengas que comenzar desde cero, este codelab proporciona un andamio del código fuente de la aplicación para que lo completes. En los siguientes pasos, se muestra cómo finalizar la aplicación, incluido el uso de los complementos del compilador de búferes de protocolo para generar el código estándar de gRPC.

Primero, crea el directorio de trabajo del codelab y cd en él:

mkdir streaming-grpc-rust-getting-started && cd streaming-grpc-rust-getting-started

Descarga y extrae el codelab:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/2026.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-2026/codelabs/grpc-rust-streaming/start_here

Como alternativa, puedes descargar el archivo .zip que contiene solo el directorio del codelab y descomprimirlo de forma manual.

El código fuente completo está disponible en GitHub si deseas omitir la escritura de una implementación.

3. Define mensajes y servicios

El primer paso es definir el servicio gRPC de la aplicación, sus métodos RPC y sus tipos de mensajes de solicitud y respuesta con búferes de protocolo. Tu servicio proporcionará lo siguiente:

  • Métodos RPC llamados ListFeatures, RecordRoute y RouteChat que el servidor implementa y el cliente llama
  • Los tipos de mensajes Point, Feature, Rectangle, RouteNote y RouteSummary, que son estructuras de datos intercambiadas entre el cliente y el servidor cuando se llaman los métodos anteriores

Estos métodos RPC y sus tipos de mensajes se definirán en el archivo proto/routeguide.proto del código fuente proporcionado.

Los búferes de protocolo se conocen comúnmente como protobufs. Para obtener más información sobre la terminología de gRPC, consulta Conceptos básicos, arquitectura y ciclo de vida de gRPC.

Define tipos de mensajes

Primero, definamos los mensajes que usarán nuestras RPCs. En el archivo proto/routeguide.proto del código fuente, primero define el tipo de mensaje Point. Un Point representa un par de coordenadas de latitud y longitud en un mapa. Para este codelab, usa números enteros para las coordenadas:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Los números 1 y 2 son números de ID únicos para cada uno de los campos de la estructura message.

A continuación, define el tipo de mensaje Feature. Un Feature usa un campo string para el nombre o la dirección postal de algo en una ubicación especificada por un Point:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Luego, un mensaje Rectangle que representa un rectángulo de latitud y longitud, representado como dos puntos diagonalmente opuestos "lo" y "hi".

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

También un mensaje RouteNote que representa un mensaje enviado en un punto determinado.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

También requeriremos un mensaje RouteSummary. Este mensaje se recibe en respuesta a una RPC RecordRoute, que se explica en la siguiente sección. Contiene la cantidad de puntos individuales recibidos, la cantidad de funciones detectadas y la distancia total recorrida como la suma acumulativa de la distancia entre cada punto.

message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

Define métodos de servicio

Primero, definamos nuestro servicio y, luego, definamos nuestros mensajes. Para definir un servicio, debes especificar un servicio con nombre en tu archivo .proto. El archivo proto/routeguide.proto tiene una estructura service llamada RouteGuide que define uno o más métodos proporcionados por el servicio de la aplicación.

Define métodos RPC dentro de la definición de tu servicio y especifica sus tipos de solicitud y respuesta. En esta sección del codelab, definiremos lo siguiente:

ListFeatures

Obtiene las Features disponibles dentro del Rectangle determinado. Los resultados se transmiten en lugar de mostrarse de inmediato (p.ej., en un mensaje de respuesta con un campo repetido), ya que el rectángulo puede abarcar un área grande y contener una gran cantidad de funciones.

Un tipo adecuado para esta RPC es una RPC de transmisión del servidor: el cliente envía una solicitud al servidor y obtiene una transmisión para leer una secuencia de mensajes. El cliente lee desde la transmisión que se muestra hasta que no haya más mensajes. Como puedes ver en nuestro ejemplo, debes especificar un método de transmisión del servidor colocando la palabra clave stream antes del tipo de respuesta.

rpc ListFeatures(Rectangle) returns (stream Feature) {}

RecordRoute

Acepta una transmisión de Points en una ruta que se está atravesando y muestra un RouteSummary cuando se completa el recorrido.

En este caso, parece adecuada una RPC de transmisión del cliente: el cliente escribe una secuencia de mensajes y los envía al servidor, de nuevo con una transmisión proporcionada. Una vez que el cliente termina de escribir los mensajes, espera a que el servidor los lea todos y muestre su respuesta. Para especificar un método de transmisión del cliente, coloca la palabra clave stream antes del tipo de solicitud.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

RouteChat

Acepta una transmisión de RouteNotes enviada mientras se atraviesa una ruta, mientras recibe otras RouteNotes (p.ej., de otros usuarios).

Este es exactamente el tipo de caso de uso para la transmisión bidireccional. Una RPC de transmisión bidireccional tiene ambos lados que envían una secuencia de mensajes con una transmisión de lectura y escritura. Las dos transmisiones operan de forma independiente, por lo que los clientes y los servidores pueden leer y escribir en el orden que deseen.

Por ejemplo, el servidor podría esperar a recibir todos los mensajes del cliente antes de escribir sus respuestas, o bien podría leer un mensaje y, luego, escribir un mensaje, o alguna otra combinación de lecturas y escrituras.

Se conserva el orden de los mensajes en cada transmisión. Para especificar este tipo de método, coloca la palabra clave stream antes de la solicitud y la respuesta.

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

4. Genera el código del cliente y del servidor

Ya te proporcionamos el código generado del archivo .proto en el directorio generated/, incluidas todas las adiciones que realizaste anteriormente. Sin embargo, nos gustaría tomar un momento para explicar cómo funciona la generación de código.

Nuestro archivo .proto describe todas las estructuras y funciones que usa un cliente o un servidor. Usamos una secuencia de comandos de compilación de Cargo (build.rs) junto con el paquete grpc-protobuf-build para generar este código de forma automática.

En Cargo.toml, ya agregamos grpc-protobuf-build como una dependencia de compilación.

En build.rs, configuramos grpc_protobuf_build::CodeGen para compilar proto/routeguide.proto en el directorio generated/. Las líneas clave son las siguientes:

grpc_protobuf_build::CodeGen::new()
    .include("proto")
    .input("routeguide.proto")
    .output_dir("generated")
    .compile()
    .unwrap();

Esto llama a la generación de código del paquete grpc_protobuf_build y le pasa el routeguide.proto. Lo incluimos en un código para que solo se ejecute cuando se pasa una marca de función, de modo que solo se vuelva a generar cuando lo desees. No tienes que ejecutarlo ahora, ya que generamos el código por ti.

cargo build --bin routeguide-server --features regenerate_proto

Cuando ejecutas cargo build, build.rs compila las definiciones de búferes de protocolo en el directorio generate/, incluidos los siguientes elementos:

  • Definiciones de estructuras para los tipos de mensajes Point y Feature
  • Un rasgo de servicio de Tonic que deberemos implementar para el servidor: route_guide_server::RouteGuide
  • Un tipo de cliente de gRPC-Rust que usaremos para llamar al servidor: route_guide_client::RouteGuideClient<T>

Puedes consultar la guía de protoc-gen-rust-grpc para obtener más información.

A continuación, implementaremos los métodos de servicio en el servidor.

5. Implementa el servicio

Primero, veamos cómo creamos un servidor RouteGuide. Hay dos partes para hacer que nuestro servicio RouteGuide haga su trabajo:

  • Implementar la interfaz de servicio generada a partir de nuestra definición de servicio: realizar el "trabajo" real de nuestro servicio
  • Ejecutar un servidor de gRPC para escuchar las solicitudes de los clientes y enviarlas a la implementación del método correcto

En src/server/server.rs, podemos incluir el código generado en el alcance a través de la macro include_generated_proto! de gRPC y, luego, importar el rasgo RouteGuide y Point.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

pub use grpc_pb::{
    route_guide_server::{RouteGuideServer, RouteGuide},
    Point, Feature, Rectangle, RouteNote, RouteSummary
};

Podemos comenzar definiendo una estructura para representar nuestro servicio. Por ahora, podemos hacerlo en src/server/server.rs:

#[derive(Debug)]
pub struct RouteGuideService {
    features: Vec<Feature>,
}

Ahora, debemos implementar el rasgo route_guide_server::RouteGuide de nuestro código generado.

Implementa RouteGuide

Debemos implementar la interfaz RouteGuide generada. Así se vería la implementación. Esto ya está en la plantilla.

#[tonic::async_trait]
impl RouteGuide for RouteGuideService {
    async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
        ...
    }

    async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
        ...
    }

    async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
        ...
    }
}

Analicemos cada implementación de RPC en detalle.

RPC de transmisión del servidor: ListFeatures

Comencemos con ListFeatures. Esta es una RPC de transmisión del servidor (el cliente enviará un mensaje y el servidor responderá con muchos), por lo que debemos enviar varias Features a nuestro cliente.

async fn list_features(
        &self,
        request: Request<Rectangle>,
    ) -> Result<Response<ListFeaturesStream>, Status> {
    println!("ListFeatures = {:?}", request);

    let (tx, rx) = mpsc::channel(4);
    let features = self.features.clone();

    tokio::spawn(async move {
        for feature in &features[..] {
            if in_range(&feature.location().to_owned(), request.get_ref()) {
                println!("  => send {feature:?}");
                tx.send(Ok(feature.clone())).await.unwrap();
            }
        }
        println!(" /// done sending");
    });

    let output_stream = ReceiverStream::new(rx);
    Ok(Response::new(Box::pin(output_stream)))
}

Como puedes ver, obtenemos un objeto de solicitud (el Rectangle en el que nuestro cliente quiere encontrar Features). Esta vez, debemos mostrar una transmisión de valores. Creamos un canal y generamos una nueva tarea asíncrona en la que realizamos una búsqueda y enviamos las funciones que satisfacen nuestras restricciones al canal. La mitad de la transmisión del canal se muestra a la persona que llama, envuelta en un tonic::Response.

RPC de transmisión del cliente: RecordRoute

Ahora, veamos algo un poco más complicado: el método de transmisión del cliente RecordRoute, en el que obtenemos una transmisión de Points del cliente y mostramos un solo RouteSummary con información sobre su viaje. Obtiene una transmisión como entrada, que el servidor puede usar para leer y escribir mensajes. Puede iterar a través de los mensajes del cliente con su método next() y mostrar su única respuesta.

async fn record_route(
        &self,
        request: Request<tonic::Streaming<Point>>,
    ) -> Result<Response<RouteSummary>, Status> {
    println!("RecordRoute");
    let mut stream = request.into_inner();
    let mut summary = RouteSummary::default();
    let mut last_point = None;
    let now = Instant::now();

    while let Some(point) = stream.next().await {
        let point = point?;
        println!("  ==> Point = {point:?}");

        // Increment the point count
        summary.set_point_count(summary.point_count() + 1);

        // Find features
        for feature in &self.features[..] {
            if feature.location().latitude() == point.latitude() {
                if feature.location().longitude() == point.longitude(){
                    summary.set_feature_count(summary.feature_count() + 1);
                }
            }
        }

        // Calculate the distance
        if let Some(ref last_point) = last_point {
            let new_dist = summary.distance() + calc_distance(last_point, &point);
            summary.set_distance(new_dist);
        }
        last_point = Some(point);
    }
    summary.set_elapsed_time(now.elapsed().as_secs() as i32);
    Ok(Response::new(summary))
}

En el cuerpo del método, usamos el método next() de la transmisión para leer repetidamente las solicitudes de nuestro cliente a un objeto de solicitud (en este caso, un Point) hasta que no haya más mensajes. Si es None, la transmisión sigue siendo buena y puede continuar leyendo.

RPC de transmisión bidireccional: RouteChat

Por último, veamos nuestra RPC de transmisión bidireccional RouteChat().

async fn route_chat(
        &self,
        request: Request<tonic::Streaming<RouteNote>>,
    ) -> Result<Response<RouteChatStream>, Status> {
    println!("RouteChat");

    let mut notes: HashMap<(i32, i32), Vec<RouteNote>> = HashMap::new();
    let mut stream = request.into_inner();

    let output = async_stream::try_stream! {
        while let Some(note) = stream.next().await {
            let note = note?;
            let location = note.location();
            let key = (location.latitude(), location.longitude());
            let location_notes = notes.entry(key).or_insert(vec![]);
            location_notes.push(note);
            for note in location_notes {
                yield note.clone();
            }
        }
    };
    Ok(Response::new(Box::pin(output)))
}

Esta vez, obtenemos una transmisión que, como en nuestro ejemplo de transmisión del cliente, se puede usar para leer y escribir mensajes. Sin embargo, esta vez mostramos valores a través de la transmisión de nuestro método mientras el cliente sigue escribiendo mensajes en su transmisión de mensajes. La sintaxis para leer y escribir aquí es muy similar a nuestro método de transmisión del cliente, excepto que el servidor muestra un RouteChatStream. Aunque cada lado siempre obtendrá los mensajes del otro en el orden en que se escribieron, tanto el cliente como el servidor pueden leer y escribir en cualquier orden: las transmisiones operan de forma completamente independiente.

Creamos la transmisión de salida con try_stream!, lo que indica que la transmisión podría mostrar errores.

Inicia el servidor

Una vez que implementamos este método, también debemos iniciar un servidor de gRPC para que los clientes puedan usar nuestro servicio. Completa main().

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:10000".parse().unwrap();
    println!("RouteGuideServer listening on: {addr}");
    let route_guide = RouteGuideService {
        features: load(),
    };
    let svc = RouteGuideServer::new(route_guide);
    Server::builder().add_service(svc).serve(addr).await?;
    Ok(())
}

Esto es lo que sucede en main(), paso a paso:

  1. Especifica el puerto que queremos usar para escuchar las solicitudes del cliente.
  2. Crea un RouteGuideService con las funciones cargadas.
  3. Crea una instancia del servidor de gRPC con RouteGuideServer::new() usando el servicio que creamos.
  4. Registra la implementación de nuestro servicio con el servidor de gRPC.
  5. Llama a serve() en el servidor con los detalles de nuestro puerto para realizar una espera de bloqueo hasta que se detenga el proceso.

6. Crea el cliente

En esta sección, veremos cómo crear un cliente de Rust para nuestro servicio RouteGuide en src/client/client.rs.

Primero, incluye el código generado en el alcance.

mod grpc_pb {
    grpc::include_generated_proto!("generated", "routeguide");
}

use grpc_pb::route_guide_client::RouteGuideClient;
use grpc_pb::{Point, Rectangle, RouteNote};

Llama a métodos de servicio

Ahora, veamos cómo llamamos a nuestros métodos de servicio. En gRPC-Rust, las RPCs de transmisión son asíncronas y no bloqueadoras, y usan la sintaxis async/await de Rust y las transmisiones de Tokio.

RPC de transmisión del servidor: PrintFeatures

En las RPCs de transmisión del servidor, el cliente envía un solo mensaje de solicitud al servidor y recibe una transmisión de mensajes de respuesta. Aquí es donde en client.rs llamamos al método de transmisión del servidor list_features() (que corresponde a la declaración de RPC ListFeatures que se encuentra en nuestro proto). A su vez, el servidor enviará una transmisión de mensajes Feature:

async fn print_features(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let rectangle = proto!(Rectangle {
        lo: proto!(Point {
            latitude: 400_000_000,
            longitude: -750_000_000,
        }),
        hi: proto!(Point {
            latitude: 420_000_000,
            longitude: -730_000_000,
        }),
    });

    let mut stream = client.list_features(rectangle).await;

    while let Some(feature) = stream.recv().await {
        println!(
            "FEATURE: Name = \"{}\", Lat = {}, Lon = {}",
            feature.name(),
            feature.location().latitude(),
            feature.location().longitude()
        );
    }
    let status = stream.status().await;
    assert!(status.is_ok(), "{:?}", status);
    Ok(())
}

RPC de transmisión del cliente: RecordRoute

Cuando usamos la transmisión del cliente, el cliente abrirá una transmisión al servidor y enviará una secuencia de mensajes. Recibirá un solo mensaje de respuesta cuando finalice la transmisión.

Aquí, iniciamos la llamada con client.record_route().await, enviamos varias coordenadas Point generadas una por una a través de la transmisión con stream.send(point).await y, luego, cerramos la transmisión con stream.close_and_recv().await para recibir el mensaje RouteSummary del servidor único.

async fn run_record_route(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let mut rng = rand::rng();
    let point_count: i32 = rng.random_range(2..100);

    let mut points = vec![];
    for _ in 0..=point_count {
        points.push(random_point(&mut rng));
    }

    println!("Traversing {} points", points.len());
    let mut stream = client.record_route().await;

    for point in &points {
        if stream.send(point).await.is_err() {
            break;
        }
    }

    match stream.close_and_recv().await {
        Ok(response) => {
            println!(
                "SUMMARY: Feature Count = {}, Distance = {}",
                response.feature_count(),
                response.distance()
            );
        }
        Err(e) => println!("something went wrong: {e:?}"),
    }
    Ok(())
}

RPC de transmisión bidireccional: RouteChat

Por último, veamos nuestra RPC de transmisión bidireccional RouteChat(). Aquí, tanto el cliente como el servidor pasarán una secuencia de mensajes de un lado a otro. Generamos una tarea tokio para enviar mensajes de forma continua al servidor con tx.send(note).await.is_err(). Mientras tanto, rx.recv().await escucha los mensajes de respuesta del servidor y los imprime a medida que llegan.

async fn run_route_chat(client: &RouteGuideClient<Channel>) -> Result<(), Box<dyn Error>> {
    let (mut tx, mut rx) = client.route_chat().await;

    let start = time::Instant::now();
    tokio::spawn(async move {
        let mut interval = time::interval(Duration::from_millis(50));
        for _ in 0..10 {
            let time = interval.tick().await;
            let elapsed = time.duration_since(start);
            let note = proto!(RouteNote {
                location: proto!(Point {
                    latitude: 409146138 + elapsed.as_millis() as i32,
                    longitude: -746188906,
                }),
                message: format!("at {elapsed:?}"),
            });
            if tx.send(note).await.is_err() {
                return;
            }
        }
        tx.close();
    });

    while let Some(note) = rx.recv().await {
        println!(
            "Note: Latitude = {}, Longitude = {}, Message = \"{}\"",
            note.location().latitude(),
            note.location().longitude(),
            note.message()
        );
    }
    let status = rx.status().await;
    assert!(status.is_ok(), "{:?}", status);
    Ok(())
}

Aunque cada lado siempre obtendrá los mensajes del otro en el orden en que se escribieron, tanto el cliente como el servidor pueden leer y escribir en cualquier orden: las transmisiones operan de forma completamente independiente.

Crea y pasa el cliente

Para llamar a los métodos de servicio, primero debemos crear un canal para comunicarnos con el servidor. Para ello, primero creamos un extremo, nos conectamos a ese extremo y pasamos el canal creado cuando se conecta a RouteGuideClient::new() de la siguiente manera:

// Create channel to connect to server
let channel = Channel::builder(
    "dns:///[::1]:10000",
    Arc::new(LocalChannelCredentials::new()),
)
.build();

// Create a new client
let client = RouteGuideClient::new(channel);

Con este cliente creado, podemos llamar a los métodos que escribimos anteriormente y pasar el cliente a ellos. Agregamos todo este código a main(), que usa el entorno de ejecución asíncrono de Tokio. Este es el código completo:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create channel to connect to server
    let channel = Channel::builder(
        "dns:///[::1]:10000",
        Arc::new(LocalChannelCredentials::new()),
    )
    .build();

    // Create a new client
    let client = RouteGuideClient::new(channel);

    println!("\n*** SERVER STREAMING ***");
    print_features(&client).await?;

    println!("\n*** CLIENT STREAMING ***");
    run_record_route(&client).await?;

    println!("\n*** BIDIRECTIONAL STREAMING ***");
    run_route_chat(&client).await?;

    Ok(())
}

7. Probar

Para ejecutar tu cliente y servidor, primero verifica que ambos destinos binarios estén definidos en Cargo.toml:

[[bin]]
name = "routeguide-server"
path = "src/server/server.rs"

[[bin]]
name = "routeguide-client"
path = "src/client/client.rs"

Luego, ejecuta los siguientes comandos desde nuestro directorio de trabajo:

  1. Ejecuta el servidor en una terminal:
cargo run --bin routeguide-server
  1. Ejecuta el cliente desde otra terminal:
cargo run --bin routeguide-client

Verás un resultado como este:

*** SERVER STREAMING ***
FEATURE: Name = "Patriots Path, Mendham, NJ 07945, USA", Lat = 407838351, Lon = -746143763
FEATURE: Name = "101 New Jersey 10, Whippany, NJ 07981, USA", Lat = 408122808, Lon = -743999179
FEATURE: Name = "U.S. 6, Shohola, PA 18458, USA", Lat = 413628156, Lon = -749015468
...
*** CLIENT STREAMING ***
Traversing 86 points
SUMMARY: Feature Count = 0, Distance = 803709356

*** BIDIRECTIONAL STREAMING ***
Note: Latitude = 409146138, Longitude = -746188906, Message = "at 112.45µs"
Note: Latitude = 409146139, Longitude = -746188906, Message = "at 1.00011245s"
Note: Latitude = 409146140, Longitude = -746188906, Message = "at 2.00011245s"

8. ¿Qué sigue?

9. Colaboradores de este codelab

  • Cathy Zhao
  • Arvind Bright
  • Nathaniel Ford