Basic4GL, Copyright (C) 2003-2016 Tom Mulgrew
Network engine guide
29-Apr-2016
Tom Mulgrew
Basic4GL does not support regular TCP/IP networking, or common protocols such as ftp or http (although some of these have been implemented by 3rd party plugins).
The Basic4GL network routines use their own custom protocol, built on top of UDP/IP. You can use it to write programs that network with other Basic4GL programs, but not (for example) to write an FTP client.
Please be aware that IP v6 is not supported at this time.
The Basic4GL network engine is designed primarily for writing low latency networked games. It allows you to establish a network connection between two running Basic4GL programs and send blocks of data (which we call "messages") back and forth. Basic4GL will attempt to send these messages as quickly as possible (with some extra logic for reliability and/or ordering if required).
And that's all it does.
It is up to you to decide what data to send, how to send it, and how often. But with a bit of planning it is possible to write some responsive multiplayer lan or internet games without too much fuss.
The network engine uses UDP/IP packets for communication. Any network that can do TCP/IP can do UDP/IP (as TCP is built on top of UDP), so your programs can run over the Internet and TCP/IP local networks.
Basic4GL uses its own protocol for handling connection lifetime and reliable packet delivery which is optimised towards writing responsive networked games. It lets you choose which messages must get through and which ones don't matter if they get lost on the way. You can also choose which messages must arrive in the same order they were sent and which ones don't matter so much. This upshot of this is that a carefully designed application will have the best chance to be able to continue smoothly if a data packet is lost in transmission, which is important for realtime games. (Unlike TCP/IP which has to stop for a few seconds if it hits an error). The downside is because the Basic4GL network engine uses its own protocol it can only talk to other applications using the same protocol, i.e. other Basic4GL programs.
The network engine supports:
The bulk of a program's network code usually involves:
A network message is a block of data, similar to a small disk file. In fact Basic4GL uses the file I/O functions to read and write the contents of network messages. Instead of using OpenFileWrite and OpenFileRead, you use SendMessage and ReceiveMessage, but otherwise it's just like accessing a disk file.
Compare this program to write a simple text file:
dim file
file = OpenFileWrite("files\test.txt") ' Open a file for output
WriteString(file, "Some text") ' Write some text
CloseFile(file) ' Close the file
With this program to send a message over a network connection:
dim msg
...
msg = SendMessage(connection) ' Create a message to send down connection
WriteString(msg, "Some text") ' Write some text
CloseFile(msg) ' Send the message
(Note: The above program is incomplete...)
All of Basic4GL's file I/O functions except "OpenFileRead" and "OpenFileWrite" can be used with Basic4GL network messages. These functions are described in the File I/O section of the Basic4GL Programmer's Guide.
Two connect two computer over a network, you must do the following:
At this point both the client and the server have a "connection" with which they can send and receive data. Data sent down the server's connection will be received by the client's connection and vice versa.
(Note: This can be extended to connect multiple computers together, by having one as the server and having the rest of them as clients that connect to the server. In this case the server will have multiple "connection"s, one for each client.)
Format:
NewServer(port)
Where port is the port number on which the server "listen"s for
connection requests.
NewServer() creates a server and returns a handle to
identify the server to other functions (such as AcceptConnection()).
Example:
dim server
server = NewServer(8000) ' Create a new server on port 8000
' ... Run the program
DeleteServer(server) ' Close and delete the server
Format:
DeleteServer(server)
Where server is a server handle returned from NewServer().
Shuts
down and deletes the server. Any connections accepted by the server will
automatically be disconnected and deleted.
It is good practice to close server objects (and connections) when finished
with them.
If not closed explicitly, Basic4GL will automatically close them
when the program ends.
Format:
ConnectionPending(server)
Where server is a server handle returned from NewServer().
ConnectionPending() returns true if a client has asked for a connection to
the server and is waiting for the server to accept or reject it.
The
connection can now be accepted with AcceptConnection() or rejected with
RejectConnection().
Format:
AcceptConnection(server)
Where server is a server handle returned from NewServer().
AcceptConnection() accepts a pending connection request, creates a
corresponding connection object and returns a handle for it.
If no connection
is pending, AcceptConnection() does nothing and returns 0.
Example:
const port = 8000
dim server, connection
' Create server
server = NewServer(port)
printr "Server created. Waiting for connections"
' Wait for incoming connections
while true
if ConnectionPending(server) then
printr "Connection accepted"
' Accept connection
connection = AcceptConnection(server)
' ... Do something here
Sleep(1000)
' Close connection now that we're finished
DeleteConnection(connection)
endif
wend
Format:
RejectConnection(server)
Where server is a server handle returned from NewServer().
Rejects an incoming connection request.
It is good practice to reject any pending connections when you know that you cannot handle them - for example if your server already has all the connections it can handle. The client connection will disconnect immediately rather than wait and eventually timeout from receiving no response.
Rejecting the connection also removes it from the pending connection queue, so if your server is able to accept connections again later on it will not see the old pending connection request.
Format:
NewConnection(address, port)
Creates a new connection and attempts to connect to a server at the specified
address and port.
address is a text string specifying the network
name to connect to. It can either be a DNS address (e.g. "someserver.com"), a
numeric IP address (e.g. "192.168.0.1") or "localhost" (meaning connect to the
same computer).
port is the port number. It must be the same one as
the server is listening on, otherwise it wont find the server.
NewConnection() returns a handle identifying the connection that can be passed to other functions (such as SendMessage()).
Format:
DeleteConnection(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
Deletes a network connection.
If the connection is active, it will be
closed, and a notification sent to the corresponding connection at the other end
to inform it of the close.
Basic4GL also automatically closes and deletes any outstanding network connections when the program finishes.
Programs should monitor the "connection state" of their connections, especially to detect whether the connection has become disconnected, which happens if either end call DeleteConnection(), or if the connection has been disconnected somehow.
Client connections follow this pattern:
Or if the connection is not accepted:
Server accepted connections are similar to client connections, except they are never in the "Handshaking" state (the connection is considered connected as soon as AcceptConnection() is called):
The following functions are used to detect the different states.
Format:
ConnectionConnected(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
ConnectionConnected() returns true if the connection is still connected, or
false if the connection has been disconnected.
Connections are considered
"connected" when they are created, and remain that way until either:
Format:
ConnectionHandshaking(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
Returns true if the connection is in the hand-shaking state.
Connections created by NewConnection() are considered to be "hand-shaking"
until the server accepts the connection (and the confirmation notification is
received).
Once the connection is established, it leaves the hand-shaking
state (ConnectionHandshaking() will then return false), and the connection is
ready to send and receive messages.
Note: Server connections created with AcceptConnection() do not have a hand-shaking phase. For these ConnectionHandshaking() will always return false, as the connection is fully established as soon as it has been accepted.
Format:
ConnectionAddress(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
Returns the IP address of the computer at the other end of the network connection, in numeric format (e.g. "192.168.0.1").
Data is passed through connections as "messages", variable length blocks of data which are transmitted and received as a single item.
Format:
SendMessage(connection, channel, reliable, smoothed)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
SendMessage() creates a message ready to be sent down connection,
and returns a handle representing the message.
You can then pass this handle
to the Write...() file I/O functions (WriteByte(), WriteString(), etc) to write
data to the message, just as you would write data to a file. Refer to
the file I/O functions in the Basic4GL Programmers' Guide
for more information.
Once the message is ready, call CloseFile() to close the message and send it.
SendMessage() has 3 options which affect message delivery:
Channel is a "channel number" and affects the order in which messages are received.
Depending on network conditions messages can arrive at the receiving end in a different order than which they were sent. For example a message (or part of a message) may be lost in transmission and have to be resent, delaying it long enough for other messages to get in infront of it.
The Basic4GL network engine supports ordering of messages through "channels". Every connection has 32 channels (numbered 0 through 31 inclusive). Messages sent within a single channel are guaranteed to be received in the same order as they were sent - with the exception of channel # 0 which is the unordered channel.
Two messages sent down different channels are not guaranteed to be received in the same order.
The multiple channels to allow you to specify for which messages the ordering is important. A good choice of channels can affect network performance, especially over unreliable networks (such as an internet connection). If an ordered message is delayed, the whole channel will stall until the message is received and slotted into its correct order. However other channels will still keep receive messages. So if a game was using on ordered channel for chat messages, and a different channel for position updates, the engine can keep receiving position updates even if a chat message is lost and must be re-transmitted.
Reliable is true if the message must be delivered.
Depending on network conditions, some messages may be lost in transmission. The reliable flag specifies whether this is acceptable for this message (reliable = false) or whether the message must get through, in which case the network engine will keep resending the packet until delivery is confirmed.
Packet "smoothing" attempts to smooth out the network lag by measuring the average amount of time it takes for packets to arrive, and occasionally delaying early arriving packets before releasing them to the application.
This can result in a smoother experience (especially for messages conveying position updates and game events), but be aware that it does add extra latency to some packets.
Format:
MessagePending(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
MessagePending() returns true if a message has been received and can be fetched with ReceiveMessage().
Format:
MessageChannel(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
MessageChannel() returns the channel number of the pending message. (See SendMessage() for more information).
Format:
MessageReliable(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
MessageReliable() returns whether the pending message was sent as a reliable message (MessageReliable() = true) or as an unreliable message. (See SendMessage() for more information).
Format:
MessageSmoothed(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
MessageSmoothed() returns whether the pending message was sent as a smoothed message (MessageSmoothed() = true) or not. (See SendMessage() for more information).
Format:
ReceiveMessage(connection)
Where connection is a connection handle returned by NewConnection() or AcceptConnection().
ReceivedMessage() fetches the current pending message from the connection and
returns a handle representing the message.
You can then pass this handle to
the Read...() file I/O functions (ReadByte(), ReadChar(), etc) to read data from
the message, just as you would read data from a file. The Seek() and EndOfFile()
functions may also be used. Refer to the file I/O functions in the
Basic4GL Programmers'
Guide for more information.
Once you have finished with the message, you should discard it with CloseFile(), in order to free up resources.
There are two flags which indicate the current connection state of a connection:
When a client connection is created with NewConnection(), connected
and handshaking are both set.
If the connection succeeds,
connected remains set, and handshaking is cleared.
If the
connection fails (either rejected by the server, or times out),
connected is cleared. (handshaking may remain set
though...)
Thus the code to establish a client connection might look something like this:
dim connection, address$, port
' Get connection details
print "Address?:": address$ = input$()
print "Port?:": port = val(input$())
' Attempt to connect to server
printr "Connecting..."
connection = NewConnection(address$, port)
while ConnectionConnected(connection) and ConnectionHandshaking(connection): wend
' Check if succeeded
if ConnectionConnected(connection) then
printr "Connection succeeded"
' Do something with connection
' ...
else
printr "Connection failed"
endif
' Close connection
DeleteConnection(connection)
If you attempt to use a connection while in the handshaking stage the network engine will do it's best to accomodate this. Specifically:
When a server connection is created with AcceptConnection(), connected
is set and handshaking is cleared.
The connection is considered
established and can be used immediately.
Network connections have a number of parameters which affect how they behave and perform in different network conditions. These affect timeouts, automatic resends, timing and also have an effect on the amount of bandwidth used. Often you will not need to configure these parameters as they have defaults should work in a number of different network conditions. However they are available should you need them.
Be careful when adjusting connection settings, as they can cause the network connection to fail if setup incorrectly.
Connection settings can be changed after a connection is created (with NewConnection or AcceptConnection).
Format:
SetConnectionTimeout(milliseconds)
Where milliseconds is the number of milliseconds after which
a connection times out and disconnects if no response is received from the other
side.
The default is 60000 (60 seconds).
Format:
SetConnectionHandshakeTimeout(milliseconds)
Where millisecondsis the number of milliseconds after which
a connection attempt will timeout if no reply is received from the
server.
The default is 10000 (10 seconds).
Format:
SetConnectionKeepAlive(milliseconds)
If the connection has not sent anything for this amount of time it will automatically send a "keep alive" message to let the other end know that it is still connected. This prevents the connection from timing out at the other end.
How often keep alive messages need to be send depends on the connection time-out, as well as the network latency, and packet loss. Setting it to a quarter of the connection time-out (for example), will give the network engine 3 or 4 attempts to get the keep-alive packet through before the connection times out.
If your program already sends a constant stream of network traffic (e.g. position updates in a real time game) then that traffic will keep the connection alive, and explicit "keep-alive" packets are not important.
Format:
SetConnectionReliableResend(milliseconds)
This affects sending of reliable messages. When a reliable messages is sent, the connection will continually send the message until it receives confirmation from the other end that the message has been delivered. This setting controls how long the connection waits before resending the message. The default is 200 (0.2 seconds).
The lower this value is, the less delay there will be when packet loss occurs. However setting the value lower than the ping time will use up extra bandwidth, as a reliable message will be sent twice (or more) before the confirmation notification is received.
Format:
SetConnectionDuplicates(count)
Specifies the number of times each message is duplicated when sent.
The default is 1.
Setting this number higher decreases the likelyhood of
packet loss at the cost of extra bandwidth.
Format:
SetConnectionSmoothingPercentage(percentage)
This setting only affects packets that have been sent with the "smoothing" parameter set to true.
The "smoothing" timing algorithm attempts to add artificial lag such that this percentage of packets arrive on time. The defaut is 80 (percent).
Setting this number lower will decrease artificial lag but decreases "smoothness", whereas setting it higher will increase artificial lag and increase "smoothness".