Sockets and Client-Server Communication
A threaded server accepts each connection in its main loop and hands that connection to a new thread rather than handling it directly
Why One Client at a Time Is Not Enough
Imagine a server that handles one connection at a time. It finishes each client completely before it even looks at the next incoming request. If the first client is slow to read its data, every other client waits in line behind it. The server is correct, but it is only as fast as its slowest client.
A threaded server is a different design. Whenever a connection request arrives, the server gives that connection to a separate thread and immediately returns to listening for more requests. The part of the program that waits for new connections stays free to do its job, while each connection is handled elsewhere. This article follows one concrete program, DateServerWithThreads, to show how that handoff is written and where to look when something goes wrong.
What do you think happens?
If the server hands each connection to its own thread, which part of the program must stay free so that a new client can be accepted while an earlier client is still being served?
Reveal answer
The main loop that calls accept()
The source says the main thread must stay free to call accept(). Each client's work happens in a connection thread, so the main loop can return to listening for new requests right away.
Finding the Core of DateServerWithThreads
Before reading any explanation, look at the core of the program. The program has two parts that matter most. The main loop is the part that runs forever, waiting for clients. The ConnectionHandler class is the part that does the actual work for one client. Your first task when reading a threaded server is to find these two pieces and separate them in your mind.
Within that code, two statements matter. One creates the handler object, and the other starts it. Also notice which method contains the logic that talks to the client. The source identifies that method as run(), and the rest of this article traces how control moves between the main loop and run().
Tracing One Connection Through the Main Loop
Walk through a single connection step by step. A client attempts to connect to the listening port. The accept() call in the main thread returns a Socket that represents that one client. The main thread wraps that socket in a new ConnectionHandler object and calls start() on it. Calling start() is what causes the handler's run() method to execute in its own thread. Once start() has returned, the main thread has no further involvement with that client. It simply loops back to accept() and waits for the next one.
The figure shows the handoff as a path of data. The socket is produced by the main thread, passed to the handler object, and then used inside run() to send the reply. The main thread appears on this path only up to the point where the handler is started. After that, the connection's data path runs through the handler thread, while the main thread's path returns to waiting for the next client.
What the run() Method Owns
The run() method takes over the job that a single-connection program would do directly. Its body is copied from the original single-threaded date program, which is why the handler looks so familiar. Everything that talks to one client lives here, and it runs in the connection's own thread.
- 1Read the client's address and keep it in a variable, so that log messages can name the client.
- 2Build the current date and time.
- 3Wrap the client's output stream in a writer.
- 4Send the date as one line.
- 5Call
flush()to push the bytes out rather than leaving them in a buffer. - 6Close the client socket.
Notice that the socket is the thread's resource. The main thread created it and handed it over, and run() is the code that uses it to send the reply and then closes it. Every other step in the list is a task performed on behalf of that one client, so the whole list belongs to a single connection thread.
Why the Main Thread Must Not Block
The main thread must stay free to call accept(). If it became stuck while setting up one connection, the server would be unable to accept new clients promptly. The source explains the reason in a specific way. Creating the handler object and starting it takes very little time. The constructor of the connection thread only stores the socket it was given. Constructors run in the main thread, so any slow work placed in a constructor would delay the main loop.
This is why the program keeps all the reading and writing inside run(). That method runs in the new thread, so slow client-facing work happens off the main thread. The main thread's only jobs during a handoff are to create the handler, store the socket, and call start().
The sequence shows the timing. The main thread hands off the first connection and returns to accept() without waiting for the handler to finish. Because it is already listening again, it can receive the second client while the first handler is still working. The second client is then served through its own handler, so neither client waits in line behind the other.
Where Connection Failures Show Up
When a connection fails in this design, you do not look at the main loop first. The most useful places to look are the flush and close path inside run(), and the error message printed by the catch block. The flush and close steps are where the bytes actually leave the handler, and the catch block is where the failure is reported. Because run() is the code that talks to the client, a failure during that talk is most likely to surface in the connection thread.
Common mistake Searching only the main loop when a single client fails
Why it failsThe main loop only accepts the connection and starts the handler. The client-facing work, including sending the date and closing the socket, happens in
run().Do this insteadStart with run(), then check the flush and close path and the catch block's error message.
Common mistake Moving slow reading or writing into the connection thread's constructor
What it looks likePutting the date-sending code inside the constructor that stores the socket
Why it failsConstructors run in the main thread, so slow work there delays the
accept()loop for every other client.Do this insteadKeep the constructor to storing the socket and put all client-facing work in run().
Common mistake Assuming start() runs run() in the main thread instead of a new thread
Common mistake Forgetting that flush() is needed to push bytes out of the buffer before the socket is closed
Worked Example: Two Clients Arriving Close Together
Tracing two clients through DateServerWithThreads
Two clients connect to the server one after the other. Trace which thread does each piece of work, and say where control is at each point.
- 1Client one connects
The
accept()call in the main thread returns a Socket for client one. The main thread is the only thread involved so far. - 2Handoff for client one
The main thread creates a
ConnectionHandleraround that socket and callsstart(). The constructor only stores the socket.start()causesrun()to execute in a new thread, and the main thread loops back toaccept(). - 3Client two connects while client one is served
The main thread is back in
accept(), so it receives client two's socket. Client one's handler may still be running in its own thread, and that does not stop the main loop. - 4Handoff for client two
The main thread creates a second
ConnectionHandler, stores the new socket, and callsstart(). Now two handler threads each run their ownrun()method. - 5Each handler finishes its own client
Inside each
run(), the handler reads the client address, builds the date, wraps the output stream, sends the line, callsflush(), and closes its own socket. Neither handler touches the other client's socket.
The main thread only accepts and hands off. Each client is served by its own handler thread, and the accept loop never waits on either client's reading or writing.
Practice: Trace a Failing Client
A client connects to the threaded date server, but the date line never reaches it and an error is printed. Without changing any code, name the thread that is most likely doing the failing work, the method where you would look first, and the two places in that method where the failure would most likely be visible.
Hints
- Ask which thread runs the code that talks to the client.
- The source names the flush and close path and the catch block's error message as the places to check.
- Remember that the main loop only accepts and hands off the socket.
Key Takeaways from the Threaded Design
- 1The main loop calls
accept(), wraps each returned socket in aConnectionHandler, callsstart(), and loops back toaccept(). - 2
start()causesrun()to execute in a new thread, so the main thread has no further involvement with that client. - 3
run()owns all client-facing work for one connection: reading the address, building the date, writing and flushing the line, and closing the socket. - 4The main thread must not block, so the connection constructor only stores the socket and slow work stays in
run(). - 5When a connection fails, check the flush and close path in
run()and the error message printed by the catch block.
At a glance
- A threaded server's main loop accepts each connection and hands the socket to a new
ConnectionHandlerthread instead of serving it directly. run()performs all client-facing work for one connection, including sending the date, flushing the output, and closing the socket.- The main thread must stay free to call
accept(), so the connection constructor should only store the socket and must not block. - When a connection fails, look at the flush and close path inside
run()and at the error message printed by the catch block.