C#与php自定义数据流传输

C#与php自定义数据流传输

  • 介绍
  • 一、客户端与服务器数据传输流程图
    • 客户端发送数据给服务器:
    • 服务器返回数据给客户端:
  • 二、自定义数据流
    • C#版本数据流
    • PHP版本数据流
  • 三、数据传输测试
    • 1.在Unity中创建一个C#脚本NetWorkManager.cs
    • 2.服务器www目录创建StreamTest.php脚本代码如下:
    • 结果如下:
    • 这里需要注意一个问题,自定义数据类写入过程和读取过程顺序必须一致,否则无法获取数据。
    • PHP中的pack与unpack的方法将数据转换为二进制的方法最好了解下。

介绍

如果不了解Unity与web如何通讯的可以看我之前的文章。
无论传输什么类型的数据,如int、float、string等,他们都被保存在文本中,接下来我们从字符串中解析这些数据。

一、客户端与服务器数据传输流程图

客户端发送数据给服务器:

在这里插入图片描述

服务器返回数据给客户端:

在这里插入图片描述

二、自定义数据流

C#版本数据流

我们要创建一个C#版本的数据流类,它的主要功能是将各种不同类型的数据压入一个单独的字符创中,或将从服务器读回的字节数组解析成响应的数据,这里要清楚不同类型数据所占字节长度,如32位int即占用4个字节,短整型short占2个字节等,代码如下:

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;


public class PostStream {

    public Dictionary<string,string> Headers = new Dictionary<string, string>();
    const int HASHSIZE = 16;        //末尾16个字节保存md5数字签名
    const int BYTE_LEN = 1;         //byte占一个字节
    const int SHORT16_LEN = 2;      //short占2个字节
    const int INT32_LEN = 4;        //int占4个字节
    const int FLOAT_LEN = 4;        //float占4个字节

    private int m_index = 0;
    public int Length
    {
        get
        {
            return m_index;
        }
    }

    //秘密密码,用于数字签名
    private string m_secretKey = "123456";

    //存储Post信息
    private string[,] m_field;

    /// <summary>
    /// 最大传输数量
    /// </summary>
    private const int MAX_POST = 128;

    /// <summary>
    /// 单位Post信息所存储的信息量
    /// </summary>
    private const int PAIR = 2;

    /// <summary>
    /// 信息头索引
    /// </summary>
    private const int HEAD = 0;

    /// <summary>
    /// 信息内容索引
    /// </summary>
    private const int CONTENT = 1;

    /// <summary>
    /// 收到的字节数组
    /// </summary>
    private byte[] m_bytes = null;
    public byte[] BYTES { get { return m_bytes; } }

    /// <summary>
    /// 发送的字符串
    /// </summary>
    private string m_content = "";

    /// <summary>
    /// 读取是否出现错误
    /// </summary>
    private bool m_errorRead = false;

    /// <summary>
    /// 是否进行数字签名
    /// </summary>
    private bool m_sum = true;

    /// <summary>
    /// 构造函数初始化
    /// </summary>
    public PostStream()
    {
        Headers = new Dictionary<string,string>();
        m_index = 0;
        m_bytes = null;
        m_content = "";
        m_errorRead = false;
    }

    //这个类的第一部分是将不同类型的数据按POST格式压入到m_content字符串和二位字符串数组m_field中。m_content中的数据时实际发送的数据,m_field中的数据用于MD5数字签名。
    #region 写入数据

    /// <summary>
    /// 开始压数据,issum参数用来标识是否进行MD5数字签名
    /// </summary>
    public void BeginWrite(bool issum)
    {
        m_index = 0;
        m_sum = issum;
        m_field = new string[MAX_POST, PAIR];
        Headers.Add("Content-Type", "application/x-www-form-urlencoded");
    }

    /// <summary>
    /// head表示POST的名字,content是实际的数据内容
    /// </summary>
    /// <param name="head"></param>
    /// <param name="content"></param>
    public void Write(string head, string content)
    {
        if (m_index >= MAX_POST) return;

        m_field[m_index, HEAD] = head;
        m_field[m_index, CONTENT] = content;

        m_index++;
        if (m_content.Length == 0)
            m_content += (head + "=" + content);
        else
            m_content += ("&" + head + "=" + content);
        Debug.LogError(m_content);
    }

    /// <summary>
    /// 使用MD5对字符串进行数字签名
    /// </summary>
    public void EndWrite()
    {
        if (m_sum)
        {
            string hasstring = "";
            for (int i = 0; i < MAX_POST; i++)
                hasstring += m_field[i, CONTENT];
            hasstring += m_secretKey; //数据: content1content2...m_secretKey
            Debug.LogError("hasstring=" + hasstring);
            m_content += "&key=" + Md5Sum(hasstring);//数据: head1=content1&head2=content2...&key=(hasstring的MD5值)
            Debug.LogError("m_content=" + m_content);
        }
        m_bytes = Encoding.UTF8.GetBytes(m_content);
    }

    #endregion

    //第二部分是读取从服务器返回的数据。从服务器返回的数据时一个单独的字节数组,我们将这个数组解析为相应的数据,这个过程用到了最多的是BitConverter函数,它可以将相应长度的字节转为对应的数据
    #region 读取数据

    /// <summary>
    /// 读取数据
    /// </summary>
    /// <param name="www"></param>
    /// <param name="issum"></param>
    /// <returns></returns>
    public bool BeginRead(WWW www,bool issum)
    {
        m_bytes = www.bytes;
        m_content = www.text;

        m_sum = issum;

        //错误
        if (m_bytes == null)
        {
            m_errorRead = true;
            return false;
        }

        //读取前2个字节,获得字符串长度
        short length = 0;
        this.ReadShort(ref length);//服务器这里做了处理,在写入数据时先写入一个short类型的数据代表数据长度
        if (length != m_bytes.Length)
        {
            m_index = length;
            m_errorRead = true;
            return false;
        }

        //比较本地与服务器数字签名是否一致
        if (m_sum)
        {
            byte[] localhash = GetLocalHash(m_bytes, m_secretKey);
            byte[] hashbytes = GetCurrentHash(m_bytes);
            if (!ByteEquals(localhash,hashbytes))
            {
                m_errorRead = true;
                return false;
            }
        }
        return true;
    }

    /// <summary>
    /// 忽略一个字节
    /// </summary>
    public void IgnoreByte()
    {
        if (m_errorRead) return;
        m_index += BYTE_LEN;
    }

    /// <summary>
    /// 读取一个字节
    /// </summary>
    public void ReadByte(ref byte bts)
    {
        if (m_errorRead) return;

        bts = m_bytes[m_index];
        m_index += BYTE_LEN;
    }

    /// <summary>
    /// 读取一个short
    /// </summary>
    /// <param name="number"></param>
    public void ReadShort(ref short number)
    {
        if (m_errorRead) return;
        number = System.BitConverter.ToInt16(m_bytes,m_index);
        m_index += SHORT16_LEN;
    }

    /// <summary>
    /// 读取一个int
    /// </summary>
    public void ReadInt(ref int number)
    {
        if (m_errorRead) return;
        number = System.BitConverter.ToInt32(m_bytes,m_index);
        m_index += INT32_LEN;
    }

    /// <summary>
    /// 读取一个float
    /// </summary>
    public void ReadFloat(ref float number)
    {
        if (m_errorRead) return;
        number = System.BitConverter.ToSingle(m_bytes, m_index);
        m_index += FLOAT_LEN;
    }

    /// <summary>
    /// 读取一个字符串
    /// </summary>
    public void ReadString(ref string str)
    {
        if (m_errorRead) return;

        short num = 0;
        ReadShort(ref num);

        str = Encoding.UTF8.GetString(m_bytes,m_index,(int)num);

        m_index += num;
    }

    /// <summary>
    /// 读取一个bytes数组
    /// </summary>
    /// <param name="bytes"></param>
    public void ReadBytes(ref byte[] bytes)
    {
        if (m_errorRead) return;
        short len = 0;
        ReadShort(ref len);

        //字节流
        bytes = new byte[len];
        for (int i = m_index; i < m_index + len; i++)
        {
            bytes[i - m_index] = m_bytes[i];
        }

        m_index += len;
    }

    /// <summary>
    /// 结束读取
    /// </summary>
    /// <returns></returns>
    public bool EndRead()
    {
        if (m_errorRead) return false;
        else return true;
    }

    #endregion

    /// <summary>
    /// 去掉服务器返回的数字签名,使用本地秘钥重新计算数字签名
    /// </summary>
    /// <returns></returns>
    public static byte[] GetLocalHash(byte[] bytes,string key)
    {
        //hash bytes
        byte[] hashbytes = null;

        int n = bytes.Length - HASHSIZE;

        if (n < 0) return hashbytes;

        //获得key的bytes
        byte[] keybytes = System.Text.ASCIIEncoding.ASCII.GetBytes(key);

        //创建用于hash的bytes
        byte[] getbytes = new byte[n + keybytes.Length];

        for (int i = 0; i < n; i++)
        {
            getbytes[i] = bytes[i];
        }

        keybytes.CopyTo(getbytes,n);

        System.Security.Cryptography.MD5 md5;
        md5 = System.Security.Cryptography.MD5CryptoServiceProvider.Create();

        return md5.ComputeHash(getbytes);
    }

    /// <summary>
    /// 获得从服务器返回的数字签名
    /// </summary>
    /// <param name="bytes"></param>
    /// <returns></returns>
    public static byte[] GetCurrentHash(byte[] bytes)
    {
        byte[] hashbytes = null;
        if (bytes.Length < HASHSIZE) return hashbytes;

        hashbytes = new byte[HASHSIZE];

        for (int i = bytes.Length - HASHSIZE; i < bytes.Length; i++)
        {
            hashbytes[i - (bytes.Length - HASHSIZE)] = bytes[i];
        }
        return hashbytes;
    }

    #region 比较两个bytes数组是否相等

    /// <summary>
    /// 比较两个bytes数组是否相等
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <returns></returns>
    public static bool ByteEquals(byte[] a,byte[] b)
    {
        if (a == null || b == null || a.Length != b.Length) return false;

        for (int i = 0; i < a.Length; i++)
        {
            if (a[i] != b[i]) return false;
        }

        return true;
    }

    #endregion

    #region 取字符串md5值

    /// <summary>
    /// md5值
    /// </summary>
    /// <param name="strToEncrypt">//数据: head1content1head2content2...m_secretKey</param>
    /// <returns></returns>
    public static string Md5Sum(string strToEncrypt)
    {
        byte[] bs = UTF8Encoding.UTF8.GetBytes(strToEncrypt);

        System.Security.Cryptography.MD5 md5;
        md5 = System.Security.Cryptography.MD5CryptoServiceProvider.Create();

        byte[] hashBytes = md5.ComputeHash(bs);

        string hashString = "";
        for (int i = 0; i < hashBytes.Length; i++)
        {
            hashString += System.Convert.ToString(hashBytes[i], 16).PadLeft(2,'0');
        }

        return hashString.PadLeft(32,'0');
    }

    #endregion
}


PHP版本数据流

PHP版本的代码与C#版本如出一辙,只是换成了PHP的语法:

<?php 
//PHPStream.php
define("BYTE",1);
define("SHORT",2);
define("INT",4);
define("FLOAT",4);
define("HASHSIZE",16);
define("PKEY",123456);


class PHPStream
{
    private $Key = "";
    public $bytes = "";
    public $Content = "";
    public $index = 0;
    public $ErrorRead = false;
    
    //开始写数据
    function  BeginWrite( $Key )
    {
        $this->index=0;
        $this->bytes="";
        $this->Content="";
        $this->ErrorRead=false;
        
        //total bytes length
        $this->WriteShort(0);//服务器这里在发送数据的时候会先去写入一个short,代表数据长度
        if ( strlen($Key) )
        {
            $this->Key=$Key;
        }
    }
    
    //写一个byte
    function WriteByte( $byte )
    {
        //$this->bytes.=pack('c',$byte);
        $this->bytes.=$byte;
        $this->index+=BYTE;
    }
    
    //写一个short
    function WriteShort( $number )
    {
        $this->bytes.=pack("v",$number);
        $this->index+=SHORT;
    }
    
    //写一个32位int
    function WriteInt( $number )
    {
        $this->bytes.=pack("V",$number);
        $this->index+=INT;
    }
    
    //写一个float
    function WriteFLOAT( $number )
    {
        $this->bytes.=pack("f",$number);
        $this->index+=FLOAT;
    }
    
    //写一个字符串
    function WriteString( $str )
    {
        $len=strlen($str);
        $this->WriteShort($len);
        
        $this->bytes.=$str;
        $this->index+=$len;
    }
    
    //写一组byte
    function WriteBytes( $bytes )
    {
        $len=strlen($bytes);
        $this->WriteShort($len);
        
        $this->bytes.=$bytes;
        $this->index+=$len;
    }
    
    function EndWrite()
    {
        //数字签名
        if ( strlen($this->Key)>0 )
        {
            $len=$this->index+HASHSIZE;
            $str=pack("v",$len);
            
            //猜测这里的bytes内部对应是0-1 2-3 4-5 6-7
            //猜测内部为键值对 $str[0] = $str[1]
            $this->bytes[0]=$str[0];//猜测为key值
            $this->bytes[1]=$str[1];//猜测为key值对应的value
            
            //获取md5值
            $hashbytes=md5($this->bytes.$this->Key,true);
            
            $this->bytes.=$hashbytes;
            
        }
        else 
        {
            $str=pack("v",$this->index);
            $this->bytes[0]=$str[0];
            $this->bytes[1]=$str[1];
        }
    }
    
    //开始读入数据
    function BeginRead( $Key )
    {
        $this->index=0;
        $this->bytes="";
        $this->Content="";
        $this->ErrorRead=false;
        
        if ( strlen($Key)>0 )//strlen检测字符串长度
        {
            $this->Key=$Key;
        }
    }
    
    //读取POST信息
    function Read( $head )
    {
        if( isset($_POST[$head]) )
        {
            $this->Content.=$_POST[$head];
            return $_POST[$head];
        }
        else
        {
            $this->ErrorRead=true;
        }
    }
    
    //结束读取
    function EndRead()
    {
        if ($this->ErrorRead) return false;
        
        if (strlen($this->Key)<1) return true;//如果不需要签名验证则将原本的PKEY改为空字符串
        
        //取得数字签名
        $hashkey="";
        if ( isset($_POST["key"]) ) $hashkey=$_POST["key"];
        else 
        {
            $this->ErrorRead=true;
            return false;
        }
        
        //重新计算数字签名
        $localhash=md5($this->Content.$this->Key);
        
        //比较数字签名
        if (strcmp($hashkey,$localhash)==0) return true;//strcmp检测两个字符串是否一致
        else
        {
            $this->ErrorRead=true;
            return false;
        }
        
    }
}
?>

三、数据传输测试

1.在Unity中创建一个C#脚本NetWorkManager.cs

在脚本中创建一个WWW实例,分别发送int、float、short和string类型的数据至服务器,服务器收到后再将这些数据返回给Unity,下面是C#代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NetworkManager : MonoBehaviour {

    public const string URL = "http://192.168.1.5:8088/StreamTest.php";

    private void Start()
    {
        StartCoroutine(Test());
    }

    IEnumerator Test()
    {
        PostStream poststream = new PostStream();

        int integer = 1000;
        float number = 8.99f;
        short small = 30;
        string txt = "编程其乐无穷";

        //开始写入数据并指定需要签名认证
        poststream.BeginWrite(true);

        //写入数据Content: integer=1000
        poststream.Write("integer",integer.ToString());

        //写入数据Content: integer=1000&number=8.99
        poststream.Write("number",number.ToString());

        //写入数据Content: integer=1000&number=8.99&short=30
        poststream.Write("short",small.ToString());

        //写入数据Content: integer=1000&number=8.99&short=30&string=编程其乐无穷
        poststream.Write("string",txt);

        //1.最终签名认证的数据Content: integer=1000&number=8.99&short=30&string=编程其乐无穷&key=c344b95687a03452d4bf479a89affb94 
        //解释: c344b95687a03452d4bf479a89affb94为“10008.9930编程其乐无穷123456”字符串的MD5值  
        //123456为用于签名的密码 其组成是由写入的数组+密码组成
        //2.最终非签名认证的数据Content: integer=1000&number=8.99&short=30&string=编程其乐无穷
        poststream.EndWrite();
                              
        //服务器Post请求
        WWW www = new WWW(URL,poststream.BYTES,poststream.Headers);

        yield return www;

        //无错误
        if (www.error != null)
        {
            Debug.LogError(www.error);
        }
        else//读取返回值
        {
            poststream = new PostStream();
            poststream.BeginRead(www, true);
            poststream.ReadInt(ref integer);
            poststream.ReadFloat(ref number);
            poststream.ReadShort(ref small);
            poststream.ReadString(ref txt);

            bool ok = poststream.EndRead();
            if (ok)
            {
                Debug.LogError(integer);
                Debug.LogError(number);
                Debug.LogError(small);
                Debug.LogError(txt);
            }
            else
            {
                Debug.LogError("error");
            }
        }
    }
}

2.服务器www目录创建StreamTest.php脚本代码如下:

<?php 
//StreamTest.php
header('Content-Type:text/html; charset=utf-8');
require_once("PHPStream.php");//引用PHPStream.php文件

//read
$stream=new PHPStream();
$stream->BeginRead(PKEY);//与客户端对应的数字签名密码
$integer=$stream->Read("integer");//从传入的数据中找到Key值为integer的Value
$number=$stream->Read("number");//从传入的数据中找到Key值为number的Value
$short=$stream->Read("short");//从传入的数据中找到Key值为short的Value
$str=$stream->Read("string");//从传入的数据中找到Key值为string的Value
$ok=$stream->EndRead();

if ($ok)
{
    //开始写入一个short: bytes=pack("v",0)
    //开始的index: index = 0+2
    $stream->BeginWrite(PKEY);

    //写入一个Int: bytes=pack("v",0)+pack("V",$integer)
    //当前index: index = 0 + 2 + 4
    $stream->WriteInt($integer);
    
    //写入一个Float: bytes=pack("v",0)+pack("V",$integer)+pack("f",$number)
    //当前index: index = 0 + 2 + 4 + 4
    $stream->WriteFloat($number);

    //写入一个Float: bytes=pack("v",0)+pack("V",$integer)+pack("f",$number)+pack("v",$short)
    //当前index: index = 0 + 2 + 4 + 4 + 2
    $stream->WriteShort($short);

    //写入一个String: bytes=pack("v",0)+pack("V",$integer)+pack("f",$number)+pack("v",$short)+[pack("v",strlen($str))+$str]
    //当前index: index = 0 + 2 + 4 + 4 + 2 + (2 + strlen($str))
    $stream->WriteString($str);

    //带有签名bytes 末尾加md5(bytes=pack("v",0)+pack("V",$integer)+pack("f",$number)+pack("v",$short)+[pack("v",strlen($str))+$str]) (无签名则不加)
    //带有签名index: index = 0 + 2 + 4 + 4 + 2 + (2 + strlen($str)) + 16 (无签名就去掉+16)
    $stream->EndWrite();
    
    echo $stream->bytes;
}
else
{
    echo "error";
}
?>

结果如下:

在这里插入图片描述

这里需要注意一个问题,自定义数据类写入过程和读取过程顺序必须一致,否则无法获取数据。

PHP中的pack与unpack的方法将数据转换为二进制的方法最好了解下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/269114.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RHCE9学习指南 第8章 用户管理

8.1 基本概念 用户在操作系统中是非常重要的一块&#xff0c;我们登录系统&#xff0c;访问共享文件夹等都需要用户进行验证。所以&#xff0c;掌握管理用户的知识是非常有必要的。 说到用户&#xff0c;我们会提到两个名词账户信息和木马信息。 账户信息&#xff1a;我们可以…

新零售模式:重新定义商业未来

随着科技的飞速发展&#xff0c;我们的生活方式正在经历着前所未有的变革。其中&#xff0c;新零售模式正逐渐成为商业领域的新热点&#xff0c;它正在重新定义我们的购物方式&#xff0c;并为企业带来更多的商业机会。 一、新零售模式概述 新零售模式是指将互联网、大数据、…

阿里云服务器记录

阿里云服务器记录 CentOS 8.4 64位 SCC版 CentOS 7.9 64位 SCC版 CentOS 7.9 64位 CentOS 7.9 64位 UEFI版 Alibaba Cloud Linux Anolis OS CentOS Windows Server Ubuntu Debian Fedora OpenSUSE Rocky Linux CentOS Stream AlmaLinux 阿里云服务器有个scc版&#xff0c;这个…

用CHAT了解更多知识点

问CHAT&#xff1a;什么是硅基生命和碳基生命&#xff1f; CHAT回复&#xff1a;硅基生命和碳基生命是两种理论性的生物体类型&#xff0c;这些生物体主要是由硅或碳元素以及其他元素构成的。 碳基生命是我们当前所熟知的生命形式。碳元素能够形成稳定且复杂的分子&#xff0c;…

Java网络爬虫拼接姓氏,名字并写出到txt文件(实现随机取名)

目录 1.爬取百家姓1.爬取代码2.爬取效果 2.爬取名字1.筛选男生名字2.筛选女生名字 3.数据处理&#xff08;去除重复&#xff09;4.拼接数据5.将数据写出到文件中 1.爬取百家姓 目标网站&#xff0c;仅作为实验目的。 ①爬取姓氏网站&#xff1a; https://hanyu.baidu.com/shic…

【Linux系统基础】(5)在Linux上集群化环境前置准备及部署Zookeeper、Kafka软件详细教程

集群化环境前置准备 介绍 在前面&#xff0c;我们所学习安装的软件&#xff0c;都是以单机模式运行的。 后续&#xff0c;我们将要学习大数据相关的软件部署&#xff0c;所以后续我们所安装的软件服务&#xff0c;大多数都是以集群化&#xff08;多台服务器共同工作&#xf…

python使用apscheduler定时任务,固定周几运行程序

在add_job中添加参数day_of_week即可&#xff1a; day_of_week "0"表示&#xff1a;只有周一运行day_of_week "0-4"表示&#xff1a;周一到周五运行day_of_week "0,1,2"表示&#xff1a;周一二三运行 示例程序 from datetime import datet…

C++ boost planner_cond_.wait(lock) 报错1225

1.如下程序段 boost unique_lock doesn’t own the mutex: Operation not permitted 问题&#xff1a; 其中makePlan是一个线程。这里的unlock导致错误这个报错 boost unique_lock doesn’t own the mutex: Operation not permitted bool navigation::makePlan(){ //cv::named…

MySQL中如何快速定位占用CPU过高的SQL

作为DBA工作中都会遇到过数据库服务器CPU飙升的场景&#xff0c;我们该如何快速定位问题&#xff1f;又该如何快速找到具体是哪个SQL引发的CPU异常呢&#xff1f;下面我们说两个方法。聊聊MySQL中如何快速定位占用CPU过高的SQL。 技术人人都可以磨炼&#xff0c;但处理问题的思…

JVM内存结构Java内存模型Java对象模型

悟空老师思维导图&#xff1a;https://naotu.baidu.com/file/60a0bdcaca7c6b92fcc5f796fe6f6bc9https://naotu.baidu.com/file/60a0bdcaca7c6b92fcc5f796fe6f6bc9 1.JVM内存结构&&Java内存模型&&Java对象模型 1.1.JVM内存结构 1.2.Java对象模型 Java对象模型…

顺序表的实现

目录 一. 数据结构相关概念​ 二、线性表 三、顺序表概念及结构 3.1顺序表一般可以分为&#xff1a; 3.2 接口实现&#xff1a; 四、基本操作实现 4.1顺序表初始化 4.2检查空间&#xff0c;如果满了&#xff0c;进行增容​编辑 4.3顺序表打印 4.4顺序表销毁 4.5顺…

酷开系统千屏千面,深度探索消费者喜好

为什么大家这么喜欢用酷开系统呢&#xff1f;当然是因为它好用啊&#xff01;酷开系统基于人工智能技术&#xff0c;为消费者提供个性化的服务。它具有“千人千面”的推荐特性&#xff0c;即根据消费者的需求和喜好&#xff0c;自动生成个性化的内容推荐和界面布局。 01.更智能…

pngPackerGUI是一款免费的图集打包工具,png图片打包plist工具

pngPackerGUI是一款免费的图集打包工具&#xff0c;png图片打包plist工具 手把手教你使用pngPackerGUI_V2.0此软件是在pngpacker_V1.1软件基础之后&#xff0c;开发的界面化操作软件&#xff0c;方便不太懂命令行的小白快捷上手使用。1.下载并解压缩软件&#xff0c;得到如下目…

Python桌面开发技术 PyQt6教程专栏 一周快速上手,开发桌面应用

大家好&#xff0c;我是python222小锋老师。 近日锋哥又卷了一波课程&#xff0c;Python桌面开发技术 PyQt6教程&#xff0c;文字版视频版。一周掌握。 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(…

LeetCode刷题--- 字母大小写全排列

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏 力扣递归算法题 http://t.csdnimg.cn/yUl2I 【C】 http://t.csdnimg.cn/6AbpV 数据结构与算法 http://t.csdnimg.cn/hKh2l 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回…

cesium实现二三维联动

记录项目中实现二三维地图联动 效果如下&#xff1a; 第一步&#xff1a;现在页面中加载二三维地图&#xff08;地图的初始化已省略&#xff09; <template><div><div><button click"show">二三维联动</button></div><div&…

css学习笔记7(浮动)

css学习笔记7&#xff08;浮动&#xff09; 六、浮动1.浮动的简介2.元素浮动后的特点3.浮动影响3.1浮动后会有哪些影响3.2浮动后会有哪些影响 4.浮动布局练习 六、浮动 1.浮动的简介 ​ 在最初&#xff0c;浮动是用来实现文字环绕图片效果的&#xff0c;现在浮动是主流的页面…

VR全景对普通人的生活有哪些好处?

许多普通人对VR全景还全然没有概念&#xff0c;这是因为VR全景虽然一直在快速发展&#xff0c;但目前为止也不过几年而已&#xff0c;但这发展的几年同样为我们普通人的生活带来了切实的改变和便利。VR全景技术为人们带来了沉浸感和真实感的体验&#xff0c;让我们感受到迥异于…

C# SqlSugar 数据库 T4模板

生成效果 模板代码 <# template debug"false" hostspecific"true" language"C#" #> <# output extension".cs" #> <# assembly name"System.Core" #> <# assembly name"System.Data" #>…

图片素材管理软件Eagle for mac提高素材整理维度

Eagle for mac是一款图片素材管理软件&#xff0c;支持藏网页图片&#xff0c;网页截屏&#xff0c;屏幕截图和标注&#xff0c;自动标签和筛选等功能&#xff0c;让你设计师方便存储需要的素材和查找&#xff0c;提供工作效率。 Eagle mac软件介绍 Eagle mac帮助你成为更好、…