How to Build a Remote Desktop control App in C#: A Complete TCP Client-Server Tutorial

C# Tutorials
Remote screening
tcp block diagram

Introduction

Have you ever wanted to control another computer remotely—whether for IT support, collaboration, or accessing your home PC? In this step-by-step guide, you’ll learn how to build a fully functional remote desktop application using C# and TCP networking—one of the most reliable protocols for real-time communication.

Understanding TCP Client-Server Communication

TCP (Transmission Control Protocol) is a connection-oriented protocol that ensures:
✔ Reliable Data Delivery – No packets are lost (unlike UDP)
✔ Ordered Data Transmission – Data arrives in the correct sequence
✔ Error Checking – Automatic retransmission if corruption occurs

In our application:

  • The Host (Server) listens for incoming connections and streams its screen.
  • The Client (Controller) connects to the host and sends input commands (mouse/keyboard).

🚀 By the end of this tutorial, you’ll have:
✔ A TCP server (host) that streams its screen
✔ A TCP client (controller) that sends mouse/keyboard inputs
✔ Real-time screen sharing with JPEG compression
✔ Remote control capabilities (mouse movements, clicks, keystrokes)

Perfect For:

  • IT Support – Fix issues on remote PCs
  • Home Automation – Control a media center PC
  • Educational Projects – Learn networking & C#

🔧 Prerequisites

Before we start, ensure you have:

  • Visual Studio 2022 (or newer)
  • Basic C# & .NET knowledge
  • Two PCs (or VMs) for testing

Part 1: Building the Host (Server) Application

The host application runs on the PC being controlled. It:

  • Listens for incoming connections
  • Captures and sends screenshots
  • Accepts remote input commands

Key Code Snippets

1. Starting the TCP Server

The TCP server is being initialized and started. The method first sets a specific port number (5000) where the server will listen for incoming client connections.

C#
private void StartServer()
{
    int port = 5000;
    server = new TcpListener(IPAddress.Any, port);//ip address
    server.Start();
    isListening = true;

    listenerThread = new Thread(ListenForClients);
    listenerThread.IsBackground = true;
    listenerThread.Start();

    Log($"[Host] Server started on port {port}");
}

2. Handling Client Authentication

The server continuously listens for incoming client connections. When a client connects, it reads an authentication message (preceded by its length), and verifies whether the client sends a correct handshake message (HELLO_HOST). Depending on the message, it responds with either ACCESS_GRANTED or ACCESS_DENIED. If authenticated, it starts a background thread to handle further data (like receiving screenshots); otherwise, it closes the connection.

C#
 while (isListening)
 {
     try
     {
         client = server.AcceptTcpClient();
         Log("[Host] Client connected.");

         stream = client.GetStream();

         // Read auth message length
         byte[] lengthBuffer = new byte[4];
         int lenRead = stream.Read(lengthBuffer, 0, 4);
         if (lenRead != 4)
         {
             client.Close();
             continue;
         }
         int authLength = BitConverter.ToInt32(lengthBuffer, 0);

         // Read auth message
         byte[] authBuffer = new byte[authLength];
         int totalRead = 0;
         while (totalRead < authLength)
         {
             int read = stream.Read(authBuffer, totalRead, authLength - totalRead);
             if (read == 0) break;
             totalRead += read;
         }
         string clientMessage = Encoding.UTF8.GetString(authBuffer);
         Log($"[Host] Received: {clientMessage}");

         string response = clientMessage == "HELLO_HOST" ? "ACCESS_GRANTED" : "ACCESS_DENIED";

         // Send response length + response
         byte[] respBytes = Encoding.UTF8.GetBytes(response);
         byte[] respLen = BitConverter.GetBytes(respBytes.Length);
         stream.Write(respLen, 0, 4);
         stream.Write(respBytes, 0, respBytes.Length);
         Log($"[Host] Sent: {response}");

         if (response == "ACCESS_GRANTED")
         {
             // Start receiving screenshots & handle input
             receiveThread = new Thread(ReceiveDataLoop);
             receiveThread.IsBackground = true;
             receiveThread.Start();
         }
         else
         {
             client.Close();
         }
     }
     catch (Exception ex)
     {
         Log("[Error] " + ex.Message);
     }
 }

3. Picturebox update

Method safely updates a PictureBox control (pictureBox1) with a new image. If the update is being called from a non-UI thread, it uses Invoke to marshal the update to the UI thread. Once on the correct thread, it disposes of the previous image (to free resources) and sets the new image to display in the PictureBox.

C#
 private void UpdatePictureBox(Image img)
 {
     if (pictureBox1.InvokeRequired)
     {
         pictureBox1.Invoke(new Action<Image>(UpdatePictureBox), img);
     }
     else
     {
         pictureBox1.Image?.Dispose();
         pictureBox1.Image = new Bitmap(img);
     }
 }

Part 2: Building the Client (Controller) Application

The client connects to the host and:

  • Receives live screen updates
  • Sends mouse/keyboard inputs

Key Code Snippets

1. Connecting to the Host

when the button is clicked, the client attempts to connect to a host server using a TCP connection on port 5000. It sends an authentication message (HELLO_HOST) with a length prefix and waits for the server’s response. Based on the server’s reply (ACCESS_GRANTED or ACCESS_DENIED), it either starts background threads for screen sharing and input handling or shows a failure message and cleans up the connection.

C#
  private void button1_Click(object sender, EventArgs e)
  {
      ConnectToHost();
  }

  private void ConnectToHost()
  {
      string hostIp = "***.***.***.***"; // 🔧 Replace with Host IP
      int port = 5000;

      try
      {
          client = new TcpClient();
          client.Connect(hostIp, port);
          stream = client.GetStream();

          // Send AUTH message with length prefix
          string authMsg = "HELLO_HOST";
          byte[] authBytes = Encoding.UTF8.GetBytes(authMsg);
          byte[] authLen = BitConverter.GetBytes(authBytes.Length);
          stream.Write(authLen, 0, 4);
          stream.Write(authBytes, 0, authBytes.Length);

          // Read response length and response
          byte[] respLenBuf = new byte[4];
          int read = stream.Read(respLenBuf, 0, 4);
          if (read != 4)
          {
              Log("Failed to read response length");
              return;
          }

          int respLen = BitConverter.ToInt32(respLenBuf, 0);
          byte[] respBuf = new byte[respLen];
          int totalRead = 0;
          while (totalRead < respLen)
          {
              int r = stream.Read(respBuf, totalRead, respLen - totalRead);
              if (r == 0) break;
              totalRead += r;
          }
          string response = Encoding.UTF8.GetString(respBuf);
          Log("[Client] Server Response: " + response);

          if (response == "ACCESS_GRANTED")
          {
              MessageBox.Show("Access Granted by Host!", "Connection Successful", MessageBoxButtons.OK, MessageBoxIcon.Information);

              running = true;
              sendThread = new Thread(SendScreenLoop);
              sendThread.IsBackground = true;
              sendThread.Start();

              receiveThread = new Thread(ReceiveInputCommands);
              receiveThread.IsBackground = true;
              receiveThread.Start();
          }
          else
          {
              MessageBox.Show("Access Denied by Host.", "Connection Failed", MessageBoxButtons.OK, MessageBoxIcon.Warning);
              Cleanup();
          }
      }
      catch (Exception ex)
      {
          Log("[Client] Error: " + ex.Message);
          MessageBox.Show("Connection Error:\n" + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
          Cleanup();
      }
  }

2. Receiving & Displaying Screen Updates

The client continuously captures screenshots of the primary display, compresses them to JPEG format, and sends them to the host over the network connection. Each image is sent with a length prefix to ensure proper transmission. This loop runs in the background while the connection is active, with a slight delay to control the frame rate (approx. every 200ms).

C#
 private void SendScreenLoop()
 {
     try
     {
         while (running && client.Connected)
         {
             using (Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height))
             {
                 using (Graphics g = Graphics.FromImage(bmp))
                 {
                     g.CopyFromScreen(0, 0, 0, 0, bmp.Size);
                 }

                 using (MemoryStream ms = new MemoryStream())
                 {
                     bmp.Save(ms, ImageFormat.Jpeg); // compress screen image to jpeg
                     byte[] imgBytes = ms.ToArray();

                     // Send image length prefix
                     byte[] lenBytes = BitConverter.GetBytes(imgBytes.Length);
                     stream.Write(lenBytes, 0, 4);

                     // Send image bytes
                     stream.Write(imgBytes, 0, imgBytes.Length);
                     stream.Flush();
                 }
             }
             Thread.Sleep(200); // Adjust for desired FPS
         }
     }
     catch (Exception ex)
     {
         Log("[SendScreenLoop] Error: " + ex.Message);
         running = false;
     }
 }

3. Sending Mouse/Keyboard Commands

The client continuously listens for incoming input commands from the host. Each command is received with a length prefix to ensure the complete message is read correctly. Once a full command is received, it is decoded and passed to a method that processes the input (like mouse or keyboard events). The loop runs while the connection is active and cleans up on error or disconnection.

C#
 private void ReceiveInputCommands()
 {
     try
     {
         while (running && client.Connected)
         {
             // Read length of incoming command
             byte[] lenBuf = new byte[4];
             int read = stream.Read(lenBuf, 0, 4);
             if (read != 4) break;

             int cmdLen = BitConverter.ToInt32(lenBuf, 0);
             if (cmdLen <= 0) break;

             byte[] cmdBuf = new byte[cmdLen];
             int totalRead = 0;
             while (totalRead < cmdLen)
             {
                 int r = stream.Read(cmdBuf, totalRead, cmdLen - totalRead);
                 if (r == 0) break;
                 totalRead += r;
             }
             string command = Encoding.UTF8.GetString(cmdBuf);

             ProcessInputCommand(command);
         }
     }
     catch (Exception ex)
     {
         Log("[ReceiveInputCommands] Error: " + ex.Message);
         running = false;
     }
     finally
     {
         Cleanup();
     }
 }

3. Download the source code

Click here to download the source code

4. DEMO video

Also Read | How to open PDF/PPT File in Visual c# app

Denesh Neupane

Denesh is a tech enthusiast who enjoys sharing ideas and creative digital solutions. With a passion for turning concepts into real projects, he loves exploring new technologies.

Read More from Denesh Neupane →

Discussion (0)

Share Your Thoughts

Your email address will not be published. Required fields are marked *