Gitlib Gitlib
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)

Ravior

以梦为马,莫负韶华
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)
  • PHP

    • PHP-FPM使用指南
    • PHP7新特性总结
    • PHP安全编程
    • PHP安全配置总结
    • PHP变量的值类型和引用类型
    • PHP标准规范PSR
    • PHP操作Zookeeper实践
    • PHP错误和异常处理机制详解
    • PHP的Session运行机制
    • PHP底层运行机制和原理
    • PHP反射模拟实现注解路由
    • PHP高级用法总结
    • PHP开发常用文档总结
    • PHP开发入门:Memcached扩展安装
    • PHP开发入门:PHP7安装部署
    • PHP开发入门:Redis扩展安装
    • PHP开发SPL总结
    • PHP框架常见URL模式
    • PHP扩展开发入门
    • PHP垃圾回收机制
    • PHP类的自动加载
    • PHP输入输出流
    • PHP微服务开发指南
    • PHP协程
    • PHP写时拷贝技术
    • PHP性能优化之Opcache
    • PHP依赖注入和控制反转
    • PHP运行模式(SAPI)
    • PHP中file_get_contents与curl区别
    • RPC的简单实现
    • Protobuf:高效数据结构化工具
    • P3P协议详解
    • Laravel之集合(Collection)总结
    • Laravel实践总结
    • Laravel之ORM总结
    • 中高级PHP实践总结
    • PHP Socket编程实战
  • Golang

  • Python

  • Javascript

  • 其他语言

  • 编程语言
  • PHP
Ravior
2018-11-27

PHP Socket编程实战

一直以来,PHP很少用于socket编程,但某些情况下,我们依然也可以使用PHP来实现socket编程。

关于socket的PHP官方手册:http://php.net/manual/zh/book.sockets.php (opens new window)

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

服务端 socket.server.php:

<?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);
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

启动socket服务器:

php socket.server.php
1

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

$ 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
5
6
7
8
9
10

服务器端输出:

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
1
2
3
4

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

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

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

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

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

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

<?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);
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

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

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
1
2
3
4
#PHP#Socket
上次更新: 2022/12/02, 22:04:34
中高级PHP实践总结
Golang语法特点总结

← 中高级PHP实践总结 Golang语法特点总结→

最近更新
01
常用游戏反外挂技术总结
11-27
02
Golang开发实践万字总结
11-11
03
Redis万字总结
10-30
更多文章>
Theme by Vdoing | Copyright © 2011-2022 Ravior | 粤ICP备17060229号-3 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式