23.网络通信-套接字Socket-TCP通信-同步-区分消息类型
23.1 知识点
如何发送之前的自定义类的二进制信息
- 继承
BaseData
类 - 实现其中的序列化、反序列化、获取字节数等相关方法
- 发送自定义类数据时,进行序列化
- 接收自定义类数据时,进行反序列化
问题:当将序列化的二进制数据发送给对象时,对方如何区分?
举例:
- PlayerInfo: 玩家信息
- ChatInfo: 聊天信息
- LoginInfo: 登录信息
- 等等
这些数据对象序列化后是长度不同的字节数组。将它们发送给对象后,对方如何区分出它们分别是什么消息?如何选择对应的数据类反序列化它们?
如何区分消息类型
解决方案:
为发送的信息添加标识,比如添加消息 ID。在所有发送的消息的头部加上消息 ID(可以是 int、short、byte、long,根据实际情况选择)。
举例说明:
消息构成:
- 如果选用 int 类型作为消息 ID 的类型
- 前 4 个字节为消息 ID
- 后面的字节为数据类的内容
这样每次收到消息时,先把前 4 个字节取出来解析为消息 ID,再根据 ID 进行消息反序列化即可。
实践区分消息类型
创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性
public class BaseMessage : BaseData
{
public override int GetBytesNum()
{
throw new System.NotImplementedException();
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
throw new System.NotImplementedException();
}
public override byte[] Writing()
{
throw new System.NotImplementedException();
}
public virtual int GetID()
{
return 0;
}
}
让想要被发送的消息继承消息基类,实现序列化反序列化方法
/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData//0
{
//1
public string name;
public int atk;
public int lev;
//2
public override int GetBytesNum()
{
int num = 0;
num += 4 + Encoding.UTF8.GetBytes(name).Length;//name
num += 4;//atk
num += 4;//lev
return num;
}
//4
public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
name = ReadString(bytes, ref index);
atk = ReadInt(bytes, ref index);
lev = ReadInt(bytes, ref index);
return index - beginIndex;
}
//3
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteString(bytes, name, ref index);
WriteInt(bytes, atk, ref index);
WriteInt(bytes, lev, ref index);
return bytes;
}
}
public class PlayerMessage : BaseMessage
{
//成员变量
public int playerID;
public PlayerData playerData;
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
//先写消息ID
WriteInt(bytes, GetID(), ref index);
//写这个消息的成员变量
WriteInt(bytes, playerID, ref index);
WriteData(bytes, playerData, ref index);
return bytes;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
//反序列化不需要去解析ID 因为在这一步之前 就应该把ID反序列化出来
//用来判断到底使用哪一个自定义类来反序化
int index = beginIndex;
playerID = ReadInt(bytes, ref index);
playerData = ReadData<PlayerData>(bytes, ref index);
return index - beginIndex;
}
public override int GetBytesNum()
{
return 4 + //消息ID的长度
4 + //playerID的字节数组长度
playerData.GetBytesNum();//playerData的字节数组长度
}
/// <summary>
/// 自定义的消息ID 主要用于区分是哪一个消息类
/// </summary>
/// <returns></returns>
public override int GetID()
{
return 1001;
}
}
修改客户端和服务端收发消息的逻辑
服务端
//发送
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 666;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "我是韬老狮的服务端";
playerMessage.playerData.atk = 99;
playerMessage.playerData.lev = 50;
socketClient.Send(playerMessage.Writing());
//发送字符串转成的字节数组给客户端
socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));
客户端
//接收数据
//声明接收数据字节数组
byte[] receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
int receiveNum = socketTcp.Receive(receiveBytes);
//首先解析消息的ID
//使用字节数组中的前四个字节 得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
switch (msgID)
{
case 1001:
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.Reading(receiveBytes, 4);
print(playerMessage.playerID);
print(playerMessage.playerData.name);
print(playerMessage.playerData.atk);
print(playerMessage.playerData.lev);
break;
}
//重新声明接收数据字节数组 接收字符串 服务端分了两次发 我们也要分两次接
receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
receiveNum = socketTcp.Receive(receiveBytes);
print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
总结
区分消息的关键点是在数据字节数组头部加上消息 ID。只要前后端定义好统一的规则,通过 ID 就可以决定如何反序列化消息,并且可以决定应该如何处理该消息。
23.2 知识点代码
BaseMessage
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BaseMessage : BaseData
{
public override int GetBytesNum()
{
throw new System.NotImplementedException();
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
throw new System.NotImplementedException();
}
public override byte[] Writing()
{
throw new System.NotImplementedException();
}
public virtual int GetID()
{
return 0;
}
}
PlayerData
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData//0
{
//1
public string name;
public int atk;
public int lev;
//2
public override int GetBytesNum()
{
int num = 0;
num += 4 + Encoding.UTF8.GetBytes(name).Length;//name
num += 4;//atk
num += 4;//lev
return num;
}
//4
public override int Reading(byte[] bytes, int beginIndex = 0)
{
int index = beginIndex;
name = ReadString(bytes, ref index);
atk = ReadInt(bytes, ref index);
lev = ReadInt(bytes, ref index);
return index - beginIndex;
}
//3
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteString(bytes, name, ref index);
WriteInt(bytes, atk, ref index);
WriteInt(bytes, lev, ref index);
return bytes;
}
}
PlayerMessage
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMessage : BaseMessage
{
//成员变量
public int playerID;
public PlayerData playerData;
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
//先写消息ID
WriteInt(bytes, GetID(), ref index);
//写这个消息的成员变量
WriteInt(bytes, playerID, ref index);
WriteData(bytes, playerData, ref index);
return bytes;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
//反序列化不需要去解析ID 因为在这一步之前 就应该把ID反序列化出来
//用来判断到底使用哪一个自定义类来反序化
int index = beginIndex;
playerID = ReadInt(bytes, ref index);
playerData = ReadData<PlayerData>(bytes, ref index);
return index - beginIndex;
}
public override int GetBytesNum()
{
return 4 + //消息ID的长度
4 + //playerID的字节数组长度
playerData.GetBytesNum();//playerData的字节数组长度
}
/// <summary>
/// 自定义的消息ID 主要用于区分是哪一个消息类
/// </summary>
/// <returns></returns>
public override int GetID()
{
return 1001;
}
}
Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型服务端
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型
{
internal class Program
{
static void Main(string[] args)
{
//1.创建套接字Socket(TCP)
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Bind方法将套接字与本地地址绑定
try
{
IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);//把本机作为服务端程序 IP地址传入本机
socketTcp.Bind(iPEndPoint);//绑定
}
catch (Exception e)
{
//如果IP地址不合法或者端口号被占用可能报错
Console.WriteLine("绑定报错" + e.Message);
return;
}
//3.用Listen方法监听
socketTcp.Listen(1024);//最大接收1024个客户端
Console.WriteLine("服务端绑定监听结束,等待客户端连入");
//4.用Accept方法等待客户端连接
//5.建立连接,Accept返回新套接字
Socket socketClient = socketTcp.Accept();
//Accept是阻塞式的方法 会把主线程卡主 一定要等到客户端接入后才会继续执行后面的代码
//客户端接入后 返回新的Socket对象 这个新的Socket可以理解为客户段和服务端的通信通道
Console.WriteLine("有客户端连入了");
//6.用Send和Receive相关方法收发数据
//发送
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 666;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "我是韬老狮的服务端";
playerMessage.playerData.atk = 99;
playerMessage.playerData.lev = 50;
socketClient.Send(playerMessage.Writing());
//发送字符串转成的字节数组给客户端
socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));
//声明接受客户端信息的字节数组 声明1024容量代表能接受1kb的信息
byte[] result = new byte[1024];
//接受客户端信息 返回值为接受到的字节数
int receiveNum = socketClient.Receive(result);
//打印 远程发送信息的客户端的IP和端口 以及 发送过来的字符串
Console.WriteLine("接受到了{0}发来的消息:{1}",
socketClient.RemoteEndPoint.ToString(),
Encoding.UTF8.GetString(result, 0, receiveNum));
//7.用Shutdown方法释放连接
//注意断开的是客户段和服务端的通信通道
socketClient.Shutdown(SocketShutdown.Both);
//8.关闭套接字
//注意关闭的是客户段和服务端的通信通道
socketClient.Close();
}
}
}
Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class Lesson23_网络通信_套接字Socket_TCP通信_同步_区分消息类型 : MonoBehaviour
{
void Start()
{
#region 知识点一 如何发送之前的自定义类的2进制信息
//1.继承BaseData类
//2.实现其中的序列化、反序列化、获取字节数等相关方法
//3.发送自定义类数据时 序列化
//4.接受自定义类数据时 反序列化
//抛出问题:
//当将序列化的2进制数据发送给对象时,对方如何区分?
//举例:
//PlayerInfo:玩家信息
//ChatInfo:聊天信息
//LoginInfo:登录信息
//等等
//这些数据对象序列化后是长度不同的字节数组
//将它们发送给对象后,对方如何区分出他们分别是什么消息
//如何选择对应的数据类反序列化它们?
#endregion
#region 知识点二 如何区分消息类型
//解决方案:
//为发送的信息添加标识,比如添加消息ID
//在所有发送的消息的头部加上消息ID(int、short、byte、long都可以,根据实际情况选择)
//举例说明:
//消息构成
//如果选用int类型作为消息ID的类型
//前4个字节为消息ID
//后面的字节为数据类的内容
//####***************************
//这样每次收到消息时,先把前4个字节取出来解析为消息ID
//再根据ID进行消息反序列化即可
#endregion
#region 知识点三 实践区分消息类型
//实践步骤
//1.创建消息基类,基类继承BaseData,基类添加获取消息ID的方法或者属性
//2.让想要被发送的消息继承消息基类,实现序列化反序列化方法
//3.修改客户端和服务端收发消息的逻辑
//客户端逻辑
//1.创建套接字Socket Tcp
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Connect方法与服务端相连
//确定服务端的IP和端口 正常来说填的应该是远端服务器的ip地址以及端口号
//由于只有一台电脑用于测试 本机也当做服务器 所以传入当前电脑的ip地址
IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
try
{
//连接
socketTcp.Connect(iPEndPoint);
}
catch (SocketException e)
{
//如果连接没有开启或者服务器异常 会报错 不同的返回码代表不同报错
if (e.ErrorCode == 10061)
print("服务器拒绝连接");
else
print("连接服务器失败" + e.ErrorCode);
return;
}
//3.用Send和Receive相关方法收发数据
//接收数据
//声明接收数据字节数组
byte[] receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
int receiveNum = socketTcp.Receive(receiveBytes);
//首先解析消息的ID
//使用字节数组中的前四个字节 得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
switch (msgID)
{
case 1001:
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.Reading(receiveBytes, 4);
print(playerMessage.playerID);
print(playerMessage.playerData.name);
print(playerMessage.playerData.atk);
print(playerMessage.playerData.lev);
break;
}
//重新声明接收数据字节数组 接收字符串 服务端分了两次发 我们也要分两次接
receiveBytes = new byte[1024];
//Receive方法接受数据 返回接收多少字节
receiveNum = socketTcp.Receive(receiveBytes);
print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
//发送数据
socketTcp.Send(Encoding.UTF8.GetBytes("你好,我是韬老狮的客户端"));
//4.用Shutdown方法释放连接
socketTcp.Shutdown(SocketShutdown.Both);
//5.关闭套接字
socketTcp.Close();
#endregion
#region 总结
//区分消息的关键点,是在数据字节数组头部加上消息ID
//只要前后端定义好统一的规则
//那么我们可以通过ID来决定如何反序列化消息
//并且可以决定我们应该如何处理该消息
#endregion
}
}
23.3 练习题
修改之前的服务端综合练习2和客户端综合练习,让他们收发的消息都是区分了消息类型的BaseMessage
在TcpNetManager把存储收发消息的队列都改成BaseMessage类型,同时收发方法也改成BaseMessage类型
private Queue<BaseMessage> sendMsgQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储待发送的消息
private Queue<BaseMessage> receiveQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储接收到的消息
// 发送消息
public void Send(BaseMessage baseMessage)
{
sendMsgQueue.Enqueue(baseMessage); // 将消息添加到发送消息队列
}
// 在独立线程中处理发送消息的逻辑
private void SendMsg(object obj)
{
while (isConnected) // 只要连接有效
{
if (sendMsgQueue.Count > 0) // 如果发送消息队列中有待发送的消息
{
// 从队列中取出消息并发送到服务器
socket.Send(sendMsgQueue.Dequeue().Writing());
}
}
}
// 在独立线程中处理接收消息的逻辑
private void ReceiveMsg(object obj)
{
while (isConnected) // 只要连接有效
{
if (socket.Available > 0) // 如果有可接收的数据
{
// 接收从服务器发送来的数据,并将数据转换成字符串后存储到接收消息队列
receiveNum = socket.Receive(receiveBytes);
//首先把收到字节数组的前4个字节 读取出来得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
BaseMessage baseMessage = null;
switch (msgID)
{
case 1001:
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.Reading(receiveBytes, 4);
baseMessage = playerMessage;
break;
}
//如果消息为空 那证明是不知道类型的消息 没有解析
if (baseMessage == null)
continue;
//收到消息 解析消息为字符串 并放入公共容器
receiveQueue.Enqueue(baseMessage);
}
}
}
void Update()
{
// 在Unity的每一帧中检查是否有待处理的接收消息,如果有,则打印出来
if (receiveQueue.Count > 0)
{
BaseMessage baseMessage = receiveQueue.Dequeue();
if (baseMessage is PlayerMessage)
{
PlayerMessage playerMessage = (baseMessage as PlayerMessage);
print(playerMessage.playerID);
print(playerMessage.playerData.name);
print(playerMessage.playerData.lev);
print(playerMessage.playerData.atk);
}
}
}
客户端主脚本点击发送按钮时改成发送PlayerMessage这个自定义数据结构
sendButton.onClick.AddListener(() =>
{
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 1111;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "韬老狮客户端发送的信息";
playerMessage.playerData.atk = 22;
playerMessage.playerData.lev = 10;
TcpNetManager.Instance.Send(playerMessage);
});
ClientSocket收发消息改成使用BaseMessage类型
// 发送消息给客户端
public void Send(BaseMessage baseMessage)
{
if (clientSocket != null)
{
try
{
clientSocket.Send(baseMessage.Writing()); // 将消息编码为UTF-8字节数组并发送给客户端
}
catch (Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
Close(); // 如果发送出现异常,关闭套接字连接
}
}
}
// 接收来自客户端的消息
public void Receive()
{
if (clientSocket == null)
return;
try
{
if (clientSocket.Available > 0)// 如果套接字中有可读数据
{
byte[] result = new byte[1024 * 5]; // 创建一个缓冲区来存储接收到的数据
int receiveNum = clientSocket.Receive(result);// 从套接字接收数据并存储在缓冲区中
//收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
int msgID = BitConverter.ToInt32(result, 0);
BaseMessage baseMessage = null;
switch (msgID)
{
case 1001:
baseMessage = new PlayerMessage();
baseMessage.Reading(result, 4);
break;
}
if (baseMessage == null)
return;
ThreadPool.QueueUserWorkItem(HandleMessage, baseMessage);
}
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
Close(); // 如果接收出现异常,关闭套接字连接
}
}
// 处理接收到的消息
private void HandleMessage(object obj)
{
BaseMessage baseMessage = obj as BaseMessage;
if (baseMessage is PlayerMessage)
{
PlayerMessage playerMessage = baseMessage as PlayerMessage;
Console.WriteLine(playerMessage.playerID);
Console.WriteLine(playerMessage.playerData.name);
Console.WriteLine(playerMessage.playerData.lev);
Console.WriteLine(playerMessage.playerData.atk);
}
}
ServerSocket对客户端广播消息改成广播BaseMessage类型的消息
// 向所有客户端广播消息
public void Broadcast(BaseMessage baseMessage)
{
foreach (ClientSocket client in clientSocketDictionary.Values)
{
client.Send(baseMessage);
}
}
服务端主脚本监听到输入B:1001命令,对客户单进行广播一个自定义PlayerMessage 类
while (true)
{
// 从控制台读取用户输入
string input = Console.ReadLine();
// 如果用户输入 "Quit",则关闭服务器
if (input == "Quit")
{
serverSocket.Close();
}
// 如果用户输入以 "B:" 开头,表示要广播消息给所有客户端
else if (input.Substring(0, 2) == "B:")
{
if (input.Substring(2) == "1001")
{
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 9876;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "服务器端发来的消息";
playerMessage.playerData.lev = 99;
playerMessage.playerData.atk = 80;
serverSocket.Broadcast(playerMessage);
}
}
}
23.4 练习题代码
TcpNetManager
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class TcpNetManager : BaseSingletonInMonoBehaviour<TcpNetManager>
{
private Socket socket; // 创建Socket对象,用于网络通信
private Queue<BaseMessage> sendMsgQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储待发送的消息
private Queue<BaseMessage> receiveQueue = new Queue<BaseMessage>(); // 创建一个队列,用于存储接收到的消息
private byte[] receiveBytes = new byte[1024 * 1024]; // 创建一个字节数组,用于存储接收到的数据
private int receiveNum; // 用于存储接收到的字节数
private bool isConnected = false; // 用于标识是否已连接到服务器
// 连接服务器
public void Connect(string ip, int port)
{
if (isConnected) // 如果已连接,则直接返回
return;
if (socket == null) // 如果套接字为空,创建一个套接字对象
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); // 创建一个IP终结点对象
try
{
// 尝试连接到指定的IP地址和端口
socket.Connect(ipPoint);
isConnected = true; // 标记已连接
ThreadPool.QueueUserWorkItem(SendMsg); // 创建并启动发送消息的线程
ThreadPool.QueueUserWorkItem(ReceiveMsg); // 创建并启动接收消息的线程
}
catch (SocketException e)
{
if (e.ErrorCode == 10061) // 如果连接被服务器拒绝
print("服务器拒绝连接");
else
print("连接失败" + e.ErrorCode + e.Message); // 打印连接失败的信息
}
}
// 关闭连接
public void Close()
{
if (socket != null) // 如果套接字对象存在
{
socket.Shutdown(SocketShutdown.Both); // 关闭套接字的发送和接收
socket.Close(); // 关闭套接字连接
isConnected = false; // 标记连接已关闭
}
}
// 当对象被销毁时,确保关闭连接
private void OnDestroy()
{
Close(); // 调用关闭连接的方法
}
// 发送消息
public void Send(BaseMessage baseMessage)
{
sendMsgQueue.Enqueue(baseMessage); // 将消息添加到发送消息队列
}
// 在独立线程中处理发送消息的逻辑
private void SendMsg(object obj)
{
while (isConnected) // 只要连接有效
{
if (sendMsgQueue.Count > 0) // 如果发送消息队列中有待发送的消息
{
// 从队列中取出消息并发送到服务器
socket.Send(sendMsgQueue.Dequeue().Writing());
}
}
}
// 在独立线程中处理接收消息的逻辑
private void ReceiveMsg(object obj)
{
while (isConnected) // 只要连接有效
{
if (socket.Available > 0) // 如果有可接收的数据
{
// 接收从服务器发送来的数据,并将数据转换成字符串后存储到接收消息队列
receiveNum = socket.Receive(receiveBytes);
//首先把收到字节数组的前4个字节 读取出来得到ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
BaseMessage baseMessage = null;
switch (msgID)
{
case 1001:
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.Reading(receiveBytes, 4);
baseMessage = playerMessage;
break;
}
//如果消息为空 那证明是不知道类型的消息 没有解析
if (baseMessage == null)
continue;
//收到消息 解析消息为字符串 并放入公共容器
receiveQueue.Enqueue(baseMessage);
}
}
}
void Update()
{
// 在Unity的每一帧中检查是否有待处理的接收消息,如果有,则打印出来
if (receiveQueue.Count > 0)
{
BaseMessage baseMessage = receiveQueue.Dequeue();
if (baseMessage is PlayerMessage)
{
PlayerMessage playerMessage = (baseMessage as PlayerMessage);
print(playerMessage.playerID);
print(playerMessage.playerData.name);
print(playerMessage.playerData.lev);
print(playerMessage.playerData.atk);
}
}
}
}
Lesson23_练习题
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Lesson23_练习题 : MonoBehaviour
{
public InputField InputField;
public Button sendButton;
void Start()
{
TcpNetManager.Instance.Connect("127.0.0.1", 8080);
sendButton.onClick.AddListener(() =>
{
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 1111;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "韬老狮客户端发送的信息";
playerMessage.playerData.atk = 22;
playerMessage.playerData.lev = 10;
TcpNetManager.Instance.Send(playerMessage);
});
}
}
ClientSocket
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Lesson23_练习题
{
class ClientSocket
{
private static int CLIENT_BEGIN_ID = 1; // 静态变量,用于为客户端分配唯一的客户端ID
public int clientID; // 客户端的唯一ID
public Socket clientSocket; // 与客户端通信的套接字对象
/// <summary>
/// 是否是连接状态
/// </summary>
public bool isClientConnected => this.clientSocket.Connected; // 判断套接字是否处于连接状态
public ClientSocket(Socket clientSocket)
{
this.clientID = CLIENT_BEGIN_ID; // 初始化客户端ID
this.clientSocket = clientSocket; // 初始化套接字
++CLIENT_BEGIN_ID; // 为下一个客户端分配不同的ID
}
// 关闭套接字连接
public void Close()
{
if (clientSocket != null)
{
clientSocket.Shutdown(SocketShutdown.Both); // 关闭套接字的读写
clientSocket.Close(); // 关闭套接字连接
clientSocket = null;
}
}
// 发送消息给客户端
public void Send(BaseMessage baseMessage)
{
if (clientSocket != null)
{
try
{
clientSocket.Send(baseMessage.Writing()); // 将消息编码为UTF-8字节数组并发送给客户端
}
catch (Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
Close(); // 如果发送出现异常,关闭套接字连接
}
}
}
// 接收来自客户端的消息
public void Receive()
{
if (clientSocket == null)
return;
try
{
if (clientSocket.Available > 0)// 如果套接字中有可读数据
{
byte[] result = new byte[1024 * 5]; // 创建一个缓冲区来存储接收到的数据
int receiveNum = clientSocket.Receive(result);// 从套接字接收数据并存储在缓冲区中
//收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
int msgID = BitConverter.ToInt32(result, 0);
BaseMessage baseMessage = null;
switch (msgID)
{
case 1001:
baseMessage = new PlayerMessage();
baseMessage.Reading(result, 4);
break;
}
if (baseMessage == null)
return;
ThreadPool.QueueUserWorkItem(HandleMessage, baseMessage);
}
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
Close(); // 如果接收出现异常,关闭套接字连接
}
}
// 处理接收到的消息
private void HandleMessage(object obj)
{
BaseMessage baseMessage = obj as BaseMessage;
if (baseMessage is PlayerMessage)
{
PlayerMessage playerMessage = baseMessage as PlayerMessage;
Console.WriteLine(playerMessage.playerID);
Console.WriteLine(playerMessage.playerData.name);
Console.WriteLine(playerMessage.playerData.lev);
Console.WriteLine(playerMessage.playerData.atk);
}
}
}
}
ServerSocket
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Lesson23_练习题
{
class ServerSocket
{
// 服务器端Socket
public Socket serverSocket;
// 保存客户端连接的所有Socket的字典
public Dictionary<int, ClientSocket> clientSocketDictionary = new Dictionary<int, ClientSocket>();
// 用于标识服务器是否关闭的标志
private bool isServerClose;
// 开启服务器端
public void Start(string ipString, int port, int clientSocketMaxNum)
{
// 初始化服务器关闭标志为假
isServerClose = false;
// 创建服务器套接字,指定地址族为IPv4、套接字类型为流套接字、协议类型为TCP
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 创建IP终结点,指定IP地址和端口号
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ipString), port);
// 将套接字绑定到指定的IP终结点
serverSocket.Bind(serverEndPoint);
// 启动服务器套接字,同时指定同时等待连接的最大客户端数
serverSocket.Listen(clientSocketMaxNum);
// 启动线程池中的线程来处理客户端连接请求和消息接收
ThreadPool.QueueUserWorkItem(AcceptClientConnect);
ThreadPool.QueueUserWorkItem(ReceiveClientMessage);
}
// 关闭服务器端
public void Close()
{
// 设置服务器关闭标志为真
isServerClose = true;
// 关闭所有客户端连接
foreach (ClientSocket client in clientSocketDictionary.Values)
{
client.Close();
}
clientSocketDictionary.Clear();
// 关闭服务器套接字的读写
serverSocket.Shutdown(SocketShutdown.Both);
// 关闭服务器套接字
serverSocket.Close();
// 将服务器套接字设置为null
serverSocket = null;
}
// 接受客户端连接
private void AcceptClientConnect(object obj)
{
while (!isServerClose)
{
try
{
// 等待并接受一个客户端连接请求
Socket clientSocket = serverSocket.Accept();
// 创建一个新的ClientSocket对象来管理客户端连接
ClientSocket client = new ClientSocket(clientSocket);
// 向客户端发送欢迎消息
//client.Send("欢迎连入服务器");
// 将客户端Socket对象添加到字典中,以客户端ID作为键
clientSocketDictionary.Add(client.clientID, client);
}
catch (Exception e)
{
Console.WriteLine("客户端连入报错" + e.Message);
}
}
}
// 接收客户端消息
private void ReceiveClientMessage(object obj)
{
while (!isServerClose)
{
if (clientSocketDictionary.Count > 0)
{
foreach (ClientSocket client in clientSocketDictionary.Values)
{
// 从每个客户端接收消息
client.Receive();
}
}
}
}
// 向所有客户端广播消息
public void Broadcast(BaseMessage baseMessage)
{
foreach (ClientSocket client in clientSocketDictionary.Values)
{
client.Send(baseMessage);
}
}
}
}
Lesson23_练习题服务端
namespace Lesson23_练习题
{
internal class Program
{
static void Main(string[] args)
{
// 创建一个ServerSocket对象,用于处理服务器端的操作
ServerSocket serverSocket = new ServerSocket();
// 启动服务器,绑定到本地IP地址 127.0.0.1,监听端口 8080,允许最大连接数为 1024
serverSocket.Start("127.0.0.1", 8080, 1024);
// 输出服务器开启成功的消息
Console.WriteLine("服务器开启成功");
while (true)
{
// 从控制台读取用户输入
string input = Console.ReadLine();
// 如果用户输入 "Quit",则关闭服务器
if (input == "Quit")
{
serverSocket.Close();
}
// 如果用户输入以 "B:" 开头,表示要广播消息给所有客户端
else if (input.Substring(0, 2) == "B:")
{
if (input.Substring(2) == "1001")
{
PlayerMessage playerMessage = new PlayerMessage();
playerMessage.playerID = 9876;
playerMessage.playerData = new PlayerData();
playerMessage.playerData.name = "服务器端发来的消息";
playerMessage.playerData.lev = 99;
playerMessage.playerData.atk = 80;
serverSocket.Broadcast(playerMessage);
}
}
}
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com