using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
public class Lesson5 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
#region 知识点一 Socket套接字的作用
//它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类)
//类名:Socket
//命名空间:System.Net.Sockets
//Socket套接字是支持TCP/IP网络通信的基本操作单位
//一个套接字对象包含以下关键信息
//1.本机的IP地址和端口
//2.对方主机的IP地址和端口
//3.双方通信的协议信息
//一个Sccket对象表示一个本地或者远程套接字信息
//它可以被视为一个数据通道
//这个通道连接与客户端和服务端之间
//数据的发送和接受均通过这个通道进行
//一般在制作长连接游戏时,我们会使用Socket套接字作为我们的通信方案
//我们通过它连接客户端和服务端,通过它来收发消息
//你可以把它抽象的想象成一根管子,插在客户端和服务端应用程序上,通过这个管子来传递交换信息
#endregion
#region 知识点二 Socket的类型
//Socket套接字有3种不同的类型
//1.流套接字
// 主要用于实现TCP通信,提供了面向连接、可靠的、有序的、数据无差错且无重复的数据传输服务
//2.数据报套接字
// 主要用于实现UDP通信,提供了无连接的通信服务,数据包的长度不能大于32KB,不提供正确性检查,不保证顺序,可能出现重发、丢失等情况
//3.原始套接字(不常用,不深入讲解)
// 主要用于实现IP数据包通信,用于直接访问协议的较低层,常用于侦听和分析数据包
//通过Socket的构造函数 我们可以申明不同类型的套接字
//Socket s = new Socket()
//参数一:AddressFamily 网络寻址 枚举类型,决定寻址方案
// 常用:
// 1.InterNetwork IPv4寻址
// 2.InterNetwork6 IPv6寻址
// 做了解:
// 1.UNIX UNIX本地到主机地址
// 2.ImpLink ARPANETIMP地址
// 3.Ipx IPX或SPX地址
// 4.Iso ISO协议的地址
// 5.Osi OSI协议的地址
// 7.NetBios NetBios地址
// 9.Atm 本机ATM服务地址
//参数二:SocketType 套接字枚举类型,决定使用的套接字类型
// 常用:
// 1.Dgram 支持数据报,最大长度固定的无连接、不可靠的消息(主要用于UDP通信)
// 2.Stream 支持可靠、双向、基于连接的字节流(主要用于TCP通信)
// 做了解:
// 1.Raw 支持对基础传输协议的访问
// 2.Rdm 支持无连接、面向消息、以可靠方式发送的消息
// 3.Seqpacket 提供排序字节流的面向连接且可靠的双向传输
//参数三:ProtocolType 协议类型枚举类型,决定套接字使用的通信协议
// 常用:
// 1.TCP TCP传输控制协议
// 2.UDP UDP用户数据报协议
// 做了解:
// 1.IP IP网际协议
// 2.Icmp Icmp网际消息控制协议
// 3.Igmp Igmp网际组管理协议
// 4.Ggp 网关到网关协议
// 5.IPv4 Internet协议版本4
// 6.Pup PARC通用数据包协议
// 7.Idp Internet数据报协议
// 8.Raw 原始IP数据包协议
// 9.Ipx Internet数据包交换协议
// 10.Spx 顺序包交换协议
// 11.IcmpV6 用于IPv6的Internet控制消息协议
//2、3参数的常用搭配:
// SocketType.Dgram + ProtocolType.Udp = UDP协议通信(常用,主要学习)
// SocketType.Stream + ProtocolType.Tcp = TCP协议通信(常用,主要学习)
// SocketType.Raw + ProtocolType.Icmp = Internet控制报文协议(了解)
// SocketType.Raw + ProtocolType.Raw = 简单的IP包通信(了解)
//我们必须掌握的
//TCP流套接字
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//UDP数据报套接字
Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
#endregion
#region 知识点三 Socket的常用属性
//1.套接字的连接状态
if(socketTcp.Connected)
{
}
//2.获取套接字的类型
print(socketTcp.SocketType);
//3.获取套接字的协议类型
print(socketTcp.ProtocolType);
//4.获取套接字的寻址方案
print(socketTcp.AddressFamily);
//5.从网络中获取准备读取的数据数据量
print(socketTcp.Available);
//6.获取本机EndPoint对象(注意 :IPEndPoint继承EndPoint)
//socketTcp.LocalEndPoint as IPEndPoint
//7.获取远程EndPoint对象
//socketTcp.RemoteEndPoint as IPEndPoint
#endregion
#region 知识点四 Socket的常用方法
//1.主要用于服务端
// 1-1:绑定IP和端口
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
// 1-2:设置客户端连接的最大数量
socketTcp.Listen(10);
// 1-3:等待客户端连入
socketTcp.Accept();
//2.主要用于客户端
// 1-1:连接远程服务端
socketTcp.Connect(IPAddress.Parse("118.12.123.11"), 8080);
//3.客户端服务端都会用的
// 1-1:同步发送和接收数据
// 1-2:异步发送和接收数据
// 1-3:释放连接并关闭Socket,先与Close调用
socketTcp.Shutdown(SocketShutdown.Both);
// 1-4:关闭连接,释放所有Socket关联资源
socketTcp.Close();
#endregion
#region 总结
//这节课我们只是对Socket有一个大体的认识
//主要要建立的概念就是
//TCP和UDP两种长连接通信方案都是基于Socket套接字的
//我们之后只需要使用其中的各种方法,就可以进行网络连接和网络通信了
//这节课必须掌握的内容就是如何声明TCP和UDP的Socket套接字
#endregion
}
// Update is called once per frame
void Update()
{
}
}
注意:如果需要在两台PC上使用以下代码测试通信需要确保两台PC处于统一局域网(连接同一WIFI),并将服务器与客户端代码中的本机回环IP(127.0.0.1)改为作为服务器PC的IP地址(可以在cmd中输入ipconfig查询)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TeachTcpServer
{
class Program
{
static void Main(string[] args)
{
#region 知识点一 回顾服务端需要做的事情
//1.创建套接字Socket
//2.用Bind方法将套接字与本地地址绑定
//3.用Listen方法监听
//4.用Accept方法等待客户端连接
//5.建立连接,Accept返回新套接字
//6.用Send和Receive相关方法收发数据
//7.用Shutdown方法释放连接
//8.关闭套接字
#endregion
#region 知识点二 实现服务端基本逻辑
//1.创建套接字Socket(TCP)
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Bind方法将套接字与本地地址绑定
try
{
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
}
catch (Exception e)
{
Console.WriteLine("绑定报错" + e.Message);
return;
}
//3.用Listen方法监听
socketTcp.Listen(1024);
Console.WriteLine("服务端绑定监听结束,等待客户端连入");
//4.用Accept方法等待客户端连接
//5.建立连接,Accept返回新套接字
Socket socketClient = socketTcp.Accept();
Console.WriteLine("有客户端连入了");
//6.用Send和Receive相关方法收发数据
//发送
socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));
//接受
byte[] result = new byte[1024];
//返回值为接受到的字节数
int receiveNum = socketClient.Receive(result);
Console.WriteLine("接受到了{0}发来的消息:{1}",
socketClient.RemoteEndPoint.ToString(),
Encoding.UTF8.GetString(result, 0, receiveNum));
//7.用Shutdown方法释放连接
socketClient.Shutdown(SocketShutdown.Both);
//8.关闭套接字
socketClient.Close();
#endregion
#region 总结
//1.服务端开启的流程每次都是相同的
//2.服务端的 Accept、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容
//抛出问题:
//如何让服务端可以服务n个客户端?
//我们会在之后的综合练习题进行讲解
#endregion
Console.WriteLine("按任意键退出");
Console.ReadKey();
}
}
}
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class Lesson6 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
#region 知识点一 回顾客户端需要做的事情
//1.创建套接字Socket
//2.用Connect方法与服务端相连
//3.用Send和Receive相关方法收发数据
//4.用Shutdown方法释放连接
//5.关闭套接字
#endregion
#region 知识点二 实现客户端基本逻辑
//1.创建套接字Socket
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Connect方法与服务端相连
//确定服务端的IP和端口
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
try
{
socket.Connect(ipPoint);
}
catch (SocketException e)
{
if (e.ErrorCode == 10061)
print("服务器拒绝连接");
else
print("连接服务器失败" + e.ErrorCode);
return;
}
//3.用Send和Receive相关方法收发数据
//接收数据
byte[] receiveBytes = new byte[1024];
int receiveNum = socket.Receive(receiveBytes);
print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
//发送数据
socket.Send(Encoding.UTF8.GetBytes("你好,我是唐老狮的客户端"));
//4.用Shutdown方法释放连接
socket.Shutdown(SocketShutdown.Both);
//5.关闭套接字
socket.Close();
#endregion
#region 总结
//1.客户端连接的流程每次都是相同的
//2.客户端的 Connect、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容
//抛出问题:
//如何让客户端的Socket不影响主线程,并且可以随时收发消息?
//我们会在之后的综合练习题讲解
#endregion
}
// Update is called once per frame
void Update()
{
}
}
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachTcpServerExercises2
{
class ServerSocket
{
//服务端Socket
public Socket socket;
//客户端连接的所有Socket
public Dictionary clientDic = new Dictionary();
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);
client.Send("欢迎连入服务器");
clientDic.Add(client.clientID, client);
}
catch (Exception e)
{
Console.WriteLine("客户端连入报错" + e.Message);
}
}
}
//接收客户端消息
private void Receive(object obj)
{
while (!isClose)
{
if(clientDic.Count > 0)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Receive();
}
}
}
}
public void Broadcast(string info)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Send(info);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachTcpServerExercises2
{
class ClientSocket
{
private static int CLIENT_BEGIN_ID = 1;
public int clientID;
public Socket socket;
public ClientSocket(Socket socket)
{
this.clientID = CLIENT_BEGIN_ID;
this.socket = socket;
++CLIENT_BEGIN_ID;
}
///
/// 是否是连接状态
///
public bool Connected => this.socket.Connected;
//我们应该封装一些方法
//关闭
public void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
//发送
public void Send(string info)
{
if(socket != null)
{
try
{
socket.Send(Encoding.UTF8.GetBytes(info));
}
catch(Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
Close();
}
}
}
//接收
public void Receive()
{
if (socket == null)
return;
try
{
if(socket.Available > 0)
{
byte[] result = new byte[1024 * 5];
int receiveNum = socket.Receive(result);
ThreadPool.QueueUserWorkItem(MsgHandle, Encoding.UTF8.GetString(result, 0, receiveNum));
}
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
Close();
}
}
private void MsgHandle(object obj)
{
string str = obj as string;
Console.WriteLine("收到客户端{0}发来的消息:{1}", this.socket.RemoteEndPoint, str);
}
}
}
using System;
namespace TeachTcpServerExercises2
{
class Program
{
static void Main(string[] args)
{
ServerSocket 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:" )
{
socket.Broadcast(input.Substring(2));
}
}
}
}
}
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 sendMsgQueue = new Queue();
//用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取
private Queue receiveQueue = new Queue();
//用于收消息的水桶(容器)
private byte[] receiveBytes = new byte[1024 * 1024];
//返回收到的字节数
private int receiveNum;
//是否连接
private bool isConnected = false;
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void Update()
{
if(receiveQueue.Count > 0)
{
print(receiveQueue.Dequeue());
}
}
//连接服务端
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);
try
{
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 Send(string info)
{
sendMsgQueue.Enqueue(info);
}
private void SendMsg(object obj)
{
while (isConnected)
{
if (sendMsgQueue.Count > 0)
{
socket.Send(Encoding.UTF8.GetBytes(sendMsgQueue.Dequeue()));
}
}
}
//不停的接受消息
private void ReceiveMsg(object obj)
{
while (isConnected)
{
if(socket.Available > 0)
{
receiveNum = socket.Receive(receiveBytes);
//收到消息 解析消息为字符串 并放入公共容器
receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
}
}
}
public void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
isConnected = false;
}
}
private void OnDestroy()
{
Close();
}
}
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.Instance.Connect("127.0.0.1", 8080);
}
// Update is called once per frame
void Update()
{
}
}
在网络中传输的消息都继承自这个类
提供了序列化,反序列化各个基础数据类型的方法
自定义数据只需要继承该类重写Writing、Reading、GetNum方法,使用提供的基础类型序列化/反序列化方法即可实现自定义数据类型的序列化/反序列化
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
public abstract class BaseData
{
///
/// 用于子类重写的 获取字节数组容器大小的方法
///
///
public abstract int GetBytesNum();
///
/// 把成员变量 序列化为 对应的字节数组
///
///
public abstract byte[] Writing();
///
/// 把2进制字节数组 反序列化到 成员变量当中
///
/// 反序列化使用的字节数组
/// 从该字节数组的第几个位置开始解析 默认是0
public abstract int Reading(byte[] bytes, int beginIndex = 0);
///
/// 存储int类型变量到指定的字节数组当中
///
/// 指定字节数组
/// 具体的int值
/// 每次存储后用于记录当前索引位置的变量
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, index);
index += sizeof(bool);
}
protected void WriteString(byte[] bytes, string value, ref int index)
{
//先存储string字节数组的长度
byte[] strBytes = Encoding.UTF8.GetBytes(value);
//BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);
//index += sizeof(int);
WriteInt(bytes, strBytes.Length, ref index);
//再存 string字节数组
strBytes.CopyTo(bytes, index);
index += strBytes.Length;
}
protected void WriteData(byte[] bytes, BaseData data, ref int index)
{
data.Writing().CopyTo(bytes, index);
index += data.GetBytesNum();
}
///
/// 根据字节数组 读取整形
///
/// 字节数组
/// 开始读取的索引数
///
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 value = bytes[index];
index += sizeof(byte);
return value;
}
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 = ReadInt(bytes, ref index);
//再读取string
string value = Encoding.UTF8.GetString(bytes, index, length);
index += length;
return value;
}
protected T ReadData(byte[] bytes, ref int index) where T:BaseData,new()
{
T value = new T();
index += value.Reading(bytes, index);
return value;
}
}
(更新中)