基于webapi的websocket聊天室(四)
上一篇实现了多聊天室。这一片要继续改进的是实现收发文件,以及图片显示。
效果
问题
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.cs
public 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;
}
}
主要就是进行了消息的解析,以及调用了RCP
中type
的的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=='')return
if(!isopen)return
if(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 对象的 URL
const 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.data
if(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.data
if(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.data
if(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() {
// 生成随机的 UUID
const 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]);
}
}
// 将收集到的数据转换为 ArrayBuffer
toArray() {
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,用于操作 ArrayBuffer
const dataView = new DataView(buffer);
// 将一个数值写入到 DataView 中
dataView.setInt32(0, number, true); // 第二个参数表示字节偏移量,第三个参数表示是否使用小端序(true 表示使用)
// 创建一个 Uint8Array,从 ArrayBuffer 中获取数据
const uint8Array = new Uint8Array(buffer);
this.write(uint8Array);
}
}
</script>
</body>
</html>
基于webapi的websocket聊天室(四)的更多相关文章
- 基于springboot的websocket聊天室
WebSocket入门 1.概述 1.1 Http #http简介 HTTP是一个应用层协议,无状态的,端口号为80.主要的版本有1.0/1.1/2.0. #http1.0/1.1/2.0 1.HTT ...
- 基于flask的网页聊天室(四)
基于flask的网页聊天室(四) 前言 接前天的内容,今天完成了消息的处理 具体内容 上次使用了flask_login做用户登录,但是直接访问login_requare装饰的函数会报401错误,这里可 ...
- 使用.NET Core和Vue搭建WebSocket聊天室
博客地址是:https://qinyuanpei.github.io. WebSocket是HTML5标准中的一部分,从Socket这个字眼我们就可以知道,这是一种网络通信协议.WebSocket是 ...
- websocket聊天室
目录 websocket方法总结 群聊功能 基于websocket聊天室(版本一) websocket方法总结 # 后端 3个 class ChatConsumer(WebsocketConsumer ...
- WebSocket聊天室demo
根据Socket异步聊天室修改成WebSocket聊天室 WebSocket特别的地方是 握手和消息内容的编码.解码(添加了ServerHelper协助处理) ServerHelper: using ...
- Netty入门(一)之webSocket聊天室
一:简介 Netty 是一个提供 asynchronous event-driven (异步事件驱动)的网络应用框架,是一个用以快速开发高性能.高可靠性协议的服务器和客户端. 换句话说,Netty 是 ...
- 基于flask的网页聊天室(三)
基于flask的网页聊天室(三) 前言 继续上一次的内容,今天完成了csrf防御的添加,用户头像的存储以及用户的登录状态 具体内容 首先是添加csrf的防御,为整个app添加防御: from flas ...
- 基于flask的网页聊天室(二)
基于flask的网页聊天室(二) 前言 接上一次的内容继续完善,今天完成的内容不是很多,只是简单的用户注册登录,内容具体如下 具体内容 这次要加入与数据哭交互的操作,所以首先要建立相关表结构,这里使用 ...
- 基于flask的网页聊天室(一)
基于flask的网页聊天室(一) 基本目标 基于flask实现的web聊天室,具有基本的登录注册,多人发送消息,接受消息 扩展目标 除基本目标外添加当前在线人数,消息回复,markdown支持,历史消 ...
- 用Java构建一个简单的WebSocket聊天室
前言 首先对于一个简单的聊天室,大家应该都有一定的概念了,这里我们省略用户模块的讲解,而是单纯的先说说聊天室的几个功能:自我对话.好友交流.群聊.离线消息等. 今天我们要做的demo就能帮我们做到这一 ...
随机推荐
- DOM(文档对象模型):理解网页结构与内容操作的关键技术
DOM(文档对象模型)定义了一种访问和操作文档的标准.它是一个平台和语言无关的接口,允许程序和脚本动态访问和更新文档的内容.结构和样式.HTML DOM用于操作HTML文档,而XML DOM用于操作X ...
- 开发指导—利用CSS动画实现HarmonyOS动效(二)
注:本文内容分享转载自HarmonyOS Developer官网文档 点击查看<开发指导-利用CSS动画实现HarmonyOS动效(一)> 3. background-position ...
- 如何将 ASP.NET Core MVC 项目的视图分离到另一个项目
如何将 ASP.NET Core MVC 项目的视图分离到另一个项目 在当下这个年代 SPA 已是主流,人们早已忘记了 MVC 以及 Razor 的故事.但是在某些场景下 SSR 还是有意想不到效果. ...
- 力扣1337(java&python)-矩阵中战斗力最弱的 K 行(简单)
题目: 给你一个大小为 m * n 的矩阵 mat,矩阵由若干军人和平民组成,分别用 1 和 0 表示. 请你返回矩阵中战斗力最弱的 k 行的索引,按从最弱到最强排序. 如果第 i 行的军人数量少于第 ...
- 阿里云数据库开源重磅发布:PolarDB三节点高可用的功能特性和关键技术
简介:在3月2日的阿里云开源 PolarDB 企业级架构发布会上,阿里云数据库技术专家孟勃荣 带来了主题为<PolarDB 三节点高可用>的精彩演讲.三节点高可用功能主要为 PolarD ...
- 龙蜥利器:系统运维工具 SysAK的云上应用性能诊断 | 龙蜥技术
简介:本文从大量的性能诊断实践出发,来介绍 SysAK 在性能诊断上的方法论及相关工具. 文/张毅:系统运维SIG核心成员.SysAK 项目负责人:毛文安:系统运维 SIG 负责人. 系统运维既 ...
- [FAQ] Windows 终端 `git diff` 出现 LF 空格 ^M 符号, 处理方式
可能是终端内的换行配置和 IDE 当中的不一致. 比如 PHPStorm 的: Git 终端使用 git config core.autocrlf 查看是 true 还是 false. 是 tru ...
- [FAQ] Argument 3 passed to Lcobucci\JWT\Signer\Hmac::doVerify() must be an instance of Lcobucci\JWT\Signer\Key, null given
出现这个错误,说明没有找到 key,在使用 laravel-jwt 之前需要生成加密 key,使用: $ php artisan jwt:secret Link:https://www.cnblogs ...
- Echarts立体地图加3D柱图可点击可高亮选中的开发
注意 echarts请使用v5.1.0以上版本,低版本会无法显示,或者无法触发点击事件. 若有闪屏bug,不要设置temporalSuperSampling属性. 注意图层顺序. 实现原理 借助 ec ...
- kafka connect gui 可视化管理工具
kafka connect gui 可视化管理工具 官网地址:http://www.redisant.cn/ka 连接到 Kafka Connect 支持各种认证方式,支持 SSL/TLS 安全连接 ...