WebSocket connections in Python

Why and when to use WebSockets?

Zato WebSockets channels are an attractive counterpart to REST-based APIs. Their main characteristics are:

  • Low overhead, ideal for IoT and high-performance server-side integrations.
  • A technology that is well-known in the frontend programming world, frequently used for long-running bi-directional connections from browsers to servers.
  • Ability to send push notifications and events from servers to WebSocket clients.
  • A natural, REST-like programming interface revolving around the usage of Python dicts or JSON for requests and responses.
  • Ability to reuse all the existing external tooling built with REST and OpenAPI in mind - it is possible to invoke WebSocket-using services with tools that do not know about the technology as such, e.g. to keep using Postman for testing the business logic of your services.
  • In restrictive environments, it is of advantage that WebSockets are a well-known technology based on top of HTTP which means that firewalls, and related network equipment that may possibly require additional effort before it can accept protocols other than HTTP, are often already familiar with WebSockets.
  • Being part of the overall Zato platform, WebSocket channels can always make use of other parts of it, e.g. in enterprise environments, browser-based connections can be authenticated with LDAP, CAS or similar resources.

Zato offers a dedicated client for WebSocket services that hides all the low-level details under a familiar API, as below:

# Zato
from zato.wsx_client import Client

# Prepare credentials
address  = 'ws://my.server:33055/api/v1/channel'
username = 'my.username'
password = 'my.password'

# Our service to invoke
service = 'zato.ping'

# Request to send - note that it is a regular dict
request = {'Hello': 'World'}

# Create a client object
client = Client(address, username, password)

# Connect to a Zato server - opens a long-running session and sends credentials
client.connect()

# Invoke the service defined above - this will block until a response arrives
response = client.invoke(request)

# Log what we received
print('Response ->', response)

WebSocket channels created in Dashboard are programming language-agnostic, which means that they may be used to invoke services and publish or receive messages from any application that speaks WebSockets and JSON, e.g. Python, JavaScript or any other.

To utilize WebSocket channels, applications (API clients) need to follow a protocol described in this chapter.

  • Upon opening an initial TCP stream, the client needs to obtain a token within a certain time (by default, 5 seconds)
  • Token returned by Zato identifies uniquely this particular client connection down to its TCP socket
  • The token is a random string considered a secret and must not be shared with any other client or connection
  • No other client is allowed to re-use an already issued token and each connection has exactly one token throughout its existence
  • Although the token is a strong random string, it can be used only for client identification and it must not be used on client side for any cryptographic purposes (there are dedicated crypto APIs in Zato for such needs)
  • Each token has a TTL, a Time To Live, which defaults to 864000 seconds = 10 days. Each time the channel is invoked the token's TTL is extended by that many seconds from current time. If token reaches its TTL, any subsequent invocation will be rejected and the TCP socket will be closed.
  • Clients must send WebSocket Ping frames (per RFC-6455) at least once in 30 seconds. If 5 consecutive pings from a client are missed, the connection is dropped by Zato.
  • Ping frames must not send business data, they are used only for keeping connections alive
  • Applications may both invoke API services as as well take part in publish/subscribe workflows - in the latter case they can act as publishers, subscribers, or both

Connecting, and obtaining a session token

Clients establish an initial WebSocket connection to the address a given channel utilizes, possibly taking into account the fact that there may be a load-balancer performing URL transformations or port mappings in front of Zato servers.

Once a connection is created, clients need to create a session and receive a session token. This step needs to be done even if for a given WebSocket channel no client credentials are needed.

Clients must use the session token in all requests over the same TCP connection. The token cannot be used any other connections, it is specific to one TCP stream.

Right after a TCP connection is established, before logging in, an information entry is stored in server logs. This includes data about from what IP address and domain name the connection comes from as well as the address and name of the WebSocket channel.

INFO - New connection from 127.0.0.1:51738 (localhost) to 127.0.0.1:48902 (my.wsx.channel)

If a client connects but does not initiate the create-session request within the expected time, the TCP connection will be closed with an accompanying warning message in server logs:

WARNING - Peer 127.0.0.1:51738 (localhost) did not create session within 5s,
closing its connection to 127.0.0.1:48902 (my.wsx.channel), cid:`23b3cae088382f62106edd67`

Otherwise, if the client creates a session successfully, an informational message will be saved to logs:

INFO - Client 127.0.0.1:57288 (localhost ws.eee4b49d6fd835b424175ed5)
logged in successfully to 127.0.0.1:48902 (my.wsx.channel)

Request

ElementDatatypeOptionalNotes
metadict---A dictionary of metadata for the request
meta.actionstring---A constant value of "create-session"
meta.usernamestringYesUsername to authenticate with (if needed)
meta.secretstringYesPassword or other channel-specific secret to authenticate with (if needed)
meta.idstring---Client-generated request ID - must be globally unique (e.g. UUID4). Returned in responses in the in_reply_to element to let clients know in reply to which request a given response is returned.
meta.timestampdatetime---When the message was generated by client, must be in UTC using ISO-8601 YYYY-MM-DDTHH:mm:ss.ssssss
meta.client_idstring---An arbitrary business ID of the client - any value identifying the client can be used, e.g. ID of a business application such as crm.prod.1 or ID of an IoT device connecting to Zato such as printer.mx2.3910
meta.client_namestringYesHuman-friendly identifier of the client (in addition to client_id)
{"meta": {
  "action": "create-session",
  "username": "user1",
  "secret": "PNVmGkLejnhsAZ5VzzfdHQkvGg",
  "id": "238dc406351444d0869390af9541da59",
  "timestamp": "2022-11-16T15:53:25.717215",
  "client_id": "p.33915",
  "client_name": "Printer #33915, Fifth floor"
  }}

Response

ElementDatatypeOptionalNotes
metadict---A dictionary of metadata for the response
meta.statusinteger---An overall status code, using HTTP status code, e.g. 200 is OK
meta.timestampdatetime---When the response was produced by Zato, in UTC
idstring---Response ID - its last element is a correlation ID that can be used to look up details in server log
in_reply_tostringYesIn reply to what request ID the response is returned
dataany---Business data related to the response. In case of an error it will be an error message. Otherwise, it will contain an element with the session token.
data.tokenstringYesA randomly generated session token - returned only if meta status is 200

Samples:

{"meta":{
  "status":200,
  "timestamp":"2022-04-11T09:17:16.954645",
  "in_reply_to":"238dc406351444d0869390af9541da59",
  "id":"zato.ws.srv.rsp-auth.39d6e397054e410810afeac2"},
"data":{
  "token":"ws.token.b76561f8c145cd6baf086d04"}}
{"meta": {
  "status":403,
  "timestamp":"2022-04-11T09:02:20.918796",
  "id":"zato.ws.srv.msg-err.82608509fb56261fd8c90910"},
  "data":"You are not authorized to access this resource"}

Invoking services

Clients with a session token can invoke the service that is mounted on a WebSocket channel they are connected to.

It means that if a client wants to invoke multiple services, the service from the channel needs to forward messages to the actual ones using self.invoke and send the response back to the calling client by assigning it to self.response.payload. That allows one to implement white-lists where only selected services can be made accessible to the caller and attempts to invoke any other one will be rejected.

There is also a default WebSockets gateway service that can be used to invoke any service, it is documented in this chapter below.

Request

ElementDatatypeOptionalNotes
metadict---A dictionary of metadata for the request
meta.actionstring---A constant value of "invoke-service"
meta.idstring---Same as in "create-session" action
meta.timestampdatetime---
meta.tokenstring---Session token returned by "create-session" action
dataany---Arbitrary data required by the channel's service - can be a string, integer, dict or anything else that the service expects

This sample assumes that service zato.helpers.echo is mounted on the channel - it is a built-in service that echoes back everything in receives on input.

{"meta": {
  "action": "invoke-service",
  "id": "36df91fca2444dcaadc7199691217cfd",
  "timestamp": "2016-11-16T15:53:25.717215",
  "token": "ws.token.7a1729f99acbfe21b0cca337"},
  "data": {
    "customer_id": "123",
    "account_id": "456"
  }}

Response

ElementDatatypeOptionalNotes
metadict---Same as in "create-session" action
meta.statusinteger---
meta.timestampdatetime---
idstring---
in_reply_tostringYes
dataany---Business data related to the response. In case of an error it will be an error message. Otherwise, it will contain a response that the channel's service returned - may be a string, integer, dict or any other datatype

Again, the sample shows what zato.helpers.echo may return if it is assigned to a WebSocket channel.

{"meta":{
  "status":200,
  "timestamp":"2022-04-11T11:38:34.400736",
  "in_reply_to":"36df91fca2444dcaadc7199691217cfd",
  "id":"zato.ws.srv.rsp-ok.05308f81590751038cc9167f"},
  "data":{
    "customer_id":"123",
    "account_id":"456"
  }}

Closing sessions and logging out

There is no separate API call to close a session and log out. It suffices to close the WebSocket connection along with its TCP stream - this will be immediately recognized by Zato and all the relevant shutdown and cleanup actions will be performed.

Afterwards, the session token cannot be used anymore even by the same client; a new session needs to be created with its own token instead.

Using the default WebSockets gateway

A built-in service called helpers.web-sockets-gateway can be mounted on a WebSocket channel to make it possible for clients to invoke any other arbitrary service.

Note, however, that clients will truly have access to any service deployed on the server they will connect to, including all the internal ones. If this is not desirable, the gateway service can be subclassed to implement a white-listing logic, i.e. to filter out requests that attempt to invoke a service outside of the list of permitted ones.


Schedule a meaningful demo

Book a demo with an expert who will help you build meaningful systems that match your ambitions

"For me, Zato Source is the only technology partner to help with operational improvements."

John Adams, Program Manager of Channel Enablement at Keysight