Skip to content

Commit 9582d8e

Browse files
committed
Add stream socket keepalive context options
This adds so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream socket context options that are used to set their upper case C macro variants by setsockopt function. The test requires sockets extension and just tests that the values are being set. This is because a real test would be slow and difficult to show that those options really work due to how they work internally. Closes GH-20381
1 parent bd8da04 commit 9582d8e

File tree

6 files changed

+378
-28
lines changed

6 files changed

+378
-28
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ PHP NEWS
7373
while COW violation flag is still set). (alexandre-daubois)
7474

7575
- Streams:
76+
. Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream
77+
socket context options.
7678
. Added so_reuseaddr streams context socket option that allows disabling
7779
address resuse.
7880
. Fixed bug GH-20370 (User stream filters could violate typed property

UPGRADING

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ PHP 8.6 UPGRADE NOTES
4646
. Added stream socket context option so_reuseaddr that allows disabling
4747
address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
4848
Windows.
49+
. Added stream socket context options so_keepalive, tcp_keepidle,
50+
tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive
51+
options.
4952

5053
========================================
5154
3. Changes in SAPI modules
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
--TEST--
2+
stream_socket_server() and stream_socket_client() SO_KEEPALIVE context option test
3+
--EXTENSIONS--
4+
sockets
5+
--SKIPIF--
6+
<?php
7+
if (!defined('TCP_KEEPIDLE') && !defined('TCP_KEEPALIVE')) {
8+
die('skip TCP_KEEPIDLE/TCP_KEEPALIVE not available');
9+
}
10+
if (!defined('TCP_KEEPINTVL')) {
11+
die('skip TCP_KEEPINTVL not available');
12+
}
13+
if (!defined('TCP_KEEPCNT')) {
14+
die('skip TCP_KEEPCNT not available');
15+
}
16+
?>
17+
--FILE--
18+
<?php
19+
// Test server with SO_KEEPALIVE enabled
20+
$server_context = stream_context_create([
21+
'socket' => [
22+
'so_keepalive' => true,
23+
'tcp_keepidle' => 60,
24+
'tcp_keepintvl' => 10,
25+
'tcp_keepcnt' => 5,
26+
]
27+
]);
28+
29+
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
30+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server_context);
31+
32+
if (!$server) {
33+
die('Unable to create server');
34+
}
35+
36+
$addr = stream_socket_get_name($server, false);
37+
$port = (int)substr(strrchr($addr, ':'), 1);
38+
39+
// Test client with SO_KEEPALIVE enabled
40+
$client_context = stream_context_create([
41+
'socket' => [
42+
'so_keepalive' => true,
43+
'tcp_keepidle' => 30,
44+
'tcp_keepintvl' => 5,
45+
'tcp_keepcnt' => 3,
46+
]
47+
]);
48+
49+
$client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 30,
50+
STREAM_CLIENT_CONNECT, $client_context);
51+
52+
if (!$client) {
53+
die('Unable to create client');
54+
}
55+
56+
$accepted = stream_socket_accept($server, 1);
57+
58+
if (!$accepted) {
59+
die('Unable to accept connection');
60+
}
61+
62+
// Verify server side (accepted connection)
63+
$server_sock = socket_import_stream($accepted);
64+
$server_keepalive = socket_get_option($server_sock, SOL_SOCKET, SO_KEEPALIVE);
65+
echo "Server SO_KEEPALIVE: " . ($server_keepalive ? "enabled" : "disabled") . "\n";
66+
67+
if (defined('TCP_KEEPIDLE')) {
68+
$server_idle = socket_get_option($server_sock, SOL_TCP, TCP_KEEPIDLE);
69+
echo "Server TCP_KEEPIDLE: $server_idle\n";
70+
} else {
71+
$server_idle = socket_get_option($server_sock, SOL_TCP, TCP_KEEPALIVE);
72+
echo "Server TCP_KEEPIDLE: $server_idle\n";
73+
}
74+
75+
$server_intvl = socket_get_option($server_sock, SOL_TCP, TCP_KEEPINTVL);
76+
echo "Server TCP_KEEPINTVL: $server_intvl\n";
77+
78+
$server_cnt = socket_get_option($server_sock, SOL_TCP, TCP_KEEPCNT);
79+
echo "Server TCP_KEEPCNT: $server_cnt\n";
80+
81+
// Verify client side
82+
$client_sock = socket_import_stream($client);
83+
$client_keepalive = socket_get_option($client_sock, SOL_SOCKET, SO_KEEPALIVE);
84+
echo "Client SO_KEEPALIVE: " . ($client_keepalive ? "enabled" : "disabled") . "\n";
85+
86+
if (defined('TCP_KEEPIDLE')) {
87+
$client_idle = socket_get_option($client_sock, SOL_TCP, TCP_KEEPIDLE);
88+
echo "Client TCP_KEEPIDLE: $client_idle\n";
89+
} else {
90+
$client_idle = socket_get_option($client_sock, SOL_TCP, TCP_KEEPALIVE);
91+
echo "Client TCP_KEEPIDLE: $client_idle\n";
92+
}
93+
94+
$client_intvl = socket_get_option($client_sock, SOL_TCP, TCP_KEEPINTVL);
95+
echo "Client TCP_KEEPINTVL: $client_intvl\n";
96+
97+
$client_cnt = socket_get_option($client_sock, SOL_TCP, TCP_KEEPCNT);
98+
echo "Client TCP_KEEPCNT: $client_cnt\n";
99+
100+
fclose($accepted);
101+
fclose($client);
102+
fclose($server);
103+
104+
// Test server with SO_KEEPALIVE disabled
105+
$server2_context = stream_context_create(['socket' => ['so_keepalive' => false]]);
106+
$server2 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
107+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server2_context);
108+
109+
$addr2 = stream_socket_get_name($server2, false);
110+
$port2 = (int)substr(strrchr($addr2, ':'), 1);
111+
112+
$client2 = stream_socket_client("tcp://127.0.0.1:$port2");
113+
$accepted2 = stream_socket_accept($server2, 1);
114+
115+
$server2_sock = socket_import_stream($accepted2);
116+
$server2_keepalive = socket_get_option($server2_sock, SOL_SOCKET, SO_KEEPALIVE);
117+
echo "Server disabled SO_KEEPALIVE: " . ($server2_keepalive ? "enabled" : "disabled") . "\n";
118+
119+
fclose($accepted2);
120+
fclose($client2);
121+
fclose($server2);
122+
123+
// Test client with SO_KEEPALIVE disabled
124+
$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
125+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
126+
127+
$addr3 = stream_socket_get_name($server3, false);
128+
$port3 = (int)substr(strrchr($addr3, ':'), 1);
129+
130+
$client3_context = stream_context_create(['socket' => ['so_keepalive' => false]]);
131+
$client3 = stream_socket_client("tcp://127.0.0.1:$port3", $errno, $errstr, 30,
132+
STREAM_CLIENT_CONNECT, $client3_context);
133+
134+
$client3_sock = socket_import_stream($client3);
135+
$client3_keepalive = socket_get_option($client3_sock, SOL_SOCKET, SO_KEEPALIVE);
136+
echo "Client disabled SO_KEEPALIVE: " . ($client3_keepalive ? "enabled" : "disabled") . "\n";
137+
138+
fclose($client3);
139+
fclose($server3);
140+
?>
141+
--EXPECT--
142+
Server SO_KEEPALIVE: enabled
143+
Server TCP_KEEPIDLE: 60
144+
Server TCP_KEEPINTVL: 10
145+
Server TCP_KEEPCNT: 5
146+
Client SO_KEEPALIVE: enabled
147+
Client TCP_KEEPIDLE: 30
148+
Client TCP_KEEPINTVL: 5
149+
Client TCP_KEEPCNT: 3
150+
Server disabled SO_KEEPALIVE: disabled
151+
Client disabled SO_KEEPALIVE: disabled

main/network.c

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,9 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd,
452452
/* Bind to a local IP address.
453453
* Returns the bound socket, or -1 on failure.
454454
* */
455-
/* {{{ php_network_bind_socket_to_local_addr */
456-
php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port,
457-
int socktype, long sockopts, zend_string **error_string, int *error_code
455+
php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned port,
456+
int socktype, long sockopts, php_sockvals *sockvals, zend_string **error_string,
457+
int *error_code
458458
)
459459
{
460460
int num_addrs, n, err = 0;
@@ -533,6 +533,35 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po
533533
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&sockoptval, sizeof(sockoptval));
534534
}
535535
#endif
536+
#ifdef SO_KEEPALIVE
537+
if (sockopts & STREAM_SOCKOP_SO_KEEPALIVE) {
538+
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&sockoptval, sizeof(sockoptval));
539+
}
540+
#endif
541+
542+
/* Set socket values if provided */
543+
if (sockvals != NULL) {
544+
#if defined(TCP_KEEPIDLE)
545+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
546+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
547+
}
548+
#elif defined(TCP_KEEPALIVE)
549+
/* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */
550+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
551+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
552+
}
553+
#endif
554+
#ifdef TCP_KEEPINTVL
555+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) {
556+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl));
557+
}
558+
#endif
559+
#ifdef TCP_KEEPCNT
560+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) {
561+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
562+
}
563+
#endif
564+
}
536565

537566
n = bind(sock, sa, socklen);
538567

@@ -560,7 +589,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po
560589
return sock;
561590

562591
}
563-
/* }}} */
592+
593+
php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port,
594+
int socktype, long sockopts, zend_string **error_string, int *error_code
595+
)
596+
{
597+
return php_network_bind_socket_to_local_addr_ex(host, port, socktype, sockopts, NULL, error_string, error_code);
598+
}
564599

565600
PHPAPI zend_result php_network_parse_network_address_with_port(const char *addr, size_t addrlen, struct sockaddr *sa, socklen_t *sl)
566601
{
@@ -824,11 +859,9 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
824859
* enable non-blocking mode on the socket.
825860
* Returns the connected (or connecting) socket, or -1 on failure.
826861
* */
827-
828-
/* {{{ php_network_connect_socket_to_host */
829-
php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port,
862+
php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port,
830863
int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
831-
int *error_code, const char *bindto, unsigned short bindport, long sockopts
864+
int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals
832865
)
833866
{
834867
int num_addrs, n, fatal = 0;
@@ -952,6 +985,40 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short
952985
}
953986
}
954987
#endif
988+
989+
#ifdef SO_KEEPALIVE
990+
{
991+
int val = 1;
992+
if (sockopts & STREAM_SOCKOP_SO_KEEPALIVE) {
993+
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&val, sizeof(val));
994+
}
995+
}
996+
#endif
997+
998+
/* Set socket values if provided */
999+
if (sockvals != NULL) {
1000+
#if defined(TCP_KEEPIDLE)
1001+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
1002+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
1003+
}
1004+
#elif defined(TCP_KEEPALIVE)
1005+
/* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */
1006+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
1007+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
1008+
}
1009+
#endif
1010+
#ifdef TCP_KEEPINTVL
1011+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) {
1012+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl));
1013+
}
1014+
#endif
1015+
#ifdef TCP_KEEPCNT
1016+
if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) {
1017+
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
1018+
}
1019+
#endif
1020+
}
1021+
9551022
n = php_network_connect_socket(sock, sa, socklen, asynchronous,
9561023
timeout ? &working_timeout : NULL,
9571024
error_string, error_code);
@@ -998,7 +1065,15 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short
9981065

9991066
return sock;
10001067
}
1001-
/* }}} */
1068+
1069+
php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port,
1070+
int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
1071+
int *error_code, const char *bindto, unsigned short bindport, long sockopts
1072+
)
1073+
{
1074+
return php_network_connect_socket_to_host_ex(host, port, socktype, asynchronous, timeout,
1075+
error_string, error_code, bindto, bindport, sockopts, NULL);
1076+
}
10021077

10031078
/* {{{ php_any_addr
10041079
* Fills any (wildcard) address into php_sockaddr_storage

main/php_network.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ typedef int php_socket_t;
124124
#define STREAM_SOCKOP_IPV6_V6ONLY_ENABLED (1 << 4)
125125
#define STREAM_SOCKOP_TCP_NODELAY (1 << 5)
126126
#define STREAM_SOCKOP_SO_REUSEADDR (1 << 6)
127+
#define STREAM_SOCKOP_SO_KEEPALIVE (1 << 7)
127128

128129
/* uncomment this to debug poll(2) emulation on systems that have poll(2) */
129130
/* #define PHP_USE_POLL_2_EMULATION 1 */
@@ -266,10 +267,28 @@ typedef struct {
266267
} php_sockaddr_storage;
267268
#endif
268269

270+
#define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 0)
271+
#define PHP_SOCKVAL_TCP_KEEPCNT (1 << 1)
272+
#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 2)
273+
274+
typedef struct {
275+
unsigned int mask;
276+
struct {
277+
int keepidle;
278+
int keepcnt;
279+
int keepintvl;
280+
} keepalive;
281+
} php_sockvals;
282+
269283
BEGIN_EXTERN_C()
270284
PHPAPI int php_network_getaddresses(const char *host, int socktype, struct sockaddr ***sal, zend_string **error_string);
271285
PHPAPI void php_network_freeaddresses(struct sockaddr **sal);
272286

287+
PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port,
288+
int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
289+
int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals
290+
);
291+
273292
PHPAPI php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port,
274293
int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
275294
int *error_code, const char *bindto, unsigned short bindport, long sockopts
@@ -286,6 +305,10 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd,
286305
#define php_connect_nonb(sock, addr, addrlen, timeout) \
287306
php_network_connect_socket((sock), (addr), (addrlen), 0, (timeout), NULL, NULL)
288307

308+
PHPAPI php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned port,
309+
int socktype, long sockopts, php_sockvals *sockvals, zend_string **error_string, int *error_code
310+
);
311+
289312
PHPAPI php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned port,
290313
int socktype, long sockopts, zend_string **error_string, int *error_code
291314
);

0 commit comments

Comments
 (0)