- Lab
- Core Tech

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.

Path Info
Table of Contents
-
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!
-
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
, andis_student
. Thename
field is of typestring
and has a field number of1
. Theage
field is of typeint32
and has a field number of2
. Theis_student
field is of typebool
and has a field number of3
.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 range16
-2047
require two bytes. It's recommended to reserve field numbers1
-15
for frequently used fields to minimize encoded message size.In the next task, you'll define a message with three fields.
-
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 integerint64
: 64-bit signed integeruint32
: 32-bit unsigned integeruint64
: 64-bit unsigned integersint32
: 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-pointdouble
: 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 astring
field.age
is anint32
field.is_student
is abool
field.grade_point_average
is afloat
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. - Integer types:
-
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
, andBICYCLE
. 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 aVehicle
message:message Vehicle { string make = 1; string model = 2; int32 year = 3; VehicleType type = 4; }
In this example, the
Vehicle
message has a field namedtype
of typeVehicleType
. This field can hold one of the constant values defined in theVehicleType
enum.When setting the value of an enum field, you use the enum constant name, like
VehicleType.CAR
orVehicleType.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. -
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
andage
, thePerson
message defines a nested message calledAddress
, which has its own fields:street
,city
, andcountry
.You can use the nested
Address
message as a field type within thePerson
message. In this case, there are two fields of typeAddress
:home_address
andwork_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 thehome_address
in thePerson
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. -
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:-
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; }
-
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; }
-
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. -
-
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 theimport
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 thegoogle/protobuf/
directory.For example, to use the
Duration
type, you would import thegoogle/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 forfile.proto
.In the next task, you'll import the
Timestamp
type in yourDevice
message. -
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 namedMyMethod
. The method takes aMyRequest
message as input and returns aMyResponse
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.
-
Challenge
Conclusion
Congratulations on successfully completing this Code Lab!
To compile your
SmartHome.proto
file, use the Protocol Buffers compiler (protoc
) available in thebin
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:
-
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.
-
Add Device Method: Extend the
SmartHomeSystem
service with a new RPC method for adding devices to the system. This method could take a newAddDeviceRequest
message as input, containing the necessary information to create a new device, and return a response indicating the success or failure of the operation. -
Update Device Status Method: Add another RPC method to the
SmartHomeSystem
service for updating the status of a device. This method could take anUpdateDeviceStatusRequest
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!
-
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.