Unity组件开发--长连接webSocket
1.下载安装UnityWebSocket 插件
https://gitee.com/cambright/UnityWebSocket/

引入unity项目:

2.定义消息体结构:ExternalMessage和包结构Package:
using ProtoBuf;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace UTNET
{[ProtoContract]public class ExternalMessage{[ProtoMember(1)]// 请求命令类型: 0 心跳,1 业务public int cmdCode;// 协议开关,用于一些协议级别的开关控制,比如 安全加密校验等。 : 0 不校验[ProtoMember(2)]public int protocolSwitch;// 业务路由(高16为主, 低16为子)[ProtoMember(3)]public uint cmdMerge;// 响应码: 0:成功, 其他为有错误[ProtoMember(4,DataFormat = DataFormat.ZigZag)]public int responseStatus;// 验证信息(错误消息、异常消息),通常情况下 responseStatus == -1001 时, 会有值[ProtoMember(5)]public string validMsg;// 业务请求数据[ProtoMember(6)]public byte[] data;// 消息标记号;由前端请求时设置,服务器响应时会携带上;[ProtoMember(7, DataFormat = DataFormat.ZigZag)]public int msgId;}[ProtoContract]public class Package{[ProtoMember(1)]public uint packageType;[ProtoMember(2)]public string route = null;[ProtoMember(3)]public uint packID = 0;[ProtoMember(4)]public byte[] buff = null;[ProtoMember(5)]public string modelName = null;}//[ProtoContract]//public class Message<T>//{// [ProtoMember(1)]// public uint err;// [ProtoMember(2)]// public string errMsg = default;// [ProtoMember(3)]// public T data = default;// public Message() { }// public Message(uint err, string errMsg, T info)// {// this.err = err;// this.errMsg = errMsg;// this.data = info;// }//}[ProtoContract]public class HandShake{[ProtoMember(1)]public string token;}[ProtoContract]public class Heartbeat{[ProtoMember(1)]public uint heartbeat;}
}
3.定义包协议结构:PackageProtocol
using ProtoBuf;
using System;
using System.IO;
using System.Text;
using UnityEngine.XR;namespace UTNET
{public enum PackageType{HEARTBEAT = 1,REQUEST = 2,PUSH = 3,KICK = 4,RESPONSE = 5,HANDSHAKE = 6,ERROR = 7,NOTIFY = 8}public class PackageProtocol{//获取cmdpublic static uint getCmd(uint merge){return merge >> 16;}//获取subCmdpublic static uint getSubCmd(uint merge){return merge & 0xFFFF;}//获取mergeCmdpublic static uint getMergeCmd(uint cmd, uint subCmd){return (cmd << 16) + subCmd;}public static byte[] Encode(PackageType type){Package sr = new Package(){packageType = (uint)type};return Serialize(sr);}public static byte[] Encode<T>(PackageType type, uint packID, string route, T info, string modelName = null){Package sr = new Package(){packageType = (uint)type,packID = packID,route = route,buff = Serialize<T>(info),modelName = modelName};return Serialize(sr);}public static byte[] EncodeEx<T>(uint packID, uint cmdMerge, T info){//Package sr = new Package()//{// packageType = (uint)type,// packID = packID,// route = route,// buff = Encoding.Default.GetBytes(SerializeEx<T>(info)),//};//return SerializeEx(sr);ExternalMessage sr = new ExternalMessage(){cmdCode = 100,protocolSwitch = 1,cmdMerge = cmdMerge,responseStatus = 0,validMsg = "",data = Encoding.UTF8.GetBytes(SerializeEx<T>(info)),};return Serialize(sr);}public static byte[] Encode<T>(uint packID, uint cmdMerge, T info){ExternalMessage sr = new ExternalMessage(){cmdCode = 100,protocolSwitch = 1,cmdMerge = cmdMerge,responseStatus = 0,validMsg = "",data = Serialize<T>(info),msgId = (int)packID,};return Serialize(sr);}public static string SerializeEx<T>(T t){using (MemoryStream ms = new MemoryStream()){Serializer.Serialize<T>(ms, t);return Encoding.UTF8.GetString(ms.ToArray());}}public static byte[] Serialize<T>(T info){if (typeof(T) == typeof(int)){int value = (int)Convert.ChangeType(info, typeof(int));value = (value << 1);info = (T)Convert.ChangeType(info, typeof(T));}MemoryStream ms = new MemoryStream();Serializer.Serialize(ms, info);byte[] buff = ms.ToArray();ms.Close();return buff;//try//{// //涉及格式转换,需要用到流,将二进制序列化到流中// using (MemoryStream ms = new MemoryStream())// {// //使用ProtoBuf工具的序列化方法// Serializer.Serialize<T>(ms, info);// //定义二级制数组,保存序列化后的结果// byte[] result = new byte[ms.Length];// //将流的位置设为0,起始点// ms.Position = 0;// //将流中的内容读取到二进制数组中// ms.Read(result, 0, result.Length);// return result;// }//}//catch (Exception ex)//{// return null;//}}public static ExternalMessage Decode(byte[] buff){//protobuf反序列化MemoryStream mem = new MemoryStream(buff);ExternalMessage rs = Serializer.Deserialize<ExternalMessage>(mem);mem.Close();return rs;}public static T DecodeInfo<T>(byte[] buff){if (buff == null) return default;T rs;if (typeof(T) == typeof(int)){int value;using (var stream = new MemoryStream(buff)){value = Serializer.Deserialize<int>(stream);}//转zig zagvalue = (value >> 1) ^ -(value & 1);rs = (T)Convert.ChangeType(value, typeof(T));}else{MemoryStream mem = new MemoryStream(buff);rs = Serializer.Deserialize<T>(mem);mem.Close();}return rs;}//public static T MyMethod<T>(byte[] buff) where T : Object//{// return null;//}}
}
4.引入UniTask插件:UniTask中文使用指南(一) - 知乎
UniTask保姆级教程_unitask安装-CSDN博客
项目地址:GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

5.定义协议索引管理,添加长连接的协议:ProtoMaps
using System;
using System.Collections.Generic;namespace UTNET
{public interface IProto{}//R ��������, S ��������//public class Proto<R, S> : IProtopublic class Proto : IProto{public string name; //Э������public uint mid { get; set; } //��idpublic uint sid { get; set; } //��idpublic uint mergeid;public int typ; //Э������//public R r;//public S s;public Proto(string name, uint mid, uint sid, int typ){this.name = name;this.mid = mid;this.sid = sid;this.typ = typ;}}public class ProtoMaps{private static readonly ProtoMaps instance = new ProtoMaps();public static ProtoMaps Instance{get { return instance; }}//public Dictionary<string, IProto> protos = new Dictionary<string, IProto>();public Dictionary<string, Proto> protos = new Dictionary<string, Proto>();public Dictionary<uint, Proto> id2proto = new Dictionary<uint, Proto>();public void Add(string name, Proto pb){protos.Add(name, pb);}public void SortById(){foreach (var item in protos){var pb = item.Value;uint mergeid = PackageProtocol.getMergeCmd(pb.mid, pb.sid);pb.mergeid = mergeid;id2proto.Add(mergeid, pb);}}public ProtoMaps(){this.Add("login", new Proto("login", 1, 2, 1));this.Add("registerInfo", new Proto("registerInfo", 1, 1, 1));this.Add("enterRoom", new Proto("enterRoom", 1, 13, 3));this.Add("roleMove", new Proto("roleMove", 4, 3, 3));//this.Add("onNewRoleEnter", new Proto("onNewRoleEnter", 7, 1, 2));//服务器主动广播的角色退出this.Add("roleExitRoom", new Proto("roleExitRoom", 7, 2, 2));this.Add("talkMrg", new Proto("talkMrg", 1, 9, 3));//玩家点击的角色退出this.Add("playerExitRoom", new Proto("playerExitRoom", 1, 5, 3));this.Add("gameFrame", new Proto("gameFrame", 4, 2, 1));this.Add("gameObjUsed", new Proto("gameObjUsed", 4, 4, 3));this.Add("getOtherInfo", new Proto("getOtherInfo", 1, 23, 1));this.Add("heatBeat", new Proto("heatBeat", 1, 120, 1));SortById();}public Proto Name2Pb(string name){//Proto pb = ProtoMaps.Instance.protos[name];if (protos.ContainsKey(name)){return protos[name];}return null;}internal Proto GetByMergeId(uint cmdMerge){if (id2proto.ContainsKey(cmdMerge)){return id2proto[cmdMerge];}return null;}}}
6.定义心跳协议类:HeartBeatServiceGameObject
using System;
using UnityEngine;
using UnityWebSocket;
namespace UTNET
{public class HeartBeatServiceGameObject : MonoBehaviour{public Action OnServerTimeout;private WebSocket socket;public float interval = 0;public long lastReceiveHeartbeatTime;void Start(){}static DateTime dt = new DateTime(1970, 1, 1);public static long GetTimestamp(){TimeSpan ts = DateTime.Now.ToUniversalTime() - dt;return (long)ts.TotalSeconds;}public float t;void Update(){t += Time.deltaTime;if (t > interval){CheckAndSendHearbeat();t = 0;}}private void CheckAndSendHearbeat(){//檢查最後一次取得心跳包的時間是否小於客戶端心跳間隔時間long curTime = GetTimestamp();long intervalSec = curTime - lastReceiveHeartbeatTime;if (intervalSec > interval){//Debug.Log(string.Format("XXXX CheckAndSendHearbeat:s1:{0} l:{1} s:{2}", curTime, lastReceiveHeartbeatTime, intervalSec));this.enabled = false;OnServerTimeout?.Invoke();}else{//Debug.Log(string.Format(" CheckAndSendHearbeat:s1:{0} l:{1} s:{2}", curTime, lastReceiveHeartbeatTime, intervalSec));this.enabled = true;SendHeartbeatPack();}}public void HitHole(){lastReceiveHeartbeatTime = GetTimestamp();}private void SendHeartbeatPack(){//lastSendHeartbeatPackTime = DateTime.Now;byte[] package = PackageProtocol.Encode(PackageType.HEARTBEAT);socket.SendAsync(package);//*/}internal void Setup(uint interval, Action onServerTimeout, WebSocket socket){this.socket = socket;this.interval = (interval / 1000 )/2;this.OnServerTimeout = onServerTimeout;this.enabled = true;SendHeartbeatPack();}internal void ResetTimeout(uint interval){this.enabled = true;this.interval = (interval / 1000) / 2;t = 0;//long s1 = GetTimestamp();//long s = (s1 - lastReceiveHeartbeatTime);//Debug.Log(string.Format("ResetTimeout: s1:{0} l:{1} s:{2} s > interval:{3}", s1, lastReceiveHeartbeatTime, s, s > interval));lastReceiveHeartbeatTime = GetTimestamp();SendHeartbeatPack();}internal void Stop(){this.enabled = false;t = 0;}}
}
7.定义协议类Protocol:
using Cysharp.Threading.Tasks;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityWebSocket;namespace UTNET
{public class Protocol{Dictionary<uint, Action<ExternalMessage>> packAction = new Dictionary<uint, Action<ExternalMessage>>();UniTaskCompletionSource<bool> handshakeTcs;Dictionary<uint, UniTaskCompletionSource<ExternalMessage>> packTcs = new Dictionary<uint, UniTaskCompletionSource<ExternalMessage>>();//Dictionary<uint, Action<T>> attention = new Dictionary<uint, Action<T>>();WebSocket socket;public HeartBeatServiceGameObject heartBeatServiceGo;public Action OnReconected;public Action<string> OnError;public void SetSocket(WebSocket socket){this.socket = socket;}public UniTask<bool> HandsharkAsync(string token){handshakeTcs = new UniTaskCompletionSource<bool>();return handshakeTcs.Task;}internal void Notify<T>(string route, T info){byte[] packBuff = PackageProtocol.Encode<T>(PackageType.NOTIFY,0,route,info);socket.SendAsync(packBuff);}public UniTask<ExternalMessage> RequestAsync<T>(uint packID, uint route, T info = default, string modelName = null){lock (packTcs){UniTaskCompletionSource<ExternalMessage> pack = new UniTaskCompletionSource<ExternalMessage>();byte[] packBuff = PackageProtocol.Encode<T>(packID, route, info);packTcs.Add(packID, pack);//packTcs.Add(route, pack); //暂不支持,同一协议多次发送//byte[] ints = System.BitConverter.GetBytes(IPAddress.HostToNetworkOrder(testInt));//ByteBuffer bbBuffer = ByteBuffer.wrap(bs);//bbBuffer.order(ByteOrder.BIG_ENDIAN);socket.SendAsync(packBuff);return pack.Task;}}public void CanceledAllUTcs(){lock (packTcs){foreach (var tcs in packTcs){tcs.Value.TrySetCanceled();}packTcs.Clear();if(handshakeTcs!=null) handshakeTcs.TrySetCanceled();}}public async void OnReceive(byte[] bytes){try{await UniTask.SwitchToMainThread();ExternalMessage package = PackageProtocol.Decode(bytes);uint mId = PackageProtocol.getCmd(package.cmdMerge);uint subId = PackageProtocol.getSubCmd(package.cmdMerge);Proto pb = ProtoMaps.Instance.GetByMergeId(package.cmdMerge);if (package.responseStatus != 0){Debug.LogError("收到 cmd:" + mId + " &subcmd:" + subId);Debug.LogError("收到网络消息 " + package.cmdMerge);Debug.LogError("协议返回错误信息,查询errcode,后面可能根据弹窗处理: Errmsg->" + package.validMsg + " & Errcode->" + package.responseStatus );onShowNetError(package.responseStatus, mId, subId);return;}if (pb == null){Debug.Log("收到未在 ProtoMaps 注册的 网络消息 " + package.cmdCode);return;}if (PackageProtocol.getCmd(package.cmdMerge) == 0){Debug.Log("心跳数据 ");return;}ResponseHandler(package);PushHandler(package);//if (pb.typ == 1)//{//}//else if (pb.typ == 2)//{// PushHandler(package);//}//else//{// if (!ResponseHandler(package))// PushHandler(package);//}//Package package = PackageProtocol.Decode(bytes);//Debug.Log(package.packageType);//switch ((PackageType)package.packageType)//{// case PackageType.HEARTBEAT:// //Debug.LogWarning("get HEARTBEAT");// heartBeatServiceGo.HitHole();// break;// case PackageType.RESPONSE:// ResponseHandler(package);// break;// case PackageType.PUSH:// PushHandler(package);// break;// case PackageType.HANDSHAKE:// HandshakeHandler(package);// break;// case PackageType.KICK:// //HandleKick(package);// break;// case PackageType.ERROR:// ErrorHandler(package);// break;// default:// Debug.Log("No match packageType::" + package.packageType);// break;//}}catch (Exception e){ExternalMessage package = PackageProtocol.Decode(bytes);Proto pb = ProtoMaps.Instance.GetByMergeId(package.cmdMerge);Debug.Log("错误协议-----" + JsonUtility.ToJson(pb));await UniTask.SwitchToMainThread();Debug.LogError(e);throw e;}}public void StopHeartbeat(){if (heartBeatServiceGo != null){Debug.Log("Stop Heartbeat");heartBeatServiceGo.Stop();//heartBeatServiceGo = null;}}public void onShowNetError(int responseStatus,uint mid,uint subid) {}public void SetOnNet(uint route, Action<ExternalMessage> ac){lock (packAction){if (!packAction.ContainsKey(route)){packAction.Add(route, ac);}}}private void PushHandler(ExternalMessage pack){lock (packAction){uint route = pack.cmdMerge;if (packAction.ContainsKey(route)){
#if SOCKET_DEBUGDebug.Log(string.Format("[Push] <<-- [{0}] {1}", pack.route, JsonUtility.ToJson(pack)));
#endifpackAction[route]?.Invoke(pack);//packAction.Remove(route); //监听事件不考虑删除由netmanager代理}}}private bool ResponseHandler(ExternalMessage package){lock (packTcs){uint msgId = (uint)package.msgId;//Debug.LogError("响应消息id" + msgId);if (packTcs.ContainsKey(msgId)){packTcs[msgId].TrySetResult(package);packTcs.Remove(msgId);return true;//if (packTcs.ContainsKey(package.cmdMerge))//{// packTcs.Remove(package.cmdMerge);// return true;//}}}return false;}//private void ResponseHandler(ExternalMessage package)//{// lock (packTcs)// {// packTcs[package.packID].TrySetResult(package);// if (packTcs.ContainsKey(package.packID))// {// packTcs.Remove(package.packID);// }// }//}private void HandshakeHandler(Package package){Heartbeat msg = PackageProtocol.DecodeInfo<Heartbeat>(package.buff);if (msg.heartbeat > 0){handshakeTcs.TrySetResult(false);return;}if (heartBeatServiceGo == null){}else{OnReconected?.Invoke();heartBeatServiceGo.ResetTimeout(msg.heartbeat);}//*/handshakeTcs.TrySetResult(true);}private void ErrorHandler(Package package){}private void OnServerTimeout(){if (socket.ReadyState == WebSocketState.Connecting){socket.CloseAsync();}if (heartBeatServiceGo != null && socket.ReadyState != WebSocketState.Connecting && socket.ReadyState != WebSocketState.Open){heartBeatServiceGo.Stop();}}}
}
8.定义服务器对应的协议消息:TestProto.cs
using ProtoBuf;
using ProtoBuf.Meta;
using System.Collections.Generic;[ProtoContract]
public class RegisterInfo
{[ProtoMember(1)]public string phoneNum;[ProtoMember(2)]public string account;[ProtoMember(3)]public string pwd;
}[ProtoContract]
public class LoginVerify
{[ProtoMember(1)]public string account;[ProtoMember(2)]public string pwd;[ProtoMember(3, DataFormat = DataFormat.ZigZag)]public int loginBizCode;[ProtoMember(4, DataFormat = DataFormat.ZigZag)]public int spaceId;
}[ProtoContract]
public class LobbyTalkMsg
{[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long id;[ProtoMember(2, DataFormat = DataFormat.ZigZag)]public long usrId;[ProtoMember(3)]public string nickName;[ProtoMember(4, DataFormat = DataFormat.ZigZag)]public int to_usrId;[ProtoMember(5)]public string msg;[ProtoMember(6, DataFormat = DataFormat.ZigZag)]public int msg_type;
}[ProtoContract]
public class RequestSuccess
{[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public int success;}[ProtoContract]
public class GameFrame {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long frameId;[ProtoMember(2)]public string opc;[ProtoMember(3)]public string world;[ProtoMember(4)]public TankLocation location;}[ProtoContract]
public class UserInfo {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long id;[ProtoMember(2)]public string nickName;[ProtoMember(3)]public string account;[ProtoMember(4)]public string gender;[ProtoMember(5)]public string headImgUrl;[ProtoMember(6, DataFormat = DataFormat.ZigZag)]public long avatarId;[ProtoMember(7)]public string avatar;[ProtoMember(8)]public string hold;[ProtoMember(9)]public string mtk;[ProtoMember(10)]public string ltk;[ProtoMember(11, DataFormat = DataFormat.ZigZag)]public long roomId;[ProtoMember(12, DataFormat = DataFormat.ZigZag)]public long team;[ProtoMember(13)]public TankLocation tankLocation;[ProtoMember(14, DataFormat = DataFormat.ZigZag)]public int speed;
}[ProtoContract]
public class TankLocation {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public float x;[ProtoMember(2, DataFormat = DataFormat.ZigZag)]public float y;[ProtoMember(3, DataFormat = DataFormat.ZigZag)]public float z;[ProtoMember(4, DataFormat = DataFormat.ZigZag)]public float dx;[ProtoMember(5, DataFormat = DataFormat.ZigZag)]public float dy;[ProtoMember(6, DataFormat = DataFormat.ZigZag)]public float dz;[ProtoMember(7, DataFormat = DataFormat.ZigZag)]public long playerId;[ProtoMember(8, DataFormat = DataFormat.ZigZag)]public long time;}[ProtoContract]
public class TankPlayer {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long id;[ProtoMember(2, DataFormat = DataFormat.ZigZag)]public int speed;[ProtoMember(3)]public TankLocation tankLocation;[ProtoMember(4)]public string nickname;[ProtoMember(5)]public string hold;// 其他属性: key: 子弹 id 1 玩具弹, 2 雪弹; value : 数量[ProtoMember(6)]public Dictionary<int, int> tankBulletMap;
}[ProtoContract]
public class TankEnterRoom {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long roomId;[ProtoMember(2, DataFormat = DataFormat.ZigZag)]public long time;[ProtoMember(3, DataFormat = DataFormat.ZigZag)]public long team;[ProtoMember(4, DataFormat = DataFormat.ZigZag)]public long playerId;[ProtoMember(5)]public string sharetoken;[ProtoMember(6)]public string usertoken;[ProtoMember(7)]public string world;[ProtoMember(8)]public List<UserInfo> tankPlayerList;}[ProtoContract]
public class UserIds {[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public long[] userIds;
}[ProtoContract]
public class UserInfos
{[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public UserInfo[] users;
}[ProtoContract]
public class HeartBeat
{[ProtoMember(1, DataFormat = DataFormat.ZigZag)]public int beat;
}
9.定义客户端处理消息类:Client:
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Threading;
using System.Collections;
using UnityWebSocket;namespace UTNET
{public enum NetWorkState{CONNECTING,CONNECTED,DISCONNECTED,TIMEOUT,ERROR,KICK}public class Client{private static int RqID = 0;//public NetWorkState state;public Action OnReconected, OnDisconnect, OnConnected;public Action<string> OnError;public uint retry;public bool isConnect = false;Protocol protocol;WebSocket socket;UniTaskCompletionSource<bool> utcs;private bool isForce;private bool isHeardbeat = false;private string host;public Client(string host){Debug.Log("Client" + host);this.host = host;this.createSocket();}private void createSocket(){if (socket != null) {}Debug.Log("createSocket host=" + this.host);utcs = new UniTaskCompletionSource<bool>();if (socket != null) {socket.CloseAsync();socket.OnClose += ReCreateSocket;}else {socket = new WebSocket(this.host);socket.OnOpen += OnOpen;socket.OnMessage += OnMessage;socket.OnClose += OnClose;socket.OnError += OnErr;}}private void ReCreateSocket(object sender, CloseEventArgs e) {socket = new WebSocket(this.host);socket.OnOpen += OnOpen;socket.OnMessage += OnMessage;socket.OnClose += OnClose;socket.OnError += OnErr;}//断线重连逻辑public IEnumerator ReconectScoektAsync(){while (true){yield return new WaitForSeconds(2);Debug.Log(" client : reconectScoekt");if (socket.ReadyState == WebSocketState.Connecting || socket.ReadyState == WebSocketState.Open) break;socket.ConnectAsync();}}//开始心跳逻辑public IEnumerator StartHeardBeat(){isHeardbeat = true;while (true){Debug.Log("开始发送心跳");if (isHeardbeat == false) continue;yield return new WaitForSeconds(2);//发送心跳this.sendHeardbeat();}}private async void sendHeardbeat(){int u1 = await this.RequestAsync<int, int>("heartbeat", 21);}public UniTask<bool> ConnectAsync(){socket.ConnectAsync();return utcs.Task;}private void OnOpen(object sender, OpenEventArgs e){if (protocol == null)protocol = new Protocol();protocol.SetSocket(socket);protocol.OnReconected = OnReconected;protocol.OnError = OnError;//bool isOK = await protocol.HandsharkAsync(this.token);Debug.Log("open:" + e);isConnect = true;utcs.TrySetResult(true);OnConnected?.Invoke();}private async void OnClose(object sender, CloseEventArgs e){if (socket.ReadyState == UnityWebSocket.WebSocketState.Connecting || socket.ReadyState == UnityWebSocket.WebSocketState.Open) return;await UniTask.SwitchToMainThread();isConnect = false;Cancel();if (!isForce){await UniTask.Delay(1000);//socket.Open();}OnDisconnect?.Invoke();}public void OnErr(object sender, ErrorEventArgs e){utcs.TrySetResult(false);isConnect = false;//Debug.LogError(e.Exception.Message);}private void OnMessage(object sender, MessageEventArgs e){protocol.OnReceive(e.RawData);isConnect = true;}public void OnNet(uint mid, uint sid, Action<ExternalMessage> cb){uint merge = PackageProtocol.getMergeCmd(mid, sid);protocol.SetOnNet(merge, cb);}public void OnNetEx(string name, Action<ExternalMessage> cb){Proto pb = ProtoMaps.Instance.protos[name] as Proto;uint merge = PackageProtocol.getMergeCmd(pb.mid, pb.sid);protocol.SetOnNet(merge, cb);}public void Notify<T>(string route, T info = default){//uint rqID = (uint)Interlocked.Increment(ref RqID);try{
#if SOCKET_DEBUGDebug.Log(string.Format("[Notify] -->> [{0}] {1}", route, JsonUtility.ToJson(info)));
#endifprotocol.Notify<T>(route, info);}catch (Exception e){Debug.Log(string.Format("[Notify Exception]{0}", e.Message));throw e;}}public async UniTask<S> RequestAsyncEx<T, S>(uint mid, uint sid, T info = default, string modelName = null){uint cmdMerge = PackageProtocol.getMergeCmd(mid, sid);uint rqID = (uint)Interlocked.Increment(ref RqID);try{
#if SOCKET_DEBUGDebug.Log(string.Format("[{0}][Request] -->> [{1}] {2}", rqID, route, JsonUtility.ToJson(info)));
#endifExternalMessage pack = await protocol.RequestAsync<T>(rqID, cmdMerge, info, modelName);S msg = PackageProtocol.DecodeInfo<S>(pack.data);#if SOCKET_DEBUGDebug.Log(string.Format("[{0}][Request] <<-- [{1}] {2} {3}", rqID, route, JsonUtility.ToJson(msg), JsonUtility.ToJson(msg.info)));
#endifreturn msg;}catch (Exception e){//Debug.Log(string.Format("[{0}][RequestAsync Exception]{1}", rqID, e.Message));throw e;}}public async UniTask<S> RequestAsync<T, S>(string name, T info = default, string modelName = null){// #if UNITY_WEBGL && !UNITY_EDITOR //只在WebGl下生效if (PlayerData.Instance.IsEditorMode) {S msg = PackageProtocol.DecodeInfo<S>(null);return msg;}//#endif//Debug.Log($"sendMsg {name}");Proto pb = ProtoMaps.Instance.Name2Pb(name);uint cmdMerge = PackageProtocol.getMergeCmd(pb.mid, pb.sid);uint rqID = (uint)Interlocked.Increment(ref RqID);//Debug.LogError("调试消息id" + rqID);try{
#if SOCKET_DEBUGDebug.Log(string.Format("[{0}][Request] -->> [{1}] {2}", rqID, route, JsonUtility.ToJson(info)));
#endifExternalMessage pack = await protocol.RequestAsync<T>(rqID, cmdMerge, info, modelName);S msg = PackageProtocol.DecodeInfo<S>(pack.data);
#if SOCKET_DEBUGDebug.Log(string.Format("[{0}][Request] <<-- [{1}] {2} {3}", rqID, route, JsonUtility.ToJson(msg), JsonUtility.ToJson(msg.info)));
#endifreturn msg;}catch (Exception e){//Debug.Log(string.Format("[{0}][RequestAsync Exception]{1}", rqID, e.Message));//Debug.Log("房间网络连接断开.......");S msg = PackageProtocol.DecodeInfo<S>(null);return msg;throw e;}}public void Cancel(bool isForce = false){this.isForce = isForce;utcs.TrySetCanceled();if (socket.ReadyState != UnityWebSocket.WebSocketState.Closed){socket.CloseAsync();}if (protocol != null){protocol.StopHeartbeat();protocol.CanceledAllUTcs();}}public UnityWebSocket.WebSocketState getSocketState() {if (socket != null) {return socket.ReadyState;}return 0;}public void Close() {Debug.Log("UnityWebSocket......" + socket);Debug.Log("UnityWebSocket状态......"+ socket.ReadyState);if (socket!=null && socket.ReadyState == UnityWebSocket.WebSocketState.Open) {socket.CloseAsync();Debug.Log("强制玩家关闭连接......");}}void OnDestroy(){socket.CloseAsync(); }}
}
10.最终定义网络管理器:NetManager.cs
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UTNET;public class NetMessage<T> {public uint mergeid;delegate void Message(object sender, T msg);}public class NetManager : MonoBehaviour {private NetManager() { }public static NetManager Instance;public string token, version;public Client client;public bool isConeccet = false;public string HaiMetaSpaceUrl = "*****";//192.144.138.221 /game.icewhale.netDictionary<string, List<Action<ExternalMessage>>> actions = new Dictionary<string, List<Action<ExternalMessage>>>();private Coroutine rec;private void Awake() {Instance = this;}async void Start() {DontDestroyOnLoad(this);Screen.sleepTimeout = SleepTimeout.NeverSleep;pushBtn();}//public Client getNewClient() {// return newClient;//}//public void setNewClient(Client vNewClient) {// newClient = vNewClient;//}public void webOpenHaiMetaUrl(string param = null) {//string url = this.HaiMetaSpaceUrl+param;//UnityEngine.Application.ExternalEval("window.location.href = \"" + url + "\";");}public void webOpenNewSpace(string spaceUrl,string spaceId){string url = spaceUrl + "?param={\"spaceId\":"+ spaceId + ",\"userName\":"+PlayerData.Instance.Name+",\"templateId\":"+ PlayerData.Instance.TemplateId + "}" + "&token=" + PlayerData.Instance.ltk + "&type=4"+ "&state=1";UnityEngine.Application.ExternalEval("window.location.href = \"" + url + "\";");}private void Update() {//if (client == null && newClient != null) {// client = getNewClient();//}}public async void CreateConeccetionBtn(){//string host = string.Format("wss://*****");Debug.Log("CreateConeccetionBtn" +Host.gameServer);client = new Client(Host.gameServer);client.OnDisconnect = OnDisconnect;client.OnReconected = OnReconected;client.OnError = OnError;client.OnConnected = OnConnected;bool isConeccet = await client.ConnectAsync();if (isConeccet){Debug.Log("网络链接成功");}}//public void resetClient() {// setNewClient(client);//}private void OnConnected() {Debug.Log("OnConnected");if (rec != null)StopCoroutine(rec);this.TriggerEvent(EventName.SocketOpen, null);}private void OnError(string msg) {Debug.LogError(string.Format("err msg:{0}", msg));}private void OnReconected() {Debug.Log("OnReconect");}private void OnDisconnect() {Debug.Log("OnDisconnect");//client = null;rec = StartCoroutine(client.ReconectScoektAsync()); //断线重连逻辑}public void pushBtn() {LoadNetCofig();}void LoadNetCofig() {var configPath = Application.dataPath;#if UNITY_EDITORvar filepath = Path.Combine(Application.dataPath.Replace("Assets", ""), "config.txt");
#elsevar filepath = Path.Combine(Application.dataPath, "config.txt");
#endifDebug.Log("configPath" + filepath);filepath = filepath.Replace("\\", "/");LoadFileSetNetwork(filepath);}async void LoadFileSetNetwork(string filepath) {UnityWebRequest www = UnityWebRequest.Get(filepath);await www.SendWebRequest();if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError) {Debug.LogError(www.error);}else {string json = www.downloadHandler.text;var data = LitJson.JsonMapper.ToObject(json);if ((string)data["AssetBundleIP"] != string.Empty) {Host.AssetBundleIP = (string)data["AssetBundleIP"];}
#if UNITY_EDITORvar serverMode = AppConst.serverMode;
#elsevar serverMode = (ServerMode)int.Parse(HttpHelper.GetUrlParam("severAPI"));
#endifAppConst.serverMode = serverMode;switch (serverMode) {case ServerMode.preview:Host.ApiHost = (string)data["preview"];break;case ServerMode.dev:Host.ApiHost = (string)data["dev"];break;}#if !UNITY_EDITORvar paramUrl = HttpHelper.GetUrlParam("param");JsonData paramdata = JsonMapper.ToObject(UtilsFunc.UnicodeToString(paramUrl));try {string spaceId = paramdata["spaceId"].ToJson();PlayerData.Instance.SpaceId = spaceId;PlayerData.Instance.ltk = HttpHelper.GetUrlParam("token");}catch (Exception e) {Debug.LogError(e.Message + '\n' + e.StackTrace);}
#endifDebug.Log("LoadFileSetNetwork SpaceId" + PlayerData.Instance.SpaceId);//如果在编辑器下面,并且不填sapceId,那么则进入发布模式if (string.IsNullOrEmpty(PlayerData.Instance.SpaceId) == false) {await HttpHelper.Instance.GetSceneConfig();}#if !UNITY_EDITORif (paramdata.ContainsKey("templateId")) {if (paramdata["templateId"] != null) {string tempStr = (string)paramdata["templateId"];bool isExist = int.TryParse(tempStr,out var tempId);if (isExist) PlayerData.Instance.TemplateId = tempId;}}string stateUrl = HttpHelper.GetUrlParam("state");
#elsestring stateUrl = string.IsNullOrEmpty(PlayerData.Instance.SpaceId) == true ? AppConst.PublicMode : AppConst.ViewMode;
#endifvar arg = new SceneLoadActionArgs();arg.state = stateUrl;EventManager.Instance.TriggerEvent(EventName.LoadSceneAction, arg);if (stateUrl == AppConst.PublicMode) {PlayerData.Instance.IsEditorMode = true;PlayerData.Instance.IsPublicMode = true;EventManager.Instance.TriggerEvent(EventName.OnPublishEnter);HttpHelper.Instance.GetDefaultSpaceImg();return;}#if !UNITY_EDITORawait HttpHelper.Instance.GetServerConfig();
#elseHost.gameServer = (string)data["local"];
#endifCreateConeccetionBtn();}}private async UniTaskVoid CreateConeccetion() {Debug.Log("开始...");bool isConeccet = await client.ConnectAsync();if (isConeccet) {Debug.Log("网络链接成功");// this.Register<RegisterInfo>("test", (int code) =>// {// Debug.Log("网络事件" +code);// });if (client != null) client.isConnect = true;//this.SendAPI();}elseDebug.Log("多次链接仍让未成功");}public void AddSyncHandler(string name, Action<SyncWrapData> call) {NetManager.Instance.Register<GameFrame>("gameFrame", (ExternalMessage pack) => {GameFrame info = PackageProtocol.DecodeInfo<GameFrame>(pack.data);//if (info.location.playerId == PlayerData.Instance.PlayerId) {// return; //自己不用给自己同步//}var opc = info.opc;SyncWrapData dataBase = null;try {dataBase = JsonUtility.FromJson<SyncWrapData>(opc);if (dataBase.playerId != PlayerData.Instance.PlayerId.ToString()) { //只接受不同playerId的数据call.Invoke(dataBase);}}catch (Exception e) {//Debug.LogError(e);}});}public async void SendSyncData(SyncWrapData syncBase) {string jsonData = JsonUtility.ToJson(syncBase);if (NetManager.Instance.client == null) {Debug.LogError("NetManager.Instance.client == null");return;}var gameFrame = new GameFrame();gameFrame.opc = jsonData;await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);}public async void SendSyncWorldData(SyncWorldData worldData) {string jsonData = LitJson.JsonMapper.ToJson(worldData);if (NetManager.Instance.client == null) {Debug.LogError("NetManager.Instance.client == null");return;}//Debug.Log("SendSyncWorldData worldData:" + jsonData);var gameFrame = new GameFrame();gameFrame.world = jsonData;await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);}public async void SendFrameLocationData(SyncTransfomData syncPos,SyncWrapData wrap) {if (NetManager.Instance.client == null) {Debug.LogError("NetManager.Instance.client == null");return;}var gameFrame = new GameFrame();gameFrame.opc = JsonUtility.ToJson(wrap);gameFrame.location = new TankLocation();SyncTransfomUtil.ToTankLocation(ref gameFrame.location, syncPos);gameFrame.location.playerId = PlayerData.Instance.PlayerId;await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);}public void Register<T>(string name, Action<ExternalMessage> ac) {//#if UNITY_WEBGL && !UNITY_EDITOR //只在WebGl下生效if (PlayerData.Instance.IsEditorMode) return;//#endif//加入事件列表Proto pb = ProtoMaps.Instance.Name2Pb(name);List<Action<ExternalMessage>> list = null;if (actions.ContainsKey(name)) {list = actions[name];}else {list = new List<Action<ExternalMessage>>();actions.Add(name, list);}list.Add(ac);client.OnNetEx(name, (ExternalMessage pack) => {T info = PackageProtocol.DecodeInfo<T>(pack.data);//Debug.Log("网络事件" + JsonUtility.ToJson(info));//遍历事件列表依次回调foreach (Action<ExternalMessage> cb in list) {cb?.Invoke(pack);}});}public async void SendAPI() {//请求注册//RegisterInfo register = new RegisterInfo();//register.account = "abc";//register.phoneNum = "abc";//register.pwd = "abc";//RegisterInfo a = await client.RequestAsync<RegisterInfo, RegisterInfo>("test", register);//Debug.Log(a.phoneNum);client.OnNetEx("login", (ExternalMessage pack) => {Debug.LogError("回调返回");//T info = PackageProtocol.DecodeInfo<T>(pack.data);Debug.Log("网络事件" + JsonUtility.ToJson(info));遍历事件列表依次回调//foreach (Action<ExternalMessage> cb in list)//{// cb?.Invoke(pack);//}});LoginVerify loginVerify = new LoginVerify();loginVerify.account = "18060974935";loginVerify.pwd = "123456ab";loginVerify.loginBizCode = 1;UserInfo ret = await NetManager.Instance.client.RequestAsync<LoginVerify, UserInfo>("login", loginVerify);Debug.Log(ret.nickName);TankEnterRoom myEnterRoom = new TankEnterRoom();myEnterRoom.roomId = 1002;myEnterRoom.team = 1;TankEnterRoom ret2 = await NetManager.Instance.client.RequestAsync<TankEnterRoom, TankEnterRoom>("enterRoom", myEnterRoom);Debug.Log(ret2.time);//int code = await client.RequestAsync<String, int>("gameObjUsed", "ok");}public bool isConnected() {return isConeccet;}private void OnDestroy() {if (client != null) {client.Cancel();}}
}
11.发送网络消息的应用:
UserInfo ret = await NetManager.Instance.client.RequestAsync<LoginVerify, UserInfo>("login", loginVer);
12.监听网络消息:
NetManager.Instance.Register<TankEnterRoom>("enterRoom", (ExternalMessage pack) =>
{Debug.Log("新玩家进去");this.onNewRoleEnter2(pack);
});
public void onNewRoleEnter2(ExternalMessage pack) {TankEnterRoom info = PackageProtocol.DecodeInfo<TankEnterRoom>(pack.data);List<UserInfo> roleList = info.tankPlayerList;Debug.Log("新进入房间玩家列表" + roleList.ToString());long roleId = info.playerId;Debug.Log("新进入房间玩家id" + roleId);foreach (UserInfo vRole in roleList){Debug.Log("新进入房间玩家" + JsonUtility.ToJson(vRole));if (vRole.id == roleId && vRole.id!=PlayerData.Instance.PlayerId){if (!RolesManager.Instance.isExistRoleById(vRole.id)) {GuestinfoDataModel.Instance.OnNewRoleIntoRoom(vRole);RolesManager.Instance.synPlayer(vRole, true);} }}}
相关文章:
Unity组件开发--长连接webSocket
1.下载安装UnityWebSocket 插件 https://gitee.com/cambright/UnityWebSocket/ 引入unity项目: 2.定义消息体结构:ExternalMessage和包结构Package: using ProtoBuf; using System; using System.Collections; using System.Collections.Ge…...
书客、柏曼、松下护眼台灯哪款更靠谱?实测核心数据对比PK!
随着科技时代的到来,人们的生活水平在不断提高,不少家长开始担心自家孩子的近视问题,护眼台灯在家庭中的讨论热度也越来越高,光线舒适又具备多种功能,不少家长都给孩子入手了护眼台灯。不过作为家电博主,我…...
MQTT协议
一.MQTT协议概述 MQTT(Message Queuing Telemetry Transport)是一种轻量级的、基于发布/订阅模式的消息传输协议,广泛应用于物联网(IoT)领域中的设备连接、传感器数据传输等场景。 MQTT协议使用TCP/IP协议栈作为底层…...
Unity编辑器扩展(外挂)
每日一句:未来的样子藏在现在的努力里 目录 什么是编译器开发 C#特性[System.Serializable] 特殊目录 命名空间 /*检视器属性控制*/ //添加变量悬浮提示文字 //给数值设定范围(最小0,最大150) //指定输入框,拥有5行 //默认…...
oracle 19c容器数据库data dump数据泵传输数据(2)---11g导19c
目录 1.在11gnon-cdb数据库中创建测试用户 2.在19cCDB容器数据库中新建pdb2 3.执行命令导出 4.执行命令导入 Exporting from a Non-CDB and Importing into a PDB 我們要記住一点:如果是全库导出导入的话,目标数据库没有的表空间我们要事先创建&#…...
Java-网络爬虫(二)
文章目录 前言一、WebMagic二、使用步骤1. 搭建 Maven 项目2. 引入依赖 三、入门案例四、核心对象&组件1. 核心对象SipderRequestSitePageResultItemsHtml(Selectable) 2. 四大组件DownloaderPageProcessorSchedulerPipeline 上篇:Java-网…...
【android】rk3588-android-bt
文章目录 蓝牙框架HCI接口蓝牙VENDORLIBvendorlib是什么 代码层面解读vendorlib1、 vendorlib实现,协议栈调用2、协议栈实现,vendorlib调用(回调函数)2.1、 init函数2.2、BT_VND_OP_POWER_CTRL对应处理2.3、BT_VND_OP_USERIAL_OPE…...
如何在 Microsoft Edge 浏览器中启用自动刷新
你是否经常发现自己在使用 Microsoft Edge 时点击刷新按钮?如果您需要一个网页以设定的时间间隔自动更新,那么请接着往下看。 在这篇博文中,我们探讨如何在 Microsoft Edge 浏览器中启用和管理自动刷新功能。 为什么选择自动刷新࿱…...
Redis之集群方案比较
哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般&a…...
WPF 布局
了解 WPF中所有布局如下,我们一一尝试实现,本文档主要以图形化的形式展示每个布局的功能。 布局: Border、 BulletDecorator、 Canvas、 DockPanel、 Expander、 Grid、 GridView、 GridSplitter、 GroupBox、 Panel、 ResizeGrip、 Separat…...
#Uniapp:uni-app中vue2生命周期--11个
uni-app中vue2生命周期 生命周期钩子描述H5App端小程序说明beforeCreate在实例初始化之后被调用 详情√√√created在实例创建完成后被立即调用 详情√√√beforeMount在挂载开始之前被调用 详情√√√mounted挂载到实例上去之后调用 详情 注意:此处并不能确定子组…...
pytorch 分布式 Node/Worker/Rank等基础概念
分布式训练相关基本参数的概念如下: Definitions Node - A physical instance or a container; maps to the unit that the job manager works with. Worker - A worker in the context of distributed training. WorkerGroup - The set of workers that execute the same f…...
《动手学深度学习》学习笔记 第8章 循环神经网络
本系列为《动手学深度学习》学习笔记 书籍链接:动手学深度学习 笔记是从第四章开始,前面三章为基础知识,有需要的可以自己去看看 关于本系列笔记: 书里为了让读者更好的理解,有大篇幅的描述性的文字,内容很…...
腾讯实验平台基于 StarRocks 构建湖仓底座
作者: 腾讯大数据平台部科学实验中心Tech Lead、专家工程师 马金勇博士 腾讯大数据平台部科学实验中心数据负责人、专家工程师 胡明杰 StarRocks Contributor、腾讯高级工程师 刘志行 在 2022 年,腾讯 A/B Test 团队启动了海外商业化版本 ABetterChoice …...
【基础工具篇使用】ADB 的安装和使用
文章目录 ADB的命令安装ADB 命令使用查看帮助 ——adb help查看连接设备 ADB的命令安装 ADB 命令的全称为“Android Debug Bridge”,从英文中看出主要是用作安卓的调试工具。ADB 命令在嵌入式开发中越来越常用了 在 Windows 上按“win”“R”组合件打开运行, 输入 …...
数字图像处理练习题
数字图像处理练习题 文章目录 数字图像处理练习题第 一 章1.什么是数字图像?2.数字图像有哪些特点?3.数字图像处理的目的是什么?4.简述数字图像的历史。5.数字图像有哪些主要应用?6.列举生活中数字图像的获得途径。7.结合自己的生活实例,举出一个数字图像的应用实例8.数字图…...
开关电源PFC电路原理详解及matlab仿真
PFC全称“Power Factor Correction”,意为“功率因数校正”。PFC电路即能对功率因数进行校正,或者说能提高功率因数的电路。是开关电源中很常见的电路。 在电学中,功率因数PF指有功功率P(单位w)与视在功率S(…...
SpringBoot+Hutool实现图片验证码
图片验证码在注册、登录、交易、交互等各类场景中都发挥着巨大作用,能够防止操作者利用机器进行暴力破解、恶意注册、滥用服务、批量化操作和自动发布等行为。 创建一个实体类封装,给前端返回的验证码数据: Data public class ValidateCodeV…...
【MySQL】MySQL版本8+ 窗口函数 Lead 的两种使用
力扣题 1、题目地址 1709. 访问日期之间最大的空档期 2、模拟表 表:UserVisits Column NameTypeuser_idintvisit_datedate 该表没有主键,它可能有重复的行该表包含用户访问某特定零售商的日期日志。 3、要求 假设今天的日期是 ‘2021-1-1’ 。 …...
Hive 的 安装与使用
目录 1 安装 MySql2 安装 Hive3 Hive 元数据配置到 MySql4 启动 Hive5 Hive 常用交互命令6 Hive 常见属性配置 Hive 官网 1 安装 MySql 为什么需要安装 MySql? 原因在于Hive 默认使用的元数据库为 derby,开启 Hive 之后就会占用元数据库,且不与其他客户…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...
关于uniapp展示PDF的解决方案
在 UniApp 的 H5 环境中使用 pdf-vue3 组件可以实现完整的 PDF 预览功能。以下是详细实现步骤和注意事项: 一、安装依赖 安装 pdf-vue3 和 PDF.js 核心库: npm install pdf-vue3 pdfjs-dist二、基本使用示例 <template><view class"con…...
