閣下在閱讀本奏章時,請先閱讀tcp/ip低層通訊說明,不然疑惑在心,影響大腦神經^^
先解釋以下知識點:
程式:process是一個執行中的程式,它的管制由核心【系統】來完成,建立,銷燬,排程。
程式憑證:每個程式都有自己的程式標識,不然一堆程式我怎麼知道哪個程式是什麼,程式標識是一個整形數字,同時每個程式擁有自己的所屬使用者和組標識,以便知道這個程式是哪個使用者及組執行的。
程式在執行的時候會在作業系統【unix,linux/proc目錄】下生成一個proc/PID的目錄【win我不知道了,我不喜歡win,閣下知道可指點本人^^】,作業系統有多個執行中的程式,其中有一些特別的程式如init程式它的程式號一般為1,是所有程式的祖先程式,這個程式是不會死的,除非你澆水了^_^【當然有的系統如阿里雲的伺服器的可能程式名稱不是init】
在這裡你可以檢視到當前程式的詳細資訊,並且你可以讀取這些檔案,你可以讀取status可以檢視程式的詳細資訊,如程式id,pid,程式名稱,狀態,有效使用者和真實使用者及組等資料,同時你應該看到fd目錄,它是程式的檔案列表,linux它把一些裝置對映成檔案了
針對輸入裝置對應的是0【比如你要獲取鍵盤輸入的資料,一般來說驅動程式是編寫好鍵盤的驅動程式來讀取的,輸出裝置是1【如果你用write函式可以直接write(1,data,size),當然了這個函式僅在c語言下使用,php不可以的,php的STDOUT它對應的內容是fopen('php://stdout', 'w'),一個程式啟動後預設會開啟這3個檔案【所以你在fd目錄會下會看到0,1,2,xx這些檔案,它們就是一個個的檔案描述符,用數字表示,0就是輸入,早期就是這樣玩,後來為了方便就起了個綽號叫stdin,stdout,stderr。
PS:關於程式組,會話,控制終端,程式組長,會話首領,守護程式以及程式的控制,程式訊號,包括程式排程請自行google
一般情況下我們編寫一個伺服器程式是這樣的
int sockfd = socket();
struct sockaddr_in address;
address.sin_port = 埠號
address.sin_addr = ip
bind(sockfd,address);
listen();
int connfd = accept();
recv(connfd);
首先建立一個套接字,然後繫結好ip和埠【為什麼要繫結,因為當這個程式執行時,它建立好的sockfd會放在程式的【檔案列表】裡【在哪裡檢視啊,就是上面講過的proc/PID程式號/fd目錄下建立,當然裡面預設有3個了即0,1,2,你再建立就從3開始,所以你去開啟linux下的proc/PID你的伺服器程式號/fd下看,它即sockfd已經關聯了接收緩衝區,傳送緩衝區,等待佇列等資料,並且人家還繫結了ip和埠】
當該程式執行到accept或是recv時它立馬阻塞。
阻塞:
作業系統一般是多個任務執行的,就是多個程式執行,每個程式的執行次序,時間由作業系統控制,採用一種時分技術,不斷的從佇列裡取出,執行,這個時候由於執行到recv,但是現在沒有資料啊【所以當前程式會放在等待佇列裡面,然後cpu執行其它程式了,這時候我們叫阻塞】,因為網路卡還沒有接收到資料,當網路卡接受到資料時【想想怎麼接受的,接受後放在哪個位置上】,此時它立馬產生一箇中斷請求。
中斷:
當cpu在執行主程式時,如果硬體產生了一箇中斷請求,cpu會立馬停止正在執行的主程式,並且跳轉到中斷程式,並執行中斷程式,因為這是硬體中斷,優先順序最高,會先執行【如果擼過微控制器或是微機應該瞭解】,當網路卡接受,它會把接受的資料寫入記憶體,當然首先它會指定某一塊儲存單元即先通過地址匯流排尋找到一塊地址單元,然後再通過資料匯流排把資料寫入記憶體,控制匯流排的指令將是寫入記憶體操作【可以去了解下cpu和記憶體的一個執行情況】
喚醒程式:
此時網路卡接受到資料並寫入記憶體,然後響應中斷程式以後,就會根據埠號把資料寫入對應的socket同時喚醒等待佇列中的程式,同時網路卡接受的資料由於含有ip和埠號,所以系統會找到埠號對應的sockfd檔案描述符【這就是你為什麼要繫結埠的理由,不然它找不到對應的sockfd】。
以上就是recv到資料後的一個簡單說明。
IO模型之多路複用:
linux它提供了select,poll,epoll等多路利用的介面,當然有一些開源庫如libevent它做了許多封裝工作【後面有機會再說】
上面那個recv它只能監聽一個客戶端,並且整個程式在沒有資料到達時,它就阻塞了,啥也沒有做了,所以我們監聽多個檔案描述符,這個時候就用select。
int fd[sock1,sock2,sock3...];
ret = select(...fd...);
你沒有看錯,它是個檔案描述集,select呼叫後,核心會線性輪詢哪些sockfd就緒了就是該檔案描述符產生的各種事件,當其中sock1接受到資料以後,select會立馬返回,當然是返回集合,所以你要自己遍歷,判斷哪個是已經就緒的,你再進行讀寫操作。
當然這種方式效率不高,因為當連線數量一多,這fd集合越來越大,效能就慢慢下降了,並且它預設可以開啟的【該程式能開啟的檔案數量】最多是1024個在linux下,即使你去修改也沒有用,它效能依然低下,畢竟它是線性表,採用這樣的資料結構使它無法支援很大的併發量。所以你懂的你在linux下玩workerman它預設用的就是select,除非你裝了libevent。
聽說【我只聽說^_^】apache採用的就是select,nginx採用的是epoll。
為了提升效能,後來別人提出了增加版本的epoll
int epollfd = epoll_create();//建立一個epoll_event poll物件 內部採用非常平衡的二叉排序樹來存放它們叫紅黑樹
因為二叉排序樹有時候插入或是刪除操作要遍歷深度太多,所以搞成平衡的二叉排序樹,減少遍歷次數,使之效能提高,具體可以去看看二叉排序樹,平衡樹AVL,再看紅黑樹【首先先擼一下二叉樹^_^】
採用這種資料結構儲存每個sockfd檔案描述符,在插入或是刪除時效能都能提升,比線性表遍歷次數要少許多了,這就是為什麼人家要玩演算法的原因,畢竟作業系統整個核心就是演算法
epoll_event pool它還有一個就緒列表成員
epoll_ctl(epollfd,sockfd)
for(;;){
num = epoll_wait(epollfd,events,...)
}
當網路卡接受到資料cpu響應中斷程式後,它並不會立馬喚醒當前的程式,而是將sock直接引用到該物件的rdlist就緒列表成員裡【它是一個連結串列,有的是雙向連結串列】所以你可以去看看線性表的鏈式儲存結構怎麼玩
然後最喚醒程式直接返回就緒的檔案描述符列表
關於它的詳情內容請自行查詢資料!!!
Epoll如果是LT模式則是輪詢模式,ET同是回撥事件模式,它的演算法時間複雜度為O(1)效能最高
下面是使用php的函式編寫的簡易多程式伺服器
<?php
/**
* Created by PhpStorm.
* User: 1655664358@qq.com
* Date: 2018/8/7
* Time: 13:26
*/
class Server
{
public $_context;
const DEFAULT_BACKLOG=1024;
public $socket;
public $pidS = [];
public $readFds = [];
public $writeFds = [];
public $exceptions = [];
public function run()
{
umask(0);
if (!posix_setsid()){
throw new RuntimeException("setsid error");
}
$pid = pcntl_fork();
if ($pid>0){
exit(0);
}
cli_set_process_title("jackcsm");
$context_option['socket']['backlog'] = static::DEFAULT_BACKLOG;
$this->_context = stream_context_create($context_option);
stream_context_set_option($this->_context, 'socket', 'so_reuseport', 1);
$this->socket = stream_socket_server("tcp://0.0.0.0:12345",$errno,$error);
stream_set_blocking($this->socket,0);
$this->readFds[] = $this->socket;
for ($i=0;$i<4;$i++){
$this->worker();
}
}
function worker()
{
$pid = pcntl_fork();
if ($pid>0){
$this->pidS[$pid] = $pid;
}else if ($pid==0){
srand();
mt_srand();
while (1){
$reads = $this->readFds;
$writes = $this->writeFds;
$exceptions = $this->exceptions;
set_error_handler(function(){});
$ret = stream_select($reads,$writes,$exceptions,1024);
restore_error_handler();
if (!$ret){
continue;
}
if ($reads){
foreach ($reads as $fd){
if ($fd == $this->socket){
set_error_handler(function(){});
$connect = stream_socket_accept($this->socket,0,$remoteAddr);
stream_set_blocking($connect, 0);
restore_error_handler();
//stream_set_blocking($connect,0);
$this->readFds[] = $connect;
$this->writeFds[] = $connect;
$this->exceptions[] = $connect;
}else{
$data = fread($fd,10240);
if ($data==''){
foreach ($reads as $k=>$fdNode){
if ($k == $fd){
unset($this->readFds[$k]);
fclose($fd);
}
}
foreach ($writes as $k=>$fdNode){
if ($k == $fd){
unset($this->writeFds[$k]);
fclose($fd);
}
}
}else{
file_put_contents("jackcsm.log",$data);
//echo "來自客戶端的資料:".$data.PHP_EOL;
}
// $pid = posix_getpid().posix_getppid();
//
// fwrite($fd,"<html>hello,world--$data--$pid</html>",1024);
// foreach ($reads as $k=>$fdNode){
// if ($k == $fd){
// unset($this->readFds[$k]);
// }
// }
// foreach ($writes as $k=>$fdNode){
// if ($k == $fd){
// unset($this->writeFds[$k]);
// }
// }
}
}
}
if ($writes){
foreach ($writes as $fd) {
if ($fd!=$this->socket){
set_error_handler(function(){});
fwrite($fd,"hello,world",20);
restore_error_handler();
foreach ($reads as $k=>$fdNode){
if ($k == $fd){
unset($this->readFds[$k]);
}
}
foreach ($writes as $k=>$fdNode){
if ($k == $fd){
unset($this->writeFds[$k]);
}
}
fclose($fd);
}
}
}
}
}
}
}
$pid = pcntl_fork();
if($pid==-1){
throw new RuntimeException("fork error");
exit(0);
}else if($pid==0){
(new Server())->run();
}else{
exit("exit");
}
它監聽的是12345埠,你可以使用http,websocket或是telent客戶端去連線它。
可以自行去執行程式碼,不過只支援在linux下pcntl擴充套件PHP官方不支援在win下,當然win是支援多程式的【你在win上裝docker,虛擬機器也可以】
另外你也可以檢視我相關的註解workerman框架註解