远程控制同网段计算机USB设备是否可用

最近公司有这样的一个需求,即老板要在他的电脑上控制同网段内的其它电脑上的USB存储设备是否可用,在一定程度上防止资料外泄。看到这里也许有人会说了,不让用U盘,直接发QQ文件或者邮件不就行了吗?哈哈,这些电脑都没有连接外网!!!好了,废话不多说。

刚拿到这个需求的时候,第一反应,是到网上找看有没有现成的东西大笑,万能的度娘还真找到一个,哇~收费的抓狂!只能自己开发了可怜

先分析需求,通过查资料,比较容易实现的有两种方式,一、修改注册表键值,二、删除USB存储设备驱动,并且清除USB使用记录。知道了怎么禁用USB存储设备,就开始着手做了,共分下边几步:

1、查找出同网段在线计算机

2、服务端发送相应指令给客户端

3、客户端执行操作并反馈状态

大的来说是这样的实现原理,但是中间涉及到几个问题

1、通讯问题,之前没有接触过socket,这是第一次做。最开始想的是只有一个服务端,其余都是客户端。服务端即老板的计算机,客户端即员工的计算机,当员工电脑开机的时候,主动请求老板的电脑,并保持长连接。这样做貌似很复杂的样子,没办法,只能怪学艺不精了委屈。所以只好换了一种方式,一个客户端,对应N个服务端。客户端即老板的计算机,服务端即员工的计算机,这样,只要员工的计算机在线并且能ping通,老板需要设置的时候,直接连接即可。为了顺应思路,以下所说的客户端均指的是员工电脑(真正的服务端),服务端指的是老板的电脑(真正的客户端)。

2、客户端上的软件表现形式。最开始想到的是放到桌面右下角的托盘区域,但是想想觉得不妥,这样做比较直观,需要防止员工退出软件。后来想着试试做成windows服务,因为不知道做成服务以后,socket还能不能用,只能先试试,结果发现木有问题大笑

3、怎么修改注册表键值,之前没有做过,后来问的度娘

4、最头疼的问题,即清理注册表USB存储设备使用记录的问题。win7和XP系统的不同之处在于,权限系统的变更导致的对注册表敏感数据的操作没有权限。在百度上搜索了好多文章,最后找到一个方法,即psexec.exe,可以以system权限运行软件,操作敏感数据。psexec.exe是命令行启动的,附带了一堆参数,具体参数都是什么含义请自行百度,我用到的就是 psexec.exe -i -d -s xx.exe这个命令,这就引出下一个问题了

5、c# winform 怎么执行命令行代码,我去,还得找万能的度娘

解决了以上问题,只完成了最基本的需求,即可以以注册表的方式、驱动的方式禁用、启用USB存储设备。需要完善的地方:

1、端口等的配置,我做的时候端口是写死的

2、控制端权限,现在我没做权限,打开直接运行,谁都可以使用

3、不能查询受控端计算机当前状态是否禁用,这个很好实现,有别的项目了,懒得做了

4、怎么防止别人修改注册表或者还原驱动程序,现在能想到的是定时查询,如果被还原了再次设置

5、控制端没有保存当前设置以供受控端查询比对,这个实现也不难

6、受控端退出服务以后失效,再做一个服务,用以保护主服务,当主服务被停止的时候,自动启动主服务

由于新项目,待以后有时间了再做完善吧

下面放出控制端截图:


1、查找计算机:

网上的方式多种多样,我用了一个最快的方式,但不是最保险的,即ping的方式,速度很快,只列出在线计算机的IP地址。缺点是,如果对方计算机屏蔽了ping就废了,不过好在都是内网的,可控。

实现代码如下:

<span style="white-space:pre">	</span>private void EnumComputers()
        {
            listBox1.Items.Clear();

            try
            {
                string startIP = textBox1.Text.Trim();
                string endIP = textBox2.Text.Trim();

                if(!checkIP(startIP) || !checkIP(endIP))
                {
                    MessageBox.Show("开始IP或者结束IP格式错误");
                    return;
                }

                string[] startIPArr = startIP.Split('.');
                string[] endIPArr = endIP.Split('.');

                int startNum = Convert.ToInt32(startIPArr[3]);
                int endNum = Convert.ToInt32(endIPArr[3]);

                for (int i = startNum; i <= endNum; i++)
                {
                    Ping myPing;
                    myPing = new Ping();
                    string pingIP = startIPArr[0] + "." + startIPArr[1] + "." + startIPArr[2] + "." + i;
                    myPing.SendAsync(pingIP, 1000, null);
                    //PingReply pr = myPing.Send(pingIP, 1000, null);
                    myPing.PingCompleted += new PingCompletedEventHandler(_myPing_PingCompleted);
                    //if (pr.Status==IPStatus.Success)
                    //    listBox1.Items.Add(pingIP);
                }
            }
            catch
            {
            }
        }

        private void _myPing_PingCompleted(object sender, PingCompletedEventArgs e)
        {
            if (e.Reply.Status == IPStatus.Success)
            {
                if (!listBox1.Items.Contains(e.Reply.Address.ToString()))
                    listBox1.Items.Add(e.Reply.Address.ToString());
            }

        }
正则匹配IP地址的

<span style="white-space:pre">	</span>private bool checkIP(string str)
        {
            string pattern = @"\d+\.\d+\.\d+\.\d+";
            Match m = Regex.Match(textBox1.Text, pattern);// 匹配正则表达式

            if (m.Success)
                return true;
            else
                return false;
        }

下边是控制端的核心了,socket通信部分,不要看我的事件名称有汉字,这个是c# winform 的contextMenuStrip自动生成的,懒得改,不要吐槽啊!

<span style="white-space:pre">	</span>private void 启用USBToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (listBox1.SelectedItem != null && listBox1.SelectedItems.Count > 0)
            {
                for (int i = 0; i < listBox1.SelectedItems.Count; i++)
                {
                    if (!checkValue(textBox3.Text.Trim()))
                    {
                        MessageBox.Show("端口号只能为正整数");
                        return;
                    }
                    int myProt = Convert.ToInt32(textBox3.Text.Trim());
                    //设定服务器IP地址  
                    string setIP = listBox1.SelectedItems[i].ToString();
                    IPAddress ip = IPAddress.Parse(setIP);
                    Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    try
                    {
                        clientSocket.Connect(new IPEndPoint(ip, myProt)); //配置服务器IP与端口  
                        //Console.WriteLine("连接服务器成功");
                        richTextBox1.Text += "IP为 " + setIP + "的客户端连接成功\r\n";
                        //通过clientSocket接收数据  
                        int receiveLength = clientSocket.Receive(result);
                        //Console.WriteLine("接收服务器消息:{0}", Encoding.ASCII.GetString(result, 0, receiveLength));
                        richTextBox1.Text += "IP为 " + setIP + " 的客户端返回状态:" + Encoding.ASCII.GetString(result, 0, receiveLength) + "\r\n";

                        string sendIp = "3";
                        clientSocket.Send(Encoding.ASCII.GetBytes(sendIp));
                        //Console.WriteLine("向服务器发送消息:" + sendIp);
                        richTextBox1.Text += "向IP为 " + setIP + " 的客户端发送指令:启用USB(注册表)\r\n";
                        richTextBox1.Text += "向IP为 " + setIP + " 的客户端发送完毕" + "\r\n";
                        //Console.WriteLine("发送完毕,按回车键退出");

                        //Console.ReadLine();

                        receiveLength = clientSocket.Receive(result);
                        string msg = Encoding.ASCII.GetString(result, 0, receiveLength);
                        if (msg == "1")
                            richTextBox1.Text += "IP为 " + setIP + " 的客户端设置成功" + "\r\n\r\n";
                        else
                            richTextBox1.Text += "IP为 " + setIP + " 的客户端设置失败" + "\r\n\r\n";
                    }
                    catch (Exception err)
                    {
                        richTextBox1.Text += "IP为 " + setIP + " 的客户端连接失败," + err.Message + "\r\n";
                        //Console.WriteLine("连接服务器失败," + err.Message + ",请按回车键退出!");
                        //Console.ReadLine();
                    }
                    finally
                    {
                        if (clientSocket != null)
                            clientSocket.Close();
                    }
                }
            }
            else
            {
                MessageBox.Show("请选择远程主机");
            }
        }

控制端的核心代码就这些,具体请自己实现。界面上,用的到的控件有 textbox  listbox  richtextbox  button  以及右键菜单的 contextMenuStrip


下边说说受控端的windows 服务

不知道怎么创建windows 服务的请自行解决,这些不是本文讨论的重点,不做赘述。

首先,在OnStart里启用连接监听,这样在控制端连接的时候,会发送握手信息,并且接收控制端的指令,具体代码如下

<span style="white-space:pre">	</span>protected override void OnStart(string[] args)
        {
            int myProt = 61231;
            //服务器IP地址  
            string setIP = getIP();
            IPAddress ip = IPAddress.Parse(setIP);
            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serverSocket.Bind(new IPEndPoint(ip, myProt));  //绑定IP地址:端口  
            serverSocket.Listen(10);    //设定最多10个排队连接请求  
            Console.WriteLine("启动监听{0}成功", serverSocket.LocalEndPoint.ToString());
            //通过Clientsoket发送数据  
            Thread myThread = new Thread(ListenClientConnect);
            myThread.Start();
            Console.ReadLine();
        }

        /// <summary>  
        /// 监听客户端连接  
        /// </summary>  
        private static void ListenClientConnect()
        {
            while (true)
            {
                Socket clientSocket = serverSocket.Accept();
                clientSocket.Send(Encoding.ASCII.GetBytes("Server Say Hello"));
                Thread receiveThread = new Thread(ReceiveMessage);
                receiveThread.Start(clientSocket);
            }
        }

接收指令代码如下:

<span style="white-space:pre">	</span>/// <summary>  
        /// 接收消息  
        /// </summary>  
        /// <param name="clientSocket"></param>  
        private static void ReceiveMessage(object clientSocket)
        {
            Socket myClientSocket = (Socket)clientSocket;

            try
            {
                //通过clientSocket接收数据  
                int receiveNumber = myClientSocket.Receive(result);
                //Console.WriteLine("接收客户端{0}消息{1}", myClientSocket.RemoteEndPoint.ToString(), Encoding.ASCII.GetString(result, 0, receiveNumber));
                string clientMsg = Encoding.ASCII.GetString(result, 0, receiveNumber);
                //Console.WriteLine(clientMsg);
                //wirteLog(clientMsg);

                //获取到传递的数据
                if (clientMsg == "4")
                {
                    bool a = RegToStopUSB();
                    if (a)
                        //ListenClientConnect("1");
                        myClientSocket.Send(Encoding.ASCII.GetBytes("1"));
                    else
                        myClientSocket.Send(Encoding.ASCII.GetBytes("0"));
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                myClientSocket.Shutdown(SocketShutdown.Both);
                myClientSocket.Close();
            }
        }

然后根据接收到的指令,调用不同的函数来执行不同的操作,代码如下:

<span style="white-space:pre">	</span>/// <summary>
        /// 禁用USB
        /// </summary>
        /// <returns>状态</returns>
        private static bool RegToStopUSB()
        {
            try
            {
                RegistryKey regKey = Registry.LocalMachine;
                string keyPath = @"SYSTEM\CurrentControlSet\Services\USBSTOR";
                RegistryKey openKey = regKey.OpenSubKey(keyPath, true);
                openKey.SetValue("Start", 4);
                openKey.Close();
                return true;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

/// <summary>
        /// 启用USB
        /// </summary>
        /// <returns>状态</returns>
        private static bool RegToRunUSB()
        {
            try
            {
                RegistryKey regKey = Registry.LocalMachine; //读取注册列表HKEY_LOCAL_MACHINE  
                string keyPath = @"SYSTEM\CurrentControlSet\Services\USBSTOR"; //USB 大容量存储驱动程序  
                RegistryKey openKey = regKey.OpenSubKey(keyPath, true);
                openKey.SetValue("Start", 3); //设置键值对(3)为开启USB(4)为关闭  
                openKey.Close(); //关闭注册列表读写流  
                return true;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
<span style="white-space:pre">	</span>/// <summary>
        /// 备份驱动
        /// </summary>
        /// <returns></returns>
        private static int backFile()
        {
            try
            {
                if (!Directory.Exists("d:\\pic"))
                    Directory.CreateDirectory(@"C:\Windows\infBack\");

                if (File.Exists(@"C:\Windows\inf\usbstor.inf") && File.Exists(@"C:\Windows\inf\usbstor.PNF"))
                {
                    string fromFile = @"C:\Windows\inf\usbstor.inf";
                    string toFile = @"C:\Windows\infBack\usbstor.inf";

                    if (File.Exists(fromFile))
                        File.Copy(fromFile, toFile, true);

                    fromFile = @"C:\Windows\inf\usbstor.PNF";
                    toFile = @"C:\Windows\infBack\usbstor.PNF";

                    if (File.Exists(fromFile))
                        File.Copy(fromFile, toFile, true);

                    return 1;
                }
                else
                {
                    return -2;//驱动文件不存在
                }
            }
            catch (Exception err)
            {
                return -1;//错误
            }
        }

<span style="white-space:pre">	</span>/// <summary>
        /// 还原驱动
        /// </summary>
        /// <returns></returns>
        private static int restoreFile()
        {
            try
            {
                if (Directory.Exists(@"C:\Windows\infBack\") && File.Exists(@"C:\Windows\infBack\usbstor.inf") && File.Exists(@"C:\Windows\infBack\usbstor.PNF"))
                {
                    string fromFile = @"C:\Windows\infBack\usbstor.inf";
                    string toFile = @"C:\Windows\inf\usbstor.inf";

                    File.Copy(fromFile, toFile, true);

                    fromFile = @"C:\Windows\infBack\usbstor.PNF";
                    toFile = @"C:\Windows\inf\usbstor.PNF";

                    File.Copy(fromFile, toFile, true);

                    return 1;
                }
                else
                {
                    return -10;//备份目录不存在
                }
            }
            catch (Exception err)
            {
                return -1;
            }
        }

<span style="white-space:pre">	</span>/// <summary>
        /// 删除U盘记录
        /// </summary>
        private static bool delRegUsbLog()
        {
            

            try
            {

                RegistryKey key = Registry.LocalMachine;

                RegistryKey sunKey = key.OpenSubKey(@"SYSTEM\ControlSet001\Enum\USBSTOR", true);

                string[] sunKeyNames = sunKey.GetSubKeyNames();

                foreach (string name in sunKeyNames)
                {
                    if (name.Contains("Disk&"))
                    {
                        //删除注册表项
                        sunKey.DeleteSubKeyTree(name + @"\", false);
                    }
                }


                //============================================================//
                RegistryKey sunKey1 = key.OpenSubKey(@"SYSTEM\ControlSet002\Enum\USBSTOR", true);

                string[] sunKeyNames1 = sunKey1.GetSubKeyNames();

                foreach (string name in sunKeyNames1)
                {

                    //删除注册表项
                    sunKey1.DeleteSubKeyTree(name + @"\", false);
                }

                //============================================================//
                RegistryKey sunKey2 = key.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USBSTOR", true);

                string[] sunKeyNames2 = sunKey2.GetSubKeyNames();

                foreach (string name in sunKeyNames2)
                {

                    //删除注册表项
                    sunKey2.DeleteSubKeyTree(name + @"\", false);
                }

                //============================================================//
                RegistryKey sunKey3 = key.OpenSubKey(@"SYSTEM\ControlSet001\Enum\USB", true);

                string[] sunKeyNames3 = sunKey3.GetSubKeyNames();

                foreach (string name in sunKeyNames3)
                {
                    if (name.Contains("VID_"))
                    {
                        //删除注册表项
                        sunKey3.DeleteSubKeyTree(name + @"\", false);
                    }
                }

                //============================================================//
                RegistryKey sunKey4 = key.OpenSubKey(@"SYSTEM\ControlSet002\Enum\USB", true);

                string[] sunKeyNames4 = sunKey4.GetSubKeyNames();

                foreach (string name in sunKeyNames4)
                {
                    if (name.Contains("VID_"))
                    {
                        //删除注册表项
                        sunKey4.DeleteSubKeyTree(name + @"\", false);
                    }
                }

                //============================================================//
                RegistryKey sunKey5 = key.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USB", true);

                string[] sunKeyNames5 = sunKey5.GetSubKeyNames();

                foreach (string name in sunKeyNames5)
                {
                    if (name.Contains("VID_"))
                    {
                        //删除注册表项
                        sunKey5.DeleteSubKeyTree(name + @"\", false);
                    }
                }

                return true;

            }
            catch (Exception err)
            {

                return false;
            }
        }

以上是windows 服务的核心代码。服务写完以后,刚开始是使用了打包安装的方式,发现有个问题,就是在安装完成以后就要清理注册表、备份驱动、删除驱动、启动服务等等,觉得这样做不方便,于是又写了一个安装程序 抓狂
安装程序需要复制文件、安装服务、启动服务、设置注册表禁用USB存储设备、删除注册表USB存储设备使用记录、备份USB存储设备驱动、删除USB存储设备驱动,这就需要执行cmd命令行、执行.bat批处理等等,简直太虐了,只能一点一点来了,还是先贴完成截图:



点击 开始安装 以后,会执行以下步骤

1、复制文件到相应文件夹中

2、执行 psexec.exe -i -d -s usbSetup.exe 命令,以system权限打开usbSetup,执行安装服务、启动服务、设置注册表键值、删除注册表中USB存储设备使用记录、备份驱动文件、删除驱动驱动文件等一系列操作

具体代码如下:

开始安装 按钮事件

<span style="white-space:pre">	</span>private void button1_Click(object sender, EventArgs e)
        {
            //开始安装

            button1.Text = "正在安装...";
            button1.Enabled = false;

            //richTextBox1.Text += "正在展开文件...\r\n";
            writeLog("正在展开文件...");

            //服务程序目标路径
            string toPath = @"C:\windows\usbServer\";

            //服务程序文件路径
            string fromPath = Application.StartupPath + "\\usbServer";

            copyFile(fromPath, toPath);

            int isNet = 0;
            List<string> list = GetDotNetVersions();
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i].Contains("4.0"))
                    isNet = 1;
            }

            if (isNet == 0)
            {
                //安装.net
                System.Diagnostics.Process.Start(Application.StartupPath + @"\.net\dotNetFx40_Client_x86_x64.exe").WaitForExit();

                //安装.net语言包
                System.Diagnostics.Process.Start(Application.StartupPath + @"\.net\dotNetFx40LP_Client_x86_x64zh-Hans.exe").WaitForExit();
            }

            //执行cmd,打开真正的安装程序
            string installStr = Application.StartupPath + @"\psTools\run.bat";
            Cmd(installStr);

            this.Close();
            Application.Exit();

        }
<span style="white-space:pre">	</span>/// <summary>
        /// 执行cmd命令
        /// </summary>
        /// <param name="c">要执行的命令</param>
        public void Cmd(string c)
        {
            System.Diagnostics.Process process = new System.Diagnostics.Process();
            process.StartInfo.FileName = "cmd.exe";
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.RedirectStandardInput = true;
            process.Start();

            process.StandardInput.WriteLine(c);
            process.StandardInput.AutoFlush = true;
            process.StandardInput.WriteLine("exit");

            //StreamReader reader = process.StandardOutput;//截取输出流

            //string output = reader.ReadLine();//每次读取一行

            //while (!reader.EndOfStream)
            //{
            //    //PrintThrendInfo(output);
            //    writeLog(output);
            //    output = reader.ReadLine();
            //}

            process.WaitForExit();
        }
复制文件

<span style="white-space:pre">	</span>private void copyFile(string fromPath,string toPath)
        {
            try
            {
                //检查目标目录是否以目录分割字符  
                //结束如果不是则添加之   
                if (toPath[toPath.Length - 1] != Path.DirectorySeparatorChar)
                    toPath += Path.DirectorySeparatorChar;
                //判断目标目录是否存在如果不存在则新建之  
                if (!Directory.Exists(toPath))
                    Directory.CreateDirectory(toPath);
                //得到源目录的文件列表,该里面是包含  
                //文件以及目录路径的一个数组    
                //如果你指向copy目标文件下面的文件    
                //而不包含目录请使用下面的方法    
                //string[]fileList=  Directory.GetFiles(srcPath);  
                string[] fileList =
                    Directory.GetFileSystemEntries(fromPath);
                //遍历所有的文件和目录    
                foreach (string file in fileList)
                {
                    //先当作目录处理如果存在这个  
                    //目录就递归Copy该目录下面的文件    
                    if (Directory.Exists(file))
                        copyFile(file,toPath + Path.GetFileName(file));
                    //否则直接Copy文件   
                    else
                    {
                        File.Copy(file, toPath + Path.GetFileName(file), true);
                        writeLog("正在展开--->" + Path.GetFileName(file));
                    }
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.ToString());
            }  
        }

<span style="white-space:pre">	</span>/// <summary>
        /// 写入状态
        /// </summary>
        /// <param name="str">要显示的值</param>
        private void writeLog(string str)
        {
            richTextBox1.Text += str + "\r\n";
            richTextBox1.Focus();
        }

<span style="white-space:pre">	</span>/// <summary>
        /// 获取已经安装的.net版本
        /// </summary>
        /// <returns></returns>
        private List<string> GetDotNetVersions()
        {
            DirectoryInfo[] directories = new DirectoryInfo(
                Environment.SystemDirectory + @"\..\Microsoft.NET\Framework").GetDirectories("v?.?.*");
            List<string> list = new List<string>();
            foreach (DirectoryInfo info2 in directories)
            {
                list.Add(info2.Name.Substring(1));
            }
            return list;
        }

以上代码是初始化安装界面,还有一个真正的安装界面,样子和这个一样,但是执行的代码可是不一样,具体代码如下:

<span style="white-space:pre">	</span>/// <summary>
        /// 安装服务
        /// </summary>
        private void installServer()
        {
            writeLog("正在安装服务...");

            string cmdStr = @"%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\installutil.exe c:\windows\usbServer\usbService.exe";
            Cmd(cmdStr);

            writeLog("安装成功!");

            writeLog("正在启动服务...");
            cmdStr = @"Net Start usbService";
            Cmd(cmdStr);

            writeLog("启动服务成功!");
            
            writeLog("开始清理缓存文件...\r\n");

            t1 = new Thread(new ThreadStart(delRegUsbLog));
            t1.Start();
        }

其它的代码,比如注册表禁用、启用、清理USB使用记录、备份驱动、删除驱动上边已经贴出相关代码了,不再赘述

这里需要注意一点的是,因为是要在richtextbox里实时显示状态的,所以用到了线程,需要在窗体的load事件里加入如下代码

Control.CheckForIllegalCrossThreadCalls = false;
该处执行cmd命令的函数使用的是上边的public void Cmd(string c),区别是放开了注释的部分,用于实时显示状态

另外,那个批处理的代码如下:

@echo off 

set a= %cd%
echo %a%
set b=%a:psTools=install%
echo %b%

set c= %a%\psTools\psexec.exe -i -d -s %b%\install\usbSetup

echo %c%

start %c%

至此,核心代码基本完成。完整代码因为种种原因不能往外放,只要自己用点心可以拿核心代码拼装一个。

总结一下,做这个东西用了一天半的时间。其中,比较耗费时间的是找思路,怎么做这件事。另外,socket没有接触过,都是从网上找的现成代码,使用之后发现了个bug,cpu使用率会飚到100%,自己改了一下。安装的时候清理USB存储设备使用记录的权限问题,也耗费了不少脑细胞,最后找到psexec.exe这个工具,想要的去百度,然后到微软官方下载,就不放具体下载地址了。这个小软件除了本文一开始描述的需要完善的地方以外,满足我们的需求是没有问题的。

特别说明,注册表禁用USB、注册表USB存储设备使用记录、USB存储设备驱动文件位置请自行百度,答案一堆一堆的, 写了这么多,实在是懒得贴了,见谅见谅!

此文仅用于记录及回顾,也给有这方面需求的朋友一个思路,如果有更好的方案或者有错误的地方,欢迎批评指正!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值