1、Socket的常用属性和方法
创建Socket TCP流套接字
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
1.1 常用属性
1)套接字的连接状态
socketTcp.Connected
2)获取套接字的类型
socketTcp.SocketType
3)获取套接字的协议类型
socketTcp.ProtocolType
4)获取套接字的寻址方案
socketTcp.AddressFamily
5)从网络中获取准备读取的数据数据量
socketTcp.Available
6)获取本机EndPoint对象(注意:IPEndPoint继承EndPoint)
socketTcp.LocalEndPoint as IPEndPoint
7)获取远程EndPoint对象
socketTcp.RemoteEndPoint as IPEndPoint;
1.2 同步常用方法
1.2.1 主要用于服务端方法
1)绑定IP和端口, Bind(ip地址和端口)
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
2)设置客户端连接的最大数量 , Listen(最大连接数)
socketTcp.Listen(99);
3)等待客户端连入,Accept()
socketTcp.Accept();
1.3.2 主要用于客户端方法
1)连接远程服务端,Connect(ip地址和端口)
socketTcp.Connect(IPAddress.Parse("118.12.123.1"), 8080);
1.4.3 客户端服务端都会用的方法
1)同步发送和接收数据,Send()和Receive()
发送
socketTcp.Send
接收
socketTcp.Receive()
2)释放连接并关闭socket,先于close调用
socketTcp.Shutdown(SocketShutdown.Both);
3)关闭连接,释放所有Socket管理资源
socketTcp.Close();
1.3 异步常用方法
1.3.1 主要用于服务端方法
1)等待客户端连入方式1,BeginAccept和EndAccept
socketTcp.BeginAccept(AcceptCallback, socketTcp);
private void AcceptCallback(IAsyncResult result)
{
try
{
//获取传入参数
Socket s = result.AsyncState as Socket;
//通过调用EndAccept就可以得到连入的客户端Socket
Socket clientSocket = s.EndAccept(result);
s.BeginAccept(AcceptCallback, s);
}
catch (SocketException e)
{
print(e.SocketErrorCode);
}
}
2)等待客户端连入方式2, AcceptAsync
SocketAsyncEventArgs e = new SocketAsyncEventArgs();
e.Completed += (socket, args) =>
{
//首先判断是否成功
if(args.SocketError == SocketError.Success)
{
Socket clientSocket = args.AcceptSocket;
(socket as Socket).AcceptAsync(args);
}
else
{
print("连入客户端失败" + args.SocketError);
}
};
socketTcp.AcceptAsync(e);
1.3.2 主要用于客户端方法
1)连接远程服务端方式1,BeginConnect和EndConnect
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.BeginConnect(ipPoint, (res) =>
{
Socket s = res.AsyncState as Socket;
try
{
s.EndConnect(res);
}
catch (SocketException e)
{
print("连接出错" + e.SocketErrorCode + e.Message);
}
}, socketTcp);
2) 连接远程服务端方式2,ConnectAsync
SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
IPEndPoint ipPoint2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
e.RemoteEndPoint = ipPoint2;
e.Completed += (socket, args) =>
{
if(args.SocketError == SocketError.Success)
{
//连接成功
}
else
{
//连接失败
print(args.SocketError);
}
};
socketTcp.ConnectAsync(e2);
1.3.3 客户端服务端都会用的方法
1)接受消息
接受消息方式1,BeginReceive和EndReceive
socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallback, socketTcp);
private void ReceiveCallback(IAsyncResult result)
{
try
{
Socket s = result.AsyncState as Socket;
//这个返回值是你收到了多少个字节
int num = s.EndReceive(result);
//进行消息处理
Encoding.UTF8.GetString(resultBytes, 0, num);
//如果还要继续接受
s.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallback, s);
}
catch (SocketException e)
{
print("接受消息出问题" + e.SocketErrorCode + e.Message);
}
}
接受消息方式2,SendAsync
SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();
byte[] bytes2 = Encoding.UTF8.GetBytes("你好呀,好兄弟");
e3.SetBuffer(bytes2, 0, bytes2.Length);
e3.Completed += (socket, args) =>
{
if(args.SocketError != SocketError.Success)
{
print("发送成功");
}
else
{
}
};
socketTcp.SendAsync(e3);
2)发送消息
发送消息方式1,BeginSend和EndSend
byte[] bytes = Encoding.UTF8.GetBytes("41414214241");
socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
{
try
{
Socket s = result.AsyncState as Socket;
s.EndSend(result);
}
catch (SocketException e)
{
print("发送错误" + e.SocketErrorCode + e.Message);
}
}, socketTcp);
发送消息方式2, ReceiveAsync
e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);
e4.Completed += (socket, args) =>
{
if(args.SocketError!= SocketError.Success)
{
//收取存储在容器当中的字节
//Buffer是容器
//BytesTranferred是收取了多少个字节
Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
args.SetBuffer(0, args.Buffer.Length);
//接受完消息 再接受下一条
(socket as Socket).ReceiveAsync(args);
}
};
socketTcp.ReceiveAsync(e4);
2、Socket实现TCP通讯流程
2.1 服务端
1)创建套接字socket(TCP)
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2)用Bind方法将套接字与本地地址绑定
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
3)用Listen方法监听
socketTcp.Listen(1024);
4)用Accept方法等待客户端连连接
Socket socketClient = socketTcp.Accept();
5)建立连接,Accept返回新套接字
Socket socketClient = socketTcp.Accept();
6)用send和Receive相关方法收发数据
//发送
PlayerMsg msg = new PlayerMsg();
msg.playerID = 666;
msg.playerData = new PlayerData();
msg.playerData.name = "我是NBB服务端";
msg.playerData.atk = 99;
msg.playerData.lev = 120;
socketClient.Send(msg.Writing());
//接受
byte[] res = new byte[1024];
int receiveNum = socketClient.Receive(res);
7)用shutdown方法释放连接
socketClient.Shutdown(SocketShutdown.Both);
8)关闭套接字
socketClient.Close();
2.2 客户端
1)创建套接字socket
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2)用connect方法与服务端相连
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Connect(ipPoint);
3)用send和Receive相关方法收发数据
//接受数据
byte[] receiveBytes = new byte[1024];
int receiveNum = socket.Receive(receiveBytes);
//首先解析消息的ID
int msgID = BitConverter.ToInt32(receiveBytes, 0);
switch (msgID)
{
case 1001:
PlayerMsg msg = new PlayerMsg();
msg.Reading(receiveBytes, 4);
Debug.Log(msg.playerID);
Debug.Log(msg.playerData.name);
Debug.Log(msg.playerData.atk);
Debug.Log(msg.playerData.lev);
break;
default:
break;
}
Debug.Log("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
//发送数据
socket.Send(Encoding.UTF8.GetBytes("你好,我是客户端"));
4)用shutdown方法释放连接
socket.Shutdown(SocketShutdown.Both);
5)关闭套接字
socket.Close();
3、简单实现TCP通讯完整代码
实现功能:消息区分、分包黏包、心跳消息客户端长时间不发送消息自动断开
3.1 消息类型区分代码
1)BaseData
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
public abstract class BaseData
{
/// <summary>
/// 用于子类重写的 获取字节数组容器大小的方法
/// </summary>
/// <returns></returns>
public abstract int GetBytesNum();
/// <summary>
/// 把成员变量序列化为 对应的字节数组
/// </summary>
/// <returns></returns>
public abstract byte[] Writing();
/// <summary>
/// 把二进制字节数组 反序列化 到成员变量当中
/// </summary>
/// <param name="bytes">反序列化使用的字节数组</param>
/// <param name="beginIndex">从该字节数组的第几个位置开始解析 默认是 0</param>
public abstract int Reading(byte[] bytes, int beginIndex = 0);
/// <summary>
/// 存储int类型变量到指定的字节数组
/// </summary>
/// <param name="bytes">指定字节数组</param>
/// <param name="value">具体的int值</param>
/// <param name="index">每次存储后用于记录当前索引位置的变量</param>
protected void WriteInt(byte[] bytes, int value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(int);
}
protected void WriteShort(byte[] bytes, short value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(short);
}
protected void WriteLong(byte[] bytes, long value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(long);
}
protected void WriteFloat(byte[] bytes, float value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, index);
index += sizeof(float);
}
protected void WriteByte(byte[] bytes, byte value, ref int index)
{
bytes[index] = value;
index += sizeof(byte);
}
protected void WriteBool(byte[] bytes, bool value, ref int index)
{
BitConverter.GetBytes(value).CopyTo(bytes, (int)index);
index += sizeof(bool);
}
protected void WriteString(byte[] bytes, string value, ref int index)
{
//先存储string字节数组长度
byte[] strBytes = Encoding.UTF8.GetBytes(value);
int num = strBytes.Length;
BitConverter.GetBytes(num).CopyTo(bytes, index);
index += sizeof(int);
//在存储 string 字节数组
strBytes.CopyTo(bytes, index);
index += num;
}
protected void WriteData(byte[] bytes, BaseData data, ref int index)
{
data.Writing().CopyTo(bytes, index);
index += data.GetBytesNum();
}
/// <summary>
/// 根据字节数组读取整形
/// </summary>
/// <param name="bytes">读取数组</param>
/// <param name="index">开始读取的索引位置</param>
/// <returns></returns>
protected int ReadInt(byte[] bytes, ref int index)
{
int value = BitConverter.ToInt32(bytes, index);
index += sizeof(int);
return value;
}
protected short ReadShort(byte[] bytes, ref int index)
{
short value = BitConverter.ToInt16(bytes, index);
index += sizeof(short);
return value;
}
protected long ReadLong(byte[] bytes, ref int index)
{
long value = BitConverter.ToInt64(bytes, index);
index += sizeof(long);
return value;
}
protected float ReadFloat(byte[] bytes, ref int index)
{
float value = BitConverter.ToSingle(bytes, index);
index += sizeof(float);
return value;
}
protected byte ReadByte(byte[] bytes, ref int index)
{
byte b = bytes[index];
index++;
return b;
}
protected bool ReadBool(byte[] bytes, ref int index)
{
bool value = BitConverter.ToBoolean(bytes, index);
index += sizeof(bool);
return value;
}
protected string ReadString(byte[] bytes, ref int index)
{
int length = BitConverter.ToInt32(bytes, index);
index += sizeof(int);
string value = Encoding.UTF8.GetString(bytes, index, length);
index += length;
return value;
}
protected T ReadData<T>(byte[] bytes, ref int index) where T : BaseData, new()
{
T value = new T();
index += value.Reading(bytes, index);
return value;
}
}
2)BaseMsg
using System.Collections;
using System.Collections.Generic;
public class BaseMsg : 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;
}
}
3)PlayerData
using System.Collections;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData
{
public string name;
public int atk;
public int lev;
public override int GetBytesNum()
{
return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;
}
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;
}
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;
}
}
4)PlayerMsg
using System.Collections;
using System.Collections.Generic;
public class PlayerMsg : BaseMsg
{
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 + //palyerID
playerData.GetBytesNum(); //playerData
}
/// <summary>
/// 自定义的消息ID 主要用于区分是哪一个消息类
/// </summary>
/// <returns></returns>
public override int GetID()
{
return 1001;
}
}
5)QuitMsg
using System.Collections;
using System.Collections.Generic;
public class QuitMsg : BaseMsg
{
public override int GetBytesNum()
{
return 8;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
return 0;
}
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, 0, ref index);
return bytes;
}
public override int GetID()
{
return 1003;
}
}
6)HeartMsg
using System.Collections;
using System.Collections.Generic;
public class HeartMsg : BaseMsg
{
public override int GetBytesNum()
{
return 8;
}
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, 0, ref index);
return bytes;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
return 0;
}
public override int GetID()
{
return 999;
}
}
3.2 服务端
1)ClientSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TeachTcpServerExercises2
{
class ClientSocket
{
private static int CLIENT_BIGIN_ID = 1;
public int clientID;
public Socket socket;
//用于处理分包时 缓存的 字节数组 和字节数组长度
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
//上一次收到消息的时间
private long frontTime = -1;
//超时时间
private static int TIME_OUT_TIME = 10;
public ClientSocket(Socket socket)
{
this.clientID = CLIENT_BIGIN_ID;
this.socket = socket;
++CLIENT_BIGIN_ID;
//为了方便理解 所有开一个线程专门计时 但是这种方式比较消极性能 不建议这样使用
ThreadPool.QueueUserWorkItem(CheckTimeOut);
}
/// <summary>
/// 间隔一段时间 检测一次超时 如果超时就会主动断开该客户端的连接
/// </summary>
/// <param name="obj"></param>
private void CheckTimeOut(object obj)
{
while (Connected)
{
if (frontTime != -1 &&
DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
{
Program.socket.AddDelSocket(this);
break;
}
Thread.Sleep(5000);
}
}
/// <summary>
/// 是否是连接状态
/// </summary>
public bool Connected => this.socket.Connected;
//我们应该封装一些方法
//关闭
public void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
//发送
public void Send(BaseMsg info)
{
if(Connected)
{
try
{
socket.Send(info.Writing());
}
catch (Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
Program.socket.AddDelSocket(this);
//Close();
}
}
else
{
Program.socket.AddDelSocket(this);
}
}
//接收
public void Receive()
{
if (!Connected)
{
Program.socket.AddDelSocket(this);
return;
}
try
{
if(socket.Available > 0)
{
byte[] res = new byte[1024 * 5];
int receiveNum = socket.Receive(res);
HandleReceiveMsg(res, receiveNum);
收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
//int msgID = BitConverter.ToInt32(res, 0);
//BaseMsg msg = null;
//switch (msgID)
//{
// case 1001:
// msg = new PlayerMsg();
// msg.Reading(res, 4);
// break;
// default:
// break;
//}
//if (msg == null)
// return;
//ThreadPool.QueueUserWorkItem(MsgHandle, msg);
}
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
//解析消息错误 也认为 要把socket断开了
Program.socket.AddDelSocket(this);
//Close();
}
}
//处理接收消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if (cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
baseMsg = new PlayerMsg();
baseMsg.Reading(cacheBytes, nowIndex);
break;
case 1003:
baseMsg = new QuitMsg();
//由于该消息没有消息体 所有都不用反序列化
break;
case 999:
baseMsg = new HeartMsg();
//由于该消息没有消息体 所有都不用反序列化
break;
default:
break;
}
if (baseMsg != null)
ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
//receiveQueue.Enqueue(baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果不满足 证明有分包
//那么我们需要把当前收到的内容 记录下来
//有待下次接收到消息后 再做处理
//receiveBytes.CopyTo(cacheBytes, 0);
//cacheNum = receiveNum;
//如果进行了 ID和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
private void MsgHandle(object obj)
{
BaseMsg msg = obj as BaseMsg;
if(msg is PlayerMsg)
{
Console.WriteLine("PlayerMeg");
PlayerMsg playerMsg = msg as PlayerMsg;
Console.WriteLine(playerMsg.playerID);
Console.WriteLine(playerMsg.playerData.name);
Console.WriteLine(playerMsg.playerData.atk);
Console.WriteLine(playerMsg.playerData.lev);
}
else if(msg is QuitMsg)
{
//收到断开连接消息 把字迹添加到待移除的列表当中
Program.socket.AddDelSocket(this);
}
else if(msg is HeartMsg)
{
//收到心跳消息 记录收到消息的时间
frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
Console.WriteLine("收到心跳消息");
}
}
}
}
2)ServerSocket
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TeachTcpServerExercises2
{
class ServerSocket
{
//服务端Socket
public Socket socket;
//客户端连接的所有Socket
public Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();
//有待移除的客户端socket 避免在foreach时直接从字典中移除 出现问题
private List<ClientSocket> delList = new List<ClientSocket>();
private bool isClose;
//开启服务器端
public void Start(string ip, int port, int num)
{
isClose = false;
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
socket.Bind(ipPoint);
socket.Listen(num);
ThreadPool.QueueUserWorkItem(Accept);
ThreadPool.QueueUserWorkItem(Receive);
}
//关闭服务器端
public void Close()
{
isClose = true;
foreach (ClientSocket client in clientDic.Values)
{
client.Close();
}
clientDic.Clear();
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
//接受客户端连入
private void Accept(object obj)
{
while (!isClose)
{
try
{
//连入一个客户端
Socket clientSocket = socket.Accept();
ClientSocket client = new ClientSocket(clientSocket);
lock (clientDic)
{
clientDic.Add(client.clientID, client);
}
Console.WriteLine("客户端" + clientSocket.RemoteEndPoint + "连入服务器");
}
catch (Exception e)
{
Console.WriteLine("客户端连入报错" + e.Message);
}
}
}
//接收客户端消息
private void Receive(object obj)
{
while (!isClose)
{
if(clientDic.Count > 0)
{
lock (clientDic)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Receive();
}
CloseDelListSocket();
}
}
}
}
public void Broadcast(BaseMsg info)
{
lock (clientDic)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Send(info);
}
}
}
//添加待移除的 socket内容
public void AddDelSocket(ClientSocket socket)
{
if (!delList.Contains(socket))
{
delList.Add(socket);
}
}
//判断有没有 断开连接的 把其 移除
public void CloseDelListSocket()
{
//判断有没有 断开连接的 把其 移除
for (int i = 0; i < delList.Count; i++)
{
CloseClientSocket(delList[i]);
}
delList.Clear();
}
//关闭客户端连接 从字典中移除
public void CloseClientSocket(ClientSocket socket)
{
lock (clientDic)
{
socket.Close();
if (clientDic.ContainsKey(socket.clientID))
{
clientDic.Remove(socket.clientID);
Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
}
}
}
}
}
3)启动代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TeachTcpServerExercises2
{
class Program
{
public static ServerSocket socket;
static void Main(string[] args)
{
socket = new ServerSocket();
socket.Start("127.0.0.1", 8080, 1024);
Console.WriteLine("服务器开启成功");
while (true)
{
string input = Console.ReadLine();
if(input == "Quit")
{
socket.Close();
}
else if(input.Substring(0, 2) == "B:")
{
if(input.Substring(2) == "1001")
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 9833;
msg.playerData = new PlayerData();
msg.playerData.name = "服务器端发来的消息";
msg.playerData.atk = 1313;
msg.playerData.lev = 9999;
socket.Broadcast(msg);
}
}
}
}
}
}
3.3 客户端
1)NetMgr
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 NetMgr : MonoBehaviour
{
private static NetMgr instance;
public static NetMgr Instance => instance;
//客户端Socket
private Socket socket;
//用于发送消息的队列 公共容器 主线程里面放 发送线程从里面取
private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
//用于接受消息的队列 公共容器 子线程往里面放 主线程从里面取
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
用于收消息的水桶(容器)
//private byte[] receiveBytes = new byte[1024 * 1024];
返回收到的字节数
//private int receiveNum;
//用于处理分包时 缓存的 字节数组 和字节数组长度
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
//是否连接
private bool isConneted = false;
//发送心跳消息间隔时间
private int SEND_HEARTMSG_TIME = 2;
private HeartMsg heartMsg = new HeartMsg();
private
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
//客户端循环定时给服务端发送心跳消息
InvokeRepeating("SendHeartMsg", 0, SEND_HEARTMSG_TIME);
}
private void SendHeartMsg()
{
if(isConneted)
Send(heartMsg);
}
// Update is called once per frame
void Update()
{
if(receiveQueue.Count > 0)
{
BaseMsg msg = receiveQueue.Dequeue();
if(msg is PlayerMsg)
{
PlayerMsg playerMsg = (PlayerMsg)msg;
Debug.Log(playerMsg.playerID);
Debug.Log(playerMsg.playerData.name);
Debug.Log(playerMsg.playerData.atk);
Debug.Log(playerMsg.playerData.lev);
}
}
}
//连接服务端
public void Connect(string ip, int port)
{
//如果是连接状态直接返回
if (isConneted)
return;
if(socket == null)
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//连接服务端
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
try
{
socket.Connect(ipPoint);
isConneted = true;
//开启发送线程
ThreadPool.QueueUserWorkItem(SendMsg);
//开启接收线程
ThreadPool.QueueUserWorkItem(ReceiveMsg);
}
catch (SocketException e)
{
if (e.ErrorCode == 10061)
Debug.Log("服务拒绝连接");
else
Debug.Log("连接失败:" + e.ErrorCode + e.Message);
}
}
//发送消息
public void Send(BaseMsg msg)
{
sendMsgQueue.Enqueue(msg);
}
/// <summary>
/// 用于测试直接发字节数组的方法
/// </summary>
/// <param name="bytes"></param>
public void SendTest(byte[] bytes)
{
socket.Send(bytes);
}
private void SendMsg(object obj)
{
while (isConneted)
{
if(sendMsgQueue.Count > 0)
{
socket.Send(sendMsgQueue.Dequeue().Writing());
}
}
}
//不停的接受消息
private void ReceiveMsg(object obj)
{
while (isConneted)
{
if(socket.Available > 0)
{
byte[] receiveBytes = new byte[1024 * 1024];
int receiveNum = socket.Receive(receiveBytes);
首先把收到字节数组的前4个字节 读取出来得到ID
//int msgID = BitConverter.ToInt32(receiveBytes, 0);
//BaseMsg baseMsg = null;
//switch (msgID)
//{
// case 1001:
// PlayerMsg msg = new PlayerMsg();
// msg.Reading(receiveBytes, 4);
// baseMsg = msg;
// break;
// default:
// break;
//}
如果消息为空 那证明是不知道类型的消息 没有解析
//if (baseMsg == null)
// continue;
收到消息 解析消息为字符串 并放入公共容器
//receiveQueue.Enqueue(baseMsg);
HandleReceiveMsg(receiveBytes, receiveNum);
}
}
}
//处理接收消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if(cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if(cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
PlayerMsg msg = new PlayerMsg();
msg.Reading(cacheBytes, nowIndex);
baseMsg = msg;
break;
default:
break;
}
if (baseMsg != null)
receiveQueue.Enqueue(baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果不满足 证明有分包
//那么我们需要把当前收到的内容 记录下来
//有待下次接收到消息后 再做处理
//receiveBytes.CopyTo(cacheBytes, 0);
//cacheNum = receiveNum;
//如果进行了 ID和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
public void Close()
{
if(socket != null)
{
Debug.Log("客户端主动断开连接");
//主动发送一条断开连接的消息给服务端
//QuitMsg quitMsg = new QuitMsg();
//socket.Send(quitMsg.Writing());
//socket.Shutdown(SocketShutdown.Both);
//socket.Disconnect(false);
//socket.Close();
socket = null;
isConneted = false;
}
}
private void OnDestroy()
{
Close();
}
}
2)启动代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
if(NetMgr.Instance == null)
{
GameObject obj = new GameObject("Net");
obj.AddComponent<NetMgr>();
}
NetMgr.Instance.Connect("127.0.0.1", 8080);
}
// Update is called once per frame
void Update()
{
}
}
3)消息发送测试代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR;
public class Lesson7 : MonoBehaviour
{
public Button btn;
public Button btn1;
public Button btn2;
public Button btn3;
public InputField input;
// Start is called before the first frame update
void Start()
{
btn.onClick.AddListener(() =>
{
if(input.text != "")
{
PlayerMsg ms = new PlayerMsg();
ms.playerID = 123123;
ms.playerData = new PlayerData();
ms.playerData.name = "hellow";
ms.playerData.atk = 0;
ms.playerData.lev = 1;
NetMgr.Instance.Send(ms);
}
});
//黏包测试
btn1.onClick.AddListener(() =>
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 1001;
msg.playerData = new PlayerData();
msg.playerData.name = "当老师1";
msg.playerData.atk = 1;
msg.playerData.lev = 1;
PlayerMsg msg2 = new PlayerMsg();
msg2.playerID = 1002;
msg2.playerData = new PlayerData();
msg2.playerData.name = "当老师2";
msg2.playerData.atk = 2;
msg2.playerData.lev = 2;
byte[] bytes = new byte[msg.GetBytesNum() + msg2.GetBytesNum()];
msg.Writing().CopyTo(bytes, 0);
msg2.Writing().CopyTo(bytes, msg.GetBytesNum());
NetMgr.Instance.SendTest(bytes);
});
//分包测试
btn2.onClick.AddListener(async () =>
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 1001;
msg.playerData = new PlayerData();
msg.playerData.name = "当老师1";
msg.playerData.atk = 1;
msg.playerData.lev = 1;
byte[] bytes = msg.Writing();
//分包
byte[] bytes1 = new byte[10];
byte[] bytes2 = new byte[bytes.Length - 10];
//分成第一个包
Array.Copy(bytes, 0, bytes1, 0, 10);
//分第二个包
Array.Copy(bytes, 10, bytes2 , 0, bytes.Length - 10);
NetMgr.Instance.SendTest(bytes1);
await Task.Delay(500);
NetMgr.Instance.SendTest(bytes2);
});
//分包、黏包测试
btn3.onClick.AddListener(async () =>
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 1001;
msg.playerData = new PlayerData();
msg.playerData.name = "当老师1";
msg.playerData.atk = 1;
msg.playerData.lev = 1;
PlayerMsg msg2 = new PlayerMsg();
msg2.playerID = 1002;
msg2.playerData = new PlayerData();
msg2.playerData.name = "当老师2";
msg2.playerData.atk = 2;
msg2.playerData.lev = 2;
byte[] bytes1 = msg.Writing();//消息A
byte[] bytes2 = msg2.Writing();//消息B
byte[] bytes2_1 = new byte[10];
byte[] bytes2_2 = new byte[bytes2.Length - 10];
//分成第一个包
Array.Copy(bytes2, 0, bytes2_1, 0, 10);
//分第二个包
Array.Copy(bytes2, 10, bytes2_2, 0, bytes2.Length - 10);
//消息A和消息B前一段的 黏包
byte[] bytes = new byte[bytes1.Length + bytes2_1.Length];
bytes1.CopyTo(bytes, 0);
bytes2_1.CopyTo(bytes, bytes1.Length);
NetMgr.Instance.SendTest(bytes);
await Task.Delay(1000);
NetMgr.Instance.SendTest(bytes2_2);
});
}
// Update is called once per frame
void Update()
{
}
}