Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8ae9b7b

Browse files
新增《PHP扩展开发》-协程-如何bind和listen
1 parent 83ec5fb commit 8ae9b7b

6 files changed

+427
-2
lines changed

‎.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ tests/*/*.php
3333
tests/*/*.exp
3434
tests/*/*.log
3535
tests/*/*.sh
36-
.vscode
36+
.vscode
37+
.DS_Store

‎README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,5 @@ docker build -t study -f docker/Dockerfile .
201201
[75、stream_socket_server源码分析](./docs/《PHP扩展开发》-协程-stream_socket_server源码分析.md)
202202

203203
[76、替换php_stream_generic_socket_factory](./docs/《PHP扩展开发》-协程-替换php_stream_generic_socket_factory.md)
204+
205+
[77、如何bind和listen](./docs/《PHP扩展开发》-协程-如何bind和listen.md)
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# 如何bind和listen
2+
3+
在上篇文章中,我们成功的创建出了一个`php_stream`类型的资源对象。这篇文章,我们来实现一下对`socket``bind``listen`。测试脚本如下:
4+
5+
```php
6+
<?php
7+
8+
study_event_init();
9+
10+
Sgo(function () {
11+
$ctx = stream_context_create(['socket' => ['so_reuseaddr' => true, 'backlog' => 128]]);
12+
$socket = stream_socket_server(
13+
'tcp://0.0.0.0:6666',
14+
$errno,
15+
$errstr,
16+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
17+
$ctx
18+
);
19+
if (!$socket) {
20+
echo "$errstr ($errno)" . PHP_EOL;
21+
exit(1);
22+
}
23+
var_dump($socket);
24+
sleep(10000);
25+
});
26+
27+
study_event_wait();
28+
```
29+
30+
这段代码很简单。我们没有开启`hook`功能,所以用的是`PHP`内核原来的函数。执行脚本:
31+
32+
```shell
33+
[root@7b6ef640478b study]# php test.php
34+
resource(5) of type (stream)
35+
36+
```
37+
38+
此时,另起一个终端,查看端口情况:
39+
40+
```shell
41+
[root@7b6ef640478b test]# netstat -antp | grep 6666
42+
tcp 0 0 0.0.0.0:6666 0.0.0.0:* LISTEN 37874/php
43+
[root@7b6ef640478b test]#
44+
```
45+
46+
我们发现,测试会用端口占用。也就是说,当我们调用`stream_socket_server`,并且指定了`STREAM_SERVER_BIND``STREAM_SERVER_LISTEN`标志,那么就会占用端口。现在,我们开启`hook`功能,测试代码如下:
47+
48+
```php
49+
<?php
50+
51+
study_event_init();
52+
53+
Study\Runtime::enableCoroutine();
54+
55+
Sgo(function () {
56+
$ctx = stream_context_create(['socket' => ['so_reuseaddr' => true, 'backlog' => 128]]);
57+
$socket = stream_socket_server(
58+
'tcp://0.0.0.0:6666',
59+
$errno,
60+
$errstr,
61+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
62+
$ctx
63+
);
64+
if (!$socket) {
65+
echo "$errstr ($errno)" . PHP_EOL;
66+
exit(1);
67+
}
68+
var_dump($socket);
69+
sleep(10000);
70+
});
71+
72+
study_event_wait();
73+
```
74+
75+
执行结果:
76+
77+
```shell
78+
[root@7b6ef640478b study]# php test.php
79+
resource(5) of type (stream)
80+
81+
```
82+
83+
此时,另起一个终端,查看端口占用:
84+
85+
```shell
86+
[root@7b6ef640478b test]# netstat -antp | grep 6666
87+
[root@7b6ef640478b test]#
88+
```
89+
90+
发现没有占用端口。说明没有调用操作系统的`bind``listen`接口。
91+
92+
分析没有`hook`的内核源码可以知道,`bind``listen`的操作是在`socket_ops``socket_set_option`函数里面完成的。因为我们的这个函数没有去实现,所以自然就无法`bind``listen`端口了。我们这里来实现一下:
93+
94+
```cpp
95+
static int socket_set_option(php_stream *stream, int option, int value, void *ptrparam)
96+
{
97+
php_study_netstream_data_t *abstract = (php_study_netstream_data_t *) stream->abstract;
98+
php_stream_xport_param *xparam;
99+
100+
switch(option)
101+
{
102+
case PHP_STREAM_OPTION_XPORT_API:
103+
xparam = (php_stream_xport_param *)ptrparam;
104+
105+
switch(xparam->op)
106+
{
107+
case STREAM_XPORT_OP_BIND:
108+
xparam->outputs.returncode = php_study_tcp_sockop_bind(stream, abstract, xparam);
109+
return PHP_STREAM_OPTION_RETURN_OK;
110+
case STREAM_XPORT_OP_LISTEN:
111+
xparam->outputs.returncode = abstract->socket->listen(xparam->inputs.backlog);
112+
return PHP_STREAM_OPTION_RETURN_OK;
113+
default:
114+
/* fall through */
115+
;
116+
}
117+
}
118+
return PHP_STREAM_OPTION_RETURN_OK;
119+
}
120+
```
121+
122+
当我们在`PHP`函数`stream_socket_server`中设置了`STREAM_SERVER_BIND`或者`STREAM_SERVER_LISTEN`,实际上`PHP`底层就会调用到我们的这个`socket_set_option`函数。我们来分析一下这段代码。核心代码就是这个`switch`语句了:
123+
124+
```cpp
125+
switch(xparam->op)
126+
{
127+
case STREAM_XPORT_OP_BIND:
128+
xparam->outputs.returncode = php_study_tcp_sockop_bind(stream, abstract, xparam);
129+
return PHP_STREAM_OPTION_RETURN_OK;
130+
case STREAM_XPORT_OP_LISTEN:
131+
xparam->outputs.returncode = abstract->socket->listen(xparam->inputs.backlog);
132+
return PHP_STREAM_OPTION_RETURN_OK;
133+
default:
134+
/* fall through */
135+
;
136+
}
137+
```
138+
139+
这个`xparam->op`此时对应着我们在`stream_socket_server`中设置的`$flags`,也就是`STREAM_SERVER_BIND | STREAM_SERVER_LISTEN`。所以,这里两个`case`都会命中。(注意,虽然`switch`这里只会执行其中的一个`case`,但是,内核会分别两次调用我们的`socket_set_option`函数)
140+
141+
这里我们需要定义一下`STREAM_XPORT_OP_BIND``STREAM_XPORT_OP_LISTEN`:
142+
143+
```cpp
144+
enum
145+
{
146+
STREAM_XPORT_OP_BIND,
147+
STREAM_XPORT_OP_CONNECT,
148+
STREAM_XPORT_OP_LISTEN,
149+
STREAM_XPORT_OP_ACCEPT,
150+
STREAM_XPORT_OP_CONNECT_ASYNC,
151+
STREAM_XPORT_OP_GET_NAME,
152+
STREAM_XPORT_OP_GET_PEER_NAME,
153+
STREAM_XPORT_OP_RECV,
154+
STREAM_XPORT_OP_SEND,
155+
STREAM_XPORT_OP_SHUTDOWN,
156+
};
157+
```
158+
159+
这个要和`PHP`内核里面的顺序一致。然后我们需要实现`php_study_tcp_sockop_bind`这个函数:
160+
161+
```cpp
162+
static int php_study_tcp_sockop_bind(php_stream *stream, php_study_netstream_data_t *abstract, php_stream_xport_param *xparam)
163+
{
164+
char *host = NULL;
165+
int portno;
166+
167+
host = parse_ip_address(xparam, &portno);
168+
169+
if (host == NULL)
170+
{
171+
return -1;
172+
}
173+
174+
int ret = abstract->socket->bind(ST_SOCK_TCP, host, portno);
175+
176+
if (host)
177+
{
178+
efree(host);
179+
}
180+
return ret;
181+
}
182+
```
183+
184+
代码很简单,就是去调用我们封装好的`bind`函数。但是,这里有一点需要注意的是,我们需要从`stream_socket_server`函数的第一个参数里面解析出`host`和`port`。这里就是通过`parse_ip_address`来实现的。具体细节我们无需过多的关注,只需要知道这个函数是从`PHP`内核复制出来的即可:
185+
186+
```cpp
187+
/**
188+
* copy from php src file: xp_socket.c
189+
*/
190+
static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *portno, int get_err, zend_string **err)
191+
{
192+
char *colon;
193+
char *host = NULL;
194+
195+
#ifdef HAVE_IPV6
196+
char *p;
197+
198+
if (*(str) == '[' && str_len > 1)
199+
{
200+
/* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */
201+
p = (char *)memchr(str + 1, ']', str_len - 2);
202+
if (!p || *(p + 1) != ':')
203+
{
204+
if (get_err)
205+
{
206+
*err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str);
207+
}
208+
return NULL;
209+
}
210+
*portno = atoi(p + 2);
211+
return estrndup(str + 1, p - str - 1);
212+
}
213+
#endif
214+
if (str_len)
215+
{
216+
colon = (char *)memchr(str, ':', str_len - 1);
217+
}
218+
else
219+
{
220+
colon = NULL;
221+
}
222+
if (colon)
223+
{
224+
*portno = atoi(colon + 1);
225+
host = estrndup(str, colon - str);
226+
}
227+
else
228+
{
229+
if (get_err)
230+
{
231+
*err = strpprintf(0, "Failed to parse address \"%s\"", str);
232+
}
233+
return NULL;
234+
}
235+
236+
return host;
237+
}
238+
239+
/**
240+
* copy from php src file: xp_socket.c
241+
*/
242+
static inline char *parse_ip_address(php_stream_xport_param *xparam, int *portno)
243+
{
244+
return parse_ip_address_ex(xparam->inputs.name, xparam->inputs.namelen, portno, xparam->want_errortext, &xparam->outputs.error_text);
245+
}
246+
```
247+
248+
`OK`,我们现在实现完了`socket_set_option`这个函数。我们重新编译、安装扩展:
249+
250+
```shell
251+
[root@7b6ef640478b study]# make clean && make install
252+
----------------------------------------------------------------------
253+
Installing shared extensions: /usr/lib/php/extensions/debug-non-zts-20180731/
254+
Installing header files: /usr/include/php/
255+
[root@7b6ef640478b study]#
256+
```
257+
258+
编写测试脚本:
259+
260+
```shell
261+
<?php
262+
263+
study_event_init();
264+
265+
Study\Runtime::enableCoroutine();
266+
267+
Sgo(function () {
268+
$ctx = stream_context_create(['socket' => ['so_reuseaddr' => true, 'backlog' => 128]]);
269+
$socket = stream_socket_server(
270+
'tcp://0.0.0.0:6666',
271+
$errno,
272+
$errstr,
273+
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
274+
$ctx
275+
);
276+
if (!$socket) {
277+
echo "$errstr ($errno)" . PHP_EOL;
278+
exit(1);
279+
}
280+
var_dump($socket);
281+
sleep(10000);
282+
});
283+
284+
study_event_wait();
285+
```
286+
287+
执行脚本:
288+
289+
```shell
290+
[root@7b6ef640478b study]# php test.php
291+
resource(5) of type (stream)
292+
293+
```
294+
295+
然后查看端口占用情况:
296+
297+
```shell
298+
[root@7b6ef640478b ~]# netstat -antp | grep 6666
299+
tcp 0 0 0.0.0.0:6666 0.0.0.0:* LISTEN 6822/php
300+
[root@7b6ef640478b ~]#
301+
```
302+
303+
符合预期。

‎docs/《PHP扩展开发》-协程-替换php_stream_generic_socket_factory.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,5 @@ resource(5) of type (stream)
340340
```
341341
342342
`OK`,符合预期。
343+
344+
[下一篇:如何bind和listen](./《PHP扩展开发》-协程-如何bind和listen.md)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /