PHP Socket编程实战

一直以来,PHP很少用于socket编程,毕竟是一门脚本语言,效率会成为很大的瓶颈,但是不能说PHP就无法用于socket编程,也不能说PHP的socket编程性能就有多么的低,例如知名的一款PHP socket框架 workerman 就是用纯PHP开发,并且号称拥有优秀的性能,所以在某些环境下,PHP socket编程或许也可一展身手。

关于socket的PHP官方手册:http://php.net/manual/zh/book.sockets.php

以下是一个简单的Socket编程示例:

服务端 socket.server.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php

require '../common.php';

// 定义socket监听的IP和端口
$address = '0.0.0.0';
$port = 8801;

/*
+-------------------------------
* @socket通信整个过程
+-------------------------------
* @socket_create
* @socket_bind
* @socket_listen
* @socket_accept
* @socket_read
* @socket_write
* @socket_close
+--------------------------------
*/

// 创建嵌套字
$sock = socket_create(AF_INET, SOCK_STREAM, 0);

if (FALSE === $sock) {
$errcode = socket_last_error($sock);
print_log(socket_strerror($errcode));
exit(-1);
}

// 绑定ip地址及端口
if (!socket_bind($sock, $address , $port))
{
$errcode = socket_last_error();
print_log("socket bind fail: " . socket_strerror($errcode));
exit(-1);
}

// 监听客户端连接
if (!socket_listen($sock))
{
$errcode = socket_last_error();
print_log("socket listen fail: " . socket_strerror($errcode));
exit(-1);
}

// I/O阻塞,获取客户端连接
while(true) {
// 获取一个客户端连接
$conn = socket_accept($sock);
if ($conn) {
// 获取连接过来的客户端ip地址和端口
socket_getpeername($conn, $addr, $port);
print_log("client connect server: ip = $addr, port = $port");
while (true) {
// I/O阻塞,读取客户端发送的信息
$data = socket_read($conn, 1024);

if (empty($data)) {
// 客户端关闭
socket_close($conn);
print_log("client close");
break;
} else {
print_log("read from client: $data");
// 回写给客户端
socket_write($conn, $data);
}
}
} else {
print_log("socket_accept() failed: reason: ". socket_strerror($conn));
}
}
socket_close($sock);

启动socket服务器:

1
php socket.server.php

之后这个服务器就一直阻塞在那里,等待客户端连接,可以用telnet命令来连接这个服务器:

1
2
3
4
5
6
7
8
9
10
$ telnet  148.70.234.50 8801
Trying 148.70.234.50...
Connected to gitlib.com.
Escape character is '^]'.
hello world!
hello world!
nihao
nihao
nihao
nihao

服务器端输出:

1
2
3
4
2018-11-27 17:24:18 client connect server: ip = 61.28.108.243, port = 11263
2018-11-27 17:24:24 read from client: hello world!
2018-11-27 17:24:52 read from client: nihao
2018-11-27 17:26:03 read from client: nihao

如果我们再启动一个socket连接,就会发现socket服务器无法响应请求,结果如下:

1
2
3
4
5
$ telnet 148.70.234.50 8801
Trying 148.70.234.50...
Connected to cms.gitlib.com.
Escape character is '^]'.
nihao

目前的Socket服务器一次只能处理一个客户端的连接和数据传输,因为一个客户端连接过来后,进程就去负责读写客户端数据,当客户端没有传输数据时,tcp服务器处于阻塞读状态,无法再去处理其他客户端的连接请求了。

解决这个问题的一种办法就是采用多进程服务器,每当一个客户端连接过来,服务器开一个子进程专门负责和该客户端的数据传输,而父进程仍然监听客户端的连接,但是起进程的代价是昂贵的,这种多进程的机制显然支撑不了高并发。

另一个解决办法是使用IO多路复用机制,使用php为我们提供的socket_select方法,它可以监听多个socket,如果其中某个socket状态发生了改变,比如从不可写变为可写,从不可读变为可读,这个方法就会返回,从而我们就可以去处理这个socket,处理客户端的连接,读写操作等等。

接下来,使用socket_select()优化之前 socket.server.php 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<?php

require '../common.php';

// 定义socket监听的IP和端口
$address = '0.0.0.0';
$port = 8801;

/*
+-------------------------------
* @socket通信整个过程
+-------------------------------
* @socket_create
* @socket_bind
* @socket_listen
* @socket_accept
* @socket_read
* @socket_write
* @socket_close
+--------------------------------
*/

// 创建嵌套字
$sock = socket_create(AF_INET, SOCK_STREAM, 0);

if (FALSE === $sock) {
$errcode = socket_last_error($sock);
print_log(socket_strerror($errcode));
exit(-1);
}

// 绑定ip地址及端口
if (!socket_bind($sock, $address , $port))
{
$errcode = socket_last_error();
print_log("socket bind fail: " . socket_strerror($errcode));
exit(-1);
}

// 监听客户端连接
if (!socket_listen($sock))
{
$errcode = socket_last_error();
print_log("socket listen fail: " . socket_strerror($errcode));
exit(-1);
}

/* 要监听的三个sockets数组 */
$read_socks = array();
$write_socks = array();
$except_socks = NULL; // 注意 php 不支持直接将NULL作为引用传参,所以这里定义一个变量

$read_socks[] = $sock;

while (1)
{
/* 这两个数组会被改变,所以用两个临时变量 */
$tmp_reads = $read_socks;
$tmp_writes = $write_socks;

// int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )
$count = socket_select($tmp_reads, $tmp_writes, $except_socks, NULL); // timeout 传 NULL 会一直阻塞直到有结果返回

foreach ($tmp_reads as $read) {

if ($read == $sock) {
/* 有新的客户端连接请求 */
$conn = socket_accept($sock); //响应客户端连接, 此时不会造成阻塞
if ($conn) {
socket_getpeername($conn, $addr, $port); //获取远程客户端ip地址和端口
print_log("client connect server: ip = $addr, port = $port");

// 把新的连接sokcet加入监听
$read_socks[] = $conn;
$write_socks[] = $conn;
}
} else {
/* 客户端传输数据 */
$data = socket_read($read, 1024); //从客户端读取数据, 此时一定会读到数组而不会产生阻塞

if (empty($data)) {
// 移除对该 socket 监听
foreach ($read_socks as $key => $val) {
if ($val == $read) unset($read_socks[$key]);
}

foreach ($write_socks as $key => $val) {
if ($val == $read) unset($write_socks[$key]);
}


socket_close($read);
print_log("client close");
} else {
socket_getpeername($read, $addr, $port); //获取远程客户端ip地址和端口
print_log("read from client # $addr:$port: $data");

if (in_array($read, $tmp_writes)) {
// 回写给客户端
socket_write($read, $data);
}
}
}
}
}

socket_close($sock);

现在,socket服务器就可以支持多个客户端同时连接了,服务器输入如下:

1
2
3
4
2018-11-27 17:44:18 client connect server: ip = 61.28.108.243, port = 11287
2018-11-27 17:44:18 read from client # 61.28.108.243:11287: a
2018-11-27 17:44:18 client connect server: ip = 61.28.108.243, port = 11288
2018-11-27 17:44:18 read from client # 61.28.108.243:11288: b
有用就打赏一下作者吧!