Skip to content
Alexander Gordeyko edited this page Oct 3, 2021 · 21 revisions

Net

A library that provides creating net sockets. ##Introduction One of the way to connect two applications is to use Berkeley sockets (or another net sockets based on Berkeley ones). One can create socket-server and socket-client and the library helps to do it in a simple way.

Structure of the library

The library consists of several classes and interfaces: SocketServer, SocketClient, SocketServer/SocketServerBase etc. For those who are not going to develop the library the most important classes are SocketServer and SocketClient representing an interface of the interaction itself. As one may guess by naming, SocketServer class has methods and fields for server socket and SocketClient has methods and fields for client one.

Steps of interaction

There are seven steps of socket interaction and these steps are: creating, binding, listening, connecting/accepting, reading/writing, preparing to close, closing. Let's consider them for sockets to be a client and a server.

Client steps

The first action you have to do with any socket is to create it. As mentioned before, the class representing client socket is SocketClient so to create client you should use constructor,

var client = new SocketClient("127.0.0.1", 60001, -1, true);

The arguments are:

  • first - host IP-address, string;
  • second - port, integer;
  • third - reconnect delay, ms, integer;
  • fourth - flag "isWithLength" indicating if the length of datagramm is added to datagramm itself, boolean.

The first argument contains a server's IP, default value is "127.0.0.1". The second argument is a number of server's port to connect to. The third argument is a time, in milliseconds, the client has to wait for before it is to connect again; it's needed if connection is broken or the process of connecting is failed. -1 means that there is no reconnect at all, default value is -1. The fourth argument is a flag that indicates if the length of a datagramm is sent with the datagramm itself. If true, the datagramm consists of two parts: a data and a four-byte integer being equal to length of data. The default value is true.

It is strongly recommended to use sockets when isWithLength is true only.

Note also, that program will surely crash if client's isWithLength is not equal to server's one.

If all the default values seems to be what you need, you can type just

var client = new SocketClient(60001);

Next step one should make is binding socket to local end point. Although it is necessary to bind server to pair IP/port, there is no need to do client the same way. Anyhow, the Connect method does it automatically so one doesn't need to mess with one's head.

The third step is listening and it relates to server only.

The fourth step of interaction is connecting. The client socket connects to server by using server's host/port - as you remember, we typed these in constructor's arguments. Moreover, the Connect method is in constructor too so you need to do nothing but what has already been done. It is guarenteed, that there will be no data sent or received before connecting ends and client is completely configured.

By calling the constructor you "simultaneously" create the client and connect it to the server.

The fifth and the main step is sending and receiving a data. To send data one should form it by using a BytesOutput class and send by send method. The code is:

var data:BytesOutput = new BytesOutput();
data.writeString("Hello, World!");
client.send(data);

It's pretty simple.

To receive data one should use callback being of BytesInput -> Void type in client's onData signal. For example, server sent a number with double-precision floating-point format. You can receive it this way:

client.onData.add(receiveCallback);

/*Some code.*/

static function receiveCallback(b_in:BytesInput):Void {
    trace(b_in.readDouble());
}

Of course, it is not necessary callback to be static - it is just because used in static main function. You may declare callback inside main like this:

function receiveCallback(b_in:BytesInput):Void {
    trace(b_in.readDouble());
}
client.onData.add(receiveCallback);

But the most comfortable way is using an anonymous function:

client.onData.add(function(b_in:BytesInput) {
    trace(b_in.readDouble());
});

When all the sending/receiving is done, the socket has to be correctly destroyed. There is a Shutdown function to do it the right way, it prepares the socket to destroy. In conclusion, there is a Close function to destroy the socket. Both these functions are in SocketClient's method destroy so all you need to do is to call it:

client.destroy();

It is guaranteed, that sending/receiving process executing when destroy is called ends correctly.

To destroy the client just call the destroy method.

Server steps

The class representing a server is SocketServer. Creating server is even simpler than doing client. The only argument one should set is port:

var server:SocketServer = new SocketServer(60001);

The argument is a number of port the clients to connect to. The considering why there is just one argument lies below.

Notice, that port in server's and client's constructors must be the same.

As noted before, the second step is binding a socket to local end point. Although it was mentioned that binding is necessary for server, the host IP needed to bind to is not a constructor's argument. The reason why is that IP is always 127.0.0.1. Furthermore, there is no need in reconnect so there is no reconnect delay; an isWithLength field can be set by direct access, its meaning is just the same as client's one.

The third step is listening. It is implemented by socket's Listen method, the default count of clients listened simultaneously is one thousand; this value can not be changed. 1000 clients ought to be enough for everybody :-).

The fourth step is accepting the clients. To accept it is used asynchronous methods BeginAccept and EndAccept. All these methods - Bind, Listen, BeginAccept - is in the constructor so by calling it you do all the work needed to prepare the interaction. The EndAccept method is called in accept callback.

There is also a signal misleadingly named onConnect that allows one to do something when accepting is done. It is of SocketClient -> Void type and it can be used this way:

server.onConnect.add(function(cl:SocketClient) {
    /*Some code.*/
});

Of course, you may use not only anonymous function as considered before.

To prepare a server to interact one doesn't need to do anything but calling the constructor.

The sending/receiving process is similar to client's one. For example, you can use an onConnect signal to send a data to accepted client. As noted before, when accepting becomes finished, the signal dispatches; the argument of function using here is a socket corresponding to accepted client. It means that you can send a data this way:

server.onConnect.add(function(cl:SocketClient) {
    var b_out:BytesOutput = new BytesOutput();
    b_out.writeDouble(23.23);
    cl.send(b_out);
});

And the data will be sent to the client accepted. On the other hand, you can also use server's method send that provides sending to all the clients:

var b_out:BytesOutput = new BytesOutput();
b_out.writeDouble(23.23);
server.send(b_out);

Or you can use send2other method which sends data to all the clients except chosen one. The using looks like this:

server.onConnect.add(function(cl:SocketClient) {
    var b_out:BytesOutput = new BytesOutput();
    b_out.writeDouble(23.23);
    server.send2other(b_out, cl);
});

To receive the data one can use an onData signal the same way as client. Let the client send the string "Hello, World!", then to receive it type:

server.onData.add(function(b_in:BytesInput) {
    trace(b_in.readString("Hello, World!".length));
});

The sixth and seventh steps are just the same as in a client. To destroy server correctly one should just call

server.destroy();

and it will do all the work needed.

##Examples Let's consider a simple example. We need a client and a server, the client will connect to the server, the server will send a data to the client when it gets accepted, the client will do to the server.

    package ;

    import haxe.io.BytesInput;
    import haxe.io.BytesOutput;
    import pony.net.SocketClient;
    import pony.net.SocketServer;


    class Main 
    {
	
	static function main() 
	{
		var server:SocketServer = new SocketServer(60001);
		var client:SocketClient = new SocketClient(60001);
		server.onConnect.add(function(cl:SocketClient)
		{
			var b_out:BytesOutput = new BytesOutput();
			b_out.writeDouble(23.23);
			cl.send(b_out);
		});
		client.onData.add(function(b_in:BytesInput)
		{
			trace("Server says: " + b_in.readDouble() + ".");
			var b_out:BytesOutput = new BytesOutput();
			b_out.writeDouble(34.34);
			client.send(b_out);
		});
		server.onData.add(function(b_in:BytesInput)
		{
			trace("Client says: " + b_in.readDouble() + ".");
		});
		Sys.getChar(false);
	}
    }

The first thing you should note is that exactly pony.net.SocketClient/Server imported, neither pony.net.cs.SocketClient/Server nor pony.net.nodejs.SocketClient/Server nor anything another one.

The output is:

Main.hx:27: Server says: 23,23.
Main.hx:34: Client says: 34,34.

The two first strings are:

var server:SocketServer = new SocketServer(60001);
var client:SocketClient = new SocketClient(60001);

Here the server and the client is created, the client also starts to connect and the server starts to accept.

server.onConnect.add(function(cl:SocketClient) {
    var b_out:BytesOutput = new BytesOutput();
    b_out.writeDouble(23.23);
    cl.send(b_out);
});

Here we define the callback which is called when server accepts a client.

client.onData.add(function(b_in:BytesInput) {
    trace("Server says: " + b_in.readDouble() + ".");
    var b_out:BytesOutput = new BytesOutput();
    b_out.writeDouble(34.34);
    client.send(b_out);
});

The defining of the callback which is called when client receives a data.

server.onData.add(function(b_in:BytesInput) {
    trace("Client says: " + b_in.readDouble() + ".");
});

In conclusion, the defining of the callback which is called when server receives a data.

Sys.getChar(false);

Don't forget to stop for asynchronous sockets to finish their executing before the application does.

The example a little more complicated. We have now five clients, and the server tells about connecting client to all the clients. The code is:

    package ;

    import haxe.io.BytesInput;
    import haxe.io.BytesOutput;
    import pony.net.SocketClient;
    import pony.net.SocketServer;

    class Main 
    {
	
	static function main() 
	{
		var server:SocketServer = new SocketServer(60001);
		server.onData.add(function(b_in:BytesInput)
		{
			var num = b_in.readInt16();
			var b_out:BytesOutput = new BytesOutput();
			b_out.writeInt16(num);
			server.send(b_out);
		});
		for (i in 0...5) 
		{
			var client:SocketClient = new SocketClient(60001);
			client.onData.add(function(b_in:BytesInput)
			{
				var num:Int = b_in.readInt16();
				if (num == i) trace("Client # " + i + "says: Client # " + num + " connected. It's me!") 
				else trace("Client # " + i + "says: Client # " + num + " connected.");
			});
			var b_out:BytesOutput = new BytesOutput();
			b_out.writeInt16(i);
			client.send(b_out);
		}
		
		Sys.getChar(false);
    }
    }

The output is:

Main.hx:31: Client # 0says: Client # 0 connected. It's me!
Main.hx:32: Client # 0says: Client # 1 connected.
Main.hx:32: Client # 0says: Client # 2 connected.
Main.hx:31: Client # 2says: Client # 2 connected. It's me!
Main.hx:31: Client # 1says: Client # 1 connected. It's me!
Main.hx:32: Client # 0says: Client # 3 connected.
Main.hx:32: Client # 1says: Client # 2 connected.
Main.hx:32: Client # 2says: Client # 3 connected.
Main.hx:31: Client # 3says: Client # 3 connected. It's me!
Main.hx:32: Client # 1says: Client # 3 connected.
Main.hx:32: Client # 1says: Client # 4 connected.
Main.hx:32: Client # 0says: Client # 4 connected.
Main.hx:32: Client # 2says: Client # 4 connected.
Main.hx:32: Client # 3says: Client # 4 connected.
Main.hx:31: Client # 4says: Client # 4 connected. It's me!

All the actions are just the same as in previous example.

The example with reconnect. We have client trying to connect to server while it doesn't exist. The code is:

    package ;

    import haxe.io.BytesInput;
    import haxe.io.BytesOutput;
    import pony.net.SocketClient;
    import pony.net.SocketServer;
    import pony.time.Timer;

    class Main 
    {
	
	static function main() 
	{
		var server:SocketServer = null;
		var client:SocketClient = new SocketClient(60001, 500);
		Timer.delay(500, function() 
		{
			server = new SocketServer(60001);
			server.onConnect.add(function(cl:SocketClient)
			{
				trace("Client successfully connected.");
			});
		});
		Sys.getChar(false);
	}
    }

Firstly note, that we use pony.time.Timer, it helps us to create a server. We have "empty" server,

var server:SocketServer = null;

and want the client to reconnect every 500 ms,

var client:SocketClient = new SocketClient(60001, 500);

Then we create "non-empty" server using Timer,

    Timer.delay(500, function() 
		{
			server = new SocketServer(60001);
			server.onConnect.add(function(cl:SocketClient)
			{
				trace("Client successfully connected.");
			});
		});

The output is:

SocketClientBase.hx:89: Reconnect after 500 ms
Main.hx:25: Client successfully connected.

##Links

Since the C# sockets seems to be the most uncorrect one, I decided to list these links. They may be useful for those who face some troubles using C# sockets and, being a desperado, decide to solve them.