從源碼說swoole進程間通訊原理

  1. 本文件假設你有c++和多進程編程的基礎知識。
  2. swoole進程間通訊可使用套接字(swoole_process::write/ swoole_process::read),也可使用消息隊列(push/pop)。本文的只講述套接字通訊部分。
  3. 本文使用的swoole源碼爲1.9版本

1. swoole_process中的__construct和start究竟作了什麼

爲了說明swoole進程間是如何使用unix socket進行通訊的,咱們先從源碼入手,看看__construct和start函數究竟作了些什麼。對於源碼,咱們只選取和本問題相關的部分進行解讀。php

1.1 __construct

swoole_process.c 
static PHP_METHOD(swoole_process, __construct)
{
    /*pipe_type便是__constrct的第三參數$create_pipe, 默認爲2*/
    long pipe_type = 2;

    if (pipe_type > 0)
    {
        swPipe *_pipe = emalloc(sizeof(swWorker));
        int socket_type = pipe_type == 1 ? SOCK_STREAM : ,SOCK_DGRAM;
        /*建立pipe, 本質是套接字*/
        if (swPipeUnsock_create(_pipe, 1, socket_type) < 0)
        {
            RETURN_FALSE;
        }

        process->pipe_object = _pipe;
        /*獲取主進程用於讀寫的文件描述符*/
        process->pipe_master = _pipe->getFd(_pipe, SW_PIPE_MASTER);
        /*獲取工做進程用於讀寫的文件描述符*/
        process->pipe_worker = _pipe->getFd(_pipe, SW_PIPE_WORKER); 
        process->pipe = process->pipe_master;
        /*對當前swoole_process對象(php中new出的)的屬性值pipe進行賦值*/
        zend_update_property_long(swoole_process_class_entry_ptr, getThis(), ZEND_STRL("pipe"), process->pipe_master TSRMLS_CC)
}

swPipeUnsock_create作了什麼呢?html

pipe/PipeUnsock.c 
int swPipeUnsock_create(swPipe *p, int blocking, int protocol)
{

    p->blocking = blocking;
    /*建立本地套接字*/
    ret = socketpair(AF_UNIX, protocol, 0, object->socks);
    if (ret < 0)
    {
        swWarn("socketpair() failed. Error: %s [%d]", strerror(errno), errno);
        return SW_ERR;
    }
    else
    {
        /*
         * swoole_config.h:#define SW_SOCKET_BUFFER_SIZE      (8*1024*1024)
         * 由此可知套接字buffer大小爲8M
         */
        int sbsize = SwooleG.socket_buffer_size;
        swSocket_set_buffer_size(object->socks[0], sbsize);
        swSocket_set_buffer_size(object->socks[1], sbsize);
    }

    return 0;
}

1.2 start

swoole_process.c
static PHP_METHOD(swoole_process, start)
{
    swWorker *process = swoole_get_object(getThis());

    /*
     * 建立進程
     * 注意:
     * 1. 建立後父子進程中各有一個swoole_process對象,它們是雙胞胎。
     * 2. 子進程繼承父進程打開的文件描述符,因此以前__construct時建立的套接子,子進程中也有一份
     */

    pid_t pid = fork();
    if (pid < 0)
    {
        swoole_php_fatal_error(E_WARNING, "fork() failed. Error: %s[%d]", strerror(errno), errno);
        RETURN_FALSE;
    }
    /*主進程邏輯*/
    else if (pid > 0)
    {
        process->pid = pid;
        process->child_process = 0;
        zend_update_property_long(swoole_server_class_entry_ptr, getThis(), ZEND_STRL("pid"), process->pid TSRMLS_CC);
        RETURN_LONG(pid);
    }
    /*子進程邏輯*/
    else
    {
        process->child_process = 1;
        SW_CHECK_RETURN(php_swoole_process_start(process, getThis() TSRMLS_CC));
    }
    RETURN_TRUE;
}

子進程邏輯在php_swoole_process_start()中執行,咱們繼續看react

swoole_process.c
int php_swoole_process_start(swWorker *process, zval *object TSRMLS_DC)
{
    /*
     * 設置子進程中的swoole_process用於通訊的描述符
     * 對比前文,主進程中的swoole_process使用的是process->pipe_master
     */
    process->pipe = process->pipe_worker;
    process->pid = getpid();

    /*更新子進程中swoole_process(php對象)的相應屬性*/
    zend_update_property_long(swoole_process_class_entry_ptr, object, ZEND_STRL("pid"), process->pid TSRMLS_CC);
    zend_update_property_long(swoole_process_class_entry_ptr, object, ZEND_STRL("pipe"), process->pipe_worker TSRMLS_CC);
}

可見 
__construt的主要工做是使用socketpair建立一對套接字,並指定主進程中的swoole_process對象用於讀寫的套接字。 
start的主要工做是,建立子進程,設置子進程中的swoole_process對象用於讀寫的套接字。c++

2. 通訊原理

2.1 read/write源碼解讀

swoole_process.c
static PHP_METHOD(swoole_process, read)
{
    /*默認每次讀寫大小*/
    long buf_size = 8192;

    /*上限值*/
    if (buf_size > 65536)
    {
        buf_size = 65536;
    }

    swWorker *process = swoole_get_object(getThis());

    char *buf = emalloc(buf_size + 1);
    /*從進程中保留的套接字中讀取數據*/
    int ret = read(process->pipe, buf, buf_size);;

    /*設置php函數swoole_process->read返回值*/
    SW_ZVAL_STRINGL(return_value, buf, ret, 0);
}
swoole_process.c
static PHP_METHOD(swoole_process, write)
{
    int ret;

    /*如下兩種狀況的本質都是調用write函數向process-pipe中寫入數據*/
    //async write
    if (SwooleG.main_reactor)
    {
        ret = SwooleG.main_reactor->write(SwooleG.main_reactor, process->pipe, data, (size_t) data_len);
    }
    else
    {
        ret = swSocket_write_blocking(process->pipe, data, data_len);
    }

    ZVAL_LONG(return_value, ret);
}

2.2 通訊原理總結

swoole進程間使用套接字通訊的原理以下: 
1. 父進程使用socketpair建立一對套接字 
2. 建立子進程時,子進程繼承了這對套接字 
3. 父子進程使用系統的read,write函數對各自的套接字進行讀寫完成通訊。 
4. 對於多個子進程,父進程實際上是爲每一個子進程建立一對套接字用於通訊。 
5. 子進程之間的通訊,好比A向B發消息,本質是fork A進程時,A從父進程處繼承了向B發消息的套接字,從而完成了向B的通訊。編程

3.關於SOCK_STREAM與SOCK_DGRAM

3.1 手冊中的一個錯誤

手冊中說:默認的方式是流式。但從1.1節的__construct源碼中咱們能夠看到,默認使用的是SOCK_DGRAM方式swoole

3.2 SOCK_STREAM與SOCK_DGRAM的區別

此參數經__construct的第三參數傳入,最終做用於socketpair的protocol字段socket

ret = socketpair(AF_UNIX, protocol, 0, object->socks);

在一般意義來講SOCK_STREAM與SOCK_DGRAM分別用於tcp通訊和udp通訊,前者有序(先發先至),可靠;後者不保證順序及數據可靠性。但在本地套接字中,因爲是本機兩進程通訊,不會涉及數據丟失,亂序等問題。那麼這兩個參數的區別在哪呢? 
下面是我看到的一個很是清晰明瞭的解釋:async

The difference between SOCK_STREAM and SOCK_DGRAM is in the semantics of consuming data out of the socket.tcp

Stream socket allows for reading arbitrary number of bytes, but still preserving byte sequence. In other words, a sender might write 4K of data to the socket, and the receiver can consume that data byte by byte. The other way around is true too - sender can write several small messages to the socket that the receiver can consume in one read. Stream socket does not preserve message boundaries.函數

Datagram socket, on the other hand, does preserve these boundaries - one write by the sender always corresponds to one read by the receiver (even if receiver’s buffer given to read(2) or recv(2) is smaller then that message).

也就是說SOCK_STREAM是流式的,數據沒有消息邊界,發送方屢次寫入的數據,可能讀取方一次就讀取了。發送一次寫入的數據,讀取方可能分屢次纔讀完。回到swoole意味着這種方式下,write與read的次數並非一一對應的。你須要本身設置邊界來切分消息。

SOCK_DGRAM方式,數據自然是有邊界的,讀寫次數必定是一一對應的。回到swoole,意味着這種方式下,只要你的單條消息不超單次讀寫上限(默認8192字節),就不須要自行設置邊界來切分消息。

看一個例子

<?php                                                                                 
$process1 = new swoole_process(function($process){                                    
    $i = 1;                                                                           
    while (true) {                                                                    
        $msg = $process->read();                                                      
        echo $msg,"\n";                                                               
        echo "read $i time\n";                                                        
        $i++;                                                                         
    }                                                                                 
}, false, 1);                                                                         

$num = 10;                                                                            

$process2 = new swoole_process(function($process) use ($process1, $num){              
    for ($i=0; $i<$num; $i++){                                                        
        $msg = $process1->write("hello 1 i'm 2;");                                    
        if ($i % 5 == 0){                                                             
            sleep(1);                                                                 
        }                                                                             
    }                                                                                 
});

$process2->start();                                                                   
$process1->start();

new第三參數設爲1,使用SOCK_STREAM通訊。運行結果以下:

hello 1 i'm 2;
read 1 time
hello 1 i'm 2;hello 1 i'm 2;hello 1 i'm 2;
read 2 time
hello 1 i'm 2;hello 1 i'm 2;
read 3 time
hello 1 i'm 2;hello 1 i'm 2;
read 4 time
hello 1 i'm 2;hello 1 i'm 2;
read 5 time

process2向process1寫了10次數據,process1用了5次讀完 
將new第三參數設爲2,使用SOCK_DGRAM通訊。運行結果以下:

hello 1 i'm 2;
read 1 time
hello 1 i'm 2;
read 2 time
hello 1 i'm 2;
read 3 time
hello 1 i'm 2;
read 4 time
hello 1 i'm 2;
read 5 time
hello 1 i'm 2;
read 6 time
hello 1 i'm 2;
read 7 time
hello 1 i'm 2;
read 8 time
hello 1 i'm 2;
read 9 time
hello 1 i'm 2;
read 10 time

10次寫對應10次讀

相關文章
相關標籤/搜索