一、说明
1、本文实验内容所涉及的开发环境说明:
- win11
- VisualStudio2022(.Net6.0)
- Unity2021.3.40
2、本文参考资料
【1】NativeWebSocketUnity包:
https://github.com/endel/NativeWebSocket
【2】asp.net core架设websocket国外小哥案例:
https://medium.com/bina-nusantara-it-division/implementing-websocket-client-and-server-on-asp-net-core-6-0-c-4fbda11dbceb
【3】感谢自动纺织机:ChatGPT/Claude
二、本文要实现的测试内容
- 1、实现一个ASP.NET CORE服务端,它上面跑着一个websocket服务
- 2、在【控制台应用程序】中与服务器websocket通信
- 3、在【Unity程序】中与服务器websocket通信
三、测试效果截图
1、服务端截图
有一个用户连入时,显示简单信息
2、控制台应用程序截图
连接成功后,提示。
服务器一旦有用户连入,广播用户信息,广播总用户数
3、Unity程序截图
连接成功后,提示。
服务器一旦有用户连入,广播用户信息,广播总用户数
四、服务端创建
1、打开vs2022 -> 【创建新项目】
2、选择【ASP.NET CORE 空】项目模版
3、配置新项目
设置完毕点击【下一步】
4、 其他信息
譬如设置框架
设置完毕,点击【创建】
5、编写服务端逻辑
服务端代码
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;
using System.Text;
var builder = WebApplication.CreateBuilder(args); //
var connections = new List<WebSocket>(); //连接对象列表
//builder.WebHost.UseUrls("http://localhost:6969");
builder.WebHost.UseUrls("http://192.168.0.173:6969");
var app = builder.Build(); //构建web应用程序实例
app.UseWebSockets(); //启用websocket支持
app.Map("/ws", async context => //app.Map() 把某类网络请求与某个处理函数关联起来:当用户请求的是【/ws】时,服务器响应后面的兰姆达方法
{
/**************************
每个ws连接请求发生的时候,服务器都会调用该处理流程
这是一个并发流程,每一个ws请求都会经过它的处理
**************************/
if (context.WebSockets.IsWebSocketRequest)
{
var curName = context.Request.Query["id"];//有用户连入
var id = context.Connection.Id;
var ip = context.Connection.RemoteIpAddress;
Debug.WriteLine($"有用户连入:用户名为[{curName}]【Id= [{context.Connection.Id}] ip = [{context.Connection.RemoteIpAddress}]】");
using var ws = await context.WebSockets.AcceptWebSocketAsync();
connections.Add(ws); //添加连接的用户
//广播信息
await Broadcast($"有用户连入:id = [{id}],ip = [{ip}]");
await Broadcast($"当前连接数量:{connections.Count}");
await ReceiveMessage(ws,
async (result, buffer) =>
{
if (result.MessageType == WebSocketMessageType.Text) //UTF8 文本信息
{
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
await Broadcast(ip + ":" + message);
}
else if (result.MessageType == WebSocketMessageType.Close || ws.State == WebSocketState.Aborted) //连接关闭
{
connections.Remove(ws);
await Broadcast($"{ip}-{id}用户掉线了");
await Broadcast($"当前用户连接数为{connections.Count}");
await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
});
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
});
await app.RunAsync();
//接收消息
async Task ReceiveMessage(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
{
var buffer = new byte[4096];
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
handleMessage(result, buffer);
}
}
//广播消息
async Task Broadcast(string message)
{
var bytes = Encoding.UTF8.GetBytes(message);
foreach (var socket in connections)
{
if (socket.State == WebSocketState.Open)
{
var arraySegment = new ArraySegment<byte>(bytes, 0, bytes.Length);
await socket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
五、控制台客户端创建
1、创建新项目
2、选择【C#控制台应用】
3、配置新项目
4、设置.NET框架
5、编辑客户端逻辑
完整代码
// See https://aka.ms/new-console-template for more information
using System.Net.WebSockets;
using System.Text;
string name;
while (true)
{
Console.Write("请输入名字:");
name = Console.ReadLine();
break;
}
var ws = new ClientWebSocket();
Console.WriteLine("开始连接服务器!");
//await ws.ConnectAsync(new Uri("ws://localhost:6969/ws"),CancellationToken.None);
await ws.ConnectAsync(new Uri("ws://192.168.0.173:6969/ws"), CancellationToken.None);
Console.WriteLine("连接成功!");
var receiveTask = Task.Run(async () =>
{
var buffer = new byte[1024];
while (true)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer),CancellationToken.None);
if(result.MessageType == WebSocketMessageType.Close)
{
break;
}
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine("收到信息:" + message);
}
});
var sendTask = Task.Run(async () =>
{
while (true)
{
var message =Console.ReadLine();
if(message == "exit")
{
break;
}
var bytes = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(new ArraySegment<byte>(bytes),WebSocketMessageType.Text,true,CancellationToken.None);
}
});
//发送或接收
await Task.WhenAny(receiveTask, sendTask);
if (ws.State == WebSocketState.Closed)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
//发送和接收
await Task.WhenAll(receiveTask, sendTask);
六、Unity客户端创建
1、UI设计
2、层级关系
3、行为脚本清单
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using NativeWebSocket;
using System;
using Cysharp.Threading.Tasks;
public class ConnectTest : MonoBehaviour
{
/// <summary>
/// IP地址
/// </summary>
public TMP_InputField ipf_ip;
/// <summary>
/// 端口号
/// </summary>
public TMP_InputField ipf_port;
/// <summary>
/// 连接服务器btn
/// </summary>
public Button btn_connect;
/// <summary>
/// 提示信息
/// </summary>
public TMP_Text text_info;
private WebSocket websocket;
// Start is called before the first frame update
void Start()
{
//连接服务器-按钮-逻辑
btn_connect.onClick.AddListener(async () =>
{
var ip = ipf_ip.text;
var port = ipf_port.text;
try
{
websocket = new WebSocket($"ws://{ip}:{port}/ws");
Debug.Log($"开始连接服务——ws://{ip}:{port}/ws");
text_info.text += $"\n 开始连接服务——ws://{ip}:{port}/ws \n";
//连接打开时...
websocket.OnOpen += () =>
{
text_info.text += "连上服务器\n";
};
//连接出错时...
websocket.OnError += (e) =>
{
text_info.text += $"连接出错:{e}\n";
};
//连接关闭时...
websocket.OnClose += (e) =>
{
text_info.text += $"连接关闭:{e}\n";
};
//收到二进制数据时...
websocket.OnMessage += (bytes) =>
{
var message = System.Text.Encoding.UTF8.GetString(bytes);
text_info.text += $"收到消息: {message} \n";
};
//开始连接
await websocket.Connect();
}
catch (Exception e)
{
Debug.Log($"连接服务器出错:{e.Message}");
text_info.text = $"连接服务器出错:{e.Message}";
}
});
}
void Update()
{
#if !UNITY_WEBGL || UNITY_EDITOR
websocket.DispatchMessageQueue();
#endif
}
}
至此,全部菜上齐!
后面会进行ASP.NET CORE 、websocket原理、应用和代码的讲解