Skip to main content

Create minimal client and server

This Transport workflow covers all aspects of the Unity.Networking.Transport package and helps you create a sample project that highlights how to use the com.unity.transport API to:

  • Configure
  • Connect
  • Send data
  • Receive data
  • Close a connection
  • Disconnect
  • Timeout a connection

The goal is to make a remote add function. The flow will be: a client connects to the server, and sends a number, this number is then received by the server that adds another number to it and sends it back to the client. The client, upon receiving the number, disconnects and quits.

Using the NetworkDriver to write client and server code is similar between clients and servers; there are a few subtle differences demonstrated in this guide.

Creating a Server

A server is an endpoint that listens for incoming connection requests and sends and receives messages.

Start by creating a C# script in the Unity Editor.

Filename: Assets\Scripts\ServerBehaviour.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ServerBehaviour : MonoBehaviour {

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}
}

Boilerplate code

As the com.unity.transport package is a low level API, there is a bit of boiler plate code you might want to setup. This is an architecture design Unity chose to make sure that you always have full control.

note

As development on the com.unity.transport package evolves, more abstractions may be created to reduce your workload on a day-to-day basis.

The next step is to clean up the dependencies and add our boilerplate code:

Filename: Assets\Scripts\ServerBehaviour.cs

using UnityEngine;
using UnityEngine.Assertions;

using Unity.Collections;
using Unity.Networking.Transport;

...

Code walkthrough

ServerBehaviour.cs

Adding the members we need the following code:

Filename: Assets\Scripts\ServerBehaviour.cs

using ...

public class ServerBehaviour : MonoBehaviour {

public NetworkDriver m_Driver;
private NativeList<NetworkConnection> m_Connections;

void Start () {
}

void OnDestroy() {
}

void Update () {
}

Code walkthrough

public NetworkDriver m_Driver;
private NativeList<NetworkConnection> m_Connections;

You need to declare a NetworkDriver. You also need to create a NativeList to hold our connections.

Start method

Filename: Assets\Scripts\ServerBehaviour.cs

void Start ()
{
m_Driver = NetworkDriver.Create();
var endpoint = NetworkEndPoint.AnyIpv4;
endpoint.Port = 9000;
if (m_Driver.Bind(endpoint) != 0)
Debug.Log("Failed to bind to port 9000");
else
m_Driver.Listen();

m_Connections = new NativeList<NetworkConnection>(16, Allocator.Persistent);
}

Code walkthrough

The first line of code, m_Driver = NetworkDriver.Create();, just makes sure you are creating your driver without any parameters.

    if (m_Driver.Bind(endpoint) != 0)
Debug.Log("Failed to bind to port 9000");
else
m_Driver.Listen();

Then we try to bind our driver to a specific network address and port, and if that does not fail, we call the Listen method.

info

the call to the Listen method sets the NetworkDriver to the Listen state. This means that the NetworkDriver will now actively listen for incoming connections.

m_Connections = new NativeList<NetworkConnection>(16, Allocator.Persistent);

Finally we create a NativeList to hold all the connections.

OnDestroy method

Both NetworkDriver and NativeList allocate unmanaged memory and need to be disposed. To make sure this happens we can simply call the Dispose method when we are done with both of them.

Add the following code to the OnDestroy method on your MonoBehaviour:

Filename: Assets\Scripts\ServerBehaviour.cs

public void OnDestroy()
{
if (m_Driver.IsCreated)
{
m_Driver.Dispose();
m_Connections.Dispose();
}
}

The check for m_Driver.IsCreated ensures we don't dispose of the memory if it hasn't been allocated (for example, if the component is disabled).

Server Update loop

As the com.unity.transport package uses the Unity C# Job System internally, the m_Driver has a ScheduleUpdate method call. Inside our Update loop you need to make sure to call the Complete method on the JobHandle that is returned, to know when you are ready to process any updates.

void Update () {

m_Driver.ScheduleUpdate().Complete();
note

In this example, we are forcing a synchronization on the main thread to update and handle our data later in the MonoBehaviour::Update call. The workflow Creating a jobified client and server shows you how to use the Transport package with the C# Job System.

The first thing we want to do, after you have updated your m_Driver, is to handle your connections. Start by cleaning up any old stale connections from the list before processing any new ones. This cleanup ensures that, when we iterate through the list to check what new events we have gotten, we dont have any old connections laying around.

Inside the "Clean up connections" block below, we iterate through our connection list and just simply remove any stale connections.

    // Clean up connections
for (int i = 0; i < m_Connections.Length; i++)
{
if (!m_Connections[i].IsCreated)
{
m_Connections.RemoveAtSwapBack(i);
--i;
}
}

Under "Accept new connections" below, we add a connection while there are new connections to accept.

    // Accept new connections
NetworkConnection c;
while ((c = m_Driver.Accept()) != default(NetworkConnection))
{
m_Connections.Add(c);
Debug.Log("Accepted a connection");
}

Now we have an up-to-date connection list. You can now start querying the driver for events that might have happened since the last update.

    DataStreamReader stream;
for (int i = 0; i < m_Connections.Length; i++)
{
if (!m_Connections[i].IsCreated)
continue;

Begin by defining a DataStreamReader. This will be used in case any Data event was received. Then we just start looping through all our connections.

For each connection we want to call PopEventForConnection while there are more events still needing to get processed.

    NetworkEvent.Type cmd;
while ((cmd = m_Driver.PopEventForConnection(m_Connections[i], out stream)) != NetworkEvent.Type.Empty)
{
note

There is also the NetworkEvent.Type PopEvent(out NetworkConnection con, out DataStreamReader slice) method call, that returns the first available event, the NetworkConnection that its for and possibly a DataStreamReader.

We are now ready to process events. Lets start with the Data event.

    if (cmd == NetworkEvent.Type.Data)
{

Next, we try to read a uint from the stream and output what we have received:

    uint number = stream.ReadUInt();
Debug.Log("Got " + number + " from the Client adding + 2 to it.");

When this is done we simply add two to the number we received and send it back. To send anything with the NetworkDriver we need a instance of a DataStreamWriter. A DataStreamWriter is a new type that comes with the com.unity.transport package. You get a DataStreamWriter when you start sending a message by calling BeginSend.

After you have written your updated number to your stream, you call the EndSend method on the driver and off it goes:

    number +=2;

m_Driver.BeginSend(NetworkPipeline.Null, m_Connections[i], out var writer);
writer.WriteUInt(number);
m_Driver.EndSend(writer);
}
note

We are passing NetworkPipeline.Null to the BeginSend function. This way we say to the driver to use the unreliable pipeline to send our data. it's also possible to not specify a pipeline.

Finally, you need to handle the disconnect case. This is pretty straight forward, if you receive a disconnect message you need to reset that connection to a default(NetworkConnection). As you might remember, the next time the Update loop runs you will clean up after yourself.

                else if (cmd == NetworkEvent.Type.Disconnect)
{
Debug.Log("Client disconnected from server");
m_Connections[i] = default(NetworkConnection);
}
}
}
}

That is the whole server. See ServerBehaviour.cs for the full source code.

Creating a Client

The client code looks pretty similar to the server code at first glance, but there are a few subtle differences. This part of the workflow covers the differences between them, and not so much the similarities.

ClientBehaviour.cs

You still define a NetworkDriver but instead of having a list of connections we now only have one. There is a Done flag to indicate when we are done, or in case you have issues with a connection, you can exit quickly.

Filename: Assets\Scripts\ClientBehaviour.cs

using ...

public class ClientBehaviour : MonoBehaviour {

public NetworkDriver m_Driver;
public NetworkConnection m_Connection;
public bool Done;

void Start () { ... }
public void OnDestroy() { ... }
void Update() { ... }
}

Creating and Connecting a Client

Start by creating a driver for the client and an address for the server.

void Start () {
m_Driver = NetworkDriver.Create();
m_Connection = default(NetworkConnection);

var endpoint = NetworkEndPoint.LoopbackIpv4;
endpoint.Port = 9000;
m_Connection = m_Driver.Connect(endpoint);
}

Then call the Connect method on your driver.

Cleaning up this time is a bit easier because you don’t need a NativeList to hold your connections, so it simply just becomes:

public void OnDestroy()
{
m_Driver.Dispose();
}

Client Update loop

You start the same way as you did in the server by calling m_Driver.ScheduleUpdate().Complete(); and make sure that the connection worked.

void Update()
{
m_Driver.ScheduleUpdate().Complete();

if (!m_Connection.IsCreated)
{
if (!Done)
Debug.Log("Something went wrong during connect");
return;
}

You should recognize the code below, but if you look closely you can see that the call to m_Driver.PopEventForConnection was switched out with a call to m_Connection.PopEvent. This is technically the same method, it just makes it a bit clearer that you are handling a single connection.

    DataStreamReader stream;
NetworkEvent.Type cmd;
while ((cmd = m_Connection.PopEvent(m_Driver, out stream)) != NetworkEvent.Type.Empty)
{

Now you encounter a new event you haven't seen yet: a NetworkEvent.Type.Connect event. This event tells you that you have received a ConnectionAccept message and you are now connected to the remote peer.

note

In this case, the server that is listening on port 9000 on NetworkEndPoint.LoopbackIpv4 is more commonly known as 127.0.0.1.

    if (cmd == NetworkEvent.Type.Connect)
{
Debug.Log("We are now connected to the server");

uint value = 1;
m_Driver.BeginSend(m_Connection, out var writer);
writer.WriteUInt(value);
m_Driver.EndSend(writer);
}

When you establish a connection between the client and the server, you send a number (that you want the server to increment by two). The use of the BeginSend / EndSend pattern together with the DataStreamWriter, where we set value to one, write it into the stream, and finally send it out on the network.

When the NetworkEvent type is Data, as below, you read the value back that you received from the server and then call the Disconnect method.

note

A good pattern is to always set your NetworkConnection to default(NetworkConnection) to avoid stale references.

    else if (cmd == NetworkEvent.Type.Data)
{
uint value = stream.ReadUInt();
Debug.Log("Got the value = " + value + " back from the server");
Done = true;
m_Connection.Disconnect(m_Driver);
m_Connection = default(NetworkConnection);
}

Lastly, we need to handle potential server disconnects:


else if (cmd == NetworkEvent.Type.Disconnect)
{
Debug.Log("Client got disconnected from server");
m_Connection = default(NetworkConnection);
}
}
}

See ClientBehaviour.cs for the full source code.

Putting it all together

To take this for a test run, you can add a new empty GameObject to our Scene.

GameObject Added

Add add both of our behaviours to it:

Inspector

Click Play. Five log messages should load in your Console window:

Console