WebSocket实现简易的FTP客户端
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客户端的更多相关文章
- C语言:自己编写的简易ftp客户端,包含(列表,进入目录,上传文件,下载文件,删除文件)功能
//简易ftp客户端#include <stdio.h> #include <string.h> #include <sys/types.h> #include & ...
- day-1 用python编写一个简易的FTP服务器
从某宝上购买了一份<Python神经网络深度学习>课程,按照视频教程,用python语言,写了一个简易的FTP服务端和客户端程序,以前也用C++写过聊天程序,编程思路差不多,但是pytho ...
- ClassicFTP for Mac(FTP 客户端)破解版安装
1.软件简介 ClassicFTP 是 macOS 系统上一款易于使用的 FTP 客户端,让您能够从远程服务器(网站)或网络查看,编辑,上传,下载和删除文件的免费的软件.Mac 下的一款使用 F ...
- 使用 Socket 通信实现 FTP 客户端程序(来自IBM)
FTP 客户端如 FlashFXP,File Zilla 被广泛应用,原理上都是用底层的 Socket 来实现.FTP 客户端与服务器端进行数据交换必须建立两个套接字,一个作为命令通道,一个作为数据通 ...
- Socket网络编程--FTP客户端
Socket网络编程--FTP客户端(1)(Windows) 已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解F ...
- ftp客户端命令使用简记
OS:windows8.1评估版 程序和功能 tftp客户端勾选上 Win+R:运行,键入cmd,键入ftp -help 如下图: 使用ftp客户端可以做的事:将文件传送到运行FTP服务器服务(经常称 ...
- Socket网络编程--FTP客户端(1)(Windows)
已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解FTP作用 就是一个提供一个文件的共享协议. 1.了解FTP协议 ...
- 用edtftpj实现Java FTP客户端工具
edtftpj是一个java FTP工具包,使用非常方便,感觉比Apache的好用,但Apache更灵活.edtftpj有多种版本,分别是java..net和js版本.对于Java版的有一个免费版本. ...
- Socket网络编程--FTP客户端(60篇socket博客,而且都比较简单、深入浅出)
已经好久没有写过博客进行分享了.具体原因,在以后说. 这几天在了解FTP协议,准备任务是写一个FTP客户端程序.直接上干货了. 0.了解FTP作用 就是一个提供一个文件的共享协议. 1.了解FTP协议 ...
随机推荐
- [转载]linux上用PHP读取WORD文档
在linux上用PHP读取WORD文档,其实是使用了 antiword程序把word文档转化为txt文档. 再使用php执行系统命令调用而已. 具体操作如下: 1.安装antiword 官方站:htt ...
- HTML 网页开发、CSS 基础语法——八.HTML基本语法
表格制作 1.表格基础 创建一个简单的表格至少有三个标签组成,分别是<table>,<tr>,<td>标签. table:表格,定义的是整个的表格大结构. tr:t ...
- python numpy loadtxt
用numpy加载csv文件数据 发现python numpy loadtxt 方法和数据的结构有很大关系当我的数据有第一行文字是这样子的时候 我程序的运行结果永远都报错,编码格式也处理了统一utf-8 ...
- element-ui上传多个文件时会发送多个请求
1. element-ui的默认 默认是异步多次请求上传单个文件 如果业务就是单纯的上传文件,那么这个样子是没有问题的 前端代码参考 https://element-plus.gitee.io/#/z ...
- GIT打补丁 - patch和diff应用
一. 准备工作: [root@guangzhou gittest]# git br * master [root@guangzhou gittest]# git chk -b patch-test1 ...
- Docker小白到实战之Docker Compose在手,一键足矣
前言 Docker可以将应用程序及环境很方便的以容器的形式启动,但当应用程序依赖的服务比较多,或是遇到一个大系统拆分的服务很多时,如果还一个一个的根据镜像启动容器,那就有点累人了,到这有很多小伙伴会说 ...
- enum 试图表达64位数
enum AttributeType: unsigned long long{ aa = 1, bb = 2, cc = 0x842AC1040000}; int main() { DWORD64 b ...
- 面试官:Java从编译到执行,发生了什么?
面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」? 候选者:很好理解啊,因为我们有JVM. 候选者:Java源代码会被编译为class文件,cla ...
- GIS应用|快速开发REST数据服务
随着计算机的快速发展,GIS已经在各大领域得到应用,和我们的生活息息相关, 但是基于GIS几大厂商搭建服务,都会有一定的门槛,尤其是需要server,成本高,难度大,这里介绍一种在线GIS云平台,帮你 ...
- 【.Net vs Java? 】 看一看二者的类有多像?
1. 包(Package).命名空间(NameSpace) 1.1 概念 在Java中常用的是包(Package),较少提到NameSpace的概念.Java官方文档中这样说: 为了使类型更易于查找和 ...