Tcp通讯
一款网络游戏分为客户端和服务端两个部分, 客户端程序运行在用户的电脑或手机上,服务端程序运行在游戏运营商的服务器上。多个客户端通过网络与服务端通信。TCP 连接指的是一种游戏中常用的网络通信协议, 与之对应的还有UDP 协议、KCP 协议、HTTP 协议等。本片文章只讨论Tcp通讯。
先来认识Socket
- 网络上的两个程序通过一个双向的通信连接实现数据交换, 这个连接的一端称为一个Socket。一个Socket包含了进行网络通信必需的五种信息:连接使用的协议、本地主机的IP地址、本地的协议端口、远程主机的IP地址和远程协议端口,如果把Socket理解成一台手机, 那么本地主机IP地址和端口相当于自己的手机号码, 远程主机IP地址和端门相当于对方的号码。至少需要两台手机才能打电话, 同样地, 至少需要两个Socket 才能进行网络通信。
理解IP地址和端口
IP地址
- 网络上的计算机都是通过IP地址识别的, 应用程序通过通信端口彼此通信。通俗地讲, 可以理解为每一个IP地址对应于一台计算机(实际上一台计算机可以有多个IP地址,此处仅作方便理解的解释)。
- 在Wmdows命令提示符中输入ipconfig, 便能够查看本机的IP地址,本地机进程间通信时Ip可使用:127.0.0.1。至于ipconfig查询出的ip与127.0.0.1之间的区别,可以自己百度
端口
“端口” 是英文port的意译, 是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口。通俗地讲, 每个Socket连接都是从一台计算机的一个端口连接到另外一台计符机的某个端口
代码部分
写代码前先了解通讯协议
- 通信协议是通信双方对数据传送控制的一种约定, 通信双方必须共同遵守, 方能“ 知道对方在说什么” 和“让对方听懂我的话” 。例如, 当有玩家在场景里面走动, 就需要将位置信息广播给其他在线玩家, 那么该发送什么样的数据给服务端呢?本文会使用一种最简单的字符串协议来实现。协议格式如下所示, 消息名和消息体用“ |" 隔开, 消息体中各个参数用“ ,“ 隔开。
- 本文使用的协议格式:消息名|参数1,参数2,…
- 当然协议格式可以是其他的形式,但是要做到服务端和客户端的统一;
- 要做什么事情由消息名决定, 消息名为“ Move"表示移动, “ Leave" 表示离开, “Enter" 表示进入场景…;
- 以移动消息为例,它至少需要告诉其他在线玩家以下信息:
- 谁在移动—通过参数1表明身份, 可以使用客户端的IP和端口表示;
- 目的地是什么—通过参数2到4说明目的地坐标点。
- 所以该客户端会发送类似下面的字符串给其他客户端。其中: “Move" 代表这条协议是移动同步协议, “127.0.0.1: 1234" 代表了客户端的身份, “10,0,8" 三个值代表目的地的坐标。
- Move I 12 7. 0. 0. 1:1234,20,0,50,
- 其他客户端收到服务端转发的字符串后, 使用Split(’|’)和Split(’,’)便可将协议中各个参数解析出来, 进而处理数据。
格外注意内容
- 在Unity中, 只有主线程才能操作Unity组件,所以在下列代码部分处理客户端接收的数据时我们会创建一个消息列表, 每当收到消息便在列表末端添加数据, 这个列表由主线程读取, 它可以作为主线程和异步接收线程之间的桥梁。由于MonoBehaviour的Update方法在主线程中执行、可让Update方法每次从消息列表中读取几条信息并处理, 处理后便在消息列表中删除它们。
编写客户端程序
在实际的网络游戏开发中, 网络模块往往是作为一个底层模块用的, 它应该和具体的游戏逻辑分开, 而不应该把处理逻辑的代码和网络模块糅杂在一起。一个可行的做法是,给网络管理类添加回调方法, 当收到某种消息时就自动调用某个函数, 这样便能够将游戏逻辑和底层模块分开。
- 网络模块中最核心的地方是一个称为NetManager的静态类, 这个类对外提供了三个最主要的接口。
- Connect方法, 调用后发起连接;
- AddListener方法, 消息监听。其他模块可以通过AddListener设置某个消息名对应的处理方法, 当网络校块接收到这类消息时, 就会回调处理方法;
- Send方法, 发送消息给服务端。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public static class NetManager
{
static Socket socket;
//接收缓冲区
static byte[] readBuff = new byte[1024];
//委托类型
public delegate void MsgListener(string str);
//监听列表
private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
//消息列表
static List<string> msgList = new List<string>();
//是否已经连接服务器
public static bool isConnected
{
get
{
bool connected = true;
if (socket==null||!socket.Connected)
{
connected = false;
}
return connected;
}
}
//添加监听
public static void AddListener(string msgName, MsgListener listener)
{
listeners[msgName] = listener;
}
//获取描述
public static string GetDesc()
{
if (!isConnected)
{
return "";
}
return socket.LocalEndPoint.ToString();
}
//连接
public static void Connect(string ip, int port)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAd = IPAddress.Parse(ip);
socket.BeginConnect(ipAd, port, ConnectCallback, socket);
}
static void ConnectCallback(IAsyncResult ar)
{
try
{
Socket _socket = ar.AsyncState as Socket;
_socket.EndConnect(ar);
_socket.BeginReceive(readBuff, 0, readBuff.Length, SocketFlags.None, ReceiveCallback, _socket);
}
catch (Exception e)
{
Debug.Log("Error:" + e.ToString());
}
}
static void ReceiveCallback(IAsyncResult ar)
{
try
{
Socket _socket = ar.AsyncState as Socket;
int count = _socket.EndReceive(ar);
string recvStr = Encoding.Default.GetString(readBuff, 0, count);
msgList.Add(recvStr);
_socket.BeginReceive(readBuff, 0, readBuff.Length, SocketFlags.None, ReceiveCallback, _socket);
}
catch (Exception e)
{
Debug.Log("Error:" + e.ToString());
}
}
//发送
public static void Send(string sendStr)
{
if (string.IsNullOrEmpty(sendStr) || socket == null || !socket.Connected)
{
return;
}
byte[] sendBytes = Encoding.Default.GetBytes(sendStr);
socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, SendCallback, socket);
}
static void SendCallback(IAsyncResult ar)
{
try
{
Socket _socket = ar.AsyncState as Socket;
_socket.EndSend(ar);
}
catch (Exception e)
{
Debug.Log("Error:" + e.ToString());
}
}
public static void Update()
{
if (msgList.Count <= 0)
{
return;
}
string msgStr = msgList[0];
msgList.RemoveAt(0);
string[] split = msgStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
//监听回调
if (listeners.ContainsKey(msgName))
{
//处理接收到的消息
listeners[msgName](msgArgs);
}
}
}
- 简单的NetManager类已经构建完毕,但是它仍然不够健壮,代码没有处理粘包分包、线程冲突等问题等待后续完善。。。
经过上面的努力我们现在已经能够对服务器发起连接、发送消息、接收消息,但是现在我们还是缺少对接收的消息进行下一步的处理,所以新建一个Main组件, 并挂到场景中任一物体上。在Start方法中涸用NetManager的AddListener方法, 分别监听Enter、Move和Leave三个协议, 然后调用Connect方法连接服务端
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Main : MonoBehaviour
{
void Start()
{
//添加消息处理事件
NetManager.AddListener("Enter", OnEnter);
NetManager.AddListener("Move", OnMove);
NetManager.AddListener("Leave", OnLeave);
NetManager.AddListener("List", OnList);
NetManager.Connect("127.0.0.1", 8888);
StartCoroutine(WaitConnect());
}
//等待连接服务器
IEnumerator WaitConnect() {
float timer = 0;
while (!NetManager.isConnected)
{
timer += Time.deltaTime;
Debug.Log("Wait Connect...");
if (timer>5)
{
break;
}
yield return new WaitForFixedUpdate();
}
if (!NetManager.isConnected)
{
Debug.LogError("连接超时...");
yield break;
}
Init();
}
//连接上服务器之后需要处理的一些内容可以写在这个方法中
void Init(){
}
private void Update()
{
NetManager.Update();
}
//收到服务器消息 有玩家进入游戏
void OnEnter(string msg) {
Debug.Log("Enter:"+msg);
//解析参数
string[] split = msg.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float euly = float.Parse(split[4]);
if (desc==NetManager.GetDesc())//是自己
{
return;
}
Debug.Log("服务器通知:"+desc+" 自己:"+NetManager.GetDesc());
//添加一个角色
}
//收到服务器消息 有玩家移动
void OnMove(string msg) {
Debug.Log("Move:" + msg);
string[] split = msg.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//处理其他玩家移动
}
//收到服务器消息 有玩家掉线
void OnLeave(string msg) {
Debug.Log("Leave:" + msg);
//解析参数
string[] split = msg.Split(',');
string desc = split[0];
//玩家desc掉线处理
}
//收到服务器消息 自己请求的玩家列表
private void OnList(string msg)
{
Debug.Log("List:" + msg);
//解析参数
string[] split = msg.Split(',');
int count = (split.Length - 1) / 6;
for (int i = 0; i < count; i++)
{
string desc = split[i * 6 + 0];
float x = float.Parse(split[i * 6 + 1]);
float y = float.Parse(split[i * 6 + 2]);
float z = float.Parse(split[i * 6 + 3]);
float eulY = float.Parse(split[i * 6 + 4]);
int hp = int.Parse(split[i * 6 + 5]);
//是自己
if (desc == NetManager.GetDesc())
{
continue;
}
//添加一个角色
}
}
}
至此我们的客户端初步具备了与服务器沟通并翻译服务器信息的简单能力
编写服务端程序
既然客户端可以通过AddListener把网络协议和具体的处理函数对应起来, 那服务端能不能有类似的机制,把底层网络模块和具体的消息处理函数分开呢?答案必须是肯定的。
如果网络模块能在解析协议名后, 自动调用名为“ Msg+协议名” 的方法, 那便大功告成, 而这其中, C#的反射机制是实现该功能的关键。
假设所有的消息处理方法都定义在MsgHandler类中, 且都是静态方法, 通过**typeof(MsgHandler).GetMethod(funName)**便能够获取
MsgHandler类中名为funName的静态方法。
其中牵扯到的反射知识可以在百度上查阅学习
//服务器主结构 负责连接、广播、接收请求
public class Program
{
//所有客户端字典 通过套接字来找到对应的客户端
public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
Socket listenfd = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEd = new IPEndPoint(ipAdr,8888);
listenfd.Bind(ipEd);
listenfd.Listen(0);
//等待客户端连接
listenfd.BeginAccept(AcceptCallback,listenfd);
Console.ReadKey();
}
static void AcceptCallback(IAsyncResult ar)
{
try
{
Socket listenfd = ar.AsyncState as Socket;
Socket clientfd = listenfd.EndAccept(ar);
ClientState state = new ClientState(clientfd);
clients.Add(clientfd,state);
//异步的方式接收客户端的请求信息
clientfd.BeginReceive(state.readBuff,0,state.readBuff.Length,SocketFlags.None,ReceiveCallback,state);
//等待下一个客户端的连接
listenfd.BeginAccept(AcceptCallback, listenfd);
}
catch (Exception e)
{
Console.WriteLine("Socket Accept fail" + e.ToString());
}
}
//接收客户端的请求信息
static void ReceiveCallback(IAsyncResult ar) {
ClientState state = ar.AsyncState as ClientState;
Socket clientfd = state.socket;
try
{
int count = clientfd.EndReceive(ar);
if (count==0) {
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
object[] ob = { state };
mei.Invoke(null, ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return;
}
string recvStr = Encoding.Default.GetString(state.readBuff);
string[] split = recvStr.Split('|');
string msgName = split[0];
string msgArgs = split[1];
//利用反射处理消息
//处理这条消息的方法名
//处理数据的方法名格式:Mst+消息名
string funName = "Msg" + msgName;
MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
object[] o = {state,msgArgs};
mi.Invoke(null,o);
//Console.WriteLine("接收消息:"+recvStr);
clientfd.BeginReceive(state.readBuff, 0, state.readBuff.Length, SocketFlags.None, ReceiveCallback, state);
}
catch (Exception e)
{
MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
object[] ob = {state};
mei.Invoke(null,ob);
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Accept fail" + e.ToString());
}
}
//广播消息
public static void SendAll(string sendStr) {
foreach (ClientState item in clients.Values)
{
item.Send(sendStr);
}
}
}
我们给客户端单独构造出一个类来进行数据的存储和交互
//客户端类
public class ClientState
{
//玩家的一些属性包括血量、位置.....
public int hp = -100;
public float x = 0;
public float y = 0;
public float z = 0;
public float euly = 0;
public Socket socket;
public byte[] readBuff = new byte[1024];
public ClientState(Socket _socket) {
socket = _socket;
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="data"></param>
public void Send(string data) {
Console.WriteLine("发送消息:" + data);
byte[] sendBytes = Encoding.Default.GetBytes(data);
socket.BeginSend(sendBytes,0,sendBytes.Length ,SocketFlags.None,SendCallback,socket);
}
void SendCallback(IAsyncResult ar) {
try
{
Socket _socket = ar.AsyncState as Socket;
_socket.EndSend(ar);
}
catch (Exception e)
{
Console.WriteLine("Socket Accept fail" + e.ToString());
}
}
}
对所有客户端发来的消息,都在MsgHandler类中进行处理
//负责处理接收到的客户端消息,
//这个类在Program类接收到客户端消息时根据消息名,通过反射来调用这个类中的处理方法
class MsgHandler
{
//处理Enter消息
public static void MsgEnter(ClientState c,string msgArgs) {
Console.WriteLine("Enter" + msgArgs);
string[] split = msgArgs.Split(',');
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
float euly = float.Parse(split[4]);
c.hp = 100;
c.x = x;
c.y = y;
c.z = z;
c.euly = euly;
string sendStr = "Enter|" + msgArgs;
Program.SendAll(sendStr);
}
//处理List消息
public static void MsgList(ClientState c,string msgArgs) {
Console.WriteLine("List"+msgArgs);
string sendStr = "List|";
foreach (ClientState item in Program.clients.Values)
{
sendStr += item.socket.RemoteEndPoint.ToString()+",";
sendStr += item.x.ToString() + ",";
sendStr += item.y.ToString() + ",";
sendStr += item.z.ToString() + ",";
sendStr += item.euly.ToString() + ",";
sendStr += item.hp.ToString()+",";
}
c.Send(sendStr);
}
//处理Move消息
public static void MsgMove(ClientState c,string msgArgs) {
string[] split = msgArgs.Split(',');
string desc = split[0];
float x = float.Parse(split[1]);
float y = float.Parse(split[2]);
float z = float.Parse(split[3]);
//赋值
c.x = x;
c.y = y;
c.z = z;
//广播
string sendStr = "Move|"+msgArgs;
Program.SendAll(sendStr);
}
有时候服务端需要对玩家上线、玩家下线等事件做出反应。比如游戏中,如果游戏玩家下线,服务端就需要通知其他客户端该玩家下线,从而使客户端删除角色。对此,可以使用类似于消息处理的方法来处理事件,添加一个处理事件的类EventHandler,在里面定义一些消息处理函数(目前只有处理玩家下线的OnDisconnect)就可实现该功能。
//其他的事件处理类
public class EventHandler
{
//处理有客户端掉线的问题
public static void OnDisconnect(ClientState c) {
Console.WriteLine("OnDisconnect");
string desc = c.socket.RemoteEndPoint.ToString();
string sendStr = "Leave|"+desc+",";
Program.SendAll(sendStr);
}
}
通过以上代码已经把基本的通讯骨架给搭建了起来,后面代码的补充与更改要根据实际项目的需求,针对性的增删,至于本文中存在的问题(代码没有处理粘包分包、线程冲突等问题)下一次的博客再进行处理;
本文参考了罗培羽—《Unity3D网络游戏实战》
来源:CSDN
作者:任桥-光耀
链接:https://blog.csdn.net/weixin_42498461/article/details/104449035