文章目录

  • 一、前言
  • 二、思考问题与解决方案
    • 1、思考问题
    • 2、解决方案
      • 2.1、Unity中如何开启摄像头并对图像进行采样
      • 2.2、图像如何中转给其他客户端
      • 2.3、如何实现清晰度切换
      • 2.4、客户端如何对图像进行解码并显示
  • 三、实际操作
    • 0、思维导图
    • 1、界面设计与制作
    • 2、UI素材获取
    • 3、创建Unity工程
    • 4、制作UI界面
    • 5、下载Mirror网络插件
    • 6、写C#代码
      • 6.1、网络管理器:VideoChatNetwork.cs
      • 6.2、摄像头画面:Player.cs
      • 6.3、业务逻辑:MainLogic.cs
      • 6.4、界面交互:MainPanel.cs
    • 7、挂脚本
      • 7.1、VideoChatNetwork脚本
      • 7.2、Player脚本
      • 7.3、MainPanel脚本
    • 8、Editor环境下测试
    • 9、发布应用
      • 9.1、发布Windows平台exe
      • 9.2、发布Android平台apk
    • 10、真机测试
  • 四、工程源码
  • 五、完毕

一、前言

嗨,大家好,我是新发。
事情是这样的,我前几天写了一篇《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
有同学留言问我多人在线视频聊天切换清晰度怎么做,

嗯,作为一位热心的技术博主,我一般都是能帮则帮。
嘛,今天就来写个多人视频聊天的功能吧(并且可以切换清晰度)。

二、思考问题与解决方案

1、思考问题

多人视频聊天大家应该都不陌生,像腾讯视频会议那样,多个人的视频画面同时显示在界面中。

我的实现思路是每个客户端对本地摄像头画面进行采样,得到帧图像,然后对图像进行适当的压缩,转为字节流上传给服务端,接着服务端根据每个客户端设置的清晰度对帧图像进行压缩,然后转发帧图像给其他客户端,其他客户端接收到帧图像字节流后进行解码,最后显示到界面中。
画成图是这样子:


要实现上面的功能,我们需要先思考并解决以下几个必要问题:
1 Unity中如何开启摄像头并对图像进行采样?
2 图像如何中转给其他客户端?
3 如何实现清晰度切换?
4 客户端如何对图像进行解码并显示?

2、解决方案

2.1、Unity中如何开启摄像头并对图像进行采样

Unity提供了WebCamTexture这个类,通过它我们可以很方便的访问摄像头图像。
具体用法我下文会讲。

2.2、图像如何中转给其他客户端

正常情况我们需要搭建一个中转的服务端,要实现数据的序列化、网络通信、数据的反序列化等。这里我想使用Mirror网络库来实现。在之前那篇文章中我也有介绍过Mirror:《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》


2.3、如何实现清晰度切换

客户端把清晰度设置告诉服务端,服务端根据清晰度对图像进行压缩,把压缩后的图像下发给客户端。
由于我们是使用Mirror,服务端也是Unity客户端,所以我们可以直接使用Texture2DEncodeToJPG接口对图像进行压缩,第二个参数就是压缩率,值从1~100(默认是75),

public static byte[] EncodeToJPG(this Texture2D tex, int quality);
2.4、客户端如何对图像进行解码并显示

通过网络传输过来的图像是字节流,我们需要把它反序列化为Unity可现实的图像Texture2D,我们直接使用Texture2DLoadImage接口,

public static bool LoadImage(this Texture2D tex, byte[] data);

三、实际操作

下面,撸起袖子开始动手实际操作吧~

0、思维导图

养成好习惯,动手前先画思维图,如下:

1、界面设计与制作

使用axure快速原型设计工具先简单设计一下界面,
登录界面,Host就是作为房主,Client就是作为路人,

视频聊天界面,排列显示多个视频画面,可切换视频清晰度,可随时退出房间,

2、UI素材获取

简单的UI素材资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮

找一个形状合适的,可以进行调色,我一般是调成白色,

因为Unity中可以设置Color,这样我们只需要一个白色按钮就可以在Unity中创建不同颜色的按钮了。
弄点基础的美术资源,

注:那个头像是我自己用PhotoShop画的哦,我之前用PhotoShop画过一幅原创连环画,如下:

3、创建Unity工程

我使用的Unity版本为2021.1.7f1c1 个人版,我们要做的是一个多人视频聊天的功能,不需要使用到3D相关的内容,所以我们创建工程时使用2D模板,工程名就叫UnityVideoChat吧~

创建成功,

4、制作UI界面

根据我们的原型设计,使用UGUI制作界面:MainPanel.prefab

如下,


其中用于渲染视频图像的UI独立做成一个预设:VideoImage.prefab,方便进行克隆(每连接一客户端就克隆一个VideoImage

如下,使用RawImage组件来显示图像,

5、下载Mirror网络插件

MirrorUnity Asset Store地址:
https://assetstore.unity.com/packages/tools/network/mirror-129321

Mirror插件添加到自己的账号中,然后回到Unity,在Package Manager中就可以下载了,

下载下来导入Unity中,

6、写C#代码

6.1、网络管理器:VideoChatNetwork.cs

先画个图,方便大家直观地知道VideoChatNetwork做什么:

注:VideoChatNetwork.cs脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。

创建VideoChatNetwork.cs脚本,它需要继承Mirror.NetworkManager

// VideoChatNetwork.csusing Mirror;public class VideoChatNetwork : NetworkManager
{// ...
}

启动服务端:

StartHost();

启动客户端:

StartClient();

关闭服务端:

StopHost();

关闭客户端:

StopClient();

定义消息CreatePlayerMessage(用于传递用户名):

public struct CreatePlayerMessage : NetworkMessage
{public string name;
}

服务器启动成功回调,注册CreatePlayerMessage消息响应函数,在响应函数中实例化Player并添加到NetworkServer中:

public override void OnStartServer()
{base.OnStartServer();// 注册事件NetworkServer.RegisterHandler<CreatePlayerMessage>(OnCreatePlayer);// ...
}void OnCreatePlayer(NetworkConnection connection, CreatePlayerMessage createPlayerMessage)
{// 实例化PlayerGameObject playergo = Instantiate(playerPrefab);playergo.GetComponent<Player>().accountName = createPlayerMessage.name;// 添加PlayerNetworkServer.AddPlayerForConnection(connection, playergo);
}

客户端连接成功回调:

public override void OnClientConnect(NetworkConnection conn)
{base.OnClientConnect(conn);// 转发通知conn.Send(new CreatePlayerMessage { name = MainLogic.instance.accountName });
}

连接断开回调:

public override void OnClientDisconnect(NetworkConnection conn)
{// TODO 重新登录
}
6.2、摄像头画面:Player.cs

Player思维导图:

注:Player.cs脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。

创建Player.cs脚本,它需要继承Mirror.NetworkBehaviour

using Mirror;public class Player : NetworkBehaviour
{// ...
}

先定义一些必要的UI对象,其中用户名使用[SyncVar]注解进行自动同步,

public RawImage videoImage;
[SyncVar]
public string accountName;
public Text accountNameText;

Start函数中判断是否是本地用户isLocalPlayer,如果是,则开启摄像头:

// Player.csprivate WebCamTexture webCam;private void Start()
{if (isLocalPlayer){// 开启摄像头WebCamDevice[] devices = WebCamTexture.devices;webCam = new WebCamTexture(devices[0].name, 128, 128, 5);  //设置宽、高和帧率   webCam.Play();}// ...
}

Update函数中对摄像头图像进行采样,0.3秒采集一帧,可适当进行调整,同时把图像转为字节流并发送给服务端,

// Player.csprivate float timer;public void Update()
{if (isLocalPlayer && null != webCam){timer += Time.deltaTime;if (timer > 0.3f){timer = 0;// 采样videoImage.texture = webCam;// 图像转字节流    var bytes = MainLogic.instance.WebCamTextureToBytes(webCam);// 发送字节流给服务端CmdSendTextureBytes(bytes);}}}

发送图像字节流给服务端,注意Command为客户端远程调用服务端,

// 发送图像字节流给服务端
// Command为客户端远程调用服务端
[Command]
public void CmdSendTextureBytes(byte[] texture)
{RpcReceiveTexture(texture);
}

客户端接收服务端的图像字节流数据,并显示到RawImage上,注意ClientRpc为服务端远程调用客户端,

// 客户端接收服务端的图像字节流数据,并显示到RawImage上
// ClientRpc为服务端远程调用客户端
[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{if(!isLocalPlayer){// 压缩var compressedTex = MainLogic.instance.CompressTexture(textureBytes);// 显示videoImage.texture = MainLogic.instance.BytesToTexture2D(compressedTex);}
}

上面我们出现了Mirror三个注解:[SyncVar][Command][ClientRpc],想要理解它们最好的办法是反编译我们的C#dll,看它生成的代码(使用ILSpy.exedll进行反编译)。

[SyncVar]
[SyncVar]会对我们的变量做自动同步(自动序列化、网络传递、反序列化),例:

[SyncVar]
public string accountName;

编译后生成的代码:

[SyncVar]
public string accountName;public string NetworkaccountName
{get{return accountName;}[param: In]set{if (!SyncVarEqual(value, ref accountName)){string text = accountName;SetSyncVar(value, ref accountName, 1uL);}}
}// 序列化
public override bool SerializeSyncVars(NetworkWriter writer, bool forceAll)
{bool result = base.SerializeSyncVars(writer, forceAll);if (forceAll){writer.WriteString(accountName);return true;}writer.WriteULong(base.syncVarDirtyBits);if ((base.syncVarDirtyBits & 1L) != 0L){writer.WriteString(accountName);result = true;}return result;
}// 反序列化
public override void DeserializeSyncVars(NetworkReader reader, bool initialState)
{base.DeserializeSyncVars(reader, initialState);if (initialState){string text = accountName;NetworkaccountName = reader.ReadString();return;}long num = (long)reader.ReadULong();if ((num & 1L) != 0L){string text2 = accountName;NetworkaccountName = reader.ReadString();}
}

我们代码中对accountName的操作,都被替代为对NetworkaccountName的操作,比如:

playergo.GetComponent<Player>().accountName = createPlayerMessage.name;

变成了:

playergo.GetComponent<Player>().NetworkaccountName = createPlayerMessage.name;

[Command]
[Command]表示客户端远程调用服务端。
例:

[Command]
public void CmdSendTextureBytes(byte[] texture)
{RpcReceiveTexture(texture);
}

编译后生成的代码:

[Command]
public void CmdSendTextureBytes(byte[] texture)
{PooledNetworkWriter writer = NetworkWriterPool.GetWriter();writer.WriteBytesAndSize(texture);SendCommandInternal(typeof(Player), "CmdSendTextureBytes", writer, 0);NetworkWriterPool.Recycle(writer);
}protected void UserCode_CmdSendTextureBytes(byte[] texture)
{RpcReceiveTexture(texture);
}protected static void InvokeUserCode_CmdSendTextureBytes(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection)
{if (!NetworkServer.active){Debug.LogError((object)"Command CmdSendTextureBytes called on client.");}else{((Player)obj).UserCode_CmdSendTextureBytes(reader.ReadBytesAndSize());}
}static Player()
{RemoteCallHelper.RegisterCommandDelegate(typeof(Player), "CmdSendTextureBytes", InvokeUserCode_CmdSendTextureBytes, requiresAuthority: true);
}

我们可以看到,它把我们的调用转成了网络消息,变量做了序列化、传递和反序列化。
[ClientRpc]
[ClientRpc]表示服务端远程调用客户端。
例:

[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{if(!isLocalPlayer){// 压缩var compressedTex = MainLogic.instance.CompressTexture(textureBytes);// 显示videoImage.texture = MainLogic.instance.BytesToTexture2D(compressedTex);}
}

编译后生成的代码:

[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{PooledNetworkWriter writer = NetworkWriterPool.GetWriter();writer.WriteBytesAndSize(textureBytes);SendRPCInternal(typeof(Player), "RpcReceiveTexture", writer, 0, includeOwner: true);NetworkWriterPool.Recycle(writer);
}protected void UserCode_RpcReceiveTexture(byte[] textureBytes)
{if (!base.isLocalPlayer){byte[] compressedTex = MainLogic.instance.CompressTexture(textureBytes);videoImage.texture = (Texture)(object)MainLogic.instance.BytesToTexture2D(compressedTex);}
}protected static void InvokeUserCode_RpcReceiveTexture(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection)
{if (!NetworkClient.active){Debug.LogError((object)"RPC RpcReceiveTexture called on server.");}else{((Player)obj).UserCode_RpcReceiveTexture(reader.ReadBytesAndSize());}
}static Player()
{RemoteCallHelper.RegisterRpcDelegate(typeof(Player), "RpcReceiveTexture", InvokeUserCode_RpcReceiveTexture);
}

我们可以看到,它把我们的调用转成了网络消息,变量做了序列化、传递和反序列化。

6.3、业务逻辑:MainLogic.cs

MainLogic思维导图:

注:MainLogic.cs脚本完整代码见我的文末的工程源码,下面只讲一些重点的地方。

创建MainLogic.cs脚本,全局唯一一个实例对象,我们使用单例模式:

// MainLogic.cspublic class MainLogic
{// 单例模式private static MainLogic s_instance;public static MainLogic instance{get{if (null == s_instance)s_instance = new MainLogic();return s_instance;}}
}

定义成员变量:

/// <summary>
/// 用户名
/// </summary>
public string accountName;/// <summary>
/// 清晰度,0:高清,1:标清,2:普通
/// </summary>
public int definition;

初始化,设置成员变量和回调函数:

private Action backToLoginCb;/// <summary>
/// 初始化
/// </summary>
/// <param name="network">网络管理器</param>
/// <param name="backToLoginCb">回调登录界面的回调函数</param>
public void Init(VideoChatNetwork network, Action backToLoginCb)
{this.network = network;this.backToLoginCb = backToLoginCb;
}public void OnClientDisconnect()
{if (null != backToLoginCb)backToLoginCb();
}

启动服务端,IP地址默认是localhost

/// <summary>
/// 启动服务端
/// </summary>
/// <param name="ip">IP地址</param>
/// <param name="account">用户名</param>
/// <param name="cb">成功回调函数</param>
public void StartHost(string ip, string account, Action cb)
{if (!NetworkClient.isConnected && !NetworkServer.active){this.accountName = account;network.networkAddress = ip;network.DoStartHost(cb);}
}

启动客户端,IP地址默认是localhost

/// <summary>
/// 启动客户端
/// </summary>
/// <param name="ip">IP地址</param>
/// <param name="account">用户名</param>
/// <param name="cb">回调函数</param>
public void StartClient(string ip, string account, Action cb)
{this.accountName = account;network.networkAddress = ip;network.DoStartClient(cb);
}

关闭网络:

/// <summary>
/// 关闭网络
/// </summary>
public void Close()
{network.StopHost();network.StopClient();
}

切换清晰度:

/// <summary>
/// 切换清晰度
/// </summary>
public void SwitchDefinition(int v)
{definition = v;
}/// <summary>
/// 根据图像清晰度进行图像压缩
/// </summary>
public byte[] CompressTexture(byte[] texture)
{if (null == texture) return null;switch (definition){case 0:default:{return texture;}case 1:{var tex2D = MainLogic.instance.BytesToTexture2D(texture);return tex2D.EncodeToJPG(40);}case 2:{var tex2D = MainLogic.instance.BytesToTexture2D(texture);return tex2D.EncodeToJPG(10);}}
}

字节流转Texture2D

/// <summary>
/// 字节流转Texture2D
/// </summary>
/// <param name="textureBytes">图像字节流</param>
/// <returns></returns>
public Texture2D BytesToTexture2D(byte[] textureBytes)
{Texture2D tex2D = new Texture2D(30, 30);tex2D.LoadImage(textureBytes);return tex2D;
}

摄像头图像转字节流:

/// <summary>
/// 摄像头图像转字节流
/// </summary>
/// <param name="webCam">摄像头帧画面</param>
/// <param name="quality">压缩了,1~100,默认75</param>
/// <returns></returns>
public byte[] WebCamTextureToBytes(WebCamTexture webCam, int quality = 75)
{Texture2D texture = new Texture2D(webCam.width, webCam.height);int y = 0;while (y < texture.height){int x = 0;while (x < texture.width){Color color = webCam.GetPixel(x, y);texture.SetPixel(x, y, color);++x;}++y;}texture.Apply();var bytes = texture.EncodeToJPG(quality);return bytes;
}
6.4、界面交互:MainPanel.cs

MainPanel思维导图:

创建MainPanel.cs脚本,在MainPanel脚本中我们去写界面交互的代码,
先定义一些必要的UI成员,

// IP地址输入框
public InputField ipInput;
// 用户名输入框
public InputField accountInput;
// 房主按钮
public Button hostBtn;
// 房客按钮
public Button clientBtn;
// 清晰度下拉框
public Dropdown definitionDropdown;
// 登录UI父节点
public GameObject loginObj;
// 视频聊天UI父节点
public GameObject videoChatObj;
// 关闭网络按钮
public Button closeBtn;

Awake中进行初始化:

private void Awake()
{var networkObj = GameObject.Find("NetworkManager");MainLogic.instance.Init(networkObj.GetComponent<VideoChatNetwork>(), () =>{loginObj.SetActive(true);videoChatObj.SetActive(false);});
}

Start函数中设置各个按钮的响应逻辑:

void Start()
{// Host按钮hostBtn.onClick.AddListener(() =>{MainLogic.instance.StartHost(ipInput.text, accountInput.text, () =>{loginObj.SetActive(false);videoChatObj.SetActive(true);});});// Client按钮clientBtn.onClick.AddListener(() =>{MainLogic.instance.StartClient(ipInput.text, accountInput.text, () =>{loginObj.SetActive(false);videoChatObj.SetActive(true);});});// 关闭网络按钮closeBtn.onClick.AddListener(() =>{MainLogic.instance.Close();loginObj.SetActive(true);videoChatObj.SetActive(false);});// 分辨率下拉框definitionDropdown.onValueChanged.AddListener((v) => {MainLogic.instance.SwitchDefinition(v);});
}

7、挂脚本

7.1、VideoChatNetwork脚本

场景中创建一个空物体,重命名为NetworkManager

在它身上挂上VideoChatNetwork脚本(它会自动挂上KcpTransport脚本),

VideoChatNetwork赋值Player Prefab,并去掉Auto Create Player的勾选,

KcpTransport使用的是可靠UDP传输,在KcpTransport组件上可以设置端口号,

7.2、Player脚本

VideoImage.prefab预设根节点挂上````Player脚本(它会自动挂上NetworkIdentity脚本),赋值Player脚本的UI```成员,

7.3、MainPanel脚本

MainPanel.prefab预设挂上MainPanel脚本,并赋值UI成员,

8、Editor环境下测试

Editor环境下运行,效果如下:

9、发布应用

9.1、发布Windows平台exe

转为PC平台,添加场景,

设置窗口尺寸为1280*720

执行Build,打出exe

9.2、发布Android平台apk

转为Android平台,添加场景,


设置包名为com.linxinfa.videochat

执行Build,打包出apk

10、真机测试

apk安装到手机上,手机可以正常访问摄像头,

我们在PC端运行客户端,连接手机的IP地址,

手机与电脑的视频聊天如下,画面清晰度切换,效果还是比较明显的,

四、工程源码

本文工程我以上次到CODE CHINA,感兴趣的同学可自行下载下来学习。
地址:https://codechina.csdn.net/linxinfa/UnityVideoChat
注:我使用的Unity版本为2021.1.7f1c1 个人版

五、完毕

好了,就写到这里吧,音频的同步我没有写,思路是使用UnityMicrophone进行声音采样,上传服务器进行转发,这里就留给各位去实现啦,

另外,如果本文的Mirror网络库你不熟悉,可能看代码会比较吃力,建议可以先看看我之前那篇文章中关于Mirror网络库的介绍:《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》

天色已晚,我要去冲凉先了,拜拜~

我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~

最后,与皮皮猫合个影吧~

【游戏开发实战】Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)相关推荐

  1. 【Unity】2D游戏-愤怒的小鸟教学实战(附源码和实现步骤 超详细)

    需要源码和资源文件请点赞关注收藏后评论区留言私信~~~ 下面我们将在Unity3D中实现愤怒的小鸟的简单版,游戏中最复杂的部分是物理系统,但是借助于Unity3D编辑器,我们就不用担心太多了 一.效果 ...

  2. 从零实现在线云相亲APP|程序员脱单神器(内附源码Demo)

    实时音视频通话涉及到的技术栈.人力成本.硬件成本非常大,一般个人开发者基本无法独立完成一个功能健全并且稳定的实时音视频应用.本文介绍一天之内,无任何实时音视频低层技术的android开发者完成实时相亲 ...

  3. Android Studio App开发入门之选择按钮的讲解及使用(包括复选框,开关按钮,单选按钮,附源码)

    运行有问题或需要图片资源请点赞关注收藏后评论区留言~~~ 在学习复选框之前,先了解一下CompoundButton,在Android体系中,CompoundButton类是抽象的复合按钮,因为是抽象类 ...

  4. Python开发实战案例之网络爬虫(附源码)-张子良-专题视频课程

    Python开发实战案例之网络爬虫(附源码)-35人已学习 课程介绍         课程特色: 特色1:案例驱动-围绕两大完整的Python网络爬虫实战开发案例:IT电子书下载网络爬虫和股票交易数据 ...

  5. 新书推荐:《Android Studio开发实战:从零基础到App上线》终章

    <Android Studio开发实战:从零基础到App上线>是一部Android开发的实战教程,由浅入深.由基础到高级,带领读者一步一步走进App开发的神奇世界. 全书共分为16章.其中 ...

  6. 王者级微信小程序开发实战教学 从零到高手搭建微信小程序框架开发教程

    王者级微信小程序开发实战教学 从零到高手搭建微信小程序框架开发教程 小程序进阶 王者级微信小程序开发实战教学课程,讲师手把手对同学们进行微信小程序开发的进阶实战,从零开始搭建,从本地到云端开始系统化的 ...

  7. python ai应用开发_AI应用开发实战 - 从零开始搭建macOS开发环境

    AI应用开发实战 - 从零开始搭建macOS开发环境 联系我们 OpenmindChina@microsoft.com 零.前提条件 一台能联网的电脑,使用macOS操作系统 请确保鼠标.键盘.显示器 ...

  8. 测速源码_物联网之智能平衡车开发实战项目(附源码)

    自从上次分享了"适合练手的10个前端实战项目(附源码)"之后,很多小伙伴就私信问有没有物联网相关的实战项目教程,那么今天就给大家分享一个物联网工作初期经常接触的项目:智能平衡车开发 ...

  9. 【Java游戏开发】坦克大战(附源码+课件+资料)

    本课程讲解了一个坦克大战游戏的详细编写流程,即使你是刚入门java的新手,只要你简单掌握了该游戏所需要的javase基础知识,便可以跟随教程视频完成属于你自己的坦克大战游戏!同时还可以加深和巩固你对面 ...

最新文章

  1. 20140417--Linux课程讲解目录索引
  2. pthread 立即停止线程_线程取消(pthread_cancel)
  3. 【CyberSecurityLearning 28】批处理与简单病毒
  4. 通俗易懂量子计算的原理
  5. ajax post传送数组以及java后台接收数组
  6. 为什么matlab显示error,【求救】我安装了资源 MATLAB R2012b 后,显示有error……
  7. dubbo源码解析-zookeeper创建节点
  8. dede列表页if判断输出html,首页、列表页调用文章body内容的两种方法
  9. oracle安装错误10301,Oracle数据库案例整理-Oracle系统运行时故障-表空间所在的目录没有可用空间导致收集统计信息失败...
  10. Vscode 如何使用内置浏览器?
  11. 替换Mac的home brew源
  12. PS CC 2018安装插件imagemotion
  13. word html 预览 打印出来,word预览时文字在表格中,打印出来却没有.doc
  14. C# 操作word之在表格中插入新行、删除指定行
  15. 深度学习:淘气3000问
  16. win10 uwp 使用 Border 布局
  17. 通过OTA的方式在局域网分发iOS应用
  18. python输出箭头代码_OS X和代码在Python中的“向上箭头”历史记录.InteractiveConsole...
  19. SpringCloud Zuul 网关
  20. 你必须知道的家庭急救常识

热门文章

  1. 什么叫中断、中断向量、中断向量表?
  2. 【转】腾讯 百度 网易游戏 华为Offer及笔经面经
  3. ArcGIS合并和拆分地图
  4. 高中计算机八字标语,八字高考口号霸气押韵
  5. MySQL 版本:'for the right syntax to use near 'identified by 'password' with grant option'
  6. android 三屏手机游戏,大象侠攻略三屏操作手速必须要快
  7. Springboot+Sqlserver+mybatisplus 如何进行配置?
  8. GlusterFS技术概要分析(转自oschina)
  9. ChatGPT OpenAI 人工智能语言处理工具
  10. 腾讯欲全资收购搜狗,目的是什么?