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);elsem_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_secretKeyDebug.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 bytesbyte[] hashbytes = null;int n = bytes.Length - HASHSIZE;if (n < 0) return hashbytes;//获得key的bytesbyte[] keybytes = System.Text.ASCIIEncoding.ASCII.GetBytes(key);//创建用于hash的bytesbyte[] 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;}}//写一个bytefunction WriteByte( $byte ){//$this->bytes.=pack('c',$byte);$this->bytes.=$byte;$this->index+=BYTE;}//写一个shortfunction WriteShort( $number ){$this->bytes.=pack("v",$number);$this->index+=SHORT;}//写一个32位intfunction WriteInt( $number ){$this->bytes.=pack("V",$number);$this->index+=INT;}//写一个floatfunction 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;}//写一组bytefunction 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=1000poststream.Write("integer",integer.ToString());//写入数据Content: integer=1000&number=8.99poststream.Write("number",number.ToString());//写入数据Content: integer=1000&number=8.99&short=30poststream.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的方法将数据转换为二进制的方法最好了解下。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华清远见嵌入式学习——ARM——作业4

作业要求&#xff1a; 代码运行效果图&#xff1a; 代码&#xff1a; do_irq.c: #include "key_it.h" extern void printf(const char *fmt, ...); unsigned int i 0;//延时函数 void delay(int ms) {int i,j;for(i0;i<ms;i){for(j0;j<2000;j);} }void do_i…

基于AR+地图导航的景区智慧导览设计

随着科技的飞速发展&#xff0c;智慧旅游已经成为现代旅游业的一个重要趋势。在这个背景下&#xff0c;景区智慧导览作为智慧旅游的核心组成部分&#xff0c;正逐渐受到越来越多游客的青睐。本文将深入探讨地图导航软件在景区智慧导览中的应用&#xff0c;并分析其为游客和景区…

【JavaWeb学习笔记】18 - 文件上传下载

项目代码 https://github.com/yinhai1114/JavaWeb_LearningCode/tree/main/fileupdown 目录 文件上传 一、基本介绍 二、文件上传的基本原理 ​编辑 三、文件上传应用实例 四、文件上传的注意细节 1.解决中文乱码问题 2.分割文件夹 3.防止重名 4.百度WebUploader 5.空…

redis 从0到1完整学习 (七):ZipList 数据结构

文章目录 1. 引言2. redis 源码下载3. zipList 数据结构3.1 整体3.2 entry 数据结构分析3.3 连锁更新 4. 参考 1. 引言 前情提要&#xff1a; 《redis 从0到1完整学习 &#xff08;一&#xff09;&#xff1a;安装&初识 redis》 《redis 从0到1完整学习 &#xff08;二&am…

常用API:Object

Object Object类的作用&#xff1a; Object类是Java中所有类的祖宗类&#xff0c;因此&#xff0c;Java中所有的对象都可以直接使用Object类中提供的一些方法。 Object类的常见方法 方法名说明public String toString&#xff08;&#xff09;返回对象的字符串表示形式publi…

kubernetes -pod 实践

一、资源与对象 1、pod 容器都是由镜像启动的,但在容器外面会包裹通过Pod将容器包裹起来这个是K8s的概念,在这个Pod里面可以有一个或多个容器,那这个Pod的有什么特征呢 Pod里的所有容器都会调度在同一个节点上运行0。Pod中的所有容器会共享同一网络,它们有一个唯一的IP,…

PDF编辑工具--Acrobat Pro DC 2023中文

Acrobat Pro DC 2023是一款功能强大的PDF编辑和管理软件&#xff0c;它可以帮助用户在创建、编辑、转换和共享PDF文档方面达到前所未有的高度。这款软件提供了丰富的编辑功能&#xff0c;使用户能够轻松添加注释、高亮、下划线、插入文本等&#xff0c;自由地编辑PDF文档。除了…

枚举的使用

背景以及定义 枚举是在jdk1.5以后引入的.主要用途是:将常量组织起来,在这之前表示一组常量通常使用定义常量的方式: public static final int RED 1; public static final int GREEN 2; public static final int BLUE 3; 但是常量举例有不好的地方,例如:可能碰巧有一个数字…

【Web API系列】使用getDisplayMedia来实现录屏功能

文章目录 前言一、认识getD该处使用的url网络请求的数据。二、使用步骤1.使用方法一实现录屏2.使用方法二实现录屏3. 运行效果 延伸 前言 Web API经过长期的发展&#xff0c;尤其是最近&#xff0c;发展相当迅猛&#xff0c;现在已经支持很多功能了&#xff0c;一些原生就支持…

[Linux]——彻底学通权限

学习权限 一、权限概念二、权限管理2.1文件访问者分类&#xff08;人&#xff09;2.2文件类型和访问权限&#xff08;事物的属性&#xff09;2.3 文件访问权限的相关设置方法 三、目录的权限3.1、进入目录的权限3.2、粘滞位 四、关于权限的总结 一、权限概念 Linux下有两种用户…

【AI】Langchain-Chatchat搭建本地知识库-未完,先记录踩的坑

事先说一下&#xff0c;我本地的显卡4070只有12G显存&#xff0c;无法运行本地知识库&#xff0c;我把自己折腾的过程和遇到的坑先记录一下吧&#xff0c;后续如果有算力的话就再跑一遍试试。后续来了&#xff1a;【AI】使用阿里云免费服务器搭建Langchain-Chatchat本地知识库 …

阿赵UE学习笔记——4、新建关卡

阿赵UE学习笔记目录 大家好&#xff0c;我是阿赵。   之前介绍了虚幻引擎的常用窗口功能&#xff0c;这次开始创建游戏内的世界了。首先先从创建关卡开始。 一、创建新关卡 在使用UE引擎制作游戏&#xff0c;首先要有一个场景作为基础&#xff0c;这个场景在UE里面成为关卡。…

带你认识 WIDGET、WINDOW 、FRAME和 FRAMEGROUP

1、概述 在 YonBuilder 移动开发中&#xff0c;开发者需要了解一些常用的概念术语&#xff0c;其中和App整体框架结构及页面层级组成相关最重要几个重要概念&#xff0c;就是 Widget、 Window 和 Frame、frameGroup。掌握了这几个概念&#xff0c;对于开发者开发 App 时的 API…

linux离线安装jdk11

1. 下载java11&#xff0c;Java Downloads | Oracle 2. 文件安装 jdk11&#xff1a; mkdir /usr/lib/jvm/ 将下载的文件&#xff0c;解压到/usr/lib/jvm/下 tar -zxf jdk-11.0.21_linux-x64_bin.tar.gz -C /usr/lib/jvm/ 3. 将以下命令写入bashrc文件 export JAVA_HOME/us…

26、湾湾国立阳明交通大学、湾湾长庚纪念医院提出:ALL Attention U-Net,独属头部CT分割的[玛格丽特]

本文由台湾国立阳明交通大学、台湾长庚纪念医院于2023年12月16日在arXiv<Image and Video Processing>发表。 论文地址&#xff1a; 2312.10483.pdf (arxiv.org) 0、Abstract 脑出血在 Head CT扫描中作为第一线工具&#xff0c;帮助专家诊断不同类型的出血。然而&…

Tomcat面试题(10道含答案),由浅入深

请解释Tomcat的基本概念和作用 Tomcat是一个开源的Java Web服务器和Servlet容器&#xff0c;用于提供基于Java的应用程序运行环境。它支持Java Servlet规范&#xff0c;使得开发者能够快速构建和部署基于Web的应用程序。 请描述Tomcat的目录结构&#xff0c;并解释各个目录的…

cmd启动Java项目提示:jar中没有主清单属性

1、问题 2、原因 在IDEA中开发SpringBoot项目并打成jar包&#xff0c; 需要添加springboot打包插件&#xff0c;如果不添加&#xff0c;仅仅用maven进行打包&#xff0c;打成包里面是少文件的。 <build><plugins><!--springboot打包插件--><plugin>&…

ElasticSearch之RestClient笔记

1. ElasticSearch 1.1 倒排索引 1.2 ElasticSearch和Mysql对比 1.3 RestClient操作 导入依赖 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.15.…

枚举(蓝桥杯备赛系列)acwing版

枚举 前言 hello&#xff0c;大家好&#xff0c;前面一段时间已经是把acwing Linux基础课讲完了&#xff0c;其实那些内容完全可以带领小白入门Linux我说过如果有人留言要Linux和Windows server 配置DNS Web ftp 的内容我就做一期&#xff0c;但是没人留言我也就先不自作多情了…

【HeyGen】让照片开口说话 —— 登录和使用:详细指南!

【HeyGen】让照片开口说话 关于HeyGen登录HeyGenStep1. 点击”免费开始使用“Step2. 账号注册Step3. 再次点击”Get Started“Step4. 常规个人信息调查Step5. 登录成功 使用HeyGenStep1. 上传Avatar&#xff08;头像&#xff09;Step2. 选定Avatar&#xff08;头像&#xff09;…