Unity Networking(UNet)函数时序统计和分析

背景和概述

Unity Networking是官方自Unity5.1以来推出的新网络通信解决方案。UNet是非官方但更民间更精简的叫法。

本文需要读者有基础的UNet知识。

了解UNet时序,可以更好更严谨地编写UNet相关的业务逻辑代码。
本文针对UNet的HLAPI进行时序统计和分析。
本文可作为工具文档,需要时可进行时序查阅。

在有时序统计的基础上,本文再参考Unity Networking 5.3源代码(Bitbucket网站可能需要翻墙)进行整合分析,可以帮助了解底层发生的具体逻辑。

当前将Unity(就算是Headless)运行在Linux服务器上,会出现一定的性能问题。联系咨询过Unity内部开发同学Ian和一位和蔼大胖子,获知Headless所剔除的功能模块并不多,仅仅是最终不提交(也不能提交)到GPU、DSP而已。原话是“Modulization is hard”、“Should not run Unity on the server”。
所以当前,应避免粗暴地将Unity运行在“关键”服务器上。

  • 应从业务层着手剔除Headless模式下所启用的业务功能(如模型、渲染、物理、音效等)
  • 将Unity运行于“非关键”服务器(比如用于外挂分析,等)是可能可行的
  • 将Unity无状态地运行,多“关键”服务器(比如用于战斗)共享该Unity服务器,是有成功案例的
  • 但粗暴地每一局游戏都在服务器运行一个Unity进程是欠妥的

可惜的是,UNet的默认思路正是最后一种。由于Ian并非UNet Team的开发同学,所以其并不十分了解将于Unity5.4(但被delay了)的Server Library所完成的功能。但一种推测是,Server Library正是为了避免将Unity运行于服务器,而是提供UNet、Unity的基础功能(Math等),让我们服务器同学利用UNet接口,重新实现逻辑。

测试方法

测试Unity版本为5.3.1。运行平台是OSX。
测试NetworkManager通过NBNetworkManager继承并override掉关键函数;测试Player的Prefab名字为NBPlayer。通过在这个Prefab加上测试脚本TestNetworkBehaviour进行日志输出。
通过分析日志,可以统计UNet函数的时序。


函数时序概括

以下为关键函数的罗列,以供快速查询之用。
如需可细看下一章节的详细文档及分析。

Dedicated Server情况

NetworkManager NetworkBehaviour
Server初始化阶段 Server初始化阶段
Awake()
Start()
OnStartServer()
ServerChangeScene()
OnServerSceneChanged()
Client初始化阶段 Client初始化阶段
OnServerConnect()
OnServerReady()
Player初始化阶段 Player初始化阶段
OnServerAddPlayer()
Awake()
OnEnable()
OnStartServer()
OnRebuildObservers()
OnSerialize()(多次)
Start()
Player运转阶段 Player运转阶段
FixedUpdate()(多次)
Update()(多次)
OnSerialize()(多次)
Player销毁阶段 Player销毁阶段
OnDisable()
OnDestroy()
OnServerDisconnect
Server销毁阶段 Server销毁阶段
OnStopServer()

Remote Client情况

NetworkManager NetworkBehaviour
Client初始化阶段 Client初始化阶段
Awake()
Start()
OnStartClient()
OnClientConnect()
OnClientSceneChanged()
Player初始化阶段 Player初始化阶段
Awake()
OnEnable()
OnDeserialize()
PreStartClient()
OnStartClient()
OnStartLocalPlayer()
OnStartAuthority()
(后面运转阶段也可能调到)
OnDeserialize()(多次)
Start()
Player运转阶段 Player运转阶段
FixedUpdate()(多次)
Update()(多次)
OnDeserialize()(多次)
Player销毁阶段 Player销毁阶段
OnNetworkDestroy()
OnDisable()
OnDestroy()
Client销毁阶段 Client销毁阶段
OnStopClient()

Host情况

NetworkManager NetworkBehaviour
Host初始化阶段 Host初始化阶段
Awake()
Start()
OnStartHost()
OnStartServer()
ServerChangeScene()
OnServerConnect()
(LocalClient混杂进来的Server函数)
OnStartClient()
(LocalClient混杂进来的Client函数)
OnClientConnect()
(LocalClient混杂进来的Client函数)
OnServerSceneChanged()
OnClientSceneChanged()
(LocalClient混杂进来的Client函数)
OnServerReady()
(LocalClient混杂进来的Server函数)
OnServerAddPlayer()
(LocalClient混杂进来的Server函数)
OnServerConnect()
OnServerReady()
Player初始化阶段 Player初始化阶段
OnServerAddPlayer()
Awake()
OnEnable()
OnStartServer()
PreStartClient()
OnStartClient()
OnRebuildObservers()
OnSerialize()(多次)
OnSetLocalVisibility()
Start()
Player运转阶段 Player运转阶段
FixedUpdate()(多次)
Update()(多次)
OnSerialize()(多次)
Player销毁阶段 Player销毁阶段
OnNetworkDestroy()
OnDisable()
OnDestroy()
OnServerDisconnect
Host销毁阶段 Host销毁阶段
OnStopHost()
OnStopServer()
ServerChangeScene()
(LocalClient混杂进来的Server函数)
OnStopClient()
(LocalClient混杂进来的Client函数)

函数时序的详细文档及分析

以下为严格按照时间次序进行罗列的UNet函数时序,附上官方文档。重要地方也结合源码进行解释。

Dedicated Server情况

Dedicated Server的Server初始化阶段

NetworkManager.Awake()
NetworkManager目前的Awake()(被不好地设计)为非virtual的私有方法。所以子类应注意不能再定义Awake(),否则将hide掉基类的Awake()。

NetworkManager.Start()

NetworkManager (NewBorn.NBNetworkManager).Start()

NetworkManager.OnStartServer()

public void OnStartServer();
Description
This hook is invoked when a server is started - including when a host is started.
StartServer has multiple signatures, but they all cause this hook to be called.

Server初始化函数。调用肯定比看似相似的OnStartClient()早。
注意在OnStartServer()之后,才进行网络Connect的初始化、才进行场景的切换。

NetworkManager (NewBorn.NBNetworkManager).OnStartServer()

NetworkManager.ServerChangeScene()

public void ServerChangeScene(string newSceneName);
Parameters
newSceneName The name of the scene to change to. The server will change scene immediately, and a message will be sent to connected clients to ask them to change scene also.
Description
This causes the server to switch scenes and sets the networkSceneName.
Clients that connect to this server will automatically switch to this scene. This is called autmatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.

StartServer()里、OnStartServer()之后,调用ServerChangeScene()进行场景切换。之后在任意时刻,也可以手动调用它进行中途的场景切换。
在ServerChangeScene()里,会发出MsgType.Scene通知当前已连接上的Client也进行场景的切换。

Battle_Demo_Official
NetworkManager (NewBorn.NBNetworkManager).ServerChangeScene()

NetworkManager.OnServerSceneChanged()

public void OnServerSceneChanged(string sceneName);
Parameters
sceneName The name of the new scene.
Description
Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().

Server完成场景切换后的一个回调。
在本回调之前,Server会收集场景所有已有NetworkIdentity的GameObject,并发出Spawn的Message,从而通知已连接上的Client进行Spawn。

Battle_Demo_Official
NetworkManager (NewBorn.NBNetworkManager).OnServerSceneChanged()
2016-01-01T10:57:28.8472060+08:00

至此,Server的初始化阶段结束。之后(通过上面的时间10:57:28和下面的时间11:04:10就可以看出时间差),当有Client连接进Server的时候,函数流程就进入该Client的初始化阶段。

Dedicated Server的Client初始化阶段

NetworkManager.OnServerConnect()

public void OnServerConnect(Networking.NetworkConnection conn);
Parameters
conn Connection from client.
Description
Called on the server when a new client connects.

新玩家新Client和Server建立连接后的回调函数。
Client刚连接上来,第一个问题肯定是“我现在在什么场景?”。所以在本回调之前,Server会发出MsgType.Scene,通知客户端进行场景加载。

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerConnect()
2016-01-01T11:04:10.7621350+08:00

NetworkManager.OnServerReady()

public void OnServerReady(Networking.NetworkConnection conn);
Parameters
conn Connection from client.
Description
Called on the server when a client is ready.
The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process

当Client“准备好”(加载好场景,一些自定义的初始化)后,需要发送MsgType.Ready给Server。
Server收到这个Message了之后,就会调用本OnServerReady()函数。
Client准备好了之后,接着问题是“我Client当前场景有什么网络对象可见和需要同步?”
所以在OnServerReady()里会调用NetworkServer.SetClientReady(),进行该Client的可见性检测,然后在NetworkServer.SendSpawnMessage()里下发MsgType.ObjectSpawn进行Spawn。

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerReady()

至此,Client已经连接好、加载好场景、同步好已有的网络对象。
所以Server将进入Player初始化阶段。

Dedicated Server的Player初始化阶段

NetworkManager.OnServerAddPlayer()

public void OnServerAddPlayer(Networking.NetworkConnection conn, short playerControllerId);
Parameters
conn Connection from client.
playerControllerId Id of the new player.
extraMessageReader An extra message object passed for the new player.
Description
Called on the server when a client adds a new player with ClientScene.AddPlayer.
The default implementation for this function creates a new player object from the playerPrefab.

新连接上来的Client连接好了、场景准备好了、其他有NetworkIdentity的GameObject同步好了,接下来准备为该Client准备属于它自己的Player了。
通过调用ClientScene.AddPlayer()发出MsgType.AddPlayer可以通知服务器添加属于该connection的Player,然后Server就响应该Message会调用OnServerAddPlayer()。
用户可以在OnServerAddPlayer()自定义新建Player的逻辑,包括直接Instantiate新Player、或者从自己的Spawn机制里取出Player、给Player修改初始化属性等。
然后,在OnServerAddPlayer()里就会调用NetworkServer.AddPlayerForConnection(),继而一系列初始化Player逻辑(生成netId、决定Observer、收集SyncVar、发送MsgType.Spawn给Client),通知Client真正去创建Player。

hostId: 0 connectionId: 1 isReady: True channel count: 2, 0
NetworkManager (NewBorn.NBNetworkManager).OnServerAddPlayer()

至此,Player的Prefab在Server已被Instantiate出来,继而调用

等函数,正式开始NetworkIdentity/NetworkBehaviour的函数流程。
NetworkBehaviour和NetworkIdentity是相互依存的。许多NetworkBehaviour的UNet相关函数事实上都是在其配对的NetworkIdentity中被NetworkIdentity所触发调用的。

NetworkBehaviour.Awake()
留意到这次测试Player的GameObject的instanceID是-10746。

只有localPlayerAuthority=True
这个UNet配置变量是合法的。其他UNet变量都是非法的。

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).Awake()

NetworkBehaviour.OnEnable()

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnEnable()

NetworkBehaviour.OnStartServer()

public void OnStartServer();
Description
This hook is invoked when a server is started - including when a host is started.
StartServer has multiple signatures, but they all cause this hook to be called.

在NetworkIdentity.OnStartServer()里,

  • 会cache住NetworkIdentity所同GameObject的所有NetworkBehaviour
  • 会生成netId给自己
  • 通过NetworkServer.instance.SetLocalObjectOnServer(),更新isServer的标志位
  • 调用这些NetworkBehaviour的OnStartServer()函数。

所以这个时候,netIdisServer合法了。

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnStartServer()

NetworkBehaviour.OnRebuildObservers()

public bool OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize);
Parameters
observers The new set of observers for this object.
initialize True if the set of observers is being built for the first time.
Returns
bool Return true if this function did work.
Description
Callback used by the visibility system to (re)construct the set of observers that can see this object.
Implementations of this callback should add network connections of players that can see this object to the observers set.

这个新的NetworkIdentity在Server创建了,但哪些Client是其真正的“观察者”(Observer)呢?只有这些观察者Client,才需要在他们的运行时里创建这个新NetworkIdentity及其GameObject。

NetworkIdentity会调用其所有NetworkBehaviour的OnRebuildObservers()。
默认情况下,是当前已连接的所有Client都能观察到这个新NetworkIdentity。
但如果有NetworkBehaviour的OnRebuildObservers()返回了true,则以HashSet<NetworkConnection> observers里存在的连接作为Observer。

observers=System.Collections.Generic.HashSet`1[UnityEngine.Networking.NetworkConnection], initialize=True, 
go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnRebuildObservers()

NetworkBehaviour.OnSerialize()

public bool OnSerialize(Networking.NetworkWriter writer, bool initialState);
Parameters
writer Writer to use to write to the stream.
initialState If this is being called to send initial state.
Returns
bool True if data was written.
Description
Virtual function to override to send custom serialization data.

决定了Observer后、给这些Observer发送MsgType.Spawn之前,需要在Server把这个新NetworkIdentity的GameObject的所有同步属性进行序列化

OnSerialize()和OnDeserialize()
是用于自定义NetworkBehaviour中变量的序列化和反序列化的虚函数。前者必然是只在Server被调用、后者必然是只在Client被调用。
事实上,[SyncVar]修饰的变量和SyncList变量都是通过编译时UNet将这些变量的序列化反序列化逻辑自动生成在OnSerialize()和OnDeserialize()中的。
所以要注意,如果你在NetworkBehaviour中显式override掉了这两个函数,则该NetworkBehaviour的[SyncVar]修饰的变量和SyncList变量都需要你自行编写代码实现序列化反序列化。

serializeCount=1, writer=UnityEngine.Networking.NetworkWriter, initialState=True,
go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnSerialize()

NetworkBehaviour.Start()

一帧真正开始。

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).Start()

至此,在Dedicated Server的Player初始化阶段已结束。接下来是Player运转阶段。

Dedicated Server的Player运转阶段

NetworkBehaviour.FixedUpdate()(多次)

fixedUpdateCount=22, go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).FixedUpdate()

NetworkBehaviour.Update()(多次)

updateCount=22, go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).Update()

NetworkBehaviour.OnSerialize()(多次)

serializeCount=16, writer=UnityEngine.Networking.NetworkWriter, initialState=False,
go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnSerialize()

Dedicated Server的Player销毁阶段

通过调用NetworkServer.Destroy(gameObject);,gameObject进入销毁阶段。

NetworkBehaviour.OnDisable()

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnDisable()

NetworkBehaviour.OnDestroy()
留意到所有变量皆已非法。
留意到在Server并不会调用OnNetworkDestroy()。

go.instanceID=-10746,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 2 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnDestroy()

至此,Player的NetworkIdentity/NetworkBehaviour流程结束。

NetworkManager.OnServerDisconnect()

public void OnServerDisconnect(Networking.NetworkConnection conn);
Parameters
conn Connection from client.
Description Called on the server when a client disconnects.

hostId: 0 connectionId: 2 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerDisconnect()

Dedicated Server的Server销毁阶段

略。因为在Server kill掉Unity不能及时输出日志。

至此,Player的整个Dedicated Server流程结束。


Remote Client情况

在上面已有Dedicated Server情况的前提下,Remote Client情况将适度从简,仅针对差异性进行描述。

Remote Client的Client初始化阶段

NetworkManager.Awake()
NetworkManager目前的Awake()(被不好地设计)为非virtual的私有方法。所以子类应注意不能再定义Awake(),否则将hide掉基类的Awake()。

NetworkManager.Start()

NetworkManager (NewBorn.NBNetworkManager).Start()

NetworkManager.OnStartClient()

public void OnStartClient(Networking.NetworkClient client);
Parameters
client The NetworkClient object that was started.
Description
This is a hook that is invoked when the client is started.
StartClient has multiple signatures, but they all cause this hook to be called.

当调用NetworkManager.StartClient()的时候,在其内部进行:

  • Client注册各种Client相关的MsgType监听(MsgType.Connect/Disconnect/Scene/
  • 连接,
  • 然后会调用OnStartClient()
UnityEngine.Networking.NetworkClient
NetworkManager (NewBorn.NBNetworkManager).OnStartClient()

Client连接成功后,第一个问题肯定是“我现在在什么场景?”。通过之前Dedicated Server情况的分析可知,Server会在Client连接成功后、OnServerConnect()之前通过MsgType.Scene通知客户端切换场景
所以此时之后,Client将进行场景加载并成功。

BattleStarter.Awake()(场景中本就有的GameObject)

BattleStarter (NewBorn.BattleStarter).Awake()

GlobalObject.OnLevelWasLoaded()(加载场景前就DontDestroyOnLoad的GameObject)

GlobalObject (MoreFun.GlobalObjectComponent).OnLevelWasLoaded()

NetworkManager.OnClientConnect()

public void OnClientConnect(Networking.NetworkConnection conn);
Parameters
conn Connection to the server.
Description
Called on the client when connected to a server.
The default implementation of this function sets the client as ready and adds a player.

当场景加载成功后,才在NetworkManager.FinishLoadScene()里调用OnClientConnect()
在OnClientConnect()里,当没有OnlineScene或当前就是OnlineScene时,就会立刻调用ClientScene.Ready()告诉Server本Client已准备好。
所以此时的isReady是False。

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnClientConnect()

NetworkManager.OnClientSceneChanged()

public void OnClientSceneChanged(Networking.NetworkConnection conn);
Parameters
conn The network connection that the scene change message arrived on.
Description
Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.

当场景加载成功后、调用OnClientConnect()后、在NetworkManager.FinishLoadScene()里继续调用OnClientSceneChanged()

OnClientSceneChanged()必然会调用ClientScene.Ready()告诉Server本Client已准备好。所以根据之前的Dedicated Server情况分析可知,Server会在OnServerReady()里会调用NetworkServer.SetClientReady(),进行该Client的可见性检测、并进行已在Server的NetworkIdentity的GameObject进行反序列化和Spawn。
然后,如果NetworkManager配置成AutoCreatePlayer为true,则OnClientSceneChanged()还会在本Client找不到LocalPlayer时调用ClientScene.AddPlayer(0)通知Server生成本Client的玩家。

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnClientSceneChanged()

其他Player的GameObject的其他脚本的Awake()
因此,其他Player就会比LocalPlayer先行在本Client被Spawn出来。

go.instanceID=-64522
NBPlayer(Clone) (NewBorn.PlayerController).Awake()

BattleStarter.OnStartClient()
本身就在场景里的有NetworkIdentity的GameObject也比LocalPlayer先行被Spawn出来。

BattleStarter (NewBorn.BattleStarter).OnStartClient()

至此,Remote Client的Client本身就初始化好了。
接下来,由于ClientScene.OnObjectSpawn()监听了MsgType.ObjectSpawn,所以当Server生成本Client的LocalPlayer(或者Spawn其他任意GameObject时),本Client都会进入NetworkIdentity/NetworkBehaviour的函数流程。

Remote Client的Player初始化阶段

NetworkBehaviour.Awake()
只有localPlayerAuthority=True这个UNet配置变量是合法的。其他UNet变量都是非法的。

go.instanceID=-65642,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).Awake()

NetworkBehaviour.OnEnable()

go.instanceID=-65642,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnEnable()

NetworkBehaviour.OnDeserialize()

public void OnDeserialize([Networking.NetworkReader reader, bool initialState);
Parameters
reader Reader to read from the stream.
initialState True if being sent initial state.
Description
Virtual function to override to receive custom serialization data.

Server把这个新NetworkIdentity的GameObject的所有同步属性进行序列化会连同发送MsgType.Spawn一并下发。
所以Client接受Server的数据后也通过OnDeserialize()在本地进行反序列化。留意到这是第一次反序列化initialState=True

OnSerialize()和OnDeserialize()
是用于自定义NetworkBehaviour中变量的序列化和反序列化的虚函数。前者必然是只在Server被调用、后者必然是只在Client被调用。
事实上,[SyncVar]修饰的变量和SyncList变量都是通过编译时UNet将这些变量的序列化反序列化逻辑自动生成在OnSerialize()和OnDeserialize()中的。
所以要注意,如果你在NetworkBehaviour中显式override掉了这两个函数,则该NetworkBehaviour的[SyncVar]修饰的变量和SyncList变量都需要你自行编写代码实现序列化反序列化。

deserializeCount=1, reader=NetBuf sz:87 pos:87, initialState=True,
go.instanceID=-65642,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnDeserialize()

NetworkBehaviour.PreStartClient()

public void PreStartClient();
Description
An internal method called on client objects to resolve GameObject references.

留意到经过上一步的OnDeserialize()之后,合法变量为

  • netId=7
  • isClient=True
  • isServer=False
  • gameObject.name已经改变为跟server所给予的名字(NBPlayer7)。
  • 事实上,所有SyncVar此时皆已合法。
go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).PreStartClient()

NetworkBehaviour.OnStartClient()

public void OnStartClient();
Description
Called on every NetworkBehaviour when it is activated on a client.
Objects on the host have this function called, as there is a local client on the host. The values of SyncVars on object are guaranteed to be initialized correctly with the latest state from the server when this function is called on the client.

和PreStartClient()没什么区别。当然,SyncVar已经合法。

go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnStartClient()

NetworkBehaviour.OnStartLocalPlayer()

public void OnStartLocalPlayer();
Description
Called when the local player object has been set up.
This happens after OnStartClient(), as it is triggered by an ownership message from the server. This is an appropriate place to activate components or functionality that should only be active for the local player, such as cameras and input.

在一个Client运行时中,只有一个Connection,有很多个Player。众多Player中,只有和这个Connection绑定起来的Player,才“提拔为”LocalPlayer,代表的是本Client玩家的“MyPlayer”。所以,此时合法的变量就比较好理解了。
合法变量:

  • playerControllerId=0
  • connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
  • isLocalPlayer=True

Server发送MsgType.Owner给Client,然后Client就进行LocalPlayer的更新
注意,从源码看来。LocalPlayer可以有多个。

另,应注意,LocalPlayer的“Local”,和Host模式下的LocalClient的“Local”可不是同一个概念,应分清区别:

  • LocalClient的Local可理解为“同机器的”:是Host模式下,和Server同处于一部物理机器上的一种特殊的Client。LocalClient的并列相反概念是RemoteClient,RemoteClient是指和Server处于不同物理机器上的常见Client。
  • LocalPlayer的Local可理解为“我自己的”,不管是RemoteClient,还是LocalClient,它们都会有本客户端的自己的LocalPlayer。LocalPlayer的并列相反概念是DumbPlayer(作者本人喜欢的叫法),DumbPlayer是指本客户端代表其他玩家的Player。
go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnStartLocalPlayer()

NetworkBehaviour.OnStartAuthority()

public void OnStartAuthority();
Description
This is invoked on behaviours that have authority, based on context and the LocalPlayerAuthority value on the NetworkIdentity.
This is called after OnStartServer and OnStartClient.When NetworkIdentity.AssignClientAuthority() is called on the server, this will be called on the client that owns the object. When an object is spawned with NetworkServer.SpawnWithClientAuthority(), this will be called on the client that owns the object.

当一个NetworkIdentity配置有LocalPlayerAuthority时,此NetworkIdentity认为是可以授权给Client的。只有一个Client真正有Authority的时候,才可以在该GameObject的NetworkBehaviour中发送Command给Server。
什么时候Client才真正有Authority呢?LocalPlayer都是有Authority的。另自Unity5.2开始,也允许非Player在运行时通过在Server调用NetworkIdentity.AssignClientAuthority()NetworkServer.SpawnWithClientAuthority()将Authority赋予特定的Connection,即指定该Connection的Client也拥有该非Player的Authority,即允许该Client也可以在该非Player的NetworkBehaviour中发Command给Server。此时OnStartAuthority()这个函数是可以在那个时候再被调用到的。

Authority要么只有Server拥有,要么只有Client拥有。

合法变量:

  • hasAuthority=True
go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnStartAuthority()

NetworkBehaviour.Start()
一帧真正开始。

go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).Start()

至此,NetworkBehaviour的初始化阶段已结束。接下来是正常运转阶段。

Remote Client的Player运转阶段

NetworkBehaviour.FixedUpdate()(多次)

fixedUpdateCount=15, go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).FixedUpdate()

NetworkBehaviour.Update()(多次)

updateCount=4, go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).Update()

NetworkBehaviour.OnDeserialize()(多次)

deserializeCount=5, reader=NetBuf sz:28 pos:28, initialState=False, go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnDeserialize()

Remote Client的Player销毁阶段

通过调用NetworkServer.Destroy(gameObject);,gameObject进入销毁阶段。

NetworkBehaviour.OnNetworkDestroy()

public void OnNetworkDestroy();
Description
This is invoked on clients when the server has caused this object to be destroyed.
This can be used as a hook to invoke effects or do client specific cleanup.

留意只有客户端才会被调用OnNetworkDestroy()。

go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnNetworkDestroy()

NetworkBehaviour.OnDisable()

go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnDisable()

NetworkBehaviour.OnDestroy()

go.instanceID=-65642,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=
connectionToServer=hostId: 0 connectionId: 1 isReady: True channel count: 2
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=True
isLocalPlayer=True
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnDestroy()

Remote Client的Client销毁阶段

通过点击NetworkManagerHUD的Stop按钮,停止Remote Client。

NetworkManager.OnStopHost()

public void OnStopHost();
Description
This hook is called when a host is stopped.

NetworkManagerHUD有bug。Remote Client情况点击它也调用StopHost(),所以OnStopHost()也会被错误地调用。
正式情况应忽略。

NetworkManager (NewBorn.NBNetworkManager).OnStopHost()

NetworkManager.OnStopClient()

public void OnStopClient();
Description
This hook is called when a client is stopped.

在NetworkManager.StopClient()调用时,先调用NetworkManager.OnStopClient(),然后再断连接、清除GameObject、跳转到OfflineScene。

NetworkManager (NewBorn.NBNetworkManager).OnStopClient()

至此,Remote Client整个流程结束。


Host情况

在已有上面Dedicated Server、Remote Client的情况,Host情况也将适当从略。

Host初始化阶段

NetworkManager.Start()

NetworkManager (NewBorn.NBNetworkManager).Start()

NetworkManager.StartHost()

public Networking.NetworkClient StartHost();
Returns
NetworkClient The client object created - this is a "local client".
Description
This starts a network "host" - a server and client in the same application.
The client returned from StartHost() is a special "local" client that communicates to the in-process server using a message queue instead of the real network. But in almost all other cases, it can be treated as a normal client.

NetworkManager (NewBorn.NBNetworkManager).StartHost()

NetworkManager.OnStartHost()

NetworkManager (NewBorn.NBNetworkManager).OnStartHost()

NetworkManager.OnStartServer()

NetworkManager (NewBorn.NBNetworkManager).OnStartServer()

NetworkManager.ServerChangeScene()

Battle_Demo_Official
NetworkManager (NewBorn.NBNetworkManager).ServerChangeScene()

NetworkManager.OnServerConnect()(2次)(LocalClient混杂进来的Server函数)

hostId: -1 connectionId: 0 isReady: False channel count: 0
NetworkManager (NewBorn.NBNetworkManager).OnServerConnect()

NetworkManager.OnStartClient()(LocalClient混杂进来的Client函数)

UnityEngine.Networking.LocalClient
NetworkManager (NewBorn.NBNetworkManager).OnStartClient()

NetworkManager.OnClientConnect()(LocalClient混杂进来的Client函数)

hostId: -1 connectionId: 0 isReady: False channel count: 0
NetworkManager (NewBorn.NBNetworkManager).OnClientConnect()

NetworkManager.OnServerSceneChanged()

Battle_Demo_Official
NetworkManager (NewBorn.NBNetworkManager).OnServerSceneChanged()

NetworkManager.OnClientSceneChanged()(LocalClient混杂进来的Client函数)

hostId: -1 connectionId: 0 isReady: False channel count: 0
NetworkManager (NewBorn.NBNetworkManager).OnClientSceneChanged()

NetworkManager.OnServerReady()(LocalClient混杂进来的Server函数)

hostId: -1 connectionId: 0 isReady: False channel count: 0
NetworkManager (NewBorn.NBNetworkManager).OnServerReady()

NetworkManager.OnServerAddPlayer()(LocalClient混杂进来的Server函数)

hostId: -1 connectionId: 0 isReady: True channel count: 0, 0
NetworkManager (NewBorn.NBNetworkManager).OnServerAddPlayer()

至此,Host的Server初始化逻辑(混杂着LocalClient的初始化逻辑)结束。
进入Player初始化阶段。

Host情况的Player初始化阶段

NetworkManager.OnServerConnect()

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerConnect()

NetworkManager.OnServerReady()

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerReady()

NetworkManager.OnServerAddPlayer()

hostId: 0 connectionId: 1 isReady: True channel count: 2, 0
NetworkManager (NewBorn.NBNetworkManager).OnServerAddPlayer()

至此,开始Player的Prefab在Host已被Instantiate出来。正式开始NetworkIdentity/NetworkBehaviour的函数流程。

NetworkBehaviour.Awake()

go.instanceID=-78256,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).Awake()

NetworkBehaviour.OnEnable()

go.instanceID=-78256,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=0
playerControllerId=-1
connectionToClient=
connectionToServer=
isClient=False
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnEnable()

NetworkBehaviour.OnStartServer()
合法变量:

  • netId=2。由于是Host,所以不需调用OnDeserialize()。立刻确定了netId。
  • isServer=True
go.instanceID=-78256,go=NBPlayer(Clone) (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=False
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer(Clone) (MoreFun.TestNetworkBehaviour).OnStartServer()

NetworkBehaviour.PreStartClient()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).PreStartClient()

NetworkBehaviour.OnStartClient()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnStartClient()

NetworkBehaviour.OnRebuildObservers()

observers=System.Collections.Generic.HashSet`1[UnityEngine.Networking.NetworkConnection], initialize=True, 
go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnRebuildObservers()

NetworkBehaviour.OnSerialize()(多次)

serializeCount=1, writer=UnityEngine.Networking.NetworkWriter, initialState=True, go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnSerialize()

NetworkBehaviour.OnSetLocalVisibility()

public void OnSetLocalVisibility(bool vis);
Parameters
vis New visibility state.
Description
Callback used by the visibility system for objects on a host.
Objects on a host (with a local client) cannot be disabled or destroyed when they are not visibile to the local client. So this function is called to allow custom code to hide these objects. A typical implementation will disable renderer components on the object. This is only called on local clients on a host.

vis=True, go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnSetLocalVisibility()

NetworkBehaviour.Start()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).Start()

至此,Host情况的NetworkIdentity/NetworkBehaviour的初始化阶段已结束。接下来是正常运转阶段。

Host情况的Player运转阶段

NetworkBehaviour.FixedUpdate()(多次)

fixedUpdateCount=5, 
go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).FixedUpdate()

NetworkBehaviour.Update()(多次)

updateCount=4, 
go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).Update()

NetworkBehaviour.OnSerialize()(多次)

serializeCount=4, writer=UnityEngine.Networking.NetworkWriter, initialState=False, go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnSerialize()

Host情况的Player销毁阶段

通过调用NetworkServer.Destroy(gameObject);,gameObject进入销毁阶段。

NetworkBehaviour.OnNetworkDestroy()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=True
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnNetworkDestroy()

NetworkBehaviour.OnDisable()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=7
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnDisable()

NetworkBehaviour.OnDestroy()

go.instanceID=-78256,go=NBPlayer7 (UnityEngine.GameObject)
netId=0
playerControllerId=0
connectionToClient=hostId: 0 connectionId: 1 isReady: True channel count: 2
connectionToServer=
isClient=True
isServer=False
localPlayerAuthority=True
hasAuthority=False
isLocalPlayer=False
NBPlayer7 (MoreFun.TestNetworkBehaviour).OnDestroy()

NetworkManager.OnServerDisconnect()

hostId: 0 connectionId: 1 isReady: False channel count: 2
NetworkManager (NewBorn.NBNetworkManager).OnServerDisconnect()

至此,Host情况的Player流程结束。

Host销毁流程

NetworkManager.OnStopHost()

NetworkManager (NewBorn.NBNetworkManager).OnStopHost()

NetworkManager.OnStopServer()

NetworkManager (NewBorn.NBNetworkManager).OnStopServer()

NetworkManager.ServerChangeScene()

BattleOffline
NetworkManager (NewBorn.NBNetworkManager).ServerChangeScene()

NetworkManager.OnStopClient()

NetworkManager (NewBorn.NBNetworkManager).OnStopClient()

至此,Host情况全部流程结束。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的阅读 13,304评论 5 6
  • 晴空万里 尘埃 肆意飞扬着 黄色的生命 分子般 在平坦的路上奔波 阴雨连绵 水涡 轻松转悠着 五彩的灵魂 仙子般 ...
    千里非辞阅读 183评论 0 2
  • 卓麦阅读 156评论 1 1
  • 《一往情深》 天冷了,路上的行人很少,我冷的发抖,搓了搓冻僵的手指,但想到马上回到家了,...
    楠二阅读 614评论 7 11