socket的中文名字叫作套接字,這種東西就是對TCP/IP的「封裝」。現實中的網絡實際上只有四層而已,從上至下分別是應用層、傳輸層、網絡層、數據鏈路層。最經常使用的http協議則是屬於應用層的協議,而socket,能夠簡單粗暴的理解爲是傳輸層的一種東西。若是仍是很難理解,那再粗暴地點兒tcp://218.221.11.23:9999,看到沒?這就是一個tcp socket。php
socket賦予了咱們操控傳輸層和網絡層的能力,從而獲得更強的性能和更高的效率,socket編程是解決高併發網絡服務器的最經常使用解決和成熟的解決方案。任何一名服務器程序員都應當掌握socket編程相關技能。git
在php中,能夠操控socket的函數一共有兩套,一套是socket_*系列的函數,另外一套是stream_*系列的函數。socket_*是php直接將C語言中的socket抄了過來獲得的實現,而stream_*系則是php使用流的概念將其進行了一層封裝。下面用socket_*系函數簡單爲這一系列文章開個篇。程序員
先來作個最簡單socket服務器:github
<?php $host = '0.0.0.0'; $port = 9999; // 建立一個tcp socket $listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); // 將socket bind到IP:port上 socket_bind( $listen_socket, $host, $port ); // 開始監聽socket socket_listen( $listen_socket ); // 進入while循環,不用擔憂死循環死機,由於程序將會阻塞在下面的socket_accept()函數上 while( true ){ // 此處將會阻塞住,一直到有客戶端來鏈接服務器。阻塞狀態的進程是不會佔據CPU的 /* 之因此不會佔據CPU,由於CPU運算的時候,相似有個指揮官的傢伙會調度,進程切換,簡稱調度,它只會指揮準備開始打戰和正在打戰的人,而正在休息軍人(阻塞中)不須要命令他們打戰,這樣也符合常理了 你也能夠看到下圖,調度只在運行和就緒之間的,因此cpu不會傻傻等正在休息的士兵起來了,在再指揮 */ // 因此你不用擔憂while循環會將機器拖垮,不會的 $connection_socket = socket_accept( $listen_socket ); // 向客戶端發送一個helloworld $msg = "helloworld\r\n"; socket_write( $connection_socket, $msg, strlen( $msg ) ); socket_close( $connection_socket ); } socket_close( $listen_socket );
將文件保存爲server.php,而後執行php server.php運行起來。客戶端咱們使用telnet就能夠了,打開另一個終端執行telnet 127.0.0.1 9999按下回車便可。運行結果以下:編程
簡單解析一下上述代碼來講明一下tcp socket服務器的流程:服務器
上面這個案例中,有兩個很大的缺陷:網絡
分析了上述問題後,又聯想到了前面說的多進程,那咱們能夠在accpet到一個請求後就fork一個子進程來處理這個客戶端的請求,這樣當accept了第二個客戶端後再fork一個子進程來處理第二個客戶端的請求,這樣問題不就解決了嗎?OK!擼一把代碼演示一下:併發
<?php $host = '0.0.0.0'; $port = 9999; // 建立一個tcp socket $listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); // 將socket bind到IP:port上 socket_bind( $listen_socket, $host, $port ); // 開始監聽socket socket_listen( $listen_socket ); // 進入while循環,不用擔憂死循環死機,由於程序將會阻塞在下面的socket_accept()函數上 while( true ){ // 此處將會阻塞住,一直到有客戶端來鏈接服務器。阻塞狀態的進程是不會佔據CPU的 // 因此你不用擔憂while循環會將機器拖垮,不會的 $connection_socket = socket_accept( $listen_socket ); // 當accept了新的客戶端鏈接後,就fork出一個子進程專門處理 $pid = pcntl_fork(); // 在子進程中處理當前鏈接的請求業務 if( 0 == $pid ){ // 向客戶端發送一個helloworld $msg = "helloworld\r\n"; socket_write( $connection_socket, $msg, strlen( $msg ) ); // 休眠5秒鐘,能夠用來觀察時候能夠同時爲多個客戶端提供服務 echo time().' : a new client'.PHP_EOL; sleep( 5 ); socket_close( $connection_socket ); exit; } } socket_close( $listen_socket );
將代碼保存爲server.php,而後執行php server.php,客戶端依然使用telnet 127.0.0.1 9999,只不過此次咱們開啓兩個終端來執行telnet。重點觀察當第一個客戶端鏈接上去後,第二個客戶端時候也能夠鏈接上去。運行結果以下:socket
經過接受到客戶端請求的時間戳能夠看到如今服務器能夠同時爲N個客戶端服務的。可是,接着想,若是前後有1萬個客戶端來請求呢?這個時候服務器會fork出1萬個子進程來處理每一個客戶端鏈接,這是會死人的。fork自己就是一個很浪費系統資源的系統調用,1W次fork足以讓系統崩潰,即使當下系統承受住了1W次fork,那麼fork出來的這1W個子進程也夠系統內存喝一壺了,最後是好不容易費勁fork出來的子進程在處理完畢當前客戶端後又被關閉了,下次請求還要從新fork,這自己就是一種浪費,不符合社會主義主流價值觀。若是是有人惡意攻擊,那麼系統fork的數量還會呈直線上漲一直到系統崩潰。tcp
因此,咱們就再次提出增進型解決方案。咱們能夠預估一下業務量,而後在服務啓動的時候就fork出固定數量的子進程,每一個子進程處於無限循環中並阻塞在accept上,當有客戶端鏈接擠進來就處理客戶請求,當處理完成後僅僅關閉鏈接但自己並不銷燬,而是繼續等待下一個客戶端的請求。這樣,不只避免了進程反覆fork銷燬巨大資源浪費,並且經過固定數量的子進程來保護系統不會因無限fork而崩潰。
<?php $host = '0.0.0.0'; $port = 9999; // 建立一個tcp socket $listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); // 將socket bind到IP:port上 socket_bind( $listen_socket, $host, $port ); // 開始監聽socket socket_listen( $listen_socket ); // 給主進程換個名字 cli_set_process_title( 'phpserver master process' ); // 按照數量fork出固定個數子進程 for( $i = 1; $i <= 10; $i++ ){ $pid = pcntl_fork(); if( 0 == $pid ){ cli_set_process_title( 'phpserver worker process' ); while( true ){ $conn_socket = socket_accept( $listen_socket ); $msg = "helloworld\r\n"; socket_write( $conn_socket, $msg, strlen( $msg ) ); socket_close( $conn_socket ); } } } // 主進程不能夠退出,代碼演示比較粗暴,爲了避免保證退出直接走while循環,休眠一秒鐘 // 實際上,主進程真正該作的應該是收集子進程pid,監控各個子進程的狀態等等 while( true ){ sleep( 1 ); } socket_close( $connection_socket );
將文件保存爲server.php後php server.php執行,而後再用ps -ef | grep phpserver | grep -v grep來看下服務器進程狀態:
能夠看到master進程存在,除此以外還有10個子進程處於等待服務狀態,再同一個時刻能夠同時爲10個客戶端提供服務。咱們經過telnet 127.0.0.1 9999來嘗試一下,運行結果以下圖:
好啦,php新的征程系列就先經過一個簡單的入門開始啦!下篇將會講述一些比較深入的理論基礎知識。