Wednesday, June 30, 2010

How to create a socket server in PHP

EDIT: If you need to submit bugs or want to improve my
work, its now available on github!

Ever
tried searching for information on how to properly create a multi-client socket server in PHP?  You’ll get
plenty of results with outdated and messy source code, some of which won’t even work.

This
was the conclusion I'd come to a couple years ago when I decided that I wanted to try my hand at writing an
IRC server.  The why is not important… (For the fun of building an IRC Server, if you can call
that fun
).  So I googled around a bit until I finally found some code that worked on its own, and
quickly built a semi-functional IRC server using it, and headed off to sleep at 5am.

The
next day I was very, very happy with the results of my hard labor, but it wasn't good enough, so I started
re-writing it from scratch as an Object, and thus I created class::IRCServer.

Then,
once I felt that I was finished screwing around with my newly built IRCd, I decided to modify the function
enough to be used on its own as a socket server, to share with the world.  However, that was a couple years
ago (The non-edited version of this article was written in 2010).

Recently, however,
PHP has evolved to a point where the Socket Server class I had written was out-dated, buggy, and just plain
bad.

So I built a new library from
scratch after looking at the old code, and came up with Navarr\Socket
.  The code is open-source,
MIT-license, and available on Github.  I heavily encourage you to fork and submit pull requests.  The code
is namespaced, follows the PSR-2 standard,   and even has some PHPUnit tests.

35 comments:

  1. Out of interest, what are the limitations as far as the max_clients is concerned?

    ReplyDelete
  2. As much as your system can handle I suppose. I haven't really tested it, nor do I know
    PHP well enough to say.

    ReplyDelete
  3. Hi, Navarr -

    Thank you for taking the time to put this code
    together!

    I have a question for you about this line in SocketServer::loop_once():
     
    $this->trigger_hooks("CONNECT",$this->clients[$i],"");

    I'm assuming that
    "CONNECT" is a method that needs development.  Is this correct?

    _______________________

    I
    have another question for you in regards to JavaScript.  Do you think JavaScript frameworks that abstract
    developers away from HTML/CSS will end up being the future of web development?

    ReplyDelete
  4. If you're asking what I think you're asking, no.  "CONNECT" is the name of the hook
    being triggered.

    When using the class, you attach a method to that hook with $server->hook("CONNECT","method_name");_______________________As
    for the JavaScript bit.. I think it has an important place, but won't be the future.

    ReplyDelete
  5. with this example i get this if a client connects:
    PHP Warning:  Parameter 1
    to handle_connect() expected to be a reference, value given in /var/rba/socketserver.class.php on line 193

    ReplyDelete
  6. This is a masterpiece, after many days searching on google and many days struggling
    with socket servers, I've found you! and your wonderful code.

    This is definetly a great
    job, thank you very much for making my day man.

    ReplyDelete
  7. I'm glad you liked it!  If you have any bugs or any improvements to make with it,
    please feel free to put in a bug report or fork it on github at 
    https://github.com/navarr/PHP-SocketServer

    ReplyDelete
  8. Hello again, as I said before this is a masterpiece, unfortunatelly I've been testing
    as a socket server whit several devices connecting by GPRS, the connection is fine, but after a while the
    server doesn't seem to recognize that the device have changes socket, for some reason I think it could be a
    problem with the phone company and it's APN. 

    I have only 6 devices and almost 1000
    sockets opened. Have you experienced something like this before? Do I have to implement a system like a
    heartbeat to recognize the disconnection?

    Any advice is very welcome. I will keep
    working on it, and I'll post re results.

    Thanks anyway!

    ReplyDelete
  9. it should recognize and clear up disconnected sockets. I will probably need to do more
    testing before I can get back to you with a proper answer.

    ReplyDelete
  10. i experience this error too, any help?

    ReplyDelete
  11. This seems to be a general issue. I'm not passing it as a reference. Its only a warning
    so it can be ignored. Additionally, you can remove the ampersand from the first parameter of handle_connect.

    ReplyDelete
  12. Wicked bro!
    After proper check on out it's working, and some tuning to add
    some auth form for connected clients, and a couple of more treats, its been a solid php socks class so
    far... 

    nice.

    ReplyDelete
  13. Hi there. I've been trying to get my head around socket servers for days - as you say,
    the examples out there are either poorly explained or flatly don't work. For me, nothing worked; I kept
    getting socket bind errors, saying the host lookup failed. In fact that's what I get with yours, too, to
    start with, but then I changed the 'null' host reference to 'localhost'. This time the browser hung, as
    though it was loading a massive file. Is this expected behaviour? I opened a new tab and loaded the page
    again and it said the address was already in use. Presumably this means the previous page load started the
    server, and it's still running. How would I restart it? Do I need the console for this sort of thing? Thanks
    again.

    ReplyDelete
  14. If you try to run a server on a web page it will hang, yes.

    The
    expected use of this is over command line PHP, not as a web page. Binding as null means that it listens on
    all ports, if you're using shared hosting its possible that your provider doesn't allow it to listen for
    incoming connections. Using localhost means it'll listen to connections to it from within the server.
    -----
    Navarr
    T. Barnier
    [email protected]
    http://navarr.me/

    ReplyDelete
  15. Thanks, Navarr. So when would one start the script via the console - when launching the
    site? Would it then run, er, forever? I'm trying to get my head around the process here. Never done PHP via
    command line before (not even sure how on Mac...)

    ReplyDelete
  16. This program is a server in itself. You'd start it when you want the server running and
    it runs until an error or you close it.

    ReplyDelete
  17. Presumably if it's, say, a chat ap, though, I'd want it running constantly. So I'd
    start it when I launched the site and stop it... never?

    ReplyDelete
  18. Yes, essentially. Though in that case node us with web sockets would be the better
    technology. If your host doesn't allow node, you probably won't be allowed to run a socket server.

    If
    you're building a chat app you'll instead want to publish chats to a database and use Ajax polling and Ajax
    posting to keep it up to date.

    ReplyDelete
  19. Hey thanks for this fantastic class! Awesome piece of work! Just having a similar
    problem to others below.. When disconnecting/ inputting or connecting I receive this message


    "PHP
    Warning: Parameter 1 to handle_connect() expected to be a reference, value given in
    /var/www/test/SocketServer.class.php on line 203"


    or


    "PHP
    Warning: Parameter 1 to handle_input() expected to be a reference, value given in
    /var/www/test/SocketServer.class.php on line 203
    "
    etc


    And
    the functions do not run. Hopefully you can tell me where I'm being stupid!

    ReplyDelete
  20. I fixed it in the example. On that line just put an ampersand (&) in front of
    $this and $client.

    ReplyDelete
  21. Salvatore CampanellaOctober 9, 2012 at 9:13 PM

    Hi,
    I'm trying to use this cose. I'm trying with my Public Static IP using
    telnet, when I type the command line, this is what get out.


    telnet ***
    35100Trying ***...Connected to ***.Escape character is '^]'.


    Any ideas?

    ReplyDelete
  22. First off, it may not be entirely situated for telnet. There are a couple weird things
    going on there. It's a raw connection handler atm.


    Secondly, that looks like
    it's working... was there no "String?" request? If not, it may be telnet not knowing what to do with it.

    I
    recommend you download PuTTY.

    ReplyDelete
  23. Salvatore CampanellaOctober 9, 2012 at 10:09 PM

    Meanwhile, thanks for the reply.


    I tried using putty on
    port 35100 through SSH, but does not connect to the server either.Point out that the server is already using
    apache listen on port 35100, to address all requests that come to the script in question. La porta รจ aperta
    e il reindirizzamento funziona correttamente.
    Maybe I did not understand what you mean by use
    PuTTY. Is there something I'm missing?


    thanks.

    ReplyDelete
  24. This isn't a web page script. This is intended to be run from the command line with its
    own port.

    ReplyDelete
  25. Thank you.It's nice code.

    ReplyDelete
  26. Great job man! This works fine for me and will be very useful for my projects.

    ReplyDelete
  27. Thanks for the post. It was definitely educational, but the code is not very practical.
    The limitation of max_clients is too severe. And the blocking of the sockets that can happen on each
    iteration of max_clients can be a severe performance problem. As it stands, any kind of heavy handle_input
    processing would block all processing on all other connected sockets until that one socket connection was
    handled.

    If anyone is looking here for a jumpstart on their socket server, its good to
    get your feet wet, but you'll be wanting to settle with a non-blocking socket that forks each connection off
    into its own process so that you can process all connections asynchronously.

    Closest
    thing I've found to this is https://code.google.com/p/phpsocketdaemon/ but I haven't actually tested it yet
    and reviewed the code indepth to say if it does the job well. I originally skipped over it simply due to its
    age without updates.

    Also regarding this article and code, there's another explanation
    written at http://devzone.zend.com/209/writing-socket-servers-in-php/ which uses a lot of the same code as
    in the class above (not sure who came first, but doesn't matter) - so its a good read for those learning
    about socket servers and playing with this class.

    ReplyDelete
  28. Correct me if I'm wrong, but without some cross-socket communication forking will
    prevent the processes from sharing variables, right? (Not to mention a complete lack of semaphores and such
    in the PHP language, since it hasn't yet been written to take advantage of those types of features).

    Either
    way, thank you - I think this class is very good for getting your feet wet, as you've said. The DevZone
    article definitely came first - but was one of the many articles that I was referencing when I wanted to
    create this. I believe this will only block on input from any client plus processing time.



    Either
    way, I think a language like NodeJS is *much* better suited to this type of work (and the max connections
    can of course be changed)

    ReplyDelete
  29. Well cross-client communication could happen via proper DB integration, which is
    probably how it should be done considering most applications. I just finished some testing on that
    phpsocketdaemon script i linked and it doesn't seem to be asynchronous as it claims. Two connections in,
    while one is processing some data input, then other is waiting, and only continues after that first
    finishes. I've yet to dig into code to see if that behavior is configurable, or if its pretty much the same
    limitation.

    it'd be like having a web server that could only handle 1 request at a
    time, definitely a hefty limitation for any real world application.

    so i'm still on the
    hunt for the magic article or class that does the trick. If I have to write it up from scratch, I would
    probably rather go NodeJS since as you point out, it is much better suited since it is precisely what it was
    built for.

    ReplyDelete
  30. Well, not everything would be synced in the DB - for example an array of socket
    connections, or pointers to sockets, etc. It's not that big of a limitation for smaller servers (since a
    single processor can only do one thing at a time anyway), but once you get into quad-core etc then it
    becomes a limitation.

    In that case, it would be more preferential to do non IO
    intensive stuff for the socket and then stuff like updating the database in a quick fork so that the main
    program can continue on to the next socket.


    Keeping all of your socket and
    quick processing in one place while keeping your IO intensive stuff in another.


    This
    then only becomes a problem if you need to wait on data (from a file, db, etc.) before communicating with
    the socket and jumping from the next.


    But yeah, NodeJS is better for this
    kind of thing xD;

    ReplyDelete
  31. Thats a good point, and one that hadn't occurred to me. Just fork the intensive stuff.
    While there is overhead in forking, at least all the other clients aren't waiting on what one of the other
    clients caused to be executed. Again, clearly nodejs wins on efficiency, but in the end a little fork logic
    in the handler makes php able to handle it as well without the bottlenecks.


    In
    the end I just added some fork logic to the phpsocketdaemon i linked before. It doesn't have any setting for
    max clients, nor does it loop based on max clients, but the code is virtually without any comments, and
    there's not a lick of documentation, so there might still be some bombshells hiding in there I've yet to run
    across.


    In any case, your article gave me the well-documented launch point
    that I really needed, so hats off to you, thanks!

    ReplyDelete
  32. No problem and glad it could help. :)

    ReplyDelete
  33. Hi Navarr, Nice job thank you!

    I have a question. How can i send a
    message from one client connected to the other client?

    Not mass message to all others,
    just for a selected client?

    For example i have a client with id:0 another with id:1,
    third with id:2 etc. and i would like to send message from id:0 to id:2

    $clients[2]->socket
    does not seem to working?

    10x in advance!

    Daniel

    ReplyDelete
  34. If you want to be able to reference other clients, you'll want to create a hashmap with
    ids as keys and references to the socket object as values.


    On connect you'll
    want to do something like $clientMap[1] = &$client;

    You should also remember to
    clean this array up on disconnect.


    However you should still be able to do
    $clients[2]->socket (there is no reason that shouldn't work so long as $clients[2] is set and the
    socket is still connected).


    Let me know if you have any more problems. I
    think I'm going to re-evaluate the class and PSR-2 it and write tests and everything too. (Off topic, more
    of a reminder for myself)

    ReplyDelete
  35. Hey thank you!
    hashmap is a nice idea. i didn't know that i can save
    references to the socket object in it.
    $clients[2]->socket was a stupid mistake from me. I
    have forgotten to reference $server ;-)



    thanks once again, it
    just works!

    ReplyDelete