# Socket Programming in Java: Understanding TCP Communication

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:

```java
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:

1.  **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.
    
2.  **Blocking accept()**: The `accept()` method blocks until a client connects. This is why we run it in a loop.
    
3.  **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.
    
4.  **Connection awareness**: `readLine()` returning `null` tells us the client has disconnected. With UDP we'd need our own keep-alive mechanism to detect this.
    
5.  **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:

```java
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:

1.  **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.
    
2.  **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.
    
3.  **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.
    
4.  **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:

```java
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.");
        }
    }
}
```

```java
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:

1.  **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.
    
2.  **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.
    
3.  **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.
    
4.  **Concurrent Connections**: The server dedicates one thread per client, all sharing the client map under synchronization.
    
5.  **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:

```java
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:

1.  **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.
    
2.  **Stream framing**: `DataInputStream`/`DataOutputStream` let us send structured metadata (name, size, hash) followed by raw bytes, and everything arrives in order.
    
3.  **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.
    
4.  **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.
    
5.  **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!
