WebSocket实现简易的FTP客户端

WebScoket的简单应用,实现一个简易的FTP,即文件上传下载,可以查看上传人,下载次数,打开多个Web可以多人上传。
说在前面的话 文件传输协议(File Transfer Protocol,FTP)是使用TCP协议传输的,这里用Websocket只是仿照日常使用的FTP客户端的上传下载做了一个简易的模型,主要做学习使用,未接触过WebSocket可以从这里获取一点小小的帮助,因为博主也算是在学习实践状态。如有错误,还请各大佬加以斧正。
效果图也在后边,本可以放在前边让更有读下去的欲望,但是还是读者希望能够知其然,知其所以然。
依照惯例,源代码在文末,需要自取~
认识WebSocket 其实概念性的问题有很多文章以及各大教程都有写,可以直接食用,这里推荐一下菜鸟教程
https://www.runoob.com/html/html5-websocket.html
这里我个人简单总结使用方式,本文后续也使用此
HTML5 WebSocket

至此一个简单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 { /// /// 获取文件列表WebSocket /// public void GetAllFile() { if (HttpContext.IsWebSocketRequest) { HttpContext.AcceptWebSocketRequest(FileTableHandle); } else { HttpContext.Response.Write("非WebSocket请求不处理!"); } }....... }

这里便是后台控制获取处理过来的方法
  • HttpContext.IsWebSocketRequest用来判断是否为WebSocket请求
  • AcceptWebSocketRequest 派生类中实现时,接收AspNetWebSocket请求指定的用户函数,通俗点讲就是传一个方法进去,告诉他WebSocket后续请求使用这个方法
public class WebSocketController : Controller { /// /// 文件列表WebSock /// /// WebSocket上下文 /// 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 buffer = new ArraySegment(new byte[uploadDTO.FileSize]); if (uploadDTO.FileSize < 1024) { buffer = new ArraySegment(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 sendTableBuf = new ArraySegment(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框架
WebSocket实现简易的FTP客户端
文章图片

选择文件
    文件ID 文件名 上传人 最后修改时间 下载次数 文件类型 操作

    前端使用了layui,虽然layui王朝落寞,但是对我这样的只会一些原生js,jq,一丢丢vue知识的后端来说,搭建一个简易美观的页面还是很方便的。
    不喜欢看源码,可以直接跳到-【运行效果】
    Web端-WebSocket链接

    以上:首先每个新的Web打开这个页面,要同步当前文件列表

    以上:包含了读取文件,上传文件,以及下载文件方法。
    • 读取文件与上传文件,是将文件转成二进制传输的,在后端接收之后,保存到指定文件目录,并且使用一个字典保存了文件信息。
    • 下载文件:获取文件列表时,便将文件信息读取到了,请求WebSocket地址,获取文件二进制信息,拼接一个a标签,跳转链接去下载,并且指定了文件名,便可以直接下载到对应的文件。
    .NET后端-WebSocket与文件 处理 获取文件列表在上文便已经用简易的Demo演示了,依葫芦画瓢,做一些修改。
    web端在上传的时候,请求后端地址,可以在地址后边拼接一些参数,将文件信息存储下来。
    public class WebSocketController : Controller { // 文件传输对象 public static FileUploadDTO uploadDTO = new FileUploadDTO(); // 文件列表,全部采用内存处理,可自行改为数据存储 public static Dictionary fileDatas = new Dictionary(); /// /// 下载文件WebSocket /// /// 传入的文件模型 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 { /// /// 下载文件 /// /// WebSocket上下文 /// 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 buffer = new ArraySegment(new byte[uploadDTO.FileSize]); if (uploadDTO.FileSize < 1024) { buffer = new ArraySegment(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 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 sendTableBuf = new ArraySegment(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 出去。
    /// /// 保存文件 /// /// /// /// 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; } }/// /// 根据文件路径获取文件二进制数据. /// /// /// public ArraySegment 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 pReadByteB = new ArraySegment(pReadByte); return pReadByteB; } catch (Exception ex) { Console.WriteLine(ex.Message); ArraySegment pReadByteC = new ArraySegment(new byte[0]); return pReadByteC; } }/// /// 初始化表格拼接 /// /// public string InitFileTable() { StringBuilder stringBuilder = new StringBuilder(); foreach (var item in fileDatas) { stringBuilder.Append($"{item.Value.FileID }"); stringBuilder.Append($"{item.Value.FileName }"); stringBuilder.Append($"{item.Value.UserName }"); stringBuilder.Append($"{item.Value.LastModified }"); stringBuilder.Append($"{item.Value.DownLoadCount }"); stringBuilder.Append($"{item.Value.FileType }"); stringBuilder.Append($"下载"); } return stringBuilder.ToString(); }

    细心的读者应该发现了,这里有BUG
    • 因为在对Web端send过来的数据进行UTF-8解码之后,会得到文件内容,如果此时上传一个空文本文件,由于没有命中上传存储的筛选条件,他并不会存储文件到磁盘。
    • 或者上传一个文本里包含了Download文字,他便进行下载操作。
    其实这里也容易解决,将上传下载分别用不同的WebSocket对象即可,读者可以拉代码下来自行修改~
    RUN 好的,至此一个简易的WebSocket版本FTP客户端就做好了,看下运行效果吧。
    运行效果
    源代码 打开源代码,F5即可运行
    暂不支持断线续传,不支持大文件上传下载,后续有空可能会更新
    源代码仓库 https://gitee.com/yi_zihao/simple-web-socket.git
    参考资料 【WebSocket实现简易的FTP客户端】【菜鸟教程】HTML5 WebSocket https://www.runoob.com/html/html5-websocket.html
    【张果-博客园】WebSocket与消息推送 https://www.cnblogs.com/best/p/5695570.html
    【张善友-博客园】TCP/IP, WebSocket 和 MQTT https://www.cnblogs.com/shanyou/p/4085802.html
    【微软文档】WebSocket https://docs.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocket.sendasync?view=netframework-4.7.2

      推荐阅读