I think it's a good way to keep your comms stack clean. A socket is just an instance of an abstract "character stream", specialized to work over a network. Other instances include files (over the disk), strings (over internal memory), and STDIN (over the keyboard). Unix got this one exactly right with its "everything is a file" mantra.
If you can separate the socket-specific details from the stream-agnostic usage, you can easily use any input source you'd like. You can do testing easily by using STDIN or a file as a source, and it's not hard to imagine using in-memory streams between NPCs and the game world so that PCs and NPCs interact in the same manner. Fewer code paths means less complexity!
I started out with a monolithic module for handling connections, but it rapidly became apparent that I needed separate abstractions.
1) A Descriptor, an abstract class which provides high level stream mechanics (such as turning echo on and off, sending and receiving data, reporting screen width and terminal capabilities)
2) A ConcreteDescriptor which receives an input stream and performs protocol negotiations, and possibly some basic character set manipulations. It is likely that data compression would be done here or even below this interface.
3) A Communicator, which controls instances of the ConcreteDescriptor through the Descriptor interface. This is the module responsible for doing things like asking for the player name, etc. It may attach private data to each descriptor. While at first this seems unclean, it is in fact, necessary because an instance of a ConcreteDescriptor does not yet have a player connected to it to which you can attach state. Nor do you want multiple Communicator objects; it exists fine as a Singleton. (Well, if you had different portals to different worlds, you may want multiple Communicators, but I am only running a single MUD in my namespace).
there are several other modules…
4) Graphics, which provides text formatting based on the character set and display width. This is called out to by the Communicator and higher level modules to do fancy formatting tricks like centering text. It is possible you may want to have multiple Graphics modules to render to different markup types. I haven't got this far yet, but it is a step towards a slightly cleaner architecture.
5) VT100, Ansi modules which provide support functions for color, input key mapping to the Graphics and Communicator modules. Mostly this is equivalent in my system to a few terminfo libraries.
6) Unicode, a module which given a list of supported Unicode code blocks, and a character set to translate to, translates embedded text sequences into either the appropriate glyphs or Latin romanized equivalents. This module must be invoked above or by the graphics module, as it may change the width of characters (for example ? would be rewritten as TZ if the glyph is not supported on the code page.) You may want to invoke this at a very high level, inside game state where it knows how to do appropriate things like swapping names to proper Title Case and so forth, as that information could be lost in the output.
I didn't find any need to create a separate Acceptor object, much of what would constitute an Acceptor (such as Telnet protocol negotiation) is included directly in the ConcreteDescriptor. While that state could be jettisoned after connection (one of the benefits of an Acceptor), in our case, it is useful to have it hang around, as things like IP address, port, terminal type, Telnet user variables, all are quite useful. It seems decoupling the Acceptor from the telnet specifics would require creating a new layer which has to communicate with the descriptor without any real value.
The real value was in pulling the Communicator object outside of the descriptor so that there is a uniform high level way to interact with the game. ConcreteDescriptors can track their own protocol negotiation, and the Communicator can now store state information (the "where are we in the dialog of logging in") without having this pollute the low level code.