基于socket开发网络调试助手

 

 1.什么是Socket?

在计算机领域socket被翻译为套接字,它是计算机之间进行通信的一种方式,通过socket这种约定,一台计算机可以向另外一台计算机发送数据和接收数据。

2.Socket的本质?

Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是socket编程接口。

3.socket的作用?

可以实现不同虚拟机或者是计算机之间的通信。

4.socket的典型应用?

(1)socket的典型应用之一 就是web服务器和浏览器,浏览器获取用户输入的URL,向服务器发起请求,服务器分析接收到的URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、

图片、视频等元素呈现给用户。

(2)QQ或者微信等聊天工具也是socket的应用之一,本地的QQ或者微信程序就是客户端,登录过程就是连接服务器的过程,聊天过程其实就是socket的发送和接收过程。

socket主要包括以下几种接口:

 

 socket位于应用层和传输层之间,把socket比作门,门外是邮局,你要送信就要通过门,把信从门送出去到邮局,然后由邮局帮你送达目标的门,目的地主任再打开门,从门取出来邮局送过来的信,上述比喻

中,邮局就是传输层(及更下面的层),而门内就是应用。

socket的编程方式?

socket,一切皆文件,都可以用"打开open------>读写read/write------->close"模式来操作。socket就是该模式的一个实现,socket即是一个特殊的文件,一些socket函数就是对其进行的操作(读/写IO,打开,关闭)

因此socket提供了类似于连接(Connect),关闭(Close),发送,接收等方法调用。

数据的传输方式:STREAM和DREAM

(1)STREAM表示面向连接的数据传输方式,数据可以准确无误的到达另一台计算机,如果损坏或丢失,可以重新发送,但是效率较慢。

(2)DREAM表示无连接的数据传输方式,计算机只管传输数据,不做数据校验,DREAM所作的校验工作少,所以效率比STREAM高。

QQ视频聊天就是使用DREAM传输数据,因为首先要保证通信效率,尽可能减小延迟,而数据的准确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点和杂音,不会对通

信质量有质的影响。

接下来,我们将使用socket来开发网络调试助手。

服务器端程序的编写:

第一步,创建一个用于通信的socket套接字

第二步,给已经创建的套接字绑定一个端口号,这个一般通过设置网络套接口地址和调用Bind()函数来实现。

第三步,调用Listen()函数使套接字成为一个监听的套接字。

第四步,调用accept()函数来接收客户端的连接,这时候就可以和客户端通信了。

第五步,处理客户端的连接请求。

第六步,终止连接。

界面搭建:

启动服务器的程序编写:

复制代码
   #region 开启服务
        /// <summary>
        /// 开启服务
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btn_StartService_Click(object sender, EventArgs e)
        {
            // 第一步:调用socket()函数创建一个用于通信的套接字,这里使用基于Tcp的方式
            socketSever = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 第二步:给已经创建的套接字绑定一个端口号,这一般通过设置网络套接口地址和调用bind()函数来实现。

            IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(this.txt_IP.Text), int.Parse(this.txt_Port.Text));

            try
            {
                socketSever.Bind(ipe);
            }
            catch (Exception ex)
            {
                //写入日志

                AddLog(2, "服务器开启失败:" + ex.Message);
                return;
            }

            // 第三步:调用listen()函数使套接字成为一个监听套接字。这个10表示的是存放在缓冲池的连接数(类似于数据库连接池),并不是最大的连接的客户端数量
//当接收到客户端连接,就会从缓冲区中拿掉一个,缓冲区就会空出一个位置,如果把监听客户端的线程注释掉,那么会发现,缓冲区的位置移植无法释放,
socketSever.Listen(10); //创建一个监听的线程 Task.Run(new Action(() => { CheckListening(); })); AddLog(0, "服务器开启成功"); this.btn_StartService.Enabled = false; } #endregion
复制代码

监听线程

复制代码
  #region 监听线程

        /// <summary>
        /// 检查监听的线程方法体
        /// </summary>
        private void CheckListening()
        {
            while (true)
            {
                // 第四步:调用accept()函数来接受客户端的连接,这是就可以和客户端通信了,Accept是一个阻塞式的,当有客户端连接上服务器的时候,就会继续向下执行。
                Socket socketClient = socketSever.Accept();

                string client = socketClient.RemoteEndPoint.ToString();

                AddLog(0, client + "上线了");
                //添加该客户端到字典中
                CurrentClientlist.Add(client, socketClient);

                UpdateOnline(client, true);

                Task.Run(new Action(() =>
                {
                    ReceiveMessage(socketClient);
                }));
            }
        }


        #endregion
复制代码

接收来自客户端的消息

复制代码
   #region 多线程接收数据
        /// <summary>
        /// 接收客户端数据的方法
        /// </summary>
        /// <param name="socketClient"></param>
        private void ReceiveMessage(Socket socketClient)
        {
            while (true)
            {
                // 创建一个10M的缓冲区

                byte[] buffer = new byte[1024 * 1024 * 10];

                int length = -1;

                string client = socketClient.RemoteEndPoint.ToString();

                // 第五步:处理客户端的连接请求。
                try
                {
//Receive也是一个阻塞式的方法,当接收到消息后,将会继续执行。接收到的数据将会放到缓冲区buffer中
//如果客户端断线,一个将会进入try catch,另外一个会进入接收的length为0 length
= socketClient.Receive(buffer); } catch (Exception) {
//客户端下线 UpdateOnline(client,
false); AddLog(0, client + "下线了");
//从字典中移除 CurrentClientlist.Remove(client);
break; } if (length > 0) { string msg = string.Empty; MessageType type = (MessageType)buffer[0]; //客户端发送消息时,首个字节表示的是发送的数据的类型,实际真正的数据长度是length-1,根据首字节来判断接收数据的类型 switch (type) {
//接收的是ASCLL
case MessageType.ASCII: msg = Encoding.ASCII.GetString(buffer, 1, length - 1); AddLog(0, client + "" + msg); break;
//接收UTF8
case MessageType.UTF8: msg = Encoding.UTF8.GetString(buffer, 1, length - 1); AddLog(0, client + "" + msg); break;
//接收十六进制
case MessageType.Hex: msg = HexGetString(buffer, 1, length - 1); AddLog(0, client + "" + msg); break;
//接收文件
case MessageType.File: Invoke(new Action(() => { SaveFileDialog sfd = new SaveFileDialog(); sfd.Filter = "txt files(*.txt)|*.txt|xls files(*.xls)|*.xls|xlsx files(*.xlsx)|*.xlsx|All files(*.*)|*.*"; if (sfd.ShowDialog() == DialogResult.OK) { string fileSavePath = sfd.FileName; using (FileStream fs = new FileStream(fileSavePath, FileMode.Create)) {
//将缓冲区的数据写入 fs.Write(buffer,
1, length - 1); } AddLog(0, "文件成功保存至" + fileSavePath); } })); break;
//接收JSON
case MessageType.JSON: Invoke(new Action(() => { string res = Encoding.Default.GetString(buffer, 1, length); List<Student> StuList = JSONHelper.JSONToEntity<List<Student>>(res); new FrmJSON(StuList).Show(); AddLog(0, "接收JSON数据:" + res); })); break; default: break; } }
//length<=0,也视为客户端下线
else { UpdateOnline(client, false); AddLog(0, client + "下线了"); CurrentClientlist.Remove(client); break; } } } #endregion
复制代码

字节数组转16进制字符串方法

复制代码
 #region 16进制字符串处理
        /// <summary>
        /// 将字节数组转换为16进制字符串
        /// </summary>
        /// <param name="buffer">字节数组</param>
        /// <param name="start">起始位置</param>
        /// <param name="length">截取的数组长度</param>
        /// <returns></returns>
        private string HexGetString(byte[] buffer, int start, int length)
        {
            string Result = string.Empty;

            if (buffer != null && buffer.Length >= start + length)
            {
                //截取字节数组

                byte[] res = new byte[length];

                Array.Copy(buffer, start, res, 0, length);

                string Hex = Encoding.Default.GetString(res, 0, res.Length);

                // 01   03 0 40 0A
                if (Hex.Contains(" "))
                {
                    string[] str = Regex.Split(Hex, "\\s+", RegexOptions.IgnoreCase);

                    foreach (var item in str)
                    {
                        Result += "0x" + item + " ";
                    }
                }
                else
                {
                    Result += "0x" + Hex;
                }

            }
            else
            {
                Result = "Error";
            }
            return Result;
        }


        #endregion
复制代码

更新在线列表方法,当客户端上线/下线的时候,服务器端的列表可以更新。

复制代码
 #region 在线列表更新
        /// <summary>
        /// 在线列表更新
        /// </summary>
        /// <param name="client">客户端ip</param>
        /// <param name="operate">从列表中移除或者从列表中增加</param>
        private void UpdateOnline(string client, bool operate)
        {
            //是否是跨线程访问
            if (!this.lst_Online.InvokeRequired)
            {
                if (operate)
                {
                    this.lst_Online.Items.Add(client);
                }
                else
                {
                    foreach (string item in this.lst_Online.Items)
                    {
                        if (item == client)
                        {
                            this.lst_Online.Items.Remove(item);
                            break;
                        }
                    }
                }
            }
            else
            {
                Invoke(new Action(() =>
                {
                    if (operate)
                    {
                        this.lst_Online.Items.Add(client);
                    }
                    else
                    {
                        foreach (string item in this.lst_Online.Items)
                        {
                            if (item == client)
                            {
                                this.lst_Online.Items.Remove(item);
                                break;
                            }
                        }
                    }
                }));
            }
        }

        #endregion
复制代码

显示日志方法

复制代码
        /// <summary>
        /// 显示日志的方法
        /// </summary>
        /// <param name="index">0,1,2分别表示显示日志信息的种类:正常,警告,错误</param>
        /// <param name="info">日志信息</param>
        private void AddLog(int index, string info)
        {
            if (!this.lst_Rcv.InvokeRequired)
            {
                ListViewItem lst = new ListViewItem("   " + CurrentTime, index);

                lst.SubItems.Add(info);

                lst_Rcv.Items.Insert(lst_Rcv.Items.Count, lst);

            }
            else
            {
                Invoke(new Action(() =>
                {
                    ListViewItem lst = new ListViewItem("   " + CurrentTime, index);

                    lst.SubItems.Add(info);

                    lst_Rcv.Items.Insert(lst_Rcv.Items.Count, lst);
                }));
            }
        }
复制代码

接收数据类型的枚举

public enum MessageType
    {
        ASCII,
        UTF8,
        Hex,
        File,
        JSON
    }

服务器发送指定格式的消息数据给指定的客户端和群发客户端,这里首先要将发送的数据转换为你要发送的那种数据格式

复制代码
      

        //创建字典集合,键是ClientIp,值是SocketClient
        private Dictionary<string, Socket> CurrentClientlist = new Dictionary<string, Socket>();

        #region 发送ASCII

        private void btn_SendASCII_Click(object sender, EventArgs e)
        {
            //选择要发送的数据的客户端
            if (this.lst_Online.SelectedItems.Count > 0)
            {
                AddLog(0, "发送内容:" + this.txt_Send.Text.Trim());

                byte[] send = Encoding.ASCII.GetBytes(this.txt_Send.Text.Trim());

                //创建最终发送的数组
                byte[] sendMsg = new byte[send.Length + 1];

                //整体拷贝数组,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg

Array.Copy(send, 0, sendMsg, 1, send.Length); //给首字节赋值,为了方便客户端识别该数据是什么类型的数据 sendMsg[0] = (byte)MessageType.ASCII; foreach (var item in this.lst_Online.SelectedItems) { //获取Socket对象 string client = item.ToString(); CurrentClientlist[client]?.Send(sendMsg); } this.txt_Send.Clear(); } else { MessageBox.Show("请选择你要发送的客户端对象!", "发送消息"); } } #endregion
复制代码
复制代码
        #region 发送Hex
        private void btn_SendHex_Click(object sender, EventArgs e)
        {
            if (this.lst_Online.SelectedItems.Count > 0)
            {
                AddLog(0, "发送内容:" + this.txt_Send.Text.Trim());
                //默认的是Hex
                byte[] send = Encoding.Default.GetBytes(this.txt_Send.Text.Trim());

                //创建最终发送的数组
                byte[] sendMsg = new byte[send.Length + 1];

                //整体拷贝数组,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg
                Array.Copy(send, 0, sendMsg, 1, send.Length);
                //给首字节赋值
                sendMsg[0] = (byte)MessageType.Hex;

                foreach (var item in this.lst_Online.SelectedItems)
                {
                    //获取Socket对象
                    string client = item.ToString();

                    CurrentClientlist[client]?.Send(sendMsg);
                }

                this.txt_Send.Clear();
            }
            else
            {
                MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
            }
        }

        #endregion
复制代码
复制代码
       #region 发送UTF8
        private void btn_SendUTF8_Click(object sender, EventArgs e)
        {
            if (this.lst_Online.SelectedItems.Count > 0)
            {
                AddLog(0, "发送内容:" + this.txt_Send.Text.Trim());
                
                byte[] send = Encoding.UTF8.GetBytes(this.txt_Send.Text.Trim());

                //创建最终发送的数组
                byte[] sendMsg = new byte[send.Length + 1];

                //整体拷贝数组
                Array.Copy(send, 0, sendMsg, 1, send.Length);

                //给首字节赋值

                sendMsg[0] = (byte)MessageType.UTF8;

                foreach (var item in this.lst_Online.SelectedItems)
                {
                    //获取Socket对象
                    string client = item.ToString();

                    CurrentClientlist[client]?.Send(sendMsg);
                }

                this.txt_Send.Clear();
            }
            else
            {
                MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
            }
        }

        #endregion
复制代码
复制代码
       #region 发送文件
        private void btn_SendFile_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(this.txt_File.Text))
            {
                MessageBox.Show("请先选择你要发送的文件路径", "发送文件");
                return;
            }
            else
            {
                if (this.lst_Online.SelectedItems.Count > 0)
                {
                    //分两次发送:第一次发送文件名,第二次发送文件内容
                    using (FileStream fs = new FileStream(this.txt_File.Text, FileMode.Open))
                    {
                        //第一次发送文件名称

                        //获取文件名称
                        string filename = Path.GetFileName(this.txt_File.Text);
                        //获取后缀名
                        string fileExtension = Path.GetExtension(this.txt_File.Text);

                        string strMsg = "发送文件:" + filename + "." + fileExtension;

                        byte[] send1 = Encoding.UTF8.GetBytes(strMsg);

                        byte[] send1Msg = new byte[send1.Length + 1];

                        Array.Copy(send1, 0, send1Msg, 1, send1.Length);

                        send1Msg[0] = (byte)MessageType.UTF8;

                        foreach (var item in this.lst_Online.SelectedItems)
                        {
                            //获取Socket对象
                            string client = item.ToString();

                            CurrentClientlist[client]?.Send(send1Msg);
                        }


                        //第二次发送文件内容

                        byte[] send2 = new byte[1024 * 1024 * 10];

                        //有效长度
                        int length = fs.Read(send2, 0, send2.Length);

                        byte[] send2Msg = new byte[length + 1];

                        Array.Copy(send2, 0, send2Msg, 1, length);

                        send2Msg[0] = (byte)MessageType.File;

                        foreach (var item in this.lst_Online.SelectedItems)
                        {
                            //获取Socket对象
                            string client = item.ToString();

                            CurrentClientlist[client]?.Send(send2Msg);
                        }

                        this.txt_File.Clear();

                        AddLog(0, strMsg);
                    }
                }
                else
                {
                    MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
                }
            }
        }

        #endregion
复制代码
复制代码
        #region 选择文件
        private void btn_SelectFile_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            //设置默认的路径
            ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                this.txt_File.Text = ofd.FileName;

                AddLog(0, "选择文件:" + this.txt_File.Text);
            }
        }

        #endregion
复制代码
复制代码
  #region 发送JSON
        private void btn_SendJSON_Click(object sender, EventArgs e)
        {
            if (this.lst_Online.SelectedItems.Count > 0)
            {
                //创建集合
                List<Student> stuList = new List<Student>()
            {
                new Student(){ StudentID=10001,StudentName="小明",ClassName="软件一班"},
                new Student(){ StudentID=10002,StudentName="小红",ClassName="软件二班"},
                new Student(){ StudentID=10003,StudentName="小花",ClassName="软件三班"},
            };

                //将对象转换为JSON字符串
                string str = JSONHelper.EntityToJSON(stuList);

                byte[] send = Encoding.Default.GetBytes(str);

                byte[] sendMsg = new byte[send.Length + 1];

                Array.Copy(send, 0, sendMsg, 1, send.Length);

                sendMsg[0] = (byte)MessageType.JSON;

                foreach (var item in this.lst_Online.SelectedItems)
                {
                    //获取Socket对象
                    string client = item.ToString();

                    CurrentClientlist[client]?.Send(sendMsg);
                }

            }
            else
            {
                MessageBox.Show("请选择你要发送的客户端对象!", "发送消息");
            }
        }

        #endregion
复制代码

JsonHelper类

复制代码
    public class JSONHelper
    {
        /// <summary>
        /// 实体对象转换成JSON字符串
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <returns></returns>
        public static string EntityToJSON<T>(T x)
        {
            string result = string.Empty;

            try
            {
                result = JsonConvert.SerializeObject(x);
            }
            catch (Exception)
            {
                result = string.Empty;
            }
            return result;

        }

        /// <summary>
        /// JSON字符串转换成实体类
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="json"></param>
        /// <returns></returns>
        public static T JSONToEntity<T>(string json)
        {
            T t = default(T);
            try
            {
                t = (T)JsonConvert.DeserializeObject(json, typeof(T));
            }
            catch (Exception)
            {
                t = default(T);
            }

            return t;              
        }

    }
复制代码

客户端程序编写步骤:

第一步,调用socket()函数创建一个用于通信的套接字。

第二步,通过设置套接字地址结构,说明客户端与之通信的服务器的Ip地址和端口号。

第三步,调用connect()函数来建立与服务器的连接。

第四步,调用读写函数发送或者接收数据。

第五步,终止连接。

 界面搭建:

 

 连接服务器,同时客户端要判断服务器是否断开连接

复制代码
       #region 连接服务器
        private void btn_Connect_Click(object sender, EventArgs e)
        {
            AddLog(0, "与服务器连接中");

            // 第一步:调用socket()函数创建一个用于通信的套接字。
            socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 第二步:通过设置套接字地址结构,说明客户端与之通信的服务器的IP地址和端口号。
            IPEndPoint ipe = new IPEndPoint(IPAddress.Parse(this.txt_IP.Text.Trim()), int.Parse(this.txt_Port.Text.Trim()));

            // 第三步:调用connect()函数来建立与服务器的连接。

            try
            {
                socketClient.Connect(ipe);
            }
            catch (Exception ex)
            {
                AddLog(2, "连接服务器失败:" + ex.Message);
                return;
            }

            //创建一个监听来自服务器消息的线程

            Task.Run(new Action(() =>
            {
                CheckReceiveMsg();
            }));

            AddLog(0, "成功连接至服务器");

            this.btn_Connect.Enabled = false;
        }

        #endregion
复制代码
复制代码
       #region 多线程接收来自服务器的数据

        private void CheckReceiveMsg()
        {
            while (true)
            {
                // 创建一个缓冲区

                byte[] buffer = new byte[1024 * 1024 * 10];

                int length = -1;

                //  第四步:调用读写函数发送或者接收数据。
                try
                {
                    length = socketClient.Receive(buffer);
                }
                catch (Exception)
                {
                    // 服务器断线,可以添加服务器断线的日志
                    break;
                }

                if (length > 0)
                {
                    string msg = string.Empty;

                    MessageType type = (MessageType)buffer[0];

                    switch (type)
                    {
                        case MessageType.ASCII:
                           //来自服务器的消息内容
                            msg = Encoding.ASCII.GetString(buffer, 1, length - 1);

                            AddLog(0,  "服务器:" + msg);

                            break;
                        case MessageType.UTF8:

                            msg = Encoding.UTF8.GetString(buffer, 1, length - 1);

                            AddLog(0,  "服务器:" + msg);

                            break;
                        case MessageType.Hex:

                            msg = HexGetString(buffer, 1, length - 1);

                            AddLog(0,  "服务器:" + msg);

                            break;
                        case MessageType.File:

                            Invoke(new Action(() =>
                            {
                                SaveFileDialog sfd = new SaveFileDialog();

                                sfd.Filter = "txt files(*.txt)|*.txt|xls files(*.xls)|*.xls|xlsx files(*.xlsx)|*.xlsx|All files(*.*)|*.*";

                                if (sfd.ShowDialog() == DialogResult.OK)
                                {
                                    string fileSavePath = sfd.FileName;

                                    using (FileStream fs = new FileStream(fileSavePath, FileMode.Create))
                                    {
                                        fs.Write(buffer, 1, length - 1);
                                    }

                                    AddLog(0, "文件成功保存至" + fileSavePath);
                                }
                            }));

                            break;
                        case MessageType.JSON:

                            Invoke(new Action(() =>
                            {
                                string res = Encoding.Default.GetString(buffer, 1, length);

                                List<Student> StuList = JSONHelper.JSONToEntity<List<Student>>(res);

                                new FrmJSON(StuList).Show();

                                AddLog(0, "接收JSON数据:" + res);

                            }));

                            break;
                        default:
                            break;
                    }

                }
               
                else
                {
                    // 如果length<=0,说明服务器断线了,可以添加服务器断线的日志
                    break;
                }
            }
        }

        #endregion
复制代码

客服端发送消息给服务器,这里就只发送ASCLL,其余的数据格式和服务器端类似,有兴趣的可以下载源码查看。

复制代码
       #region 发送ASCII
        private void btn_SendASCII_Click(object sender, EventArgs e)
        {
            AddLog(0, "发送内容:" + this.txt_Send.Text.Trim());

            byte[] send = Encoding.ASCII.GetBytes(this.txt_Send.Text.Trim());

            //创建最终发送的数组
            byte[] sendMsg = new byte[send.Length + 1];

            //整体拷贝数组
            Array.Copy(send, 0, sendMsg, 1, send.Length);

            //给首字节赋值

            sendMsg[0] = (byte)MessageType.ASCII;

            socketClient?.Send(sendMsg);

            this.txt_Send.Clear();
        }

        #endregion
复制代码

客户端下线时,需要断开与服务器的连接

       #region 窗体关闭
        private void FrmTCPClient_FormClosing(object sender, FormClosingEventArgs e)
        {
            //客户端下线的时候,关闭客户端套接字
            socketClient?.Close();
        }

        #endregion

 断开服务器

      #region 断开服务器

        private void btn_DisConn_Click(object sender, EventArgs e)
        {
            socketClient?.Close();
        }

        #endregion

 

源码下载链接:https://pan.baidu.com/s/1wIXLC0AlGyM1VoYeYOKMpg

提取码:8qjn

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

,把send数组,从索引为1开始,拷贝send.Length个长度的字节数组给sendMsg
posted @   WellMandala  阅读(1411)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示