WebSocket實現簡易的FTP客戶端
- 2021 年 11 月 13 日
- 筆記
- ASP .NET Core
WebScoket的簡單應用,實現一個簡易的FTP,即文件上傳下載,可以查看上傳人,下載次數,打開多個Web可以多人上傳。
說在前面的話
文件傳輸協議(File Transfer Protocol,FTP)是使用TCP協議傳輸的,這裡用Websocket只是仿照日常使用的FTP客戶端的上傳下載做了一個簡易的模型,主要做學習使用,未接觸過WebSocket可以從這裡獲取一點小小的幫助,因為部落客也算是在學習實踐狀態。如有錯誤,還請各大佬加以斧正。
效果圖也在後邊,本可以放在前邊讓更有讀下去的慾望,但是還是讀者希望能夠知其然,知其所以然。
依照慣例,源程式碼在文末,需要自取~
認識WebSocket
其實概念性的問題有很多文章以及各大教程都有寫,可以直接食用,這裡推薦一下菜鳥教程
//www.runoob.com/html/html5-websocket.html
這裡我個人簡單總結使用方式,本文後續也使用此
HTML5 WebSocket
<script type="text/javascript">
// 初始化
var webSocketGetAll;
var useUrl = "WS://localhost:44380/WebSocket/GetAllFile";
webSocketGetAll = new WebSocket(useUrl);
webSocketGetAll.onopen = function()
{
// Web Socket 已連接上,使用 send() 方法發送數據
webSocketGetAll.send("發送數據");
alert("數據發送中...");
};
webSocketGetAll.onmessage = function (evt)
{
var received_msg = evt.data;
alert("數據已接收...");
......
業務相關的程式碼
......
};
webSocketGetAll.onclose = function()
{
alert("連接已關閉...");
};
webSocketGetAll.onerror = function (e)
{
console.log("發生異常:" + e.message);
}
</script>
至此一個簡單WebSocket對象就創建完了,這裡做一下解釋。
- webSocketGetAll是new出來 WebSocket對象。
- useUrl 是請求的後台地址,這個地址必須要 WS 開頭,當我們使用webSocketGetAll.send(“發送數據”) 時,就是請求了該地址,我們在後台使用webSocket.ReceiveAsync(buffer, cancellation) 接收。
- 創建連接WebSocket之後,可以看到他有4個事件,open,message,error,close,顧名思義就可以知道他們的用途,之後主要使用的是message事件,也就是webSocketGetAll.onmessage,他是在客戶端接收服務端數據時觸發的
// websocket的send方法
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
web端寫好,接下來就是服務端,也就是useUrl中請求的地址。
AspNetWebSocket
public class WebSocketController : Controller
{
/// <summary>
/// 獲取文件列表WebSocket
/// </summary>
public void GetAllFile()
{
if (HttpContext.IsWebSocketRequest)
{
HttpContext.AcceptWebSocketRequest(FileTableHandle);
}
else
{
HttpContext.Response.Write("非WebSocket請求不處理!");
}
}
.......
}
這裡便是後台控制獲取處理過來的方法
- HttpContext.IsWebSocketRequest用來判斷是否為WebSocket請求
- AcceptWebSocketRequest 派生類中實現時,接收AspNetWebSocket請求指定的用戶函數,通俗點講就是傳一個方法進去,告訴他WebSocket後續請求使用這個方法
public class WebSocketController : Controller
{
/// <summary>
/// 文件列表WebSock
/// </summary>
/// <param name="socketContext">WebSocket上下文</param>
/// <returns></returns>
public async Task FileTableHandle(AspNetWebSocketContext socketContext)
{
WebSocket webSocket = socketContext.WebSocket;
CancellationToken cancellation = new CancellationToken();
while (webSocket.State == WebSocketState.Open)
{
byte[] bufferInit = new byte[1024];
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[uploadDTO.FileSize]);
if (uploadDTO.FileSize < 1024)
{
buffer = new ArraySegment<byte>(bufferInit);
}
// 等待接收
WebSocketReceiveResult result = await webSocket.ReceiveAsync(buffer, cancellation);
// 可以得到客戶端發送過來的數據;
string userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);
if (userMessage.Equals("Init"))
{
var trStr = InitFileTable();
ArraySegment<byte> sendTableBuf = new ArraySegment<byte>(Encoding.UTF8.GetBytes(trStr));
await webSocket.SendAsync(sendTableBuf, WebSocketMessageType.Text, true, cancellation);
}
}
......
}
FileTableHandle方法就是上文需要的 用戶函數
- while循環保證一直保持監聽, await webSocket.ReceiveAsync(buffer, cancellation); 在這裡斷點,每次請求進來就會從此處進入。
- Encoding.UTF8.GetString(buffer.Array, 0, result.Count); 經過轉碼之後,就可以獲取傳過來的數據,此時應該獲取到的應該是Web頁面發送的 「發送數據」。
- WebSocket是雙工通訊,當然也可以立即給Web頁面發送數據回去,這裡是使用
await webSocket.SendAsync(sendTableBuf, WebSocketMessageType.Text, true, cancellation);
來發送的,這裡可以看到還可以選擇發送類型,之後文件傳輸便使用的二進位。
public enum WebSocketMessageType
{
// 文本
Text,
// 二進位
Binary,
// 關閉
Close
}
後端SendAsync之後,就會進入Web頁面的webSocketGetAll.onmessage 方法中。
好的,花費了一些篇幅,簡單介紹了WebSocket的創建、請求、接收、響應等待流程,接下來就進入正題,創建一個簡單FTP客戶端。
二話不說上程式碼
Web端-創建一個文件列表
這個文件列表大概長這樣,項目使用了默認的MVC框架
<form class="layui-form layui-form-pane1" style="padding-top:10px">
<div class="layui-form-item">
<div class="layui-input-inline">
<label>用戶名:</label> <input class="layui-input" id="userName" value="測試帳號1">
</div>
<div class="layui-btn-container layui-inline">
<input type="file" name="file" class=" layui-btn layui-btn-normal" style="display:inline" id="file">選擇文件
<button class="layui-btn layui-inline" type="button" id="UploadFile">上傳文件</button>
</div>
</div>
<div id="view">
<ul></ul>
</div>
<div class="layui-form">
<table class="layui-table" id="FileTable">
<thead>
<tr>
<td>文件ID</td>
<td>文件名</td>
<td>上傳人</td>
<td>最後修改時間</td>
<td>下載次數</td>
<td>文件類型</td>
<td>操作</td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</form>
前端使用了layui,雖然layui王朝落寞,但是對我這樣的只會一些原生js,jq,一丟丟vue知識的後端來說,搭建一個簡易美觀的頁面還是很方便的。
不喜歡看源碼,可以直接跳到-【運行效果】
Web端-WebSocket鏈接
<script type="text/javascript">
// 初始化
var webSocketGetAll;
var webSocketFile;
var downloadFileName = "文件名.txt";
// 下載用連接
var filesUrl = "WS://localhost:44380/WebSocket/DownLoad";
// 獲取用連接,即刷新
var useUrl = "WS://localhost:44380/WebSocket/GetAllFile";
$(function ()
{
//W1 每次刷新,或者重新連接進來獲取文件表格
webSocketGetAll = new WebSocket(useUrl);
webSocketGetAll.onopen = function () {
console.log("初始化鏈接WebScoket創建成功");
// 發送初始化請求
webSocketGetAll.send("Init");
}
webSocketGetAll.onmessage = function (e) {
// 初始化成功後,填充文件表格
var data = e.data;
$("#FileTable tbody").html("");
$("#FileTable tbody").append(data);
}
// W2 新進入的請求,下載文件使用該WebSocket
webSocketFile = new WebSocket(filesUrl);
webSocketFile.onopen = function () {
console.log("下載WebSocket鏈接,初始連接成功");
}
webSocketFile.onmessage = function (e) {
var data = e.data;
// 利用a標籤實現下載
if (data instanceof Blob) {
console.info(data);
const url = window.URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', downloadFileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
$("#FileTable tbody").html("");
$("#FileTable tbody").append(data);
}
$("#UploadFile").on("click", function () {
var fileController = document.getElementById("file").files;
var filetest = fileController[0];
uploadOperate(filetest);
})
})
</script>
以上:首先每個新的Web打開這個頁面,要同步當前文件列表
<script type="text/javascript">
// 下載文件方法,傳入參數為: 文件id-文件名
function downLoadFileTd(id) {
var index = id.indexOf('-');
downloadFileName = id.substring(index + 1, id.length - index + 1);
console.info(id);
if (webSocketFile) {
webSocketFile.send("DownLoad-" + id);
}
}
// 讀取文件對象。
var reader = new FileReader();
// 讀取文件 核心方法
function readBlob(file) {
reader.readAsArrayBuffer(file);
}
// 上傳文件 核心方法
function uploadOperate(filec) {
if (filec) {
//讀取文件
readBlob(filec);
//讀取成功 發送文件
reader.onload = function () {
blob = this.result;
var upLoadFilesUrl = filesUrl +
"?FileName=" + filec.name +
"&FileSize=" + filec.size +
"&LastModified=" + filec.lastModified +
"&FileType=" + filec.type +
"&UserName=" + $("#userName").val();
var webSocketFileUpLoad = new WebSocket(upLoadFilesUrl);
webSocketFileUpLoad.onopen = function () {
console.log("connect 鏈接創建成功");
webSocket = webSocketFileUpLoad;
webSocketFileUpLoad.send(blob);
}
webSocketFileUpLoad.onmessage = function (e) {
var data = e.data;
if (data instanceof Blob) {
console.info(data);
const url = window.URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', downloadFileName)
debugger
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
$("#FileTable tbody").html("");
$("#FileTable tbody").append(data);
}
webSocketFileUpLoad.onclose = function (e) {
console.log("WebSocket 連接已斷開。");
}
webSocketFileUpLoad.onerror = function (e) {
console.log("發生異常:" + e.message);
}
}
}
}
</script>
以上:包含了讀取文件,上傳文件,以及下載文件方法。
- 讀取文件與上傳文件,是將文件轉成二進位傳輸的,在後端接收之後,保存到指定文件目錄,並且使用一個字典保存了文件資訊。
- 下載文件:獲取文件列表時,便將文件資訊讀取到了,請求WebSocket地址,獲取文件二進位資訊,拼接一個a標籤,跳轉鏈接去下載,並且指定了文件名,便可以直接下載到對應的文件。
.NET後端-WebSocket與文件 處理
獲取文件列表在上文便已經用簡易的Demo演示了,依葫蘆畫瓢,做一些修改。
web端在上傳的時候,請求後端地址,可以在地址後邊拼接一些參數,將文件資訊存儲下來。
public class WebSocketController : Controller
{
// 文件傳輸對象
public static FileUploadDTO uploadDTO = new FileUploadDTO();
// 文件列表,全部採用記憶體處理,可自行改為數據存儲
public static Dictionary<int, FileUploadDTO> fileDatas = new Dictionary<int, FileUploadDTO>();
/// <summary>
/// 下載文件WebSocket
/// </summary>
/// <param name="fileUploadDTO">傳入的文件模型</param>
public void DownLoad(FileUploadDTO fileUploadDTO)
{
if (HttpContext.IsWebSocketRequest) //判斷一下是否是WebSocket鏈接
{
if (fileUploadDTO != null && fileUploadDTO.FileSize > 0)
{
uploadDTO = fileUploadDTO;
uploadDTO.FileID = fileDatas.Count();
fileDatas.Add(fileDatas.Count(), uploadDTO);
}
HttpContext.AcceptWebSocketRequest(DownLoadHandle);
}
else
{
HttpContext.Response.Write("我不處理!");
}
}
...... 其他業務程式碼 ......
}
這裡使用了一個字典模擬文件存儲,可以自行改造成資料庫存儲或其他。
public class WebSocketController : Controller
{
/// <summary>
/// 下載文件
/// </summary>
/// <param name="socketContext">WebSocket上下文</param>
/// <returns></returns>
public async Task DownLoadHandle(AspNetWebSocketContext socketContext)
{
WebSocket webSocket = socketContext.WebSocket;
CancellationToken cancellation = new CancellationToken();
while (webSocket.State == WebSocketState.Open)
{
byte[] bufferInit = new byte[1024];
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[uploadDTO.FileSize]);
if (uploadDTO.FileSize < 1024)
{
buffer = new ArraySegment<byte>(bufferInit);
}
// 等待接收
WebSocketReceiveResult result = await webSocket.ReceiveAsync(buffer, cancellation);
// 可以得到客戶端發送過來的數據;
string userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);
if (userMessage.Contains("DownLoad"))
{
int.TryParse(userMessage.Split('-')[1], out int downFileID);
if (webSocket.State == WebSocketState.Open)
{
ArraySegment<byte> sendBuf = GetFileByteBySavePath(downFileID);
await webSocket.SendAsync(sendBuf, WebSocketMessageType.Binary, true, cancellation);
}
}
else if (!string.IsNullOrEmpty(userMessage))
{
//存儲文件
SaveFile(buffer.Array, uploadDTO);
}
// 刷新Table
var trStr = InitFileTable();
ArraySegment<byte> sendTableBuf = new ArraySegment<byte>(Encoding.UTF8.GetBytes(trStr));
if (webSocket.State == WebSocketState.Open)
{
await webSocket.SendAsync(sendTableBuf, WebSocketMessageType.Text, true, cancellation);
}
}
}
}
DownLoadHandle 其實集合了上傳下載功能,或許叫做FileHandle更合適,讀者可以自行拉取程式碼修改。(我可不是懶)
- 上傳文件:接著上部分講,在Web端通過拼接請求參數,後端獲取文件資訊之後,便會等待Web端發送文件流進來,接著用是否包含DownLoad簡單區分為上傳文件,使用SaveFile將二進位存儲為文件,保存到磁碟中。
- 下載文件:在建立起連接之後,如果Web端send(“DownLoad-5”) ,則會獲取字典中ID=5的文件,去獲取他的文件路徑,然後讀取成二進位流,再由後端await webSocket.SendAsync 出去。
/// <summary>
/// 保存文件
/// </summary>
/// <param name="br"></param>
/// <param name="uploadModel"></param>
/// <returns></returns>
public bool SaveFile(byte[] br, FileUploadDTO uploadModel)
{
string filePath = "D://"; //文件路徑
filePath = Path.Combine(filePath, uploadModel.FileName);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
}
try
{
FileStream fstream = System.IO.File.Create(filePath, br.Length);
fstream.Write(br, 0, br.Length); //二進位轉換成文件
fstream.Close();
uploadDTO.FileSavePath = filePath;
return true;
}
catch (Exception ex)
{
//拋出異常資訊
return false;
}
}
/// <summary>
/// 根據文件路徑獲取文件二進位數據.
/// </summary>
/// <param name="downFileID"></param>
/// <returns></returns>
public ArraySegment<byte> GetFileByteBySavePath(int downFileID)
{
if (fileDatas.TryGetValue(downFileID, out FileUploadDTO fileModel))
{
fileDatas[downFileID].DownLoadCount++;
}
_ = new byte[fileModel.FileSize];
try
{
FileStream fileStream = new FileStream(fileModel.FileSavePath, FileMode.Open, FileAccess.Read);
BinaryReader r = new BinaryReader(fileStream);
r.BaseStream.Seek(0, SeekOrigin.Begin); //將文件指針設置到文件開
byte[] pReadByte = r.ReadBytes((int)r.BaseStream.Length);
if (fileStream != null)
fileStream.Close();
ArraySegment<byte> pReadByteB = new ArraySegment<byte>(pReadByte);
return pReadByteB;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
ArraySegment<byte> pReadByteC = new ArraySegment<byte>(new byte[0]);
return pReadByteC;
}
}
/// <summary>
/// 初始化表格拼接
/// </summary>
/// <returns></returns>
public string InitFileTable()
{
StringBuilder stringBuilder = new StringBuilder();
foreach (var item in fileDatas)
{
stringBuilder.Append($"<tr><td>{item.Value.FileID }</td>");
stringBuilder.Append($"<td>{item.Value.FileName }</td>");
stringBuilder.Append($"<td>{item.Value.UserName }</td>");
stringBuilder.Append($"<td>{item.Value.LastModified }</td>");
stringBuilder.Append($"<td>{item.Value.DownLoadCount }</td>");
stringBuilder.Append($"<td>{item.Value.FileType }</td>");
stringBuilder.Append($"<td onclick='downLoadFileTd(\"{item.Value.FileID}-{item.Value.FileName}\")'>下載</td></tr>");
}
return stringBuilder.ToString();
}
細心的讀者應該發現了,這裡有BUG
- 因為在對Web端send過來的數據進行UTF-8解碼之後,會得到文件內容,如果此時上傳一個空文本文件,由於沒有命中上傳存儲的篩選條件,他並不會存儲文件到磁碟。
- 或者上傳一個文本里包含了Download文字,他便進行下載操作。
其實這裡也容易解決,將上傳下載分別用不同的WebSocket對象即可,讀者可以拉程式碼下來自行修改~
RUN
好的,至此一個簡易的WebSocket版本FTP客戶端就做好了,看下運行效果吧。
運行效果
源程式碼
打開源程式碼,F5即可運行
暫不支援斷線續傳,不支援大文件上傳下載,後續有空可能會更新
源程式碼倉庫 //gitee.com/yi_zihao/simple-web-socket.git
參考資料
【菜鳥教程】HTML5 WebSocket //www.runoob.com/html/html5-websocket.html
【張果-部落格園】WebSocket與消息推送 //www.cnblogs.com/best/p/5695570.html
【張善友-部落格園】TCP/IP, WebSocket 和 MQTT //www.cnblogs.com/shanyou/p/4085802.html
【微軟文檔】WebSocket //docs.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocket.sendasync?view=netframework-4.7.2