惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
博客园_首页
N
News | PayPal Newsroom
有赞技术团队
有赞技术团队
The Hacker News
The Hacker News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
S
SegmentFault 最新的问题
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
P
Privacy & Cybersecurity Law Blog
AI
AI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Schneier on Security
Schneier on Security
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
量子位
Forbes - Security
Forbes - Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tor Project blog
Recorded Future
Recorded Future
A
About on SuperTechFans
J
Java Code Geeks
The Register - Security
The Register - Security
PCI Perspectives
PCI Perspectives
H
Hacker News: Front Page
V2EX - 技术
V2EX - 技术
S
Secure Thoughts
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Scott Helme
Scott Helme
T
The Exploit Database - CXSecurity.com
Y
Y Combinator Blog
AWS News Blog
AWS News Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
IT之家
IT之家
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
C
CERT Recently Published Vulnerability Notes
L
LangChain Blog
F
Full Disclosure
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The GitHub Blog
The GitHub Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
C# 中 Socket 通信编程的同步实现 - 元视角
飞鸿踏雪 · 2015-03-15 · via 元视角

本文通过分析和总结 C# 中 Socket 通信编程的关键技术,按照同步实现的方式实现了一个简单的 Socket 聊天程序,目的是通过这个程序来掌握 Socket 编程,为进一步开发 Unity3D 网络游戏打下一个坚实的基础。

Socket 编程基础

关于 Socket 编程基础部分的内容,主要是了解和掌握.NET 框架下为 Socket 编程提供的相关类和接口方法。.NET 中常见的网络相关的 API 都集中在 System.Net 和 System.Net.Socket 这两个命名空间下,大家可以通过 MSDN 去了解这两个命名空间下相关的类和方法。这里援引一位朋友总结的一篇文章http://www.cnblogs.com/sunev/archive/2012/08/05/2604189.html,大家可以从这里获得更为直观的认识。

什么是 Socket 编程的同步实现

本文的目的是按照同步实现的方式来实现一个简单的 Socket 聊天程序,因此在解决这个问题前,我们首先来看看什么是 Socket 编程的同步实现。所谓 Socket 编程的同步实现就是指按照同步过程的方法来实现 Socket 通信。从编程来说,我们常用的方法或者函数都是同步过程。因为当我们调用一个方法或者函数的时候我们能够立即得到它的返回值。可是我们知道在 Socket 通信中,我们不能保证时时刻刻连接都通畅、更不能够保证时时刻刻都有数据收发,因为我们就需要不断去读取相应的值来确定整个过程的状态。这就是 Socket 编程的同步实现了,下面我们来看具体的实现过程。

如何实现 Socket 同步通信

服务端

服务端的主要职责是处理各个客户端发送来的数据,因此在客户端的 Socket 编程中需要使用两个线程来循环处理客户端的请求,一个线程用于监听客户端的连接情况,一个线程用于监听客户端的消息发送,当服务端接收到客户端的消息后需要将消息处理后再分发给各个客户端。

基本流程

  • 创建套接字
  • 绑定套接字的 IP 和端口号——Bind()
  • 将套接字处于监听状态等待客户端的连接请求——Listen()
  • 当请求到来后,接受请求并返回本次会话的套接字——Accept()
  • 使用返回的套接字和客户端通信——Send()/Receive()
  • 返回,再次等待新的连接请求
  • 关闭套接字

代码示例

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace TCPLib
{
    public class TCPServer
    {
        private byte[] result = new byte[1024];
        /// <summary>
        /// 最大的监听数量
        /// </summary>
        private int maxClientCount;
        public int MaxClientCount
        {
            get { return maxClientCount; }
            set { maxClientCount = value; }
        }

        /// <summary>
        /// IP地址
        /// </summary>
        private string ip;
        public string IP
        {
            get { return ip; }
            set { ip = value; }
        }

        /// <summary>
        /// 端口号
        /// </summary>
        private int port;
        public int Port
        {
            get { return port; }
            set { port = value; }
        }

        /// <summary>
        /// 客户端列表
        /// </summary>
        private List<Socket> mClientSockets;
        public List<Socket> ClientSockets
        {
            get { return mClientSockets; }
        }

        /// <summary>
        /// IP终端
        /// </summary>
        private IPEndPoint ipEndPoint;

        /// <summary>
        /// 服务端Socket
        /// </summary>
        private Socket mServerSocket;

        /// <summary>
        /// 当前客户端Socket
        /// </summary>
        private Socket mClientSocket;
        public Socket ClientSocket 
        {
            get { return mClientSocket;  }
            set { mClientSocket = value; }
        }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="port">端口号</param>
        /// <param name="count">监听的最大树目</param>
        public TCPServer(int port, int count)
        {
            this.ip = IPAddress.Any.ToString();
            this.port = port;
            this.maxClientCount=count;

            this.mClientSockets = new List<Socket>();

            //初始化IP终端
            this.ipEndPoint = new IPEndPoint(IPAddress.Any, port);
            //初始化服务端Socket
            this.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //端口绑定
            this.mServerSocket.Bind(this.ipEndPoint);
            //设置监听数目
            this.mServerSocket.Listen(maxClientCount);
        }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="ip">ip地址</param>
        /// <param name="port">端口号</param>
        /// <param name="count">监听的最大数目</param>
        public TCPServer(string ip,int port,int count)
        {
            this.ip = ip;
            this.port = port;
            this.maxClientCount = count;

            this.mClientSockets = new List<Socket>();

            //初始化IP终端
            this.ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            //初始化服务端Socket
            this.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //端口绑定
            this.mServerSocket.Bind(this.ipEndPoint);
            //设置监听数目
            this.mServerSocket.Listen(maxClientCount);

        }

        /// <summary>
        /// 定义一个Start方法将构造函数中的方法分离出来
        /// </summary>
        public void Start()
        {
            //创建服务端线程,实现客户端连接请求的循环监听
            var mServerThread = new Thread(this.ListenClientConnect);
            //服务端线程开启
            mServerThread.Start();
        }

        /// <summary>
        /// 监听客户端链接
        /// </summary>
        private void ListenClientConnect()
        {
            //设置循环标志位
            bool flag = true;
            while (flag)
            {
                //获取连接到服务端的客户端
                this.ClientSocket = this.mServerSocket.Accept();
                //将获取到的客户端添加到客户端列表
                this.mClientSockets.Add(this.ClientSocket);
                //向客户端发送一条消息
                this.SendMessage(string.Format("客户端{0}已成功连接到服务器", this.ClientSocket.RemoteEndPoint));
                //创建客户端消息线程,实现客户端消息的循环监听
                var mReveiveThread = new Thread(this.ReceiveClient);
                //注意到ReceiveClient方法传入了一个参数
                //实际上这个参数就是此时连接到服务器的客户端
                //即ClientSocket
                mReveiveThread.Start(this.ClientSocket);
            }
        }

        /// <summary>
        /// 接收客户端消息的方法
        /// </summary>
        private void ReceiveClient(object obj)
        {
            //获取当前客户端
            //因为每次发送消息的可能并不是同一个客户端,所以需要使用var来实例化一个新的对象
            //可是我感觉这里用局部变量更好一点
            var mClientSocket = (Socket)obj;
            // 循环标志位
            bool flag = true;
            while (flag)
            {
                try
                {
                    //获取数据长度
                    int receiveLength = mClientSocket.Receive(result);
                    //获取客户端消息
                    string clientMessage = Encoding.UTF8.GetString(result, 0, receiveLength);
                    //服务端负责将客户端的消息分发给各个客户端
                    this.SendMessage(string.Format("客户端{0}发来消息:{1}",mClientSocket.RemoteEndPoint,clientMessage));

                }
                catch (Exception e)
                {
                    //从客户端列表中移除该客户端
                    this.mClientSockets.Remove(mClientSocket);
                    //向其它客户端告知该客户端下线
                    this.SendMessage(string.Format("服务器发来消息:客户端{0}从服务器断开,断开原因:{1}",mClientSocket.RemoteEndPoint,e.Message));
                    //断开连接
                    mClientSocket.Shutdown(SocketShutdown.Both);
                    mClientSocket.Close();
                    break;
                }
            }
            
        }

        /// <summary>
        /// 向所有的客户端群发消息
        /// </summary>
        /// <param name="msg">message</param>
        public void SendMessage(string msg)
        {
            //确保消息非空以及客户端列表非空
            if (msg == string.Empty || this.mClientSockets.Count <= 0) return;
            //向每一个客户端发送消息
            foreach (Socket s in this.mClientSockets)
            {
                (s as Socket).Send(Encoding.UTF8.GetBytes(msg));
            }
        }

        /// <summary>
        /// 向指定的客户端发送消息
        /// </summary>
        /// <param name="ip">ip</param>
        /// <param name="port">port</param>
        /// <param name="msg">message</param>
        public void SendMessage(string ip,int port,string msg)
        {
            //构造出一个终端地址
            IPEndPoint _IPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
            //遍历所有客户端
            foreach (Socket s in mClientSockets)
            {
                if (_IPEndPoint == (IPEndPoint)s.RemoteEndPoint)
                {
                    s.Send(Encoding.UTF8.GetBytes(msg));
                }
            }
        }
    }
}

好了,现在我们已经编写好了一个具备接收和发送数据能力的服务端程序。现在我们来尝试让服务端运行起来:

using System;
using System.Collections.Generic;
using System.Text;
using TCPLib;
using System.Net;
using System.Net.Sockets;

namespace TCPLib.Test
{
    class Program
    {
        static void Main(string[] args)
        {
            //指定IP和端口号及最大监听数目的方式
            TCPLib.TCPServer s1 = new TCPServer("127.0.0.1", 6001, 10);
            //指定端口号及最大监听数目的方式
            TCPLib.TCPServer s2 = new TCPServer(6001, 10);
           
            //执行Start方法
            s1.Start();
        
        }
    }

}

现在我们来看看编写客户端 Socket 程序的基本流程

客户端

客户端相对于服务端来说任务要轻许多,因为客户端仅仅需要和服务端通信即可,可是因为在和服务器通信的过程中,需要时刻保持连接通畅,因此同样需要两个线程来分别处理连接情况的监听和消息发送的监听。

基本流程

  • 创建套接字保证与服务器的端口一致
  • 向服务器发出连接请求——Connect()
  • 和服务器端进行通信——Send()/Receive()
  • 关闭套接字

代码示例

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace TCPLib
{
    public class TCPClient
    {
        /// <summary>
        /// 定义数据
        /// </summary>
        private byte[] result = new byte[1024];

        /// <summary>
        /// 客户端IP
        /// </summary>
        private string ip;
        public string IP
        {
            get { return ip; }
            set { ip = value; }
        }

        /// <summary>
        /// 客户端端口号
        /// </summary>
        private int port;
        public int Port
        {
            get { return port; }
            set { port = value; }
        }

        /// <summary>
        /// IP终端
        /// </summary>
        private IPEndPoint ipEndPoint;

        /// <summary>
        /// 客户端Socket
        /// </summary>
        private Socket mClientSocket;

        /// <summary>
        /// 是否连接到了服务器
        /// 默认为flase
        /// </summary>
        private bool isConnected = false;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="ip">IP地址</param>
        /// <param name="port">端口号</param>
        public TCPClient(string ip, int port)
        {
            this.ip=ip;
            this.port=port;
            //初始化IP终端
            this.ipEndPoint = new IPEndPoint(IPAddress.Parse(this.ip), this.port);
            //初始化客户端Socket
            mClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        }

        public void Start()
        {
            //创建一个线程以不断连接服务器
            var mConnectThread = new Thread(this.ConnectToServer);
            //开启线程
            mConnectThread.Start();
        }

        /// <summary>
        /// 连接到服务器
        /// </summary>
        private void ConnectToServer()
        {
            //当没有连接到服务器时开始连接
            while (!isConnected)
            {
                try
                {
                    //开始连接
                    mClientSocket.Connect(this.ipEndPoint);
                    this.isConnected = true;
                }
                catch (Exception e)
                {
                    //输出Debug信息
                    Console.WriteLine(string.Format("因为一个错误的发生,暂时无法连接到服务器,错误信息为:{0}",e.Message));
                    this.isConnected = false;
                }

                //等待5秒钟后尝试再次连接
                Thread.Sleep(5000);
                Console.WriteLine("正在尝试重新连接...");
            }

            //连接成功后
            Console.WriteLine("连接服务器成功,现在可以和服务器进行会话了");
            //创建一个线程以监听数据接收
            var mReceiveThread = new Thread(this.ReceiveMessage);
            //开启线程
            mReceiveThread.Start();
        }

        /// <summary>
        /// 因为客户端只接受来自服务器的数据
        /// 因此这个方法中不需要参数
        /// </summary>
        private void ReceiveMessage()
        {
            //设置循环标志位
            bool flag = true;
            while (flag)
            {
                try
                {
                    //获取数据长度
                    int receiveLength = this.mClientSocket.Receive(result);
                    //获取服务器消息
                    string serverMessage = Encoding.UTF8.GetString(result, 0, receiveLength);
                    //输出服务器消息
                    Console.WriteLine(serverMessage);
                }
                catch (Exception e)
                {
                    //停止消息接收
                    flag = false;
                    //断开服务器
                    this.mClientSocket.Shutdown(SocketShutdown.Both);
                    //关闭套接字
                    this.mClientSocket.Close();

                    //重新尝试连接服务器
                    this.isConnected = false;
                    ConnectToServer();
                }
            }
            
        }

        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="msg">消息文本</param>
        public void SendMessage(string msg)
        {
            if(msg==string.Empty || this.mClientSocket==null) return;

            mClientSocket.Send(Encoding.UTF8.GetBytes(msg));
        }
    }
}

同样地,我们现在来运行客户端程序,这样客户端就可以和服务端进行通信了:

using System;
using System.Collections.Generic;
using System.Text;
using TCPLib;
using System.Net;
using System.Net.Sockets;

namespace TCPLib.Test
{
    class Program
    {
        static void Main(string[] args)
        {
            //保证端口号和服务端一致
            TCPLib.TCPClient c = new TCPClient("127.0.0.1",6001);
            //执行Start方法
            c.Start();
            while(true)
            {
                //读取客户端输入的消息
                string msg = Console.ReadLine();
                //发送消息到服务端
                c.SendMessage(msg);
            }
        
        }
    }

}

注意要先运行服务端的程序、再运行客户端的程序,不然程序会报错,嘿嘿!好了,下面是今天的效果演示图:

聊天窗口效果演示 聊天窗口效果演示

客户端下线效果演示 客户端下线效果演示

总结

今天我们基本上写出了一个可以使用的用例,不过这个例子目前还存在以下问题:

  • 这里仅仅实现了发送字符串的功能,如何让这个程序支持更多的类型,从基础的 int、float、double、string、single 等类型到 structure、class 甚至是二进制文件的类型?

  • 如何让这个用例更具有扩展性,我们发现所有的 Socket 编程流程都是一样的,唯一不同就是在接收到数据以后该如何去处理,因为能不能将核心功能和自定义功能分离开来?

  • 在今天的这个用例中,数据传输的缓冲区大小我们人为设定为 1024,那么如果碰到比这个设定更大的数据类型,这个用例该怎么来写?

好了,这就是今天的内容了,希望大家喜欢,同时希望大家关注我的博客!

2016 年 1 月 24 日更新: 要解决“支持更多类型的问题”,可以从两种思路来考虑,即实现所有类型到 byte[]类型的转换或者是实现所有类型到 string 类型的转换,对于第二种思路我们通常称之为序列化,序列化可以解决所有类型到 string 类型的转换问题,唯一可能需要考量的一个部分就是缓冲区的大小问题。

要解决“将核心功能和自定义功能分离”这个问题,可以考虑使用委托机制来实现,委托机制可以理解为一个函数的指针,在需要将函数的控制权交给用户来处理的场景中,委托都是一种有效而明智的选择。