-
Notifications
You must be signed in to change notification settings - Fork 109
Using Web Socket to Do Real‐Time Progress Report
The More Effortless, Easier and Natural Way Than HTTP Request - MySqlBackup.NET Progress Report Series
Web Socket establishes a single constant connection between the frontend and backend server. This allows a REAL-TIME and BI-DIRECTION communication. Both sides can initiate the message sending to the other side. This type of data transmission has lower lantency, cleaner and effortless for data exchanges. It works very well for this kind of constant interval communication between frontend and backend. Now, backend server (API) can pro-actively send the progress status updates to the frontend without just passively waiting for the frontend to do http request to get the updates.
We're using Vanilla ASP.NET Web Forms (Zero ViewState/Server Control/UpdatePanel) to demonstrate the idea, however, it's logic is migratable to MVC, .NET Core.
Let's begin by first exploring the basics of Web Socket.
It is always the frontend initiates the web socket communication with the backend.
Assume you have created a web forms page (more on this later) to serve as the api endpoint as follow:
http://mywebsite.com/apiBackup
or (SSL secure connection)
https://mywebsite.com/apiBackup
or (localhost development environment)
http://localhost:51283/apiBackup
Then URL and protocol for the web socket will look like this:
ws://mywebsite.com/apiBackup
or (SSL secure connection)
wss://mywebsite.com/apiBackup
or (localhost development environment)
ws://localhost:51283/apiBackup
Establishing Web Socket connection:
let wsUrl = "wss://mywebsite.com/apiBackup";
// Create a web socket and establish connection
webSocket = new WebSocket(wsUrl);
// Sending a message to the backend api server
// Can be sent anytime while the connection is still alive
webSocket.send("Hello Backend!");
// Close the Web Socket connection, initiates by the frontend
webSocket.close();
// Events
// This happens when the backend accepted the connection
webSocket.onopen = function () {
...
};
// This happens when the backend server sends message to the frontend
webSocket.onmessage = function (event) {
// Capture the message
let msg = event.data;
console.log(msg);
// Example of output:
// "Hi! Frontend!"
...
};
// Backend closed the connection
webSocket.onclose = function (event) {
...
};
// There is an error in connecting the web socket
webSocket.onerror = function (error) {
...
};
So, from the frontend perspective, that's the web socket in action in a nutshell.
Now, let's move to the C# backend to see how the server handles web socket.
Create a blank web forms, which again, looks like this:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
Deletes all frontend markup, leave only the first line, the page directive declaration:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>
Switch to code behind:
namespace myweb
{
public partial class apiBackup : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
}
Normally, we'll capture the http request as follow:
public partial class apiBackup : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// For Key-Value Pairs
// Either Form Post or Query String
string content1 = Request["key1"] + "";
string content2 = Request["key2"] + "";
string content3 = Request["key3"] + "";
// or for JSON, XML or CSV
string content = "";
using (var reader = new StreamReader(Request.InputStream))
{
content = reader.ReadToEnd();
}
var classObject = JsonSerializer.Deserialize<ClassObject>(content)
}
}
To identify the request is a Web Socket request:
public partial class apiBackup : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Context.IsWebSocketRequest)
{
// handle web socket request
}
else
{
// http request
string content1 = Request["key1"] + "";
string content2 = Request["key2"] + "";
string content3 = Request["key3"] + "";
// or for JSON, XML or CSV
string content = "";
using (var reader = new StreamReader(Request.InputStream))
{
content = reader.ReadToEnd();
}
var classObject = JsonSerializer.Deserialize<ClassObject>(content)
}
}
}
Handling Web Socket Request:
protected void Page_Load(object sender, EventArgs e)
{
if (Context.IsWebSocketRequest)
{
Context.AcceptWebSocketRequest(HandleWebSocketProgressOnly);
}
else
{
// http request
// form post, query string, json, xml, csv, binary, etc...
}
}
private async Task HandleWebSocketProgressOnly(AspNetWebSocketContext httpContext)
{
// get the real web socket instance
WebSocket webSocket = httpContext.WebSocket;
if (!IsUserAuthenticated())
{
await webSocket.CloseAsync(WebSocketCloseStatus.PolicyViolation, "Authentication failed", CancellationToken.None);
return;
}
// ------------------------------
// Sending message to the frontend
// ------------------------------
string text = "Hello, Frontend!";
byte[] textBytes = Encoding.UTF8.GetBytes(text);
await webSocket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
// ------------------------------
// Close the connection, initiates by backend server
// ------------------------------
// Initiates the closing
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
// ------------------------------
// Receiving incoming message
// ------------------------------
byte[] buffer = new byte[1024];
// keep listening for messages until connection is closed
while (webSocket.State == WebSocketState.Open)
{
try
{
string incomingMessage = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
// Text, JSON Formatted String, XML Formatted String, CSV Formatted String....
if (incomingMessage.MessageType == WebSocketMessageType.Text)
{
string incomingText = Encoding.UTF8.GetString(buffer, 0, incomingMessage.Count);
// example output: "Hello Backup!"
}
else if (incomingMessage.MessageType == WebSocketMessageType.Close)
{
// frontend is closing, respond with close acknowledgment
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
// exit the loop
break;
}
}
catch (WebSocketException ex)
{
// Handle WebSocket exceptions
Console.WriteLine($"WebSocket error: {ex.Message}");
break;
}
}
}
bool IsUserAuthenticated()
{
// User authentication logic here
// Check if user is logged in and has backup permissions
if (Session["user_login"] == null)
{
return false;
}
// TEMPORARY - for testing and debugging use
return true;
}
In order for another thread to use the Web Socket to send message, we can cache the Web Socket with a ConcurrentDictionary
.
// declare a global caching dictionary
static ConcurrentDictionary<int, WebSocket> dicWebSocket = new ConcurrentDictionary<int, WebSocket>();
private async Task HandleWebSocketProgressOnly(AspNetWebSocketContext httpContext)
{
// get the real web socket instance
WebSocket webSocket = httpContext.WebSocket;
int taskId = GetNewTaskId();
// save the web socket to the global dictionary
dicWebSocket[taskId] = webSocket
...
}
Accessing the Web Socket from another thread:
int taskId = 1;
// get the Web Socket from the global dictionary
var webSocket = dicWebSocket[taskId];
// send the message
string text = "Hello, Frontend!";
byte[] textBytes = Encoding.UTF8.GetBytes(text);
await webSocket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
That is the basics of handling Web Socket.
At the backend, the backup and restore of MySql database task will still be initiated by normal HTTP request, only the progress reporting section will be handled through Web Socket.
Let's perform a complete life cycle of backup process:
Basic HTML Structure:
<div>
Percentage Complete: <span id="labelPercent"></span>
</div>
<div>
Task Status: <span id="labelStatus"></span>
</div>
<button type="button" onclick="startBackup();">Backup</button>
<button type="button" onclick="stopTask();">Stop</button>
JavaScript:
let urlApiEndpoint = "/apiBackup";
// the UI container for Percentage Completed
let labelPercent = document.querySelector("#labelPercent");
let labelStatus = document.querySelector("#labelStatus");
let currentTaskId = 0;
let webSocket = null;
let isConnecting = false;
async function startBackup() {
const formData = new FormData();
formData.append('action', 'start_backup');
// send a http request to start the backup
const response = await fetch(urlApiEndpoint, {
credentials: 'include',
body: formData
});
if (response.ok) {
// server respond successfully
// indicates backup progress run successfully
// get the json
let jsonObject = await response.json();
// get the task id
currentTaskId = jsonObject.TaskId;
// start the web socket progress status monitoring here
connectWebSocket();
}
else {
// get the error message
let errMsg = respond.text();
}
}
async function stopTask() {
const formData = new FormData();
formData.append('action', 'stop_task');
// send a http request to start the backup
const response = await fetch(urlApiEndpoint, {
credentials: 'include',
body: formData
});
if (response.ok) {
// successfully stopped
// do nothing, let the server sends the last update
}
else {
// get the error message
let errMsg = respond.text();
}
}
Establishing Web Socket
function connectWebSocket(taskId) {
// prevention of overwriting current connection
if (isConnecting || (webSocket && webSocket.readyState === WebSocket.OPEN)) {
return;
}
// set a flag to indicate that a connection attempt is underway
isConnecting = true;
// determine Web Socket URL (ws:// or wss://)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/${urlApiEndpoint}`;
try {
// create and open connection
webSocket = new WebSocket(wsUrl);
// ----------------------------
// Web Socket's events
// ----------------------------
// webSocket.onopen // connection success
// webSocket.onmessage // server send message
// webSocket.onclose // connection closing
// webSocket.onerror // connection error
// handling events
// successfully connected
webSocket.onopen = function () {
isConnecting = false;
// tell the server that in this connection
// we are interested to get progress status for this task id
webSocket.send(`taskid:${currentTaskId}`);
};
// receiving message from the backend
webSocket.onmessage = function (event) {
try {
// convert the text into json
const data = JSON.parse(event.data);
// update the values to the UI
labelPercent.textContent = data.PercentCompleted + "%";
labelStatus.textContent = data.TaskStatus;
// close WebSocket when task is completed
if (data.IsCompleted) {
// frontend has received the task completion notice
// tell the backend that the connection can be closed for now
webSocket.close(1000, "Task completed");
}
} catch (err) {
console.error('Error parsing WebSocket message:', err);
}
};
webSocket.onclose = function (event) {
isConnecting = false;
webSocket = null;
};
webSocket.onerror = function (error) {
console.error('WebSocket error:', error);
isConnecting = false;
};
} catch (err) {
console.error('Failed to create WebSocket:', err);
isConnecting = false;
}
}
First, write a class object to store the progress status info:
class TaskInfo
{
[JsonIgnore]
public WebSocket TaskWebSocket { get; set; } = null;
public int TaskId { get; set; }
public int TaskType { get; set; } // 1 = Backup, 2 = Restore
public int PercentCompleted { get; set; } = 0;
public bool IsCompleted { get; set; } = false;
public bool IsCancelled { get; set; } = false;
public bool RequestCancel { get; set; } = false;
public bool HasError { get; set; } = false;
public string ErrorMsg { get; set; } = "";
public string TaskStatus
{
get
{
if (IsCancelled)
return "Task Cancelled";
if (HasError)
return "Error";
if (IsCompleted)
return "Completed";
return "Running";
}
}
}
Handle the C# logic:
public partial class apiBackup : System.Web.UI.Page
{
static ConcurrentDictionary<int, TaskInfo> dicTaskInfo = new ConcurrentDictionary<int, TaskInfo>();
protected void Page_Load(object sender, EventArgs e)
{
if (Context.IsWebSocketRequest)
{
Context.AcceptWebSocketRequest(HandleWebSocketProgressOnly);
}
else
{
// Serves as API Endpoint
try
{
string action = (Request["action"] + "").Trim().ToLower();
switch (action)
{
case "start_backup":
StartBackup();
break;
case "start_restore":
StartRestore();
break;
case "stop_task":
Stop();
break;
default:
// HTTP Error 400 Bad Request
Response.StatusCode = 400;
Response.Write("Empty or unsupported request");
break;
}
}
catch (Exception ex)
{
// HTTP Error 500 Internal Server Error
Response.StatusCode = 500;
Response.Write(ex.Message);
}
}
}
}
Starting the backup process:
void StartBackup()
{
try
{
if (!IsUserAuthenticated())
{
// HTTP Error 401 Unauthorized
Response.StatusCode = 401;
Response.Write("Unauthorized");
return;
}
int newTaskId = GetNewTaskId();
TaskInfo taskInfo = new TaskInfo();
taskInfo.TaskId = newTaskId;
taskInfo.TaskWebSocket = null;
// save the task info to the global dictionary
dicTaskInfo[newTaskId] = taskInfo;
// start process in another separated thread
_ = Task.Run(() => BackupBegin(newTaskId));
// return task ID immediately to the frontend
// creating an anonymouse object as temporary class to hold data
var result = new { TaskId = newTaskId, Status = "Task started" };
// send the result as JSON to the frontend
Response.ContentType = "application/json";
Response.Write(JsonSerializer.Serialize(result));
}
catch (Exception ex)
{
// HTTP Error 500 Internal Server Error
Response.StatusCode = 500;
Response.Write(ex.Message);
}
}
void BackupBegin(int taskId)
{
if (dicTaskInfo.TryGetValue(taskId, out var taskInfo))
{
try
{
string sqlFile = Server.MapPath("~/backup.sql");
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// report the progress 5 times per second
mb.ExportInfo.IntervalForProgressReport = 200;
// pass the task id to the event subscriber
mb.ExportProgressChanged += (sender, e) => Mb_ExportProgressChanged(sender, e, taskId);
// begin the task
mb.ExportToFile(sqlFile);
}
// passing this line, indicates the task is stopped
if (taskInfo.RequestCancel)
{
// task is cancelled by the request of user
taskInfo.IsCancelled = true;
// clean up incomplete file
try
{
File.Delete(sqlFile);
}
catch { }
}
else
{
// task is fully completed
}
}
catch (Exception ex)
{
taskInfo.HasError = true;
taskInfo.ErrorMsg = ex.Message;
}
// marks the task is completed
taskInfo.IsCompleted = true;
// sends the final update to the frontend
string json = JsonSerializer.Serialize(taskInfo);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
await socket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e, int thisTaskId)
{
// get the task info from the global dictionary
if (dicTaskInfo.TryGetValue(thisTaskId, out var taskInfo))
{
taskInfo.Percentage = CalculatePercent(e.TotalRowsInAllTables, e.CurrentRowIndexInAllTables);
if (taskInfo.RequestCancel)
{
((MySqlBackup)sender).StopAllProcess();
}
// check if Web Socket is connected or captured
if (taskInfo.TaskWebSocket?.State == WebSocketState.Open)
{
// send the progress status to the frontend
string json = JsonSerializer.Serialize(taskInfo);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
await socket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
User request task to be cancelled/stopped:
void Stop()
{
// get the task id from the http request
if (int.TryParse(Request["taskid"] + "", out int taskid))
{
// get the task info from the global dictionary
if (dicTaskInfo.TryGetValue(taskid, out TaskInfo taskInfo))
{
// set the flag to tell the main process to stop
taskInfo.RequestCancel = true;
// done. default response = ok/success
// nothing to modify
return;
}
}
// set the response status as error
// HTTP Error 500 Internal Server Error
Response.StatusCode = 500;
Response.Write(ex.Message);
}
This completes the cycle of a basic backup process.
For the Restore process, everything will be same as the Backup process, the only difference is there is an extra step to upload the SQL backup file during the first http request initialization.
JavaScript for "Restore" process and uploading the SQL file:
Assume that we have a file upload input:
<button type="button" onclick="startRestore();">Restore</button>
<input type="file" id="fileRestore" />
The JavaScript for Restore:
let fileRestore = document.querySelector("#fileRestore");
async function startRestore() {
const formData = new FormData();
formData.append('action', 'start_restore');
formData.append('file', fileRestore.files[0]);
// everything else below is just same as the backup
// send a http request to start the backup
const response = await fetch(urlApiEndpoint, {
credentials: 'include',
body: formData
});
if (response.ok) {
// server respond successfully
// indicates progress run successfully
// get the json
let jsonObject = await response.json();
// get the task id
currentTaskId = jsonObject.TaskId;
// start the web socket progress status monitoring here
connectWebSocket();
}
else {
// get the error message
let errMsg = respond.text();
}
}
The C# Backend Logic for Handling Restore and File Upload:
void StartRestore()
{
try
{
if (!IsUserAuthenticated())
{
// HTTP Error 401 Unauthorized
Response.StatusCode = 401;
Response.Write("Unauthorized");
return;
}
// no file uploaded
if (Request.Files.Count == 0 || Request.Files[0].ContentLength == 0)
{
// HTTP Error 400 Bad Request
Response.StatusCode = 400;
Response.Write("No file uploaded");
return;
}
var file = Request.Files[0];
int newTaskId = GetNewTaskId();
TaskInfo taskInfo = new TaskInfo();
taskInfo.TaskId = newTaskId;
taskInfo.TaskWebSocket = null;
// save the task info to the global distionary
dicTaskInfo[newTaskId] = taskInfo;
// save and process uploaded file
string sqlFile = Server.MapPath("~/backup.sql");
file.SaveAs(sqlFile);
// start process in another separated thread
_ = Task.Run(() => RestoreBegin(newTaskId, sqlFilePath));
// creating an anonymouse object as temporary class to hold data
var result = new { TaskId = newTaskId, Status = "Task started" };
// send the result as JSON to the frontend
Response.ContentType = "application/json";
Response.Write(JsonSerializer.Serialize(result));
}
catch (Exception ex)
{
// HTTP Error 500 Internal Server Error
Response.StatusCode = 500;
Response.Write(ex.Message);
}
}
The rest of the code is almost same as the backup process:
void RestoreBegin(int thisTaskId, string filePathSql)
{
if (dicTaskInfo.TryGetValue(thisTaskId, out TaskInfo taskInfo))
{
try
{
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// report progress 5 times per second
mb.ImportInfo.IntervalForProgressReport = 200;
// send the task id to the event subscriber
mb.ImportProgressChanged += (sender, e) => Mb_ImportProgressChanged(sender, e, thisTaskId);
mb.ImportFromFile(filePathSql);
}
// passing this line, indicates the task is stopped
if (taskInfo.RequestCancel)
{
// task is cancelled by the request of user
taskInfo.IsCancelled = true;
}
else
{
// task is fully completed
}
}
catch (Exception ex)
{
taskInfo.HasError = true;
taskInfo.ErrorMsg = ex.Message;
}
// marks the task is completed
taskInfo.IsCompleted = true;
// sends the final update to the frontend
string json = JsonSerializer.Serialize(taskInfo);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
await socket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e, int thisTaskId)
{
// get the task info from the global dictionary
if (dicTaskInfo.TryGetValue(thisTaskId, out var taskInfo))
{
taskInfo.Percentage = CalculatePercent(e.TotalBytes, e.CurrentBytes);
if (taskInfo.RequestCancel)
{
((MySqlBackup)sender).StopAllProcess();
}
// check if Web Socket is connected or captured
if (taskInfo.TaskWebSocket?.State == WebSocketState.Open)
{
// send the progress status to the frontend
string json = JsonSerializer.Serialize(taskInfo);
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
await socket.SendAsync(new ArraySegment<byte>(jsonBytes), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
For full complete implementation that covers all progress values update and error handling, you may visit the source code at:
Frontend:
Backend:
- MySqlBackup.NET - C# Open Source MySQL Backup & Restore tool
- mdn web docs - (Frontend JS) The WebSocket API (WebSockets)
- mdn web docs - (Backend Server) The WebSocket API (WebSockets)
- mdn web docs - HTTP response status codes