介绍
在阅读了罗培羽著作的Unity3D网络游戏实战一书后,博主综合自己的开发经验与考虑进行部分修改和调整,将通用的客户端网络模块和通用的服务端框架进行提取,形成专栏,介绍Socket网络编程,希望对其他人有所帮助。目录如下:
一、通用服务端框架
(一)、定义套接字和多路复用
(二)、客户端信息类和通用缓冲区结构
(三)、Protobuf 通信协议
(四)、数据处理和关闭连接
(五)、Messenger 事件发布、订阅系统
(六)、单点发送和广播数据
(七)、时间戳和心跳机制
二、通用客户端网络模块
(一)、Connect 连接服务端
(二)、Receive 接收并处理数据
(三)、Send 发送数据
(四)、Close 关闭连接
本篇内容:
Socket套接字的定义:
首先编写服务器初始化的方法Init,接受一个参数port,即监听的端口,在Main函数中调用Init传入端口以启动服务器。
using System.Net;
using System.Net.Sockets;
namespace SK.Framework.Sockets
{
///
/// 服务器
///
public class Server
{
//定义套接字
private static Socket socket;
private static void Main(string[] args)
{
Init(8801);
}
//服务器初始化
//port: 端口
private static void Init(int port)
{
Console.WriteLine("服务器启动...");
//TODO
}
}
}
Socket在调用Listen监听方法之前,必须先调用Bind方法,需要声明服务器的IP地址及监听的端口,如果不关心使用哪个本地端口,可以使用0作为端口号,系统将会自动分配1024到5000之间的可用端口号。Listen方法中参数backlog代表可排队等待接受的传入连接的数量,即挂起的连接队列的最大长度。
using System.Net;
using System.Net.Sockets;
namespace SK.Framework.Sockets
{
///
/// 服务器
///
public class Server
{
//定义套接字
private static Socket socket;
private static void Main(string[] args)
{
Init(8801);
}
//服务器初始化
//port: 端口
private static void Init(int port)
{
Console.WriteLine("服务器启动...");
//Socket Tcp协议
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//服务器IP地址
IPAddress ipAddress = IPAddress.Parse("0.0.0.0");
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
//Bind
socket.Bind(ipEndPoint);
//Listen 开启监听
socket.Listen(100);
//TODO
}
}
}
Select多路复用:
// 摘要:
// Determines the status of one or more sockets.
//
// 参数:
// checkRead:
// An System.Collections.IList of System.Net.Sockets.Socket instances to check for
// readability.
//
// checkWrite:
// An System.Collections.IList of System.Net.Sockets.Socket instances to check for
// writability.
//
// checkError:
// An System.Collections.IList of System.Net.Sockets.Socket instances to check for
// errors.
//
// microSeconds:
// The time-out value, in microseconds. A -1 value indicates an infinite time-out.
//
// 异常:
// T:System.ArgumentNullException:
// The checkRead parameter is null or empty. -and- The checkWrite parameter is null
// or empty -and- The checkError parameter is null or empty.
//
// T:System.Net.Sockets.SocketException:
// An error occurred when attempting to access the socket.
//
// T:System.ObjectDisposedException:
// .NET 5.0 and later: One or more sockets are disposed.
public static void Select(IList? checkRead, IList? checkWrite, IList? checkError, int microSeconds)
关于Select方法的官方文档链接地址:
https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socket.select?view=net-6.0
该方法可以帮助我们实现non-block非阻塞方式,第一个参数checkRead代表需要检测可读性的Socket列表,第四个参数microSeconds代表阻塞等待的时长,单位为毫秒,例如传入1000则代表设置1秒的阻塞等待时长,当1秒内没有可读消息时,它会停止阻塞,返回空的checkRead列表,程序继续运行。
代码实现如下,其中的Client类定义了代表客户端信息的相关内容,在后续章节中进行介绍。
using ProtoBuf;
using System.Net;
using System.Net.Sockets;
namespace SK.Framework.Sockets
{
///
/// 服务器
///
public class Server
{
//定义套接字
private static Socket socket;
//用于检测可读性的Socket列表
private readonly static List checkReadableList = new List();
//客户端Socket及客户端信息字典
private readonly static Dictionary clients = new Dictionary();
private static void Main(string[] args)
{
Init(8801);
}
//服务器初始化
//port: 端口
private static void Init(int port)
{
Console.WriteLine("服务器启动...");
//Socket Tcp协议
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//服务器IP地址
IPAddress ipAddress = IPAddress.Parse("0.0.0.0");
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
//Bind
socket.Bind(ipEndPoint);
//Listen 开启监听
socket.Listen(0);
//循环
while (true)
{
//首先重置用于检测可读性的Socket列表
OnCheckReadableListReset();
//使用Select检测可读 实现non-block非阻塞方式
//arg4: 超时值 单位毫秒 此处设置1000表示 1秒内没有可读消息时停止阻塞 返回空的列表
Socket.Select(checkReadableList, null, null, 1000);
//遍历检查可读对象
for (int i = 0; i < checkReadableList.Count; i++)
{
Socket s = checkReadableList[i];
if (s == socket) OnListenEvent(s);
else OnClientEvent(s);
}
}
}
private static void OnCheckReadableListReset()
{
checkReadableList.Clear();
//进行Select的列表包含监听套接字socket以及每个已经连接的客户端套接字
checkReadableList.Add(socket);
foreach (Client client in clients.Values)
{
checkReadableList.Add(client.socket);
}
}
//监听事件
private static void OnListenEvent(Socket s) {}
//客户端消息事件
private static void OnClientEvent(Socket s) {}
}
}
其中OnListenEvent方法用于处理客户端连接的消息,代码如下:
//监听事件
private static void OnListenEvent(Socket s)
{
try
{
//接受客户端连接
Socket socket = s.Accept();
Console.WriteLine($"客户端接入: {socket.RemoteEndPoint}");
Client client = new Client(socket);
//加入字典
clients.Add(socket, client);
}
catch (SocketException error)
{
Console.WriteLine($"客户端接入失败: {error}");
}
}
OnClientEvent方法用于处理客户端发送来的消息,代码如下:
//客户端消息事件
private static void OnClientEvent(Socket s)
{
//从字典中获取该客户端信息类
Client client = clients[s];
//该客户端的读缓冲区
ByteArray readBuff = client.readBuff;
//如果缓冲区剩余空间不足 清除
if (readBuff.remain
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?