📜 ⬆️ ⬇️

Productive PHP network server

Have you tried to order a piglet with house wine roasted on a gun shambole at McDonalds and, for dessert, a girl next to you at a table for a pleasant conversation during the meal? Did not even think about it ?? Just about - the article is about this, about the stereotypes of the programmer and the laziness driving progress. But seriously - in the article we will write a very useful high-performance PHP network server for many people in a couple of hours. I am absolutely serious :-)


In the good old days ...


In the good old days, when people were closer to nature, fresh beer pleased us with a pleasant bitterness and women smelled exquisitely - the programmers were closer to the hardware and wrote in C and, in moments of inspiration, in assembler. But probably the most important thing is that programmers understood what tcp is, how it differs from udp, and how to effectively interact with the operating system kernel via the system call interface.

Laziness comes ...


But laziness was taking its own course and an approach to development was gradually formed - close to the ideology of the fictional abstract world from the Lord of the Rings .

People began to create objects in the programs of the fictional world, philosophical concepts, exchanging messages and increasingly began to break away from reality and nature. And if in C ++ they tried to linger alone with nature through pointers and controlled work with memory, in Java and C # laziness took its toll and the programmers found themselves in an ideal, but far from efficient, universe of rubber women and non-alcoholic beer. Philosophy lost in creating a universal API for working with all kinds of file systems or mandatory exception handling (Java).

And now even on the sides it became scary to watch: developers generally “spilled themselves” to such an extent that they do not use compilers :-) Many systems are created in weakly typed scripting languages ​​like Python / PHP - which not only support OOP well, but are so powerful that allow one function to effectively load a file into a variable :-)

Processors and threads


Many people firmly believed in the hardware support for OOP at the processor level in the 90s, this never happened. But laziness continues to influence and makes now piously believe in the effective implementation of programming language flows - taking into account the fashion on the reproduction of processor cores. Those. I don’t want to strain and just write “new thread” and everything will work efficiently and quickly.

In the meantime ...


Meanwhile, the world is captured by effective solutions in C in the style of nginx , productive NoSQL solutions are created “close to hardware” , and when it comes to speed and performance, the brain, obsessed with laziness and advertising, begins to move and feel something is not right! “They lie” about threads - they do not work effectively enough, even on multi-core glands. Although theoretically they should!

Lost origins ...


Ask the developer now about the difference between close and shutdown or about the differences in the process from the flow ... and more and more often you see what it means to “expressively bite your nails” :-) But to write a useful server, you need to understand well how the operating system works There are network protocols, how the nature of things is arranged in general and what a real beer is! :-)
')

It's not a programming language


And no matter, believe in what programming language you are going to make a useful network server. It is important how deeply you understand what you are going to do and what immunity you have against advertising and your own technological laziness.

Compound processing


People knew how to efficiently handle network sockets when they were closer to nature. Of course, the core of the operating system should deal with this and notify you when an event occurs:
1) A new socket came in a listening connection ( listen ) - and it can be taken into processing ( accept )
2) You can read from the socket without blocking the process ( read ).
3) You can write to the socket without blocking the process ( write ).

In the world of physical laws, other methods of processing connections, such as creating a bunch of threads or, excuse me, but sometimes they do this because of laziness and perfectionism, processes - they work slower and “eat” much more memory. And although often “excuses” of the type: “it is cheaper to buy one more piece of hardware than to teach a programmer to asynchronous processing of demultiplexed sockets” - they work, sometimes you find yourself in a situation where you need to solve the problem effectively on current equipment - reducing costs by 1-2 orders of magnitude.
According to this principle, the well-known nginx works, processing tens of thousands of connections by several processes of the operating system.

For me, it still remains a mystery why, despite the appearance in the same java about 10 years ago, libraries for solving server problems "in the nginx style" - it has not received proper distribution and applications continue to "figure out" on streams, despite all the deadlock and waste of this approach! :-)

Why doesn't everyone do that?


Just laziness :-) Although it is also considered that asynchronous processing of demultiplexed sockets is much more difficult from a programming point of view than 50 lines in a separate process. But below I will show how to write a similar server, even on a sharpened bit for other PHP tasks, to write a similar server - quite simply.

Php and sockets


In PHP, there is support for "native expensive" BSD sockets. But this extension, unfortunately, does not support ssl / tls.
Therefore, it is necessary to climb into the streaming interface, streams, filled with abstractions, “goblins and necromorphs”, a bit alienated from nature and a healthy lifestyle. If you take a shovel and discard a bunch of husks, behind this interface you can see network sockets and work quite effectively with them.

Bits of code


I will not give the entire source code of the network server, but walk through the key parts of the code. In general, the server steadily keeps without recompiling PHP up to 1024 open sockets in one process, taking about 18-20MB (this is dofiga by C standards, but believe me, there are PHP scripts that eat gigabytes) and straining only one processor core (yes, syscpu is noticeably larger, but without it). If you rebuild PHP, then select can work with a much larger number of sockets.

Server core

Server core tasks:
1) Check the array of tasks
2) For each task create a connection
3) Check that the socket in the task can be read or written without blocking the process
4) Release the resources (socket, etc.) of the task
5) Accept the connection from the control socket to add the task - without blocking the process

In simple words, we fill in the core of the job server with sockets (for example, go around sites and collect data, etc.) and the core IN ONE PROCESS starts to walk hundreds of tasks at the same time.

The task

The task is an object in the OOP terminology of the FSM type. There is a strategy inside the object - let's say: “go to this address, create a request, download the answer, parse, etc. returns to the beginning and at the end write the result in NoSQL. ” Those. You can create a task from simple loading of content to a complex chain of load testing with numerous branches - and this is all, I recall, living in the object of the task.
Tasks in this implementation are put through a separate control socket on port 8000 - json-objects are written to the tcp-socket and then begin their movement in the server core.

The principle of processing jobs and sockets

The main thing is not to allow the server process to block while waiting for a response in a function when reading or writing information to a network socket, while waiting for a new connection to a control socket or somewhere in complex calculations / cycles. Therefore, all job sockets are checked in the select system call and the OS kernel notifies us only when an event occurs (or by timeout).
while (true) { $ar_read = null; $ar_write = null; $ar_ex = null; //    ,     $ar_read[] = $this->controlSocket; foreach ($this->jobs as $job) { //job cleanup if ( $job->isFinished() ) { $key = array_search($job, $this->jobs); if (is_resource($job->getSocket())) { //""   stream_socket_shutdown($job->getSocket(),STREAM_SHUT_RDWR); fclose($job->getSocket()); } unset($this->jobs[$key]); $this->jobsFinished++; continue; } //  ""   ,      if ($job->isSleeping()) continue; //    if ($job->getStatus()=='DO_REQUEST') { $socket = $this->createJobSocket($job); if ($socket) { $ar_write[] = $socket; } //      } else if ($job->getStatus()=='READ_ANSWER') { $socket = $job->getSocket(); if ($socket) { $ar_read[] = $socket; } //      } else if ( $job->getStatus()=='WRITE_REQUEST' ) { $socket = $job->getSocket(); if ($socket) { $ar_write[] = $socket; } } } //              30  $num = stream_select($ar_read, $ar_write, $ar_ex, 30); 

Further, when the event occurred and the OS notified us, we start processing sockets in non-blocking mode. Yes, it is possible to optimize a little more work around the array of tasks, index tasks by the socket number and win 10ms - but for now ... just guessed, laziness :-)
  if (is_array($ar_write)) { foreach ($ar_write as $write_ready_socket) { foreach ($this->getJobs() as $job) { if ($write_ready_socket == $job->getSocket()) { $dataToWrite = $job->readyDataWriteEvent(); $count = fwrite($write_ready_socket , $dataToWrite, 1024); //        $job->dataWrittenEvent($count); } } } } if (is_array($ar_read)) { foreach ($ar_read as $read_ready_socket) { ///// command processing /// //    ,   if ($read_ready_socket == $this->controlSocket) { $csocket = stream_socket_accept($this->controlSocket); //  -   ,    .   . if ($csocket) { $req = ''; while ( ($data = fread($csocket,10000)) !== '' ) { $req .= $data; } //      $this->processCommand(trim($req), $csocket); stream_socket_shutdown($csocket, STREAM_SHUT_RDWR); fclose($csocket); } continue; /// ///// } else { //        $data = fread($read_ready_socket , 10000); foreach ($this->getJobs() as $job) { if ($read_ready_socket == $job->getSocket()) { //   .   ,   . $job->readyDataReadEvent($data); } } } } } } 

The socket itself is also initiated in non-blocking mode, it is important to set the flags, both of them! STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT:
  private function createJobSocket(BxRequestJob $job) { //Check job protocol if ($job->getSsl()) { //https $ctx = stream_context_create( array('ssl' => array( 'verify_peer' => false, 'allow_self_signed' => true ) ) ); $errno = 0; $errorString = ''; //      30-60,  -  TCP-   ,    ,   ...  $socket = stream_socket_client('ssl://'.$job->getConnectServer().':443',$errno,$errorString,30,STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT,$ctx); if ($socket === false) { $this->log(__METHOD__." connect error: ". $job->getConnectServer()." ". $job->getSsl() ."$errno $errorString"); $job->connectedSocketEvent(false); $this->connectsFailed++; return false; } else { $job->connectedSocketEvent($socket); $this->connectsCreated++; return $socket; } } else { //http ... 

Well, let's look at the code of the task itself - it should be able to work with partial answers / requests. To begin, let the server core tell us what we want to write to the socket.
 //    function readyDataWriteEvent() { if (!$this->dataToWrite) { if ($this->getParams()) { $str = http_build_query($this->getParams()); $headers = $this->getRequestMethod()." ".$this->getUri()." HTTP/1.0\r\nHost: ".$this->getConnectServer()."\r\n". "Content-type: application/x-www-form-urlencoded\r\n". "Content-Length:".strlen($str)."\r\n\r\n"; $this->dataToWrite = $headers; $this->dataToWrite .= $str; } else { $headers = $this->getRequestMethod()." ".$this->getUri()." HTTP/1.0\r\nHost: ".$this->getConnectServer()."\r\n\r\n"; $this->dataToWrite = $headers; } return $this->dataToWrite; } else { return $this->dataToWrite; } } 

Now we write the query, determining how much is left.
 //      ,      function dataWrittenEvent($count) { if ($count === false ) { //socket was reset $this->jobFinished = true; } else { $dataTotalSize = strlen($this->dataToWrite); if ($count<$dataTotalSize) { $this->dataToWrite = substr($this->dataToWrite,$count); $this->setStatus('WRITE_REQUEST'); } else { //     ,      $this->setStatus('READ_ANSWER'); } } } 

After receiving the request, we read the answer. It is important to understand when the answer is read in full. You may need to set a timeout for reading - I did not need it.
 //    ,           function readyDataReadEvent($data) { ////////// Successfull data read ///// if ($data) { $this->body .= $data; $this->setStatus('READ_ANSWER'); $this->bytesRead += strlen($data); ///// ////////// } else { //////////           ///// ////////// redirect if ( preg_match("|\r\nlocation:(.*)\r\n|i",$this->body, $ar_matches) ) { $url = parse_url(trim($ar_matches[1])); $this->setStatus('DO_REQUEST'); } else if (...) { //    ,     $this->jobFinished = true; ... } else if (...) { $this->setSleepTo(time()+$this->sleepInterval); $this->sleepInterval *=2; $this->retryCount--; $this->setStatus('DO_REQUEST'); } $this->body = ''; ... 

In the last snippet, we can hierarchically direct the FSM according to the embedded strategy, implementing various job options for the job.
At the time of writing the class, the feeling did not leave the impression that you are writing a plugin for nginx ;-)

Result


You can see how easy and succinctly we managed to solve the problem of simultaneous work with hundreds and thousands of tasks and sockets in just one PHP process. Imagine if we raise for this server how many PHP processes, how many cores on the server - yes, these are thousands of clients served. And there is no garden with threads and inefficient switching of the processor context and increased memory requirements. The PHP server process consumes only about 20MB of memory, and works like a horse :-)

Results


Understanding the benefits that the kernel of the operating system can bring us to efficiently handle network sockets - we adjusted to it and implemented a high-performance PHP server - servicing hundreds of open network sockets in one process. If necessary, you can recompile PHP and process thousands of sockets in one process.
Expand the range of knowledge, do not be lazy under the yoke of stereotypes - using even scripting weakly typed languages ​​you can make productive servers - the main thing is to know how and not to be afraid to experiment :-) Good luck to all!

Source: https://habr.com/ru/post/198540/


All Articles