飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

WebSocket实现简易的FTP客户端

时间:2021-12-04  作者:OrcCoCo  

WebScoket的简单应用,实现一个简易的FTP,即文件上传下载,可以查看上传人,下载次数,打开多个Web可以多人上传。

说在前面的话

文件传输协议(File Transfer Protocol,FTP)是使用TCP协议传输的,这里用Websocket只是仿照日常使用的FTP客户端的上传下载做了一个简易的模型,主要做学习使用,未接触过WebSocket可以从这里获取一点小小的帮助,因为博主也算是在学习实践状态。如有错误,还请各大佬加以斧正。

效果图也在后边,本可以放在前边让更有读下去的欲望,但是还是读者希望能够知其然,知其所以然。

依照惯例,源代码在文末,需要自取~

认识WebSocket

其实概念性的问题有很多文章以及各大教程都有写,可以直接食用,这里推荐一下菜鸟教程
https://域名/html/html5-域名
这里我个人简单总结使用方式,本文后续也使用此

HTML5 WebSocket

    <script type="text/javascript">
     // 初始化
    var webSocketGetAll;
    var useUrl = "WS://localhost:44380/WebSocket/GetAllFile";
    webSocketGetAll = new WebSocket(useUrl);
    域名en = function()
      {
         // Web Socket 已连接上,使用 send() 方法发送数据
         域名("发送数据");
         alert("数据发送中...");
      };
      域名ssage = function (evt) 
      { 
         var received_msg = 域名;
         alert("数据已接收...");
         ......
         业务相关的代码
         ......
      };
      域名ose = function()
      { 
         alert("连接已关闭..."); 
      };
      域名ror = function (e) 
      {
          域名("发生异常:" + 域名age);
      }
     </script>

至此一个简单WebSocket对象就创建完了,这里做一下解释。

  • webSocketGetAll是new出来 WebSocket对象。
  • useUrl 是请求的后台地址,这个地址必须要 WS 开头,当我们使用域名("发送数据") 时,就是请求了该地址,我们在后台使用域名iveAsync(buffer, cancellation) 接收。
  • 创建连接WebSocket之后,可以看到他有4个事件,open,message,error,close,顾名思义就可以知道他们的用途,之后主要使用的是message事件,也就是域名ssage,他是在客户端接收服务端数据时触发的
// websocket的send方法
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;

web端写好,接下来就是服务端,也就是useUrl中请求的地址。

AspNetWebSocket

    public class WebSocketController : Controller
    {
        /// <summary>
        /// 获取文件列表WebSocket
        /// </summary>
        public void GetAllFile()
        {
            if (域名bSocketRequest)
            {
                域名ptWebSocketRequest(FileTableHandle);
            }
            else
            {
                域名e("非WebSocket请求不处理!");
            }
        }
        
        .......
    }

这里便是后台控制获取处理过来的方法

  • 域名bSocketRequest用来判断是否为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 = 域名ocket;
            CancellationToken cancellation = new CancellationToken();
            while (域名e == 域名)
            {
                byte[] bufferInit = new byte[1024];

                ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[域名Size]);
                if (域名Size < 1024)
                {
                    buffer = new ArraySegment<byte>(bufferInit);
                }
                // 等待接收
                WebSocketReceiveResult result = await 域名iveAsync(buffer, cancellation);
                // 可以得到客户端发送过来的数据;
                string userMessage = 域名tring(域名y, 0, 域名t);
                if (域名ls("Init"))
                {
                    var trStr = InitFileTable();
                    ArraySegment<byte> sendTableBuf = new ArraySegment<byte>(域名ytes(trStr));
                    await 域名Async(sendTableBuf, 域名, true, cancellation);
                }
            }
            
            ......
      }

FileTableHandle方法就是上文需要的 用户函数

  • while循环保证一直保持监听, await 域名iveAsync(buffer, cancellation); 在这里断点,每次请求进来就会从此处进入。
  • 域名tring(域名y, 0, 域名t); 经过转码之后,就可以获取传过来的数据,此时应该获取到的应该是Web页面发送的 “发送数据”。
  • WebSocket是双工通讯,当然也可以立即给Web页面发送数据回去,这里是使用
    await 域名Async(sendTableBuf, 域名, true, cancellation);
    来发送的,这里可以看到还可以选择发送类型,之后文件传输便使用的二进制。
    public enum WebSocketMessageType
    {
        // 文本
        Text,
        // 二进制
        Binary,
        // 关闭
        Close
    }

后端SendAsync之后,就会进入Web页面的域名ssage 方法中。
好的,花费了一些篇幅,简单介绍了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);
        域名en = function () {
            域名("初始化链接WebScoket创建成功");
            // 发送初始化请求
            域名("Init");
        }
        域名ssage = function (e) {
            // 初始化成功后,填充文件表格
            var data = 域名;
            $("#FileTable tbody").html("");
            $("#FileTable tbody").append(data);
        }

        // W2 新进入的请求,下载文件使用该WebSocket
        webSocketFile = new WebSocket(filesUrl);
        域名en = function () {
            域名("下载WebSocket链接,初始连接成功");
        }
        域名ssage = function (e) {
            var data = 域名;

            // 利用a标签实现下载
            if (data instanceof Blob) {
                域名(data);
                const url = 域名teObjectURL(new Blob([data]))
                const link = 域名teElement(\'a\')
                域名lay = \'none\'
                域名 = url
                域名ttribute(\'download\', downloadFileName)
                域名ndChild(link)
                域名k()
                域名veChild(link)
            }
            $("#FileTable tbody").html("");
            $("#FileTable tbody").append(data);
        }

        $("#UploadFile").on("click", function () {
            var fileController = 域名lementById("file").files;
            var filetest = fileController[0];
            uploadOperate(filetest);

        })
    })
    </script>

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

<script type="text/javascript">
    // 下载文件方法,传入参数为:  文件id-文件名
    function downLoadFileTd(id) {
        var index = 域名xOf(\'-\');
        downloadFileName = 域名tring(index + 1, 域名th - index + 1);
        域名(id);
        if (webSocketFile) {
            域名("DownLoad-" + id);
        }
    }

    // 读取文件对象。
    var reader = new FileReader();

    // 读取文件 核心方法
    function readBlob(file) {
        域名AsArrayBuffer(file);
    }

    // 上传文件 核心方法
    function uploadOperate(filec) {
        if (filec) {
            //读取文件
            readBlob(filec);

            //读取成功  发送文件
            域名ad = function () {
                blob = 域名lt;
                var upLoadFilesUrl = filesUrl +
                    "?FileName=" + 域名 +
                    "&FileSize=" + 域名 +
                    "&LastModified=" + 域名Modified +
                    "&FileType=" + 域名 +
                    "&UserName=" + $("#userName").val();
                var webSocketFileUpLoad = new WebSocket(upLoadFilesUrl);

                域名en = function () {
                    域名("connect 链接创建成功");
                    webSocket = webSocketFileUpLoad;
                    域名(blob);
                }

                域名ssage = function (e) {
                    var data = 域名;
                    if (data instanceof Blob) {
                        域名(data);
                        const url = 域名teObjectURL(new Blob([data]))
                        const link = 域名teElement(\'a\')
                        域名lay = \'none\'
                        域名 = url
                        域名ttribute(\'download\', downloadFileName)
                        debugger
                        域名ndChild(link)
                        域名k()
                        域名veChild(link)
                    }
                    $("#FileTable tbody").html("");
                    $("#FileTable tbody").append(data);
                }
                域名ose = function (e) {
                    域名("WebSocket 连接已断开。");
                }
                域名ror = function (e) {
                    域名("发生异常:" + 域名age);
                }
            }
        }
    }

</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 (域名bSocketRequest) //判断一下是否是WebSocket链接
            {
                if (fileUploadDTO != null && 域名Size > 0)
                {
                    uploadDTO = fileUploadDTO;
                    域名ID = 域名t();
                    域名(域名t(), uploadDTO);
                }
                域名ptWebSocketRequest(DownLoadHandle);
            }
            else
            {
                域名e("我不处理!");
            }
        }
        
        ...... 其他业务代码 ......
    }

这里使用了一个字典模拟文件存储,可以自行改造成数据库存储或其他。

    public class WebSocketController : Controller
    {
        /// <summary>
        /// 下载文件
        /// </summary>
        /// <param name="socketContext">WebSocket上下文</param>
        /// <returns></returns>
        public async Task DownLoadHandle(AspNetWebSocketContext socketContext)
        {
            WebSocket webSocket = 域名ocket;
            CancellationToken cancellation = new CancellationToken();
            while (域名e == 域名)
            {
                byte[] bufferInit = new byte[1024];

                ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[域名Size]);
                if (域名Size < 1024)
                {
                    buffer = new ArraySegment<byte>(bufferInit);
                }

                // 等待接收
                WebSocketReceiveResult result = await 域名iveAsync(buffer, cancellation);

                // 可以得到客户端发送过来的数据;
                string userMessage = 域名tring(域名y, 0, 域名t);

                if (域名ains("DownLoad"))
                {
                    域名arse(域名t(\'-\')[1], out int downFileID);
                    if (域名e == 域名)
                    {
                        ArraySegment<byte> sendBuf = GetFileByteBySavePath(downFileID);
                        await 域名Async(sendBuf, 域名ry, true, cancellation);
                    }
                }
                else if (!域名llOrEmpty(userMessage))
                {
                    //存储文件
                    SaveFile(域名y, uploadDTO);
                }

                // 刷新Table
                var trStr = InitFileTable();
                ArraySegment<byte> sendTableBuf = new ArraySegment<byte>(域名ytes(trStr));
                if (域名e == 域名)
                {
                    await 域名Async(sendTableBuf, 域名, true, cancellation);
                }
            }

        }
    
    }

DownLoadHandle 其实集合了上传下载功能,或许叫做FileHandle更合适,读者可以自行拉取代码修改。(我可不是懒)

  • 上传文件:接着上部分讲,在Web端通过拼接请求参数,后端获取文件信息之后,便会等待Web端发送文件流进来,接着用是否包含DownLoad简单区分为上传文件,使用SaveFile将二进制存储为文件,保存到磁盘中。
  • 下载文件:在建立起连接之后,如果Web端send("DownLoad-5") ,则会获取字典中ID=5的文件,去获取他的文件路径,然后读取成二进制流,再由后端await 域名Async 出去。
        /// <summary>
        /// 保存文件
        /// </summary>
        /// <param name="br"></param>
        /// <param name="uploadModel"></param>
        /// <returns></returns>
        public bool SaveFile(byte[] br, FileUploadDTO uploadModel)
        {
            string filePath = "D://";   //文件路径
            filePath = 域名ine(filePath, 域名Name);
            if (域名.Exists(filePath))
            {
                域名.Delete(filePath);
            }
            try
            {
                FileStream fstream = 域名.Create(filePath, 域名th);
                域名e(br, 0, 域名th);   //二进制转换成文件
                域名e();
                域名SavePath = filePath;
                return true;
            }
            catch (Exception ex)
            {
                //抛出异常信息
                return false;
            }
        }

        /// <summary>
        /// 根据文件路径获取文件二进制数据.
        /// </summary>
        /// <param name="downFileID"></param>
        /// <returns></returns>
        public ArraySegment<byte> GetFileByteBySavePath(int downFileID)
        {
            if (域名etValue(downFileID, out FileUploadDTO fileModel))
            {
                fileDatas[downFileID].DownLoadCount++;
            }
            _ = new byte[域名Size];
            try
            {
                FileStream fileStream = new FileStream(域名SavePath, 域名, 域名);
                BinaryReader r = new BinaryReader(fileStream);
                域名(0, 域名n);    //将文件指针设置到文件开
                byte[] pReadByte = 域名Bytes((int)域名th);
                if (fileStream != null)
                    域名e();
                ArraySegment<byte> pReadByteB = new ArraySegment<byte>(pReadByte);
                return pReadByteB;
            }
            catch (Exception ex)
            {
                域名eLine(域名age);
                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)
            {
                域名nd($"<tr><td>{域名ID }</td>");
                域名nd($"<td>{域名Name }</td>");
                域名nd($"<td>{域名Name }</td>");
                域名nd($"<td>{域名Modified }</td>");
                域名nd($"<td>{域名LoadCount }</td>");
                域名nd($"<td>{域名Type }</td>");
                域名nd($"<td onclick=\'downLoadFileTd(\"{域名ID}-{域名Name}\")\'>下载</td></tr>");
            }
            return 域名ring();
        }

细心的读者应该发现了,这里有BUG

  • 因为在对Web端send过来的数据进行UTF-8解码之后,会得到文件内容,如果此时上传一个空文本文件,由于没有命中上传存储的筛选条件,他并不会存储文件到磁盘。
  • 或者上传一个文本里包含了Download文字,他便进行下载操作。

其实这里也容易解决,将上传下载分别用不同的WebSocket对象即可,读者可以拉代码下来自行修改~

RUN

好的,至此一个简易的WebSocket版本FTP客户端就做好了,看下运行效果吧。

运行效果

源代码

打开源代码,F5即可运行

暂不支持断线续传,不支持大文件上传下载,后续有空可能会更新

源代码仓库 https://域名/yi_zihao/simple-web-域名

参考资料

【菜鸟教程】HTML5 WebSocket https://域名/html/html5-域名
【张果-博客园】WebSocket与消息推送 https://域名/best/p/域名
【张善友-博客园】TCP/IP, WebSocket 和 MQTT https://域名/shanyou/p/域名
【微软文档】WebSocket https://域名/zh-cn/dotnet/api/域名域名async?view=netframework-4.7.2

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。