DEV Community

Cover image for # File Transfer Over TCP: A Practical Guide for Developers
sudip khatiwada
sudip khatiwada

Posted on

# File Transfer Over TCP: A Practical Guide for Developers

Introduction

Every time you download a file, stream a video, or sync data between devices, reliable file transfer is happening behind the scenes. At the heart of this reliability is TCP (Transmission Control Protocol), the network protocol that ensures your data arrives complete and in order.

For developers, understanding how to implement TCP file transfer isn't just academic—it's foundational knowledge that powers everything from HTTP to custom real-time applications. In this guide, we'll build a practical TCP file transfer system using Node.js, exploring how sockets and streams work together to move data reliably across networks.

By the end, you'll have working code for both a TCP server and client that can transfer files seamlessly.

TCP & Sockets, Simplified

Think of TCP as a reliable courier service for your data. Unlike UDP (which is like throwing postcards over a fence and hoping they arrive), TCP guarantees delivery, maintains order, and handles retransmissions if packets get lost.

Sockets are the communication endpoints—imagine them as dedicated phone lines between two applications. When your client socket connects to a server socket, they establish a reliable channel for bidirectional data flow.

Key concept: In Node.js, the net module provides this socket programming capability, letting you create TCP servers and clients without dealing with low-level network details.

Building the TCP Server

Here's how we build a TCP server that receives files from clients:

import { createWriteStream } from "node:fs";
import net from "node:net";

const clientsList = [];

const server = net.createServer((socket) => {
  clientsList.push(socket);

  const writeStream = createWriteStream("OpsDev.png");
  socket.pipe(writeStream);

  socket.on("close", () => {
    console.log(socket.remoteAddress, ": Client disconnected");
  });

  socket.on("error", () => {
    console.log("Client Lost");
  });

  console.log("Client Connected", socket.remoteAddress);
});

server.listen(8080, "0.0.0.0", () => {
  console.log("Server started on port 8080");
});
Enter fullscreen mode Exit fullscreen mode

Breaking it down:

  • net.createServer(callback): Creates a TCP server. The callback executes whenever a client connects, giving you a socket object for that connection.

  • clientsList array: Tracks all connected clients. This is useful for multi-client scenarios where you might want to broadcast messages or manage connections.

  • socket.pipe(writeStream): This is the magic line. It pipes incoming data packets from the network socket directly into a file write stream. Node.js streams handle buffering and backpressure automatically—no manual chunking needed.

  • Event listeners:

    • 'close' fires when the client disconnects gracefully
    • 'error' handles connection issues like network failures
  • server.listen(8080, '0.0.0.0'): Binds the server to port 8080 on all network interfaces, making it accessible from other machines.

The flow: Client connects → Server creates socket → Incoming data automatically writes to OpsDev.png → Connection closes when transfer completes.

Building the TCP Client

Now let's build the client that sends files:

import { createReadStream, createWriteStream } from "node:fs";
import net from "node:net";

process.stdin.on("data", (input) => {
  const inputString = input.toString().trim();

  if (inputString === "send") {
    const readStream = createReadStream("/Users/sudipos/Desktop/DevOps.png");

    readStream.pipe(socket);

    readStream.on("end", () => {
      console.log("File ended");
    });
  }
});

const socket = net.createConnection({ host: "172.168.29.148", port: 8080 });

const writeStream = createWriteStream("/Users/sudipos/Desktop/OpsDev.png");

socket.on("error", () => {
  console.log("Server Lost");
});
Enter fullscreen mode Exit fullscreen mode

Breaking it down:

  • net.createConnection(): Establishes a TCP socket connection to the server at the specified IP address and port. This is your client's dedicated channel.

  • process.stdin.on('data'): Listens for terminal input. When you type "send" and hit enter, it triggers the file transfer. This gives you manual control over when to send files.

  • createReadStream(filePath): Opens a readable stream from your source file. Streams read data in chunks, keeping memory usage low even for large files.

  • readStream.pipe(socket): Pipes file data directly into the TCP socket. Each chunk read from the file is automatically sent as TCP packets to the server.

  • readStream.on('end'): Fires when the entire file has been read and sent, confirming the transfer is complete.

The flow: Client connects → You type "send" → File reads in chunks → Data streams through socket → Server receives and writes to disk.

How It All Fits Together

Here's the complete data journey for TCP file transfer:

  1. Client Side: createReadStream reads file bytes → pipe() sends chunks to socket → TCP layer breaks data into packets
  2. Network Layer: TCP handles packet transmission, acknowledgments, retransmissions, and ordering
  3. Server Side: TCP reassembles packets → Socket receives byte stream → pipe() writes chunks to createWriteStream → File saved to disk

Visual flow:

[Source File] → [Read Stream] → [Client Socket] 
                                      ↓
                                [TCP Network]
                                      ↓
[Destination File] ← [Write Stream] ← [Server Socket]
Enter fullscreen mode Exit fullscreen mode

The beauty of Node.js streams is that they handle backpressure automatically. If the server can't write to disk fast enough, the client's read stream pauses until the server catches up—no data loss.

Key Considerations for Developers

Error handling: Production code needs robust error handling. What if the file doesn't exist? What if the network drops mid-transfer? Always wrap operations in try-catch blocks and handle stream errors.

Security: This plain TCP implementation sends data unencrypted. Anyone on the network can intercept packets. For production, use TLS/SSL (via Node's tls module) or higher-level protocols like HTTPS or SFTP.

File integrity: Consider adding checksums (MD5, SHA-256) to verify the received file matches the original. TCP prevents corruption, but application-level validation adds another safety layer.

Scalability: The clientsList array grows with connections. For high-traffic servers, implement connection pooling and cleanup logic to prevent memory leaks.

Protocol evolution: This foundation is how protocols like HTTP, FTP, and WebSocket work under the hood. HTTP, for instance, is just structured text messages over TCP. Understanding raw sockets helps you debug and optimize higher-level protocols.

Conclusion

Transferring files over TCP might seem complex, but as you've seen, Node.js makes it remarkably straightforward with the net module and streams. You now have a working client-server system that reliably moves data across networks—the same fundamental approach used by applications you use daily.

Next steps: Experiment with the code. Try transferring different file types. Add progress indicators. Implement resume functionality for interrupted transfers. Each enhancement will deepen your understanding of network programming and streaming architectures.

The best way to master TCP and socket programming is to build, break, and rebuild. Start with these examples, and you'll soon be architecting robust, real-time networked applications.

Top comments (0)