ZeroC Ice Overview
ZeroC Ice (Internet Communications Engine) is a modern, object-oriented middleware platform for building distributed applications. It provides:
Language-agnostic RPC : Define interfaces once, implement in any supported language
Efficient binary protocol : Compact marshalling for network efficiency
Transparent networking : Abstract away TCP/UDP complexities
Exception propagation : Domain exceptions traverse network boundaries
Ice uses the Slice language (Specification Language for Ice) to define interfaces. The slice2py compiler generates Python code from Slice definitions.
Slice Interface Definition
The contract between client and server is defined in backend/Conversor.ice:
module Conversor
{
exception UnidadInvalidaException
{
string mensaje;
};
interface ConversorUnidades
{
double convertirTemperatura(double valor, string desde, string hasta)
throws UnidadInvalidaException;
double convertirLongitud(double valor, string desde, string hasta)
throws UnidadInvalidaException;
double convertirPeso(double valor, string desde, string hasta)
throws UnidadInvalidaException;
double convertirVelocidad(double valor, string desde, string hasta)
throws UnidadInvalidaException;
string unidadesDisponibles(string categoria)
throws UnidadInvalidaException;
};
};
Slice Components
Module
Exception
Interface
module Conversor creates a namespace to prevent naming conflicts. This becomes a Python package after code generation.
UnidadInvalidaException is a domain exception that can be marshalled across the network:exception UnidadInvalidaException {
string mensaje;
};
This becomes a Python exception class that can be caught and raised like any native exception. interface ConversorUnidades defines the remote object’s contract:
Methods : Callable operations (convertirTemperatura, etc.)
Parameters : Typed arguments (double, string)
Return types : What each method returns
Exceptions : Which exceptions can be thrown
Interfaces compile to abstract base classes that servants must implement.
Code Generation
The Slice definition is compiled to Python code using slice2py:
This generates:
Conversor.py - Contains exception classes and proxy interfaces
Conversor_ice.py - Contains Ice-specific metadata
Generated files should not be edited manually. Always modify the .ice file and regenerate.
Server Implementation
Servant Pattern
The servant is the actual object implementation that handles remote calls. From backend/server.py:14-128:
import Ice
import Conversor # Generated by slice2py
# Servant inherits from generated interface
class ConversorUnidadesImpl ( Conversor . ConversorUnidades ):
def convertirTemperatura ( self , valor , desde , hasta , current = None ):
"""Implement temperature conversion logic"""
unidades = { "celsius" , "fahrenheit" , "kelvin" }
desde, hasta = desde.lower(), hasta.lower()
self ._validar(desde, hasta, unidades, "temperatura" )
# Strategy: convert to Celsius first, then to target
if desde == "fahrenheit" :
en_celsius = (valor - 32 ) * 5 / 9
elif desde == "kelvin" :
en_celsius = valor - 273.15
else :
en_celsius = valor # already Celsius
if hasta == "fahrenheit" :
return en_celsius * 9 / 5 + 32
elif hasta == "kelvin" :
return en_celsius + 273.15
return en_celsius
def _validar ( self , desde , hasta , validas , categoria ):
"""Validate units and throw ICE exception if invalid"""
errores = []
if desde not in validas:
errores.append( f "Unidad origen ' { desde } ' no valida" )
if hasta not in validas:
errores.append( f "Unidad destino ' { hasta } ' no valida" )
if errores:
ex = Conversor.UnidadInvalidaException()
ex.mensaje = " " .join(errores)
raise ex # ICE will marshal this exception to client
The current parameter is required by Ice. It provides context about the invocation (client address, operation name, etc.), but can be ignored if not needed.
Object Adapter
The Object Adapter is Ice’s component that receives network requests and dispatches them to servants. From server.py:131-159:
Initialize Communicator
with Ice.initialize(sys.argv) as communicator:
The communicator is the main Ice runtime object. It:
Manages network connections
Handles thread pools
Parses command-line Ice properties (e.g., --Ice.Trace.Network=2)
Create Object Adapter
adapter = communicator.createObjectAdapterWithEndpoints(
"ConversorAdapter" , # Adapter name (for configuration)
"default -p 10000" # Endpoint: TCP on port 10000
)
Endpoint syntax : protocol -option value
default: TCP protocol
-p 10000: Listen on port 10000
Could add -h hostname to bind specific interface
Create and Register Servant
servant = ConversorUnidadesImpl()
adapter.add(servant, Ice.stringToIdentity( "ConversorUnidades" ))
servant : Your implementation instance
identity : Unique name clients use to locate this object
stringToIdentity() converts string to Ice identity structure
Activate Adapter
Starts accepting incoming connections on the configured endpoint.
Wait for Shutdown
communicator.waitForShutdown()
Blocks until Ctrl+C or programmatic shutdown. The adapter continues serving requests on background threads.
Complete Server Code
def main ():
with Ice.initialize(sys.argv) as communicator:
# Create adapter listening on TCP port 10000
adapter = communicator.createObjectAdapterWithEndpoints(
"ConversorAdapter" , "default -p 10000"
)
# Create servant and register with identity "ConversorUnidades"
servant = ConversorUnidadesImpl()
adapter.add(servant, Ice.stringToIdentity( "ConversorUnidades" ))
# Start accepting connections
adapter.activate()
print ( "Servidor escuchando en puerto 10000... (Ctrl+C para detener)" )
# Block until shutdown signal
communicator.waitForShutdown()
if __name__ == "__main__" :
main()
Client Implementation (Flask Proxy)
The Flask server acts as an Ice client using a proxy to invoke remote methods. From web_server.py:26-73:
Proxy Creation
Initialize Communicator
self .communicator = Ice.initialize(sys.argv)
Client-side communicator for managing connections.
Create Base Proxy
base = self .communicator.stringToProxy(
"ConversorUnidades:default -p 10000"
)
Proxy string syntax : identity:endpoint
ConversorUnidades: Object identity on server
default -p 10000: Connect via TCP to port 10000 on localhost
Checked Cast
self .proxy = Conversor.ConversorUnidadesPrx.checkedCast(base)
if not self .proxy:
raise RuntimeError ( "Servidor ICE no encontrado" )
checkedCast verifies the server actually implements the expected interface by sending a network request. Returns None if types don’t match.Use uncheckedCast for better performance if you trust the server type, but you lose type safety.
Invoking Remote Methods
Once the proxy is created, calling remote methods looks like local calls:
def convert_temperatura ( self , valor , desde , hasta ):
return self .proxy.convertirTemperatura(valor, desde, hasta)
Behind the scenes , Ice:
Marshals parameters into binary format
Transmits over TCP connection to server
Waits for response (synchronous by default)
Unmarshals return value or exception
Returns result or raises exception
Exception Handling
try :
resultado = cliente.convert_temperatura(valor, desde, hasta)
except Conversor.UnidadInvalidaException as e:
# Domain exception from Slice definition
return jsonify({ 'error' : f 'Unidad inválida: { e.mensaje } ' }), 400
except Ice.ConnectionRefusedException:
# Ice infrastructure exception - server not running
return jsonify({ 'error' : 'Servidor ICE desconectado' }), 503
except Exception as e:
# Other unexpected errors
return jsonify({ 'error' : str (e)}), 500
Ice Architecture Patterns
Proxy vs Servant
Proxy (Client Side)
Location : Flask server (client)
Purpose : Represents remote object
Behavior : Marshals calls, sends over network
Type : Conversor.ConversorUnidadesPrx
Servant (Server Side)
Location : ICE server
Purpose : Implements business logic
Behavior : Processes requests, returns results
Type : ConversorUnidadesImpl(Conversor.ConversorUnidades)
Direct vs Indirect Proxies
"ConversorUnidades:default -p 10000"
Hardcodes endpoint in proxy string. Simple but requires knowing server location at compile/deploy time. Pros : Simple, no additional infrastructure
Cons : Not flexible, requires reconfiguration if server moves"ConversorUnidades" # No endpoint specified
Uses Ice registry/locator service to discover server location dynamically. Pros : Flexible, supports failover, load balancing
Cons : Requires IceGrid infrastructure
Network Protocol Details
Ice Protocol
Ice uses a compact binary protocol for efficiency:
Request Header : Operation name, request ID, context
Parameters : Marshalled in order (valor, desde, hasta)
Response : Return value or exception
Marshalling Example
For convertirTemperatura(100.0, "celsius", "fahrenheit"):
[Header: 8 bytes]
[Operation: "convertirTemperatura"]
[Param 1: double 100.0 = 8 bytes]
[Param 2: string "celsius" = 4 + 7 = 11 bytes]
[Param 3: string "fahrenheit" = 4 + 10 = 14 bytes]
Total: ~41 bytes
Response:
[Header: 8 bytes]
[Return: double 212.0 = 8 bytes]
Total: 16 bytes
Ice protocol is significantly more efficient than JSON over HTTP. A comparable REST call would be ~150+ bytes.
Connection Management
Client Connection Lifecycle
class ConversorClient :
def connect ( self ):
self .communicator = Ice.initialize(sys.argv)
base = self .communicator.stringToProxy(
"ConversorUnidades:default -p 10000"
)
self .proxy = Conversor.ConversorUnidadesPrx.checkedCast(base)
# Connection established on first method call (lazy)
def disconnect ( self ):
if self .communicator:
self .communicator.destroy() # Close all connections
Always call communicator.destroy() to clean up resources. Using context managers (with Ice.initialize()) handles this automatically.
Advanced Ice Features
Asynchronous Invocation
Ice supports async calls for non-blocking operations:
# Synchronous (blocks)
result = proxy.convertirTemperatura( 100 , "celsius" , "fahrenheit" )
# Asynchronous (returns future)
future = proxy.convertirTemperaturaAsync( 100 , "celsius" , "fahrenheit" )
result = future.result() # Block only when result needed
Oneway Calls
For fire-and-forget operations (not used in this app):
interface Logger {
void logMessage(string msg); // No return value
};
proxy.ice_oneway().logMessage( "Event occurred" ) # Returns immediately
Timeout Configuration
Set per-proxy or globally:
# Per-proxy timeout (5 seconds)
proxy = proxy.ice_timeout( 5000 )
# Global timeout via Ice properties
# --Ice.Default.InvocationTimeout=5000
Debugging Ice Applications
Server-side Tracing
python3 server.py --Ice.Trace.Network=2 --Ice.Trace.Protocol=1
Network: Logs connection events
Protocol: Logs message headers
Client-side Tracing
Add to Flask startup:
self .communicator = Ice.initialize([
'--Ice.Trace.Network=2' ,
'--Ice.Trace.Protocol=1'
])
Concurrency Ice handles concurrent requests via thread pool. Default size is typically sufficient for this workload.
Connection Pooling Ice automatically reuses connections. Multiple proxies to the same endpoint share a connection.
Compression Enable for large payloads: proxy = proxy.ice_compress( True )
Batching Batch multiple oneway calls: batch = proxy.ice_batchOneway()
batch.operation1()
batch.operation2()
batch.ice_flushBatchRequests()
Security Features
SSL/TLS Support
Ice supports encrypted connections:
# Server
adapter = communicator.createObjectAdapterWithEndpoints(
"ConversorAdapter" ,
"ssl -p 10000" # SSL instead of TCP
)
# Client
proxy = communicator.stringToProxy(
"ConversorUnidades:ssl -p 10000"
)
Requires Ice SSL configuration (certificates, keys).
Current implementation uses unencrypted TCP. Use SSL for production deployments over untrusted networks.
Next Steps
Flask Server Learn how Flask integrates the Ice client
Slice Reference Official ZeroC Ice Slice documentation