1. 服务器端初期配置与测试
1. 1 配置环境
在photonserver官网(https://doc.photonengine.com)下载SDK,解压后放于一个英文目录,会得到 如下文件,并注册账户,然后下载Photon server Licenses,100ccu免费,然后将证书文件放于deploy\bin_Win64目录,
deploy目录中deploy\bin_Win64下的PhotonControl文件是启动photonserver的,启动后如图;
用vs新建项目,项目类型为类库,即dll文件,因为photonserver会加载dll文件,工程引用所需dll,有三个,在lib文件夹下,分别为ExitGamesLibs,Photon.SocketServer,PhotonHostRuntimeInterfaces;
在deploy目录下新建MyGameServer文件夹,再建bin目录,用于存放工程的生成文件,打开工程属性,将生成目录设置为deploy/MyGameServer/bin;推荐使用目标框架为.NET Framework 4.5;准备工作完成。
1.2 Server的启动
新建类MyGameServer.cs,为主类,继承自ApplicationBase;再新建一个ClientPeer类,进行连接后的处理,继承自Photon.SocketServer.ClientPeer。
1.2.1 ClientPeer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
using PhotonHostRuntimeInterfaces;
namespace MyGameServer
{
public class ClientPeer : Photon.SocketServer.ClientPeer
{
public ClientPeer(InitRequest initRequest) : base(initRequest)
{
}
/// <summary>
/// 处理断开连接的处理工作
/// </summary>
/// <param name="reasonCode"></param>
/// <param name="reasonDetail"></param>
protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
{
}
/// <summary>
/// 处理客户端的请求
/// </summary>
/// <param name="operationRequest"></param>
/// <param name="sendParameters"></param>
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
}
}
}
1.2.2 MyGameServer.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
namespace MyGameServer
{
//所有的server端,主类都要继承自ApplicationBase
public class MyGameServer : ApplicationBase
{
//当一个客户端请求连接时调用
//使用peerbase表示和一个客户端的连接
protected override PeerBase CreatePeer(InitRequest initRequest)
{
return new ClientPeer(initRequest);
}
/// <summary>
/// server启动后就调用,初始化
/// </summary>
protected override void Setup()
{
}
/// <summary>
/// server端关闭时
/// </summary>
protected override void TearDown()
{
}
}
}
1.3. PhotonServer.config的配置
需要在PhotonServer.config文件中配置自己的Application,文件中已经有一个LoadBalancing的配置和MMoDemo的配置,Application格式都差别不大,复制MMoDemo将MMoDemo标签更改为MyGameInstance,具体修改如下。
<?xml version="1.0" encoding="Windows-1252"?>
<!--
(c) 2015 by Exit Games GmbH, http://www.exitgames.com
Photon server configuration file.
For details see the photon-config.pdf.
This file contains two configurations:
"LoadBalancing"
Loadbalanced setup for local development: A Master-server and a game-server.
Starts the apps: Game, Master, CounterPublisher
Listens: udp-port 5055, tcp-port: 4530, 843 and 943
-->
<Configuration>
<!-- Multiple instances are supported. Each instance has its own node in the config file. -->
<LoadBalancing
MaxMessageSize="512000"
MaxQueuedDataPerPeer="512000"
PerPeerMaxReliableDataInTransit="51200"
PerPeerTransmitRateLimitKBSec="256"
PerPeerTransmitRatePeriodMilliseconds="200"
MinimumTimeout="5000"
MaximumTimeout="30000"
DisplayName="LoadBalancing (MyCloud)">
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<!-- Port 5055 is Photon's default for UDP connections. -->
<UDPListeners>
<UDPListener
IPAddress="0.0.0.0"
Port="5055"
OverrideApplication="Master">
</UDPListener>
<UDPListener
IPAddress="0.0.0.0"
Port="5056"
OverrideApplication="Game">
</UDPListener>
</UDPListeners>
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<TCPListeners>
<!-- TCP listener for Game clients on Master application -->
<TCPListener
IPAddress="0.0.0.0"
Port="4530"
OverrideApplication="Master"
PolicyFile="Policy\assets\socket-policy.xml"
InactivityTimeout="10000"
>
</TCPListener>
<TCPListener
IPAddress="0.0.0.0"
Port="4531"
OverrideApplication="Game"
PolicyFile="Policy\assets\socket-policy.xml"
InactivityTimeout="10000">
</TCPListener>
<!-- DON'T EDIT THIS. TCP listener for GameServers on Master application -->
<TCPListener
IPAddress="0.0.0.0"
Port="4520">
</TCPListener>
</TCPListeners>
<!-- Policy request listener for Unity and Flash (port 843) and Silverlight (port 943) -->
<PolicyFileListeners>
<!-- multiple Listeners allowed for different ports -->
<PolicyFileListener
IPAddress="0.0.0.0"
Port="843"
PolicyFile="Policy\assets\socket-policy.xml">
</PolicyFileListener>
<PolicyFileListener
IPAddress="0.0.0.0"
Port="943"
PolicyFile="Policy\assets\socket-policy-silverlight.xml">
</PolicyFileListener>
</PolicyFileListeners>
<!-- WebSocket (and Flash-Fallback) compatible listener -->
<WebSocketListeners>
<WebSocketListener
IPAddress="0.0.0.0"
Port="9090"
DisableNagle="true"
InactivityTimeout="10000"
OverrideApplication="Master">
</WebSocketListener>
<WebSocketListener
IPAddress="0.0.0.0"
Port="9091"
DisableNagle="true"
InactivityTimeout="10000"
OverrideApplication="Game">
</WebSocketListener>
</WebSocketListeners>
<!-- Defines the Photon Runtime Assembly to use. -->
<Runtime
Assembly="PhotonHostRuntime, Culture=neutral"
Type="PhotonHostRuntime.PhotonDomainManager"
UnhandledExceptionPolicy="Ignore">
</Runtime>
<!-- Defines which applications are loaded on start and which of them is used by default. Make sure the default application is defined. -->
<!-- Application-folders must be located in the same folder as the bin_win32 folders. The BaseDirectory must include a "bin" folder. -->
<Applications Default="Master">
<Application
Name="Master"
BaseDirectory="LoadBalancing\Master"
Assembly="Photon.LoadBalancing"
Type="Photon.LoadBalancing.MasterServer.MasterApplication"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config"
>
</Application>
<Application
Name="Game"
BaseDirectory="LoadBalancing\GameServer"
Assembly="Photon.LoadBalancing"
Type="Photon.LoadBalancing.GameServer.GameApplication"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config">
</Application>
<!-- CounterPublisher Application -->
<Application
Name="CounterPublisher"
BaseDirectory="CounterPublisher"
Assembly="CounterPublisher"
Type="Photon.CounterPublisher.Application"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config">
</Application>
</Applications>
</LoadBalancing>
<!-- Instance settings -->
<MMoDemo
MaxMessageSize="512000"
MaxQueuedDataPerPeer="512000"
PerPeerMaxReliableDataInTransit="51200"
PerPeerTransmitRateLimitKBSec="256"
PerPeerTransmitRatePeriodMilliseconds="200"
MinimumTimeout="5000"
MaximumTimeout="30000"
DisplayName="MMO Demo"
>
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<!-- Port 5055 is Photon's default for UDP connections. -->
<UDPListeners>
<UDPListener
IPAddress="0.0.0.0"
Port="5055"
OverrideApplication="MMoDemo">
</UDPListener>
</UDPListeners>
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<!-- Port 4530 is Photon's default for TCP connecttions. -->
<!-- A Policy application is defined in case that policy requests are sent to this listener (known bug of some some flash clients) -->
<TCPListeners>
<TCPListener
IPAddress="0.0.0.0"
Port="4530"
PolicyFile="Policy\assets\socket-policy.xml"
InactivityTimeout="10000"
OverrideApplication="MMoDemo"
>
</TCPListener>
</TCPListeners>
<!-- Policy request listener for Unity and Flash (port 843) and Silverlight (port 943) -->
<PolicyFileListeners>
<!-- multiple Listeners allowed for different ports -->
<PolicyFileListener
IPAddress="0.0.0.0"
Port="843"
PolicyFile="Policy\assets\socket-policy.xml"
InactivityTimeout="10000">
</PolicyFileListener>
<PolicyFileListener
IPAddress="0.0.0.0"
Port="943"
PolicyFile="Policy\assets\socket-policy-silverlight.xml"
InactivityTimeout="10000">
</PolicyFileListener>
</PolicyFileListeners>
<!-- WebSocket (and Flash-Fallback) compatible listener -->
<WebSocketListeners>
<WebSocketListener
IPAddress="0.0.0.0"
Port="9090"
DisableNagle="true"
InactivityTimeout="10000"
OverrideApplication="MMoDemo">
</WebSocketListener>
</WebSocketListeners>
<!-- Defines the Photon Runtime Assembly to use. -->
<Runtime
Assembly="PhotonHostRuntime, Culture=neutral"
Type="PhotonHostRuntime.PhotonDomainManager"
UnhandledExceptionPolicy="Ignore">
</Runtime>
<!-- Defines which applications are loaded on start and which of them is used by default. Make sure the default application is defined. -->
<!-- Application-folders must be located in the same folder as the bin_win32 folders. The BaseDirectory must include a "bin" folder. -->
<Applications Default="MMoDemo">
<!-- MMO Demo Application -->
<Application
Name="MMoDemo"
BaseDirectory="MmoDemo"
Assembly="Photon.MmoDemo.Server"
Type="Photon.MmoDemo.Server.PhotonApplication"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config">
</Application>
<!-- CounterPublisher Application -->
<Application
Name="CounterPublisher"
BaseDirectory="CounterPublisher"
Assembly="CounterPublisher"
Type="Photon.CounterPublisher.Application"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config">
</Application>
</Applications>
</MMoDemo>
<!--MyGameInstance为修改的名字,DisplayName为PhotonServer下显示的名字,其他默认-->
<MyGameInstance
MaxMessageSize="512000"
MaxQueuedDataPerPeer="512000"
PerPeerMaxReliableDataInTransit="51200"
PerPeerTransmitRateLimitKBSec="256"
PerPeerTransmitRatePeriodMilliseconds="200"
MinimumTimeout="5000"
MaximumTimeout="30000"
DisplayName="My Game"
>
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<!-- Port 5055 is Photon's default for UDP connections. -->
<!--OverrideApplication修改为自定义的,其他默认-->
<UDPListeners>
<UDPListener
IPAddress="0.0.0.0"
Port="5055"
OverrideApplication="MyGame1">
</UDPListener>
</UDPListeners>
<!-- 0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here. -->
<!-- Port 4530 is Photon's default for TCP connecttions. -->
<!-- A Policy application is defined in case that policy requests are sent to this listener (known bug of some some flash clients) -->
<!--OverrideApplication修改为自定义的,其他默认-->
<TCPListeners>
<TCPListener
IPAddress="0.0.0.0"
Port="4530"
PolicyFile="Policy\assets\socket-policy.xml"
InactivityTimeout="10000"
OverrideApplication="MyGame1"
>
</TCPListener>
</TCPListeners>
<!-- Defines the Photon Runtime Assembly to use. -->
<Runtime
Assembly="PhotonHostRuntime, Culture=neutral"
Type="PhotonHostRuntime.PhotonDomainManager"
UnhandledExceptionPolicy="Ignore">
</Runtime>
<!-- Defines which applications are loaded on start and which of them is used by default. Make sure the default application is defined. -->
<!-- Application-folders must be located in the same folder as the bin_win32 folders. The BaseDirectory must include a "bin" folder. -->
<Applications Default="MyGame1">
<!-- MyGame Application -->
<!--Name修改自定义,BaseDirectory为自己程序集所在目录,BaseDirectory="MyGameServer表示
在deploy目录的MyGameServer目录下;Assembly为程序集名字,Type为程序集中主类;
其他默认-->
<Application
Name="MyGame1"
BaseDirectory="MyGameServer"
Assembly="MyGameServer"
Type="MyGameServer.MyGameServer"
ForceAutoRestart="true"
WatchFiles="dll;config"
ExcludeFiles="log4net.config">
</Application>
</Applications>
</MyGameInstance>
</Configuration>
1.4. 日志的配置与输出
再引入日志所需库,也在lib文件夹下,分别为ExitGames.Logging.Log4Net,log4net;还需要一个日志的配置文件,可以自己写也可以拷贝提供的Demo中日志配置文件做修改;在此就拷贝src-server\Mmo\Photon.MmoDemo.Server下的log4net.config文件到本工程的根目录,并设置文件属性“复制到输出目录”为始终复制,
将日志配置文件修改<filetype="log4net.Util.PatternString"value="%property{Photon:ApplicationLogPath}\\MyGame.Server.log" />,其中只修改了日志文件名字,即MyGame,其他暂为默认。整个文件如下:
<?xml version="1.0" encoding="utf-8" ?>
<log4net debug="false" update="Overwrite">
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file type="log4net.Util.PatternString" value="%property{Photon:ApplicationLogPath}\\MyGame.Server.log" />
<appendToFile value="true" />
<maximumFileSize value="5000KB" />
<maxSizeRollBackups value="2" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%d [%t] %-5p %c - %m%n" />
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
</layout>
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="DEBUG" />
<levelMax value="FATAL" />
</filter>
</appender>
<!-- logger -->
<root>
<level value="INFO" />
<!--<appender-ref ref="ConsoleAppender" />-->
<appender-ref ref="RollingFileAppender" />
</root>
<logger name="OperationData">
<level value="INFO" />
</logger>
</log4net>
然后再在主类MyGameServer中做日志的初始化和配置。最后如下
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ExitGames.Logging;
using ExitGames.Logging.Log4Net;
using log4net.Config;
using Photon.SocketServer;
namespace MyGameServer
{
//所有的server端,主类都要继承自ApplicationBase
public class MyGameServer : ApplicationBase
{
private static readonly ILogger log = LogManager.GetCurrentClassLogger();//日志
//当一个客户端请求连接时调用
//使用peerbase表示和一个客户端的连接
protected override PeerBase CreatePeer(InitRequest initRequest)
{
log.Info("一个客户端连接进来了.................");
return new ClientPeer(initRequest);
}
/// <summary>
/// server启动后就调用,初始化
/// </summary>
protected override void Setup()
{
//日志的初始化
log4net.GlobalContext.Properties["Photon:ApplicationLogPath"]=Path.Combine(
Path.Combine( this.ApplicationRootPath,"bin_Win64"),"log");//设置日志输出路径
FileInfo configFileInfo = new FileInfo(Path.Combine(this.BinaryPath, "log4net.config")); //BinaryPath为工程的bin目录
if (configFileInfo.Exists)
{
LogManager.SetLoggerFactory(Log4NetLoggerFactory.Instance);//让photon知道使用哪个日志插件
XmlConfigurator.ConfigureAndWatch(configFileInfo);//让log4net这个插件读取配置文件
}
log.Info("Setup Completed!");
}
/// <summary>
/// server端关闭时
/// </summary>
protected override void TearDown()
{
log.Info("服务器应用关闭了");
}
}
}
之后如要打印日志就可直接使用log对象进行各种日志的输出。
最后生成工程,然后deploy/MyGameServer/bin文件如下:
然后就可打开eploy\bin_Win64下PhotonControl文件,然后启动MyGame应用,日志文件会生成在deploy\bin_Win64\log下。
2. 客户端
新建unity工程,在建文件夹Plugins将\lib文件夹的Photon3Unity3D放于unity工程的Plugins文件夹下。
Server与Client“交流有三种方式”,当客户端向服务器发送请求用Peer.OpCustom()方法,服务器端会受到请求在OnOperationRequest中处理,然后用SendOperationResponse再回应请求(SendOperationResponse只能在OnOperationRequest中调用),而客户端又会在OnOperationResponse()中处理服务器端回应的请求;而服务器端还可以主动向客户端发送事件,不论在什么时候都可以发送事件Event,用SendEvent发送事件,客户端接收到事件后在OnEvent()中处理。整个过程如下:
2.1 连接服务器(PhotonEngine.cs)
继承自IPhotonPeerListener,挂载到场景中。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ExitGames.Client.Photon;
public class PhotonEngine : MonoBehaviour,IPhotonPeerListener {
private static PhotonEngine Instance;
private static PhotonPeer peer;
public static PhotonPeer Peer
{
get
{
return peer;
}
}
void Awake()
{
if(Instance==null)
{
Instance = this;
DontDestroyOnLoad(this.gameObject);
}
else if(Instance!=this)//若跳转到其他场景,删除多余的PhotonEngine
{
Destroy(this.gameObject);
return;
}
}
// Use this for initialization
void Start () {
//通过Listender接收服务器端的响应
peer = new PhotonPeer(this, ConnectionProtocol.Udp);
peer.Connect("127.0.0.1:5055", "MyGame1");//连接服务器,指定IP的端口号,第二个参数为应用名
}
// Update is called once per frame
void Update()
{
peer.Service();//需要一直调用
}
/// <summary>
/// 游戏停止或者关闭时,要断开连接
/// </summary>
void OnDestroy()
{
if(peer!=null&&peer.PeerState==PeerStateValue.Disconnected)
{
peer.Disconnect();
}
}
public void DebugReturn(DebugLevel level, string message)
{
}
public void OnEvent(EventData eventData)
{
switch(eventData.Code)
{
case 1:
Debug.Log("收到服务器发送过来的事件:"+ eventData.Code);
//**************/解析收到的服务器端的数据****************
Dictionary<byte, object> dataFromServer = eventData.Parameters;
object intValue, stringValue;
dataFromServer.TryGetValue(1, out intValue);
dataFromServer.TryGetValue(2, out stringValue);
Debug.Log(intValue.ToString() + " " + stringValue.ToString());
//**************/解析收到的服务器端的数据****************
break;
}
}
public void OnOperationResponse(OperationResponse operationResponse)
{
switch(operationResponse.OperationCode)
{
case 1:
Debug.Log("收到了服务器响应:" + operationResponse.OperationCode);
//**************/解析收到的服务器端的数据****************
Dictionary<byte, object> dataFromServer = operationResponse.Parameters;
object intValue, stringValue;
dataFromServer.TryGetValue(1, out intValue);
dataFromServer.TryGetValue(2, out stringValue);
Debug.Log(intValue.ToString() + " " + stringValue.ToString());
//**************/解析收到的服务器端的数据****************
break;
case 2:
break;
default:
break;
}
}
public void OnStatusChanged(StatusCode statusCode)
{
Debug.Log(statusCode);
}
}
2.2 测试连接服务器(Test.cs)
挂载到场景中,按下鼠标左键会打印信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour {
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
if(Input.GetMouseButtonDown(0))
{
SendRequest();
}
}
void SendRequest()
{
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add(1, 45);
data.Add(2, "这是给服务器的数据");
PhotonEngine.Peer.OpCustom(1, data, true);
}
}
2.3 服务器端的请求处理、发送、事件(ClientPeer.cs)
服务器端的处理都封装在了ClientPeer中,所以修改仍是对此修改。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
using PhotonHostRuntimeInterfaces;
namespace MyGameServer
{
public class ClientPeer : Photon.SocketServer.ClientPeer
{
public ClientPeer(InitRequest initRequest) : base(initRequest)
{
}
/// <summary>
/// 处理断开连接的处理工作
/// </summary>
/// <param name="reasonCode"></param>
/// <param name="reasonDetail"></param>
protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
{
}
/// <summary>
/// 处理客户端的请求
/// </summary>
/// <param name="operationRequest"></param>
/// <param name="sendParameters"></param>
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
switch (operationRequest.OperationCode)//通过OPCode区分请求
{
case 1:
MyGameServer.log.Info("收到了一个客户端请求");
//**************/解析收到的客户端数据****************
Dictionary<byte, object> dataFromClient = operationRequest.Parameters;
object intValue, stringValue;
dataFromClient.TryGetValue(1, out intValue);
dataFromClient.TryGetValue(2, out stringValue);
MyGameServer.log.Info("得到的参数数据是:" + intValue.ToString() + " " + stringValue.ToString());
//**************/解析收到的客户端数据****************
//**************/发送给客户端数据****************
OperationResponse opResponse = new OperationResponse(1);
Dictionary<byte, object> dataToClient = new Dictionary<byte, object>();
dataToClient.Add(1, 99);
dataToClient.Add(2, "这是给客户端的string数据");
opResponse.SetParameters(dataToClient);
SendOperationResponse(opResponse, sendParameters);//给客户端一个响应,SendOperationResponse只能在OnOperationRequest中调用
//**************/发送给客户端数据****************
//主动向客户端发起事件
EventData ed = new EventData(1);
ed.Parameters = dataToClient;
SendEvent(ed, new SendParameters()); //SendEvent可以在任何地方调用,在其他地方调用要引入ClientPeer
break;
case 2:
break;
default:
break;
}
}
}
}