PHP TCP server


Niels - September 14, 2009
Heads up!    This post was written 12 years ago.

The first thing developed for Mailjoe was its TCP server. There are many examples out there, but we didn’t find any that was multi-protocol, non-blocking and multi-process.

The snippets below are fairly small and self explanatory. Please keep in mind that all of this code worked fine during the Mailjoe beta, but is by no means finished or necessarily suitable for other purposes.

mailjoe.php, the main loop:

set_time_limit (0);
require_once("config.php");
require_once(INCL_PATH.'Daemon.php');

$__daemon_listening = true;
$__daemon_childs    = 0;

declare(ticks = 1);

pcntl_signal(SIGTERM, 'daemon_sig_handler');
pcntl_signal(SIGINT,  'daemon_sig_handler');
pcntl_signal(SIGCHLD, 'daemon_sig_handler');
daemonize();

// Setup POP3 socket
$sockPOP = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sockPOP, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind(        $sockPOP, $listen_address, $listen_port_pop3) or die('Could not bind to POP3 port/address!');
socket_listen(      $sockPOP, $max_clients);
socket_set_nonblock($sockPOP);

// Setup IMAP socket
$sockIMAP = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sockIMAP, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind(        $sockIMAP, $listen_address, $listen_port_imap) or die('Could not bind to IMAP port/address!');
socket_listen(      $sockIMAP, $max_clients);
socket_set_nonblock($sockIMAP);

// Setup SMTP socket
$sockSMTP = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sockSMTP, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind(        $sockSMTP, $listen_address, $listen_port_smtp) or die('Could not bind to SMTP port/address!');
socket_listen(      $sockSMTP, $max_clients);
socket_set_nonblock($sockSMTP);

// After we have privileged ports
posix_setuid($posix_uid);
function debug($s) {
syslog(LOG_INFO, "[".posix_getpid()."] ".$s);
}

while ($__daemon_listening) {

    $newsock = @socket_accept($sockIMAP);
    if ($newsock === false) {
    } elseif ($newsock > 0) {
        daemon_client($sockIMAP, $newsock, "imap");
    } else {
        die(socket_strerror($newsock));
    }

    $newsock = @socket_accept($sockPOP);
    if ($newsock === false) {
    } elseif ($newsock > 0) {
        debug("Accepted POP connection");
        daemon_client($sockPOP, $newsock, "pop");
    } else {
        die(socket_strerror($newsock));
    }

    $newsock = @socket_accept($sockSMTP);
    if ($newsock === false) {
    } elseif ($newsock > 0) {
        daemon_client($sockSMTP, $newsock, "smtp");
    } else {
        die(socket_strerror($newsock));
    }

    usleep(10000);

}

The relevant part from config.php:

define(BASE_PATH,'/home/mailjoe/');
define(INCL_PATH,BASE_PATH.'lib/');
define(TEMP_PATH,'/tmp/');

$listen_address     = '127.0.0.1';
$listen_port_pop3   = 110;
$listen_port_smtp   = 25;
$listen_port_imap   = 143;

As you can see, our process listens on localhost only. We were using Perdition to enable SSL as well as do some basic sanity checks for POP3 and IMAP4. SSLTunnel was used to enable SSL on the SMTP port.

We did initially use PHP’s built-in SSL capabilities but ran into a number of limitations.

daemon.php, which is based on several examples found on php.net:

function daemon_sig_handler($sig) {
    global $__daemon_childs;
    switch($sig) {
        case SIGTERM:
        case SIGINT:
            debug("SIGTERM received");
            exit();
            break;
        case SIGCHLD:
            debug("SIGCHLD received");
            while(pcntl_waitpid(-1, $status, WNOHANG)>0)
                $__daemon_childs--;
            debug("SIGCHLD finished");
            break;
    }
}

function daemon_client($sock, $newsock, $type) {
    global $__daemon_listening, $__daemon_childs;

    while($__daemon_childs >= MAX_CHILD) {
        usleep(10000);
    }

    $pid = pcntl_fork();

    if ($pid == -1) {
        debug("Failed to fork!");
        die();
    } elseif ($pid == 0) {
        $__daemon_listening = false;
        socket_close($sock);
        socket_set_block($newsock);
        $client = new $type($newsock);
        while($client->connected) {
            $sockets[0] = $newsock;
            socket_select($sockets, $write = null, $except = null, $tv_sec = NULL);
            $client->Check();
        }
        socket_close($newsock);
    } else {
        socket_close($newsock);
        $__daemon_childs++;
    }
}

function daemonize() {
    $pid = pcntl_fork();
    if ($pid == -1) {
        debug("Failed to fork!");
        exit();
    } elseif ($pid) {
        exit();
    } else {
        posix_setsid();
        chdir('/');
        umask(0);
        return posix_getpid();
    }
}

Important to note here is that daemon.php tries to create a new instance of $type, which could be POP3, SMTP or IMAP based on the socket used. You’ll have to put your own class here that can handle the connection.