客户端如何把消息发到服务器?服务器显示客户端发送的内容
通信组件
//最大程度上把协议的封包和处理隐藏在底层,让开发者只接触到协议;
//例如:发消息只需要调用一个Send
NetworkClient&TcpSocketListener
网络客户端:发送,接收消息;把数据包发到服务器
服务端监听器:接收,响应消息;服务器对数据将进行响应
//实现对原始的数据包(字节数据)进行收发,网络协议以字节为基本单元进行数据的收发(数据会变成字节流)
封包处理器PackageHandler
封包和协议(protobuf)之间的转换;//做双向转换
封包处理负责把网络上接收的数据还原成Protobuf的协议结构;或者把Protobuf的协议结构转换成字节数据
消息分发器MessageDistributer
//是一种服务,有Start,Stop之类
主要用在服务端收到客户端发来的消息,把消息分配到服务器的各个模块,主要用来做消息的接收和分发;
这里面为了提高消息的分发速度,在服务端使用了多线程
//客户端的消息分发器不需要多线程
消息分配处理MessageDispatch
//相当于函数,维护什么消息由谁来处理
与消息分发器一起工作,分发器负责基础消息的分发,不管消息是什么;而哪个消息分发到哪由消息分配处理负责
客户端Client中
的NetClient
通过类视图观察
连接到服务端
Connect
若做断线重连则调用了重连机制
客户端给服务端通讯,首先连接到服务端
public void Connect(int times = DEF_TRY_CONNECT_TIMES)
{//参数调用了重拾次数
if (this.connecting)
{//校验是否有连接
return;
}
if (this.clientSocket != null)
{//若原来有连接,把原来的连接关闭掉
this.clientSocket.Close();
}
if (this.address == default(IPEndPoint))
{
throw new Exception("Please Init first.");
}
Debug.Log("DoConnect");
this.connecting = true;
this.lastSendTime = 0;
this.DoConnect();
}
DoConnect
客户端连接服务器的过程是一种阻塞;能实时等待连接的结果
同时用了一种异步的方式(IAsyncResult;异步的方法(BeginConnect)调用完成后不会再等待
需要连接到服务器,所以需要等待(能保证连接成功或是失败能及时得到通知
捕获到足够多的异常
连接成功通讯转为非阻塞
//这里网络上用了阻塞,不用多线程会采用同样的效果:发了消息,网络差会阻塞应用
阻塞:当线程发起一个I/O请求后,如果数据尚未就绪,当前线程会被挂起,知道数据准备好并返回结果位置
//线程会暂停,直到I/O操作完成;发信息,若服务器不返回会一直等待
非阻塞:如果I/O操作不能立即完成(数据未就绪)调用会立即返回一个错误或指示操作未完成的值;这允许程序继续执行其他任务,直到再次需要检查I/O状态是否改变
//线程不会被挂起,可以继续执行其他任务,直到I/O操作完成或出现错误
void DoConnect()
{
Debug.Log("NetClient.DoConnect on " + this.address.ToString());
try
{//防止连接过程中出错,捕获信息
if (this.clientSocket != null)
{
this.clientSocket.Close();
}
//使用TCP协议则需要这种方式New
this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.clientSocket.Blocking = true;//使用Blocking阻塞状态
Debug.Log(string.Format("Connect[{0}] to server {1}", this.retryTimes, this.address) + "\n");
//异步的方法基本通过Begin..或End..实现
IAsyncResult result = this.clientSocket.BeginConnect(this.address, null, null);
//返回了一个接口reslut
//根据接口判断是否需要等待;这里用Wait等待了一个超时时间
bool success = result.AsyncWaitHandle.WaitOne(NetConnectTimeout);
//Wait会等待异步事件完成之后再执行
//若不适用Wait;则逻辑完全不阻塞往下执行
if (success)
{//若连接成功则调用End把异步请求结束
this.clientSocket.EndConnect(result);
}
}
catch(SocketException ex)
{
//catch到连接被服务器拒绝(网络专有的异常
if(ex.SocketErrorCode == SocketError.ConnectionRefused)
{
this.CloseConnection(NET_ERROR_FAIL_TO_CONNECT);
}
Debug.LogErrorFormat("DoConnect SocketException:[{0},{1},{2}]{3} ", ex.ErrorCode,ex.SocketErrorCode,ex.NativeErrorCode, ex.ToString());
}
//常规的异常
catch (Exception e)
{
Debug.Log("DoConnect Exception:" + e.ToString() + "\n");
}
//前面的连接执行完了,再判断连接状态是否成功
if (this.clientSocket.Connected)
{//连接成功,把阻塞的模式设置成False
this.clientSocket.Blocking = false;//即游戏运行时服务端客户端的通讯采用非阻塞
this.RaiseConnected(0, "Success");
}
else
{
this.retryTimes++;
if (this.retryTimes >= this.retryTimesTotal)
{
this.RaiseConnected(1, "Cannot connect to server");
}
}
this.connecting = false;//标记连接过程完成
}
发消息到服务端
SendMessage
判断连接
发送消息
//send a Protobuf message
public void SendMessage(NetMessage message)
{//NetMessage是ProtoBuf生成的//写协议文件,执行代码生成,到这里直接Send
if (!running)
{
return;
}
if (!this.Connected)
{
this.receiveBuffer.Position = 0;
this.sendBuffer.Position = sendOffset = 0;
//判断没连接再连接一次
this.Connect();
Debug.Log("Connect Server before Send Message!");
return;
}
//发送队列;执行发送时不影响玩家体验;要发送的东西放在队列中
//发的时候一个个发;真正发出去是在Update
sendQueue.Enqueue(message);
if (this.lastSendTime == 0)
{
this.lastSendTime = Time.time;
}
}
Update
public void Update()
{
if (!running)
{
return;
}
if (this.KeepConnect())
{ //保证断线重连;若断开则重连
if (this.ProcessRecv())
{//每帧判断是否有收到的数据,有则先处理收到的数据
//只接收,从服务器发到网卡,从网卡提到内存中
if (this.Connected)
{//接受完可能会断线,需要在判断一次
this.ProcessSend();//发送消息
this.ProceeMessage();//处理消息;在这里真正执行
}
}
}
}
ProceeMessage
void ProceeMessage()
{//分发器分发
MessageDistributer.Instance.Distribute();
}
在接收时,ProcessRecv
使用了Poll
bool ProcessRecv()
{
bool ret = false;
try
{
if (this.clientSocket.Blocking)
{
Debug.Log("this.clientSocket.Blocking = true\n");
}
bool error = this.clientSocket.Poll(0, SelectMode.SelectError);
//类似于Linux的服务器开发
if (error)
{
Debug.Log("ProcessRecv Poll SelectError\n");
this.CloseConnection(NET_ERROR_SEND_EXCEPTION);
return false;
}
//非阻塞形式高效查询数据
ret = this.clientSocket.Poll(0, SelectMode.SelectRead);
//Poll查询了底层事件,数据是否可读;如果没有数据则返回速度块
if (ret)
{
//接收
int n = this.clientSocket.Receive(this.receiveBuffer.GetBuffer(), 0, this.receiveBuffer.Capacity, SocketFlags.None);
if (n <= 0)
{
this.CloseConnection(NET_ERROR_ZERO_BYTE);
return false;
}
//接收完成后把数据丢到接收缓冲区(packageHandler
this.packageHandler.ReceiveData(this.receiveBuffer.GetBuffer(), 0, n);
//给packageHandler后调用一次分发
}
}
catch (Exception e)
{
Debug.Log("ProcessReceive exception:" + e.ToString() + "\n");
this.CloseConnection(NET_ERROR_ILLEGAL_PACKAGE);
return false;
}
return true;
}
//小图的这些脚本都在服务端中
PackageHandler(处理了粘包问题
ReceiveData
public void ReceiveData(byte[] data,int offset,int count)
{//用流来接收
if(stream.Position + count > stream.Capacity)
{//接收后判断是否超容量;本地缓冲是否够
throw new Exception("PackageHandler write buffer overflow");
}
//够则写进流中
stream.Write(data, offset, count);
//在做数据包的解析
ParsePackage();
}
ParsePackage
把网络收下的所有数据包调用Protobuf的协议
做封包的处理;若包没收完或不完整或收的多连在一起//粘包
bool ParsePackage()
{
if (readOffset + 4 < stream.Position)
{
int packageSize = BitConverter.ToInt32(stream.GetBuffer(), readOffset);
if (packageSize + readOffset + 4 <= stream.Position)
{//包有效
//解包UnpackMessage//Protobuf的方法
SkillBridge.Message.NetMessage message = UnpackMessage(stream.GetBuffer(), this.readOffset + 4, packageSize);
if (message == null)
{
throw new Exception("PackageHandler ParsePackage faild,invalid package");
}
MessageDistributer<T>.Instance.ReceiveMessage(this.sender, message);
this.readOffset += (packageSize + 4);
return ParsePackage();
}
}
//未接收完/要结束了
if (this.readOffset > 0)
{
long size = stream.Position - this.readOffset;
if (this.readOffset < stream.Position)
{
Array.Copy(stream.GetBuffer(), this.readOffset, stream.GetBuffer(), 0, stream.Position - this.readOffset);
}
//Reset Stream
this.readOffset = 0;
stream.Position = size;
stream.SetLength(size);
}
return true;
}
public static SkillBridge.Message.NetMessage UnpackMessage(byte[] packet,int offset,int length)
{
SkillBridge.Message.NetMessage message = null;
using (MemoryStream ms = new MemoryStream(packet, offset, length))
{//解包
message = ProtoBuf.Serializer.Deserialize<SkillBridge.Message.NetMessage>(ms);
}
return message;
}
//关于消息分配(common中
两个接口:
分发响应;给客户端用
分发请求;给服务器用
MessageDispatch<T>接口调用分发
if (message.userRegister != null) { MessageDistributer<T>.Instance.RaiseEvent(sender, message.userRegister); }
消息分配知道消息该分发给谁;具体的分发工作让分发器做
//关于消息分发(服务器通讯
有订阅Subscribe:要想知道消息需要订阅,有消息则会发送,不订阅则不会收到
取消订阅Unsubscribe
//服务器通讯使用了多线程;客户端用单线程
在Start方法中有线程数
public void Start(int ThreadNum)
{
this.ThreadCount = ThreadNum;
if (this.ThreadCount < 1) this.ThreadCount = 1;
if (this.ThreadCount > 1000) this.ThreadCount = 1000;
Running = true;
for (int i = 0; i < this.ThreadCount; i++)
{//使用了线程池
ThreadPool.QueueUserWorkItem(new WaitCallback(MessageDistribute));
}
while (ActiveThreadCount < this.ThreadCount)
{
Thread.Sleep(100);
}
}
服务端GameServer
//涉及多线程模型处理网络收发;基本的接收有基本的接收和收发
TcpSocketListener
private Socket listenerSocket;
Socket跟客户端一样声明一个变量;
Start中New完绑定在一个端口上并且监听
public void Start()
{
lock (this)
{
if (!IsRunning)
{
listenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listenerSocket.Bind(endPoint);
listenerSocket.Listen(connectionBacklog);
BeginAccept(args);
}
else
throw new InvalidOperationException("The Server is already running.");
}
}
服务端有一个监听,接收,再是数据的收发
//服务端监听时确认是否有客户端连接过来;连过来一个客户端会执行接受,接收后客户端才能开始通讯
区别
客户端创建网络对象直接发送连接,连接到服务器;
服务器先让自己绑定在一个端口上(从某个端口监听;客户端连接过来之后服务器就接受;连接正常建立;双方可以正常通讯;
//若服务器不接受客户端无法发信息给服务器
//端口:不同程序在同一个IP地址和同一个网卡下面进行数据传输
private void OnSocketAccepted(object sender, SocketAsyncEventArgs e)
{//这里使用了异步编程
SocketError error = e.SocketError;
if (e.SocketError == SocketError.OperationAborted)
return; //Server was stopped
if (e.SocketError == SocketError.Success)
{//若服务器被接受,这里会传进来是哪个客户端
Socket handler = e.AcceptSocket;//这里是客户端对应的Socket
OnSocketConnected(handler);
}
lock (this)
{
BeginAccept(e);
}
}
//关于扩展方法类ExtensionMethods
对某个类做扩展可以产生一种额外的方法//这里没有具体讲
//关于网络连接NetConnection
定义了一个连接,有效管理客户端和服务端的通讯(客户端连入服务端则会创建一个连接;若网络中断,Connection也会中断;
Connection中会有SendData,BeginReceive等
在监听器中
private void BeginAccept(SocketAsyncEventArgs args)
{
args.AcceptSocket = null;
listenerSocket.AcceptAsync(args);
/*listenerSocket.InvokeAsyncMethod(new SocketAsyncMethod(listenerSocket.AcceptAsync)
, OnSocketAccepted, args);*/
}
//接上面的若服务器被接受,进入OnSocketConnected
private void OnSocketConnected(Socket client)
{
if (SocketConnected != null)
SocketConnected(this, client);
}
SocketConnected注册给了NetService
//对用户提供服务:Service
NetService是从这里开始启用的
GameServer是从Program来的
//前边是配置文件,然后是New GameServer->New NetService->定8000端口->Init(int port)中创建监听器(在NetService中
NetService中的Init:
static TcpSocketListener ServerListener;
public bool Init(int port)
{
//创建一个监听器
ServerListener = new TcpSocketListener("127.0.0.1", GameServer.Properties.Settings.Default.ServerPort, 10);
//事件和回调;
ServerListener.SocketConnected += OnSocketConnected;
return true;
}
private void OnSocketConnected(object sender, Socket e)
{
//在 OnSocketConnected中,如果一个服务器连接上了一个客户端,则查看客的IP
IPEndPoint clientIP = (IPEndPoint)e.RemoteEndPoint;
//可以在这里对IP做一级验证,比如黑名单
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
NetSession session = new NetSession();
//若验证通过,创建一个网络连接;网络连接中也有一个回调,断开时发生DisconnectedCallback,接受数据时发生DataReceivedCallback
NetConnection<NetSession> connection = new NetConnection<NetSession>(e, args,
new NetConnection<NetSession>.DataReceivedCallback(DataReceived),
new NetConnection<NetSession>.DisconnectedCallback(Disconnected), session);
Log.WarningFormat("Client[{0}]] Connected", clientIP);
}
static void DataReceived(NetConnection<NetSession> sender, DataEventArgs e)
{
Log.WarningFormat("Client[{0}] DataReceived Len:{1}", e.RemoteEndPoint, e.Length);
//由包处理器处理封包
lock (sender.packageHandler)
{
sender.packageHandler.ReceiveData(e.Data, 0, e.Data.Length);
}
//PacketsPerSec = Interlocked.Increment(ref PacketsPerSec);
//RecvBytesPerSec = Interlocked.Add(ref RecvBytesPerSec, e.Data.Length);
}
NetService的使用:
创建一个监听器,监听器提供一个事件,
当客户端连上来了,新建一个连接,给连接两个回调,
收到数据封包处理器
//Start时把刚才创建的监听器启动了
public void Start()
{
//启动监听
Log.Warning("Starting Listener...");
ServerListener.Start();
MessageDistributer<NetConnection<NetSession>>.Instance.Start(8);
Log.Warning("NetService Started");
}
//因为消息分发器在服务端,客户端不需要如此调用;
//服务端使用了多线程因此可以Start,把线程数传入(以上代码启用了8个线程数来做消息分发
//因为大部分的CPU都是8核的;每个核心分在一个线程上是最有效率的(因此一般按照CPU核心的整数倍设置线程数
关于服务端的分发器
启动和停止
public void Start()
{
//启动监听
Log.Warning("Starting Listener...");
ServerListener.Start();
MessageDistributer<NetConnection<NetSession>>.Instance.Start(8);
Log.Warning("NetService Started");
}
public void Stop()
{
Log.Warning("Stop NetService...");
ServerListener.Stop();
Log.Warning("Stoping Message Handler...");
MessageDistributer<NetConnection<NetSession>>.Instance.Stop();
}
在UserService中
public UserService()
{
//创建一个分发器的单例然后执行订阅;这里订阅登录消息,这是登陆消息的处理器:UserLoginRequest
MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserLoginRequest>(this.OnLogin);
MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserRegisterRequest>(this.OnRegister);
}
用this.OnLogin方法执行收到的UserLoginRequest消息;
//OnLogin方法+有了以上的一行订阅->只要有消息来服务器,OnLogin中的
NetMessage message = new NetMessage();
就会被调用
//收到消息后给客户端回发;即New一个NetMessage;填充信息;把信息打包封装成字节流;发给客户端
OnLogin.cs
void OnLogin(NetConnection<NetSession> sender,UserLoginRequest request)
{
Log.InfoFormat("UserLoginRequest: User:{0} Pass:{1}", request.User, request.Passward);
NetMessage message = new NetMessage();
message.Response = new NetMessageResponse();
message.Response.userLogin = new UserLoginResponse();
TUser user = DBService.Instance.Entities.Users.Where(u => u.Username == request.User).FirstOrDefault();
if(user==null)
{
message.Response.userLogin.Result = Result.Failed;
message.Response.userLogin.Errormsg = "用户不存在";
}
else if(user.Password != request.Passward)
{
message.Response.userLogin.Result = Result.Failed;
message.Response.userLogin.Errormsg = "密码错误";
}
else
{
sender.Session.User = user;//将当前会话的用户设置为user
message.Response.userLogin.Result = Result.Success;//设置响应结果;表示操作成功
message.Response.userLogin.Errormsg = "None";
//初始化获取了用户ID,玩家信息和角色信息
message.Response.userLogin.Userinfo = new NUserInfo();
message.Response.userLogin.Userinfo.Id = 1;
message.Response.userLogin.Userinfo.Player = new NPlayerInfo();
message.Response.userLogin.Userinfo.Player.Id = user.Player.ID;
foreach(var c in user.Player.Characters)
{//从已经创建的角色中找
NCharacterInfo info = new NCharacterInfo();
info.Id = c.ID;
info.Name = c.Name;
info.Class = (CharacterClass)c.Class;
//把数据库当前已经有的填充到协议中发给客户端
message.Response.userLogin.Userinfo.Player.Characters.Add(info);//添加角色到响应
}
}
byte[] data = PackageHandler.PackMessage(message);
sender.SendData(data, 0, data.Length);
}
总结!在服务器中信息的收发
1.UserService中的订阅一行:保证能收到来自客户端的信息
2.订阅方法中的
保证把消息发给客户端
//这两处代码中间//对应例如登录消息的逻辑:查连接数据库,有无用户名和密码之类 ;把结果告诉客户端
//这些都在UserService中
手动演示
启动服务器
GameService中添加
添加服务器
点击启动~~
发现没有启动数据库
启动数据库SSMS
把DBService注释掉就不会连DB了
再启动服务端
//可以看到8个分发线程已经启动;NetService网络服务也已经启动
//输入Exit退出
启动客户端
新建一个场景,新建一个脚本挂在摄像机上
在脚本里面使用net Client连接
Connect
把网络客户端绑上来
Unity启动~~
发现没有连网络地址address
初始化IP,端口做连接
//关于IP地址:
若用多台电脑(服务端和客户端不在同电脑上
Network.NetClient.Instance.Init("****", 8000);
****位置表示的是连接到的服务端的IP地址
再启动~客户端和服务器显示
连接上IP和端口;连接到服务器
这里客户端端口34062已经连接到服务器
关掉Unity服务端
后面这条显示服务端断开
总结!
客户端连接到服务器需要的代码:
Network.NetClient.Instance.Init("127.0.0.1", 8000);
Network.NetClient.Instance.Connect();
服务器完成一个服务的监听需要:
声明一个NetService;
New;
指定监听的端口号;
执行一次Start;
(结束时需要执行Stop;
用特定协议发消息
打开proto
在里面添加一个测试消息
添加一条完整的新协议
保存
点击.cmd文件生成协议
在服务器选中解决方案生成
把服务端生成的协议放在客户端下
打开客户端的代码(exLogin.cs)
//所有协议的前缀:SkillBridge.Message
创建主消息
创建自定义消息
自定义消息填充数据
调用Send发送
//完整的客户端:
服务端添加一个HelloWorld
里面有init,Start,Stop
服务端接收:OnFirstTestRequest
把Service写成单例
//这里的Sender可以知道连进来的客户端,发送了什么数据...
GameServer添加这两
启动~~
服务端收到了DataReceived
但是消息“Helloworld!!!!"没有执行
消息分发
在消息分配器里面没有分发器负责把消息分发到服务器
在Request中添加一行
生成解决方案再启动服务端
//于是我们的helloWorld就水灵灵的出现了~
总结!
服务端:
的总结在上面
客户端:
1.遵循所有的网络请求都有Service负责,构建一个HelloWorldService,构建一个消息处理函数用来处理网络消息
2.处理的消息在Start里订阅;告诉网络底层,要用这个方法OnFirstTestRequest处理协议FirstTestRequest;OnFirstTestRequest是协议的处理器
3.在Game Server中要启动Init,Start
加协议:
1.打开proto生成新协议
2.生成解决方案
3.复制到服务端
处理消息:
在分发器中Add一行
解答:
客户端连接成功服务器;服务器完成一个服务的监听
在客户端的代码中选定消息填充数据,发送
//若是添加了特定消息要在Gameserver中启用;以及添加消息分发语句