zoukankan      html  css  js  c++  java
  • 【Remoting】.Net remoting方法实现简单的在线升级(上篇:更新文件)

    一、前言:

          最近做一个简单的在线升级Demo,使用了微软较早的.Net Remoting技术来练手。

            简单的思路就是在服务器配置一个Remoting对象,然后在客户端来执行Remoting对象中的方法。

            过程:

            (1) 读取本地dll文件的名称与版本号,与服务器的进行对比

            (2) 确认需要升级的文件名称与版本号并告诉服务器,服务器将其复制到一个临时文件夹并压缩成zip

            (3) 将服务器的zip下载到本地的临时文件夹,并解压。

            定义服务器端为UpdateServer,其配置文件为:

    <configuration>
      <system.runtime.remoting>
        <application>
          <service>
            <wellknown type="UpdateLibrary.RemoteObject, UpdateLibrary" mode="Singleton" objectUri="RemoteObject.rem"/>
          </service>
          <channels>
            <channel ref="http" port="8989">
            </channel>
          </channels>
        </application>
      </system.runtime.remoting>
      <appSettings>
        <!--<add key="Dir" value="E:server"/>-->
      </appSettings>
    </configuration>

            定义客户端为UpdateClient,其配置文件为:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <appSettings>
        <add key="ServerUrl" value="127.0.0.1:8989"/>
        <add key="Modules" value="BIMCoreDB"/>
        <add key="BufferLength" value="100"/>
      </appSettings>
    </configuration>

           定义两端共同调用的dll为UpdateLibrary。


    二、服务器端代码:

         程序主入口:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Forms;
    using System.Configuration;
    using UpdateLibrary;
    
    namespace UpdateServer
    {
        static class Program
        {
            /// <summary>
            /// 应用程序的主入口点。
            /// </summary>
            [STAThread]
            static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                LoadConfig();
                Application.Run(new FormServer());
            }
    
            private static void LoadConfig()
            {
                Config.Dir = System.IO.Path.Combine(Application.StartupPath, "serverfiles"); //更新包所在位置
                Config.TempDir = System.IO.Path.Combine(Application.StartupPath, "temp");  //临时文件夹,用于放更新文件的地方。
            }
        }
    }

              服务器窗体后台代码:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.Remoting;
    namespace UpdateServer
    {
        public partial class FormServer : Form
        {
            public FormServer()
            {
                InitializeComponent();
                try
                {
                    //remoting配置
                    RemotingConfiguration.Configure(string.Format("{0}\UpdateServer.exe.config", Application.StartupPath), false);
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            private void FormServer_Load(object sender, EventArgs e)
            {
                lbl_time.Text = "当前时间:"+DateTime.Now.ToString("T");
                tm_Server = new Timer();
                tm_Server.Tick += tm_Server_Tick;
                tm_Server.Interval = 1000;
                tm_Server.Enabled = true;
            }
            void tm_Server_Tick(object sender,EventArgs e)
            {
                lbl_time.Text = string.Empty;
                lbl_time.Text = "当前时间:" + DateTime.Now.ToString("T");
            }
        }
    }

    三、UpdateLibrary:

            UpdateLibrary类库包含三个类:
            (1)Config类:用于提取配置文件中的信息。

            (2)ZipHelper类:第三方库,用于文件压缩与解压缩。

            (3)RemoteObject类:remoting对象,实现两端之间所需要的方法。

            Config代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;
    
    namespace UpdateLibrary
    {
        /// <summary>
        /// 将配置文件中的信息传给Config对象
        /// </summary>
        public static class Config
        {
            public static string Dir { get; set; }
            public static string TempDir { get; set; }
            public static string[] Modules { get; set; }
            public static int BufferLength { get; set; }
            public static string ServerUrl { get; set; }
        }
    }

            ZipHelper代码:(比较实用的压缩与解压缩方法)

      1 using System;
      2 using System.Collections.Generic;
      3 using System.Linq;
      4 using System.Text;
      5 using ICSharpCode.SharpZipLib.Zip;
      6 using ICSharpCode.SharpZipLib.Checksums;
      7 using System.IO;
      8 
      9 namespace UpdateLibrary
     10 {
     11     public class ZipHelper
     12     {
     13         #region  压缩
     14         /// <summary>
     15         /// 压缩文件
     16         /// </summary>
     17         /// <param name="sourceFilePath"></param>
     18         /// <param name="destinationZipFilePath"></param>
     19         public static void CreateZip(string sourceFilePath, string destinationZipFilePath)
     20         {
     21             if (sourceFilePath[sourceFilePath.Length - 1] != System.IO.Path.DirectorySeparatorChar)
     22                 sourceFilePath += System.IO.Path.DirectorySeparatorChar;
     23             ZipOutputStream zipStream = new ZipOutputStream(File.Create(destinationZipFilePath));
     24             zipStream.SetLevel(6);  // 压缩级别 0-9
     25             CreateZipFiles(sourceFilePath, zipStream);
     26             zipStream.Finish();
     27             zipStream.Close();
     28         }
     29         /// <summary>
     30         /// 递归压缩文件
     31         /// </summary>
     32         /// <param name="sourceFilePath">待压缩的文件或文件夹路径</param>
     33         /// <param name="zipStream">打包结果的zip文件路径(类似 D:WorkSpacea.zip),全路径包括文件名和.zip扩展名</param>
     34         /// <param name="staticFile"></param>
     35         private static void CreateZipFiles(string sourceFilePath, ZipOutputStream zipStream)
     36         {
     37             Crc32 crc = new Crc32();
     38             string[] filesArray = Directory.GetFileSystemEntries(sourceFilePath);
     39             foreach (string file in filesArray)
     40             {
     41                 if (Directory.Exists(file))                     //如果当前是文件夹,递归
     42                 {
     43                     CreateZipFiles(file, zipStream);
     44                 }
     45                 else                                            //如果是文件,开始压缩
     46                 {
     47                     FileStream fileStream = File.OpenRead(file);
     48                     byte[] buffer = new byte[fileStream.Length];
     49                     fileStream.Read(buffer, 0, buffer.Length);
     50                     string tempFile = file.Substring(sourceFilePath.LastIndexOf("\") + 1);
     51                     ZipEntry entry = new ZipEntry(tempFile);
     52                     entry.DateTime = DateTime.Now;
     53                     entry.Size = fileStream.Length;
     54                     fileStream.Close();
     55                     crc.Reset();
     56                     crc.Update(buffer);
     57                     entry.Crc = crc.Value;
     58                     zipStream.PutNextEntry(entry);
     59                     zipStream.Write(buffer, 0, buffer.Length);
     60                 }
     61             }
     62         }
     63         #endregion
     64 
     65         #region 解压缩
     66 
     67         public static void UnZip(Stream stream, string targetPath)
     68         {
     69             using (ZipInputStream zipInStream = new ZipInputStream(stream))
     70             {
     71                 ZipEntry entry;
     72                 while ((entry = zipInStream.GetNextEntry()) != null)
     73                 {
     74                     string directorName = Path.Combine(targetPath, Path.GetDirectoryName(entry.Name));
     75                     string fileName = Path.Combine(directorName, Path.GetFileName(entry.Name));
     76                     // 创建目录
     77                     if (directorName.Length > 0)
     78                     {
     79                         Directory.CreateDirectory(directorName);
     80                     }
     81                     if (fileName != string.Empty && !entry.IsDirectory)
     82                     {
     83                         var ext = System.IO.Path.GetExtension(fileName);
     84                         using (FileStream streamWriter = File.Create(fileName))
     85                         {
     86                             int size = 4096;
     87                             byte[] data = new byte[4 * 1024];
     88                             while (true)
     89                             {
     90                                 size = zipInStream.Read(data, 0, data.Length);
     91                                 if (size > 0)
     92                                 {
     93                                     streamWriter.Write(data, 0, size);
     94                                 }
     95                                 else break;
     96                             }
     97                         }
     98                     }
     99                 }
    100             }
    101         }
    102         #endregion
    103     }
    104 }

            RemoteObject代码:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Diagnostics;
     5 using System.Text;
     6 using System.IO;
     7 using System.Collections;
     8 using Newtonsoft.Json;
     9 
    10 
    11 
    12 namespace UpdateLibrary
    13 {
    14     public class RemoteObject : MarshalByRefObject
    15     {      
    16         public string GetUpdateFileVersion()
    17         {           
    18             System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(Config.Dir);         
    19             System.IO.FileInfo[] files = dir.GetFiles("*.dll");  //获取服务器端所有dll文件 
    20 
    21             List<string> fileinfo = new List<string>();//记录文件名与文件版本
    22             foreach(var file in files)
    23             {             
    24                 string filename = System.IO.Path.GetFileNameWithoutExtension(file.ToString());//获取文件名称
    25                 fileinfo.Add(filename);
    26                 FileVersionInfo ver = FileVersionInfo.GetVersionInfo(System.IO.Path.Combine(Config.Dir, file.ToString()));
    27                 string fileversion = ver.FileVersion; //获取文件的版本
    28                 fileinfo.Add(fileversion);
    29             }
    30             string SendData = JsonConvert.SerializeObject(fileinfo);//转Json
    31             return SendData;
    32         }
    33 
    34         public IList CreateZipfile(string str_r)
    35         {
    36             List<string> templist = JsonConvert.DeserializeObject<List<string>>(str_r);//接收到确认更新的文件名
    37 
    38             foreach (string filename in templist)  //把需要更新的文件都复制到临时文件夹中
    39             {
    40                 string updatefile = Path.Combine(Config.Dir, filename + ".dll");
    41                 File.Copy(updatefile, Path.Combine(Config.TempDir, filename + ".dll"), true);
    42                 System.IO.File.SetAttributes(Path.Combine(Config.TempDir, filename + ".dll"), System.IO.FileAttributes.Normal);//去掉文件只读属性
    43             }
    44 
    45             string tempzippath=Path.Combine(Config.Dir,"tempzip");//临时压缩包路径,默认更新文件夹下的tempzip文件夹
    46             if(Directory.Exists(tempzippath)==false)  //判断是否有安放压缩包的地方
    47             {
    48                 Directory.CreateDirectory(tempzippath);
    49             }
    50 
    51             ZipHelper.CreateZip(Config.TempDir, Path.Combine(tempzippath, "Update.zip"));//将临时文件夹内的文件都压缩到tempzip文件夹下的update.zip
    52             System.IO.FileInfo  f = new FileInfo(Path.Combine(tempzippath,"Update.zip"));//获得该压缩包的大小
    53             IList SendData = new ArrayList();
    54             SendData.Add(Path.Combine(tempzippath, "Update.zip"));  //得到压缩包名称
    55             SendData.Add(f.Length);  //得到压缩包文件大小
    56             return SendData;
    57         }
    58         public byte[] GetFile(string name, int start, int length)
    59         {
    60             using (System.IO.FileStream fs = new System.IO.FileStream(System.IO.Path.Combine(Config.TempDir, name), System.IO.FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite))
    61             {
    62                 byte[] buffer = new byte[length];
    63                 fs.Position = start;
    64                 fs.Read(buffer, 0, length);
    65                 return buffer;
    66             }
    67         }
    68 
    69         public void Finish(string name)
    70         {
    71             // File.Delete(System.IO.Path.Combine(Config.TempDir, name)); //删除压缩包文件夹
    72         }
    73     }
    74 }

    四、客户端端代码:   

            程序主入口:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Forms;
    using System.Configuration;
    using UpdateLibrary;
    
    namespace UpdateClient
    {
        static class Program
        {
            /// <summary>
            /// 应用程序的主入口点。
            /// </summary>
            [STAThread]
            static void Main(string[] args)
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                LoadConfig(args);
                Application.Run(new FormClient());
            }
            private static void LoadConfig(string[] args)
            {
                Config.Dir = System.IO.Path.Combine(Application.StartupPath,"localfiles");//本地文件位置
                Config.TempDir = System.IO.Path.Combine(Application.StartupPath, "temp");//本地放更新文件的位置
                Config.ServerUrl = ConfigurationManager.AppSettings["ServerUrl"].ToString();//设置服务器Url
                Config.Modules =ConfigurationManager.AppSettings["Modules"].ToString().Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);//更新文件的名称
                Config.BufferLength = int.Parse(ConfigurationManager.AppSettings["BufferLength"].ToString());   //缓存大小
            }
        }
    }

             

               第一个窗体FormClient,用于比对文件,如果有更新则提供按钮进入更新窗体FormUpdate

      1 using System;
      2 using System.Collections.Generic;
      3 using System.ComponentModel;
      4 using System.Data;
      5 using System.Diagnostics;
      6 using System.Drawing;
      7 using System.Linq;
      8 using System.Text;
      9 using System.Windows.Forms;
     10 using UpdateLibrary;
     11 using System.IO;
     12 using Newtonsoft.Json;
     13 
     14 namespace UpdateClient
     15 {
     16     public partial class FormClient : Form
     17     {
     18         Dictionary<string, string> Localupdate = new Dictionary<string, string>();  //确认本地需要文件
     19         Dictionary<string, string> ConfirmUpdate = new Dictionary<string, string>();  //确认需要更新的文件并告诉服务器的
     20         Dictionary<string, string> ConfirmAdd = new Dictionary<string, string>();  //确认需要新增的文件
     21         public FormClient()
     22         {
     23             InitializeComponent();
     24             btn_update.Enabled = false;
     25             int ScreenWidth = Screen.PrimaryScreen.WorkingArea.Width;
     26             int ScreenHeight = Screen.PrimaryScreen.WorkingArea.Height;
     27             int x = ScreenWidth - this.Width - 5;
     28             int y = ScreenHeight - this.Height - 5;
     29             this.Location = new Point(x, y);
     30         }
     31 
     32         private void btn_update_Click(object sender, EventArgs e)
     33         {
     34             Form updatewindow = new FormUpdate(ConfirmUpdate);
     35             updatewindow.Show();
     36             this.Hide();
     37         }
     38 
     39         private void FormClient_Load(object sender, EventArgs e)
     40         {
     41                Dictionary<string, string> localfileversion = new Dictionary<string, string>();
     42                foreach (string module in Config.Modules)
     43                {
     44                    string filepath = System.IO.Path.Combine(Config.Dir, module + ".dll");
     45                    FileVersionInfo ver = FileVersionInfo.GetVersionInfo(filepath);
     46                    string dllVersion = ver.FileVersion;
     47                    localfileversion.Add(module, dllVersion);   //文件名-版本
     48                }
     49 
     50                //文件对比
     51               try
     52               {
     53                    RemoteObject remoteObject = (RemoteObject)Activator.GetObject(typeof(RemoteObject), string.Format("http://{0}/RemoteObject.rem", Config.ServerUrl));
     54                    //获取服务器更新包的版本号
     55 
     56                    string SFVersion = remoteObject.GetUpdateFileVersion();//获取服务器端更新文件的名称与版本号
     57                    List<string> Recieve = new List<string>();
     58                    Recieve = JsonConvert.DeserializeObject<List<string>>(SFVersion);//转成泛型
     59                    Dictionary<string, string> serverfileversion = new Dictionary<string, string>();//转成字典型
     60                    for (int i = 0; i < Recieve.Count; i += 2)
     61                    {
     62                         serverfileversion.Add(Recieve[i], Recieve[i + 1]);
     63                    }
     64 
     65                    if (serverfileversion.Count > 0)  //是否有更新文件
     66                    {
     67                        foreach (var serverkey in serverfileversion.Keys)
     68                       {
     69                            if (localfileversion.ContainsKey(serverkey)) //本地是否有更新文件的名称,没有说明是新增的
     70                           {
     71                                if (localfileversion[serverkey] == serverfileversion[serverkey]) //版本号相同?
     72                                {
     73                                     serverfileversion.Remove(serverkey);//不需要更新
     74                                }
     75                                else
     76                               {
     77                                    ConfirmUpdate.Add(serverkey, serverfileversion[serverkey]); //确认更新的
     78                                    Localupdate.Add(serverkey, localfileversion[serverkey]); //本地的版本
     79                               }
     80                           }
     81                           else
     82                           {
     83                                 ConfirmAdd.Add(serverkey, serverfileversion[serverkey]);//确认新增文件,用于提示
     84                           }
     85                       }
     86                    }
     87                   else
     88                   {
     89                       lblmessage.Text = "暂无更新文件";
     90                       btn_update.Visible = false;
     91                   }
     92             }
     93             catch(Exception ex)
     94             {
     95                 lblmessage.Text = ex.ToString();
     96                 btn_update.Visible = false;
     97             }
     98          
     99             if(ConfirmAdd.Count==0 && ConfirmUpdate.Count==0)
    100             {
    101                     lblmessage.Text = "没有需要更新的模块";
    102                     btn_update.Visible = false;
    103             }
    104              else
    105             {
    106                     string upinfo = string.Empty;
    107                     lblmessage.Text = "检测完成,需要更新";
    108                     btn_update.Enabled = true;
    109                     //显示更新的
    110                     if (ConfirmUpdate.Count>0)
    111                     {
    112                         upinfo = "更新文件信息:
    
    ";
    113                         foreach(var key in ConfirmUpdate.Keys)
    114                         {
    115                             upinfo += "文件名为:" + key + "
    " + "旧版本号:" + Localupdate[key] + "
    " + "新版本号:" + ConfirmUpdate[key] + "
    ";
    116                         }
    117                     }
    118 
    119                     //显示新增的
    120                     if (ConfirmAdd.Count > 0)
    121                     {
    122                         upinfo += "
    ";
    123                         upinfo += "新增文件
    ";
    124                         foreach (var key in ConfirmAdd.Keys)
    125                         {
    126                             upinfo += "文件名为:" + key + "
    " + "版本号为:" + ConfirmAdd[key] + "
    ";
    127                             ConfirmUpdate.Add(key, ConfirmAdd[key]);
    128                         }
    129                     }                 
    130                     txt_UpdateMessage.Text = upinfo;                  
    131                 }
    132             
    133         }
    134          private void FormClient_FormClosed(object sender, FormClosedEventArgs e)
    135         {
    136             Environment.Exit(0);
    137         }
    138     
    139 
    140     }
    141 }

             

               第二个窗体FormUpdate,用于更新文件,这里同样使用了backgroundWorker控件来进行异步操作。

      1 using System;
      2 using System.Collections.Generic;
      3 using System.ComponentModel;
      4 using System.Data;
      5 using System.Drawing;
      6 using System.Linq;
      7 using System.Text;
      8 using System.Windows.Forms;
      9 using UpdateLibrary;
     10 using Newtonsoft.Json;
     11 using System.Collections;
     12 using System.IO;
     13 
     14 
     15 namespace UpdateClient
     16 {
     17     public partial class FormUpdate : Form
     18     {
     19         Dictionary<string, string> serverupdatefiles = new Dictionary<string, string>();
     20         public FormUpdate(Dictionary<string, string> confirmupdate)
     21         {
     22             InitializeComponent();
     23             int ScreenWidth = Screen.PrimaryScreen.WorkingArea.Width;
     24             int ScreenHeight = Screen.PrimaryScreen.WorkingArea.Height;
     25             int x = ScreenWidth - this.Width - 5;
     26             int y = ScreenHeight - this.Height - 5;
     27             this.Location = new Point(x, y);
     28             serverupdatefiles = confirmupdate; //获得需要更新的列表
     29         }
     30 
     31         private void FormUpdate_Load(object sender, EventArgs e)
     32         {
     33             bgk_Update.RunWorkerAsync() ;
     34         }
     35 
     36         private void FormUpdate_FormClosed(object sender, FormClosedEventArgs e)
     37         {
     38             Environment.Exit(0); //终止该进程
     39         }
     40 
     41         private void bgk_Update_DoWork(object sender, DoWorkEventArgs e)
     42         {
     43             try
     44             {
     45                 RemoteObject remoteObject = (RemoteObject)Activator.GetObject(typeof(RemoteObject), string.Format("http://{0}/RemoteObject.rem", Config.ServerUrl));
     46                 bgk_Update.ReportProgress(0, "准备更新...");
     47 
     48                 List<string> list_temp = new List<string>();
     49                 list_temp = DictToList(serverupdatefiles);//将确认更新的文件名转成泛型
     50                 string str_confirmupdate = JsonConvert.SerializeObject(list_temp);//转成Json
     51 
     52                 //将确认的文件列表返回给server,使其压缩并放置在temp文件夹下
     53                 IList RecieveData = new ArrayList();
     54                 RecieveData = remoteObject.CreateZipfile(str_confirmupdate);
     55                 
     56                 string fileName = RecieveData[0].ToString(); //获得压缩包的名称
     57                 int fileLength = Convert.ToInt32(RecieveData[1]);//获得压缩包的大小
     58 
     59                 string filePath = Path.Combine(Config.TempDir,"Update.zip");//解压到本地临时文件夹下的Update.zip
     60               
     61                 using (System.IO.FileStream stream = new System.IO.FileStream(filePath, System.IO.FileMode.Create))
     62                 {
     63                     for (int i = 0; i < fileLength; i += Config.BufferLength * 1024)
     64                     {
     65                         var percent = (int)((double)i / (double)fileLength * 100);
     66                         bgk_Update.ReportProgress(percent, "正在下载更新包...");
     67                         int length = (int)Math.Min(Config.BufferLength * 1024, fileLength - i);
     68                         var bytes = remoteObject.GetFile(fileName, i, length);
     69                         stream.Write(bytes, 0, length);
     70                     }
     71                     stream.Flush();
     72                 }
     73                 remoteObject.Finish(fileName);
     74                 bgk_Update.ReportProgress(100, "正在解压");
     75                 using (System.IO.FileStream stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
     76                 {
     77                     ZipHelper.UnZip(stream, Config.TempDir);//解压获得更新文件
     78                 }
     79                 System.IO.File.Delete(filePath);//删除解压包
     80                 e.Result = "更新完成";
     81             }
     82             catch (Exception ex)
     83             {
     84                 e.Result = ex;
     85             }
     86         }
     87 
     88         private void bgk_Update_ProgressChanged(object sender, ProgressChangedEventArgs e)
     89         {
     90             lbl_message.Text = (string)e.UserState;
     91             pbr_Update.Value = e.ProgressPercentage;
     92         }
     93 
     94         private void bgk_Update_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
     95         {
     96             if (e.Result is Exception)
     97             {               
     98                 lbl_message.Text = (e.Result as Exception).Message;
     99             }
    100             else
    101             {
    102                 lbl_message.Text = (string)e.Result;
    103             }
    104         }
    105 
    106          public List<string> DictToList(Dictionary<string,string> dict)
    107         {
    108             List<string> templist = new List<string>();
    109             foreach(var key in dict.Keys)
    110             {
    111                 templist.Add(key);
    112             }
    113             return templist;           
    114         }
    115     }
    116 }

    五、小结:

    (1) 运用了Remoting技术,简单来说就是服务器通过config文件配置remoting服务;客户端调用这个remoting服务;

    (2) 使用了BackgroundWorker控件,其主要是三种方法Dowork();ProgressChanged();RunWorkerCompleted() 

         其中控件的ReportProgress()方法可以通过ProgressChanged中的label与ProgressBar来显示升级状况与进度。

    (3) 上篇以remoting方式叙述如何从服务器端下载文件到本地,下篇将介绍得到更新的dll后如何将调用旧dll的exe程序关闭再重启加载新dll的方法,从而实现更新。

  • 相关阅读:
    GDUFE ACM-1087
    背包九讲
    OJ4TH|Inverse number:Reborn
    OJ4TH|Let's play a game
    GG第四次作业
    OpenCV(3)其他常用数据类型
    OpenCV学习(2)读取视频和摄像头
    OpenCV(1)读写图像
    GG第三次作业
    GG第二次作业
  • 原文地址:https://www.cnblogs.com/lovecsharp094/p/5616880.html
Copyright © 2011-2022 走看看