天天看點

淺析PHP Socket技術

phpsocketSocket位于TCP/IP協定的傳輸控制協定,提供客戶-伺服器模式的異步通信,即客戶向伺服器發出服務請求,伺服器接收到請求後,提供相應的回報或服務!我練習了一個最基本的例子:

使用并發起一個阻塞式(block)連接配接,即伺服器如果不傳回資料流,則一直保持連接配接狀态,一旦有資料流傳入,取得内容後就立即斷開連接配接。代碼如下:

<?php
$host = www.sohu.com; //這個位址随便,用新浪的也行,主要是測試用,哪個無所謂
$page = "/index.html";
$port = 80;
$request = "GET $page HTTP/1.1\r\n";
$request .= "Host: $host\r\n";
//$request .= "Referer:$host\r\n";
$request .= "Connection: close\r\n\r\n";
//允許連接配接的逾時時間為1.5秒
$connectionTimeout = 1.5;
//允許遠端伺服器2秒鐘内完成回應
$responseTimeout = 2;
//建立一個socket連接配接
$fp = fsockopen($host, $port, $errno, $errstr, $connectionTimeout);
if (!$fp) {
    throw new Exception("Connection to $hostfailed:$errstr");
} else {
    stream_set_blocking($fp, true);
    stream_set_timeout($fp, $responseTimeout);
}
//發送請求字元串
fwrite($fp, $request);
//取得傳回的資料流内容
$content = stream_get_contents($fp);
echo $content;
$meta = stream_get_meta_data($fp);
if ($meta['timed_out']) {
    throw new Exception("Responsefrom web services server timed out.");
}
//關閉Socket連接配接
fclose($fp);
?>      

◇ Socket基礎

◇ 産生一個伺服器

◇  産生一個用戶端

在這一章裡你将了解到迷人而又讓人容易糊塗的套接字(Sockets)。Sockets在PHP中是沒有充分利用的功能。今天你将看到産生一個能使用用戶端連接配接的伺服器,并在用戶端使用socket進行連接配接,伺服器端将詳細的處理資訊發送給用戶端。

當你看到完整的socket過程,那麼你将會在以後的程式開發中使用它。這個伺服器是一個能讓你連接配接的HTTP伺服器,用戶端是一個Web浏覽器,這是一個單一的 用戶端/伺服器 的關系。

◆ Socket 基礎

PHP使用Berkley的socket庫來建立它的連接配接。你可以知道socket隻不過是一個資料結構。你使用這個socket資料結構去開始一個用戶端和伺服器之間的會話。這個伺服器是一直在監聽準備産生一個新的會話。當一個用戶端連接配接伺服器,它就打開伺服器正在進行監聽的一個端口進行會話。這時,伺服器端接受用戶端的連接配接請求,那麼就進行一次循環。現在這個用戶端就能夠發送資訊到伺服器,伺服器也能發送資訊給用戶端。

産生一個Socket,你需要三個變量:一個協定、一個socket類型和一個公共協定類型。産生一個socket有三種協定供選擇,繼續看下面的内容來擷取詳細的協定内容。

定義一個公共的協定類型是進行連接配接一個必不可少的元素。下面的表我們看看有那些公共的協定類型。

表一:協定

名字/常量     描述

AF_INET     這是大多數用來産生socket的協定,使用TCP或UDP來傳輸,用在IPv4的位址

AF_INET6     與上面類似,不過是來用在IPv6的位址

AF_UNIX   本地協定,使用在Unix和Linux系統上,它很少使用,一般都是當用戶端和伺服器在同一台及其上的時候使用

表二:Socket類型

名字/常量     描述

SOCK_STREAM   這個協定是按照順序的、可靠的、資料完整的基于位元組流的連接配接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。

SOCK_DGRAM   這個協定是無連接配接的、固定長度的傳輸調用。該協定是不可靠的,使用UDP來進行它的連接配接。

SOCK_SEQPACKET   這個協定是雙線路的、可靠的連接配接,發送固定長度的資料包進行傳輸。必須把這個包完整的接受才能進行讀取。

SOCK_RAW   這個socket類型提供單一的網絡通路,這個socket類型使用ICMP公共協定。(ping、traceroute使用該協定)

SOCK_RDM   這個類型是很少使用的,在大部分的作業系統上沒有實作,它是提供給資料鍊路層使用,不保證資料包的順序

表三:公共協定

名字/常量     描述

ICMP   網際網路控制消息協定,主要使用在網關和主機上,用來檢查網絡狀況和報告錯誤資訊

UDP       使用者資料封包協定,它是一個無連接配接,不可靠的傳輸協定

TCP 傳輸控制協定,這是一個使用最多的可靠的公共協定,它能保證資料包能夠到達接受者那兒,如果在傳輸過程中發生錯誤,那麼它将重新發送出錯資料包。

現在你知道了産生一個socket的三個元素,那麼我們就在php中使用socket_create()函數來産生一個socket。這個socket_create()函數需要三個參數:一個協定、一個socket類型、一個公共協定。socket_create()函數運作成功傳回一個包含socket的資源類型,如果沒有成功則傳回false。

Resourece socket_create(int protocol, int socketType, int commonProtocol);

現在你産生一個socket,然後呢?php提供了幾個操縱socket的函數。你能夠綁定socket到一個IP,監聽一個socket的通信,接受一個socket;現在我們來看一個例子,了解函數是如何産生、接受和監聽一個socket。

<?php
$commonProtocol = getprotobyname(“tcp”);
$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol);
socket_bind($socket, ‘localhost’, 1337);
socket_listen($socket);
// More socket functionality to come
?>      

上面這個例子産生一個你自己的伺服器端。例子第一行,

$commonProtocol = getprotobyname(“tcp”);      

使用公共協定名字來擷取一個協定類型。在這裡使用的是TCP公共協定,如果你想使用UDP或者ICMP協定,那麼你應該把getprotobyname()函數的參數改為“udp”或“icmp”。還有一個可選的辦法是不使用getprotobyname()函數而是指定SOL_TCP或SOL_UDP在socket_create()函數中。

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);      

例子的第二行是産生一個socket并且傳回一個socket資源的執行個體。在你有了一個socket資源的執行個體以後,你就必須把socket綁定到一個IP位址和某一個端口上。

socket_bind($socket, ‘localhost’, 1337);      

在這裡你綁定socket到本地計算機(127.0.0.1)和綁定socket到你的1337端口。然後你就需要監聽所有進來的socket連接配接。

socket_listen($socket);      

在第四行以後,你就需要了解所有的socket函數和他們的使用。

表四:Socket函數

函數名       描述

socket_accept()     接受一個Socket連接配接

socket_bind()     把socket綁定在一個IP位址和端口上

socket_clear_error()   清除socket的錯誤或者最後的錯誤代碼

socket_close()     關閉一個socket資源

socket_connect()     開始一個socket連接配接

socket_create_listen()   在指定端口打開一個socket監聽

socket_create_pair()   産生一對沒有差別的socket到一個數組裡

socket_create()     産生一個socket,相當于産生一個socket的資料結構

socket_get_option()     擷取socket選項

socket_getpeername()   擷取遠端類似主機的ip位址

socket_getsockname()   擷取本地socket的ip位址

socket_iovec_add()     添加一個新的向量到一個分散/聚合的數組

socket_iovec_alloc()   這個函數建立一個能夠發送接收讀寫的iovec資料結構

socket_iovec_delete()   删除一個已經配置設定的iovec

socket_iovec_fetch()   傳回指定的iovec資源的資料

socket_iovec_free()     釋放一個iovec資源

socket_iovec_set()     設定iovec的資料新值

socket_last_error()     擷取目前socket的最後錯誤代碼

socket_listen()     監聽由指定socket的所有連接配接

socket_read()     讀取指定長度的資料

socket_readv()     讀取從分散/聚合數組過來的資料

socket_recv()     從socket裡結束資料到緩存

socket_recvfrom()     接受資料從指定的socket,如果沒有指定則預設目前socket

socket_recvmsg()   從iovec裡接受消息

socket_select()     多路選擇

socket_send()     這個函數發送資料到已連接配接的socket

socket_sendmsg()     發送消息到socket

socket_sendto()     發送消息到指定位址的socket

socket_set_block()    在socket裡設定為塊模式

socket_set_nonblock()   socket裡設定為非塊模式

socket_set_option()     設定socket選項

socket_shutdown()     這個函數允許你關閉讀、寫、或者指定的socket

socket_strerror()     傳回指定錯誤号的詳細錯誤

socket_write()     寫資料到socket緩存

socket_writev()     寫資料到分散/聚合數組

(注: 函數介紹删減了部分原文内容,函數詳細使用建議參考英文原文,或者參考PHP手冊)

以上所有的函數都是PHP中關于socket的,使用這些函數,你必須把你的socket打開,如果你沒有打開,請編輯你的php.ini檔案,去掉下面這行前面的注釋:

extension=php_sockets.dll

如果你無法去掉注釋,那麼請使用下面的代碼來加載擴充庫:

<?php 
if(!extension_loaded(‘sockets’)) { 
  if(strtoupper(substr(PHP_OS, 3)) == “WIN”) { 
    dl(‘php_sockets.dll’);
  }else{
    dl(‘sockets.so’); 
  } 
} 
?>      

如果你不知道你的socket是否打開,那麼你可以使用phpinfo()函數來确定socket是否打開。你通過檢視phpinfo資訊了解socket是否打開。如下圖:

檢視phpinfo()關于socket的資訊

◆ 産生一個伺服器

現在我們把第一個例子進行完善。你需要監聽一個指定的socket并且處理使用者的連接配接。

<?php
$commonProtocol = getprotobyname("tcp");
$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol);
socket_bind($socket, 'localhost', 1337);
socket_listen($socket);
// Accept any incoming connections to the server
$connection = socket_accept($socket);
if($connection)
{
 socket_write($connection, "You have connected to the socket...\n\r"); 
} 
?>      

你應該使用你的指令提示符來運作這個例子。理由是因為這裡将産生一個伺服器,而不是一個Web頁面。如果你嘗試使用Web浏覽器來運作這個腳本,那麼很有可能它會超過30秒的限時。你可以使用下面的代碼來設定一個無限的運作時間,但是還是建議使用指令提示符來運作。

set_time_limit(0);

在你的指令提示符中對這個腳本進行簡單測試:

Php.exe example01_server.php

如果你沒有在系統的環境變量中設定php解釋器的路徑,那麼你将需要給php.exe指定詳細的路徑。當你運作這個伺服器端的時候,你能夠通過遠端登陸(telnet)的方式連接配接到端口1337來測試這個伺服器。如下圖:

上面的伺服器端有三個問題:1. 它不能接受多個連接配接。2. 它隻完成唯一的一個指令。3. 你不能通過Web浏覽器連接配接這個伺服器。

這個第一個問題比較容易解決,你可以使用一個應用程式去每次都連接配接到伺服器。但是後面的問題是你需要使用一個Web頁面去連接配接這個伺服器,這個比較困難。你可以讓你的伺服器接受連接配接,然後些資料到用戶端(如果它一定要寫的話),關閉連接配接并且等待下一個連接配接。

在上一個代碼的基礎上再改進,産生下面的代碼來做你的新伺服器端:

<?php 
// Set up our socket 
$commonProtocol = getprotobyname("tcp"); 
$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol); 
socket_bind($socket, 'localhost', 1337); 
socket_listen($socket); 
// Initialize the buffer 
$buffer = "NO DATA"; 
while(true) 
{ 
 // Accept any connections coming in on this socket
 $connection = socket_accept($socket);
 printf("Socket connected\r\n");
 // Check to see if there is anything in the buffer
 if($buffer != "")
 {
  printf("Something is in the buffer...sending data...\r\n"); 
  socket_write($connection, $buffer . "\r\n"); 
  printf("Wrote to socket\r\n");
 }
 else 
 { 
  printf("No Data in the buffer\r\n");
 }
 // Get the input
 while($data = socket_read($connection, 1024, PHP_NORMAL_READ))
 {
  $buffer = $data; 
  socket_write($connection, "Information Received\r\n"); 
  printf("Buffer: " . $buffer . "\r\n"); 
 } 
 socket_close($connection); 
 printf("Closed the socket\r\n\r\n"); 
} 
?>      

這個伺服器端要做什麼呢?它初始化一個socket并且打開一個緩存收發資料。它等待連接配接,一旦産生一個連接配接,它将列印“Socket connected”在伺服器端的螢幕上。這個伺服器檢查緩沖區,如果緩沖區裡有資料,它将把資料發送到連接配接過來的計算機。然後它發送這個資料的接受資訊,一旦它接受了資訊,就把資訊儲存到資料裡,并且讓連接配接的計算機知道這些資訊,最後關閉連接配接。當連接配接關閉後,伺服器又開始處理下一次連接配接。(翻譯的爛,附上原文)

This is what the server does. It initializes the socket and the buffer that you use to receive

and send data. Then it waits for a connection. Once a connection is created it prints 

“Socket connected” to the screen the server is running on. The server then checks to see if 

there is anything in the buffer; if there is, it sends the data to the connected computer. 

After it sends the data it waits to receive information. Once it receives information it stores 

it in the data, lets the connected computer know that it has received the information, and 

then closes the connection. After the connection is closed, the server starts the whole 

process again.

◆ 産生一個用戶端

處理第二個問題是很容易的。你需要産生一個php頁連接配接一個socket,發送一些資料進它的緩存并處理它。然後你又個處理後的資料在還頓,你能夠發送你的資料到伺服器。在另外一台用戶端連接配接,它将處理那些資料。

To solve the second problem is very easy. You need to create a PHP page that connects to 

a socket, receive any data that is in the buffer, and process it. After you have processed the

data in the buffer you can send your data to the server. When another client connects, it 

will process the data you sent and the client will send more data back to the server.

下面的例子示範了使用socket:

<?php 
// Create the socket and connect 
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 
$connection = socket_connect($socket,’localhost’, 1337); 
while($buffer = socket_read($socket, 1024, PHP_NORMAL_READ)) 
{ 
 if($buffer == “NO DATA”) 
 { 
 echo(“<p>NO DATA</p>”); 
 break;
 }
 else
 {
  // Do something with the data in the buffer 
  echo(“<p>Buffer Data: “ . $buffer . “</p>”); 
 } 
} 
echo(“<p>Writing to Socket</p>”);
// Write some test data to our socket
if(!socket_write($socket, “SOME DATA\r\n”))
{
 echo(“<p>Write failed</p>”); 
} 
// Read any response from the socket
while($buffer = socket_read($socket, 1024, PHP_NORMAL_READ))
{
 echo(“<p>Data sent was: SOME DATA<br> Response was:” . $buffer . “</p>”); 
} 
echo(“<p>Done Reading from Socket</p>”); 
?>