Orolyn Documentation

Developer Guide

Overview

Requirements

  1. Linux x86_64

  2. PHP 8.1+

  3. Datastructures

  4. Eio for File and FileStream

  5. PCNTL

  6. Zlib

Installation

Use Composer to install this package.

# Install Composer
curl -sS https://getcomposer.org/installer | php

Add Orolyn as a dependency:

composer require orolyn/orolyn

To use without composer, you will need require the autoload.php file:

require_once 'path/to/orolyn/autoload.php';

License

This library uses the GNU General Public License 3.0

Concurrent Operations

The Orolyn library uses fibers as its sole means of performing concurrent operations. This is represented in the form of tasks, and the functionality is exposed by a group of functions which interfaces with the TaskScheduler.

The aim of this component, and of the library itself, is to enable performing asynchronous operations in a familiar synchronous fashion.` While PHP has many tools and extensions available for performing normally blocking operations asynchronously, the aim here is to perform these operations in a synchronous fashion, i.e. without the use of callbacks or polling.

Part of this is achieved by designing the provided IO components in such a way so to hide the polling operations, and secondly by employing a task scheduler which loops over fibers.

Creating an asynchronous task

use function Orolyn\Lang\Async;

Async(fn () => var_dump('Hello, World!'));

In this example, we are creating an asynchronous task which outputs “Hello, World!”. This internal function executes immediately, so currently there isn’t any need for it to be asynchronous. However, we can add suspensions to the closure to better demonstrate what the Async function does.

use function Orolyn\Lang\Async;
use function Orolyn\Lang\Suspend;

$task = Async(
    function () {
        var_dump('Hello, World');
        Suspend();
        var_dump('Goodbye, World');
    }
);

var_dump('Something in between');

$task->wait();
Output
string(12) "Hello, World"
string(20) "Something in between"
string(14) "Goodbye, World"

So calling Async will execute the closure up until it hits the first Suspend.Then a call to ->wait or ->getResult will continue the closure until completion, or a call to ->resume will continue until the next suspend.

Running multiple asynchronous tasks

Here is an example of running more than one tasks at once:

Await(
    Async(
        function () {
            var_dump('Human: Hello, World!');
            Suspend();
            var_dump('Human: Goodbye, World!');
        }
    ),
    Async(
        function () {
            var_dump('World: Hello, Human!');
            Suspend();
            var_dump('World: Goodbye, Human!');
        }
    )
);
Output
string(20) "Human: Hello, World!"
string(20) "World: Hello, Human!"
string(22) "Human: Goodbye, World!"
string(22) "World: Goodbye, Human!"

Note

As of writing, nested Await calls will block all other asynchronous tasks when used outside of a task scheduler managed application. Solution coming soon.

We can see that the loop alternates between the closures on suspend.

Using asynchronous tasks for IO operations

So, these have been simple examples, however as mentioned, the rest of this library has been designed to perform synchronous-like operations in such as way so to release control of the current stack when they hit an IO block. For example, a stream which is being read from, might not immediately have available data.

Here we will make 20 consecutive calls to Stackoverflow. Firstly, the setup function which will make the call:

function make_request(string $domain): string
{
    $request = <<<EOF
GET / HTTP/1.0
Host: {$domain}


EOF;

    $socket = new Socket();
    $socket->connect(new DnsEndPoint($domain, 80));
    $socket->write($request);
    $socket->flush();

    $output = '';

    while (!$socket->isEndOfStream()) {
        $output .= $socket->read();
    }

    return $output;
}

Next we will call this function 20 times and measure the time:

$time = microtime(true);
$responses = [];

for ($i = 0; $i < 20; $i++) {
    $responses[] = make_request('stackoverflow.com');
}

var_dump(microtime(true) - $time);

And the time was around half a second:

Output
float(0.5648369789123535)

Next we will perform the socket connection and read/writes 20 times concurrently:

$time = microtime(true);

$tasks = [];

for ($i = 0; $i < 20; $i++) {
    $tasks[] = Async(fn () => make_request('stackoverflow.com'));
}

Await($tasks);

$responses = [];

foreach ($tasks as $task) {
    $responses[] = $task->getResult();
}

var_dump(microtime(true) - $time);

And the time now is much shorter:

Output
float(0.04282999038696289)

Essentially what this provides is a way to communicate with multiple sockets concurrently and without callbacks. Because, the connection, writing, the checking of connection status, and the reading are performed internally with polls which suspend execution of the current task.

Note

This precise example may yield connection errors because we are sending so many requests to Stackoverflow at once. Sorry!

Managed Application

The managed application is a method of running the main stack inside a task, therefore allowing execution of off-shoot tasks without further interaction.

Simply calling Async on the main stack will not result in completion if that closure contains any suspensions unless you call Await() or ->wait() on the task. However within a managed application, the script will continue to execute so long as there are still tasks running.

Todo: Information about the managed application.

Using Sockets

The sockets components provides a simple interface for making TCP connections. The Socket class is itself a stream implementing IInputStream and IOutputStream allowing for varying data types to be written to and read from the connection.

Like with all Orolyn IO interfaces, calls to the socket’s IO bound APIs block execution, but release control of the current task when executed within a concurrent environment.

Making a connection

Connection with sockets are performed by providing the necessary endpoint. For example to connect to an IP address we can use the following example.

Create a netcat server with port 9999:

netcat -k -l 0.0.0.0 9999
use Orolyn\Net\IPAddress;
use Orolyn\Net\IPEndPoint;
use Orolyn\Net\Sockets\Socket;

$socket = new Socket();
$socket->connect(new IPEndPoint(IPAddress::parse('0.0.0.0'), 9999));

$socket->write("Hello, World!\n");

In this example we can also use the StreamWriter:

use Orolyn\IO\StreamWriter;

$writer = new StreamWriter($socket);
$writer->writeLine('Hello, World!');
$socket->flush();

Example:

Server terminal
netcat -k -l 0.0.0.0 9999
Client terminal
php sockets.php
Server output
Hello, World!
Reading data from a socket

We can also read data from the socket. Here is an example of connecting to an HTTP server and making a simple HTTP transaction. For this example we are connecting via a DNS endpoint. To learn more about resolving domain names see documentation on the DnsResolver.

$socket = new Socket();
$socket->connect(new DnsEndPoint('google.com', 80));

$message = <<<EOF
GET / HTTP/1.0
Host: google.com


EOF;

$socket->write($message);
$socket->flush();

$response = '';

while (!$socket->isEndOfStream()) {
    if (0 < $available = $socket->getBytesAvailable()) {
        $response .= $socket->read($available);
    }

    usleep(100);
}

echo $response;
Client terminal
php domain-connect.php
Output
HTTP/1.0 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Sun, 05 Jun 2022 16:55:14 GMT
Expires: Tue, 05 Jul 2022 16:55:14 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

Concurrency

Guides on concurrency with sockets

Running an Http Server

Creating an HTTP server

Concurrency

Guides on concurrency with sockets

Working with Websockets

Resolving domain names

PHP comes with built-in functions for resolving IP addresses from a domain name:

  • gethostbyname

  • gethostbynamel

However the default implementation of these functions block execution. For this reason the Orolyn library includes the DnsResolver class.

Using the DNS Resolver

Calling the static method lookup will attempt to fetch all IP addresses associated with the domain name. Using getAddress on the entry result will fetch the first found IP address:

Example:

use Orolyn\Net\DnsResolver;

$entry = DnsResolver::lookup('google.com');

var_dump($entry->getAddress()?->toString());

Output:

string(14) "142.250.200.14"
Getting all IP addresses

Calling getAddressList will provide a list of all addresses associated with the domain name:

/** @var IList<IPAddress> */
$addresses = $entry->getAddressList();

Getting IP addresses asynchronously

Running multiple searches using the concurrency component enables multiple datagram connections to run without blocking each other, for example below we will fetch the first available IP address from each of the following domains:

use Orolyn\Net\DnsResolver;
use function Orolyn\Lang\Async;
use function Orolyn\Lang\Await;

Await(
    $task1 = Async(fn () => DnsResolver::lookup('google.com')),
    $task2 = Async(fn () => DnsResolver::lookup('stackoverflow.com')),
    $task3 = Async(fn () => DnsResolver::lookup('readthedocs.org')),
);

var_dump('Task 1: ' . $task1->getResult()->getAddress()->toString());
var_dump('Task 2: ' . $task2->getResult()->getAddress()->toString());
var_dump('Task 3: ' . $task3->getResult()->getAddress()->toString());
Output
string(23) "Task 1: 142.250.187.206"
string(22) "Task 2: 151.101.193.69"
string(19) "Task 3: 104.18.7.29"