基于webapi的websocket聊天室(四)

news/2024/11/17 3:38:01/文章来源:https://www.cnblogs.com/ggtc/p/18190128

上一篇实现了多聊天室。这一片要继续改进的是实现收发文件,以及图片显示。

效果

image

问题

websocket本身就是二进制传输。文件刚好也是二进制存储的。
文件本身的传输问题不太,但是需要传输文件元数据,比如文件名和扩展名之类的。这很必要,如果我们想知道怎么展示这个文件的话。比如这个文件是图片还是word?或者是个exe?
有两种解决办法

  • 第一种是先发送文件元数据,在发送文件二进制数据。
  • 第二种则是在websocket上定义一个文件传输协议,将文件元数据和文件二进制数据打包在一个二进制消息中发送,在服务器解析这个二进制数据。

第一种方法很简单,只是服务器至少要接受两次消息,才能完成一个文件发送。第二种方法则能通过一次消息发送传输文件。
我采用第二种方法。

传输协议

在引入文件传输的要求后,我发现简单的文本传输也不能满足了,而是需要商定好的格式化的文本,比如json文本。
要不然客户端怎么知道是要显示一个文件下载链接而不是是普通消息文本?这就需要一个type指定。
由于图片是直接显示,文件是下载。客户端收到的又只是一个字节流,客户端怎么知道对应动作?
所以最好统一使用websocket二进制传输作为聊天室数据传输的方式。
这就需要一个简单的协议了。

  • 普通消息,消息类型用message
  • 发送图片,广播图片二进制,要和普通字节流区分,消息类型用image
  • 上传文件,然后广播文件链接,要和普通消息区分,消息类型用file
    比如下载文件,要和普通字节流区分,用文件传输协议。
    我们暂且称这个协议为roomChatProtocal,简称RCP

RCP

  • RCP对象格式
    发布者 类型 数据
    字段 visitor type data
    类型 string message,file,link,image object
  • RCP传输对象格式
    发布者长度 发布者 类型 数据长度 数据
    字节流 1 byte n byte 1 byte 4 byte m byte
  • 传输方法
    对象是程序中用的,字节流是传输时用的。
    在对象与字节流之间应该有两个转换方法 Serialize Deserialize

对应实体

在程序中需要一个对象承载RCP的消息

//RCP.cs// 聊天室文本广播格式
public struct BroadcastData
{// 发布者public string visitor { get; set; }// 广播文本类型public BroadcastType type { get; set; }// 数据public object data { get; set; }
}
// 广播文本类型
public enum BroadcastType:byte
{// 发言message,// 文件file,// 链接link,// 图片image
}

对应实体传输方法

在使用RCP时需要用特定的序列化和反序列化方法来解析RCP对象

//RCP.cs// 聊天室文本广播格式
public struct BroadcastData
{//...属性// 序列化对象public static byte[] Serialize(BroadcastData cascade){}// 反序列化对象public static BroadcastData Deserialize(ArraySegment<byte> data){}
}

type协议

type指示了接收端怎么处理消息。但接收端不仅要知道怎么处理消息,还需要获得正确的能够处理的消息。
所以,每种type还应该有一个对应的消息格式。data字段应遵循这种格式

  • message
    消息长度 消息
    4 byte n byte
  • file
    文件名长度 文件名 文件长度 文件链接 文件内容
    1 byte n byte 4 byte 32 byte m byte
    • 文件名长度
      最大支持256个字节,约60字
    • 文件名
      采用utf8进行编码
    • 文件长度
      最大支持4GB文件传输
    • 文件链接
      ASCll编码,32位UUID
    • 举例
      比如传输一张名为boom.png的图片,其大小为100KB
      那么要传输的二进制数据如下
      文件名长度 文件名 文件长度 文件链接 文件内容
      0x08 0x62 6f 6f 6d 2e 70 6e 67 0x00 01 90 00 32 byte 102400 byte
  • link
    文件名长度 文件名 文件大小 文件链接
    1 byte n byte 4 byte 32 byte
  • image
    图片名长度 图片名 图片长度 图片
    1 byte n byte 4 byte m byte

对应处理方法

//RCP.cspublic class RCP
{// 创建消息的RCP传输对象public static BroadcastData Message(string visitor, string message){}// 解析RCP传输对象的消息public static string MessageResolve(BroadcastData broadcastData){}// 创建文件的RCP传输对象public static BroadcastData File((string fileName,string id, byte[] fileBuffer) file){}// 解析RCP传输对象中的文件public static (string fileName, string extension,string id, byte[] buffer) FileResolve(BroadcastData broadcastData){}// 创建链接的RCP传输对象public static BroadcastData Link(string visitor, (string fileName, int fileSize, string id) file){}// 解析RCP传输对象中的链接public static (string fileName,string id, int fileSize) LinkResolve(BroadcastData broadcastData){}// 创建图片的RCP传输对象public static BroadcastData Image(string visitor, string imageName, byte[] image){}// 解析RCP传输对象中的图片public static (string imageName, byte[] buffer) ImageResolve(BroadcastData broadcastData){}
}

聊天室改造

  • 首先需要改造一下类型
//WebSocketChatRoom.cs// 游客
public class RoomVisitor
{public WebSocket Web { get; set; }public string Name { get; set; }public string Id { get; set; }public visitorType type { get; set; }
}
// 游客类型
public enum visitorType:byte
{// 聊天室room,// 游客visitor
}
  • 核心方法
    然后是我们的使用了协议后的核心方法,解析消息,然后根据消息类型执行相应分支。
    协议只规定了消息,没规定接受到消息后的动作。
    客户端和服务器段接收到同一类型的消息时,显然有不同动作。
    message file link image
    服务器端 广播 暂存,构造链接,广播链接 单播文件 广播
    客户端 显示 下载 构造下载链接 构造图片显示

所以在这个方法中我们来定义接收到不同类型消息时服务器端的动作

/// <summary>
/// 处理二进制数据
/// </summary>
/// <param name="result"></param>
/// <param name="visitor"></param>
/// <returns></returns>
public async Task handleBytes((ArraySegment<byte> bytes, WebSocketMessageType MessageType) result,RoomVisitor visitor)
{BroadcastData recivedData = BroadcastData.Deserialize(result.bytes);BroadcastData data;switch (recivedData.type){case BroadcastType.message://广播消息await Broadcast(visitor, recivedData);break;case BroadcastType.file://文件解析,暂存,广播链接(string fileName, string extension,string id, byte[] buffer) resoved = RCP.FileResolve(recivedData);await AcceptFile(resoved);data = RCP.Link(visitor.Name, ($"{resoved.fileName}.{resoved.extension}", resoved.buffer.Length, resoved.id));await Broadcast(visitor, data);break;case BroadcastType.link://文件下载(string fileName, string id, int fileSize) resolved = RCP.LinkResolve(recivedData);(string fileName,string id, byte[] fileBuffer) linkFile =await ReadLinkFile(resolved);data = RCP.File(linkFile);await Unicast(visitor, data);break;case BroadcastType.image://图片转发await Broadcast(visitor, recivedData);break;default:await Broadcast(visitor, new BroadcastData() { type = BroadcastType.message, data = "暂时不支持此消息类型" });break;}
}

主要就是进行了消息的解析,以及调用了RCPtype的的4组解析方法。

  • 需要用到的其他方法
//WebSocketChatRoom.cs// 广播
public async Task Broadcast(RoomVisitor visitor,BroadcastData broadcastData){}
// 单播
public async Task Unicast(RoomVisitor visitor, BroadcastData broadcastData){}
// 多次接受消息
public async Task<(ArraySegment<byte> bytes, WebSocketMessageType MessageType)> GetBytes(WebSocket client, byte[] defaultBuffer){}
// 暂存在服务器,并返回
public async Task AcceptFile((string fileName, string extension,string id, byte[] buffer)  file){}
// 读取暂存在服务器的文件public async Task<(string fileName, string id, byte[] fileBuffer)> ReadLinkFile((string fileName, string id, int fileSize) link){}

完整代码

WebSocketChatRoom.cs
/// <summary>
/// 聊天室
/// </summary>
public class WebSocketChatRoom
{/// <summary>/// 成员/// </summary>public ConcurrentDictionary<string, RoomVisitor> clients=new ConcurrentDictionary<string, RoomVisitor>();private string _roomName;public string roomName { get { return _roomName; } set {_roomName = value;if (room != null){room.Name = value;}else{room = new RoomVisitor() { Name = value,type=visitorType.room };}} }public RoomVisitor room { get; set; }public WebSocketChatRoom(){}public async Task HandleContext(HttpContext context,WebSocket client){//游客加入聊天室var visitor = new RoomVisitor() { Id= System.Guid.NewGuid().ToString("N"), Name = $"游客_{clients.Count + 1}", Web = client,type= visitorType.visitor };clients.TryAdd(visitor.Id, visitor);//广播游客加入聊天室await Broadcast(room, RCP.Message(room.Name, $"{visitor.Name}加入聊天室"));//消息缓冲区。每个连接分配400字节,100个汉字的内存var defaultBuffer = new byte[400];//消息循环while (!client.CloseStatus.HasValue){try{var bytesResult = await GetBytes(client, defaultBuffer);if (bytesResult.MessageType == WebSocketMessageType.Text){//await Cascade(visitor,CascadeMeaasge(visitor,UTF8Encoding.UTF8.GetString(bytesResult.bytes.Array, 0, bytesResult.bytes.Count)));}else if (bytesResult.MessageType == WebSocketMessageType.Binary){await handleBytes(bytesResult, visitor);}}catch (Exception e){}}//广播游客退出await Broadcast(room, RCP.Message(room.Name, $"{visitor.Name}退出聊天室"));await client.CloseAsync(client.CloseStatus!.Value,client.CloseStatusDescription,CancellationToken.None);clients.TryRemove(visitor.Id, out RoomVisitor v);}/// <summary>/// 广播/// </summary>/// <param name="visitor"></param>/// <param name="broadcastData"></param>/// <returns></returns>public async Task Broadcast(RoomVisitor visitor,BroadcastData broadcastData){broadcastData.visitor = visitor.Name;foreach (var other in clients){if (visitor != null){if (other.Key == visitor.Id){continue;}}var buffer = BroadcastData.Serialize(broadcastData);if (other.Value.Web.State == WebSocketState.Open){await other.Value.Web.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None);}}}/// <summary>/// 单播/// </summary>/// <param name="visitor"></param>/// <param name="broadcastData"></param>/// <returns></returns>public async Task Unicast(RoomVisitor visitor, BroadcastData broadcastData){broadcastData.visitor = visitor.Name;var buffer = BroadcastData.Serialize(broadcastData);if (visitor.Web.State == WebSocketState.Open){await visitor.Web.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None);}}/// <summary>/// 多次接受消息/// </summary>/// <param name="client"></param>/// <param name="defaultBuffer"></param>/// <returns></returns>public async Task<(ArraySegment<byte> bytes, WebSocketMessageType MessageType)> GetBytes(WebSocket client, byte[] defaultBuffer){int totalBytesReceived = 0;int bufferSize = 1024 * 4;  // 可以设为更大,视实际情况而定byte[] buffer = new byte[bufferSize];WebSocketReceiveResult result;do{if (totalBytesReceived == buffer.Length)  // 如果缓冲区已满,扩展它{Array.Resize(ref buffer, buffer.Length + bufferSize);}var segment = new ArraySegment<byte>(buffer, totalBytesReceived, buffer.Length - totalBytesReceived);//!result.EndOfMessage时buffer不一定会被填满result = await client.ReceiveAsync(segment, CancellationToken.None);totalBytesReceived += result.Count;} while (!result.EndOfMessage);if (result.MessageType == WebSocketMessageType.Close){return (new ArraySegment<byte>(buffer, 0, totalBytesReceived), WebSocketMessageType.Close);}return (new ArraySegment<byte>(buffer, 0, totalBytesReceived), result.MessageType);}/// <summary>/// 暂存在服务器,并返回/// </summary>/// <param name="buffer"></param>/// <returns></returns>public async Task AcceptFile((string fileName, string extension,string id, byte[] buffer)  file){string fileName = $"{file.fileName}-{file.id}.{file.extension}";//每个聊天室一个文件夹string fullName = $@"C:\ChatRoom\{room.Name}\{fileName}";string directoryPath = Path.GetDirectoryName(fullName);if (!Directory.Exists(directoryPath)){Directory.CreateDirectory(directoryPath);}await File.WriteAllBytesAsync(fullName, file.buffer);}/// <summary>/// 读取暂存在服务器的文件/// </summary>/// <param name="link"></param>/// <returns></returns>public async Task<(string fileName, string id, byte[] fileBuffer)> ReadLinkFile((string fileName, string id, int fileSize) link){string fullName = $@"C:\ChatRoom\{room.Name}\{link.fileName.Split('.')[0]}-{link.id}.{link.fileName.Split('.')[1]}";byte[] buffer = await File.ReadAllBytesAsync(fullName);return (link.fileName,link.id, fileBuffer:buffer);}/// <summary>/// 处理二进制数据/// </summary>/// <param name="result"></param>/// <param name="visitor"></param>/// <returns></returns>public async Task handleBytes((ArraySegment<byte> bytes, WebSocketMessageType MessageType) result,RoomVisitor visitor){BroadcastData recivedData = BroadcastData.Deserialize(result.bytes);BroadcastData data;switch (recivedData.type){case BroadcastType.message://广播消息await Broadcast(visitor, recivedData);break;case BroadcastType.file://文件解析,暂存,广播链接(string fileName, string extension,string id, byte[] buffer) resoved = RCP.FileResolve(recivedData);await AcceptFile(resoved);data = RCP.Link(visitor.Name, ($"{resoved.fileName}.{resoved.extension}", resoved.buffer.Length, resoved.id));await Broadcast(visitor, data);break;case BroadcastType.link://文件下载(string fileName, string id, int fileSize) resolved = RCP.LinkResolve(recivedData);(string fileName,string id, byte[] fileBuffer) linkFile =await ReadLinkFile(resolved);data = RCP.File(linkFile);await Unicast(visitor, data);break;case BroadcastType.image://图片转发await Broadcast(visitor, recivedData);break;default:await Broadcast(visitor, new BroadcastData() { type = BroadcastType.message, data = "暂时不支持此消息类型" });break;}}
}/// <summary>
/// 游客
/// </summary>
public class RoomVisitor
{public WebSocket Web { get; set; }public string Name { get; set; }public string Id { get; set; }public visitorType type { get; set; }
}/// <summary>
/// 游客类型
/// </summary>
public enum visitorType:byte
{/// <summary>/// 聊天室/// </summary>room,/// <summary>/// 游客/// </summary>visitor
}
RCP.cs
/// <summary>
/// RoomChatProtocal
/// 聊天室数据传输协议
/// </summary>
public class RCP
{/// <summary>/// 创建消息的RCP传输对象/// </summary>/// <param name="visitor"></param>/// <param name="message"></param>public static BroadcastData Message(string visitor, string message){return new BroadcastData() { visitor = visitor, type = BroadcastType.message, data = message };}/// <summary>/// 解析RCP传输对象的消息/// </summary>/// <param name="broadcastData"></param>/// <returns></returns>public static string MessageResolve(BroadcastData broadcastData){return broadcastData.data?.ToString()??"";}/// <summary>/// 创建文件的RCP传输对象/// </summary>/// <returns></returns>public static BroadcastData File((string fileName,string id, byte[] fileBuffer) file){BroadcastData data = new BroadcastData();data.type = BroadcastType.file;int fileNameLength = UTF8Encoding.UTF8.GetByteCount(file.fileName);byte[] buffer = new byte[1 + fileNameLength + 4 + 32 + file.fileBuffer.Length];BinaryWriter writer = new BinaryWriter(new MemoryStream(buffer));writer.Write((byte)fileNameLength);writer.Write(UTF8Encoding.UTF8.GetBytes(file.fileName));writer.Write(file.fileBuffer.Length);writer.Write(ASCIIEncoding.ASCII.GetBytes(file.id));writer.Write(file.fileBuffer);data.data = buffer;return data;}/// <summary>/// 解析RCP传输对象中的文件/// </summary>/// <param name="broadcastData"></param>/// <returns></returns>/// <exception cref="NotImplementedException"></exception>public static (string fileName, string extension,string id, byte[] buffer) FileResolve(BroadcastData broadcastData){BinaryReader reader = new BinaryReader(new MemoryStream((byte[])broadcastData.data));int fileNameLength = reader.ReadByte() & 0x000000FF;string fileExtensionName = UTF8Encoding.UTF8.GetString(reader.ReadBytes(fileNameLength));string fileName= fileExtensionName.Split('.')[0];string extension= fileExtensionName.Split(".")[1];int fileLength=reader.ReadInt32();string id = ASCIIEncoding.ASCII.GetString(reader.ReadBytes(32));byte[] buffer= reader.ReadBytes(fileLength);return (fileName, extension, id, buffer);}/// <summary>/// 创建链接的RCP传输对象/// </summary>public static BroadcastData Link(string visitor, (string fileName, int fileSize, string id) file){int fileNameLength = UTF8Encoding.UTF8.GetByteCount(file.fileName);byte[] buffer = new byte[1 + fileNameLength + 32 + 4];BinaryWriter writer = new BinaryWriter(new MemoryStream(buffer));writer.Write((byte)fileNameLength);writer.Write(UTF8Encoding.UTF8.GetBytes(file.fileName));writer.Write(file.fileSize);writer.Write(ASCIIEncoding.ASCII.GetBytes(file.id));return new BroadcastData(){visitor = visitor,type = BroadcastType.link,data = buffer};}/// <summary>/// 解析RCP传输对象中的链接/// </summary>/// <param name="broadcastData"></param>/// <returns></returns>public static (string fileName,string id, int fileSize) LinkResolve(BroadcastData broadcastData){BinaryReader reader = new BinaryReader(new MemoryStream((byte[])broadcastData.data));int fileNameLength=reader.ReadByte() & 0x000000FF;string fileName= UTF8Encoding.UTF8.GetString(reader.ReadBytes(fileNameLength));int fileLength=reader.ReadInt32();string id=ASCIIEncoding.ASCII.GetString(reader.ReadBytes(32));return (fileName, id, fileLength);}/// <summary>/// 创建图片的RCP传输对象/// </summary>/// <param name="visitor"></param>/// <param name="imageName"></param>/// <param name="image"></param>/// <returns></returns>public static BroadcastData Image(string visitor, string imageName, byte[] image){BroadcastData data = new BroadcastData();data.visitor = visitor;data.type = BroadcastType.image;int fileNameLength = UTF8Encoding.UTF8.GetByteCount(imageName);byte[] buffer = new byte[1 + fileNameLength + 4 + 32 + image.Length];BinaryWriter writer = new BinaryWriter(new MemoryStream(buffer));writer.Write((byte)fileNameLength);writer.Write(UTF8Encoding.UTF8.GetBytes(imageName));writer.Write(image.Length);writer.Write(image);data.data = buffer;return data;}/// <summary>/// 解析RCP传输对象中的图片/// </summary>/// <param name="broadcastData"></param>/// <returns></returns>public static (string imageName, byte[] buffer) ImageResolve(BroadcastData broadcastData){BinaryReader reader = new BinaryReader(new MemoryStream((byte[])broadcastData.data));int imageNameLength = reader.ReadByte() & 0x000000FF;string imageExtensionName = UTF8Encoding.UTF8.GetString(reader.ReadBytes(imageNameLength));int imageLength = reader.ReadInt32();byte[] buffer = reader.ReadBytes(imageLength);return (imageExtensionName, buffer);}
}
/// <summary>
/// RCP传输对象
/// </summary>
public struct BroadcastData
{/// <summary>/// 发布者/// </summary>public string visitor { get; set; }/// <summary>/// 广播文本类型/// </summary>public BroadcastType type { get; set; }/// <summary>/// 数据/// </summary>public object data { get; set; }/// <summary>/// 序列化对象/// </summary>/// <param name="broadcast"></param>/// <returns></returns>/// <exception cref="Exception"></exception>public static byte[] Serialize(BroadcastData broadcast){using (MemoryStream memoryStream = new MemoryStream()){//utf8编码字符串using (BinaryWriter writer = new BinaryWriter(memoryStream)){//visitor长度,1字节writer.Write((byte)UTF8Encoding.UTF8.GetByteCount(broadcast.visitor));//visitor,n字节writer.Write(UTF8Encoding.UTF8.GetBytes(broadcast.visitor));//type,一字节writer.Write((byte)broadcast.type);//data,要么是字符串,要么是数组if (broadcast.data is string stringData){//int长度,4字节writer.Write((UTF8Encoding.UTF8.GetByteCount(stringData)));//data内容,m字节writer.Write(UTF8Encoding.UTF8.GetBytes(stringData));}else if (broadcast.data is ArraySegment<byte> ArraySegmentData){//int长度,4字节writer.Write(ArraySegmentData.Count);//data内容,m字节writer.Write(ArraySegmentData);}else if (broadcast.data is byte[] bytesData){//int长度,4字节writer.Write(bytesData.Length);//data内容,m字节writer.Write(bytesData);}else{throw new Exception("不支持的data类型,只能是string或ArraySegment<byte>");}}return memoryStream.ToArray();}}/// <summary>/// 反序列化对象/// </summary>/// <param name="data"></param>/// <returns></returns>public static BroadcastData Deserialize(ArraySegment<byte> data){BroadcastData broadcastData = new BroadcastData();BinaryReader br = new BinaryReader(new MemoryStream(data.Array!));int visitorLength = br.ReadByte() & 0x000000FF;broadcastData.visitor = UTF8Encoding.UTF8.GetString(br.ReadBytes(visitorLength));broadcastData.type = (BroadcastType)br.ReadByte();int dataLength = br.ReadInt32();broadcastData.data = br.ReadBytes(dataLength);return broadcastData;}
}/// <summary>
/// 消息类型
/// </summary>
public enum BroadcastType : byte
{/// <summary>/// 发言/// </summary>message,/// <summary>/// 文件传输/// </summary>file,/// <summary>/// 文件下载链接/// </summary>link,/// <summary>/// 图片查看/// </summary>image
}

web客户端

我简单写了个web客户端。也实现了RCP

chatRoomClient.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>聊天室</title>
</head>
<style>html{height: calc(100% - 16px);margin: 8px;}body{height: 100%;margin: 0;}
</style>
<body><div style="height: 100%;display: grid;grid-template: auto 1fr 100px/1fr;row-gap: 8px;"><div style="grid-area: 1/1/2/2;"><div style="display: grid;grid: 1fr/1fr 100px;column-gap: 8px;"><div style="grid-area: 1/1/1/2;display: flex;justify-content: end;"><label>房间</label><input style="width: 300px;" value="ws://localhost:5234/chat/房间号" name="room" oninput="changeroom(event)"/></div><button style="grid-area: 1/2/1/3;" onclick="connectRoom()" id="open">打开连接</button></div></div><div style="grid-area: 2/1/3/2;background-color: #eeeeee;overflow-y: auto;" id="chatMessages"></div><div style="grid-area: 3/1/4/2;position: relative;"><div class="toolbar"><button onclick="sendimage()">图片</button><button onclick="sendFile()">文件</button></div><textarea style="width: calc(100% - 20px);padding: 5px 10px;height: calc(100% - 33px);font-size: 16px;" id="msg"></textarea><button style="position: absolute;right: 10px;bottom: 5px;" onclick="sendmsg()">发送</button></div></div><script>var socket;var isopen=false;    function changeroom(e){document.title=`聊天室-${e.srcElement.value.split('/').reverse()[0]}`;}function sendmsg(){var msg=document.getElementById('msg').value;if(msg=='')returnif(!isopen)returnif(isopen){var broadcastData=RCP.Message(msg);var buffer=BroadcastData.Serialize(broadcastData);socket.send(buffer);broadcastData.visitor='我';broadcastData.data=RCP.MessageResolve(broadcastData);appendMsg(broadcastData,'right');document.getElementById('msg').value='';}}function sendimage(){if(!isopen)return;var input=document.createElement('input');input.type='file';input.accept='image/jpeg,image/png'input.click();input.onchange=e=>{if(e.srcElement.files.length==0)return;var image=e.srcElement.files[0];var fileReader=new FileReader();fileReader.onload=()=>{var broadcastData= RCP.Image(image.name,fileReader.result);var buffer=BroadcastData.Serialize(broadcastData);socket.send(buffer);broadcastData.visitor='我';var resolvedImage=RCP.ImageResolve(broadcastData);var extension=resolvedImage.imageName.split('.')[resolvedImage.imageName.split('.').length-1];resolvedImage.buffer=createDataURL(extension,resolvedImage.buffer);broadcastData.data=resolvedImage.buffer;appendImage(broadcastData,'right');}fileReader.readAsArrayBuffer(image);}}function sendFile(){if(!isopen)return;var input=document.createElement('input');input.type='file';input.click();input.onchange=e=>{if(e.srcElement.files.length==0)return;var file=e.srcElement.files[0];var fileReader=new FileReader();fileReader.onload=()=>{var broadcastData= RCP.File(file.name,fileReader.result);var buffer=BroadcastData.Serialize(broadcastData);socket.send(buffer);broadcastData.visitor='我';var resolve=RCP.FileResolve(broadcastData);broadcastData.data={fileName:`${resolve.fileName}.${resolve.extension}`,id:resolve.id,fileSize:resolve.buffer.length};appendLink(broadcastData,'right');}fileReader.readAsArrayBuffer(file);}}function downloadLink(fileName,id,fileSize){var broadcastData= RCP.Link(fileName,id,fileSize);var buffer=BroadcastData.Serialize(broadcastData);socket.send(buffer);}function downloadFile(fileInfo){const url=createDataURL(fileInfo.extension,fileInfo.buffer);var download=document.createElement('a');download.href=url;download.download=`${fileInfo.fileName}.${fileInfo.extension}`;download.click();}function connectRoom(){if (isopen==true) {socket.close();return;}var route=document.getElementsByName('room')[0].value;try {                socket=new WebSocket(route);   } catch (error) {console.log(error);isopen=false;document.getElementById('open').innerText='打开连接';return}            socket.addEventListener('open', (event) => {isopen=true;document.getElementById('open').innerText='关闭连接'});socket.addEventListener('message', (event) => {// 处理接收到的消息console.log('Received:', event.data);var fileReader = new FileReader();fileReader.onload=function(event){arrayBufferNew = event.target.result;// uint8ArrayNew = new Uint8Array(arrayBufferNew);handleBytes(arrayBufferNew);}fileReader.readAsArrayBuffer(event.data);});socket.addEventListener('close',event=>{isopen=false;document.getElementById('open').innerText='打开连接';})}function handleBytes(arrayBufferNew){var broadcastData=BroadcastData.Deserialize(arrayBufferNew);switch (broadcastData.type) {case BroadcastType.message:var msg=RCP.MessageResolve(broadcastData);broadcastData.data=msg;appendMsg(broadcastData);break;case BroadcastType.image:var image=RCP.ImageResolve(broadcastData);var extension=image.imageName.split('.')[image.imageName.split('.').length-1];image.buffer=createDataURL(extension,image.buffer);broadcastData.data=image.buffer;appendImage(broadcastData);break;case BroadcastType.link:var linkInfo=RCP.LinkResolve(broadcastData);broadcastData.data=linkInfo;appendLink(broadcastData);break;case BroadcastType.file:var fileInfo=RCP.FileResolve(broadcastData);downloadFile(fileInfo);break;default:break;}}function appendMsg(broadcastData,dock){var chatMessages = document.getElementById('chatMessages');if(dock!='right'){chatMessages.innerHTML+=`<div style="padding:10px;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;">${broadcastData.data}</div></div></div>`;}else{chatMessages.innerHTML+=`<div style="padding:10px;display:flex;flex-direction: column;align-items: flex-end;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;">${broadcastData.data}</div></div></div>`;}// 使用 scrollIntoView 方法将底部元素滚动到可见区域chatMessages.lastChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });}function appendImage(broadcastData,dock){var chatMessages = document.getElementById('chatMessages');if(dock!='right'){chatMessages.innerHTML+=`<div style="padding:10px;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;"><img style="height:100px;" src="${broadcastData.data}"></img></div></div></div>`;}else{chatMessages.innerHTML+=`<div style="padding:10px;display:flex;flex-direction: column;align-items: flex-end;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;"><img style="height:100px;" src="${broadcastData.data}"></img></div></div></div>`;}// 使用 scrollIntoView 方法将底部元素滚动到可见区域chatMessages.lastChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });}function appendLink(broadcastData,dock){var chatMessages = document.getElementById('chatMessages');if(dock!='right'){chatMessages.innerHTML+=`<div style="padding:10px;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;"><div style="display:grid;grid-template:2fr 1fr/1fr/auto;row-gap:5px;"><div style="grid-area:1/1/2/2;font-size:18px;max-width:300px;padding:0 5px;">${broadcastData.data.fileName}</div>    <div style="grid-area:2/1/3/2;font-size:12px;padding:0 5px;">${broadcastData.data.fileSize}字节</div>   <div style="grid-area:1/2/3/3;display:flex;align-items:center;padding:0 5px;background-color:lightblue;cursor:pointer;"><div style="display:inline-block;" onclick="downloadLink('${broadcastData.data.fileName}','${broadcastData.data.id}',${broadcastData.data.fileSize})">下载⬇</div></div>   </div></div></div></div>`;}else{chatMessages.innerHTML+=`<div style="padding:10px;display:flex;flex-direction: column;align-items: flex-end;"><div>${broadcastData.visitor}</div><div style="padding:0 50px;">                    <div style="display:inline-block;padding:5px;border-radius:5px;background-color:#ffffff;"><div style="display:grid;grid-template:2fr 1fr/1fr/auto;row-gap:5px;"><div style="grid-area:1/1/2/2;font-size:18px;max-width:300px;padding:0 5px;">${broadcastData.data.fileName}</div>    <div style="grid-area:2/1/3/2;font-size:12px;padding:0 5px;">${broadcastData.data.fileSize}字节</div>   <div style="grid-area:1/2/3/3;display:flex;align-items:center;padding:0 5px;background-color:lightgreen;"><div style="display:inline-block;">上传</div></div> </div></div></div></div>`;}// 使用 scrollIntoView 方法将底部元素滚动到可见区域chatMessages.lastChild.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });}function getMIME(params) {switch (params) {case 'jpg':return 'image/jpeg';case 'jpeg':return 'image/jpeg';case 'png':return 'image/png';default:break;}}function createDataURL(extension,buffer){// 将 ArrayBuffer 包装成 Blob 对象var MIME = getMIME(extension)const blob = new Blob([buffer], { type: MIME });// 使用 URL.createObjectURL() 创建 Blob 对象的 URLconst url = URL.createObjectURL(blob);return url;}</script><script>class BroadcastType{static message=new Uint8Array([0])[0]static file=new Uint8Array([1])[0]static link=new Uint8Array([2])[0]static image=new Uint8Array([3])[0]}class BroadcastData{visitor;type;data;static Serialize(broadcast){var writer=new BinaryWriter();writer.write(new Uint8Array([0]));writer.write(new Uint8Array([broadcast.type]));                writer.writeInt32(broadcast.data.byteLength);writer.write(new Uint8Array(broadcast.data));return writer.toArray();}static Deserialize(buffer){var broadcastData=new BroadcastData();var reader=new BinaryReader(buffer);var visitorLength=reader.readByte();var visitorBytes = reader.readBytes(visitorLength);broadcastData.visitor = new TextDecoder().decode(visitorBytes);broadcastData.type=reader.readByte();var dataLength=reader.readInt32(4);broadcastData.data = reader.readBytes(dataLength);return broadcastData;}}class RCP{static Message(message){var broadcastData=new BroadcastData();var coder=new TextEncoder();broadcastData.type=BroadcastType.message;var data=coder.encode(message);broadcastData.data=data;return broadcastData;}static MessageResolve(broadcastData){return new TextDecoder().decode(broadcastData.data);}static Image(imageName,imageBuffer){var data = new BroadcastData();data.type=BroadcastType.image;var imageNameLength=new TextEncoder().encode(imageName).length;var writer=new BinaryWriter();writer.write(new Uint8Array([imageNameLength]));writer.write(new TextEncoder().encode(imageName));writer.writeInt32(imageBuffer.byteLength);writer.write(new Uint8Array(imageBuffer));data.data = writer.toArray();return data;}static ImageResolve(broadcastData){var data=broadcastData.dataif(broadcastData.data instanceof Uint8Array)data=data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);var reader=new BinaryReader(data);var imageNameLength=reader.readByte();var coder=new TextDecoder();var imageExtensionName=coder.decode(reader.readBytes(imageNameLength));var imageLength=reader.readInt32();var buffer=reader.readBytes(imageLength);return {imageName:imageExtensionName,buffer:buffer};}static File(fileName,fileBuffer){var data = new BroadcastData();data.type=BroadcastType.file;var fileNameLength=new TextEncoder().encode(fileName).length;var writer=new BinaryWriter();writer.write(new Uint8Array([fileNameLength]));writer.write(new TextEncoder().encode(fileName));writer.writeInt32(fileBuffer.byteLength);var uuid=this.#generateUUID();var uint8uuid=this.#asciiToUint8Array(uuid);writer.write(uint8uuid);writer.write(new Uint8Array(fileBuffer));data.data = writer.toArray();return data;}static FileResolve(broadcastData){var data=broadcastData.dataif(broadcastData.data instanceof Uint8Array)data=data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);var reader=new BinaryReader(data);var fileNameLength=reader.readByte();var coder=new TextDecoder();var fileExtensionName=coder.decode(reader.readBytes(fileNameLength));var extension=fileExtensionName.split('.')[fileExtensionName.split('.').length-1];var fileLength=reader.readInt32();var linkbyte=reader.readBytes(32);var link=this.#uint8ArrayToAscii(linkbyte);var buffer=reader.readBytes(fileLength);return {fileName:fileExtensionName.replace(`.${extension}`,''),extension:extension,id:link,buffer:buffer}}static Link(fileName,id,fileSize){var data = new BroadcastData();data.type=BroadcastType.link;var fileNameLength=new TextEncoder().encode(fileName).length;var writer=new BinaryWriter();writer.write(new Uint8Array([fileNameLength]));writer.write(new TextEncoder().encode(fileName));writer.writeInt32(fileSize);var uint8uuid=this.#asciiToUint8Array(id);writer.write(uint8uuid);data.data = writer.toArray();return data;}static LinkResolve(broadcastData){var data=broadcastData.dataif(broadcastData.data instanceof Uint8Array)data=data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);var reader=new BinaryReader(data);var fileNameLength=reader.readByte();var coder=new TextDecoder();var fileExtensionName=coder.decode(reader.readBytes(fileNameLength));var fileLength=reader.readInt32();var linkbyte=reader.readBytes(32);var link=this.#uint8ArrayToAscii(linkbyte);return {fileName:fileExtensionName,id:link,fileSize:fileLength};}//工具函数static #generateUUID() {// 生成随机的 UUIDconst uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {const r = Math.random() * 16 | 0;const v = c === 'x' ? r : (r & 0x3 | 0x8);return v.toString(16);});return uuid.replace(/-/g, ''); // 移除横线,得到 32 位的 UUID}static #asciiToUint8Array(str) {const uint8Array = new Uint8Array(str.length);for (let i = 0; i < str.length; i++) {uint8Array[i] = str.charCodeAt(i);}return uint8Array;}static #uint8ArrayToAscii(uint8Array) {let asciiString = '';for (let i = 0; i < uint8Array.length; i++) {asciiString += String.fromCharCode(uint8Array[i]);}return asciiString;}}class BinaryReader {#position;#buffer;#dataView;constructor(arrayBuffer) {this.#buffer = arrayBuffer;this.#position = 0;this.#dataView=new DataView(arrayBuffer);}readByte() {var value=this.#dataView.getInt8(this.#position,true);this.#position+=1;return value;}readBytes(length) {var bytes = new Uint8Array(this.#buffer, this.#position, length);this.#position += length;return bytes;}readInt32(){var value=this.#dataView.getInt32(this.#position,true);this.#position+=4;return value;}}class BinaryWriter {#data;constructor() {this.#data = [];}// 向流中添加数据write(chunk) {for (let i = 0; i < chunk.byteLength; i++) {this.#data.push(chunk[i]);}}// 将收集到的数据转换为 ArrayBuffertoArray() {const buffer = new ArrayBuffer(this.#data.length);const view = new Uint8Array(buffer);for (let i = 0; i < this.#data.length; i++) {view[i] = this.#data[i];}return buffer;}writeInt32(number){// 创建一个 ArrayBuffer,大小为 4 字节const buffer = new ArrayBuffer(4);// 创建一个 DataView,用于操作 ArrayBufferconst dataView = new DataView(buffer);// 将一个数值写入到 DataView 中dataView.setInt32(0, number, true); // 第二个参数表示字节偏移量,第三个参数表示是否使用小端序(true 表示使用)// 创建一个 Uint8Array,从 ArrayBuffer 中获取数据const uint8Array = new Uint8Array(buffer);this.write(uint8Array);}}</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/708885.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式Linux中的LED驱动控制(以野火STM32MP157开发板为例)

在嵌入式Linux系统中,由于从硬件到软件都是自己定制的,所以很多时候需要对自己定义的设备编写驱动程序。本例就以野火STM32MP157开发板为例,讨论如何控制开发板上三个LED的亮灭。 先来看一下LED部分的电路原理图,如下所示。从上图中可以看到,三个RGB颜色的二极管采用共阳接…

BUUctf xor

0x01 关于xor xor,即为计算机中的异或计算,相同为0,不同为1。 下面是关于异或加密的四个定理A ^ 0 = A A ^ A = 0 (A ^ B) ^ C = A ^ (B ^ C) (B ^ A) ^ A = B ^ 0 = B // 明文 B;密码 A观察可知,经历异或加密后的密文,再次进行异或算法即可得到明文。 0x02 题解 先丢进…

解决VSCode中Debug和运行路径不一致的

哈喽,大家好,我是木头左!背景介绍 在Visual Studio Code(简称VSCode)中进行开发时,经常需要使用到调试(Debug)功能。然而,有时候会发现,当尝试调试程序时,程序的运行路径与预期不符。这通常会导致程序无法正确读取文件或访问资源,从而影响调试过程。为了解决这个问…

DockerDesktop安装指南以及Windows下WSL2和 Hyper-V相关问题追查

文章原创不易,转载请注明来源 ,谢谢! 一、 问题 周末在家,给自己的老的台式机安装DockerDesktop。 电脑配置是处理器 Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz 3.30 GHz机带 RAM 16.0 GB (15.9 GB 可用)系统类型 64 位操作系统, 基于 x64 的处理器版本 Windows 10 专业版…

软件设计原则—接口隔离原则

B类需要方法1好处是b类继承A类后就有了方法1的功能,问题是B类被迫有了它不使用的方法2 这个其实是根据方法的职责细分接口,只需要依赖其中一个接口就可以了客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。 下面看一个例子来理解接口隔…

科翼阁:网赚广告套路千千万,美食沙雕占大半

在网赚的世界里,有70%是新手,20%是刚上手不久的,但这类人能坚持在网赚这个行业持续发展的机率只有10%,所以只有10%的能在网赚这个行业中进行持久战的,也有只这类人能真正在网赚中赚到大钱的人,网赚需要坚持,中途退出的人绝对无法在网赚这行中得到赢利。在国外,由于网赚…

关于 双向不循环列表的创建、插入、删除、遍历、检索、销毁

双向循环链表公式双向不循环链表代码 #include <stdio.h> #include <stdlib.h> #include <string.h>//宏定义一个数据域 #define DATA_LEN 60//双向不循环链表的节点定义 typedef struct double_link_list {//数据域char data[DATA_LEN]; // 数据…

软件设计原则—依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。 简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。下面看一个例子来理解依赖倒转原则:组装电脑 现要组装一台电脑,需要配件cpu,硬盘,…

关于没有熔断降级导致服务重启问题

场景 1.k8s微服务触发重启 容器配置的健康检查采用actuator curl 127.0.0.1:8080/actuator/health 2.容器重启钩子回调curl -X POST http://127.0.0.1:8080/actuator/shutdown 最终原因是因为调用第三方服务,超时设置3秒,重试3次,三方服务挂起导致tomcat连接池占满,健康检查…

stm32 bootloader的app中断向量偏移设置,HAL库

如何设置Hal库的中断向量偏移看前几篇的 stm32f103c8t6 HAL库更改中断向量表(app部分) - 这一切足够了 - 博客园 (cnblogs.com)我这里bootloader的APP开始地址就是0x08006000,中断向量偏移0x00006000 设置完成之后编译mdk,将生成的bin文件使用ymodem写入0x08006000中 这里设…

c语言程序实验————实验报告九

c语言程序实验————实验报告九实验项目名称: 实验报告8 字符串处理函数 实验项目类型:验证性 实验日期:2024 年 5 月 16 日一、实验目的 1.掌握定义函数的方法 2.掌握函数调用、实参与形参的对应关系、参数的传递方式 3.掌握函数的嵌套调用和递归调用的方法 4.掌握全局变…

Flink精确消费一次

在大数据计算里面,计算引擎是处于承上启下的作用,对上承接数据源,对下承接各种各种数据库,比如mysql、oracle。对于任何数据计算来说要想精确消费一次,就需要支持事务或者幂等,我们最常见的支持事务的就是单点的oracle、mysql数据库,那么Flink作为分布式计算引擎,是如何…