PHP Conference Japan 2024

socket_create

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

socket_create建立一個 socket(通訊端點)

描述

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

建立並傳回一個 Socket 實例,也稱為通訊端點。典型的網路連線由 2 個 socket 組成,一個扮演客戶端的角色,另一個扮演伺服器的角色。

參數

domain

domain 參數指定 socket 要使用的協定族。

可用的位址/協定族
Domain 描述
AF_INET 基於 IPv4 網際網路的協定。TCP 和 UDP 是這個協定族中常見的協定。
AF_INET6 基於 IPv6 網際網路的協定。TCP 和 UDP 是這個協定族中常見的協定。
AF_UNIX 本機通訊協定族。高效率和低開銷使其成為 IPC(程序間通訊)的絕佳形式。
type

type 參數選擇 socket 要使用的通訊類型。

可用的 socket 類型
Type 描述
SOCK_STREAM 提供有序、可靠、全雙工、基於連線的位元組流。可能支援帶外資料傳輸機制。TCP 協定基於此 socket 類型。
SOCK_DGRAM 支援資料包(無連線、不可靠、固定最大長度的訊息)。UDP 協定基於此 socket 類型。
SOCK_SEQPACKET 為固定最大長度的資料包提供有序、可靠、雙向基於連線的資料傳輸路徑;消費者必須在每次讀取呼叫中讀取整個資料包。
SOCK_RAW 提供原始網路協定存取。這種特殊類型的 socket 可用於手動建構任何類型的協定。此 socket 類型的一個常見用途是執行 ICMP 請求(例如 ping)。
SOCK_RDM 提供可靠的資料包層,但不保證排序。這很可能未在您的作業系統上實作。
protocol

protocol 參數設定在返回的 socket 上通訊時,在指定的 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 則註解

42
kyle gibson
19 年前
我花了約 20 分鐘才找出用於 AF_UNIX socket 的正確引數。如果使用其他引數,會收到 PHP 關於「類型」不受支援的警告。我希望這能幫其他人節省時間。

<?php
$socket
= socket_create(AF_UNIX, SOCK_STREAM, 0);
// 程式碼
?>
12
rhollencamp at gmail dot com
15 年前
請注意,如果您使用 AF_UNIX 建立 socket,則會在檔案系統中建立檔案。當您呼叫 socket_close 時,不會移除此檔案 - 您應該在關閉 socket 後取消連結該檔案。
8
ab1965 at yandex dot ru
12 年前
我花了一些時間才了解一個 PHP 程序如何透過 unix udp socket 與另一個程序通訊。下方提供「伺服器」和「客戶端」程式碼範例。假設伺服器在客戶端啟動之前執行。

「伺服器」程式碼
<?php
if (!extension_loaded('sockets')) {
die(
'The sockets extension is not loaded.');
}
// 建立 unix udp socket
$socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
if (!
$socket)
die(
'無法建立 AF_UNIX socket');

// 同一個 socket 將用於 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(
'無法設定 socket 為封鎖模式');
$buf = '';
$from = '';
echo
"準備接收...\n";
// 將會封鎖等待客戶端查詢
$bytes_received = socket_recvfrom($socket, $buf, 65536, 0, $from);
if (
$bytes_received == -1)
die(
'接收 socket 資料時發生錯誤');
echo
"從 $from 接收到 $buf\n";

$buf .= "->回應"; // 在此處理客戶端查詢

// 發送回應
if (!socket_set_nonblock($socket))
die(
'無法設定 socket 為非封鎖模式');
// 客戶端的 socket 檔名從客戶端請求得知:$from
$len = strlen($buf);
$bytes_sent = socket_sendto($socket, $buf, $len, 0, $from);
if (
$bytes_sent == -1)
die(
'傳送 socket 資料時發生錯誤');
else if (
$bytes_sent != $len)
die(
$bytes_sent . ' 位元組已傳送,而不是預期的 ' . $len . ' 位元組');
echo
"請求已處理\n";
}
?>

'客戶端' 程式碼
<?php
if (!extension_loaded('sockets')) {
die(
'The sockets extension is not loaded.');
}
// 建立 unix udp socket
$socket = socket_create(AF_UNIX, SOCK_DGRAM, 0);
if (!
$socket)
die(
'無法建立 AF_UNIX socket');

// 同一個 socket 將用於稍後的 recv_from
// 如果您只想發送而不接收,則不需要綁定
$client_side_sock = dirname(__FILE__)."/client.sock";
if (!
socket_bind($socket, $client_side_sock))
die(
"無法綁定到 $client_side_sock");

// 使用 socket 發送資料
if (!socket_set_nonblock($socket))
die(
'無法設定 socket 為非封鎖模式');
// 伺服端的 socket 檔名是預先知道的
$server_side_sock = dirname(__FILE__)."/server.sock";
$msg = "訊息";
$len = strlen($msg);
// 此時 '伺服器' 進程必須正在運行並綁定以從 serv.sock 接收
$bytes_sent = socket_sendto($socket, $msg, $len, 0, $server_side_sock);
if (
$bytes_sent == -1)
die(
'傳送 socket 資料時發生錯誤');
else if (
$bytes_sent != $len)
die(
$bytes_sent . ' 位元組已傳送,而不是預期的 ' . $len . ' 位元組');

// 使用 socket 接收資料
if (!socket_set_block($socket))
die(
'無法設定 socket 為封鎖模式');
$buf = '';
$from = '';
// 將會封鎖等待伺服器回應
$bytes_received = socket_recvfrom($socket, $buf, 65536, 0, $from);
if (
$bytes_received == -1)
die(
'接收 socket 資料時發生錯誤');
echo
"從 $from 接收到 $buf\n";

// 關閉 socket 並刪除自己的 .sock 檔案
socket_close($socket);
unlink($client_side_sock);
echo
"客戶端退出\n";
?>
5
geoff at spacevs dot com
14 年前
這是一個 PHP 的 ping 函式,不使用 exec/system/passthrough/etc... 非常適合在嘗試連線到主機之前,先測試主機是否在線上。逾時時間以秒為單位。

<?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;
}
?>
1
Jean Charles MAMMANA
16 年前
我使用 SOCK_RAW 透過 socket_create() 撰寫了 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); // type = 8 : 請求
$packet .= chr(0); // code = 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 = "Select 錯誤";
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 ///
?>
-1
alexander dot krause at ed-solutions dot de
16 年前
在 UNIX 系統上,php 需要 /etc/protocols 這個檔案才能使用 SOL_UDP 和 SOL_TCP 等常數。

我的嵌入式平台上缺少這個檔案。
-1
jens at surefoot dot com
18 年前
請注意,RAW sockets(如 ping 範例所使用)在 *nix 系統上僅限 root 帳戶使用。由於網頁伺服器幾乎不會以 root 身分執行,因此它們在網頁上無法運作。

在 Windows 伺服器上,無論如何都應該可以正常運作。
-1
evan at coeus hyphen group dot com
22 年前
好的,我稍微和 Richard 聊了一下(透過電子郵件)。我們都同意 getprotobyname() 和使用常數在功能和速度上應該是一樣的,使用哪一種純粹是程式碼風格的問題。就個人而言,我們都覺得常數比較美觀 :)。

這八種不同的協定是 PHP 中實作的,而不是現存的協定總數(RFC 1340 有 98 種)。

我們唯一不同意的是使用 0 - Richard 說「根據官方的 Unix/BSD sockets,0 是完全沒問題的」。我認為由於根據 RFC 1320,0 是一個保留數字,而且在使用時通常指的是 IP,而不是其子協定之一(TCP、UDP 等)。
-4
david at eder dot 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 都會回傳 ack 的資訊
// 516 = 4 位元組的標頭 + 512 位元組的資料
socket_recvfrom($socket, $buffer, 516, 0, $host, $port);

// 將資料封包中的區塊號碼加到 ack 封包中
$packet = chr(0) . chr(4) . substr($buffer, 2, 2);
// 發送 ack
socket_sendto($socket, $packet, strlen($packet), 0, $host, $port);

// 將資料附加到回傳變數
// 對於大型檔案,此函式應該將檔案控制代碼作為引數
$ret .= substr($buffer, 4);
}
while(
strlen($buffer) == 516); // 第一個非完整封包是最後一個。
return $ret;
}
?>
-3
Anonymous
18 年前
這是一個使用 sockets 而不是 exec() 的 ping 函式。注意:我無法在沒有以 root 身分從 CLI 執行的情况下讓 socket_create() 工作。我已經計算了封包的檢查碼以簡化程式碼(訊息是 'ping',但實際上並不重要)。

<?php

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

/* 建立 socket,最後的 '1' 表示 ICMP */
$socket = socket_create(AF_INET, SOCK_RAW, 1);

/* 設定 socket 接收逾時為 1 秒 */
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 1, "usec" => 0));

/* 連接到 socket */
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);
}

?>
-5
david at eder dot 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;
}
?>
-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";
/* 建立 socket,最後的 '1' 表示 ICMP */
$socket = socket_create(AF_INET6, SOCK_RAW, getprotobyname('ipv6-icmp'));
/* 設定 socket 接收逾時為 1 秒 */
$sec=intval($timeout/1000);
$usec=$timeout%1000*1000;
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>$sec, "usec"=>$usec));
/* socket 封包參數 */
$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;
/* socket 連線 */
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