PHP Conference Japan 2024

socket_create

(PHP 4 >= 4.1.0, PHP 5, PHP 7, PHP 8)

socket_create创建一个套接字(通讯端点)

说明

socket_create(int $domain, int $type, int $protocol): Socket|false

创建并返回一个 Socket 实例,也称为通讯端点。一个典型的网络连接由 2 个套接字组成,一个执行客户端角色,另一个执行服务器端角色。

参数

domain

domain 参数指定套接字使用的协议族。

可用的地址/协议族
域(Domain) 说明
AF_INET 基于 IPv4 的互联网协议。TCP 和 UDP 是此协议族的常见协议。
AF_INET6 基于 IPv6 的互联网协议。TCP 和 UDP 是此协议族的常见协议。
AF_UNIX 本地通讯协议族。高效且开销低,使其成为 IPC(进程间通讯)的绝佳形式。
type

type 参数选择套接字使用的通讯类型。

可用的套接字类型
类型 说明
SOCK_STREAM 提供顺序的、可靠的、全双工的、基于连接的字节流。可能支持带外数据传输机制。TCP 协议基于此套接字类型。
SOCK_DGRAM 支持数据报(无连接、不可靠的固定最大长度消息)。UDP 协议基于此套接字类型。
SOCK_SEQPACKET 为固定最大长度的数据报提供顺序的、可靠的、双向的基于连接的数据传输路径;使用者需要在每次读取调用时读取整个数据包。
SOCK_RAW 提供原始网络协议访问。这种特殊类型的套接字可用于手动构造任何类型的协议。此套接字类型的一个常见用途是执行 ICMP 请求(如 ping)。
SOCK_RDM 提供不保证顺序的可靠数据报层。这很可能在你的操作系统上没有实现。
protocol

protocol 参数设置在返回的套接字上进行通讯时使用的指定 domain 内的特定协议。可以使用 getprotobyname() 按名称检索正确的值。如果所需的协议是 TCP 或 UDP,也可以使用相应的常量 SOL_TCPSOL_UDP

常见协议
名称 说明
icmp 互联网控制消息协议主要由网关和主机用于报告数据报通讯中的错误。大多数现代操作系统中都存在的“ping”命令是 ICMP 的一个应用示例。
udp 用户数据报协议是一种无连接、不可靠、具有固定记录长度的协议。由于这些方面,UDP 需要最少的协议开销。
tcp 传输控制协议是一种可靠的、基于连接的、面向流的、全双工协议。TCP 保证所有数据包将按照发送顺序接收。如果在通讯过程中有任何数据包丢失,TCP 将自动重新传输数据包,直到目标主机确认收到该数据包。出于可靠性和性能原因,TCP 实现本身决定底层数据报通讯层的适当八位字节边界。因此,TCP 应用程序必须允许部分记录传输的可能性。

返回值

socket_create() 成功时返回一个 Socket 实例,错误时返回 false。可以通过调用 socket_last_error() 检索实际的错误代码。可以将此错误代码传递给 socket_strerror() 以获取错误的文本说明。

错误/异常

如果给出了无效的 domaintypesocket_create() 将分别默认使用 AF_INETSOCK_STREAM,并额外发出一条 E_WARNING 消息。

更新日志

版本 说明
8.0.0 此函数现在成功时返回一个 Socket 实例;之前返回的是一个 资源(resource)

参见

添加注释

用户贡献的注释 12 条

up
42
kyle gibson
19 年前
我花了大约 20 分钟才找到 AF_UNIX 套接字的正确参数。如果使用其他任何参数,我都会收到一个关于“type”不受支持的 PHP 警告。我希望这能节省其他人的时间。

<?php
$socket
= socket_create(AF_UNIX, SOCK_STREAM, 0);
// 代码
?>
up
12
rhollencamp at gmail dot com
15 年前
请注意,如果使用 AF_UNIX 创建套接字,则会在文件系统中创建一个文件。调用 socket_close 时不会删除此文件 - 你应该在关闭套接字后取消链接该文件。
up
8
ab1965 at yandex dot ru
12 年前
我花了一些时间才弄明白一个 PHP 进程如何通过 unix udp 套接字与另一个进程进行通信。下面给出了“服务器”和“客户端”代码的示例。假设服务器在客户端启动之前运行。

“服务器”代码
<?php
if (!extension_loaded('sockets')) {
die(
'sockets 扩展未加载。');
}
// 创建 UNIX UDP 套接字
$socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
if (!
$socket)
die(
'无法创建 AF_UNIX 套接字');

// 同一个套接字将用于 recv_from 和 send_to
$server_side_sock = dirname(__FILE__)."/server.sock";
if (!
socket_bind($socket, $server_side_sock))
die(
"无法绑定到 $server_side_sock");

while(
1) // 服务器永不退出
{
// 接收查询
if (!socket_set_block($socket))
die(
'无法为套接字设置阻塞模式');
$buf = '';
$from = '';
echo
"准备接收...\n";
// 将阻塞以等待客户端查询
$bytes_received = socket_recvfrom($socket, $buf, 65536, 0, $from);
if (
$bytes_received == -1)
die(
'从套接字接收时发生错误');
echo
"从 $from 接收到 $buf\n";

$buf .= "->Response"; // 在此处处理客户端查询

// 发送响应
if (!socket_set_nonblock($socket))
die(
'无法为套接字设置非阻塞模式');
// 客户端套接字文件名可从客户端请求中得知:$from
$len = strlen($buf);
$bytes_sent = socket_sendto($socket, $buf, $len, 0, $from);
if (
$bytes_sent == -1)
die(
'发送到套接字时发生错误');
else if (
$bytes_sent != $len)
die(
$bytes_sent . ' 字节已发送,而不是预期的 ' . $len . ' 字节');
echo
"请求已处理\n";
}
?>

“客户端”代码
<?php
if (!extension_loaded('sockets')) {
die(
'sockets 扩展未加载。');
}
// 创建 UNIX UDP 套接字
$socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
if (!
$socket)
die(
'无法创建 AF_UNIX 套接字');

// 稍后将在 recv_from 中使用同一个套接字
// 如果您只想发送而从不接收,则无需绑定
$client_side_sock = dirname(__FILE__)."/client.sock";
if (!
socket_bind($socket, $client_side_sock))
die(
"无法绑定到 $client_side_sock");

// 使用套接字发送数据
if (!socket_set_nonblock($socket))
die(
'无法为套接字设置非阻塞模式');
// 服务器端套接字文件名是预先知道的
$server_side_sock = dirname(__FILE__)."/server.sock";
$msg = "Message";
$len = strlen($msg);
// 此时,“服务器”进程必须正在运行并绑定到从 serv.sock 接收
$bytes_sent = socket_sendto($socket, $msg, $len, 0, $server_side_sock);
if (
$bytes_sent == -1)
die(
'发送到套接字时发生错误');
else if (
$bytes_sent != $len)
die(
$bytes_sent . ' 字节已发送,而不是预期的 ' . $len . ' 字节');

// 使用套接字接收数据
if (!socket_set_block($socket))
die(
'无法为套接字设置阻塞模式');
$buf = '';
$from = '';
// 将阻塞以等待服务器响应
$bytes_received = socket_recvfrom($socket, $buf, 65536, 0, $from);
if (
$bytes_received == -1)
die(
'从套接字接收时发生错误');
echo
"从 $from 接收到 $buf\n";

// 关闭套接字并删除自己的 .sock 文件
socket_close($socket);
unlink($client_side_sock);
echo
"客户端退出\n";
?>
up
5
geoff at spacevs dot com
14 年前
这是一个不使用 exec/system/passthrough/etc... 的 PHP ping 函数。在尝试连接到主机之前,先用它来测试主机是否在线非常有用。超时以秒为单位。

<?PHP
function ping($host, $timeout = 1) {
/* 预先计算好校验和的 ICMP ping 数据包 */
$package = "\x08\x00\x7d\x4b\x00\x00\x00\x00PingHost";
$socket = socket_create(AF_INET, SOCK_RAW, 1);
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $timeout, 'usec' => 0));
socket_connect($socket, $host, null);

$ts = microtime(true);
socket_send($socket, $package, strLen($package), 0);
if (
socket_read($socket, 255))
$result = microtime(true) - $ts;
else
$result = false;
socket_close($socket);

return
$result;
}
?>
up
1
让·查尔斯·马马纳 (Jean Charles MAMMANA)
16 年前
我使用 `socket_create()` 和 `SOCK_RAW` 编写了 `ping()` 函数。
(在 Unix 系统上,您需要具有 root 权限才能执行此函数)

<?php

/// ping.inc.php 开始 ///

$g_icmp_error = "没有错误";

// 超时时间,单位为毫秒
function ping($host, $timeout)
{
$port = 0;
$datasize = 64;
global
$g_icmp_error;
$g_icmp_error = "没有错误";
$ident = array(ord('J'), ord('C'));
$seq = array(rand(0, 255), rand(0, 255));

$packet = '';
$packet .= chr(8); // 类型 = 8 : 请求
$packet .= chr(0); // 代码 = 0

$packet .= chr(0); // 校验和初始化
$packet .= chr(0); // 校验和初始化

$packet .= chr($ident[0]); // 标识符
$packet .= chr($ident[1]); // 标识符

$packet .= chr($seq[0]); // 序列号
$packet .= chr($seq[1]); // 序列号

for ($i = 0; $i < $datasize; $i++)
$packet .= chr(0);

$chk = icmpChecksum($packet);

$packet[2] = $chk[0]; // 校验和初始化
$packet[3] = $chk[1]; // 校验和初始化

$sock = socket_create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
$time_start = microtime();
socket_sendto($sock, $packet, strlen($packet), 0, $host, $port);


$read = array($sock);
$write = NULL;
$except = NULL;

$select = socket_select($read, $write, $except, 0, $timeout * 1000);
if (
$select === NULL)
{
$g_icmp_error = "选择错误";
socket_close($sock);
return -
1;
}
elseif (
$select === 0)
{
$g_icmp_error = "超时";
socket_close($sock);
return -
1;
}

$recv = '';
$time_stop = microtime();
socket_recvfrom($sock, $recv, 65535, 0, $host, $port);
$recv = unpack('C*', $recv);

if (
$recv[10] !== 1) // ICMP 协议 = 1
{
$g_icmp_error = "不是 ICMP 数据包";
socket_close($sock);
return -
1;
}

if (
$recv[21] !== 0) // ICMP 响应 = 0
{
$g_icmp_error = "不是 ICMP 响应";
socket_close($sock);
return -
1;
}

if (
$ident[0] !== $recv[25] || $ident[1] !== $recv[26])
{
$g_icmp_error = "错误的标识号";
socket_close($sock);
return -
1;
}

if (
$seq[0] !== $recv[27] || $seq[1] !== $recv[28])
{
$g_icmp_error = "错误的序列号";
socket_close($sock);
return -
1;
}

$ms = ($time_stop - $time_start) * 1000;

if (
$ms < 0)
{
$g_icmp_error = "响应时间过长";
$ms = -1;
}

socket_close($sock);

return
$ms;
}

function
icmpChecksum($data)
{
$bit = unpack('n*', $data);
$sum = array_sum($bit);

if (
strlen($data) % 2) {
$temp = unpack('C*', $data[strlen($data) - 1]);
$sum += $temp[1];
}

$sum = ($sum >> 16) + ($sum & 0xffff);
$sum += ($sum >> 16);

return
pack('n*', ~$sum);
}

function
getLastIcmpError()
{
global
$g_icmp_error;
return
$g_icmp_error;
}
/// ping.inc.php 结束 ///
?>
up
-1
alexander 点 krause 在 ed-solutions 点 de
16 年前
在 UNIX 系统上,PHP 需要 /etc/protocols 文件来获取诸如 SOL_UDP 和 SOL_TCP 之类的常量。

在我的嵌入式平台上缺少此文件。
up
-1
jens 在 surefoot 点 com
18 年前
请注意,在 *nix 系统上,RAW 套接字(如用于 ping 示例)仅限于 root 帐户。由于 Web 服务器几乎从不以 root 身份运行,因此它们无法在网页上工作。

在基于 Windows 的服务器上,它应该可以正常工作。
up
-1
evan 在 coeus 减号 group 点 com
22 年前
好的,我和 Richard 通过电子邮件聊了一下。我们都认为 getprotobyname() 和使用常量在功能和速度上应该相同,使用哪一个只是编码风格问题。就个人而言,我们都认为常量更美观 :)

这八个不同的协议是在 PHP 中实现的,而不是存在的总数(RFC 1340 中有 98 个)。

我们唯一意见不一致的是使用 0 - Richard 说:“根据官方 unix/bsd 套接字,0 是完全可以的。”我认为,根据 RFC 1320,0 是一个保留数字,并且通常用于指代 IP,而不是其子协议(TCP、UDP 等)之一。
up
-4
david 在 eder 点 us
20 年前
似乎没有任何 UDP 客户端的示例。这是一个 tftp 客户端。希望这能让某些人生活更轻松。

<?php
function tftp_fetch($host, $filename)
{
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);

// 创建请求数据包
$packet = chr(0) . chr(1) . $filename . chr(0) . 'octet' . chr(0);
// UDP 是无连接的,所以我们只管发送。
socket_sendto($socket, $packet, strlen($packet), 0x100, $host, 69);

$buffer = '';
$port = '';
$ret = '';
do
{
// $buffer 和 $port 都会返回确认所需的信息
// 516 = 4 字节的头部 + 512 字节的数据
socket_recvfrom($socket, $buffer, 516, 0, $host, $port);

// 将数据包中的块编号添加到确认包中
$packet = chr(0) . chr(4) . substr($buffer, 2, 2);
// 发送确认
socket_sendto($socket, $packet, strlen($packet), 0, $host, $port);

// 将数据追加到返回变量中
// 对于大文件,此函数应使用文件句柄作为参数
$ret .= substr($buffer, 4);
}
while(
strlen($buffer) == 516); // 第一个非完整数据包是最后一个。
return $ret;
}
?>
up
-3
匿名
18 年前
这是一个使用套接字而不是 exec() 的 ping 函数。注意:如果不以 root 身份从 CLI 运行,我无法使 socket_create() 工作。我已经计算了数据包的校验和以简化代码(消息是 'ping',但实际上无关紧要)。

<?php

function ping($host) {
$package = "\x08\x00\x19\x2f\x00\x00\x00\x00\x70\x69\x6e\x67";

/* 创建套接字,最后一个 '1' 表示 ICMP */
$socket = socket_create(AF_INET, SOCK_RAW, 1);

/* 设置套接字接收超时为 1 秒 */
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 1, "usec" => 0));

/* 连接到套接字 */
socket_connect($socket, $host, null);

/* 记录开始时间 */
list($start_usec, $start_sec) = explode(" ", microtime());
$start_time = ((float) $start_usec + (float) $start_sec);

socket_send($socket, $package, strlen($package), 0);

if(@
socket_read($socket, 255)) {
list(
$end_usec, $end_sec) = explode(" ", microtime());
$end_time = ((float) $end_usec + (float) $end_sec);

$total_time = $end_time - $start_time;

return
$total_time;
} else {
return
false;
}

socket_close($socket);
}

?>
up
-5
david 在 eder 点 us
19 年前
有时在运行 CLI 时,您需要知道自己的 IP 地址。

<?php

$addr
= my_ip();
echo
"我的 IP 地址是 $addr\n";

function
my_ip($dest='64.0.0.0', $port=80)
{
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_connect($socket, $dest, $port);
socket_getsockname($socket, $addr, $port);
socket_close($socket);
return
$addr;
}
?>
up
-4
Sakrizz
8 年前
这是使用 php 实现 icmpv6 ping 的解决方案,如果有人在使用 php 处理 icmpv6 时遇到问题,请将其放在此处。

<?php
$host
= "2a03:2880:f11b:83:face:b00c:0:25de";
$timeout = 100000;
$count = 3;
echo
"延迟:". round(1000 * pingv6($host,$timeout,$count),5) ." 毫秒 \n";

function
pingv6($target,$timeout,$count) {
echo
"目标是 IPv6 地址,". getprotobyname('ipv6-icmp'). " \n";
/* 创建套接字,最后一个 '1' 表示 ICMP */
$socket = socket_create(AF_INET6, SOCK_RAW, getprotobyname('ipv6-icmp'));
/* 设置套接字接收超时为 1 秒 */
$sec=intval($timeout/1000);
$usec=$timeout%1000*1000;
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$sec, "usec"=>$usec));
/* 套接字包参数 */
$type = "\x80";
$seqNumber = chr(floor($i/256)%256) . chr($i%256);
$checksum= "\x00\x00";
$code = "\x00";
$identifier = chr(rand(0,255)) . chr(rand(0,255));
$msg = "!\"#$%&'()*+,-./1234567";
$package = $type.$code.$checksum.$identifier.$seqNumber.$msg;
$checksum = icmpChecksum($package);
$package = $type.$code.$checksum.$identifier.$seqNumber.$msg;
/* 套接字连接 */
if(@socket_connect($socket, $target, null)){
for(
$i = 0; $i < $count; $i++){
list(
$start_usec, $start_sec) = explode(" ", microtime());
$start_time = ((float) $start_usec + (float) $start_sec);
$startTime = microtime(true);
socket_send($socket, $package, strLen($package), 0);
while (
$startTime + $timeout*1000 > microtime(true)){
if(
socket_read($socket, 255) !== false) {
list(
$end_usec, $end_sec) = explode(" ", microtime());
$end_time = ((float) $end_usec + (float) $end_sec);
$total_time = $end_time - $start_time;
echo
"往返时间 (".$i."): ". $total_time ."\n";
return
$total_time;
break;
}else{
return
"null";
echo
"超时 (".$i."),未收到回显应答\n";
break;
}
}
usleep($interval*1000);
}
socket_close($socket);
}
}

function
icmpChecksum($data){
if (
strlen($data)%2) $data .= "\x00";
$bit = unpack('n*', $data);
$sum = array_sum($bit);
while (
$sum >> 16)
$sum = ($sum >> 16) + ($sum & 0xffff);
return
pack('n*', ~$sum);
}

?>
To Top