Socket programming forms the backbone of network communication in modern applications. Whether you're building a web service, a chat application, or a distributed system, understanding how to work with sockets is essential. This blog will introduce you to the fundamentals of socket programming in Java, with a special focus on TCP communication.
Understanding Sockets: IP Address + Port
A socket represents an endpoint for network communication. Think of it as a door through which your application sends and receives data over a network. Every socket is uniquely identified by two components:
IP Address: This identifies the machine on the network. It's like a street address that tells you which house (computer) to deliver the message to. IPv4 addresses look like 192.168.1.100, while IPv6 addresses are longer and use hexadecimal notation.
Port Number: This identifies the specific application or service on that machine. If the IP address is the street address, the port number is like the apartment number. Ports range from 0 to 65535, with ports 0-1023 reserved for well-known services (like HTTP on port 80).
Together, an IP address and port number form a socket address. For example, 192.168.1.100:8080 represents a socket at IP address 192.168.1.100 on port 8080.
TCP vs UDP: Choosing Your Protocol
When working with sockets, you'll primarily use two transport protocols: TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). Understanding their differences is crucial for choosing the right one for your application.
TCP (Transmission Control Protocol):
Connection-oriented: Establishes a connection before sending data
Reliable: Guarantees delivery of all packets in the correct order
Error checking: Automatically detects and retransmits lost packets
Flow control: Adjusts transmission speed based on network conditions
Higher overhead: More network traffic due to acknowledgments and connection management
Use cases: Web browsing, email, file transfers, any application where data integrity is critical
UDP (User Datagram Protocol):
Connectionless: No connection establishment, just send data
Unreliable: No guarantee of delivery or order
No error recovery: Lost packets are not retransmitted
No flow control: Sends at whatever rate the application specifies
Lower overhead: Minimal protocol overhead
Use cases: Live video streaming, online gaming, DNS queries, IoT sensors, any application where speed matters more than reliability
The choice between TCP and UDP depends on your application's requirements. If you need every byte to arrive correctly and in order, use TCP. If you need low latency and can tolerate some packet loss, UDP is your friend.
Deep Dive into TCP Socket Programming
Now let's focus on TCP programming in Java. Unlike UDP's fire-and-forget datagrams, TCP gives you a connection: before any data flows, the client and server perform a three-way handshake, and from that point on you read and write streams of bytes rather than individual packets. Java models this with two core classes:
ServerSocket: Used by the server to listen on a port and accept incoming connections.
Socket: Represents one established connection. Both the client and the server use a Socket to read and write data through input/output streams.
We'll build several programs that demonstrate different aspects of TCP communication.
Basic TCP Server
Let's start with a simple TCP server that listens for connections and echoes back every line a client sends:
import java.net.*;
import java.io.*;
public class TCPServer {
private ServerSocket serverSocket;
private boolean running;
public TCPServer(int port) throws IOException {
// IMPORTANT: Binding to localhost (127.0.0.1) for security and learning purposes
// This means the server will only accept connections from the same machine
// For production use or to accept connections from other machines, you would use:
// serverSocket = new ServerSocket(port); // This binds to all interfaces (0.0.0.0)
// or
// serverSocket = new ServerSocket(port, 50, InetAddress.getByName("your.public.ip"));
// For learning and experimentation, localhost is recommended as it:
// 1. Doesn't require firewall configuration
// 2. Is more secure (not exposed to the network)
// 3. Works even without internet connection
InetAddress localhost = InetAddress.getByName("127.0.0.1");
serverSocket = new ServerSocket(port, 50, localhost);
System.out.println("TCP Server started on " + localhost.getHostAddress() + ":" + port);
System.out.println("NOTE: Server is bound to localhost only. Connections from other machines will be refused.");
}
public void run() {
running = true;
while (running) {
try {
// Wait for a client to connect (this method blocks until a connection arrives)
Socket clientSocket = serverSocket.accept();
// Handle each client on its own thread so multiple clients
// can be served at the same time
ClientHandler handler = new ClientHandler(clientSocket);
handler.start();
} catch (IOException e) {
if (running) {
System.err.println("Error in server: " + e.getMessage());
}
}
}
}
// Thread class that serves one connected client
private static class ClientHandler extends Thread {
private Socket socket;
ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
String clientInfo = socket.getInetAddress() + ":" + socket.getPort();
System.out.println("Client connected: " + clientInfo);
try (
// TCP gives us streams, not packets. We wrap them in
// reader/writer classes to work with lines of text.
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
String line;
// readLine() returns null when the client closes the connection -
// TCP tells us about disconnects, unlike UDP!
while ((line = in.readLine()) != null) {
System.out.println("Received from " + clientInfo + " - " + line);
out.println("Echo: " + line);
}
} catch (IOException e) {
System.err.println("Connection error with " + clientInfo + ": " + e.getMessage());
} finally {
try {
socket.close();
} catch (IOException e) {
// Ignore errors on close
}
System.out.println("Client disconnected: " + clientInfo);
}
}
}
public void stop() {
running = false;
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
// Ignore errors on close
}
}
public static void main(String[] args) {
int port = 6000; // Default port
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
System.err.println("Invalid port number. Using default port 6000.");
}
}
try {
TCPServer server = new TCPServer(port);
System.out.println("\n=== TCP Echo Server ===");
System.out.println("Server is running on localhost only for security.");
System.out.println("Clients must connect to 127.0.0.1:" + port);
System.out.println("Press Ctrl+C to stop the server.\n");
server.run();
} catch (IOException e) {
System.err.println("Could not start server: " + e.getMessage());
System.err.println("TIP: Make sure port " + port + " is not already in use.");
}
}
}
This server demonstrates several key concepts:
ServerSocket: The listening socket. It doesn't carry data itself - its only job is to accept() incoming connections and hand back a Socket for each one.
Blocking accept(): The accept() method blocks until a client connects. This is why we run it in a loop.
Streams instead of packets: Once connected, we read and write ordinary Java I/O streams. TCP handles splitting the data into packets, retransmitting lost ones, and reassembling them in order.
Connection awareness: readLine() returning null tells us the client has disconnected. With UDP we'd need our own keep-alive mechanism to detect this.
Thread per client: Because each connection is a long-lived conversation, we serve each client on its own thread so one slow client doesn't block the others.
Basic TCP Client
Now let's create a client that can connect to our server and exchange messages:
import java.net.*;
import java.io.*;
import java.util.Scanner;
public class TCPClient {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public TCPClient(String serverHost, int serverPort) throws IOException {
// Unlike UDP, TCP requires an explicit connection.
// This constructor performs the three-way handshake with the server -
// if the server isn't listening, it throws ConnectException immediately.
// For learning purposes, we recommend using "localhost" or "127.0.0.1"
// This keeps all traffic local to your machine
socket = new Socket(serverHost, serverPort);
// Set a timeout for read operations (optional but recommended)
socket.setSoTimeout(5000); // 5 second timeout
// Wrap the connection's streams for line-based text communication
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
System.out.println("Connected to " + serverHost +
" (" + socket.getInetAddress().getHostAddress() + "):" + serverPort);
System.out.println("Local endpoint: " + socket.getLocalAddress().getHostAddress() +
":" + socket.getLocalPort());
// If connecting to localhost, inform the user
if (socket.getInetAddress().isLoopbackAddress()) {
System.out.println("NOTE: Connected to localhost. This is perfect for learning!");
} else {
System.out.println("WARNING: Connected to external host. Make sure firewall allows TCP on port " + serverPort);
}
}
public void sendMessage(String message) throws IOException {
// Write one line to the connection - TCP guarantees it arrives
// completely and in order, no manual packet handling needed
out.println(message);
System.out.println("Sent: " + message);
try {
// Wait for the echoed response
String response = in.readLine();
if (response == null) {
System.out.println("Server closed the connection.");
} else {
System.out.println("Received: " + response);
}
} catch (SocketTimeoutException e) {
System.out.println("No response received (timeout)");
}
}
public void close() {
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
// Ignore errors on close
}
}
public static void main(String[] args) {
// Default to localhost for safe learning environment
String serverHost = "localhost";
int serverPort = 6000;
// Parse command line arguments
if (args.length >= 1) {
serverHost = args[0];
}
if (args.length >= 2) {
try {
serverPort = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("Invalid port number. Using default port 6000.");
}
}
System.out.println("\n=== TCP Echo Client ===");
System.out.println("For learning, it's recommended to use 'localhost' or '127.0.0.1'");
System.out.println("To connect to external servers, use their IP address or hostname");
System.out.println("Current target: " + serverHost + ":" + serverPort + "\n");
try {
TCPClient client = new TCPClient(serverHost, serverPort);
Scanner scanner = new Scanner(System.in);
System.out.println("Enter messages to send (type 'quit' to exit):");
while (true) {
System.out.print("> ");
String message = scanner.nextLine();
if ("quit".equalsIgnoreCase(message)) {
break;
}
try {
client.sendMessage(message);
} catch (IOException e) {
System.err.println("Error sending message: " + e.getMessage());
break; // Connection is broken, no point continuing
}
}
scanner.close();
client.close();
System.out.println("Client shutdown.");
} catch (ConnectException e) {
System.err.println("Could not connect: " + e.getMessage());
System.err.println("TIP: Make sure the server is running on " + serverHost + ":" + serverPort);
} catch (UnknownHostException e) {
System.err.println("Unknown host: " + e.getMessage());
System.err.println("TIP: Use 'localhost' or '127.0.0.1' for local testing.");
} catch (IOException e) {
System.err.println("Could not create client: " + e.getMessage());
}
}
}
Key points about the client:
Explicit connection: new Socket(host, port) performs the TCP handshake. If the server isn't there, you find out immediately via ConnectException - UDP would just silently drop your packets.
Persistent conversation: The same connection is reused for every message. There's no need to include the destination address with each send like UDP's DatagramPacket requires.
Failure detection: If the connection breaks mid-conversation, reads and writes throw exceptions, so the client knows to stop rather than shouting into the void.
Port assignment: The client doesn't specify a port for itself; the OS assigns one automatically.
Advanced Example: TCP Chat Application
Let's create a more sophisticated example - a multi-user chat application using TCP. This will demonstrate broadcasting and managing many concurrent connections:
import java.net.*;
import java.io.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TCPChatServer {
private ServerSocket serverSocket;
private boolean running;
// Store active clients - each entry is a live connection handler
private final Map<String, ClientHandler> clients = new HashMap<>();
public TCPChatServer(int port) throws IOException {
// IMPORTANT: Binding to localhost (127.0.0.1) for security and learning purposes
// This configuration means:
// - Only clients on the same machine can connect
// - No firewall configuration needed
// - Safe for experimentation and learning
//
// For production or multi-machine chat:
// - Use: serverSocket = new ServerSocket(port); // Binds to all interfaces
// - Or bind to specific public IP: serverSocket = new ServerSocket(port, 50, InetAddress.getByName("your.ip"));
// - Configure firewall to allow TCP traffic on the chosen port
InetAddress localhost = InetAddress.getByName("127.0.0.1");
serverSocket = new ServerSocket(port, 50, localhost);
System.out.println("TCP Chat Server started on " + localhost.getHostAddress() + ":" + port);
System.out.println("IMPORTANT: Server is bound to localhost only.");
System.out.println("Only local clients can connect. For network-wide access, modify binding address.");
System.out.println("----------------------------------------");
// Note: unlike a UDP chat server, we need NO cleanup thread and NO
// PING/keep-alive protocol. TCP tells us when a client disconnects -
// the handler's readLine() returns null or throws, and we clean up there.
}
public void run() {
running = true;
while (running) {
try {
Socket socket = serverSocket.accept();
// One thread per connected client
ClientHandler handler = new ClientHandler(socket);
handler.start();
} catch (IOException e) {
if (running) {
System.err.println("Error in server: " + e.getMessage());
}
}
}
}
// Thread class that manages one client's connection for its whole lifetime
private class ClientHandler extends Thread {
private final Socket socket;
private PrintWriter out;
private String username;
ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
String line;
while ((line = in.readLine()) != null) {
if (line.equals("LEAVE")) {
break;
}
processMessage(line);
}
} catch (IOException e) {
// Connection dropped unexpectedly - handled in finally
} finally {
handleDisconnect(this);
try {
socket.close();
} catch (IOException e) {
// Ignore errors on close
}
}
}
private void processMessage(String message) {
// Handle different message types
if (message.startsWith("JOIN:")) {
handleJoin(this, message.substring(5));
} else if (message.startsWith("MSG:")) {
handleMessage(this, message.substring(4));
} else if (message.equals("LIST")) {
handleList(this);
}
}
void send(String message) {
if (out != null) {
out.println(message);
}
}
}
private synchronized void handleJoin(ClientHandler handler, String username) {
handler.username = username;
clients.put(username, handler);
String joinMessage = "SYSTEM: " + username + " has joined the chat!";
broadcastMessage(joinMessage, handler);
// Send welcome message to the joining client
handler.send("SYSTEM: Welcome to the chat, " + username + "! Type 'LIST' to see online users.");
// Log the connection - note if it's from localhost
InetAddress address = handler.socket.getInetAddress();
String connectionInfo = address.isLoopbackAddress() ? " (local connection)" : " (external connection)";
System.out.println("Client joined: " + username + " from " + address +
":" + handler.socket.getPort() + connectionInfo);
}
private synchronized void handleMessage(ClientHandler sender, String message) {
if (sender.username == null) {
sender.send("SYSTEM: Please JOIN before sending messages.");
return;
}
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
String formattedMessage = "[" + timestamp + "] " + sender.username + ": " + message;
broadcastMessage(formattedMessage, null);
System.out.println(formattedMessage);
}
private synchronized void handleList(ClientHandler requester) {
StringBuilder sb = new StringBuilder("SYSTEM: Online users:");
for (String name : clients.keySet()) {
sb.append(" ").append(name);
}
requester.send(sb.toString());
}
private synchronized void handleDisconnect(ClientHandler handler) {
if (handler.username != null && clients.remove(handler.username) != null) {
String leaveMessage = "SYSTEM: " + handler.username + " has left the chat.";
broadcastMessage(leaveMessage, handler);
System.out.println("Client left: " + handler.username);
}
}
private synchronized void broadcastMessage(String message, ClientHandler exclude) {
for (ClientHandler client : clients.values()) {
if (client != exclude) {
client.send(message);
}
}
}
public void stop() {
running = false;
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
// Ignore errors on close
}
}
public static void main(String[] args) {
int port = 6001;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
System.err.println("Invalid port number. Using default port 6001.");
}
}
System.out.println("\n=== TCP Chat Server ===");
System.out.println("Starting server on localhost for safe learning environment...");
System.out.println("To allow connections from other machines:");
System.out.println("1. Modify the code to bind to 0.0.0.0 or your public IP");
System.out.println("2. Configure your firewall to allow TCP port " + port);
System.out.println("3. Share your public IP with clients\n");
try {
TCPChatServer server = new TCPChatServer(port);
System.out.println("Server is ready for connections!");
System.out.println("Press Ctrl+C to stop the server.\n");
server.run();
} catch (IOException e) {
System.err.println("Could not start server: " + e.getMessage());
System.err.println("TIP: Make sure port " + port + " is not already in use.");
}
}
}
import java.net.*;
import java.io.*;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
public class TCPChatClient {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private String username;
private AtomicBoolean running = new AtomicBoolean(false);
private Thread receiveThread;
// Thread class for receiving messages - chat messages can arrive at any
// moment (someone else typed), so we listen on a dedicated thread
private class ReceiveThread extends Thread {
public void run() {
try {
String message;
while (running.get() && (message = in.readLine()) != null) {
System.out.println(message);
}
} catch (IOException e) {
if (running.get()) {
System.err.println("Connection lost: " + e.getMessage());
}
}
if (running.get()) {
System.out.println("Disconnected from server. Press Enter to exit.");
running.set(false);
}
}
}
public TCPChatClient(String serverHost, int serverPort, String username)
throws IOException {
// Connect to the server - this is our single, persistent connection
// For learning: use "localhost" or "127.0.0.1"
// For network chat: use the server's IP address
this.socket = new Socket(serverHost, serverPort);
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
this.out = new PrintWriter(socket.getOutputStream(), true);
this.username = username;
// Inform user about connection type
if (socket.getInetAddress().isLoopbackAddress()) {
System.out.println("Connected to local server (localhost) - perfect for learning!");
} else {
System.out.println("Connected to remote server at " + serverHost);
System.out.println("Make sure the server allows external connections and firewall permits TCP port " + serverPort);
}
}
public void connect() {
// Send join message
out.println("JOIN:" + username);
// Start receiving messages
running.set(true);
receiveThread = new ReceiveThread();
receiveThread.start();
System.out.println("Connected to chat server as: " + username);
System.out.println("Server: " + socket.getInetAddress().getHostAddress() +
":" + socket.getPort());
System.out.println("\nCommands: /list (show users), /quit (exit), or just type to chat");
// Note: no PING thread needed! TCP keeps the connection alive and
// both sides find out promptly if it breaks.
}
public void sendChatMessage(String message) {
if (message.trim().isEmpty()) {
return;
}
if (message.equalsIgnoreCase("/list")) {
out.println("LIST");
} else {
out.println("MSG:" + message);
}
}
public void disconnect() {
if (running.getAndSet(false)) {
out.println("LEAVE");
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
// Ignore errors on close
}
// Wait for the receive thread to finish
try {
if (receiveThread != null) {
receiveThread.join(2000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
// Default to localhost for safe learning environment
String serverHost = "localhost";
int serverPort = 6001;
Scanner scanner = new Scanner(System.in);
// Get server details from command line or user input
if (args.length >= 1) {
serverHost = args[0];
}
if (args.length >= 2) {
try {
serverPort = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("Invalid port number. Using default port 6001.");
}
}
System.out.println("\n=== TCP Chat Client ===");
System.out.println("Default server: " + serverHost + ":" + serverPort);
System.out.println("\nNOTE: For learning, connect to 'localhost' or '127.0.0.1'");
System.out.println(" For network chat, use the server's IP address");
System.out.println(" Example: java TCPChatClient 192.168.1.100 6001\n");
// Get username
System.out.print("Enter your username: ");
String username = scanner.nextLine().trim();
if (username.isEmpty()) {
System.err.println("Username cannot be empty!");
scanner.close();
return;
}
try {
TCPChatClient client = new TCPChatClient(serverHost, serverPort, username);
// Connect to server
client.connect();
// Main message loop
while (client.running.get()) {
String message = scanner.nextLine();
if (message.equalsIgnoreCase("/quit")) {
break;
}
client.sendChatMessage(message);
}
client.disconnect();
} catch (ConnectException e) {
System.err.println("Could not connect: " + e.getMessage());
System.err.println("TIP: Make sure the server is running on " + serverHost + ":" + serverPort);
} catch (UnknownHostException e) {
System.err.println("Unknown host: " + e.getMessage());
System.err.println("TIP: Use 'localhost' for local testing or verify the server address.");
} catch (IOException e) {
System.err.println("Connection error: " + e.getMessage());
System.err.println("TIP: Make sure the server is running on " + serverHost + ":" + serverPort);
} finally {
scanner.close();
}
System.out.println("Chat client closed.");
}
}
This chat application demonstrates several advanced concepts:
Protocol Design: We've created a simple line-based protocol with message types (JOIN, MSG, LEAVE, LIST). Notice there's no PING - TCP's connection awareness makes keep-alives unnecessary here.
Connection = Identity: In the UDP version, the server had to track each client's address and port with every packet. Here, each client simply is its connection - the handler thread holds everything about that user.
Automatic Disconnect Detection: When a client vanishes (crash, network drop, closed terminal), the server's readLine() returns null or throws, and cleanup happens immediately. The UDP version needed a timeout-based cleanup thread for this.
Concurrent Connections: The server dedicates one thread per client, all sharing the client map under synchronization.
Broadcasting: The server writes the message to every connected client's output stream.
TCP File Transfer Example
Let's create one more example - a simple file transfer application using TCP. This is where TCP truly shines: compare this to the UDP version, which needed sequence numbers, ACKs, and retransmission logic. With TCP, we just write the bytes and the protocol guarantees they arrive intact and in order:
import java.net.*;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class TCPFileTransfer {
private static final int BUFFER_SIZE = 8192;
public static class FileSender {
public void sendFile(String filePath, String receiverHost, int receiverPort)
throws IOException, NoSuchAlgorithmException {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
throw new FileNotFoundException("File not found: " + filePath);
}
// Calculate file hash for verification
String fileHash = calculateFileHash(file);
// Connect to the receiver
// For local testing: use "localhost" or "127.0.0.1"
// For network transfer: use the receiver's IP address
try (Socket socket = new Socket(receiverHost, receiverPort)) {
// Inform about connection type
if (socket.getInetAddress().isLoopbackAddress()) {
System.out.println("Sending to localhost - ideal for testing and learning!");
} else {
System.out.println("Sending to remote host: " + receiverHost);
System.out.println("Ensure receiver is listening and firewall allows TCP port " + receiverPort);
}
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
DataInputStream in = new DataInputStream(socket.getInputStream());
// Send file metadata first - DataOutputStream handles the
// encoding, and TCP guarantees it arrives before the data
out.writeUTF(file.getName());
out.writeLong(file.length());
out.writeUTF(fileHash);
System.out.println("Sending file: " + file.getName());
System.out.println("Size: " + file.length() + " bytes");
System.out.println("Destination: " + socket.getInetAddress().getHostAddress() +
":" + receiverPort);
// Stream the file - no chunk headers, no sequence numbers,
// no ACK-per-packet. TCP handles reliability for us.
long totalSent = 0;
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
totalSent += bytesRead;
// Show progress
int progress = file.length() == 0 ? 100
: (int) ((totalSent / (double) file.length()) * 100);
System.out.print("\rProgress: " + progress + "%");
}
}
out.flush();
System.out.println("\nWaiting for receiver confirmation...");
// Read the receiver's verdict - one round trip for the
// whole file, versus one ACK per packet in the UDP version
String result = in.readUTF();
if (result.equals("OK")) {
System.out.println("File sent and verified successfully!");
} else {
System.err.println("Receiver reported a problem: " + result);
}
}
}
private String calculateFileHash(File file) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
}
}
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
public static class FileReceiver {
private ServerSocket serverSocket;
public FileReceiver(int port) throws IOException {
// IMPORTANT: Binding to localhost for security
// This ensures only local senders can transfer files to this receiver
// For network file transfers, modify to:
// this.serverSocket = new ServerSocket(port); // Binds to all interfaces
// or
// this.serverSocket = new ServerSocket(port, 50, InetAddress.getByName("your.ip"));
InetAddress localhost = InetAddress.getByName("127.0.0.1");
this.serverSocket = new ServerSocket(port, 50, localhost);
System.out.println("File receiver listening on " + localhost.getHostAddress() + ":" + port);
System.out.println("NOTE: Only accepting connections from localhost.");
System.out.println("For network transfers, modify code to bind to all interfaces.\n");
}
public void receiveFile(String outputDirectory) throws IOException, NoSuchAlgorithmException {
System.out.println("Waiting for file transfer...");
// Accept one sender connection
try (Socket socket = serverSocket.accept()) {
InetAddress senderAddress = socket.getInetAddress();
DataInputStream in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
// Read file metadata - arrives in the exact order it was sent
String fileName = in.readUTF();
long fileSize = in.readLong();
String expectedHash = in.readUTF();
System.out.println("Receiving file: " + fileName);
System.out.println("Expected size: " + fileSize + " bytes");
System.out.println("From: " + senderAddress.getHostAddress() + ":" + socket.getPort());
if (senderAddress.isLoopbackAddress()) {
System.out.println("Source: Local transfer");
} else {
System.out.println("Source: Network transfer from " + senderAddress.getHostAddress());
}
// Prepare output location
File outputDir = new File(outputDirectory);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
File outputFile = new File(outputDir, new File(fileName).getName());
// Receive the file - just read the stream until we have all
// the bytes. No reordering, no missing-packet bookkeeping.
MessageDigest md = MessageDigest.getInstance("MD5");
long totalReceived = 0;
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[BUFFER_SIZE];
while (totalReceived < fileSize) {
int toRead = (int) Math.min(buffer.length, fileSize - totalReceived);
int bytesRead = in.read(buffer, 0, toRead);
if (bytesRead == -1) {
throw new IOException("Connection closed before transfer completed");
}
fos.write(buffer, 0, bytesRead);
md.update(buffer, 0, bytesRead);
totalReceived += bytesRead;
// Show progress
int progress = fileSize == 0 ? 100
: (int) ((totalReceived / (double) fileSize) * 100);
System.out.print("\rProgress: " + progress + "%");
}
}
System.out.println("\nVerifying file integrity...");
// Verify file integrity
String receivedHash = toHex(md.digest());
if (receivedHash.equals(expectedHash)) {
System.out.println("File saved: " + outputFile.getAbsolutePath());
System.out.println("File integrity verified!");
out.writeUTF("OK");
} else {
System.err.println("File integrity check failed!");
out.writeUTF("HASH_MISMATCH");
}
out.flush();
}
}
private String toHex(byte[] digest) {
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public void close() {
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
// Ignore errors on close
}
}
}
public static void main(String[] args) {
if (args.length < 1) {
printUsage();
return;
}
String mode = args[0].toLowerCase();
try {
if (mode.equals("send")) {
if (args.length < 4) {
printUsage();
return;
}
String filePath = args[1];
String receiverHost = args[2];
int receiverPort = Integer.parseInt(args[3]);
System.out.println("\n=== TCP File Transfer - Sender Mode ===");
System.out.println("For local testing: use 'localhost' or '127.0.0.1' as receiver host");
System.out.println("For network transfer: use receiver's IP address and ensure firewall allows TCP\n");
FileSender sender = new FileSender();
sender.sendFile(filePath, receiverHost, receiverPort);
} else if (mode.equals("receive")) {
if (args.length < 3) {
printUsage();
return;
}
int port = Integer.parseInt(args[1]);
String outputDir = args[2];
System.out.println("\n=== TCP File Transfer - Receiver Mode ===");
System.out.println("Receiver is bound to localhost for security.");
System.out.println("Only local senders can transfer files to this receiver.");
System.out.println("For network transfers, modify the code to bind to all interfaces.\n");
FileReceiver receiver = new FileReceiver(port);
receiver.receiveFile(outputDir);
receiver.close();
} else {
printUsage();
}
} catch (ConnectException e) {
System.err.println("Could not connect: " + e.getMessage());
System.err.println("TIP: Make sure the receiver is running first.");
} catch (BindException e) {
System.err.println("Socket error: " + e.getMessage());
System.err.println("TIP: Port is already in use. Try a different port number.");
} catch (UnknownHostException e) {
System.err.println("Unknown host: " + e.getMessage());
} catch (IOException e) {
System.err.println("IO error: " + e.getMessage());
} catch (NoSuchAlgorithmException e) {
System.err.println("Algorithm error: " + e.getMessage());
} catch (NumberFormatException e) {
System.err.println("Invalid port number: " + e.getMessage());
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
private static void printUsage() {
System.out.println("Usage:");
System.out.println(" Send mode: java TCPFileTransfer send <file_path> <receiver_host> <receiver_port>");
System.out.println(" Receive mode: java TCPFileTransfer receive <listen_port> <output_directory>");
System.out.println();
System.out.println("Example (local transfer - recommended for learning):");
System.out.println(" Terminal 1: java TCPFileTransfer receive 6002 ./received/");
System.out.println(" Terminal 2: java TCPFileTransfer send myfile.pdf localhost 6002");
System.out.println();
System.out.println("Example (network transfer):");
System.out.println(" Receiver: java TCPFileTransfer receive 6002 ./received/");
System.out.println(" Sender: java TCPFileTransfer send myfile.pdf 192.168.1.100 6002");
}
}
This file transfer application demonstrates several important concepts:
Reliability for free: No sequence numbers, no per-packet ACKs, no retransmission logic. The UDP version needed all three; TCP provides them at the transport layer.
Stream framing: DataInputStream/DataOutputStream let us send structured metadata (name, size, hash) followed by raw bytes, and everything arrives in order.
File Integrity: We still verify an MD5 hash end-to-end - TCP protects against network corruption, but a checksum also catches bugs in our own code and disk-level issues.
Backpressure: If the receiver is slow, TCP's flow control automatically slows the sender down. With UDP, a fast sender can simply flood a slow receiver.
Progress Tracking: Both sender and receiver show transfer progress.
Best Practices and Considerations
When working with TCP sockets in Java, keep these important points in mind:
1. Message Boundaries TCP is a byte stream - it does NOT preserve your write boundaries. Three write() calls might arrive as one read, or one write might be split across several reads. Always frame your messages: use line delimiters (as in our chat), length prefixes, or serialization formats like DataOutputStream's primitives.
2. Timeouts A blocked read() can hang forever if the peer dies silently. Use socket.setSoTimeout() for reads and the connect(SocketAddress, timeout) overload for connections so your application can recover.
3. Thread Management A thread per client is fine for learning and small servers, but it doesn't scale to thousands of connections. For high-concurrency servers consider:
Thread pools (ExecutorService) to bound resource usage
Java NIO (Selector, SocketChannel) for non-blocking, event-driven I/O
Virtual threads (Java 21+), which make thread-per-connection scale again
4. Security Considerations TCP has no built-in security. Consider:
Using SSLSocket/SSLServerSocket (TLS) to encrypt traffic
Implementing authentication mechanisms
Validating all incoming data to prevent injection attacks
5. Resource Management
Always close sockets when done to free system resources - each open connection holds a file descriptor
Use try-with-resources or finally blocks to ensure cleanup
Close streams and sockets on both ends; lingering half-open connections waste resources
6. Performance Tuning
setTcpNoDelay(true) disables Nagle's algorithm for latency-sensitive traffic (at the cost of more small packets)
Buffer your streams (BufferedInputStream/BufferedOutputStream) to avoid one system call per byte
Tune the ServerSocket backlog if you expect connection bursts
7. Testing and Debugging
Test with realistic network conditions (latency, disconnects mid-transfer)
Use tools like Wireshark to inspect TCP traffic and watch the handshake in action
netstat/lsof help you spot leaked connections and ports stuck in TIME_WAIT
TCP socket programming in Java provides a solid, batteries-included foundation for building reliable networked applications. The protocol does the hard work - ordering, retransmission, flow control - so your code can focus on the application logic.
The examples we've covered - from basic echo servers to chat applications and file transfer systems - demonstrate how much simpler reliable communication becomes when the transport layer guarantees delivery. Compare the TCP file transfer to its UDP counterpart: the UDP version had to reimplement a good chunk of what TCP gives you out of the box.
Remember that the choice between TCP and UDP isn't about which is "better" - it's about which fits your specific requirements. TCP shines whenever correctness and completeness of data matter more than shaving off milliseconds: APIs, file transfers, chat, databases, and virtually everything built on HTTP.
May your connections always complete their handshake!