创建一个套接字
socket_create
创建并返回一个套接字,也称作一个通讯节点。一个典型的网络连接由 2 个套接字构成,一个运行在客户端,另一个运行在服务器端。
resource socket_create ( int $domain , int $type , int $protocol )
@parameter domain
参数指定哪个协议用在当前套接字上
Domain | 描述 |
---|---|
AF_INET |
IPv4 网络协议。TCP 和 UDP 都可使用此协议。 |
AF_INET6 |
IPv6 网络协议。TCP 和 UDP 都可使用此协议。 |
AF_UNIX |
本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。 |
@parameter type
参数用于选择套接字使用的类型
类型 | 描述 |
---|---|
SOCK_STREAM |
提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP 协议即基于这种流式套接字。 |
SOCK_DGRAM |
提供数据报文的支持。(无连接,不可靠、固定最大长度).UDP协议即基于这种数据报文套接字。 |
SOCK_SEQPACKET |
提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信;数据端通过接收每一个数据段来读取整个数据包。 |
SOCK_RAW |
提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现 ICMP 请求(例如 ping)。 |
SOCK_RDM |
提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。 |
@parameter protocol
是设置指定domain
套接字下的具体协议。这个值可以使用 getprotobyname() 函数进行读取。如果所需的协议是 TCP 或 UDP,可以直接使用常量SOL_TCP
和SOL_UDP
。
名称 | 描述 |
---|---|
icmp | Internet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如“ping”命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。 |
udp | User Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。 |
tcp | Transmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。 |
返回值
socket_create() 正确时返回一个套接字,失败时返回FALSE
。要读取错误代码,可以调用 socket_last_error()。这个错误代码可以通过 socket_strerror() 读取文字的错误说明。
socket_bind
给套接字绑定名字,注意:绑定 address
到 socket
。 该操作必须是在使用 socket_connect() 或者 socket_listen() 建立一个连接之前。
socket_bind ( resource $socket , string $address [, int $port = 0 ] ) : bool
@parameter socket
用 socket_create() 创建的一个有效的套接字资源。@parameter address
- 如果套接字是
AF_INET
族,那么address
必须是一个四点分法的 IP 地址(例如 127.0.0.1 )。 - 如果套接字是
AF_UNIX
族,那么address
是 Unix 套接字一部分(例如 /tmp/my.sock )
- 如果套接字是
@parameter port
参数port
仅仅用于AF_INET
套接字连接的时候,并且指定连接中需要监听的端口号。
socket_listen
监听socket连接
socket_listen ( resource $socket [, int $backlog = 0 ] ) : bool
@parameter socket
用 socket_create() 创建的一个有效的套接字资源。@parameter backlog
端口号
创建套接字实例
public function service(){
$tcp = getprotobyname("tcp"); //获取tcp协议号码。
$sock = socket_create(AF_INET, SOCK_STREAM, $tcp);//创建一个套接字
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
if($sock < 0)
{
throw new Exception("failed to create socket: ".socket_strerror($sock)."\n");//获取错误
}
socket_bind($sock, $this->address, $this->port);//绑定IP和端口
socket_listen($sock, $this->port);//监听端口
echo "listen on $this->address $this->port ... \n";
$this->_sockets = $sock;
}
握手处理
- 我们直接从代码示例看,这个类方法传入了两个参数,一个是socket、一个是请求包,在接收到请求包后会遍历这个请求包,获取中的
Sec-WebSocket-Key'
,Sec-WebSocket-Key
加上一个特殊字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,然后计算 SHA-1 ,之后进行 BASE-64 编码,将结果做为Sec-WebSocket-Accept
头的值并且插入在响应包中 返回给客户端。 socket_write
将字符串写入socket中
socket_write( resource `$socket` , string `$buffer` [, int `$length` = 0 ] ) : int
/**
* 握手处理
* @param $newClient socket
* @return int 接收到的信息
*/
public function handshaking($newClient, $line){
$headers = array();
$lines = preg_split("/\r\n/", $line);
foreach($lines as $line)
{
$line = chop($line);//chop()若为空则移除\n\r空格之类的。
if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))//正则获取header头
{
$headers[$matches[1]] = $matches[2];//将header存入数组
}
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $this->address\r\n" .
"WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n".
"Sec-WebSocket-Accept:$secAccept\r\n\r\n"; //构造响应包
return socket_write($newClient, $upgrade, strlen($upgrade));
}
Websocket的数据处理
数据帧格式
WebSocket客户端,服务端通信的最小单位是帧(frame),由一个或多个帧组成一条完整的消息(message)
- 数据帧格式详解:
FIN
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是消息(message)的最后一个分片(fragment)
Opcode:
4个比特。
操作码
Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作码是不认识的,那么接收端应该断开连接(fail the connection)
Mask: 1个比特。
从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
Payload length: 数据载荷的长度。 7位 或 7 + 16 位 或 7 + 64位。
假设 Payload length == x
x 为 0-126: 数据长度为x字节.
x 为 126 : 后续2个字节代表一个16位无符号整数
x 为 127 : 后续8个字节代表一个64位无符号整数
掩码算法:
首先,假设:
original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
- 数据帧的解析如下:
/**
* 解析接收数据
* @param $buffer
* @return null|string
*/
public function message($buffer){
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;//取出第8-15位
if ($len === 126) { // 126<=数据长度<= 65535 Extended payload length 有 占用2个字节
$masks = substr($buffer, 4, 4);//帧格式 Masking-key 占用4字节
$data = substr($buffer, 8);//第九个字节(索引 8 )开始 是数据
} else if ($len === 127) { // 65535 <= 数据长度 ;Extended payload length 有 占用8个字节
$masks = substr($buffer, 10, 4);// 说明 Extended payload length 占用 8个字节 2 + 8 所以从 10开始
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];//掩码转换为原文
}
return $decoded;
}