Featured resource
pluralsight tech forecast
2025 Tech Forecast

Which technologies will dominate in 2025? And what skills do you need to keep up?

Check it out
Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Exploring gRPC Foundations

This code lab will teach you the fundamentals of Protocol Buffers through hands-on experience. You'll learn how to define structured data, create enumerations, work with nested types, and design basic gRPC services. By the end of this lab, you'll have a solid foundation in Protocol Buffers syntax and concepts, preparing you for more advanced gRPC implementations in your preferred programming language.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 19m
Published
Clock icon Jul 17, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: Exploring gRPC Foundations.

    In this lab, you'll explore the fundamentals of Protocol Buffers and gRPC by creating a .proto file for a Smart Home System scenario. You'll learn the core concepts of Protocol Buffers, from basic message syntax to defining services, without diving into the complexities of client-server implementation. ---

    Introduction to Protocol Buffers and gRPC

    Protocol Buffers (protobuf) is a language-agnostic data serialization format developed by Google. It allows you to define structured data schemas and generate code in various programming languages to read and write data according to these schemas. Protocol Buffers are often used in conjunction with gRPC, a high-performance, open-source framework for remote procedure calls (RPC).

    gRPC uses Protocol Buffers as its Interface Definition Language (IDL) and underlying message interchange format. With gRPC, you define your service interfaces and message structures using Protocol Buffers, and the framework generates the necessary code for client-server communication. ---

    Lab Scenario and Structure

    In this lab, you'll work on a Smart Home System scenario. The system will involve defining various smart home devices such as thermostats, lights, and cameras. Each device will have properties like ID, name, status, and battery level. You'll also create a service for retrieving device information.

    You'll work with a single file, SmartHome.proto, which will be the main Protocol Buffer definition file for the Smart Home System.

    Throughout the lab, you'll incrementally build and refine the .proto file, adding new concepts and features with each step.

    Let's get started!

  2. Challenge

    Step 1. Basic Message Syntax

    Proto File Structure

    A Protocol Buffers definition file (.proto file) typically starts with a syntax declaration followed by an optional package statement and then message definitions:

    syntax = "proto3";
    
    package mypackage;
    
    // Message definitions ...
    

    The syntax declaration specifies the version of the Protocol Buffers language being used. The current version is proto3, which offers a simpler and more streamlined syntax compared to its predecessor, proto2.

    Package statements are used to prevent naming conflicts between different projects. They define a namespace for the protocol buffer elements within the file.

    Defining Messages

    Messages are the core building blocks in Protocol Buffers. They represent structured data and consist of a set of fields, each with a name, type, field number, and optional field label.

    To define a message, you use the message keyword followed by the message name:

    messsage MessageName {
      // ...
    }
    

    Inside the message definition, you declare the fields using the format:

    [field_label] type field_name = field_number;
    

    Here's an example of defining a simple message:

    message Person {
      string name = 1;
      int32 age = 2;
      bool is_student = 3;
    }
    

    This example defines a Person message with three fields: name, age, and is_student. The name field is of type string and has a field number of 1. The age field is of type int32 and has a field number of 2. The is_student field is of type bool and has a field number of 3.

    Field numbers are used to uniquely identify fields within a message and play an important role in backwards and forwards compatibility when working with different versions of a message.

    When you define a field in a message, you assign it a unique field number. Field numbers should not be changed once your message type is in use, as they are used to identify fields in the binary encoding of the message.

    Field numbers in the range 1-15 require one byte to encode, while numbers in the range 16-2047 require two bytes. It's recommended to reserve field numbers 1-15 for frequently used fields to minimize encoded message size.

    In the next task, you'll define a message with three fields.

  3. Challenge

    Step 2. Field Types

    Protocol Buffers support a wide range of field types that you can use to define the structure of your messages. These types can be categorized into scalar types, enumerated types, and composite types.

    Scalar Value Types

    Scalar types represent simple values and include:

    • Integer types:
      • int32: 32-bit signed integer
      • int64: 64-bit signed integer
      • uint32: 32-bit unsigned integer
      • uint64: 64-bit unsigned integer
      • sint32: 32-bit signed integer (more efficient encoding for negative numbers)
      • sint64: 64-bit signed integer (more efficient encoding for negative numbers)
    • Floating-point types:
      • float: 32-bit floating-point
      • double: 64-bit floating-point
    • Boolean type:
      • bool: true or false
    • String type:
      • string: UTF-8 encoded or 7-bit ASCII text
    • Bytes type:
      • bytes: arbitrary byte sequence no longer than 232

    When defining fields in your messages, you specify the field type followed by the field name and the field number. The field number is used to identify the field in the binary encoding and should be unique within the message.

    Here's an example showcasing different scalar value types:

    message Person {
      string name = 1;
      int32 age = 2;
      bool is_student = 3;
      float grade_point_average = 4;
    }
    

    In this example, the Person message includes fields of various scalar types:

    • name is a string field.
    • age is an int32 field.
    • is_student is a bool field.
    • grade_point_average is a float field.

    In addition to scalar types, Protocol Buffers also support enumerated types (enums) and composite types (messages and arrays). Enums allow you to define a set of named constants, while composite types enable you to create more complex structures by nesting messages or using arrays.

    In the next task, you'll practice adding fields of different types to the Device message.

  4. Challenge

    Step 3. Enumerations

    Enumerations, or enums, in Protocol Buffers allow you to define a set of named constants. They are useful when you have a field that can take on one of a limited set of values. Enums make your code more readable and less error-prone by providing a clear way to represent and validate these values.

    Defining an Enum

    To define an enum in Protocol Buffers, you use the enum keyword followed by the enum name. Inside the enum definition, you list the constant values, each with a unique integer value.

    Here's an example of defining an enum for different types of vehicles:

    enum VehicleType {
      UNKNOWN = 0;
      CAR = 1;
      TRUCK = 2;
      MOTORCYCLE = 3;
      BICYCLE = 4;
    }
    

    This example defines a VehicleType enum with five constant values: UNKNOWN, CAR, TRUCK, MOTORCYCLE, and BICYCLE. Each constant is assigned a unique integer value starting from 0.

    It's important to note that the first constant in an enum should always have a value of 0. This zero value is used as the default value if no value is explicitly set for an enum field.

    Using an Enum

    Once you have defined an enum, you can use it as a field type in your messages. When defining a field with an enum type, you use the enum name as the field type.

    Here's an example of using the VehicleType enum in a Vehicle message:

    message Vehicle {
      string make = 1;
      string model = 2;
      int32 year = 3;
      VehicleType type = 4;
    }
    

    In this example, the Vehicle message has a field named type of type VehicleType. This field can hold one of the constant values defined in the VehicleType enum.

    When setting the value of an enum field, you use the enum constant name, like VehicleType.CAR or VehicleType.MOTORCYCLE.

    In the next tasks, you'll practice defining an enum for different types of smart home devices and adding it as a field to the Device message.

  5. Challenge

    Step 4. Nested Types

    Protocol Buffers allow you to define nested types within a message. Nested types are useful when you want to group related fields together or create a hierarchical structure within your messages. They help in organizing your data and making your protocol buffer definitions more modular and readable.

    To create a nested message, you define a new message type inside an existing message. The syntax for defining a nested message is the same as defining a regular message, but it is placed within the scope of the parent message.

    Here's an example of creating a nested message:

    message Person {
      string name = 1;
      int32 age = 2;
      
      Address home_address = 3;
      Address work_address = 4;
      
      message Address {
        string street = 1;
        string city = 2;
        string country = 3;
      }
    }
    

    In addition to having two fields, name and age, the Person message defines a nested message called Address, which has its own fields: street, city, and country.

    You can use the nested Address message as a field type within the Person message. In this case, there are two fields of type Address: home_address and work_address.

    When accessing nested fields in your code, you use the dot notation to navigate through the hierarchy of messages.

    For example, to access the street field of the home_address in the Person message, you would use the following syntax:

    person.home_address.street
    

    In the next tasks, you'll create a nested message within the Device message to represent the location of a device.

  6. Challenge

    Step 5: Field Labels

    Field labels are used to specify the cardinality of a field. The field label determines how many values a field can have and how those values are handled, stored, and serialized.

    There are three types of field labels when using the proto3 syntax:

    1. optional: An optional field can either be set (with a value) or unset (without a value). If set, it contains a value explicitly provided or parsed from the wire and it will be serialized. If unset, it returns a default value and will not be serialized. For example:

      message Example {
        optional int32 id = 1;
      }
      
    2. repeated: Represents an ordered collection of the same type, allowing zero or more occurrences of the field and preserving the order of repeated values. Think about an array or a list. For example:

      message Example {
        repeated string names = 1;
      }
      
    3. map: Defines key-value pairs within the field, similar to a map or dictionary. Internally, it is represented as a repeated field of entries containing key and value. It's going to translate into a hash map or a dictionary data type depending on the map type structure that your target programming language uses. For example:

      message Example {
        map<string, int32> attributes = 1;
      }
      

      Which is equivalent to:

      message Example {
        message AttributesEntry {
          optional string key = 1;
          optional int32 value = 2;
        }
        repeated AttributesEntry attributes = 1;
      }
      

    If no explicit field label is applied, the field is optional by default.

    In the next task, you'll add a repeated field to the Device message to represent supported commands.

  7. Challenge

    Step 6: Importing Other Files

    Protocol Buffers allow you to import definitions from other .proto files, enabling code reuse and modularization.

    To import a .proto file, you use the import keyword followed by the path to the file you want to import. The imported file should be specified relative to the current file's directory or relative to the --proto_path (or -I) flag passed to the Protocol Buffers compiler.

    For example, Protocol Buffers provide a set of built-in types defined by Google, such as Timestamp, Duration, Any, and more. These types are commonly used and can be imported from the google/protobuf/ directory.

    For example, to use the Duration type, you would import the google/protobuf/duration.proto file:

    import "google/protobuf/duration.proto";
    

    After importing the file, you can use the Duration type in your messages:

    message MyMessage {
      google.protobuf.Duration message_duration = 1;
    }
    

    The Duration type represents represents a signed, fixed-length span of time represented as a count of seconds and fractions of seconds at nanosecond resolution.

    Google's protobuf types are located in the include directory that comes with the Protocol Buffer distribution. By default, this directory is in the compiler's proto path, but if your setup involves multiple directories or custom paths, or if you rely on specific versions of Google's .proto files that are not included by default, you can specify the proto path using the --proto_path (or -I) flag when running the Protocol Buffers compiler. For example:

    protoc --proto_path=./include --python_out=. file.proto
    

    This command tells the compiler to look for imported .proto files in the ./include directory and generate Python code for file.proto.

    In the next task, you'll import theTimestamp type in your Device message.

  8. Challenge

    Step 7: Defining a Service

    In addition to defining message types, Protocol Buffers also allow you to define services. Services are a way to specify the API that your server provides to clients. They define a set of methods that can be called remotely, along with the request and response message types for each method.

    Services in Protocol Buffers are typically used in conjunction with gRPC (gRPC Remote Procedure Call), a high-performance, open-source framework for remote procedure calls. gRPC uses Protocol Buffers as the Interface Definition Language (IDL) and the underlying serialization mechanism.

    To define a service in Protocol Buffers, you use the service keyword followed by the service name. Inside the service definition, you specify one or more RPC methods, each with a unique name, input message type, and output message type.

    Here's an example of defining a service:

    service MyService {
      rpc MyMethod(MyRequest) returns (MyResponse);
    }
    

    This example defines a service named MyService with a single RPC method named MyMethod. The method takes a MyRequest message as input and returns a MyResponse message.

    The input and output message types for each RPC method are defined separately using the message keyword, just like regular message types.

    message MyRequest {
      string query = 1;
    }
    
    message MyResponse {
      string result = 1;
    }
    

    In the next tasks, you'll define a request type for the service and the service itself with a method to retrieve device information.

  9. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    To compile your SmartHome.proto file, use the Protocol Buffers compiler (protoc) available in the bin directory. For example:

    bin/protoc SmartHome.proto --python_out=.
    

    This command generates a SmartHome_pb2.py file, which contains the Python classes for the defined messages. The file will be placed in the current directory (.).

    Feel free to explore the generated code in some of the supported programming languages. This will give you a better understanding of how the defined messages are translated into actual code that you can use in your applications:

    Java

    bin/protoc SmartHome.proto --java_out=.
    

    C++

    bin/protoc SmartHome.proto --cpp_out=.
    

    C#

    bin/protoc SmartHome.proto --csharp_out=.
    

    PHP

    bin/protoc SmartHome.proto --php_out=.
    

    However, all the above commands will only generate the code for the message definitions. This is because protoc requires additional plugins to generate code for gRPC services, and the installation procedure varies from language to language. You can find more information on the documentation page for each supported language. ---

    Extending the Smart Home System

    While you have already defined the core components of the Smart Home System, there are many ways you can extend and enhance its functionality. Here are a few suggestions for additional features you could add:

    1. Device Groups: Consider adding a new message type to represent device groups, allowing you to organize and manage multiple devices together. You could define fields such as group name, description, and a list of device IDs belonging to the group.

    2. Add Device Method: Extend the SmartHomeSystem service with a new RPC method for adding devices to the system. This method could take a new AddDeviceRequest message as input, containing the necessary information to create a new device, and return a response indicating the success or failure of the operation.

    3. Update Device Status Method: Add another RPC method to the SmartHomeSystem service for updating the status of a device. This method could take an UpdateDeviceStatusRequest message as input, specifying the device ID and the new status value, and return a response confirming the status update. ---

    Related Courses on Pluralsight's Library

    If you're interested in further honing your skills or exploring more topics related to Protocol Buffers and gRPC, Pluralsight offers two excellent courses:

    These courses cover many aspects of Protocol Buffers and gRPC. Check them out to continue your learning journey!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.