Concepts / Sockets and Client-Server Communication

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

  • Programming

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?

  • The connection thread's run() method

  • The main loop that calls accept()

  • The client program on the other machine

Reveal answer
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.

accept() waitsmain threadClient connectslistening portaccept() returnsSocketone clientnewConnectionHandler(socket)wraps the sockethandler.start()returns immediatelyrun() executeshandler threadLoop back to accept()main threadwaitsyespasses socketstart()new threadmain continuesrepeat
What happens step by step in the server's main loop when a client connects, and where does control go after the connection is handed off?

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.

  1. 1
    Read the client's address and keep it in a variable, so that log messages can name the client.
  2. 2
    Build the current date and time.
  3. 3
    Wrap the client's output stream in a writer.
  4. 4
    Send the date as one line.
  5. 5
    Call flush() to push the bytes out rather than leaving them in a buffer.
  6. 6
    Close the client socket.
run() methodhandler threadclient Socketreceived from main threadSend date lineone lineClient addressstored for logsflush()pushes bytes outDate and timebuilt freshClose socketends this clientWriter over outputstreamwraps socket outputusesstep 1step 2step 3step 4step 5step 6
What set of tasks does run() own for one client, and how do those tasks relate to the socket and the thread that owns them?

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().

Client oneconnects firstMain threadruns accept() loopHandler thread oneruns run()Client twoconnects laterconnectnew ConnectionHandler, start()loops back to accept()connectsecond handler startedsends date line, closes
While one connection is being set up in its own thread, what is the main thread doing, and how does a second client get served at the same time?

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 fails

    The 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 instead

    Start 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 like

    Putting the date-sending code inside the constructor that stores the socket

    Why it fails

    Constructors run in the main thread, so slow work there delays the accept() loop for every other client.

    Do this instead

    Keep 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

Worked example

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.

  1. 1
    Client 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.

  2. 2
    Handoff for client one

    The main thread creates a ConnectionHandler around that socket and calls start(). The constructor only stores the socket. start() causes run() to execute in a new thread, and the main thread loops back to accept().

  3. 3
    Client 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.

  4. 4
    Handoff for client two

    The main thread creates a second ConnectionHandler, stores the new socket, and calls start(). Now two handler threads each run their own run() method.

  5. 5
    Each handler finishes its own client

    Inside each run(), the handler reads the client address, builds the date, wraps the output stream, sends the line, calls flush(), and closes its own socket. Neither handler touches the other client's socket.

Result

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

MEDIUM

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

  1. 1
    The main loop calls accept(), wraps each returned socket in a ConnectionHandler, calls start(), and loops back to accept().
  2. 2
    start() causes run() to execute in a new thread, so the main thread has no further involvement with that client.
  3. 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.
  4. 4
    The main thread must not block, so the connection constructor only stores the socket and slow work stays in run().
  5. 5
    When 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 ConnectionHandler thread 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.