TCP Networking in Java with Socket and ServerSocket
TCP provides a reliable, ordered, byte-stream channel between two endpoints. In application code, endpoints are explicitly divided into a client that initiates a connection and a server that listens for and accepts connections. A server must be running before any client can connect.
Roles and connection flow
- Server starts a listening socket on a known port.
- Client creates a socket and attempts to connect to the server’s address and port.
- The server’s accept operation completes, returning a connected socket representing the client.
- Both sides exchange data using the socket’s input and output streams until one side shuts down or closes.
Loopback and addressing
- 127.0.0.1 (localhost) targets the local machine only; traffic never leaves the host. Useful for testing.
Socket (cliant endpoint)
- Purpose: represents one side of a TCP connection and provides input/output streams.
- Key constructor:
- Socket(String host, int port): opens a TCP connection to the given host and port.
- Core methods:
- InputStream getInputStream(): receive bytes from the peer. If the socket is closed, the stream is closed as well.
- OutputStream getOutputStream(): send bytes to the peer. Closing this stream closes the socket.
- void shutdownOutput(): finish the outbound half of the connectino; pending data is flushed, and the peer will observe end-of-stream on reads.
- void close(): closes both input and output streams and releases the socket.
ServerSocket (listening endopint)
- Purpose: listens on a TCP port and accepts incoming connections.
- Key constructor:
- ServerSocket(int port): binds and listens on the specified port.
- Core method:
- Socket accept(): blocks until a client connects, then returns a Socket for that client.
Connection and data exchange sequence
- Server: create ServerSocket and block in accept.
- Client: open Socket to server address/port.
- Server: accept completes and returns a Socket dedicated to the client.
- Client: write request bytes via getOutputStream().
- Server: read request bytes via getInputStream().
- Server: optionally write a response via getOutputStream().
- Client: read response via getInputStream().
- Either side: shut down output and/or cloce the socket to end the session.
Example: client sends a single message to a server
Server (reads the entire request and prints it):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class OneShotServer {
public static void main(String[] args) throws IOException {
try (ServerSocket listener = new ServerSocket(5000)) {
System.out.println("Server listening on 5000");
try (Socket conn = listener.accept();
InputStream in = conn.getInputStream()) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
// Read until client signals end-of-stream (shutdownOutput or close)
while ((n = in.read(chunk)) != -1) {
buffer.write(chunk, 0, n);
}
String body = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
System.out.println("Received: " + body);
}
}
}
}
Client (sends bytes, then signals end of output):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class OneShotClient {
public static void main(String[] args) throws IOException {
try (Socket sock = new Socket("127.0.0.1", 5000);
OutputStream out = sock.getOutputStream()) {
byte[] payload = "Hello over TCP — one-way message".getBytes(StandardCharsets.UTF_8);
out.write(payload);
out.flush();
// Tell the server no more data is coming
sock.shutdownOutput();
}
}
}
Example: server replies to the client
Server (reads a line-terminated request and writes a line-terminated reply):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class RequestReplyServer {
public static void main(String[] args) throws IOException {
try (ServerSocket listener = new ServerSocket(5001)) {
System.out.println("Server listening on 5001");
try (Socket conn = listener.accept();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8))) {
String line = reader.readLine(); // expects newline-terminated request
System.out.println("Client says: " + line);
String reply = "ACK: received " + (line == null ? "<no data>" : "" + line.length() + " bytes");
writer.write(reply);
writer.newLine();
writer.flush();
}
}
}
}
Client (sends a line and reads a line response):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class RequestReplyClient {
public static void main(String[] args) throws IOException {
try (Socket sock = new Socket("localhost", 5001);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8));
BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8))) {
writer.write("How are you, server?");
writer.newLine();
writer.flush();
String response = reader.readLine();
System.out.println("Response: " + response);
}
}
}
Key opertaional notes
- Always use the streams obtained from the Socket for network I/O; do not replace them with custom streams that bypass the socket.
- If the server is not listening when a client attempts to connect, the client will receive a ConnectException (connection refused).
- Closing a socket implicitly closes its streams. Alternatively, shutdownOutput() allows half-closure to signal end-of-request while keeping the connection open to receive a response.