在网络编程的客户端/服务器模型中,客户端应用程序向服务器程序请求服务,一个服务器程序通常在指定地址的端口上监听对服务的请求,也就是说,服务进程会一直处于休眠状态,直到有一个客户端向这个服务器的地址端口上发出连接请求。此时,服务器程序被“惊醒”并未客户端提供服务,也就是为客户端的请求作出适当的反应。
为了方便这种客户端/服务器模型的网络编程,90年代初期由微软联合多家公司共同制定了一套Windows下的网络编写接口即WindowsSockets规范,WindowsSockets并非一种网络协议,而是一套开放的、支持多种协议的Windows下的网络编程接口。
Socket是应用层与TCP/IP协议簇通信的中间软件抽象层,是一组接口 。在设计模式中,Socket是一个门面模式,它将复杂的TCP/IP协议簇隐藏在Socket接口后面,对用户而言一组简单的接口就是它的全部。
简单来说,Socket是封装好的TCP/IP通信协议的接口,提供网络通讯的能力,更加方便地使用协议栈。Socket赋予用户控制传输层和网络层的能力,从而得到更强的性能和更高的效率。Socket编程是解决高并发网络服务器中最常见和最成熟的解决方案。
网络上两个应用程序会通过一个双向的通信连接来实现数据的交换,此时连接的一端会被成为一个Socket。所以,Socket的运行至少具有两个端,比如一个服务端多个客户端。
Socket的运行流程
- 服务器初始化Socket后与端口进行绑定
bind
,接着会对端口进行监听listen
,并调用accept
进入阻塞状态以等待客户端的连接。 - 如果此时客户端初始化Socket后连接
connect
到服务器 - 如果连接建立成功,客户端就可以发送数据请求到服务器,服务端会接收请求并处理,最后将回应数据返回给客户端。
- 客户端读取服务器返还的数据后关闭连接,完成一次交互。
为什么说 WebSocket是一个持久化的协议呢?
WebSocket是一个持久化的协议是相对于HTTP非持久化来说的
- HTTP1.0的生命周期是以
request
请求作为界定。即一个request
请求对应一个response
响应,对于HTTP来说一次客户端与服务器的会话会到此结束。 - HTTP1.1中加入了
keep-alive
,也就是在一个HTTP连接中可以对多个请求和多个响应结束操作。然而在实时通讯方式并没太多的作用。因为HTTP只能由客户端发起请求,服务器才能返回消息,也就是说服务器不能主动向客户端推送信息,无法满足实时通讯的要求。WebSocket支持持久化连接,也就是客户端只需要一次握手,成功后即可进行数据通信。
PHP中可以操作Socket的函数分为两种,一种是以socket_
开头的系列函数,另一种是以stream_
开头的系列函数。socket_
函数是PHP将C语言中的Socket移植过来的实现,而stream_
开头的系列则是PHP使用流stream
的概念将其进行的一层封装。
例如:
$ vim server.php
服务器开发流程
- 根据协议簇或地址簇、套接字类型、协议创建套接字
- 将创建好的套接字绑定到指定主机的端口上
- 服务器开启监听
- 使服务器进入无线循环不退出的状态,当没有客户的连接时程序会阻塞在
accept
上,当有连接进入时才会往下执行,然后再次循环,为客户端提供持久服务。
<?php
//设置脚本超时事件
set_time_limit(60);//保证在连接客户端时脚本不会因为超时而中断执行
//创建socket返回socket句柄
$domain = AF_INET;//域名
$type = SOCK_STREAM;//Socket类型
$protocol = SOL_TCP;//协议类型
$socket = socket_create($domain, $type, $protocol);
if($socket < 0){
$errmsg = socket_strerror($socket);
echo "[create] {$errmsg}".PHP_EOL;
exit;
}
//绑定socket句柄绑定到对应主机的端口
$host = "127.0.0.1";
$port = 1901;
$ret = socket_bind($socket, $host, $port);
if($ret < 0){
$errmsg = socket_strerror($socket);
echo "[bind] {$errmsg}".PHP_EOL;
exit;
}
//监听外部连接
$backlog = 4;
$ret = socket_listen($socket, $backlog);
if($ret < 0){
$errmsg = socket_strerror($socket);
echo "[listen] {$errmsg}".PHP_EOL;
exit;
}
$count = 0;
$max = 5;
do{
//接收来自客户端的请求,调用另一个socket来处理通信。
$spawn = socket_accept($socket);
if($spawn < 0){
$errmsg = socket_strerror($spawn);
echo "[accept] {$errmsg}".PHP_EOL;
break;
}
//写入客户端
$message = "success";
$message = mb_convert_encoding($message, "GBK", "UTF-8");//处理中文乱码
socket_write($spawn, $message, strlen($message));
//获取客户端的输入
$buffer = socket_read($spawn, 8192);
echo "[read] {$buffer}".PHP_EOL;
if(++$count > $max){
break;
}
//关闭客户端socket
socket_close($spawn);
}while(true);
//关闭服务端socket
socket_close($socket);
运行服务器
$ php server.php
客户端
$ vim client.php
<?php
error_reporting(E_ALL);
set_time_limit(0);
$domain = AF_INET;
$type = SOCK_STREAM;
$protocol = SOL_TCP;
$socket = socket_create($domain, $type, $protocol);
if($socket < 0){
$errmsg = socket_strerror($socket);
echo "[create] {$errmsg}".PHP_EOL;
exit;
}
//设置接收套接字流的最大超时时间为1秒
$level = SOL_SOCKET;
$optname = SO_RCVTIMEO;
$optval = ["sec"=>1, "usec"=>0];
socket_set_option($socket, $level, $optname, $optval);
//设置发送套接字流的最大超时时间为6秒
$level = SOL_SOCKET;
$optname = SO_SNDTIMEO;
$optval = ["sec"=>6, "usec"=>0];
socket_set_option($socket, $level, $optname, $optval);
//连接服务器的套接字流
$address = "127.0.0.1";
$port = 1901;
$ret = socket_connect($socket, $address, $port);
if($ret < 0){
$errmsg = socket_strerror($socket);
echo "[create] {$errmsg}".PHP_EOL;
exit;
}
//向服务器写入字符串信息
$buffer = "hello world";
$buffer = mb_convert_encoding($buffer, "GBK", "UTF-8");//统一编码处理乱码
$ret = socket_write($socket, $buffer, strlen($buffer));
if(!$ret){
$errmsg = socket_strerror($socket);
echo "[write] {$errmsg}".PHP_EOL;
exit;
}
//读取服务器返还的套接字流
while($recv = socket_read($socket, 8192)){
echo "[read] {$recv}".PHP_EOL;
}
//关闭套接字流
socket_close($socket);
运行客户端
$ php client.php
代码缺陷:
- 一次只能为一个客户端提供服务,如果正在为第一个客户端发送消息期间有第二人个客户端来连接,那么第二个客户端就必须等待片刻后才行。
- 容易受到攻击,造成拒绝服务。
解决方案:
使用多进程方式,在accept
到一个请求后fork
处一个子进程来出来客户端的请求。
<?php
//创建TCP套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//将套接字绑定到指定主机的端口上
$host = "0.0.0.0";
$port = 9501;
socket_bind($socket, $host, $port);
//监听客户端的套接字连接
socket_listen($socket);
//进入死循环
while(true){
//阻塞直到有客户端连接服务器,阻塞状态进程不占用CPU
$connection = socket_accept($socket);
//当有新客户端连接时fork子进程专门处理
$pid = pcntl_fork();
//在子进程中处理当前连接的业务逻辑
if($pid == 0){
$msg = "success";
socket_write($connection, $msg, strlen($msg));
//休眠5秒用来观察同时为多个客户端提供服务
echo date("Y-m-d H:i:s")." client".PHP_EOL;
sleep(5);
//关闭连接
socket_close($connection);
//退出程序
exit;
}
}
//关闭监听的socket
socket_close($socket);
代码缺陷:如果先后有1w个客户端来请求,此时服务器会fork出1w个子进程来出来每个客户端连接。fork本身是非常消耗系统资源的系统调用。
解决方案:提前预估业务量,在服务器启动时fork出固定数量的子进程,每隔子进程处于无限循环并阻塞在accept
上,当有新客户端连接时处理请求,处理完毕仅仅关闭连接但本身并不销毁,继续等待下一个客户端的请求。避免进程反复fork和销毁带来的巨大资源浪费。
<?php
//创建TCP套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//将套接字绑定到指定主机的端口上
$host = "0.0.0.0";
$port = 9501;
socket_bind($socket, $host, $port);
//监听客户端的套接字连接
socket_listen($socket);
//为主进程设置别名
cli_set_process_title("master");
//按照实际业务量fork出固定个数的子进程
$max_process = 10;
for($i=1; $i<$max_process; $i++){
$pid = pcntl_fork();
if($pid == 0){
cli_set_process_title("worker");
while(true){
$connection = socket_accept($socket);
$msg = "success";
socket_write($connection, $msg, strlen($msg));
socket_close($connection);
}
}
}
$ php server.php
$ ps aux | grep worker
root 545 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 546 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 547 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 548 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 549 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 550 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 551 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 552 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 553 0.0 0.5 340468 5712 pts/1 S 15:30 0:00 worker
root 556 0.0 0.0 11112 976 pts/1 S+ 15:31 0:00 grep worker
$ ps -ef|grep worker|grep -v grep
root 545 1 0 15:30 pts/1 00:00:00 worker
root 546 1 0 15:30 pts/1 00:00:00 worker
root 547 1 0 15:30 pts/1 00:00:00 worker
root 548 1 0 15:30 pts/1 00:00:00 worker
root 549 1 0 15:30 pts/1 00:00:00 worker
root 550 1 0 15:30 pts/1 00:00:00 worker
root 551 1 0 15:30 pts/1 00:00:00 worker
root 552 1 0 15:30 pts/1 00:00:00 worker
root 553 1 0 15:30 pts/1 00:00:00 worker
客户端使用telnet
访问
$ telnet 127.0.0.1 9501
根据进程名称批量杀死进程
$ ps -ef|grep worker|grep -v grep|awk '{print $2}'|xargs kill -9
PHP中有以socket_
开头的一套函数API用于Socket编程,另外PHP5引入了流的抽象概念后,以stream_
开头的一套API也可以用于网络编程,两者主要区别在于:
- 流
stream
是PHP的核心概念,以stream_
开头的函数可用。 - sockets是PHP的一个扩展,大部分情况下都默认启用。
-
socket
系列函数相对底层,stream_
打头系列函数是高层的抽象。
如果有要体验原生Socket编程可用使用socket_
打头的API,否则建议使用stream_
流函数。
socket_create
socket_create
创建一个socket
套接字(通讯节点)资源,成功返回一个socket
套接字,失败返回false
,若参数错误则会给出E_WARNING
警告。
socket_create(string $net, string $stream, string $protocol) : resource
参数列表
参数1:string $net
表示网络协议,可选项包括三种:
-
AF_INET
表示IPv4网路协议,TCP与UDP均使用AF_INET
协议。 -
AF_INET6
表示IPv6网络协议,TCP与UDP均采用。 -
AF_UNIX
表示本地通讯协议,具有高性能和底层本的进程间通讯IPC
。
AF
的全称为Address Family
即地址簇,例如常见的IPv4、IPv6。
参数2:string $stream
表示套接字流即字节流的Socket类型,可选项包括五种:
-
SOCK_STREAM
表示TCP协议套接字 -
SOCK_DGRAM
表示UDP协议套接字
参数3:string $protocol
表示具体所使用的传输协议,可选项包括两种:
-
SOL_TCP
表示TCP协议 -
SOL_UDP
表示UDP协议
socket_connect
socket_connect($socket, $ip, $port):bool
socket_connect
用于连接一个套接字,若成功返回true
失败返回false
。
参数列表
参数1:$socket
表示使用socket_create
函数创建的套接字
参数2:$ip
表示需要连接的套接字所属的主机地址
参数3:$port
表示端口号
socket_bind
socket_bind
函数用于绑定一个套接字,也就是将创建的socket资源绑定到具体的IP地址的端口上,若成功则返回true
失败返回false
。
socket_bind(resource $socket, string $address[, int $port]) : bool
参数列表
参数1:resource $socket
表示使用socket_create
创建的socket资源,可认为是socket对应的编号。
参数2:string $address
表示主机地址
参数3:int $port
表示监听的端口号
socket_listen
socket_listen
函数用于监听在指定地址下监听的套接字资源的收发操作,若成功则返回true
失败返回false
socket_listen(resource $socket [, int $backlog = 0]) : bool
参数列表
参数1:resource $sockt
表示使用socket_create
创建的socket资源
参数2:int $backlog
表示最大监听套接字的个数,等待处理连接队列的最大长度。
socket_accept
socket_accept
表示接收套接字的资源信息,成功返回套接字的信息资源,失败返回false
。监听之后接收一个即将来临的新连接,如果连接建立成功则返回一个新的socket句柄。可理解为子进程,通常父进程用来接收新连接,子进程负责具体的通信。
socket_accept(resource $socket) : resource
参数列表
参数1:resource $socket
表示使用socket_create
创建的socket资源
socket_read
socket_read(resource $socket, int $length): int
socket_read
函数用于读取套接字的资源信息,获取传送的数据,若读取成功则会将套接字的资源转化为字符串,失败则返回false
。
参数列表
-
resource $socket
表示使用socket_create
创建的socket资源 -
int $length
表示socket资源中的buffer的长度
socket_write
socket_write
表示将指定数据写入到对应socket管道中,若成功则返回字符串的字节长度,失败则返回false
。
socket_write(resource $socket, string $buffer [, int $length]) : int
参数列表
-
resource $socket
表示使用socket_create
创建socket资源 -
string $buffer
表示写入到socket资源中的数据 -
int $length
表示控制写入到socket资源中的buffer的长度,如果长度大于buffer的容量则会获取buffer的容量。
socket_close
socket_close
函数用于关闭套接字资源,若成功则返回true
失败返回false
。
socket_close(resource $socket) : bool
参数列表
-
resource $socket
表示使用socket_create
或socket_accept
产生的socket资源,不用用于stream
资源的关闭。
socket_last_error
socket_last_error($socket)
socket_last_error
函数用于获取套接字最后一条错误代码,若成功则返回错误代码。
socket_strerror
socket_strerror($errcode)
socket_strerror
函数用于获取错误代码所对应的错误消息字符串,若成功则返回套接字的错误信息。
参数列表
-
$errcode
是由socket_last_error
函数返回的结果
stream_socket_server
由于创建一个socket的流程总是create、bind、listen,因此PHP提供了一个便捷函数用于一次性创建socket、绑定端口、监听端口。
stream_socket_server(
string $local_socket,
[, int &$errno
[, string &$errstr
[,int $flag = STREAM_SERVER_BIND | STREAM_SERVERR_LISTEN
[, resource $context ]]]]
)
参数列表
-
resource $local_socket
格式为协议名://地址:端口 -
int $errno
表示错误码 -
string $errmsg
表示错误信息 -
bool $flags
表示是否只使用该函数的部分功能 -
resource $context
表示使用stream_context_create
函数创建的资源流上下文