基于 WebSocket 的聊天和大文件上传(有进度提示)完美实现

时间:2024-01-18 18:38:50

大家好,好久没有写文章了,当然不是不想写,主要是工作太忙,公司有没有网络环境,不让上网,所以写的就少了。今天是2019年的最后一天,明天就要开始新的一年,当然也希望自己有一个新的开始。在2019年的最后一天,写点东西,作为这一年的总结吧!写点啥呢?最近有时间,由于公司的需要,需要实现一个自己的、Web版本的聊天工具,当然也要能传输文件。经过两个星期的无网络、艰苦的学习,终于写出了一个最初的版本。在公司里面里面已经生成正式版本了,很多类型都进行了抽象化,支持注册,头像,私信,群聊,传输大文件,类似 Web 版本的QQ。那是公司的东西,这个版本是我又重新写的,没有做过多的设计,但是功能都实现了,这个版本还比较粗糙,有时间在写第二个版本。

别的先不说,先上一个截图,让大家看一下效果,版本虽然粗糙,但是该有的功能都有了,大家可以根据自己的需要改成自己的东西。效果图如下:
基于 WebSocket 的聊天和大文件上传(有进度提示)完美实现

好了,以上就是效果图,挺实用的,大家只要稍加修改就可以使用,所有代码都是可以正常使用的。

代码挺多的,一步一步的来。我先对我的项目做个截图,让大家做到心里有数。项目分为两个部分,一个部分是类库,主要实现代码再次,还有一个就是MVC的前端的项目。

第一步:项目截图:(VS2017)
           基于 WebSocket 的聊天和大文件上传(有进度提示)完美实现
          第二步:前端代码

 <!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<style type="text/css">
html, body {
font-size: 12px;
height: 100%;
width: 100%;
overflow-y: auto;
} textarea {
font-size: 12px;
color: black;
} #messageContent {
background-color: cadetblue;
border: 1px solid black;
width: 500px;
height: 500px;
font-size: 14px;
overflow-y: auto;
} .chatLeft {
display: block;
color: chocolate;
font-size: 12px;
margin-top: 5px;
margin-left: 10px;
padding: 3px;
float: left;
clear: both;
} .chatRight {
display: block;
color: white;
font-size: 12px;
margin-top: 5px;
margin-right: 10px;
padding: 3px;
text-align: right;
float: right;
clear: both;
} .chatTitleSingle {
white-space: nowrap;
display: block;
border-radius: 3px;
padding: 5px;
color: black;
background-color: #267b8a;
font-size: 14px;
text-align: left;
} .chatTitleGroup {
white-space: nowrap;
border-radius: 3px;
display: block;
padding: 5px;
color: #b61111;
background-color: #267b8a;
font-size: 14px;
max-width: 250px;
min-width: 200px;
text-align:left;
} .chatSelfContent {
display: block;
border-radius: 3px;
padding: 5px;
font-size: 12px;
color: black;
background-color: #51d870;
max-width: 250px;
min-width: 200px;
text-align: left;
} .chatContent {
border-radius: 3px;
display: block;
padding: 5px;
font-size: 12px;
color: black;
background-color: white;
max-width: 250px;
max-width: 200px;
text-align: left;
} .loginContent {
padding: 5px;
text-align: center;
font-size: 14px;
color: gold;
font-weight: bold;
clear: both;
} .logoutContent {
padding: 5px;
text-align: center;
font-size: 14px;
color: darkslateblue;
font-weight: bold;
clear: both;
} .fileUploadedFinished {
padding: 5px;
text-align: center;
font-size: 12px;
color: darkslateblue;
clear: both;
} .offlineUser {
padding: 3px;
font-size: 14px;
color: dimgrey;
text-align: center;
clear: both;
} #chatAndFileContainer {
margin: 5px;
} #spnNoticeText {
color: red;
} .noticeMessageInContainer {
font-size: 12px;
text-align: center;
color: darkslateblue;
clear: both;
} a:link {
color: #03516f;
text-decoration: none;
} a:visited {
color: #1ea73c;
text-decoration: wavy;
} a:hover {
color: #0597d0;
text-decoration: underline;
} a:active {
color: #0bbd33;
text-decoration: none;
}
</style>
</head>
<body>
<div>
<div><span style="padding-left:5px;color:red;">提示:</span><span id="spnNoticeText">暂无连接!</span></div>
<div style="margin:5px 4px">
链接服务:<input type="text" name="name" placeholder="请输入登录标识" id="txtUserKey"/>
<button id="btnConnected" style="margin-right:10px;margin-left:10px;">建立连接</button>
<button id="btnClose">关闭连接</button>
</div>
<div id="chatAndFileContainer" style="display:none">
<div style="margin:5px 0px;">
消息内容:<textarea id="txtContent" placeholder="消息内容" cols="35" rows="5"></textarea>
</div>
<div style="margin:5px 0px;">
接受人名:<input type="text" id="txtPrivateUserKey" placeholder="群聊无需填写接收人" />
</div>
<div>
文件上传:<input type="file" id="file" style="border:1px solid black;margin:0px;padding:0px;width:300px;" multiple />
</div>
<div id="uploadProgress"></div>
<div style="margin:10px 60px;">
<button id="btnSendGroup" style="margin-right:10px">群 聊</button> <button id="btnSendPrivate">私 聊</button>
</div>
</div>
<div id="messageContent"></div>
</div>
<br />
<script type="text/javascript" src="~/Scripts/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="~/Scripts/MyScripts/ChatAndUploadFilesProcessHandler.js"></script>
</body>
</html>

第三步:JavaScript 代码,文件名:ChatAndUploadFilesProcessHandler.js

 //封装文件上传和聊天。
(function () {
//生命全局变量
var webSocketInstance;
var chatUrl = "ws://localhost:62073/HttpHandlers/WebChatHandler.ashx";
var isSendFileGroup = false;//是否是群发文件,默认状态不是群发。
var isOnline = false;
var mainProcess = {
//1、初始化基本事件
init: function () {
this.initClick();
},
//2、建立通讯事件。
initConnect: function () {
if (isOnline == false) {
var newUrl = chatUrl + "?userKey=" + $("#txtUserKey").val(); webSocketInstance = new WebSocket(newUrl); //2.1、建立网络连接的时候触发该事件
webSocketInstance.onopen = function () {
$("#spnNoticeText").html("已经连接!");
$("#chatAndFileContainer").attr("style", "display:block");
} //2.2、接受服务器发来的消息触发该事件。
webSocketInstance.onmessage = function (evt) {
$("#messageContent").append(evt.data);
} //2.3、网络错误的时候触发该事件。
webSocketInstance.onerror = function (evt) {
$("#spnNoticeText").html(JSON.stringify(evt));
} //2.4、当连接关闭的时候触发该事件。
webSocketInstance.onclose = function () {
//这里可以根据实际场景编写,比如重连机制。
$("#spnNoticeText").html("断开连接!");
$("#chatAndFileContainer").attr("style", "display:none");
}
isOnline = true;
}
else {
$("#spnNoticeText").html($("#txtUserKey").val()+"用户已经在线了!");
}
},
//3、初始化各种点击事件。
initClick: function () {
//3.1、网络连接事件
$("#btnConnected").on("click", function () {
if (document.getElementById("txtUserKey") && document.getElementById("txtUserKey").value == "") {
$("#spnNoticeText").html("请输入登录用户的标识!");
return;
}
mainProcess.initConnect();
}); //3.2、网络连接事件
$("#btnClose").on("click", function () {
if (webSocketInstance && webSocketInstance.readyState == WebSocket.OPEN) {
webSocketInstance.close();
isOnline = false;
}
}); //3.3、群发消息
$("#btnSendGroup").on("click", function () {
if (webSocketInstance) {
if (webSocketInstance.readyState == WebSocket.OPEN) {
clearUploadProgress();
var message = $("#txtContent").val(); if (message && message.length > 0) {
webSocketInstance.send(message);
} if (document.getElementById("file").files.length > 0) {
isSendFileGroup = true;
uploadFiles(); clearFilesUploader();
}
}
else if (webSocketInstance.readyState == WebSocket.CLOSED) {
$("#spnNoticeText").html("已经与服务器断开连接!");
}
else if (webSocketInstance.readyState == WebSocket.CONNECTING) {
$("#spnNoticeText").html("正在尝试与服务器建立连接!");
}
else if (webSocketInstance.readyState == WebSocket.CLOSING) {
$("#spnNoticeText").html("正在关闭与服务器的连接!");
}
}
}); //3.4、私聊发消息
$("#btnSendPrivate").on("click", function () {
var userKey = $("#txtPrivateUserKey").val();
if (userKey == null || userKey == "" || userKey.length <= 0) {
$("#spnNoticeText").html("请输入接收用户的标识!");
return;
} if (webSocketInstance) {
if (webSocketInstance.readyState == WebSocket.OPEN) {
clearUploadProgress();
var message = $("#txtContent").val(); //对消息进行拼接 "$--$--**"+ userKey +"$--$--**"+"要发送消息的内容";
if (message && message.length > 0) {
var finalMessage = "$--$--**" + userKey + "$--$--**" + message;
webSocketInstance.send(finalMessage);
} if (document.getElementById("file").files.length > 0) {
isSendFileGroup = false;
uploadFiles(); clearFilesUploader();
}
}
else if (webSocketInstance.readyState == WebSocket.CLOSED) {
$("#spnNoticeText").html("已经与服务器断开连接!");
}
else if (webSocketInstance.readyState == WebSocket.CONNECTING) {
$("#spnNoticeText").html("正在尝试与服务器建立连接!");
}
else if (webSocketInstance.readyState == WebSocket.CLOSING) {
$("#spnNoticeText").html("正在关闭与服务器的连接!");
}
}
});
}
}; //开始上传文件部分集成。
var filesUrl = "ws://localhost:62073/HttpHandlers/UploadFilesHandler.ashx";
function uploadOperate(file) {
if (file) {
var _this = this;
this.reader = new FileReader();//读取文件对象。
this.step = 1024 * 256; //每次读取文件的大小
this.curLoaded = 0; //当前读取位置
this.file = file; //当前文件对象。
this.enableRead = true;//指示是否可以继续读取。
this.total = file.size;//文件的总大小。
this.startTime = new Date();//开始读取时间。
this.createItem();
this.initWebSocket(function () {
_this.bindReader();
});
}
else {
var _this = this;
this.step = 1024 * 256;
this.curLoaded = 0;
this.enableRead = true;
this.total = 0;
}
}
uploadOperate.prototype = {
//绑定读取事件
bindReader: function () {
var _this = this;
var reader = this.reader;
var webSocketFileInstance = this.webSocketFileInstance;
reader.onload = function (e) {
//判断是否能再次读取
if (_this.enableRead == false) {
return;
}
//根据当前缓冲区控制读取速度
if (webSocketFileInstance.bufferedAmount >= _this.step * 20) {
setTimeout(function () {
_this.loadSuccess(e.loaded);
}, 5);
} else {
_this.loadSuccess(e.loaded);
}
}
//开始读取
_this.readBlob();
},
//成功读取,继续处理
loadSuccess: function (loaded) {
var _this = this;
var webSocketFileInstance = _this.webSocketFileInstance;
//使用 WebSocket 将二进制输出上传到服务器。
var blob = _this.reader.result;
if (_this.curLoaded <= 0) {
webSocketFileInstance.send(_this.file.name);
}
webSocketFileInstance.send(blob);
//当前发送完成,继续读取。
_this.curLoaded += loaded;
if (_this.curLoaded < _this.total) {
_this.readBlob();
}
else {
//发送读取完成
webSocketFileInstance.send("[file:{(:finished:)}200]");
this.showInfo('<div class=\"fileUploadedFinished\">文件名:' + fileNameTrim(_this.file.name, 6) + ',文件大小:【' + (_this.curLoaded / (1024 * 1024)).toFixed(3) + '】M,上传时间:【' + ((new Date().getTime() - _this.startTime.getTime()) / 1000) + '】秒!</div>');
}
//显示进度
_this.showProgress();
},
//创建显示项
createItem: function () {
var _this = this;
var blockquote = document.createElement("blockquote");
var abort = document.createElement("input");
abort.type = 'button';
abort.value = '暂停';
abort.onclick = function () {
_this.stop();
};
blockquote.appendChild(abort); var containue = document.createElement("input");
containue.type = 'button';
containue.value = '继续';
containue.onclick = function () {
_this.containue();
};
blockquote.appendChild(containue); var progress = document.createElement('progress');
progress.style.width = '300px';
progress.max = 100;
progress.value = 0;
blockquote.appendChild(progress);
_this.progressBox = progress; var status = document.createElement('span');
status.id = 'Status';
blockquote.appendChild(status);
_this.statusBox = status; document.getElementById('uploadProgress').appendChild(blockquote);
},
//显示进度
showProgress: function () {
var _this = this;
var percent = ((_this.curLoaded / _this.total) * 100).toFixed();
_this.progressBox.value = percent;
_this.statusBox.innerHTML = percent;
},
//读取文件
readBlob: function () {
var blob = this.file.slice(this.curLoaded, this.curLoaded + this.step);
this.reader.readAsArrayBuffer(blob);
},
//暂停读取
stop: function () {
this.enableRead = false;
var percentValue = this.percent(this.curLoaded / this.total);
if (percentValue != '100%') {
this.showInfo("<div class=\"noticeMessageInContainer\">读取终止,已读取:" + percentValue + "</div>");
}
this.reader.abort();
},
//继续读取
containue: function () {
var percentValue = this.percent(this.curLoaded / this.total);
if (percentValue != '100%') {
this.enableRead = true;
this.readBlob();
this.showInfo("<div class=\"noticeMessageInContainer\">读取继续,已读取:" + percentValue + "</div>");
}
else {
this.enableRead = false;
}
},
//计算百分比
percent: function (data) {
if (data == 0) { return 0; }
var valuePercent = Number(data * 100).toFixed();
valuePercent += "%";
return valuePercent;
},
//显示日志
showInfo: function (data) {
var html = "";
html += data;
document.getElementById("messageContent").innerHTML = document.getElementById("messageContent").innerHTML + html;
var divLogContainer = document.getElementById("messageContent");
divLogContainer.scrollTop = divLogContainer.scrollHeight;
},
//初始化 WebSocket
initWebSocket: function (onSuccess) {
var _this = this;
var webSocketFileInstance = this.webSocketFileInstance = new WebSocket(filesUrl); webSocketFileInstance.onopen = function () {
console.log("connect 链接创建成功");
if (onSuccess) {
onSuccess();
}
}
webSocketFileInstance.onmessage = function (e) {
var data = e.data;
if (isNaN(data) == false) {
showInfo('后台接受成功:' + data);
}
else {
console.info(data);
}
}
webSocketFileInstance.onclose = function (e) {
//终止读取
_this.stop();
showInfo("WebSocket 连接已经断开!");
console.log("WebSocket 连接已断开。");
}
webSocketFileInstance.onerror = function (e) {
_this.stop();
showInfo("发生异常:" + e.message);
console.log("发生异常:" + e.message);
}
}
};
window.uploadOperate = uploadOperate;
window.mainProcess = mainProcess;
})(); $(function () {
mainProcess.init();
}); //上传文件的速度取决于每次 send() 的数据的大小。Google 之所以会慢,是因为他每次 send 的数据很小。
function uploadFiles() {
var fileController = document.getElementById("file");
checkAndUploadCore(fileController, true);
} //检查文件
var fileController2 = document.getElementById("file");
fileController2.onchange = function () {
clearUploadProgress();
document.getElementById("txtContent").value = "";
checkAndUploadCore(fileController2, false);
} //如果文件名太长,就会修剪。
//fileName:文件名
//length:要截取文件名的长度。
function fileNameTrim(fileName, length) {
if (fileName && fileName.length > 0 && fileName != "") {
if (length > 0 && length >= fileName.length) {
return fileName;
}
else {
return fileName.substring(0, length) + "...";
}
}
} //清除文件上传的进度条显示。为下一次做准备。
function clearUploadProgress() {
uploadOperate();
document.getElementById("uploadProgress").innerHTML = "";
} //文件上传后将控件置为初始状态。
function clearFilesUploader() {
document.getElementById("file").value = "";
} //核心的上传文件的方法。
//uploader:上传文件的控件。
//isUpload:是否开始上传文件。
function checkAndUploadCore(uploader, isUpload) {
if (uploader && uploader.files.length > 0) {
var maxTotalSize = 5000;//单位:M
var files = uploader.files;
var fileTotalSize = 0;
var fileCount = 5;
var fileTypes = [".jpg", ".gif", ".bmp", ".png", "jpeg", ".rar", ".zip", ".txt", ".doc", ".ppt", ".xls", ".pdf", ".csv", ".docx", ".xlsx"]; //1、验证上传文件的格式。
var isValid = false;
var fileEnd = '';
if (fileTypes && fileTypes.length > 0) {
for (var m = 0; m < files.length; m++) {
fileEnd = files[m].name.substring(files[m].name.lastIndexOf("."));
isValid = false;
for (var i = 0; i < fileTypes.length; i++) {
if (fileEnd.toLowerCase() == fileTypes[i].toLowerCase()) {
isValid = true;
continue;
}
}
if (!isValid) {
break;
}
}
if (!isValid) {
alert("不支持此文件类型");
uploader.value = '';
return false;
}
} //2、检查文件上传的个数。
if (files.length > 0 && files.length > fileCount) {
alert("最多只能上传【" + fileCount + "】个文件!");
uploader.value = '';
return;
} //3、检查文件的总大小。
for (var i = 0; i < files.length; i++) {
fileTotalSize += files[i].size;
}
fileTotalSize = fileTotalSize / (1024 * 1024);
fileTotalSize = fileTotalSize.toFixed(3);
if (fileTotalSize > maxTotalSize) {
alert("上传文件总自己额大小不能大于【" + (maxTotalSize / 1024).toFixed() + "】G!");
uploader.value = '';
return;
} //4、检查文件名是否有效。
var isFileNameValid = true;
var fileName = '';
var containSpecial = RegExp(/[(\ )(\~)(\!)(\@)(\#)(\$)(\%)(\^)(\&)(\*)(\()(\))(\+)(\=)(\[)(\])(\{)(\})(\|)(\:)(\;)(\')(\")(\,)(\<)(\.)(\>)(\/)(\?)]+/);
for (var m = 0; m < files.length; m++) {
fileName = files[m].name.substring(0, files[m].name.lastIndexOf("."));
if (containSpecial.test(fileName)) {
isFileNameValid = false;
break;
}
}
if (!isFileNameValid) {
alert("文件名包含特殊字符,不可以上传!");
uploader.value = '';
return;
}
}
else {
return;
} if (isUpload) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var operate = new uploadOperate(file);
}
}
else {
var fileNameList = "";
for (var i = 0; i < files.length; i++) {
var file = files[i];
if (i == files.length - 1) {
fileNameList += file.name;
} else {
fileNameList += file.name + "\n";
}
}
document.getElementById("txtContent").value = fileNameList;
}
}

第四步:前端 文件上传代码,文件名:UploadFilesHandler.ashx

 using ChatAndUploadBaseWebSocket;
using System.Web; namespace WebApplicationForChat.HttpHandlers
{
/// <summary>
/// UploadFilesHandler 的摘要说明
/// </summary>
public class UploadFilesHandler : IHttpHandler
{
private WebSocketUploadFilesHandler uploadFileHandler; /// <summary>
/// 处理来之客户端 WebSocket 请求。
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
{
if (uploadFileHandler == null)
{
uploadFileHandler = new WebSocketUploadFilesHandler();
}
context.AcceptWebSocketRequest(uploadFileHandler.ProcessFile);
}
} /// <summary>
/// 指示该处理器是否可以重用。默认不重用。
/// </summary>
public bool IsReusable
{
get
{
return false;
}
}
}
}

第五步:前端聊天处理器代码,文件名:WebChatHandler.ashx

 using ChatAndUploadBaseWebSocket;
using System.Web; namespace WebApplicationForChat.HttpHandlers
{
/// <summary>
/// 基于 HttpHandler 实现的聊天功能。
/// </summary>
public class WebChatHandler : IHttpHandler
{
private WebSocketChatHandler chatHandler;
private string userKey = null; /// <summary>
/// 处理来至客户端的 WebSocket请求。
/// </summary>
/// <param name="context">WebSocket 请求的上下文。</param>
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
{
userKey = context.Request.QueryString["userKey"];
if (!string.IsNullOrEmpty(userKey) && !string.IsNullOrWhiteSpace(userKey))
{
if (chatHandler == null)
{
chatHandler = new WebSocketChatHandler(userKey);
}
else
{
chatHandler.CurrentUserKey = userKey;
}
context.AcceptWebSocketRequest(chatHandler.ProcessChat);
}
}
} /// <summary>
/// 指示该处理器是否可以重用,默认不可以重用。
/// </summary>
public bool IsReusable
{
get
{
return false;
}
}
}
}

          第六步:后端类库代码,文件名:IOnlineUserManager.cs

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; namespace ChatAndUploadBaseWebSocket
{
/// <summary>
/// 该类型定义在线用户的管理器的抽象接口。
/// </summary>
public interface IOnlineUserManager
{
/// <summary>
/// 增加新用户。
/// </summary>
/// <param name="userKey">增加用户的标识符名称。</param>
/// <param name="webSocket">增加的用户标识符对应的 WebSocket 对象实例。</param>
/// <returns>返回布尔类型的值,true 表示增加用户成功,false 表示增加用户失败。</returns>
void Add(string userKey, WebSocket webSocket); /// <summary>
/// 移除指定用户标识符名称的用户实例。
/// </summary>
/// <param name="userKey">要移除用户的标识符。</param>
/// <returns>返回布尔类型的值,true 表示移除用户成功,false 表示移除用户失败。</returns>
Task Remove(string userKey); /// <summary>
/// 要获取指定名称名称的用户实例。
/// </summary>
/// <param name="userKey">要获取用户实例的标识符名称。</param>
/// <returns>如果获取到就返回其实,没有就返回 Null 值。</returns>
WebSocket Get(string userKey); /// <summary>
/// 根据指定用户标识符名称判断相应用户实例是否存在。
/// </summary>
/// <param name="userKey">要判断用户实例是否存在的标识符名称。</param>
/// <returns>返回布尔类型的值,true 表示指定标识符名称的用户实例存在,false 表示不存在指定标识符名称的用户实例。</returns>
bool IsExists(string userKey); /// <summary>
/// 清空所有在线的用户实例。
/// </summary>
Task Clear(); /// <summary>
/// 获取所有在线用户的人数。
/// </summary>
int Count { get; } /// <summary>
/// 向所有在线用户发送消息,当然也包括自己在内。
/// </summary>
/// <param name="content">具体要发送消息的内容。</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <returns>该操作是异步完成的。</returns>
Task Send(string content, CancellationToken? cancellationToken = null); /// <summary>
/// 如果没有指定接受消息人员的列表,默认就是向所有人发送消息。如果指定了接收消息的人员列表,只有指定的人才会接受到消息。
/// </summary>
/// <param name="content">具体要发送消息的内容</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <param name="includedUsers">具体接收消息的用户列表。</param>
/// <returns>该操作是异步完成的。</returns>
Task Send(string content, CancellationToken? cancellationToken, params string[] includedUsers); /// <summary>
/// 如果没有指定哪些在线人员不需要接受信息,就向所用的在线用户发送消息,如果指定了不接受消息人员的列表,就去掉这些在线用户,向其他向所有在线用户发送消息。
/// </summary>
/// <param name="content">具体要发送消息的内容。</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <param name="excludedUsers">具体不需要接收消息的用户列表。</param>
/// <returns>该操作是异步完成的。</returns>
Task SendUn(string content, CancellationToken? cancellationToken = null, params string[] excludedUsers);
}
}

 

        第七步:后端类库代码,文件名:OnlineUsersManager.cs

 using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; namespace ChatAndUploadBaseWebSocket
{
/// <summary>
/// 该类型定义在线用户的管理器,该类型不可以被继承。
/// </summary>
public sealed class OnlineUsersManager:IOnlineUserManager
{
private ConcurrentDictionary<string, WebSocket> _userContainer; #region 获取单件对象 public static readonly IOnlineUserManager Current = new OnlineUsersManager(); #endregion /// <summary>
/// 初始化 OnlineUsersManager 类型的新实例。
/// </summary>
private OnlineUsersManager()
{
_userContainer = new ConcurrentDictionary<string, WebSocket>();
} /// <summary>
/// 增加新用户。
/// </summary>
/// <param name="userKey">增加用户的标识符名称。</param>
/// <param name="webSocket">增加的用户标识符对应的 WebSocket 对象实例。</param>
/// <returns>返回布尔类型的值,true 表示增加用户成功,false 表示增加用户失败。</returns>
public void Add(string userKey, WebSocket webSocket)
{
if (string.IsNullOrEmpty(userKey) || string.IsNullOrWhiteSpace(userKey) || webSocket == null)
{
return;
}
if (!_userContainer.ContainsKey(userKey))
{
_userContainer.TryAdd(userKey, webSocket);
}
} /// <summary>
/// 移除指定用户标识符名称的用户实例。
/// </summary>
/// <param name="userKey">要移除用户的标识符。</param>
/// <returns>返回布尔类型的值,true 表示移除用户成功,false 表示移除用户失败。</returns>
public async Task Remove(string userKey)
{
if (!string.IsNullOrEmpty(userKey) && !string.IsNullOrWhiteSpace(userKey))
{
if (_userContainer.ContainsKey(userKey))
{
WebSocket temp;
if (_userContainer.TryRemove(userKey, out temp))
{
await temp.CloseAsync(WebSocketCloseStatus.NormalClosure,"Close",CancellationToken.None);
}
}
}
} /// <summary>
/// 要获取指定名称名称的用户实例。
/// </summary>
/// <param name="userKey">要获取用户实例的标识符名称。</param>
/// <returns>如果获取到就返回其实,没有就返回 Null 值。</returns>
public WebSocket Get(string userKey)
{
if (!string.IsNullOrEmpty(userKey) && !string.IsNullOrWhiteSpace(userKey))
{
if (_userContainer.ContainsKey(userKey))
{
return _userContainer[userKey];
}
}
return null;
} /// <summary>
/// 根据指定用户标识符名称判断相应用户实例是否存在。
/// </summary>
/// <param name="userKey">要判断用户实例是否存在的标识符名称。</param>
/// <returns>返回布尔类型的值,true 表示指定标识符名称的用户实例存在,false 表示不存在指定标识符名称的用户实例。</returns>
public bool IsExists(string userKey)
{
bool result = false;
if (!string.IsNullOrWhiteSpace(userKey) && !string.IsNullOrEmpty(userKey))
{
return _userContainer.ContainsKey(userKey);
}
return result;
} /// <summary>
/// 清空所有在线的用户实例。
/// </summary>
public async Task Clear()
{
foreach (var item in _userContainer.Keys)
{
WebSocket socket;
if (_userContainer.TryRemove(item, out socket))
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure,"Close",CancellationToken.None);
}
}
} /// <summary>
/// 获取所有在线用户的人数。
/// </summary>
public int Count
{
get { return _userContainer.Count; }
} /// <summary>
/// 向所有在线用户发送消息,当然也包括自己在内。
/// </summary>
/// <param name="content">具体要发送消息的内容。</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <returns>该操作是异步完成的。</returns>
public async Task Send(string content, CancellationToken? cancellationToken = null)
{
if (!string.IsNullOrEmpty(content) && !string.IsNullOrWhiteSpace(content))
{
if (cancellationToken == null)
{
cancellationToken = CancellationToken.None;
}
ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content));
foreach (var item in _userContainer.Values)
{
await item.SendAsync(buffer, WebSocketMessageType.Text, true, cancellationToken.Value);
}
}
} /// <summary>
/// 如果没有指定接受消息人员的列表,默认就是向所有人发送消息。如果指定了接收消息的人员列表,只有指定的人才会接受到消息。
/// </summary>
/// <param name="content">具体要发送消息的内容</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <param name="includedUsers">具体接收消息的用户列表。</param>
/// <returns>该操作是异步完成的。</returns>
public async Task Send(string content, CancellationToken? cancellationToken, params string[] includedUsers)
{
if (!string.IsNullOrEmpty(content) && !string.IsNullOrWhiteSpace(content))
{
if (includedUsers == null || includedUsers.Length <= )
{
await Send(content, cancellationToken);
}
else
{
if (cancellationToken == null)
{
cancellationToken = CancellationToken.None;
}
ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content));
foreach (var item in _userContainer)
{
foreach (var name in includedUsers)
{
if (string.Compare(name, item.Key, true) == )
{
await item.Value.SendAsync(buffer, WebSocketMessageType.Text, true, cancellationToken.Value);
}
}
}
}
}
} /// <summary>
/// 如果没有指定哪些在线人员不需要接受信息,就向所用的在线用户发送消息,如果指定了不接受消息人员的列表,就去掉这些在线用户,向其他向所有在线用户发送消息。
/// </summary>
/// <param name="content">具体要发送消息的内容。</param>
/// <param name="cancellationToken">取消发送的标识对象。</param>
/// <param name="excludedUsers">具体不需要接收消息的用户列表。</param>
/// <returns>该操作是异步完成的。</returns>
public async Task SendUn(string content, CancellationToken? cancellationToken = null, params string[] excludedUsers)
{
if (!string.IsNullOrEmpty(content) && !string.IsNullOrWhiteSpace(content))
{
if (excludedUsers == null || excludedUsers.Length <= )
{
await Send(content,null);
}
if (cancellationToken == null)
{
cancellationToken = CancellationToken.None;
}
ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content));
foreach (var item in _userContainer)
{
foreach (var userName in excludedUsers)
{
if (string.Compare(item.Key, userName, true) != )
{
await item.Value.SendAsync(buffer, WebSocketMessageType.Text, true, cancellationToken.Value);
}
}
}
}
}
}
}

 

        第九步:后端类库代码,文件名:UploadFileExtensionValidator.cs

 using System.Text.RegularExpressions;

 namespace ChatAndUploadBaseWebSocket
{
/// <summary>
/// 该类型定义上传文件扩展名是否有效的验证器。
/// </summary>
public sealed class UploadFileExtensionValidator
{
/// <summary>
/// 验证上传的文件的格式是否是有效的。true 表示是有效的文件格式,false 表示不是有效的文件格式。
/// </summary>
/// <param name="value">要验证的文件名。</param>
/// <returns>返回布尔类型的值,true 表示是有效的文件格式,false 表示不是有效的文件格式。</returns>
public static bool ValidateFiles(string value)
{
if (!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value) && value.IndexOf(".") != -)
{
bool result = Regex.IsMatch(value, @"^.+\.(jpg|png|gif|bmp|rar|txt|zip|doc|ppt|xls|pdf|docx|xlsx|jpeg|xml|csv)(\?.+)?$",RegexOptions.IgnoreCase);
return result;
}
return false;
} /// <summary>
/// 验证图片的格式是否正确,true 表示是有效的图品格式,false 表示不是有效的图片格式。
/// </summary>
/// <param name="value">要验证的图片名称。</param>
/// <returns>返回布尔类型的值,true 表示是有效的图品格式,false 表示不是有效的图片格式。</returns>
public static bool ValidateImages(string value)
{
if (!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value) && value.IndexOf(".") != -)
{
bool result = Regex.IsMatch(value, @"^.+\.(jpg|png|gif|bmp|jpeg)(\?.+)?$", RegexOptions.IgnoreCase);
return result;
}
return false;
}
}
}

        第十步:后端类库代码,文件名:WebSocketChatHandler.cs

 using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.WebSockets; namespace ChatAndUploadBaseWebSocket
{
/// <summary>
/// 基于 HttpHandler 实现的聊天功能。
/// </summary>
public sealed class WebSocketChatHandler
{
#region 私有字段 private string _userKey; #endregion #region 构造函数 /// <summary>
/// 以指定的默认值初始化该类型的新实例。默认值:*
/// </summary>
public WebSocketChatHandler():this("*"){} /// <summary>
/// 以指定的用户标识符初始化该类型的新实例。
/// </summary>
/// <param name="userKey">用户的标识符。</param>
/// <exception cref="ArgumentNullException">userKey is null.</exception>
public WebSocketChatHandler(string userKey)
{
if (!string.IsNullOrEmpty(userKey) && !string.IsNullOrWhiteSpace(userKey))
{
_userKey = userKey;
}
else
{
throw new ArgumentNullException("userKey is null.");
}
} #endregion #region 实例属性 public string CurrentUserKey
{
get { return _userKey; }
set
{
if (!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value))
{
_userKey = value;
}
}
} #endregion #region 核心方法 /// <summary>
/// 处理客户端发送过来的文本信息。
/// </summary>
/// <param name="context">WebSocket 请求的上下文。</param>
/// <returns>返回异步操作的实例对象 Task 。</returns>
public async Task ProcessChat(AspNetWebSocketContext context)
{
#region 局部变量 string messageNotice = null;
string messageBody = null;
string content = null;
string selfContent = null;
string receiveUser = null;
string[] arrays = null;
string messageMain = null;
string offlineContent = null;
ArraySegment<byte> echor;
ArraySegment<byte> buffer;
WebSocketReceiveResult result; #endregion //1、获取 WebSocket 实例对象。
WebSocket webSocket = context.WebSocket;
bool isExists = OnlineUsersManager.Current.IsExists(CurrentUserKey);
if (isExists)
{
//表示该用户在线。
await OnlineUsersManager.Current.Send($"<div class=\"onlineUser\">用户【{CurrentUserKey}】已经在线!</div>", CancellationToken.None, CurrentUserKey);
}
else
{
OnlineUsersManager.Current.Add(CurrentUserKey,webSocket);
//表示登陆成功
//某人成功登陆后,可以给群里其他人发送登陆成功的提示消息(本人除外)
messageNotice = $"<div class=\"loginContent\">用户【{CurrentUserKey}】进入聊天室,登录时间:{DateTime.Now.ToString("yyyy-M-dd HH:mm")}</div>"; await OnlineUsersManager.Current.Send(messageNotice); //2、开始监听来至客户端的 WebSocket 请求。
while (webSocket.State == WebSocketState.Open)
{
//每次读取客户端发送来的消息的大小。
buffer = new ArraySegment<byte>(new byte[*]); result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
//关闭 WebSocket 请求
if (result.MessageType == WebSocketMessageType.Close)
{
await OnlineUsersManager.Current.Remove(CurrentUserKey); //发送离开提醒
messageNotice = $"<div class=\"logoutContent\">用户【{CurrentUserKey}】离开聊天室,退出时间:{DateTime.Now.ToString("yyyy-M-dd HH:mm")}</div>";
await OnlineUsersManager.Current.Send(messageNotice);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
else
{
//用于发送聊天内容
if (result.MessageType == WebSocketMessageType.Text)
{
messageBody = Encoding.UTF8.GetString(buffer.Array,,result.Count);
//判断是群聊还是私聊
if (messageBody.Length > && messageBody.Substring(, ) == "$--$--**")
{
//此处表示私聊
arrays = messageBody.Split(new string[] { "$--$--**" }, StringSplitOptions.RemoveEmptyEntries);
receiveUser = arrays[]; messageMain = UbbAndHtmlConverter.UBBToHTML(arrays[]);
messageMain = TextToAnchor(messageMain); var isExistsUser = OnlineUsersManager.Current.IsExists(receiveUser);
if (isExistsUser)
{
if (!string.IsNullOrEmpty(messageMain) && !string.IsNullOrWhiteSpace(messageMain))
{
//私聊给对方
content = $"<div class=\"chatLeft\"><span class=\"chatTitleSingle\">{CurrentUserKey} &nbsp;&nbsp;&nbsp;&nbsp;{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}</span><span class=\"chatContent\">{messageMain}</span></div>";
await OnlineUsersManager.Current.Send(content, CancellationToken.None, receiveUser); //私聊给自己
selfContent = $"<div class=\"chatRight\"><span class=\"chatTitleSingle\">{CurrentUserKey}&nbsp;&nbsp;&nbsp;&nbsp;{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}</span><span class=\"chatSelfContent\">{messageMain}</span></div>";
echor = new ArraySegment<byte>(Encoding.UTF8.GetBytes(selfContent));
await webSocket.SendAsync(echor, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
else
{
offlineContent = $"<div class=\"offlineUser\">用户【{receiveUser}】不在线!</div>";
echor = new ArraySegment<byte>(Encoding.UTF8.GetBytes(offlineContent));
await webSocket.SendAsync(echor, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
else
{
messageBody = UbbAndHtmlConverter.UBBToHTML(messageBody);
messageBody = TextToAnchor(messageBody); //这里表示群聊
if (OnlineUsersManager.Current.Count > )
{
//群发给他人,不包含自己
content = $"<div class=\"chatLeft\"><span class=\"chatTitleGroup\">{CurrentUserKey} &nbsp;&nbsp;&nbsp;&nbsp;{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}</span><span class=\"chatContent\">{messageBody}</span></div>";
await OnlineUsersManager.Current.SendUn(content, CancellationToken.None, CurrentUserKey); //单独在给自己发送一份
selfContent = $"<div class=\"chatRight\"><span class=\"chatTitleGroup\">{CurrentUserKey} &nbsp;&nbsp;&nbsp;&nbsp;{DateTime.Now.ToString("yyyy-MM-dd HH:mm")}</span><span class=\"chatSelfContent\">{messageBody}</span></div>";
echor = new ArraySegment<byte>(Encoding.UTF8.GetBytes(selfContent));
await webSocket.SendAsync(echor,WebSocketMessageType.Text,true,CancellationToken.None);
}
}
}
}
}
}
} /// <summary>
/// 如果文本中包含文件名,就将文件名转换带链接的文件名,便于下载。
/// </summary>
/// <param name="value">要转换的内容。</param>
/// <returns>返回成功转换的值。</returns>
private string TextToAnchor(string value)
{
if (!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value) && UploadFileExtensionValidator.ValidateFiles(value))
{
string[] values = value.Split(new string[] { "<br/>"},StringSplitOptions.RemoveEmptyEntries);
StringBuilder fileLinkBuilder = new StringBuilder(); for (int i = ; i < values.Length; i++)
{
if (UploadFileExtensionValidator.ValidateFiles(values[i]))
{
if (IsExists(values[i]))
{
if (UploadFileExtensionValidator.ValidateImages(values[i]))
{
if (i == values.Length - )
{
fileLinkBuilder.AppendFormat("<img src=\"{0}\" title=\"上传时间:{1},右键点击保存\" alt=\"{2}\" width=\"200px\">","/UploadFiles/"+values[i],DateTime.Now.ToString(),values[i]);
}
else
{
fileLinkBuilder.AppendFormat("<img src=\"{0}\" title=\"上传时间:{1},右键点击保存\" alt=\"{2}\" width=\"200px\"><br/>", "/UploadFiles/" + values[i], DateTime.Now.ToString(), values[i]);
}
}
else
{
if (i == values.Length - )
{
fileLinkBuilder.AppendFormat("<a href=\"{0}\" target=\"_blank\" title=\"右键单击保存\">{1}</a>","/UploadFiles/"+values[i],values[i]);
}
else
{
fileLinkBuilder.AppendFormat("<a href=\"{0}\" target=\"_blank\" title=\"右键单击保存\">{1}</a><br/>", "/UploadFiles/" + values[i], values[i]);
}
}
}
}
else
{
fileLinkBuilder.Append(values[i]+"<br/>");
}
}
return fileLinkBuilder.ToString();
}
return value;
} /// <summary>
/// 判断指定文件名的文件是否存在,true 表示存在,false 表示不存在。
/// </summary>
/// <param name="fileName">要判断是否存在的文件名。</param>
/// <returns>返回布尔类型的值,true 表示文件存在,false 表示文件不存在。</returns>
private bool IsExists(string fileName)
{
Thread.Sleep();
bool result = false;
if (!string.IsNullOrEmpty(fileName) && !string.IsNullOrWhiteSpace(fileName))
{
if (File.Exists(HttpContext.Current.Server.MapPath("/UploadFiles/") + fileName))
{
result = true;
}
}
return result;
} #endregion
}
}

        第十一步:后端类库代码,文件名:WebSocketUploadFilesHandler.cs

 using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.WebSockets; namespace ChatAndUploadBaseWebSocket
{
/// <summary>
/// 基于 HttpHandler 实现的文件上传的功能。
/// </summary>
public sealed class WebSocketUploadFilesHandler
{
/// <summary>
/// 初始化类型的新实例。
/// </summary>
public WebSocketUploadFilesHandler() { } /// <summary>
/// 处理从客户端上传的文件。
/// </summary>
/// <param name="context">WebSocket 请求的上下文。</param>
/// <returns>返回异步操作的实例对象 Task。</returns>
public async Task ProcessFile(AspNetWebSocketContext context)
{
ArraySegment<byte> everyTimeBufferSize;
WebSocketReceiveResult result;
string message; //1、获取当前的 WebSocket 对象。
WebSocket webSocket = context.WebSocket;
string fileName = null;
byte[] bufferAllSize = new byte[ * * ];//缓存文件总的大小,用于暂时缓存
int loaded = ; //当前缓存的位置。 //2、监听来至客户端的 WebSocket 请求
while (true)
{
//此处的值是控制读取客户端数据的长度,如果客户端发送的数据长度超过当前缓存长度,则读取多次。
everyTimeBufferSize = new ArraySegment<byte>(new byte[ * ]); //接受客户端发送来的消息。
result = await webSocket.ReceiveAsync(everyTimeBufferSize, CancellationToken.None);
if (webSocket.State == WebSocketState.Open)
{
//判断发送的数据是否已经结束。
int currentLength = Math.Min(everyTimeBufferSize.Array.Length, result.Count); try
{
//判断客户端发送的消息的类型
if (result.MessageType == WebSocketMessageType.Text)
{
message = Encoding.UTF8.GetString(everyTimeBufferSize.Array, , currentLength);
bool isValid = UploadFileExtensionValidator.ValidateFiles(message);
if (!isValid && string.Compare(message, "[file:{(:finished:)}200]", true) != )
{
continue;
}
if (string.Compare(message, "[file:{(:finished:)}200]", true) == )
{
SaveFile(fileName, bufferAllSize, loaded);
loaded = ;
}
else
{
fileName = message;
}
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
var temp = loaded + currentLength;
if (temp > bufferAllSize.Length)
{
SaveFile(fileName, bufferAllSize, loaded);
//添加到缓存区
Array.Copy(everyTimeBufferSize.Array, , bufferAllSize, , currentLength);
loaded = currentLength;
}
else
{
//添加到缓冲区
Array.Copy(everyTimeBufferSize.Array, , bufferAllSize, loaded, currentLength);
loaded = temp;
}
}
}
catch (Exception)
{
throw;
}
}
}
} /// <summary>
/// 将文件以追加的形式保存在物理磁盘上。
/// </summary>
/// <param name="fileName">要保存的文件的名称。</param>
/// <param name="buffer">每次要保存的二进制文件数据。</param>
/// <param name="loaded">要追加文件的数据长度。</param>
private void SaveFile(string fileName, byte[] buffer, int length)
{
if (string.IsNullOrEmpty(fileName) || string.IsNullOrWhiteSpace(fileName))
{
return;
}
if (buffer == null || buffer.Length <= )
{
return;
}
if (length < )
{
return;
} string currentDirectory = HttpContext.Current.Server.MapPath("/UploadFiles/");
string filePathFullName = currentDirectory + fileName;
try
{
if (!Directory.Exists(currentDirectory))
{
Directory.CreateDirectory(currentDirectory);
}
using (FileStream fileStream = new FileStream(filePathFullName, FileMode.Append, FileAccess.Write))
{
fileStream.Write(buffer, , length);
}
}
catch (Exception ex)
{
//可以写入日志
throw;
}
}
}
}

好了,全部代码都贴出去了。希望对大家有帮助。类库里面的类型还可以继续升级和优化,有时间了我写第二个版本,今天就到这里了,祝福大家元旦快乐,也祝自己和家人元旦快乐。
              
               新年新气象,也希望自己的2020年有一个优秀的成绩。