[连载]《C#通讯(串口和网络)框架的设计与实现》- 14.序列号的设计,不重复的实现一机一码

简介: 目       录 第十四章     序列号的设计... 2 14.1        设计原则... 2 14.2        设计思想... 3 14.3        代码实现... 4 14.4        代码混淆... 18 14.5        代码破解... 18 14.6        小结... 18   第十四章      序列号的设计     序列号作为软件使用授权方式之一,被广泛使用在应用软件方面。

目       录

第十四章     序列号的设计... 2

14.1        设计原则... 2

14.2        设计思想... 3

14.3        代码实现... 4

14.4        代码混淆... 18

14.5        代码破解... 18

14.6        小结... 18

 

第十四章      序列号的设计

    序列号作为软件使用授权方式之一,被广泛使用在应用软件方面。主要考虑到这几方面:1.对知识产权的保护,毕竟付出来脑力劳动和体力劳动。2.商业竞争中增加防守的能力,防止被竞争对手盗取。3.增强合同的执行效力,防止另一方由于各种原因破坏合作机制。

    基于上述方面,从保护、防守思维模式角度考虑,增加序列号功能是有必要的。每个作者或公司设计序列号的方式不一样,就是因为不一样,所以才能达到我们增加该功能的效果。

14.1     设计原则

  1. 序列号长度尽可能短

    主要是从成本角度考虑的。例如用户现场需要一个正版软件的序列号,你把序列号信息通过什么方式传递给用户呢?假设我们用对称或非对称方式生成一个很长的序列号,如果口述告诉对方的话,那么对方肯定要用纸和笔进行记录,最后输入到软件后还不一定正确;如果把序列号以文件的方式通过网络传递给对方,那么需要占用网络资源,另外对方的电脑不一定有网络环境。不管如何,很长的序列号在生成和传递的过程中可能涉及到的成本包括:材料成本、流量成本、人力成本和时间成本等。

     如果一个字符可以表达序列号所需要的完整信息,那么是最理想的。但是,这是理想状态,是不可能实现的,至少以我现在的能力是无法完成的。所以,要以最佳的长度表达出序列号的全部信息。

  1. 避免出现容易混淆的字符生成一个序列号发给了用户,这个序列号包括:数字0和字母O,数字1和字母l。难道让用户一遍一遍的试嘛,这样的用户体验太差了,虽然嘴上不说出来,至少感觉不太舒服。

14.2     设计思想

    设计的思想要看序列号要实现什么样的功能和具备什么属性。从功能角度考虑,包括:1.一个计算机一个序列号;2.尽管输入的条件都一样,每次生成的序列号都不一样;3.对使用的时限进行验证;4.序列号有注册时限,超过规定的使用时间,序列号作废,避免短时间多次注册。从属性角度考虑,包括:同样的计算机、同样的输入条件生成的序列号都不一样。

   我们把上述因素考虑进去,序列号长度为25位字符,序列号生成格式和元素信息如下图:

 

 X01-X05:为计算机的特征码,5位字符串,获得机器某个部件的ID,这个部件可能为CPU、网卡、硬盘等信息,把ID进行MD5加密后取前5个字符作为特征码,来实现一机一码。这种方式,特征码有可能有相同的情况,但是机率很小。

 X06-X13:为生成序列号的日期,8位字符串,格式为:yyyyMMdd。与后边的使用时间限制配合使用,来验证软件的使用期限。

 X14-X15:为注册时间限制,2位数字字符,从生成序列号日期算起,超过此注册时间限制,序列号将无法正常进行注册操作。

 X16-X20:为使用时间限制,5位数字字符,与生成序列号日期配合使用来验证软件使用期限。

 X21:为序列号的偏移量,1位字符,不管在什么场景下,每次生成序列号的偏移量都不一样。

X22-X25:为保留数据位,暂时不使用。自定义一个序列号字典信息,例如:_Dictionary ="JCB8EF2GH7K6MVP9QR3TXWY4",把容易混淆的字符去掉,这个可以自定义。序列号的每个部分都是通过随机生成的偏移量(X21),对字典进行位移,根据输入的数字信息对应字典的下标提取相应的字符作为序列号的一个字符。

   生成序列号的大概过程:

  1. 在字典信息的长度范围内随机生成一个偏移量数字。
  2. 根据偏移量数字对字典进行左或右的循环移动。
  3. 根据输入的数字信息,例如:2015中的2,作为下标,从字典信息中提取出相应的字符。

反向解析大概过程类似,只需要根据X21字符,与字典的字符进行匹配,对应的下标作为偏移量,就可以反向解析出各项信息。

14.3     代码实现

1.MD5操作类:

public class Safety
{
       public static string MD5(string str)
       {
              string strResult = "";
              MD5 md5 = System.Security.Cryptography.MD5.Create();
              byte[] bData = md5.ComputeHash(Encoding.Unicode.GetBytes(str));
              for (int i = 0; i < bData.Length; i++)
              {
                     strResult = strResult + bData[i].ToString("X");
              }
              return strResult;
       }
}

2.注册信息类:

public class RegInfo
{
       public RegInfo()
       {
              KeySn = "";
              Date=DateTime.MinValue;
              RegLimitDays = 0;
              UseLimitDays = 0;
              Offset = 0;
       }
       public string KeySn { get; set; }
       public DateTime Date { get; set; }
       public int RegLimitDays { get; set; }
       public int UseLimitDays { get; set; }
       public int Offset { get; set; }
}

3.偏移操作类型:

internal enum OffsetType
{
       Left,
       Right
}

4.        序列号管理类

public class LicenseManage
    {
        /// <summary>
        /// 序列号字典,把数字和字母容易混淆的字符去掉。所产生的25位序列号从这个字典中产生。
        /// </summary>
        private static string _Dictionary = "JCB8EF2GH7K6MVP9QR3TXWY4";

        /// <summary>
        /// 可以自定义字典字符串
        /// </summary>
        public static string Dictionary
        {
            get { return _Dictionary; }
            set
            {
                if (value.Length < 9)
                {
                    throw new ArgumentOutOfRangeException("设置的字典长度不能小于9个字符");
                }

                _Dictionary = value;
            }
        }

        /// <summary>
        /// 生成序列号
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="now">现在的时间</param>
        /// <param name="regLimitDays">注册天数限制,超过此天数,再进行注册,序列号就失效了,不能再使用了</param>
        /// <param name="useLimitDays">使用天数限制,超过此天数,可以设置软件停止运行等操作</param>
        /// <returns>返回序列号,例如:xxxxx-xxxxx-xxxxx-xxxxx-xxxxx</returns>
        public static string BuildSn(string key, DateTime now, int regLimitDays, int useLimitDays)
        {
            if (regLimitDays < 0 || regLimitDays > 9)
            {
                throw new ArgumentOutOfRangeException("注册天数限制范围为0-9");
            }

            if (useLimitDays < 0 || useLimitDays > 99999)
            {
                throw new ArgumentOutOfRangeException("使用天数限制范围为0-99999");
            }

            /*
             *关键字用MD5加密后,取后5个字符作为序列号第1组字符
             */
            string md5 = Safety.MD5(key);
            string x1 = md5.Substring(md5.Length - 5);

            /*
             * 生成随机偏移量
             */
            Random rand = new Random();
            int offset = rand.Next(1, Dictionary.Length - 1);

            /*
             * 第5组的第1个字符保存偏移量字符,其余4个字符随机生成,作为保留位
             */
            string x5 = Dictionary[offset].ToString();
            for (int i = 0; i < 4; i++)
            {
                x5 += Dictionary[rand.Next(0, Dictionary.Length - 1)].ToString();
            }

            /*
             * 以注册时间(yyyyMMdd)和注册时间限制生成第2组和第3组序列号,一共10位字符串
             */
            string dateSn = GetDateSn(now, offset);
            string regLimitSn = GetRegLimitSn(regLimitDays, offset);
            string x2 = dateSn.Substring(0, 5);
            string x3 = dateSn.Substring(dateSn.Length - 3) + regLimitSn;

            /*
             *以使用时间限制生成第4组序列号,一共5位字符串
             */
            string x4 = GetUseLimitSn(useLimitDays, offset);

            return String.Format("{0}-{1}-{2}-{3}-{4}", x1, x2, x3, x4, x5);
        }

        /// <summary>
        /// 注册序列号
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="sn">序列号</param>
        /// <param name="desc">描述信息</param>
        /// <returns>注册状态,成功:0</returns>
        internal static int RegSn(string key, string sn, ref string desc)
        {
            if (String.IsNullOrEmpty(key) || String.IsNullOrEmpty(sn))
            {
                throw new ArgumentNullException("参数为空");
            }

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                desc = "关键字与序列号不匹配";
                return -1;//关键字与序列号不匹配
            }

            if (regInfo.Date == DateTime.MaxValue || regInfo.Date == DateTime.MinValue || regInfo.Date > DateTime.Now.Date)
            {
                desc = "序列号时间有错误";
                return -2;//序列号时间有错误
            }

            TimeSpan ts = DateTime.Now.Date - regInfo.Date;
            if (ts.TotalDays > 9 || ts.TotalDays < 0)
            {
                desc = "序列号失效";
                return -3;//序列号失效
            }

            if (regInfo.UseLimitDays <= 0)
            {
                desc = "使用期限受限";
                return -4;//使用期限受限
            }

            Application.UserAppDataRegistry.SetValue("SN", sn);

            desc = "注册成功";
            return 0;
        }

        /// <summary>
        /// 检测序列号,试用于时钟定时调用
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <param name="desc">描述信息</param>
        /// <returns>检测状态,成功:0</returns>
        internal static int CheckSn(string key, ref string desc)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("参数为空");
            }

            object val = Application.UserAppDataRegistry.GetValue("SN");

            if (val == null)
            {
                desc = "未检测到本机的序列号";
                return -1;
            }

            string sn = val.ToString();

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                desc = "关键字与序列号不匹配";
                return -2;//关键字与序列号不匹配
            }

            if ((DateTime.Now.Date - regInfo.Date).TotalDays > regInfo.UseLimitDays)
            {
                desc = "序列使用到期";
                return -3;//关键字与序列号不匹配
            }

            desc = "序列号可用";
            return 0;
        }

        /// <summary>
        /// 获得剩余天数
        /// </summary>
        /// <param name="key">关键字,一般为CPU号、硬盘号、网卡号,用于与序列号绑定,实现一机一码</param>
        /// <returns>剩余天数</returns>
        internal static int GetRemainDays(string key)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("参数为空");
            }

            object val = Application.UserAppDataRegistry.GetValue("SN");

            if (val == null)
            {
                return 0;
            }

            string sn = val.ToString();

            LicenseInfo regInfo = GetRegInfo(sn);
            string md5 = Safety.MD5(key);
            if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
            {
                return 0;//关键字与序列号不匹配,不能使用。
            }

            //<=0的情况,证明不可以使用。
            return regInfo.UseLimitDays - (int)(DateTime.Now.Date - regInfo.Date).TotalDays;
        }

        /// <summary>
        /// 根据序列号,反推注册信息
        /// </summary>
        /// <param name="sn">序列号</param>
        /// <returns>注册信息</returns>
        private static LicenseInfo GetRegInfo(string sn)
        {
            LicenseInfo reg = new LicenseInfo();
            string[] splitSn = sn.Split('-');
            if (splitSn.Length != 5)
            {
                throw new FormatException("序列号格式错误,应该带有'-'字符");
            }

            reg.KeySn = splitSn[0];
            reg.Offset = Dictionary.IndexOf(splitSn[4][0]);
            reg.Date = GetDate(splitSn[1] + splitSn[2].Substring(0, 3), reg.Offset);
            reg.RegLimitDays = GetRegLimitDays(splitSn[2].Substring(3, 2), reg.Offset);
            reg.UseLimitDays = GetUseLimitDays(splitSn[3], reg.Offset);
            return reg;
        }

        /// <summary>
        /// 以当前时间和偏移量生成当前时间对应的字符串
        /// </summary>
        /// <param name="now">当前时间</param>
        /// <param name="offset">偏移量</param>
        /// <returns>返回日期对应的字符串,8位字符串</returns>
        private static string GetDateSn(DateTime now, int offset)
        {
            string dateSn = "";
            string date = now.ToString("yyyyMMdd");
            string newDic = Dictionary;
            for (int i = 0; i < date.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(date[i].ToString());
                dateSn += newDic[num].ToString();
            }
            return dateSn;
        }

        /// <summary>
        /// 根据注册时间序列号反推注册时间
        /// </summary>
        /// <param name="dateSn">时间字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>时间</returns>
        private static DateTime GetDate(string dateSn, int offset)
        {
            string dateStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < dateSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(dateSn[i]);
                dateStr += num;
            }
            return new DateTime(int.Parse(dateStr.Substring(0, 4)), int.Parse(dateStr.Substring(4, 2)), int.Parse(dateStr.Substring(6, 2)));
        }

        /// <summary>
        /// 以注册时间限制和偏移量生成对应的字符串
        /// </summary>
        /// <param name="regLimitDays"></param>
        /// <param name="offset"></param>
        /// <returns>返回对应的注册时间限制的字符串,2位字符串</returns>
        private static string GetRegLimitSn(int regLimitDays, int offset)
        {
            string regLimitSn = "";
            string regLimitStr = regLimitDays.ToString("00");
            string newDic = Dictionary;
            for (int i = 0; i < regLimitStr.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(regLimitStr[i].ToString());
                regLimitSn += newDic[num].ToString();
            }
            return regLimitSn;
        }

        /// <summary>
        /// 根据注册时间限制字符串反推注册时间限制
        /// </summary>
        /// <param name="regLimitSn">注册时间限制字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>注册时间限制</returns>
        private static int GetRegLimitDays(string regLimitSn, int offset)
        {
            string regLimitStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < regLimitSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(regLimitSn[i]);
                regLimitStr += num;
            }
            return int.Parse(regLimitStr);
        }

        /// <summary>
        /// 以使用时间限制和偏移量生成对应的字符串
        /// </summary>
        /// <param name="useLimitDays">使用时间限制</param>
        /// <param name="offset">偏移量</param>
        /// <returns>使用时间限制对应字符串,5位字符串</returns>
        private static string GetUseLimitSn(int useLimitDays, int offset)
        {
            string useLimitSn = "";
            string useLimitStr = useLimitDays.ToString("00000");
            string newDic = Dictionary;
            for (int i = 0; i < useLimitStr.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = int.Parse(useLimitStr[i].ToString());
                useLimitSn += newDic[num].ToString();
            }
            return useLimitSn;
        }

        /// <summary>
        /// 根据使用时间限制字符串反推使用时间限制
        /// </summary>
        /// <param name="regLimitSn">使用时间限制字符串</param>
        /// <param name="offset">偏移量</param>
        /// <returns>使用时间限制</returns>
        private static int GetUseLimitDays(string useLimitSn, int offset)
        {
            string useLimitStr = "";
            string newDic = Dictionary;
            for (int i = 0; i < useLimitSn.Length; i++)
            {
                newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
                int num = newDic.IndexOf(useLimitSn[i]);
                useLimitStr += num;
            }
            return int.Parse(useLimitStr);
        }

        /// <summary>
        /// 根据字典、偏移量和偏移类型生成新的字典
        /// </summary>
        /// <param name="dic"></param>
        /// <param name="offset"></param>
        /// <param name="offsetType"></param>
        /// <returns></returns>
        private static string GetNewDictionaryString(string dic, int offset, LicenseOffset offsetType)
        {
            StringBuilder sb = new StringBuilder(dic);
            if (offsetType == LicenseOffset.Left)
            {
                for (int i = 0; i < offset; i++)
                {
                    string head = sb[0].ToString();
                    sb.Remove(0, 1);
                    sb.Append(head);
                }
            }
            else if (offsetType == LicenseOffset.Right)
            {
                for (int i = 0; i < offset; i++)
                {
                    string end = sb[dic.Length - 1].ToString();
                    sb.Remove(dic.Length - 1, 1);
                    sb.Insert(0, end);
                }
            }
            return sb.ToString();
        }
    }

 

14.4     代码混淆

   从安全角度来讲,.NET程序如果不加混淆的话,很容易被反编译出源代码的。从专业角度来讲,即使增加了序列号功能,也无济于事,专业的人员分分钟可以破解掉,尽管这样干的人很少,但是存在这种可能性。如果一个软件人员想了解一个很好的软件,第一反映可能就是反编译。

   对于公司或商业使用的软件来讲,增加混淆还是有必要的,尽管现在开源很流行。

14.5     代码破解

    不管.NET程序如何进行混淆,理论上都是可以破解的,理论的东西就不赘述了。通常接触过的破解方式有两种:注册机方式和暴力方式。

    注册机的方式,需要通过软件的验证序列号的过程和机制反向推算出序列号的生成算法,根据反推的算法开发一个小软件,用于生成脱离作者授权生成序列号。这种方式不会破坏程序本身的代码,相对温和。暴力的方式,就是找到序列号验证部分的代码,通过删除或绕过验证代码等方式不让代码有效执行。这种方式会对程序本身的代码进行改动,所以也存在一些风险。

14.6     小结

     实现序列号有多种方式,上述方式不一定最好,但是希望对开发者有一定帮助。

 

最终实现效果图如下:

作者:唯笑志在

Email:504547114@qq.com

QQ:504547114

.NET开发技术联盟:54256083

文档下载:http://pan.baidu.com/s/1pJ7lZWf

官方网址:http://www.bmpj.net

相关文章
|
1月前
|
监控 安全
从 Racket 语言出发,创新员工网络监控软件的框架
在数字化企业环境中,员工网络监控软件对于保障信息安全和提升效率至关重要。Racket 语言凭借其独特特性和强大功能,为开发创新的监控软件提供了新可能。通过捕获和分析网络数据包、记录员工网络活动日志,甚至构建复杂的监控框架,Racket 能够满足企业的定制化需求,为企业信息安全和管理提供强有力支持。未来,基于 Racket 的创新解决方案将不断涌现。
37 6
|
6天前
|
数据采集 存储 JSON
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第27天】本文介绍了Python网络爬虫Scrapy框架的实战应用与技巧。首先讲解了如何创建Scrapy项目、定义爬虫、处理JSON响应、设置User-Agent和代理,以及存储爬取的数据。通过具体示例,帮助读者掌握Scrapy的核心功能和使用方法,提升数据采集效率。
41 6
|
14天前
|
机器学习/深度学习 人工智能
类人神经网络再进一步!DeepMind最新50页论文提出AligNet框架:用层次化视觉概念对齐人类
【10月更文挑战第18天】这篇论文提出了一种名为AligNet的框架,旨在通过将人类知识注入神经网络来解决其与人类认知的不匹配问题。AligNet通过训练教师模型模仿人类判断,并将人类化的结构和知识转移至预训练的视觉模型中,从而提高模型在多种任务上的泛化能力和稳健性。实验结果表明,人类对齐的模型在相似性任务和出分布情况下表现更佳。
38 3
|
1月前
|
安全 网络安全 区块链
网络安全与信息安全:构建数字世界的防线在当今数字化时代,网络安全已成为维护个人隐私、企业机密和国家安全的重要屏障。随着网络攻击手段的不断升级,从社交工程到先进的持续性威胁(APT),我们必须采取更加严密的防护措施。本文将深入探讨网络安全漏洞的形成原因、加密技术的应用以及提高公众安全意识的重要性,旨在为读者提供一个全面的网络安全知识框架。
在这个数字信息日益膨胀的时代,网络安全问题成为了每一个网民不可忽视的重大议题。从个人信息泄露到企业数据被盗,再到国家安全受到威胁,网络安全漏洞如同隐藏在暗处的“黑洞”,时刻准备吞噬掉我们的信息安全。而加密技术作为守护网络安全的重要工具之一,其重要性不言而喻。同时,提高公众的安全意识,也是防范网络风险的关键所在。本文将从网络安全漏洞的定义及成因出发,解析当前主流的加密技术,并强调提升安全意识的必要性,为读者提供一份详尽的网络安全指南。
|
2月前
|
存储 SQL 安全
网络安全与信息安全:守护数字世界的坚盾在这个高度数字化的时代,网络安全和信息安全已经成为个人、企业乃至国家安全的重要组成部分。本文将深入探讨网络安全漏洞、加密技术以及安全意识的重要性,旨在为读者提供一个全面的网络安全知识框架。
随着互联网技术的飞速发展,网络安全问题日益凸显。从个人信息泄露到企业数据被盗,再到国家安全受到威胁,网络安全事件层出不穷。本文将从网络安全漏洞的定义与分类入手,探讨常见的网络攻击手段;随后深入解析加密技术的原理及其在保护信息安全中的作用;最后强调提升公众与企业的安全意识的重要性,并提出具体的建议。通过综合运用这些知识点,我们可以更好地构建起一道道坚固的防线,守护我们的数字世界。
|
7天前
|
数据采集 前端开发 中间件
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第26天】Python是一种强大的编程语言,在数据抓取和网络爬虫领域应用广泛。Scrapy作为高效灵活的爬虫框架,为开发者提供了强大的工具集。本文通过实战案例,详细解析Scrapy框架的应用与技巧,并附上示例代码。文章介绍了Scrapy的基本概念、创建项目、编写简单爬虫、高级特性和技巧等内容。
27 4
|
7天前
|
网络协议 物联网 API
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第26天】Python 是一门功能强大且易于学习的编程语言,Twisted 框架以其事件驱动和异步IO处理能力,在网络编程领域独树一帜。本文深入探讨 Twisted 的异步IO机制,并通过实战示例展示其强大功能。示例包括创建简单HTTP服务器,展示如何高效处理大量并发连接。
24 1
|
25天前
|
机器学习/深度学习 数据采集 算法
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
这篇博客文章介绍了如何使用包含多个网络和多种训练策略的框架来完成多目标分类任务,涵盖了从数据准备到训练、测试和部署的完整流程,并提供了相关代码和配置文件。
42 0
目标分类笔记(一): 利用包含多个网络多种训练策略的框架来完成多目标分类任务(从数据准备到训练测试部署的完整流程)
|
6天前
|
网络协议 调度 开发者
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第27天】本文介绍了Python网络编程中的Twisted框架,重点讲解了其异步IO处理机制。通过反应器模式,Twisted能够在单线程中高效处理多个网络连接。文章提供了两个实战示例:一个简单的Echo服务器和一个HTTP服务器,展示了Twisted的强大功能和灵活性。
18 0
完成切换网络+修改网络连接图标提示的代码框架
完成切换网络+修改网络连接图标提示的代码框架