Beginner Tutorial#

Before you start#

It is highly encouraged that you read the Understanding HiSock section before reading the tutorial.

This tutorial also relies on you having basic networking knowledge (client/server, IP addresses, ports, etc.) and at least basic Python knowledge.

Lastly, if you don’t have HiSock installed, do that now! Read Installation.

This tutorial will focus on:

  • Creating a server

  • Creating a client

  • Sending and receiving data

  • Acting upon the data

Note

For this tutorial, I will be referring the IP addresses as hisock.utils.get_local_ip(). In reality, you will most likely use a hard-coded IP address from a user input.

Note

When I refer to “a way to identify the client”, I am talking about either:

  • a tuple of the IP address and port of the client or

  • a string of the client’s name (this will have ambiguity if multiple clients share the same name)

Note

There are a few terms that are used interchangeably here.

  • Message, content, and data mean the same thing.

  • Command and event mean the same thing.

Now, without further ado, let’s begin!


Creating our first server#

In HiSock, there exists a class, HiSockServer, in the server module. To create a server, you will need to create an instance of this class. The __init__ function of the HiSockServer class takes a required tuple parameter, which is the IP address as a string and port as an integer to start the server on. To find the local IP address, there is a function called utils.get_local_ip(). For the port, a number between 1024 and 65535 should be fine.

HiSockServer instances have a start() method to them, which will allow the server to listen for commands and data being sent.

import hisock

server = hisock.server.HiSockServer((hisock.utils.get_local_ip(), 6969))

server.start()

That’s basically it! Of course, this server is useless, but hey, it’s a step in the right direction! We’ll add on to this later on.

Obviously, without a client, a server is kind of pointless. So, let’s spice things up with some client code!


Creating our first client#

In HiSock, there is a class, HiSockClient, in the client module. To create a client, you will need to create an instance of this class. The __init__ function of the HiSockClient class takes two required parameters. The first parameter is a tuple with the IP address and port that the server is running on. The second (optional) parameter is for the name of the client, which is used for identification. The third (optional) parameter is the group of the client, which won’t be talked about in this tutorial.

Like HiSockServer, HiSockClient needs to have its start() method called to start the client.

import hisock

client = hisock.client.HiSockClient(
    (hisock.utils.get_local_ip(), 6969),
    name=input("What is your name? >")
)

client.start()

Like the server, this doesn’t do anything at all yet. Next, we will explore sending and receiving data in an example.


Transmitting Data#

Let’s explore transmitting data for HiSock!

HiSock is an event-driven module, and as such, has an on decorator and send() methods for both HiSockClient and HiSockServer.


Receiving data#

Note

There is another way of receiving data, which is the recv() method. This is not covered in this tutorial, but it is covered in the Intermediate-level Tutorial.

When a function is prefaced with the on decorator, it will run on something. It will listen for a command and run when that command is received.

The on decorator takes a maximum of three parameters. One of the parameters is the command to listen on. The second (optional) parameter is whether to run the listener in its own thread or not. The third (optional) parameter is whether to override a reserved command, and this tutorial won’t be covering it.

For the server: The on decorator will send a maximum of two parameters to the function it is decorating (there are a few exceptions we will touch on). The first parameter is the client data. It is an instance of ClientInfo that includes the client’s name, client IP address, and the group the client is in (can be type-casted to a dict). The second parameter is the data that is being received.

For the client: the on decorator will send a maximum of one parameter to the function it is decorating, which will be the message or content the client receives (in most cases).

Here’s an example with the on decorator in use in a server. Here, the server has a command, print_message_name, and will print the message that it gets and who sent it.

server = ...

@server.on("print_message_name")
def on_print_message_name(client_data, message: str):
    print(f'{client_data.name} sent "{message}"')

server.start()

Here’s another example with receiving data, this time on the client-side. The client will receive a command, greet, with a name. It will then print out a greeting with the name.

client = ...

@client.on("greet")
def on_greet(name: str):
    print(f"Hello there, {name}!")

client.start()

If the threaded parameter for the on decorator is True, then the function being decorated will run in a separate thread. This allows blocking code to run while still listening for updates.

It is useful if you want to get user input but also want to have the user receive other data.

client = ...

@client.on("ask_question", threaded=True)
def on_ask_question(question: str):
    """Contains blocking code with ``input()``."""
    answer = input(f"Please answer this question: {question}\n>")
    # ... send answer to server ...

@client.on("important")
def on_important(message: str):
    """This is important and cannot be missed!"""
    ...

client.start()

Sending data#

HiSock has multiple send methods. For now, we will be talking about sending to the server from one client or to one client from the server.

For the server: Sending data from the server to one client in HiSock uses the send_client() method. This method takes in a maximum of three parameters. The three parameters (in order) are a way to identify the client, the command to send, and the message being sent (optional). Although we won’t be talking about it here, send_all_clients() does exactly what it says. It will do send_client() to all the clients that are connected, and only takes in the command and optional message

For the client: Sending data to the server in HiSock uses the send() method. This method takes a maximum of two parameters. The first parameter is the command to send, and the second parameter is the message being sent (optional).

Here is an example of sending data with a server-side code block:

server = ...

@server.on("join")
def on_client_join(client_data):
    server.send_client(client_data.ip, "ask_question", "Do you like sheep?")

@server.on("question_response")
def on_question_response(client_data, response: str):
    server.send_client(client_data.ip, "grade", 100)

server.start()

And here is an example on the client-side:

client = ...

@client.on("ask_question")
def on_ask_question(question: str):
    answer = input(f"Please answer this question: {question}\n>")
    client.send("question_response", answer)

@client.on("grade")
def on_grade(grade: int):
    print(f"You got a {grade:>3}%.")

client.start()

Reserved events#

As I stated before, not every receiver has a maximum of two parameters passed to it. Here are the cases where that is the case.

HiSock has reserved events. These events shouldn’t be sent by the client or server explicitly as it is currently unsupported.

Note

Besides for string and bytes for message, these reserved events do not have type casting.

Here is a list of the reserved events:

Server:

  • join

    The client sends the event join when they connect to the server. The only parameter sent to the function being decorated is the client data.

  • leave

    The client sends the event leave when they disconnect from the server. The only parameter sent to the function being decorated is the client data.

  • name_change

    The client sends the event name_change when they change their name. The parameters sent to the listening function are (in order) the client data, the old name, and the new name.

  • group_change

    The client sends the event group_change when they change their group. The parameters sent to the listening function are (in order) the client data, the old group, and the new group.

  • message

    When the server receives a command, it’ll send an event to itself called message which will have two parameters. The two parameters are the client data who sent it and the raw data which was received.

  • *

    This will be called when there is no listener for an incoming command and data. The three parameters are the client data, the command, and the content.

Client:

  • client_connect

    When a client connects to the server, all the clients will have this event called. The only parameter for this is the client data for the client which joined.

  • client_disconnect

    When a client disconnects from the server, all the clients will have this event called. The only parameter for this is the client data for the client which left.

  • force_disconnect

    The server sends the event force_disconnect to a client when they kick the client. There are no parameters sent with the function that is being decorated with this.

  • *

    This will be called when there is no listener for an incoming command and data. The two parameters are the command and the content.


Type-casting#

HiSock has a system called “type-casting” when transmitting data.

Data sent and received can be one of the following types:

  • bytes

  • str

  • int

  • float

  • bool

  • None

  • list (with the types listed here)

  • dict (with the types listed here)

Note

There is a type hint in hisock.utils called Sendable which has these.

The type that the data gets type-casted to depends on the type hint for the message argument for the function for the event receiving the data. If there is no type hint for the argument, the data received will be bytes.

Here are a few examples this server-side code block:

@server.on("string_sent")
def on_string_sent(client_data, message: str):
    """``message`` will be of type ``string``"""
    ...

@server.on("integer_sent")
def on_integer_sent(client_data, integer: int):
   """``integer`` will be of type ``int``"""
   ...

@server.on("dictionary_sent")
def on_dictionary_sent(client_data, dictionary: dict):
   """``dictionary`` will be of type ``dict``"""
   ...

Note

Although these examples are on the server-side, they work the exact same for the client-side.

Of course, you need to be careful that the type-casting will work. Turning b"hello there" to int will fail.


Dynamic arguments#

Remember where I said the on decorator will call the function with a maximum number of parameters?

In HiSock with an _unreserved_ event, the function to handle it can be called with the maximum number of parameters or less. Note that in a reserved event, dynamic arguments doesn’t apply.

As an example, for the server: If an event has 1 argument, it will only be called with the client data. If it has 2 arguments, it will be called with the client data and the message. If it has 0 arguments, it’ll be called as a void (no arguments).

Data can be sent similarly. If there is no data sent, the server will receive the equivalent of None for the type-casted data.

Here are a few examples of this with a server-side code block.

@server.on("event1")
def on_event1(client_data, message: str):
    print(f"I have {client_data=} and {message=} as a string!")

@server.on("event2")
def on_event2(client_data, message: int):
    print(f"I have {client_data=} and {message=} as an integer! {message+1=}")

@server.on("event3")
def on_event3(client_data):
    print(f"I only have {client_data=}!")

@server.on("event4")
def on_event4():
    print("I have nothing.")

Here is an example with a client-side code block that ties into the server-side code block above:

client.send("event1", "Hello")  # Server will receive "Hello"
client.send("event1")  # Server will receive an empty string
client.send("event2", b"123")  # Server will receive 123 and output 124
client.send("event2")  # Server will receive 0 and output 1
client.send("event3", "there")  # Server won't receive "there"
client.send("event4", "Hi")  # Server won't receive anything

Conclusion#

Now, you know how to:

  • Create a server

  • Create a client

  • Transmit data

  • Work with dynamic arguments

  • Handle datatypes transmitted

  • Do stuff with the data

Have fun HiSock-ing!