一直以來,PHP不多用於socket編程,畢竟是一門腳本語言,效率會成爲很大的瓶頸,可是不能說PHP就沒法用於socket編程,也不能說PHP的socket編程性能就有多麼的低,例如知名的一款PHP socket框架 workerman 就是用純PHP開發,而且號稱擁有優秀的性能,因此在某些環境下,PHP socket編程或許也可一展身手。php
PHP提供了一系列相似C語言socket庫中的方法供咱們調用:html
- socket_accept — Accepts a connection on a socket
- socket_bind — 給套接字綁定名字
- socket_clear_error — 清除套接字或者最後的錯誤代碼上的錯誤
- socket_close — 關閉套接字資源
- socket_cmsg_space — Calculate message buffer size
- socket_connect — 開啓一個套接字鏈接
- socket_create_listen — Opens a socket on port to accept connections
- socket_create_pair — Creates a pair of indistinguishable sockets and stores them in an array
- socket_create — 建立一個套接字(通信節點)
- socket_get_option — Gets socket options for the socket
- socket_getopt — 別名 socket_get_option
- socket_getpeername — Queries the remote side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type
- socket_getsockname — Queries the local side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type
- socket_import_stream — Import a stream
- socket_last_error — Returns the last error on the socket
- socket_listen — Listens for a connection on a socket
- socket_read — Reads a maximum of length bytes from a socket
- socket_recv — 從已鏈接的socket接收數據
- socket_recvfrom — Receives data from a socket whether or not it is connection-oriented
- socket_recvmsg — Read a message
- socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout
- socket_send — Sends data to a connected socket
- socket_sendmsg — Send a message
- socket_sendto — Sends a message to a socket, whether it is connected or not
- socket_set_block — Sets blocking mode on a socket resource
- socket_set_nonblock — Sets nonblocking mode for file descriptor fd
- socket_set_option — Sets socket options for the socket
- socket_setopt — 別名 socket_set_option
- socket_shutdown — Shuts down a socket for receiving, sending, or both
- socket_strerror — Return a string describing a socket error
- socket_write — Write to a socket
更多細節請查看PHP關於socket的官方手冊:http://php.net/manual/zh/book.sockets.phplinux
一個簡單的TCP服務器示例 phptcpserver.php :編程
- <?php
-
- $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
-
- if (FALSE === $servsock)
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- if (!socket_bind($servsock, '127.0.0.1', 8888))
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- if (!socket_listen($servsock, 128))
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- while (1)
- {
- $connsock = socket_accept($servsock);
-
- if ($connsock)
- {
- socket_getpeername($connsock, $addr, $port);
- echo "client connect server: ip = $addr, port = $port" . PHP_EOL;
-
- while (1)
- {
- $data = socket_read($connsock, 1024);
-
- if ($data === '')
- {
-
- socket_close($connsock);
- echo "client close" . PHP_EOL;
- break;
- }
- else
- {
- echo 'read from client:' . $data;
- $data = strtoupper($data);
- socket_write($connsock, $data);
- }
- }
- }
- }
-
- socket_close($servsock);
啓動這個服務器:數組
- [root@localhost php]# php phptcpserver.php
以後這個服務器就一直阻塞在那裏,等待客戶端鏈接,咱們能夠用telnet命令來鏈接這個服務器:服務器
- [root@localhost ~]# telnet 127.0.0.1 8888
- Trying 127.0.0.1...
- Connected to 127.0.0.1.
- Escape character is '^]'.
- ajdjajksdjkaasda
- AJDJAJKSDJKAASDA
- 小明哈哈哈哈笑
- 小明哈哈哈哈笑
- 小明efsfsdfsdf了哈哈哈
- 小明EFSFSDFSDF了哈哈哈
服務器端輸出:併發
- [root@localhost php]# php phptcpserver.php
- client connect server: ip = 127.0.0.1, port = 50398
- read from client:ajdjajksdjkaasda
- read from client:小明哈哈哈哈笑
- read from client:小明efsfsdfsdf了哈哈哈
但其實這個TCP服務器是有問題的,它一次只能處理一個客戶端的鏈接和數據傳輸,這是由於一個客戶端鏈接過來後,進程就去負責讀寫客戶端數據,當客戶端沒有傳輸數據時,tcp服務器處於阻塞讀狀態,沒法再去處理其餘客戶端的鏈接請求了。app
解決這個問題的一種辦法就是採用多進程服務器,每當一個客戶端鏈接過來,服務器開一個子進程專門負責和該客戶端的數據傳輸,而父進程仍然監聽客戶端的鏈接,可是起進程的代價是昂貴的,這種多進程的機制顯然支撐不了高併發。框架
另外一個解決辦法是使用IO多路複用機制,使用php爲咱們提供的socket_select方法,它能夠監聽多個socket,若是其中某個socket狀態發生了改變,好比從不可寫變爲可寫,從不可讀變爲可讀,這個方法就會返回,從而咱們就能夠去處理這個socket,處理客戶端的鏈接,讀寫操做等等。來看php文檔中對該socket_select的介紹ssh
- socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout
-
- 說明
-
- int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )
-
- socket_select() accepts arrays of sockets and waits for them to change status.
- Those coming with BSD sockets background will recognize that those socket resource arrays are in fact the so-called file descriptor sets.
- Three independent arrays of socket resources are watched.
-
- You do not need to pass every array to socket_select(). You can leave it out and use an empty array or NULL instead.
- Also do not forget that those arrays are passed by reference and will be modified after socket_select() returns.
-
- 返回值
-
- On success socket_select() returns the number of socket resources contained in the modified arrays,
- which may be zero if the timeout expires before anything interesting happens.
- On error FALSE is returned. The error code can be retrieved with socket_last_error().
大體翻譯下:
socket_select --- 在給定的幾組sockets數組上執行 select() 系統調用,用一個特定的超時時間。
socket_select() 接受幾組sockets數組做爲參數,並監聽它們改變狀態
這些基於BSD scokets 可以識別這些socket資源數組實際上就是文件描述符集合。
三個不一樣的socket資源數組會被同時監聽。
這三個資源數組不是必傳的, 你能夠用一個空數組或者NULL做爲參數,不要忘記這三個數組是以引用的方式傳遞的,在函數返回後,這些數組的值會被改變。
socket_select() 調用成功返回這三個數組中狀態改變的socket總數,若是設置了timeout,而且在timeout以內都沒有狀態改變,這個函數將返回0,出錯時返回FALSE,能夠用socket_last_error() 獲取錯誤碼。
使用 socket_select() 優化以前 phptcpserver.php 代碼:
- <?php
-
- $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
-
- if (FALSE === $servsock)
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- if (!socket_bind($servsock, '127.0.0.1', 8888))
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- if (!socket_listen($servsock, 128))
- {
- $errcode = socket_last_error();
- fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));
- exit(-1);
- }
-
- $read_socks = array();
- $write_socks = array();
- $except_socks = NULL;
-
- $read_socks[] = $servsock;
-
- while (1)
- {
-
- $tmp_reads = $read_socks;
- $tmp_writes = $write_socks;
-
-
- $count = socket_select($tmp_reads, $tmp_writes, $except_socks, NULL);
-
- foreach ($tmp_reads as $read)
- {
-
- if ($read == $servsock)
- {
-
- $connsock = socket_accept($servsock);
- if ($connsock)
- {
- socket_getpeername($connsock, $addr, $port);
- echo "client connect server: ip = $addr, port = $port" . PHP_EOL;
-
-
- $read_socks[] = $connsock;
- $write_socks[] = $connsock;
- }
- }
- else
- {
-
- $data = socket_read($read, 1024);
-
- if ($data === '')
- {
-
- foreach ($read_socks as $key => $val)
- {
- if ($val == $read) unset($read_socks[$key]);
- }
-
- foreach ($write_socks as $key => $val)
- {
- if ($val == $read) unset($write_socks[$key]);
- }
-
-
- socket_close($read);
- echo "client close" . PHP_EOL;
-
- }
- else
- {
- socket_getpeername($read, $addr, $port);
-
- echo "read from client # $addr:$port # " . $data;
-
- $data = strtoupper($data);
-
- if (in_array($read, $tmp_writes))
- {
-
- socket_write($read, $data);
- }
- }
- }
- }
- }
-
- socket_close($servsock);
如今,這個TCP服務器就能夠支持多個客戶端同時鏈接了,測試下:
服務器端:
- [root@localhost php]# php phptcpserver.php
- client connect server: ip = 127.0.0.1, port = 50404
- read from client # 127.0.0.1:50404 # hello world
- client connect server: ip = 127.0.0.1, port = 50406
- read from client # 127.0.0.1:50406 # hello PHP
- read from client # 127.0.0.1:50404 # 少小離家老大回
- read from client # 127.0.0.1:50404 # 鄉音無改鬢毛衰
- read from client # 127.0.0.1:50406 # 老當益壯,
- read from client # 127.0.0.1:50406 # 寧移白首之心
- client close
- client connect server: ip = 127.0.0.1, port = 50408
稍微修改上面的服務器返回,返回一個HTTP響應頭和一個簡單的HTTP響應體,這樣就搖身一變成了一個最簡單的HTTP服務器:
- ....
-
- socket_getpeername($read, $addr, $port);
-
- echo "read from client # $addr:$port # " . $data;
-
- $response = "HTTP/1.1 200 OK\r\n";
- $response .= "Server: phphttpserver\r\n";
- $response .= "Content-Type: text/html\r\n";
- $response .= "Content-Length: 3\r\n\r\n";
- $response .= "ok\n";
-
- if (in_array($read, $tmp_writes))
- {
-
- socket_write($read, $response);
- socket_close($read);
-
- foreach ($read_socks as $key => $val)
- {
- if ($val == $read) unset($read_socks[$key]);
- }
-
- foreach ($write_socks as $key => $val)
- {
- if ($val == $read) unset($write_socks[$key]);
- }
- }
- .....
從新啓動該服務器,用curl模擬請求該http服務器:
- [root@localhost ~]# curl '127.0.0.1:8888'
- ok
- [root@localhost ~]# curl '127.0.0.1:8888'
- ok
- [root@localhost ~]# curl '127.0.0.1:8888'
- ok
- [root@localhost ~]# curl '127.0.0.1:8888'
- ok
- [root@localhost ~]# curl '127.0.0.1:8888'
- ok
- [root@localhost ~]#
服務器端輸出:
- client connect server: ip = 127.0.0.1, port = 50450
- read from client # 127.0.0.1:50450 # GET / HTTP/1.1
- User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
- Host: 127.0.0.1:8888
- Accept: */*
-
- client close
- client connect server: ip = 127.0.0.1, port = 50452
- read from client # 127.0.0.1:50452 # GET / HTTP/1.1
- User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
- Host: 127.0.0.1:8888
- Accept: */*
-
- client close
- client connect server: ip = 127.0.0.1, port = 50454
- read from client # 127.0.0.1:50454 # GET / HTTP/1.1
- User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
- Host: 127.0.0.1:8888
- Accept: */*
-
- client close
- client connect server: ip = 127.0.0.1, port = 50456
- read from client # 127.0.0.1:50456 # GET / HTTP/1.1
- User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2
- Host: 127.0.0.1:8888
- Accept: */*
-
- client close
這樣一個高併發的HTTP服務器就開發好了,用壓測軟件測試下併發能力:
看到高達5000多的QPS,有沒有小激動呢^^。
PHP是世界上最好的語言 that's all !