C#通讯转发万向节(C#串口、TCP客户端、服务端数据转发监控工具)

Qt版字节转发高性能上线

传送门:BM 字节转发(C++方向)

万向节不是万圣节

在硬件调试中,需要连接各种各样的端口(还有串口,以下统称端口)交互数据,由于上位机软件的通讯端口类型限制,不能直接用现有电脑存在的或者虚拟的端口连接使用。同时,通讯的数据不能直观的展现成需要的格式日志。所以,参考测控软件,开发了一个类似于汽车万向节的电脑程序。

一、根本需求

需要干的实事

  1. 端口数据转发 ,串口转tcp服务端、tcp客户端转串口、串口转串口等排列组合;
  2. 用起来简单明了,多一个按钮想删 ,少一个文字看不懂,不需要使用说明;
  3. 坐标郑州,挑战深圳速度;

二、概要设计

2.1 要想快,用VS

2.2 通讯底层:至少三种通讯类和公用接口类

串口类:

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace tempJH1101
{
    internal class SeriaPortCommunication : MyriadInterface
    {
        string dev = "";
        public SerialPort MySerialProt;
        string PortName = "";
        int BaudRate=0;
        public bool OnLine = false;

        /// <summary>
        /// 存字节数
        /// </summary>
        public byte[] Buffer = new byte[1024];
        /// <summary>
        /// 接受数据线程
        /// </summary>
        public Thread ReceiveThread = null;

        public delegate void MessageDelegate(byte[] data);
        public event MessageDelegate ReciveMessage = null;
        public event EventHandler<byte[]> ShowData;

        public SeriaPortCommunication(string name, int BaudRate)
        {
            PortName = name;
            this.BaudRate = BaudRate;
            MySerialProt = new SerialPort(PortName);
            MySerialProt.BaudRate = BaudRate;
            MySerialProt.DataBits = 8;
            MySerialProt.StopBits = StopBits.One;
            MySerialProt.Parity = Parity.Even;
            MySerialProt.DataReceived += MySerialProt_DataReceived;
        }

        public event EventHandler<byte[]> ExportData;

        public void OpenCom()
        {
            try
            {
                MySerialProt.Open();
            }
            catch (Exception ex)
            {
                //MessageBox.Show($"{PortName} 串口打开失败");
                OnLine = false;
                throw ex;
            }
            OnLine = true;

        }

        private void MySerialProt_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            Thread.Sleep(200);
            if (!MySerialProt.IsOpen) return;

            int len = MySerialProt.BytesToRead;
            byte[] b = new byte[len];
            MySerialProt.Read(b, 0, len);
            //if (ReciveMessage != null)
            //    ReciveMessage(b);
            if (ExportData != null)
                ExportData(this,b);
        }

        public void Send(byte[] dataBuffer, int tryCount = 3)
        {
            //while ((!MySerialProt.IsOpen) &&
            //    tryCount > 0)
            //{
            //    tryCount--;

            //}
            if (MySerialProt.IsOpen)
            {
                MySerialProt.Write(dataBuffer, 0, dataBuffer.Length);
            }
        }

        public void CloseCom()
        {
            MySerialProt.Close();
        }



        public void Open()
        {
            OpenCom();
        }

        public void Close()
        {
            CloseCom();
        }

        public void SendData(byte[] data)
        {
            Send(data);
        }

        string MyriadInterface.GetInfo()
        {
            return "串口号:"+PortName+":"+ BaudRate;
        }

        public void SetName(string str)
        {
            dev = str;
        }

        public string GetName()
        {
            return dev;
        }
    }
}

TCP客户端:

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

namespace tempJH1101
{
    public class Client : MyriadInterface
    {
        string dev = "";
        string IP;
        int Port;
        TcpClient TcpClient = new TcpClient();
        Thread ClientReceiveThread = null;
        bool IsTrue = true;

        public event EventHandler<byte[]> ExportData;

        public Client(string ip, int port)
        {
            IP = ip;
            Port = port;
        }

        private void MeterPortDataReceived()
        {
            IsTrue = true;
            while (IsTrue)
            {
                int length = 0;
                byte[] buffer = new byte[4 * 1024];
                try
                {
                    length = TcpClient.Client.Receive(buffer, 0, TcpClient.Client.Available, SocketFlags.None);

                    if (length < 1) continue;

                    byte[] b = buffer.Take(length).ToArray();
                    DealData(b);


                }
                catch (Exception)
                {
                    IsTrue = false;
                }
                Thread.Sleep(200);
            }
        }

        public void Open()
        {
            TcpClient = new TcpClient();
            TcpClient.Connect(IP, Port);
            ClientReceiveThread = new Thread(MeterPortDataReceived);
            ClientReceiveThread.Start();
        }

        public void Close()
        {
            IsTrue = false;
            TcpClient.Close();
            if (ClientReceiveThread != null)
                ClientReceiveThread.Abort();

        }

        public void SendBuf(byte[] b)
        {
            TcpClient.Client.Send(b);
        }

        void DealData(byte[] b)
        {
            if (ExportData != null)
                ExportData(this, b);
        }

        public string GetInfo()
        {
            return "客户端:" + IP + ":" + Port;
        }

        public string GetName()
        {
            return dev;
        }

        public void SetName(string str)
        {
            dev = str;
        }

        public void SendData(byte[] data)
        {
            SendBuf(data);
        }
    }
}

TCP服务端(单客户段端模式,有新的就断开旧的,一次就只连一个):

namespace tempJH1101
{
    using System;
    using System.Windows.Forms;
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Sockets;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;

    public class Server:MyriadInterface
    {
        string dev;

        public delegate void DelegateShowData(int rs, byte[] data);
        public event DelegateShowData ShowData;
        public event EventHandler<byte[]> ExportData;

        public bool IsTrue = true;

        string ServerIP;   // 服务器端ip
        int Port;                // 服务器端口

        TcpListener netListener = null;
        Thread listenerThread = null;   //监听线程,存在多个客户端,所以使用线程监听
        Socket Client = null;
        Thread ReceiveThread = null;//接收处理线程,只允许一个客户端连接

        public int MeterNo = -1;

        public Server(string ipString, int port)
        {
            ServerIP = ipString;
            Port = port;
        }

        public void StartListen()
        {
            stop();

            IsTrue = true;
            listenerThread = new Thread(new ThreadStart(NetWaiter));
            listenerThread.Start();

        }

        private void NetWaiter()
        {
            try
            {
                netListener = new TcpListener(IPAddress.Parse(ServerIP), Port);
                netListener.Start();
            }
            catch (Exception error)
            {
                MessageBox.Show(error.Message + " IP:" + ServerIP);
                return;
            }

            while (IsTrue)	//循环监听
            {
                if (!netListener.Pending())
                {
                    Thread.Sleep(1000);
                    continue;
                }
                Socket soc = netListener.AcceptSocket();
                if (ReceiveThread != null)
                {
                    IsTrue = false;
                    ReceiveThread.Abort();
                    Thread.Sleep(1000);
                }
                if (Client != null)
                {
                    try
                    {
                        Client.Shutdown(SocketShutdown.Both);
                    }
                    catch (Exception ee)
                    {

                    }
                    finally
                    {
                        Client.Close();
                    }
                    
                    
                }
                Client = soc;
                ReceiveThread = new Thread(new ParameterizedThreadStart(receiveData));// 在新的线程中接收客户端信息
                ReceiveThread.Start(Client);
                Thread.Sleep(1000);                            // 延时1秒后,接收连接请求
            }
        }

        private void receiveData(object obj)
        {
            IsTrue = true;
            while (IsTrue)
            {
                try
                {
                    byte[] buf = Receive(Client);
                    if (buf != null && buf.Length > 1)
                    {
                        if (ExportData != null)
                            ExportData(this, buf);
                        DealReadData(buf);

                        Thread.Sleep(200);      // 延时0.2秒后再接收客户端发送的消息
                    }
                }
                catch (Exception exception)
                {
                    Client.Shutdown(SocketShutdown.Both);
                    Client.Close();
                    return;
                }

                Thread.Sleep(200);      // 延时0.2秒后再接收客户端发送的消息
            }
        }

        private void DealReadData(byte[] buf)
        {
            try
            {
                //byte[] b = brain.ProcessStdCmd(MeterNo, buf);
                //Send(b);
            }
            catch (Exception ee)
            {

            }

        }

        private byte[] Receive(Socket socket)
        {
            byte[] bytes = null;
            int len = socket.Available;
            if (len > 0)
            {
                bytes = new byte[len];
                int receiveNumber = socket.Receive(bytes);
            }

            return bytes;
        }

        /// <summary>
        /// 只发送最后的连接
        /// </summary>
        /// <param name="data"></param>
        public void Send(byte[] data)
        {
            try
            {
                if (ShowData != null)
                    ShowData(1, data);
                Client.Send(data);
            }
            catch (Exception ee)
            {

            }

        }

        /// <summary>
        /// 停止服务
        /// </summary>
        public void stop()
        {
            try
            {
                IsTrue = false;

                if (netListener != null)
                {
                    netListener.Stop();
                    netListener = null;
                }

                if (listenerThread != null)
                {
                    listenerThread.Abort();
                }

                if (ReceiveThread != null)
                {
                    ReceiveThread.Abort();
                    Thread.Sleep(1000);
                }

                if (Client != null)
                {
                    Client.Shutdown(SocketShutdown.Both);
                    Client.Close();
                }
            }
            catch (Exception ee)
            {
                //MessageBox.Show(ee.Message);
            }


        }

        public string GetInfo()
        {
            return "服务端:" + ServerIP + ":" + Port;
        }

        public string GetName()
        {
            return dev;
        }

        public void SetName(string str)
        {
            dev = str;
        }

        public void Open()
        {
            StartListen();
        }

        public void Close()
        {
            stop();
        }

        public void SendData(byte[] data)
        {
            Send(data);
        }
    }
}

方便操作的公用接口类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace tempJH1101
{
    public interface MyriadInterface
    {
        string GetInfo();

        string GetName();

        void SetName(string str);

        void Open();

        void Close();

        void SendData(byte[] data);

        event EventHandler<byte[]> ExportData;



    }
}

2.3 转发的实质是两点一线,方便画线,一点多线,线和点要分离

数据的来是接收,去是发送。触发点是接收,发送不发送看有没有出去的线,都发送给谁那就找另一端。所以每个端口的接收事件都只注册一个公用的处理方法,简单,好调试。端口整整齐齐的放在数组里,我想发给谁,就直接叫索引,有一条线,我就发一次,有两条线我就发两次,反正for的是表格,循环的速度肯定比你删除的速度快,报不了错。

中枢大脑类:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace tempJH1101
{
    public class Brain
    {
        FormMain main;
        string file_path;
        public Config config;

        public event EventHandler<ShowTime> ShowBuf;

        public Brain(FormMain a)
        {
            main = a;
            file_path = Application.StartupPath + "\\" + Path.GetFileNameWithoutExtension(Application.ExecutablePath) + ".ini";

            config = new Config();
            if (File.Exists(file_path))
            {
                try
                {
                    config = (Config)BinaryToObject(file_path);
                }
                catch (Exception)
                {

                }
            }
            else
            {
                SaveConfig();
            }


        }

        public void DealData(string dev, byte[] b)
        {

            for (int i = 0; i < main.dataGridView1.Rows.Count; i++)
            {
                bool bc = Convert.ToBoolean(main.dataGridView1.Rows[i].Cells[4].Value);
                if (!bc) continue;

                int fasongid = FormMain.name.IndexOf(dev);

                string now = main.dataGridView1.Rows[i].Cells[2].Value.ToString();
                if (now == dev)
                {
                    int jieshouid = FormMain.name.IndexOf(main.dataGridView1.Rows[i].Cells[3].Value.ToString());
                    FormMain.list[jieshouid].SendData(b);

                    bool ss = Convert.ToBoolean(main.dataGridView1.Rows[i].Cells[0].Value);
                    if (!ss) continue;

                    if (ShowBuf != null)
                    {
                        ShowTime time = new ShowTime();
                        time.line = main.dataGridView1.Rows[i].Cells[1].Value.ToString(); ;
                        time.rs = 1;
                        time.buf = b;
                        ShowBuf(this, time);
                    }

                }

                now = main.dataGridView1.Rows[i].Cells[3].Value.ToString();
                if (now == dev)
                {
                    int jieshouid = FormMain.name.IndexOf(main.dataGridView1.Rows[i].Cells[2].Value.ToString());
                    FormMain.list[jieshouid].SendData(b);

                    bool ss = Convert.ToBoolean(main.dataGridView1.Rows[i].Cells[0].Value);
                    if (!ss) continue;

                    if (ShowBuf != null)
                    {
                        ShowTime time = new ShowTime();
                        time.line = main.dataGridView1.Rows[i].Cells[1].Value.ToString(); ;
                        time.rs = 0;
                        time.buf = b;
                        ShowBuf(this, time);
                    }

                }
            }
        }

        public void SaveConfig()
        {
            ObjectToBinary(file_path, config);
        }



        // 传入需要序列化的对象,写成泛型的可以对一切对象通用
        void ObjectToBinary<Config>(string path, Config u)
        {
            FileStream fs = new FileStream(path, FileMode.Create);
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(fs, u);
            fs.Close();
        }

        // 传入已经序列的文件,保存的硬盘地址
        object BinaryToObject(string path)
        {
            FileStream fs = new FileStream(path, FileMode.Open);
            BinaryFormatter bf = new BinaryFormatter();
            object r = bf.Deserialize(fs);
            fs.Close();
            return r;
        }
    }

    public struct ShowTime
    {
        public string line;
        public int rs;
        public byte[] buf;
    }

    [Serializable]
    public class Config
    {
        public bool IsHEX = true;

        public Dictionary<string, string> socket = new Dictionary<string, string>();

        public Dictionary<string, string> link = new Dictionary<string, string>();
    }
}

2.4 端口开关用复选框,转发连线用表格,数据展示用富文本,添加删除用右键,弹出用通知图标

主界面类(没有大骆驼小骆驼,一共就那几个骆驼):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace tempJH1101
{
    public partial class FormMain : Form
    {
        public Brain brain;

        public static List<MyriadInterface> list = new List<MyriadInterface>();
        public static List<string> name = new List<string>();

        public delegate void MessageDelegate(ShowTime e);
        public MessageDelegate showreal = null;

        /// <summary>
        /// 异步委托
        /// </summary>
        public IAsyncResult ProtDataReceivedResult = null;

        public FormMain()
        {
            InitializeComponent();

            brain = new Brain(this);
            brain.ShowBuf += Brain_ShowBuf1;
            showreal = new MessageDelegate(Brain_ShowBuf);

            treeView1.Nodes.Clear();
        }

        private void Brain_ShowBuf1(object sender, ShowTime e)
        {
            ProtDataReceivedResult = this.BeginInvoke(showreal, new object[] { e });
        }

        private void Brain_ShowBuf(ShowTime e)
        {

            string hex = GetHexString(e.buf);
            if (!cbHEX.Checked)
            {
                hex = Encoding.ASCII.GetString(e.buf);
            }
            hex = hex.Replace("\r", "").Replace("\n", "");
            if (hex.Length < 1) return;
            string str = DateTime.Now.ToString("HH:mm:ss.fff");
            if (e.rs == 1)
            {
                str = $"{str}\t{e.line}\t发送\t{hex}";
            }
            else
            {
                str = $"{str}\t{e.line}\t接收\t{hex}";
            }

            str = str + "\r\n";
            int intStart = richTextBox1.TextLength;
            //fctbReceive.Focus();
            richTextBox1.AppendText(str);
            richTextBox1.Select(intStart, intStart + str.Length);
            richTextBox1.SelectionColor = Color.LightGreen;
            if (e.rs == 1) richTextBox1.SelectionColor = Color.Red;
            richTextBox1.ScrollToCaret();
        }

        private string GetHexString(byte[] data)
        {

            int len = data.Length;
            string sb = "";
            for (int i = 0; i < len; i++)
            {
                sb += (data[i].ToString("X2") + " ");
            }
            return sb;
        }

        private void btnAdd_Click(object sender, EventArgs e)
        {
            MyriadInterface port = null;

            list.Add(port);

            FormNewPort f = new FormNewPort(this);

            if (f.ShowDialog() == DialogResult.OK)
            {
                list[list.Count - 1].ExportData += Port_ExportData;

                treeView1.Nodes.Add(name[name.Count - 1]);
                treeView1.Nodes[name.Count - 1].ToolTipText = list[name.Count - 1].GetInfo();
                list[name.Count - 1].SetName(name[name.Count - 1]);
            }
            else
            {
                list.RemoveAt(list.Count - 1);
            }

            //treeView1.Nodes.Clear();
            //for (int i = 0; i < name.Count; i++)
            //{
            //    treeView1.Nodes.Add(name[i]);
            //    treeView1.Nodes[i].ToolTipText = list[i].GetInfo();
            //    list[i].SetName(name[i]);
            //}

            


        }

        private void Port_ExportData(object sender, byte[] e)
        {
            string dev = ((MyriadInterface)sender).GetName();
            brain.DealData(dev, e);
        }

        private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
        {
            int id = e.Node.Index;
            if (e.Node.Checked)
            {
                list[id].Open();
            }
            else
            {
                list[id].Close();
            }
        }

        private void btnLink_Click(object sender, EventArgs e)
        {
            FormNewLine f = new FormNewLine(this);
            f.ShowDialog();
        }

        private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex == 4 && e.RowIndex > -1)
            {
                //if (Convert.ToBoolean(dataGridView1.Rows[e.RowIndex].Cells[4]))
                //{

                //}
                //else
                //{

                //}
            }
        }

        private void btnClear_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            richTextBox1.Clear();
        }

        private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (MessageBox.Show("确认退出万向节工具?", "警告", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
            {
                e.Cancel = true;
                return;
            }

            for (int i = 0; i < list.Count; i++)
            {
                list[i].Close();
            }

            brain.config.IsHEX = cbHEX.Checked;
            brain.config.socket = new Dictionary<string, string>();
            for (int i = 0; i < list.Count; i++)
            {
                MyriadInterface myriad = list[i];
                string name = myriad.GetName();
                string link = myriad.GetInfo();
                brain.config.socket.Add(name, link);
            }

            brain.config.link = new Dictionary<string, string>();
            for (int i = 0; i < dataGridView1.Rows.Count; i++)
            {
                string a1 = dataGridView1.Rows[i].Cells[1].Value.ToString();
                string a2 = dataGridView1.Rows[i].Cells[2].Value.ToString();
                //string a20 = dataGridView1.Rows[i].Cells[2].Tag.ToString();
                string a3 = dataGridView1.Rows[i].Cells[3].Value.ToString();
                //string a30 = dataGridView1.Rows[i].Cells[3].Tag.ToString();

                brain.config.link.Add(a1, a2 + "\r" + a3);

            }

            brain.SaveConfig();


            notifyIcon1.Dispose();
        }
        private void Form1_SizeChanged(object sender, EventArgs e)
        {
            if (WindowState == FormWindowState.Minimized)
            {
                ShowInTaskbar = false;
            }
            else
            {
                ShowInTaskbar = true;

            }
        }

        private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            WindowState = FormWindowState.Normal;
            Activate();
        }

        private void cbTop_CheckedChanged(object sender, EventArgs e)
        {
            TopMost = cbTop.Checked;
        }

        private void btnBreak_Click(object sender, EventArgs e)
        {
            DataGridViewRow row = dataGridView1.CurrentRow;
            string name = row.Cells[1].Value.ToString();
            if (MessageBox.Show($"确认连线\t“{name}”\t删除吗?", "警告", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;

            row.Cells[4].Value = false;
            row.Cells[0].Value = false;

            dataGridView1.Rows.Remove(row);
        }

        private void btnDelete_Click(object sender, EventArgs e)
        {
            TreeNode node = treeView1.SelectedNode;
            if (node == null) return;

            string name1 = node.Text;

            if (MessageBox.Show($"确认端口\t“{name1}”\t删除吗?", "警告", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;

            int id = name.IndexOf(name1);

            list[id].Close();

            for (int i = dataGridView1.Rows.Count - 1; i >= 0; i--)
            {
                DataGridViewRow row = dataGridView1.Rows[i];

                string rname = row.Cells[2].Value.ToString();
                if (rname == name1)
                {
                    row.Cells[4].Value = false;
                    row.Cells[0].Value = false;

                    dataGridView1.Rows.Remove(row);
                    continue;
                }

                rname = row.Cells[3].Value.ToString();
                if (rname == name1)
                {
                    row.Cells[4].Value = false;
                    row.Cells[0].Value = false;

                    dataGridView1.Rows.Remove(row);
                }

            }

            treeView1.Nodes.Remove(node);
            name.Remove(name1);
            list.RemoveAt(id);

        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            if (brain.config.IsHEX)
            {
                cbHEX.Checked = true;
            }
            else
            {
                cbASCII.Checked = true;
            }

            for (int i = 0; i < brain.config.socket.Count; i++)
            {
                string name = brain.config.socket.ElementAt(i).Key;
                string str = brain.config.socket.ElementAt(i).Value;

                string tyoe1 = str.Split(':')[0];
                string a2 = str.Split(':')[1];

                MyriadInterface port = null;
                FormMain.name.Add(name);
                if (tyoe1 == "串口号")
                {
                    port = new SeriaPortCommunication(a2.Split(':')[0], int.Parse(a2.Split(':')[1]));
                }
                else if (tyoe1 == "服务端")
                {
                    port = new Server(a2.Split(':')[0], int.Parse(a2.Split(':')[1]));
                }
                else if (tyoe1 == "客户端")
                {
                    port = new Client(a2.Split(':')[0], int.Parse(a2.Split(':')[1]));
                }
                port.SetName(name);
                port.ExportData += Port_ExportData;
                list.Add(port);
                treeView1.Nodes.Add(name);
                treeView1.Nodes[treeView1.Nodes.Count - 1].ToolTipText = port.GetInfo();

            }

            for (int i = 0; i < brain.config.link.Count; i++)
            {
                string name = brain.config.link.ElementAt(i).Key;
                string str = brain.config.link.ElementAt(i).Value;
                string[] arr = str.Split('\r');
                dataGridView1.Rows.Add(false, name, arr[0], arr[1], false);

            }



        }
    }
}

2.5 下次打开不用重新添加,ini、xml和json配置都太烦,直接序列化。单独一个exe,首次使用自动生成序列化文件,丢了也不怕。

怕丢放中枢大脑类文件里了

三、详细设计

上面就是问答题答案,全部代码见下期,关键是下期谁还看啊,直接上干货:

描述

四、万向节下载

https://download.csdn.net/download/u012619677/87755212

五、谢谢观看,我用一天(不带处理bug),深圳用几天?

代码主:gshuaijun@163.com

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值