前言
swoole_client
提供了 tcp/udp socket
的客戶端的封裝程式碼,使用時僅需 new swoole_client
即可。 swoole
的 socket client
對比 PHP
提供的 stream
族函式有哪些好處:
-
stream
函式存在超時設定的陷阱和Bug
,一旦沒處理好會導致Server
端長時間阻塞 -
fread
有8192
長度限制,無法支援UDP
的大包 -
swoole_client
支援waitall
,在有確定包長度時可一次取完,不必迴圈讀取 -
swoole_client
支援UDP connect
,解決了UDP
串包問題 -
swoole_client
是純C
的程式碼,專門處理socket
,stream
函式非常複雜。swoole_client
效能更好
除了普通的同步阻塞+select
的使用方法外,swoole_client
還支援非同步非阻塞回撥。
swoole_client::__construct
建構函式
建構函式的邏輯很簡單,就是更新 swoole_client_class_entry_ptr
指標的 type
屬性與 key
屬性。
當 type
是非同步客戶端的時候,不能在 CLI
模式下使用 SWOOLE_KEEP
。
#define php_swoole_socktype(type) (type & (~SW_FLAG_SYNC) & (~SW_FLAG_ASYNC) & (~SW_FLAG_KEEP) & (~SW_SOCK_SSL))
static PHP_METHOD(swoole_client, __construct)
{
long async = 0;
long type = 0;
char *id = NULL;
zend_size_t len = 0;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l|ls", &type, &async, &id, &len) == FAILURE)
{
swoole_php_fatal_error(E_ERROR, "socket type param is required.");
RETURN_FALSE;
}
if (async == 1)
{
type |= SW_FLAG_ASYNC;
}
if ((type & SW_FLAG_ASYNC))
{
if ((type & SW_FLAG_KEEP) && SWOOLE_G(cli))
{
swoole_php_fatal_error(E_ERROR, "The `SWOOLE_KEEP` flag can only be used in the php-fpm or apache environment.");
}
php_swoole_check_reactor();
}
int client_type = php_swoole_socktype(type);
if (client_type < SW_SOCK_TCP || client_type > SW_SOCK_UNIX_STREAM)
{
swoole_php_fatal_error(E_ERROR, "Unknown client type `%d`.", client_type);
}
zend_update_property_long(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("type"), type TSRMLS_CC);
if (id)
{
zend_update_property_stringl(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("id"), id, len TSRMLS_CC);
}
else
{
zend_update_property_null(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("id") TSRMLS_CC);
}
//init
swoole_set_object(getThis(), NULL);
swoole_set_property(getThis(), client_property_callback, NULL);
#ifdef SWOOLE_SOCKETS_SUPPORT
swoole_set_property(getThis(), client_property_socket, NULL);
#endif
RETURN_TRUE;
}
swoole_client->set
屬性設定
設定客戶端引數,必須在 connect
前執行。本函式用於從 zend
中讀出 client
的屬性,並且和使用者的配置引數合併。
static PHP_METHOD(swoole_client, set)
{
zval *zset;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
if (Z_TYPE_P(zset) != IS_ARRAY)
{
RETURN_FALSE;
}
zval *zsetting = php_swoole_read_init_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("setting") TSRMLS_CC);
sw_php_array_merge(Z_ARRVAL_P(zsetting), Z_ARRVAL_P(zset));
RETURN_TRUE;
}
static sw_inline zval* php_swoole_read_init_property(zend_class_entry *scope, zval *object, const char *p, size_t pl TSRMLS_DC)
{
zval *property = sw_zend_read_property(scope, object, p, pl, 1 TSRMLS_CC);
if (property == NULL || ZVAL_IS_NULL(property))
{
SW_MAKE_STD_ZVAL(property);
array_init(property);
zend_update_property(scope, object, p, pl, property TSRMLS_CC);
sw_zval_ptr_dtor(&property);
return sw_zend_read_property(scope, object, p, pl, 1 TSRMLS_CC);
}
else
{
return property;
}
}
swoole_client->on
註冊非同步事件回撥函式
註冊非同步事件回撥函式,函式主要更新 swoole_client_class_entry_ptr
中的各個屬性,並將其屬性回撥函式賦值給 client_property_callback
當中。
static PHP_METHOD(swoole_client, on)
{
char *cb_name;
zend_size_t cb_name_len;
zval *zcallback;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &cb_name, &cb_name_len, &zcallback) == FAILURE)
{
return;
}
zval *ztype = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), SW_STRL("type")-1, 0 TSRMLS_CC);
client_callback *cb = (client_callback *) swoole_get_property(getThis(), client_property_callback);
if (!cb)
{
cb = (client_callback *) emalloc(sizeof(client_callback));
bzero(cb, sizeof(client_callback));
swoole_set_property(getThis(), client_property_callback, cb);
}
if (strncasecmp("connect", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onConnect"), zcallback TSRMLS_CC);
cb->onConnect = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onConnect"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onConnect, cb->_onConnect);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onConnect = func_cache;
#endif
}
else if (strncasecmp("receive", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onReceive"), zcallback TSRMLS_CC);
cb->onReceive = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onReceive"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onReceive, cb->_onReceive);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onReceive = func_cache;
#endif
}
else if (strncasecmp("close", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onClose"), zcallback TSRMLS_CC);
cb->onClose = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onClose"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onClose, cb->_onClose);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onClose = func_cache;
#endif
}
else if (strncasecmp("error", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onError"), zcallback TSRMLS_CC);
cb->onError = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onError"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onError, cb->_onError);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onError = func_cache;
#endif
}
else if (strncasecmp("bufferFull", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onBufferFull"), zcallback TSRMLS_CC);
cb->onBufferFull = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onBufferFull"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onBufferFull, cb->_onBufferFull);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onBufferFull = func_cache;
#endif
}
else if (strncasecmp("bufferEmpty", cb_name, cb_name_len) == 0)
{
zend_update_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onBufferEmpty"), zcallback TSRMLS_CC);
cb->onBufferEmpty = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("onBufferEmpty"), 0 TSRMLS_CC);
sw_copy_to_stack(cb->onBufferEmpty, cb->_onBufferEmpty);
#ifdef PHP_SWOOLE_ENABLE_FASTCALL
cb->cache_onBufferEmpty = func_cache;
#endif
}
else
{
swoole_php_fatal_error(E_WARNING, "Unknown event callback type name `%s`.", cb_name);
RETURN_FALSE;
}
RETURN_TRUE;
}
swoole_client->connect
connect
connect
是客戶端模組核心的函式。
-
PHP
內部函式使用zend_parse_parameters() API
接受引數,將輸入引數轉換成c
變數。不幸的是,每次呼叫這個函式時都要對這個這個字串進行解析,這會加重效能開銷。在PHP7中新提供的方式。是為了提高引數解析的效能。對應經常使用的方法,建議使用FAST ZPP
方式。 - 利用
php_swoole_client_new
函式建立一個swClient
客戶端物件 - 如果是非同步的
TCP
客戶端,設定sock_flag
為 1,為後面的非同步connect
的引數 - 如果客戶端當前狀態是啟用而且是保持長連線的,直接返回成功,不需要再次連線。
- 根據配置引數,利用函式
php_swoole_client_check_setting
設定swClient
物件的屬性 -
如果客戶端是非同步的:
- 如果是
TCP
客戶端,需要驗證是否設定了onConnect
、onError
、onClose
三個回撥函式,如果沒有返回錯誤。onBufferFull
、onBufferEmpty
是可選回撥函式。 - 如果是
UDP
客戶端,需要驗證是否設定了onReceive
等函式,否則返回錯誤。onConnect
、onClose
兩個函式是可選回撥函式。 - 值得注意的是,
swClient
中的onConnect
等函式並沒有直接使用使用者的回撥函式,而是使用client_onConnect
等函式,將使用者的回撥函式放在了cli->object
的屬性中。
- 如果是
- 最後呼叫
connect
函式與服務端進行連線。
static PHP_METHOD(swoole_client, connect)
{
zend_long port = 0, sock_flag = 0;
char *host = NULL;
zend_size_t host_len;
double timeout = SW_CLIENT_DEFAULT_TIMEOUT;
#ifdef FAST_ZPP
ZEND_PARSE_PARAMETERS_START(1, 4)
Z_PARAM_STRING(host, host_len)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(port)
Z_PARAM_DOUBLE(timeout)
Z_PARAM_LONG(sock_flag)
ZEND_PARSE_PARAMETERS_END();
#else
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|ldl", &host, &host_len, &port, &timeout, &sock_flag) == FAILURE)
{
return;
}
#endif
swClient *cli = (swClient *) swoole_get_object(getThis());
cli = php_swoole_client_new(getThis(), host, host_len, port);
swoole_set_object(getThis(), cli);
if (cli->type == SW_SOCK_TCP || cli->type == SW_SOCK_TCP6)
{
if (cli->async == 1)
{
//for tcp: nonblock
//for udp: have udp connect
sock_flag = 1;
}
}
if (cli->keep == 1 && cli->socket->active == 1)
{
zend_update_property_bool(swoole_client_class_entry_ptr, getThis(), SW_STRL("reuse")-1, 1 TSRMLS_CC);
RETURN_TRUE;
}
else if (cli->socket->active == 1)
{
swoole_php_fatal_error(E_WARNING, "connection to the server has already been established.");
RETURN_FALSE;
}
zval *zset = sw_zend_read_property(swoole_client_class_entry_ptr, getThis(), ZEND_STRL("setting"), 1 TSRMLS_CC);
if (zset && !ZVAL_IS_NULL(zset))
{
php_swoole_client_check_setting(cli, zset TSRMLS_CC);
}
//nonblock async
if (cli->async)
{
client_callback *cb = (client_callback *) swoole_get_property(getThis(), 0);
if (!cb)
{
swoole_php_fatal_error(E_ERROR, "no event callback function.");
RETURN_FALSE;
}
if (swSocket_is_stream(cli->type))
{
if (!cb->onConnect)
{
swoole_php_fatal_error(E_ERROR, "no `onConnect` callback function.");
RETURN_FALSE;
}
if (!cb->onError)
{
swoole_php_fatal_error(E_ERROR, "no `onError` callback function.");
RETURN_FALSE;
}
if (!cb->onClose)
{
swoole_php_fatal_error(E_ERROR, "no `onClose` callback function.");
RETURN_FALSE;
}
cli->onConnect = client_onConnect;
cli->onClose = client_onClose;
cli->onError = client_onError;
cli->onReceive = client_onReceive;
cli->reactor_fdtype = PHP_SWOOLE_FD_STREAM_CLIENT;
if (cb->onBufferFull)
{
cli->onBufferFull = client_onBufferFull;
}
if (cb->onBufferEmpty)
{
cli->onBufferEmpty = client_onBufferEmpty;
}
}
else
{
if (!cb || !cb->onReceive)
{
swoole_php_fatal_error(E_ERROR, "no `onReceive` callback function.");
RETURN_FALSE;
}
if (cb->onConnect)
{
cli->onConnect = client_onConnect;
}
if (cb->onClose)
{
cli->onClose = client_onClose;
}
cli->onReceive = client_onReceive;
cli->reactor_fdtype = PHP_SWOOLE_FD_DGRAM_CLIENT;
}
zval *zobject = getThis();
cli->object = zobject;
sw_copy_to_stack(cli->object, cb->_object);
sw_zval_add_ref(&zobject);
}
//nonblock async
if (cli->connect(cli, host, port, timeout, sock_flag) < 0)
{
if (errno == 0 )
{
if (SwooleG.error == SW_ERROR_DNSLOOKUP_RESOLVE_FAILED)
{
swoole_php_error(E_WARNING, "connect to server[%s:%d] failed. Error: %s[%d]", host, (int )port,
hstrerror(h_errno), h_errno);
}
zend_update_property_long(swoole_client_class_entry_ptr, getThis(), SW_STRL("errCode")-1, SwooleG.error TSRMLS_CC);
}
else
{
swoole_php_sys_error(E_WARNING, "connect to server[%s:%d] failed.", host, (int )port);
zend_update_property_long(swoole_client_class_entry_ptr, getThis(), SW_STRL("errCode")-1, errno TSRMLS_CC);
}
RETURN_FALSE;
}
RETURN_TRUE;
}
php_swoole_client_new
php_swoole_client_new
會新建一個 swClient
客戶端物件,具體流程如下:
- 首先取出
type
屬性,觀察是否是非同步客戶端SW_FLAG_ASYNC
- 接著取出
id
屬性,觀察是否存在著connection_id
,如果沒有傳入connection_id
就要根據主域與埠號來建立connection_id
-
如果當前客戶端要保持長連線,要試圖從
php_sw_long_connections
中根據connection_id
取出客戶端物件,複用連線- 如果
php_sw_long_connections
雜湊表中並沒有connection_id
,那麼新申請一個swClient
放入雜湊表中,並調到create_socket
建立客戶端物件。 - 如果取出了客戶端物件,那麼要嘗試接受資料,如果報錯,說明連線已經失效,需要關閉該連線,並調到
create_socket
建立一個新的客戶端物件;如果還可以使用連線,就遞增reuse_count
,
- 如果
- 如果當前客戶端不需要複用長連線,那麼就利用
swClient_create
建立一個新的客戶端
swClient* php_swoole_client_new(zval *object, char *host, int host_len, int port)
{
zval *ztype;
int async = 0;
char conn_key[SW_LONG_CONNECTION_KEY_LEN];
int conn_key_len = 0;
uint64_t tmp_buf;
int ret;
ztype = sw_zend_read_property(Z_OBJCE_P(object), object, SW_STRL("type")-1, 0 TSRMLS_CC);
long type = Z_LVAL_P(ztype);
//new flag, swoole-1.6.12+
if (type & SW_FLAG_ASYNC)
{
async = 1;
}
swClient *cli;
bzero(conn_key, SW_LONG_CONNECTION_KEY_LEN);
zval *connection_id = sw_zend_read_property(Z_OBJCE_P(object), object, ZEND_STRL("id"), 1 TSRMLS_CC);
if (connection_id == NULL || ZVAL_IS_NULL(connection_id))
{
conn_key_len = snprintf(conn_key, SW_LONG_CONNECTION_KEY_LEN, "%s:%d", host, port) + 1;
}
else
{
conn_key_len = snprintf(conn_key, SW_LONG_CONNECTION_KEY_LEN, "%s", Z_STRVAL_P(connection_id)) + 1;
}
//keep the tcp connection
if (type & SW_FLAG_KEEP)
{
swClient *find = swHashMap_find(php_sw_long_connections, conn_key, conn_key_len);
if (find == NULL)
{
cli = (swClient*) pemalloc(sizeof(swClient), 1);
if (swHashMap_add(php_sw_long_connections, conn_key, conn_key_len, cli) == FAILURE)
{
swoole_php_fatal_error(E_WARNING, "failed to add swoole_client_create_socket to hashtable.");
}
goto create_socket;
}
else
{
cli = find;
//try recv, check connection status
ret = recv(cli->socket->fd, &tmp_buf, sizeof(tmp_buf), MSG_DONTWAIT | MSG_PEEK);
if (ret == 0 || (ret < 0 && swConnection_error(errno) == SW_CLOSE))
{
cli->close(cli);
goto create_socket;
}
cli->reuse_count ++;
zend_update_property_long(Z_OBJCE_P(object), object, ZEND_STRL("reuseCount"), cli->reuse_count TSRMLS_CC);
}
}
else
{
cli = (swClient*) emalloc(sizeof(swClient));
create_socket:
if (swClient_create(cli, php_swoole_socktype(type), async) < 0)
{
swoole_php_fatal_error(E_WARNING, "swClient_create() failed. Error: %s [%d]", strerror(errno), errno);
zend_update_property_long(Z_OBJCE_P(object), object, ZEND_STRL("errCode"), errno TSRMLS_CC);
return NULL;
}
//don`t forget free it
cli->server_str = sw_strndup(conn_key, conn_key_len);
cli->server_strlen = conn_key_len;
}
zend_update_property_long(Z_OBJCE_P(object), object, ZEND_STRL("sock"), cli->socket->fd TSRMLS_CC);
if (type & SW_FLAG_KEEP)
{
cli->keep = 1;
}
#ifdef SW_USE_OPENSSL
if (type & SW_SOCK_SSL)
{
cli->open_ssl = 1;
}
#endif
return cli;
}
swClient_create
- 建立客戶端就是利用
type
型別來建立不同的socket
套接字 - 如果是非同步客戶端,設定客戶端的
reactor
物件(可以分為reactor
執行緒和其他程式reactor
) - 設定
cli->socket
屬性 - 如果是非同步客戶端,利用
cli->reactor->setHandle
設定reactor
的回撥函式 - 根據非同步或者同步、資料流或者資料包,設定
swClient
的各種函式
int swClient_create(swClient *cli, int type, int async)
{
int _domain;
int _type;
bzero(cli, sizeof(swClient));
switch (type)
{
case SW_SOCK_TCP:
_domain = AF_INET;
_type = SOCK_STREAM;
break;
case SW_SOCK_TCP6:
_domain = AF_INET6;
_type = SOCK_STREAM;
break;
case SW_SOCK_UNIX_STREAM:
_domain = AF_UNIX;
_type = SOCK_STREAM;
break;
case SW_SOCK_UDP:
_domain = AF_INET;
_type = SOCK_DGRAM;
break;
case SW_SOCK_UDP6:
_domain = AF_INET6;
_type = SOCK_DGRAM;
break;
case SW_SOCK_UNIX_DGRAM:
_domain = AF_UNIX;
_type = SOCK_DGRAM;
break;
default:
return SW_ERR;
}
#ifdef SOCK_CLOEXEC
int sockfd = socket(_domain, _type | SOCK_CLOEXEC, 0);
#else
int sockfd = socket(_domain, _type, 0);
#endif
if (sockfd < 0)
{
swWarn("socket() failed. Error: %s[%d]", strerror(errno), errno);
return SW_ERR;
}
if (async)
{
if (swIsMaster() && SwooleTG.type == SW_THREAD_REACTOR)
{
cli->reactor = SwooleTG.reactor;
}
else
{
cli->reactor = SwooleG.main_reactor;
}
cli->socket = swReactor_get(cli->reactor, sockfd);
}
else
{
cli->socket = sw_malloc(sizeof(swConnection));
}
cli->buffer_input_size = SW_CLIENT_BUFFER_SIZE;
bzero(cli->socket, sizeof(swConnection));
cli->socket->fd = sockfd;
cli->socket->object = cli;
if (async)
{
swSetNonBlock(cli->socket->fd);
if (!swReactor_handle_isset(cli->reactor, SW_FD_STREAM_CLIENT))
{
cli->reactor->setHandle(cli->reactor, SW_FD_STREAM_CLIENT | SW_EVENT_READ, swClient_onStreamRead);
cli->reactor->setHandle(cli->reactor, SW_FD_DGRAM_CLIENT | SW_EVENT_READ, swClient_onDgramRead);
cli->reactor->setHandle(cli->reactor, SW_FD_STREAM_CLIENT | SW_EVENT_WRITE, swClient_onWrite);
cli->reactor->setHandle(cli->reactor, SW_FD_STREAM_CLIENT | SW_EVENT_ERROR, swClient_onError);
}
}
if (swSocket_is_stream(type))
{
cli->recv = swClient_tcp_recv_no_buffer;
if (async)
{
cli->connect = swClient_tcp_connect_async;
cli->send = swClient_tcp_send_async;
cli->sendfile = swClient_tcp_sendfile_async;
cli->pipe = swClient_tcp_pipe;
cli->socket->dontwait = 1;
}
else
{
cli->connect = swClient_tcp_connect_sync;
cli->send = swClient_tcp_send_sync;
cli->sendfile = swClient_tcp_sendfile_sync;
}
cli->reactor_fdtype = SW_FD_STREAM_CLIENT;
}
else
{
cli->connect = swClient_udp_connect;
cli->recv = swClient_udp_recv;
cli->send = swClient_udp_send;
cli->reactor_fdtype = SW_FD_DGRAM_CLIENT;
}
cli->_sock_domain = _domain;
cli->_sock_type = _type;
cli->close = swClient_close;
cli->type = type;
cli->async = async;
cli->protocol.package_length_type = `N`;
cli->protocol.package_length_size = 4;
cli->protocol.package_body_offset = 0;
cli->protocol.package_max_length = SW_BUFFER_INPUT_SIZE;
cli->protocol.onPackage = swClient_onPackage;
return SW_OK;
}
swClient_tcp_connect_async
非同步客戶端資料流連線
- 對於非同步的資料流連線來說,首先要驗證
onConnect
、onError
、onClose
回撥函式不能少。 -
swClient_inet_addr
用於為cli->server_addr.addr
賦值,主要是要利用htons
、inet_pton
轉化數值。 - 對於非同步的客戶端來說
cli->wait_dns
是 1,需要AIO
模組來非同步載入DNS
,進行swAio_dispatch
之後本函式就會立刻返回true
- 當
AIO
模組解析了DNS
之後,cli->wait_dns
會被重置為 0,再次呼叫本函式swClient_tcp_connect_async
- 呼叫
connect
函式進行建立連線,遇到EINTR
訊號中斷要進行重試 - 當錯誤是
EINPROGRESS
的時候,將套接字放入reactor
中,並設定超時時間。當我們以非阻塞的方式來進行連線的時候,返回的結果如果是 -1, 這並不代表這次連線發生了錯誤,如果它的返回結果是EINPROGRESS
,那麼就代表連線還在進行中。 後面可以將套接字放入reactor
中,如果可以寫,說明連線完成了。
static int swClient_tcp_connect_async(swClient *cli, char *host, int port, double timeout, int nonblock)
{
int ret;
cli->timeout = timeout;
if (!cli->buffer)
{
//alloc input memory buffer
cli->buffer = swString_new(cli->buffer_input_size);
if (!cli->buffer)
{
return SW_ERR;
}
}
if (!(cli->onConnect && cli->onError && cli->onClose))
{
swWarn("onConnect/onError/onClose callback have not set.");
return SW_ERR;
}
if (cli->onBufferFull && cli->buffer_high_watermark == 0)
{
cli->buffer_high_watermark = cli->socket->buffer_size * 0.8;
}
if (swClient_inet_addr(cli, host, port) < 0)
{
return SW_ERR;
}
if (cli->wait_dns)
{
if (SwooleAIO.init == 0)
{
swAio_init();
}
swAio_event ev;
bzero(&ev, sizeof(swAio_event));
int len = strlen(cli->server_host);
if (strlen(cli->server_host) < SW_IP_MAX_LENGTH)
{
ev.nbytes = SW_IP_MAX_LENGTH;
}
else
{
ev.nbytes = len + 1;
}
ev.buf = sw_malloc(ev.nbytes);
if (!ev.buf)
{
swWarn("malloc failed.");
return SW_ERR;
}
memcpy(ev.buf, cli->server_host, len);
((char *) ev.buf)[len] = 0;
ev.flags = cli->_sock_domain;
ev.type = SW_AIO_GETHOSTBYNAME;
ev.object = cli;
ev.fd = cli->socket->fd;
ev.callback = swClient_onResolveCompleted;
if (swAio_dispatch(&ev) < 0)
{
sw_free(ev.buf);
return SW_ERR;
}
else
{
return SW_OK;
}
}
while (1)
{
ret = connect(cli->socket->fd, (struct sockaddr *) &cli->server_addr.addr, cli->server_addr.len);
if (ret < 0)
{
if (errno == EINTR)
{
continue;
}
SwooleG.error = errno;
}
break;
}
if ((ret < 0 && errno == EINPROGRESS) || ret == 0)
{
if (cli->reactor->add(cli->reactor, cli->socket->fd, cli->reactor_fdtype | SW_EVENT_WRITE) < 0)
{
return SW_ERR;
}
if (timeout > 0)
{
if (SwooleG.timer.fd == 0)
{
swTimer_init((int) (timeout * 1000));
}
cli->timer = SwooleG.timer.add(&SwooleG.timer, (int) (timeout * 1000), 0, cli, swClient_onTimeout);
}
return SW_OK;
}
return ret;
}
swClient_inet_addr
轉化地址
為了解決大端小端問題,就不能直接在 connect
函式引數中傳陣列,而是應該利用 htons
、inet_pton
等函式進行轉化。
static int swClient_inet_addr(swClient *cli, char *host, int port)
{
...
cli->server_host = host;
cli->server_port = port;
void *addr = NULL;
if (cli->type == SW_SOCK_TCP || cli->type == SW_SOCK_UDP)
{
cli->server_addr.addr.inet_v4.sin_family = AF_INET;
cli->server_addr.addr.inet_v4.sin_port = htons(port);
cli->server_addr.len = sizeof(cli->server_addr.addr.inet_v4);
addr = &cli->server_addr.addr.inet_v4.sin_addr.s_addr;
if (inet_pton(AF_INET, host, addr))
{
return SW_OK;
}
}
else if (cli->type == SW_SOCK_TCP6 || cli->type == SW_SOCK_UDP6)
{
cli->server_addr.addr.inet_v6.sin6_family = AF_INET6;
cli->server_addr.addr.inet_v6.sin6_port = htons(port);
cli->server_addr.len = sizeof(cli->server_addr.addr.inet_v6);
addr = cli->server_addr.addr.inet_v6.sin6_addr.s6_addr;
if (inet_pton(AF_INET6, host, addr))
{
return SW_OK;
}
}
else if (cli->type == SW_SOCK_UNIX_STREAM || cli->type == SW_SOCK_UNIX_DGRAM)
{
cli->server_addr.addr.un.sun_family = AF_UNIX;
strncpy(cli->server_addr.addr.un.sun_path, host, sizeof(cli->server_addr.addr.un.sun_path) - 1);
cli->server_addr.addr.un.sun_path[sizeof(cli->server_addr.addr.un.sun_path) - 1] = 0;
cli->server_addr.len = sizeof(cli->server_addr.addr.un.sun_path);
return SW_OK;
}
else
{
return SW_ERR;
}
if (!cli->async)
{
...
}
else
{
cli->wait_dns = 1;
}
return SW_OK;
}
AIO
非同步 DNS
解析
所謂的 AIO
非同步並不是作業系統中真正的非同步系統呼叫,而是 swoole
利用執行緒池 + reactor
實現的非同步任務系統,當執行緒完成任務後,就會執行相應的 callback
函式,這裡 DNS
非同步解析事件的回撥函式就是 swClient_onResolveCompleted
:
static void swClient_onResolveCompleted(swAio_event *event)
{
swConnection *socket = swReactor_get(SwooleG.main_reactor, event->fd);
if (socket->removed)
{
sw_free(event->buf);
return;
}
swClient *cli = event->object;
cli->wait_dns = 0;
if (event->error == 0)
{
swClient_tcp_connect_async(cli, event->buf, cli->server_port, cli->timeout, 1);
}
else
{
SwooleG.error = SW_ERROR_DNSLOOKUP_RESOLVE_FAILED;
cli->socket->removed = 1;
cli->close(cli);
if (cli->onError)
{
cli->onError(cli);
}
}
sw_free(event->buf);
}
DNS
解析
DNS
具體的解析函式是 swoole_gethostbyname
:
- 獲取
DNS
的方法有兩種:gethostbyname2_r
與gethostbyname2
,gethostbyname2_r
是執行緒安全函式,可以用於多執行緒。 -
gethostbyname2_r
中的第四個引數buf
用於存放臨時資料 - 主機的
ip
地址可能會有多個,函式只把第一個ip
地址複製到addr
中。
struct hostent {
char *h_name; //主機的規範名
char **h_aliases; //主機的別名
int h_addrtype;//主機ip地址的型別,到底是(AF_INET),還是(AF_INET6)
int h_length;//主機ip地址的長度
char **h_addr_list;//主機的ip地址
};
#ifdef HAVE_GETHOSTBYNAME2_R
int swoole_gethostbyname(int flags, char *name, char *addr)
{
int __af = flags & (~SW_DNS_LOOKUP_RANDOM);
int index = 0;
int rc, err;
int buf_len = 256;
struct hostent hbuf;
struct hostent *result;
char *buf = (char*) sw_malloc(buf_len);
memset(buf, 0, buf_len);
while ((rc = gethostbyname2_r(name, __af, &hbuf, buf, buf_len, &result, &err)) == ERANGE)
{
buf_len *= 2;
void *tmp = sw_realloc(buf, buf_len);
if (NULL == tmp)
{
sw_free(buf);
return SW_ERR;
}
else
{
buf = tmp;
}
}
if (0 != rc || NULL == result)
{
sw_free(buf);
return SW_ERR;
}
union
{
char v4[INET_ADDRSTRLEN];
char v6[INET6_ADDRSTRLEN];
} addr_list[SW_DNS_HOST_BUFFER_SIZE];
int i = 0;
for (i = 0; i < SW_DNS_HOST_BUFFER_SIZE; i++)
{
if (hbuf.h_addr_list[i] == NULL)
{
break;
}
if (__af == AF_INET)
{
memcpy(addr_list[i].v4, hbuf.h_addr_list[i], hbuf.h_length);
}
else
{
memcpy(addr_list[i].v6, hbuf.h_addr_list[i], hbuf.h_length);
}
}
if (__af == AF_INET)
{
memcpy(addr, addr_list[index].v4, hbuf.h_length);
}
else
{
memcpy(addr, addr_list[index].v6, hbuf.h_length);
}
sw_free(buf);
return SW_OK;
}
#else
int swoole_gethostbyname(int flags, char *name, char *addr)
{
int __af = flags & (~SW_DNS_LOOKUP_RANDOM);
int index = 0;
struct hostent *host_entry;
if (!(host_entry = gethostbyname2(name, __af)))
{
return SW_ERR;
}
union
{
char v4[INET_ADDRSTRLEN];
char v6[INET6_ADDRSTRLEN];
} addr_list[SW_DNS_HOST_BUFFER_SIZE];
int i = 0;
for (i = 0; i < SW_DNS_HOST_BUFFER_SIZE; i++)
{
if (host_entry->h_addr_list[i] == NULL)
{
break;
}
if (__af == AF_INET)
{
memcpy(addr_list[i].v4, host_entry->h_addr_list[i], host_entry->h_length);
}
else
{
memcpy(addr_list[i].v6, host_entry->h_addr_list[i], host_entry->h_length);
}
}
if (__af == AF_INET)
{
memcpy(addr, addr_list[index].v4, host_entry->h_length);
}
else
{
memcpy(addr, addr_list[index].v6, host_entry->h_length);
}
return SW_OK;
}
#endif
swClient_tcp_connect_sync
同步資料流連線
- 與非同步的
TCP
連線類似,函式首先要呼叫swClient_inet_addr
來轉化主域與埠號,如果inet_pton
函式返回成功,說明並不需要進行DNS
解析,直接返回;否則就要同步呼叫swoole_gethostbyname
函式進行DNS
的解析。 - 利用
swSocket_set_timeout
函式為套接字設定超時時間。 - 如果使用
openssl
要進行SSL
握手。
static int swClient_inet_addr(swClient *cli, char *host, int port)
{
...
if (!cli->async)
{
if (swoole_gethostbyname(cli->_sock_domain, host, addr) < 0)
{
SwooleG.error = SW_ERROR_DNSLOOKUP_RESOLVE_FAILED;
return SW_ERR;
}
}
...
}
int swSocket_set_timeout(int sock, double timeout)
{
int ret;
struct timeval timeo;
timeo.tv_sec = (int) timeout;
timeo.tv_usec = (int) ((timeout - timeo.tv_sec) * 1000 * 1000);
ret = setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeo, sizeof(timeo));
if (ret < 0)
{
swWarn("setsockopt(SO_SNDTIMEO) failed. Error: %s[%d]", strerror(errno), errno);
return SW_ERR;
}
ret = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeo, sizeof(timeo));
if (ret < 0)
{
swWarn("setsockopt(SO_RCVTIMEO) failed. Error: %s[%d]", strerror(errno), errno);
return SW_ERR;
}
return SW_OK;
}
static int swClient_tcp_connect_sync(swClient *cli, char *host, int port, double timeout, int nonblock)
{
int ret, n;
char buf[1024];
cli->timeout = timeout;
if (swClient_inet_addr(cli, host, port) < 0)
{
return SW_ERR;
}
if (nonblock == 1)
{
swSetNonBlock(cli->socket->fd);
}
else
{
if (cli->timeout > 0)
{
swSocket_set_timeout(cli->socket->fd, timeout);
}
}
while (1)
{
ret = connect(cli->socket->fd, (struct sockaddr *) &cli->server_addr.addr, cli->server_addr.len);
#endif
if (ret < 0)
{
if (errno == EINTR)
{
continue;
}
}
break;
}
if (ret >= 0)
{
cli->socket->active = 1;
#ifdef SW_USE_OPENSSL
if (cli->open_ssl)
{
if (swClient_enable_ssl_encrypt(cli) < 0)
{
return SW_ERR;
}
if (swClient_ssl_handshake(cli) < 0)
{
return SW_ERR;
}
}
#endif
}
return ret;
}
SSL
客戶端加密
- 其過程與服務端
SSL
隧道加密流程相似,首先需要建立上下文swSSL_get_context
;如果要驗證對端證照,需要呼叫swSSL_set_capath
設定證照地址;如果使用http2
協商,要使用SSL_CTX_set_alpn_protos
函式設定h2
-
建立上下文之後,就可以利用
swSSL_create
建立SSL
物件繫結套接字,並利用SSL_set_tlsext_host_name
設定SSL
主域,SSL_set_tlsext_host_name(s,name)
函式來設定ClientHello
中的Server Name
SNI
是Server Name Indication
的縮寫,是為了解決一個伺服器使用多個域名和證照的SSL/TLS
擴充套件。它允許客戶端在發起SSL
握手請求時(客戶端發出ClientHello
訊息中)提交請求的HostName
資訊,使得伺服器能夠切換到正確的域並返回相應的證照。在
SNI
出現之前,HostName
資訊只存在於HTTP
請求中,但SSL/TLS
層無法獲知這一資訊。通過將HostName
的資訊加入到SNI
擴充套件中,SSL/TLS
允許伺服器使用一個IP
為不同的域名提供不同的證照,從而能夠與使用同一個IP
的多個“虛擬主機”更方便地建立安全連線。 -
swSSL_connect
函式進行SSL
握手連線 - 如果要驗證服務端證照,則呼叫
swClient_ssl_verify
函式。
int swClient_enable_ssl_encrypt(swClient *cli)
{
cli->ssl_context = swSSL_get_context(&cli->ssl_option);
if (cli->ssl_context == NULL)
{
return SW_ERR;
}
if (cli->ssl_option.verify_peer)
{
if (swSSL_set_capath(&cli->ssl_option, cli->ssl_context) < 0)
{
return SW_ERR;
}
}
cli->socket->ssl_send = 1;
#if defined(SW_USE_HTTP2) && defined(SW_USE_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L
if (cli->http2)
{
if (SSL_CTX_set_alpn_protos(cli->ssl_context, (const unsigned char *) "x02h2", 3) < 0)
{
return SW_ERR;
}
}
#endif
return SW_OK;
}
int swClient_ssl_handshake(swClient *cli)
{
if (!cli->socket->ssl)
{
if (swSSL_create(cli->socket, cli->ssl_context, SW_SSL_CLIENT) < 0)
{
return SW_ERR;
}
#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
if (cli->ssl_option.tls_host_name)
{
SSL_set_tlsext_host_name(cli->socket->ssl, cli->ssl_option.tls_host_name);
}
#endif
}
if (swSSL_connect(cli->socket) < 0)
{
return SW_ERR;
}
if (cli->socket->ssl_state == SW_SSL_STATE_READY && cli->ssl_option.verify_peer)
{
if (swClient_ssl_verify(cli, cli->ssl_option.allow_self_signed) < 0)
{
return SW_ERR;
}
}
return SW_OK;
}
swSSL_set_capath
提供可信任證照庫:
// 在建立上下文結構之後,必須載入一個可信任證照庫。這是成功驗證每個證照所必需的。
// 如果不能確認證照是可信任的,那麼 OpenSSL 會將證照標記為無效(但連線仍可以繼續)。
// OpenSSL 附帶了一組可信任證照。它們位於原始檔目錄樹的 certs/demo 目錄中。
// 不過,每個證照都是一個獨立的檔案,也就是說,需要單獨載入每一個證照。
// 在 certs 目錄下,還有一個 expired 存放過期證照的子目錄,試圖載入這些證照將會出錯。
// 在數字證照進行信任驗證之前,
// 必須為在為安全連線設定時建立的 OpenSSL SSL_CTX 物件提供一個預設的信任證照,
// 這可以使用幾種方法來提供,
// 但是最簡單的方法是將這個證照儲存為一個 PEM 檔案,
// 並使用
// SSL_CTX_load_verify_locations( ctx, file, path );
// 將其載入到 OpenSSL 中。
// 該函式有三個引數:
// ctx - 上下文指標( SSL_CTX_new 函式返回 );
// file - 包含一個或多個 PEM 格式的證照的檔案的路徑(必需);
// path - 到一個或多個 PEM 格式檔案的路徑,不過檔名必須使用特定的格式(可為 NULL);
// 如果指定成功,則返回 1 ,如果遇到問題,則返回 0 。
// 儘管當信任證照在一個目錄中有多個單獨的檔案時更容易新增或更新,
// 但是您不太可能會如此頻繁地更新信任證照,因此不必擔心這個問題。
int swSSL_set_capath(swSSL_option *cfg, SSL_CTX *ctx)
{
if (cfg->cafile || cfg->capath)
{
if (!SSL_CTX_load_verify_locations(ctx, cfg->cafile, cfg->capath))
{
return SW_ERR;
}
}
else
{
if (!SSL_CTX_set_default_verify_paths(ctx))
{
swWarn("Unable to set default verify locations and no CA settings specified.");
return SW_ERR;
}
}
if (cfg->verify_depth > 0)
{
SSL_CTX_set_verify_depth(ctx, cfg->verify_depth);
}
return SW_OK;
}
swSSL_connect
函式就是簡單呼叫 SSL_connect
來連線服務端:
int swSSL_connect(swConnection *conn)
{
int n = SSL_connect(conn->ssl);
if (n == 1)
{
conn->ssl_state = SW_SSL_STATE_READY;
conn->ssl_want_read = 0;
conn->ssl_want_write = 0;
#ifdef SW_LOG_TRACE_OPEN
const char *ssl_version = SSL_get_version(conn->ssl);
const char *ssl_cipher = SSL_get_cipher_name(conn->ssl);
swTraceLog(SW_TRACE_SSL, "connected (%s %s)", ssl_version, ssl_cipher);
#endif
return SW_OK;
}
long err = SSL_get_error(conn->ssl, n);
if (err == SSL_ERROR_WANT_READ)
{
conn->ssl_want_read = 1;
conn->ssl_want_write = 0;
conn->ssl_state = SW_SSL_STATE_WAIT_STREAM;
return SW_OK;
}
else if (err == SSL_ERROR_WANT_WRITE)
{
conn->ssl_want_read = 0;
conn->ssl_want_write = 1;
conn->ssl_state = SW_SSL_STATE_WAIT_STREAM;
return SW_OK;
}
else if (err == SSL_ERROR_ZERO_RETURN)
{
swDebug("SSL_connect(fd=%d) closed.", conn->fd);
return SW_ERR;
}
else if (err == SSL_ERROR_SYSCALL)
{
if (n)
{
SwooleG.error = errno;
return SW_ERR;
}
}
swWarn("SSL_connect(fd=%d) failed. Error: %s[%ld|%d].", conn->fd, ERR_reason_error_string(err), err, errno);
return SW_ERR;
}
swClient_ssl_verify
驗證連線的有效性:
// 連線建立後,必須檢查證照,以確定它是否有效。
// 實際上,OpenSSL 為我們完成了這項任務。
// 如果證照有致命的問題(例如,雜湊值無效),那麼將無法建立連線。
// 但是,如果證照的問題並不是致命的(當它已經過期或者尚不合法時),那麼仍可以繼續使用連線。
// 可以將 SSL 結構作為惟一引數,
// 呼叫 SSL_get_verify_result 來查明證照是否通過了 OpenSSL 的檢驗。
// 如果證照通過了包括信任檢查在內的 OpenSSL 的內部檢查,則返回 X509_V_OK。
// 如果有地方出了問題,則返回一個錯誤程式碼,在 OpenSSL 文件的 verify 部分中都進行了介紹。
// 注:該錯誤程式碼被記錄在命令列工具的 verify 選項下。
// 應該注意的是,驗證失敗並不意味著連線不能使用。
// 是否應該使用連線取決於驗證結果和安全方面的考慮。
// 例如,失敗的信任驗證可能只是意味著沒有可信任的證照。
// 連線仍然可用,只是需要從思想上提高安全意識。
// OpenSSL 在對證照進行驗證時,有一些安全性檢查並沒有執行,
// 包括證照的失效檢查和對證照中通用名的有效性驗證。
int swClient_ssl_verify(swClient *cli, int allow_self_signed)
{
if (swSSL_verify(cli->socket, allow_self_signed) < 0)
{
return SW_ERR;
}
if (cli->ssl_option.tls_host_name && swSSL_check_host(cli->socket, cli->ssl_option.tls_host_name) < 0)
{
return SW_ERR;
}
return SW_OK;
}
int swSSL_verify(swConnection *conn, int allow_self_signed)
{
int err = SSL_get_verify_result(conn->ssl);
switch (err)
{
case X509_V_OK:
return SW_OK;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
if (allow_self_signed)
{
return SW_OK;
}
else
{
return SW_ERR;
}
default:
swoole_error_log(SW_LOG_NOTICE, SW_ERROR_SSL_VEFIRY_FAILED, "Could not verify peer: code:%d %s", err, X509_verify_cert_error_string(err));
return SW_ERR;
}
return SW_ERR;
}
驗證了證照的有效性之後,還要驗證域名的統一。高版本可以直接使用 X509_check_host
函式驗證。
int swSSL_check_host(swConnection *conn, char *tls_host_name)
{
X509 *cert = SSL_get_peer_certificate(conn->ssl);
if (cert == NULL)
{
return SW_ERR;
}
#ifdef X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT
/* X509_check_host() is only available in OpenSSL 1.0.2+ */
if (X509_check_host(cert, tls_host_name, strlen(tls_host_name), 0, NULL) != 1)
{
swWarn("X509_check_host(): no match");
goto failed;
}
goto found;
#else
...
failed: X509_free(cert);
return SW_ERR;
found: X509_free(cert);
return SW_OK;
}
swClient_udp_connect
資料包連線
- 對於
UDP
來說,swoole
並不會connect
服務端進行三次握手,只是會呼叫bind
來繫結本機的一個隨機埠,這個時候客戶端可以接受任何服務端發來的訊息。 -
如果呼叫
connect
函式的時候指定了第 5 個引數為true
,那麼就會呼叫connect
函式,這時候雖然客戶端仍然不會進行三次握手,但是卻只能接受特定服務端發來的訊息呼叫了
connect
之後的UDP
特點:- 不需要給輸出操作指定目的IP和目的埠,寫到UDP的緩衝區裡的資料,將自動傳送到你呼叫connect指定的IP和埠。
- 在一個已連線的UDP套接字上,核心由輸入操作返回的資料包只有那些來自connect所指定的協議地址的資料包。目的地為這個已連線的UDP套接字的本地協議地址(IP和埠),遠端地址不是該套接字早先connect到的協議地址的資料包,不會投遞到該套接字。這樣就限制了已連線的UDP套接字能且只能與一個對端交換資料包。
- 由已連線的套接字引發的非同步錯誤發回給他們所在的程式,而未連線的UDP套接字不接受任何非同步錯誤。
- 讀寫的操作介面方法增多了,除了可以使用sendto和recvfrom的介面外,還可以使用tcp的那套操作介面–read/readv/readmsg和write/writev等
-
UDP
客戶端可選設定onConnect
,socket
建立成功會立即回撥onConnect
static int swClient_udp_connect(swClient *cli, char *host, int port, double timeout, int udp_connect)
{
if (swClient_inet_addr(cli, host, port) < 0)
{
return SW_ERR;
}
cli->socket->active = 1;
cli->timeout = timeout;
int bufsize = SwooleG.socket_buffer_size;
if (timeout > 0)
{
swSocket_set_timeout(cli->socket->fd, timeout);
}
if (cli->type == SW_SOCK_UNIX_DGRAM)
{
struct sockaddr_un* client_addr = &cli->socket->info.addr.un;
sprintf(client_addr->sun_path, "/tmp/swoole-client.%d.%d.sock", getpid(), cli->socket->fd);
client_addr->sun_family = AF_UNIX;
unlink(client_addr->sun_path);
if (bind(cli->socket->fd, (struct sockaddr *) client_addr, sizeof(cli->socket->info.addr.un)) < 0)
{
swSysError("bind(%s) failed.", client_addr->sun_path);
return SW_ERR;
}
}
if (udp_connect != 1)
{
goto connect_ok;
}
if (connect(cli->socket->fd, (struct sockaddr *) (&cli->server_addr), cli->server_addr.len) == 0)
{
swSocket_clean(cli->socket->fd);
connect_ok:
setsockopt(cli->socket->fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
setsockopt(cli->socket->fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
if (cli->async && cli->onConnect)
{
if (cli->reactor->add(cli->reactor, cli->socket->fd, cli->reactor_fdtype | SW_EVENT_READ) < 0)
{
return SW_ERR;
}
execute_onConnect(cli);
}
return SW_OK;
}
else
{
swSysError("connect() failed.");
cli->socket->active = 0;
cli->socket->removed = 1;
return SW_ERR;
}
}
swoole_client->isConnected
非同步連線判定
static PHP_METHOD(swoole_client, isConnected)
{
swClient *cli = (swClient *) swoole_get_object(getThis());
if (!cli)
{
RETURN_FALSE;
}
if (!cli->socket)
{
RETURN_FALSE;
}
RETURN_BOOL(cli->socket->active);
}