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

说在前面的话

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

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

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

认识WebSocket

其实概念性的问题有很多文章以及各大教程都有写,可以直接食用,这里推荐一下菜鸟教程

https://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即可运行

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

源代码仓库 https://gitee.com/yi_zihao/simple-web-socket.git

参考资料

【菜鸟教程】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

WebSocket实现简易的FTP客户端的更多相关文章

  1. C语言:自己编写的简易ftp客户端,包含(列表,进入目录,上传文件,下载文件,删除文件)功能

    //简易ftp客户端#include <stdio.h> #include <string.h> #include <sys/types.h> #include & ...

  2. day-1 用python编写一个简易的FTP服务器

    从某宝上购买了一份<Python神经网络深度学习>课程,按照视频教程,用python语言,写了一个简易的FTP服务端和客户端程序,以前也用C++写过聊天程序,编程思路差不多,但是pytho ...

  3. ClassicFTP for Mac(FTP 客户端)破解版安装

    1.软件简介    ClassicFTP 是 macOS 系统上一款易于使用的 FTP 客户端,让您能够从远程服务器(网站)或网络查看,编辑,上传,下载和删除文件的免费的软件.Mac 下的一款使用 F ...

  4. 使用 Socket 通信实现 FTP 客户端程序(来自IBM)

    FTP 客户端如 FlashFXP,File Zilla 被广泛应用,原理上都是用底层的 Socket 来实现.FTP 客户端与服务器端进行数据交换必须建立两个套接字,一个作为命令通道,一个作为数据通 ...

  5. Socket网络编程--FTP客户端

    Socket网络编程--FTP客户端(1)(Windows) 已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解F ...

  6. ftp客户端命令使用简记

    OS:windows8.1评估版 程序和功能 tftp客户端勾选上 Win+R:运行,键入cmd,键入ftp -help 如下图: 使用ftp客户端可以做的事:将文件传送到运行FTP服务器服务(经常称 ...

  7. Socket网络编程--FTP客户端(1)(Windows)

    已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解FTP作用 就是一个提供一个文件的共享协议. 1.了解FTP协议 ...

  8. 用edtftpj实现Java FTP客户端工具

    edtftpj是一个java FTP工具包,使用非常方便,感觉比Apache的好用,但Apache更灵活.edtftpj有多种版本,分别是java..net和js版本.对于Java版的有一个免费版本. ...

  9. Socket网络编程--FTP客户端(60篇socket博客,而且都比较简单、深入浅出)

    已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解FTP作用 就是一个提供一个文件的共享协议. 1.了解FTP协议 ...

随机推荐

  1. [转载]Samba 4实现windows匿名访问Linux共享!

    SMB(Server Messages Block,信息服务块). 由于NFS(网络文件系统)可以很好的完成Linux与Linux之间的数据共享,因而 Samba较多的用在了Linux与windows ...

  2. 项目配置shiro原缓存注解失效

    项目用springboot + shiro + ehcache @cacheable 注解不起作用原因 Shiro框架初始化比Spring框架的某些部件早,导致使用@Autowire注入Shiro框架 ...

  3. State Space Model Content

    State Space Model 状态空间模型及其卡尔曼滤波技术 混合正态分布下的状态空间模型及其滤波

  4. 踩坑系列《一》数据库建表权限 CREATE command denied to user for table

    今天在表中用Navicat连接服务器上的mysql账号进行建表,报了个这样类似的错, CREATE command denied to user for table 是数据库权限设置的问题,所以无法进 ...

  5. mysql从零开始之MySQL 创建数据库

    MySQL 创建数据库 我们可以在登陆 MySQL 服务后,使用 create 命令创建数据库,语法如下: CREATE DATABASE 数据库名; 以下命令简单的演示了创建数据库的过程,数据名为 ...

  6. Docker-初见

    目录 Docker概述 Docker历史 Docker Docker的基本组成 Docker安装 使用流程 底层原理 Docker的常用命令 Portainer 可视化面板安装 镜像原理之联合文件系统 ...

  7. 安装 webstorm--->vue

    一.先去官网下载webstorm     https://www.jetbrains.com/ 不论是Mac的还是win得都有相应的版本, 二.再去官网下载git     https://git-sc ...

  8. bzoj2038 小z的袜子 (莫队)

    题目大意 作为一个生活散漫的人,小Z每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿.终于有一天,小Z再也无法忍受这恼人的找袜子过程,于是他决定听天由命-- 具体来说,小Z把这N只袜子从1到N编 ...

  9. mysql join语句的执行流程是怎么样的

    mysql join语句的执行流程是怎么样的 join语句是使用十分频繁的sql语句,同样结果的join语句,写法不同会有非常大的性能差距. select * from t1 straight_joi ...

  10. NXOpen.UF.UFView.CycleObjects 的使用

    Public Sub CycleObjects(ByVal view As NXOpen.Tag, ByVal type As NXOpen.UF.UFView.CycleObjectsEnum, B ...