文章: PHP And Socket
書名: 《PHP Game Programming》
作者: Matt Rutledget
翻譯: heiyeluren <heiyeluren_gmail_com>
◇ 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>”);
?>
這個例子的代碼示範了用戶端串連到伺服器。用戶端讀取資料。如果這是第一時間到達這個迴圈的首次串連,這個伺服器將發送“NO DATA”返回給用戶端。如果情況發生了,這個用戶端在串連之上。用戶端發送它的資料到伺服器,資料發送給伺服器,用戶端等待響應。一旦接受到響應,那麼它將把響應寫到螢幕上。
結合Socket的坦克大戰
(因為是描述遊戲和socket結合,跟本文聯絡不大,所以不翻譯,建議參考英文原文)
[ 題外話 ]
翻譯文章的初衷是因為我個人對socket非常感興趣,而且目前國內見php的文章比較少,除了php手冊裡面的部分內容,所以在我看了《PHP Game Programming》這本書裡有關於socket的內容後毅然決定要翻譯,我知道翻譯出來的品質不行,還請見諒。
另外,我在《Core PHP Programming》Third Edition中也發現裡面的Socket內容講的不錯,如果有空,我想也許我會把它也給翻譯一下。這是我第一次翻譯文章,花了我近五個小時,文章可以說是錯誤百出,如果翻譯的不合理請見諒,如果有興趣提高這個內容可以給我發郵件。這個淩晨時分,竟然無法入眠,不知道是不是在其他角落,也有人同我一樣。
希望本文能夠給向學習PHP Socket編程的朋友一點協助,感謝你閱讀這個錯誤百出的文章。
翻譯:heiyeluren <heiyeluren_at_gmail.com>
時間:2005-8-14 04:46
ps: 本來在word來排版挺好看的文章,到了blog裡這麼難看,湊合吧。
如果轉載本文,請註明來源,畢竟我熬夜翻譯容易嗎我! -_-#