- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
上一篇实现了多聊天室。这一片要继续改进的是实现收发文件,以及图片显示.
websocket本身就是二进制传输。文件刚好也是二进制存储的。 文件本身的传输问题不太,但是需要传输文件元数据,比如文件名和扩展名之类的。这很必要,如果我们想知道怎么展示这个文件的话。比如这个文件是图片还是word?或者是个exe? 有两种解决办法 。
第一种方法很简单,只是服务器至少要接受两次消息,才能完成一个文件发送。第二种方法则能通过一次消息发送传输文件。 我采用第二种方法.
在引入文件传输的要求后,我发现简单的文本传输也不能满足了,而是需要商定好的格式化的文本,比如json文本。 要不然客户端怎么知道是要显示一个文件下载链接而不是是普通消息文本?这就需要一个type指定。 由于图片是直接显示,文件是下载。客户端收到的又只是一个字节流,客户端怎么知道对应动作? 所以最好统一使用websocket二进制传输作为聊天室数据传输的方式。 这就需要一个简单的协议了.
message
image
。file
。roomChatProtocal
,简称RCP
。发布者 | 类型 | 数据 | |
---|---|---|---|
字段 | visitor | type | data |
类型 | string | message,file,link,image | object |
发布者长度 | 发布者 | 类型 | 数据长度 | 数据 | |
---|---|---|---|---|---|
字节流 | 1 byte | n byte | 1 byte | 4 byte | m byte |
在程序中需要一个对象承载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还应该有一个对应的消息格式。data字段应遵循这种格式 。
消息长度 | 消息 |
---|---|
4 byte | n byte |
文件名长度 | 文件名 | 文件长度 | 文件链接 | 文件内容 |
---|---|---|---|---|
1 byte | n byte | 4 byte | 32 byte | m byte |
文件名长度 | 文件名 | 文件长度 | 文件链接 | 文件内容 |
---|---|---|---|---|
0x08 | 0x62 6f 6f 6d 2e 70 6e 67 | 0x00 01 90 00 | 32 byte | 102400 byte |
文件名长度 | 文件名 | 文件大小 | 文件链接 |
---|---|---|---|
1 byte | n byte | 4 byte | 32 byte |
图片名长度 | 图片名 | 图片长度 | 图片 |
---|---|---|---|
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){}
/// <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
}
/// <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客户端。也实现了RCP.
<!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聊天室(四)的文章就讲到这里了,如果你想了解更多关于基于webapi的websocket聊天室(四)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
假设浏览器被强制终止,并且没有关闭消息发送到 Tornado 服务器。 Tornado 怎么知道(或者它甚至知道?)这个连接已经在客户端终止了?翻看Tornado websocket code这对我来
我目前正在开发一款使用 WebSockets 的基于浏览器的多人游戏。我的首要任务是低延迟以及与各种平台和网络设置的兼容性。 但是我正在做密码验证。我还有聊天功能,我认为玩家的隐私很重要。因此,我认为
我必须设计一个解决方案,允许通过远程托管的网络应用程序读取本地传感器生成的实时数据。 设计仍在进行中:传感器的数据可以由安装在客户端计算机上的 Windows 应用程序/服务处理,或者由位于客户端计算
WebSocket的端口是什么? WebSocket 使用什么协议(protocol)? 当防火墙阻止除 80 和 443 端口之外的所有端口时,WebSocket 是否工作? 最佳答案 What i
有一个 fantastic answer其中详细介绍了 REST apis 的工作原理。 websockets 如何以类似的细节工作? 最佳答案 Websocket 创建并代表了服务器和客户端之间双向
请原谅我的无知,因为我在负载均衡器和 websockets 方面的经验有限。我试图了解客户端如何通过 websockets 连接到位于负载均衡器后面的服务器集群。 我对负载均衡器的理解是它们就像反向代
我正在尝试使用 websocket 发送音频消息,我应该将音频流更改为什么类型的消息,以便我可以使用套接字发送? 如果我直接使用 websocket.send(audio),我会得到一个错误“DOME
我对 WebSockets 的前景感到非常兴奋。由于我在过去构建了一些基于桌面套接字的游戏和 Web 游戏,因此我热衷于将这两种方法结合起来构建基于 Web 的多人游戏,而无需长时间轮询。 自从 Fi
我读过很多关于实时推送通知的文章。并且简历是 websocket 通常是首选技术,只要您不关心 100% 的浏览器兼容性。然而,one article指出 Long polling - potenti
我很难找到文档或教程,以便通过网络套接字发送文件。 这是我的JS: ['dragleave', 'drop'].forEach(event_name => document.addEventListe
我正在使用 Dart 的 WebSocket 类(dart:io 和 dart:html 版本)连接到 Dart WebSocket 服务器。当我让客户端使用自定义关闭代码和原因关闭 Web 套接字连
谷歌浏览器框架是否支持 websocket? 最佳答案 答案是肯定的,Chrome Frame supports websockets .我不确定,但这也可能取决于您安装的 Chrome 版本。我有
是否可以在同一应用程序(同一端口)中托管一个普通 Bottle 应用程序和一个 WebSocket 应用程序(例如: https://github.com/defnull/bottle/blob/ma
我有一个支持网络套接字的服务器。浏览器连接到我的网站,每个浏览器都会打开一个到 www.mydomain.example 的 Web 套接字。这样,我的社交网络应用程序就可以向客户端推送消息。 传统上
我是 Websockets 新手。在阅读有关 websockets 的内容时,我无法找到一些疑问的答案。我希望有人能澄清一下。 websocket 是否仅将数据广播到所有连接的客户端而不是发送到特定客
客户端可以通过 websockets 连接到服务器多长时间?是否有时间限制,它们是否有可能连在一起多年? 最佳答案 理论上,WebSocket 连接可以永远持续下去。假设端点保持正常运行,长期存在的
我正在尝试使用 websockets 制作自己的聊天客户端,并认为我会从 Tomcat 7 websocket chat example code. 开始。 .我已经成功编译并部署了ChatAnnot
我有一个使用 AdSense 的应用程序,据我所知,由于 AdSense 政策,不允许进行轮询。我想知道如果我使用 WebSockets 并且服务器在创建新数据时向客户端发送新数据并且在客户端上显示新
在 servlet 世界中,我会使用 cookie 和 HttpSession 之类的东西来识别谁在访问我的 Restful 服务以将请求路由到正确的数据。将 Sec-WebSocket-Key 用作
我必须使用 websocket 实现一个聊天应用程序,用户将通过群组聊天,可以有数千个群组,并且一个用户可以在多个群组中。我正在考虑两种解决方案: [1] 对于每个群聊,我创建一个 websocket
我是一名优秀的程序员,十分优秀!