This weekend I revisited a hobby project of mine that I haven't given much attention recently. It's a private server for the now defunct MMORPG, Club Penguin. Club Penguin is a laid back game where players take on the avatar of a colorful penguin and essentially hang out and chat with one another. They can play mini-games for fun and to earn coins, which allows them to buy clothes for their avatar and furniture for their igloo, or to adopt virtual pets called Puffles. It was built with Adobe Flash back in 2005 after a couple of reiterations from its less exciting predecessor, Penguin Chat.
This isn't an idea that I spearheaded, but I have been apart of the community it was hatched in; in fact it's how I got my start programming! The challenges of reverse engineering the game's Flash client, and understanding the concepts behind sockets and object-oriented programming in PHP, were as tough as they were rewarding to my adolescent mind.
Earlier this year I set out to rewrite the game server that I created when I was 13. With new tools at my disposal like Composer and new language features in PHP 7, I wanted to see how far I've come since my days in the Club Penguin community. When I cloned my repository from Bitbucket this past Saturday, and got back into testing what I've already completed, I was reminded of a unique problem that sat beneath a single line /* @todo */
from a few months ago.
The problem: Finding a client by their socket
I have a method called getRequests()
that acts as a Generator
for incoming data from connected clients. It's intended to be iterated over by the server daemon, which will then pass it along to a callback that interprets the data and does something with it.
Within getRequests()
there's a call to socket_select()
in order to see who's trying to talk to the server. Each user is represented by a Client
object, which is essentially a value object that stores metadata about the user, and their socket resource. When we want to poll the clients for incoming data, a list containing each connected Client
is iterated over and each Client
's socket is put into a temporary list.
When there's data to be consumed, socket_select()
will return the appropriate sockets in the array variable passed to it. That array is then iterated over and socket_recv()
is called.
If socket_recv()
returns null
then we know the user has disconnected, and their object needs to be removed from the list. How can this be done when the only data we have to identify the user is their socket?
It would be possible here to use a foreach
loop, call Client::getResource()
(which returns the socket), and do a comparison to find the right object. But I felt like this would convolute the getRequests()
method.
The solution: SplObjectStorage
I opted to use a subclass of SplObjectStorage
over an array to store the Client
objects. Semantically it made more sense, because I'm working with a set of objects and this is a data structure devoted to that purpose. SplObjectStorage
lends some useful functionality with a very negligible performance trade-off from arrays. I could place the code for socket identification in here, which I felt was more appropriate semantically and for readability. Finally, type safety! Instead of simply passing around an array, I now can type hint ClientList
.
<?php
namespace p810\phpCP;
use Exception;
class ClientList extends \SplObjectStorage {
public function findBySocket($socket): Client {
foreach ($this as $key => $client) {
if ($client->getResource() === $socket) {
return $client;
}
}
/** @todo make this better lol */
throw new Exception;
}
}
The only hurdle I ran into (if I can even call it one) is that at first I wasn't sure how to loop over an Iterator
's items from within it. One minute's worth of Googling provided me the answer.
This is a small achievement, but one that I'm proud of. It gave me a bit of wind to continue working on this project throughout my Saturday afternoon without becoming distracted and starting something else as I'm wont to do.
Sometimes with a project it's the little challenges that can bring us the most satisfaction.
Useful takeaways
- PHP's Standard PHP Library (SPL) contains a bunch of useful interfaces and classes for design solutions like the one I implemented above. It's worth checking out and becoming familiar with!
- When it feels like a function is becoming too bloated, you should consider refactoring. As was the case above with
getRequests()
, certain functionality was better suited for a new object.ClientList
allowed me to reduce the size ofgetRequests()
by ~12.5%. That's not a huge number, but it certainly makes a difference to people who may work with my code in the future; it makes the function more human friendly.
Top comments (0)