Mysql FAQ

隐式类型转换

例如 pid 字段类型是 int,我们 where pid = “1”,这样就会触发隐式类型转换。字段为数字类型,字符串数字转换成数字时,并不会导致索引失效如下图

当where查询操作符**左边为字符类型**时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。

name为字符串类型,传入了数字类型001 转换后索引失效

导致查询结果不符合预期

类型隐式转换规则

当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容,某些转换是隐式发生的

如果不与数字比较,则将十六进制值视为二进制字符串


如果参数之一是a TIMESTAMP或 DATETIME column,而另一个参数是常量,则在执行比较之前,该常量将转换为时间戳。这样做是为了使ODBC更友好。对于的参数,此操作未完成 IN()。为了安全起见,在进行比较时请始终使用完整的日期时间,日期或时间字符串。例如,要在BETWEEN与日期或时间值一起使用时获得最佳结果 ,请使用CAST()将值显式转换为所需的数据类型。


一个或多个表中的单行子查询不被视为常量。例如,如果子查询返回要与DATETIME 值进行比较的整数,则比较将作为两个整数进行。整数不转换为时间值。要将操作数作为DATETIME值进行比较 ,请使用 CAST()将子查询值显式转换为DATETIME


如果参数之一是十进制值,则比较取决于另一个参数。如果另一个参数是十进制或整数值,则将参数作为十进制值进行比较;如果另一个参数是浮点值,则将参数作为浮点值进行比较。


在所有其他情况下,将参数作为浮点数(实数)进行比较。例如,将字符串和数字操作数进行比较,将其作为浮点数的比较。

条件的由字符转为浮点时候

不以数字开头的字符串都将转换为0。如’abc’、’a123bc’、’abc123’都会转化为0;
以数字开头的字符串转换时会进行截取,从第一个字符截取到第一个非数字内容为止。比如’123abc’会转换为123,’012abc’会转换为012也就是12,’5.3a66b78c’会转换为5.3,其他同理。

避免规则,通过程序处理,尽量使类型与数据库字段保持一致

参考

mysql-8.隐式转换导致索引失效或查出不符合where条件结果

类型转换官方文档

Laravel多态关联的应用案例

多对多 多态

需求如图用户关注不同的实体
用户表
专家表
主播表
作者表
关注中间表
//用户模型
class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens, DateFormat, Filterable;

    protected $table = 'users';
    
    //关注的用户
    public function followUser()
    {
        return $this->morphedByMany($this, 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的专家
    public function followExpert()
    {
        return $this->morphedByMany('App\Models\Expert', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的主播
    public function followAnchors()
    {
        return $this->morphedByMany('App\Models\Anchors', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注的作者
    public function followAuthors()
    {
        return $this->morphedByMany('App\Models\Authors', 'followable', 'followable', 'user_id', 'followable_id')
            ->withTimestamps();
    }
    //关注者,即主动关注用户的人
    public function followerUser()
    {
        return $this->morphToMany($this, 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//专家模型
class Expert extends BaseModel
{
    protected $table = 'expert';
    //关注者,即主动关注专家的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//主播模型
class Anchors extends BaseModel
{
    protected $table = 'anchors';
    //关注者,即主动关注主播的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//作者模型
class Authors extends BaseModel
{
    protected $table = 'authors';
    //关注者,即主动关注作者的人
    public function followerUser()
    {
        return $this->morphToMany('App\Models\User', 'followable', 'followable', 'followable_id', 'user_id')
            ->withTimestamps();
    }
}
//控制器代码
class FollowController extends Controller
{
  //关注/取消
  public function toggle(Request $request)
 {
    $request->validate([
         'followable_id'=>'bail|required|integer',
        'followable_type'=>'bail|required|string|in:user,expert,authors,anchors'
    ]);
    //利用可变方法动态创建模型对象
    $type_model = "\App\Models\\".ucfirst($request->input('followable_type'));
    $follow = $type_model::findOrFail($request->input('followable_id'));
    //调用关注人关联和toggle方法动态切换关注和取消关注
    $result = $follow->followerUser()->toggle($request->user()->id);

    if ($result['attached']) {
        $status = 1;
    } else {
        $status = 0;
    }

    return response()->json(compact('status'));
 }

//关注列表
public function index(Request $request)
 {
    $request->validate([
      'followable_type'=>'bail|required|string|in:user,expert,authors,anchors'
    ]);
    //根据类型可变方法动态加载我的关注关联,实现4个类型列表
    $followable_funname = 'follow'.ucfirst($request->input('followable_type'));
    $builder = $request->user()->$followable_funname();

    if ($request->input('followable_type') == 'user') {
        $builder->select('users.id', 'name', 'avatar');
    } elseif ($request->input('followable_type') == 'expert') {
        $builder->select('expert.id', 'name', 'nickname', 'photo', 'tags')
                ->where('is_show', 1);
    } elseif ($request->input('followable_type') == 'authors') {
        $builder->select('authors.id', 'authors.user_id', 'introduction')
                ->with([
                    'user'=>function ($query) {
                        $query->select('id', 'name', 'avatar');
                    }
                ])
                ->where('status', 1);
    } elseif ($request->input('followable_type') == 'anchors') {
        $builder->select('anchors.id', 'anchors.user_id', 'games_type')
                ->with([
                    'user'=>function ($query) {
                        $query->select('id', 'name', 'avatar');
                    }
                ])
                ->where('status', 1);
   }
   //按关注时间排序
   $followed = $builder->orderBy('followable.created_at', 'desc')
           ->forPage($request->input('page', 1), $request->input('per_page', 1))
           ->get();

   return $followed;
}

}

这种抽象的写法可以减少接口数量,要看具体需求是否合适

Linux安装PHP FAQ

离线安装

很多国企和大型企业的自建机房不允许访问公网,流量只能进开发固定端口,造成安装LNMP环境时缺少各种依赖和报错。以centos系统为例

1.上传和服务器版本一致的操作系统镜像,(例:CentOS-7-x86_64-Everything-2009.iso,一定要使用everything版本包含软件包) 到服务器

2.挂载光盘镜像

mkdir /mnt/dvd //创建挂载目录
mount -o loop /xxx/CentOS-7-x86_64-Everything-2009.iso /mnt/dvd  //挂载,xxx为镜像所在目录
df -h  //查看 /mnt/dvd 挂载点是否挂载成功

3.配置yum本地源

mkdir /etc/yum.repos.d/bk //创建备份目录
mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/bk //备份源文件

vim /etc/yum.repos.d/CentOS-Media.repo  //编辑本地源
//centos6/7内容如下,file路径为上一步的镜像挂载
[local-media]
name=CentOS-$releasever-Media
baseurl=file:///mnt/dvd/
gpgcheck=1
enabled=1
gpgkey=file:///mnt/dvd/RPM-GPG-KEY-CentOS-7

//CentOS 8本地源配置文件写法与CentOS6和7不同,配置文件内容如下:
[LocalRepo_BaseOS]
name=LocalRepository_BaseOS
baseurl=file:///mnt/dvd/BaseOS
enabled=1
gpgcheck=0

[LocalRepo_AppStream]
name=LocalRepository_AppStream
baseurl=file:///mnt/dvd/AppStream
enabled=1
gpgcheck=0

//保存然后执行
yum clean all && yum makecache

//测试一下是否可用
yum install git

4 执行离线安装

使用oneinstack

一台服务器(可以使用系统版本系统的虚拟机替代)联网使用oneinstack安装后,src目录里会有相应的包,打包oneinstack 放到需要离线安装的服务器(配置好内网yum源)就是离线了。

使用lnmp一键安装包,需要下载完整包

执行CheckMirror=n ./install.sh lnmp

5 其它参考

https://blog.51cto.com/u_1836630/2417675

Linux编译安装相关知识

linux查看某个库是否安装

https://blog.csdn.net/vantian/article/details/76636327

ldconfig -p | grep pcap

Laravel项目迁移时,文件存储软链接更新问题

使用laravel本地存储文件时,通常存放到public磁盘,即根目录下storage/app/public

执行命令 创建软链接后才可以通过公共访问文件

php artisan storage:link

laravel 7以上自定义软链接目录

项目迁移更换服务器后,磁盘的挂载目录变了,导致软链接路径不能生效了需要更新软连接

方法一

通过ln命令更新

ln –snf  [新的源文件或目录]  [目标文件或目录]

方法二

使用php artisan storage:link命令重新生成,首先要删除原先的软链接目录,删除软链接时一定要注意路径后边不能带/

cd /laravel根目录/public
rm -rf storage 正确的删除软链接方式
rm -rf storage/ 错误的删除软链接方式,会删除软链接对应的真实目录导致文件丢失

PHP json函数遇到的字符编码问题

参考

json_decode produces error “Single unpaired UTF-16 surrogate in unicode escape” and returns null

PHP json_decode does not work with single unpaired surrogate caused by Node 12 well-formed JSON.stringify

php json函数只能支持utf-8编码

在调取其它语言接口时,json_decode报错Single unpaired UTF-16 surrogate in unicode escape

解决方法一

正则替换掉无效\u开头的unicode字符,然后json_decode

 $contents = preg_replace('/(?<!\\\)\\\u[a-f0-9]{4}/iu', '', $contents);
 $json = json_decode($contents, true);

解决方法二

自定义json_decode方法

 public function customJsonDecode($json)
    {
        $comment = false;
        $out = '$x=';

        for ($i = 0; $i < strlen($json); $i++)
        {
            if (!$comment)
            {
                if (($json[$i] == '{') || ($json[$i] == '[')) $out .= ' array(';
                else if (($json[$i] == '}') || ($json[$i] == ']')) $out .= ')';
                else if ($json[$i] == ':') $out .= '=>';
                else
                    $out .= $json[$i];
            }
            else
                $out .= $json[$i];
            if ($json[$i] == '"' && $json[($i - 1)] != "\\")
                $comment = !$comment;
        }

        eval($out . ';');

        return $x;
    }

Laravel 响应文件

以实时响应二维码为例

public function qrcode($activity_id, $invite)
    {   
        //二维码内容
        $activity_url = config('app.activity_url') . '?' . http_build_query(['id' => $activity_id, 'invite' => $invite]);
        //二维码图片文件的二进制数据
        $qrcodeStr = Qrcode::format('png')->size(80)->generate($activity_url);
        //直接响应文件,并设置对应的响应头
        return response($qrcodeStr)->withHeaders([
            'Access-Control-Allow-Origin' => '*',
            'Access-Control-Allow-Headers' => '*',
            'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
            'Content-Type' => 'image/png;charset=utf-8',
            'Content-Disposition' => 'inline;filename="' . $invite . '.png"'
        ]);
    }

框架实现原理,如上图所示,使用Symfony 组件响应字符串数据,就相当于原生php的

header(....);//设置文件响应头
echo $bindata;//输出文件的二进制数据字符串

Laravel 在SLB下获取客户端IP和正确加载Https访问样式

如果项目部署在云服务负载均衡下,会导致无法获取正确的客户端IP地址

解决方案

参考文档https://learnku.com/docs/laravel/8.x/requests/9369#453bc9

修改 App\Http\Middleware\TrustProxies 中间件 信任所有代理#

如果你使用 Amazon AWS 或其他的「云」的负载均衡服务,你可能不知道负载均衡器的实际 IP 地址。在这种情况下,你可以使用 * 来信任所有代理:
/**
 * 此应用的信任代理
 *
 * @var string|array
 */
protected $proxies = '*';

上述配置还可以解决,telescope和horizon在SLB下通过https访问无法正确加载样式和js问题

获取IP其它解决方案

参考https://help.aliyun.com/document_detail/54007.html?spm=a2c4g.11186623.6.933.5b203253t8UrIO

背景信息

七层负载均衡(HTTP/HTTPS协议)服务需要对应用服务器进行配置,然后使用X-Forwarded-For的方式获取客户端的真实IP地址。真实的客户端IP存放在HTTP头部的X-Forwarded-For字段,格式如下:

X-Forwarded-For: 用户真实IP, 代理服务器1-IP, 代理服务器2-IP,...

当使用此方式获取客户端真实IP时,获取的第一个地址就是客户端真实IP。

配置Nginx服务器

  1. 执行如下命令,安装http_realip_module。 wget http://nginx.org/download/nginx-1.0.12.tar.gz tar zxvf nginx-1.0.12.tar.gz cd nginx-1.0.12 ./configure --user=www --group=www --prefix=/alidata/server/nginx --with-http_stub_status_module --without-http-cache --with-http_ssl_module --with-http_realip_module make make install kill -USR2 `cat /alidata/server/nginx/logs/nginx.pid` kill -QUIT `cat /alidata/server/nginx/logs/ nginx.pid.oldbin`
  2. 执行如下命令,打开nginx.conf文件。vi /alidata/server/nginx/conf/nginx.conf
  3. 在以下配置信息后添加新的配置字段和信息。 fastcgi connect_timeout 300; fastcgi send_timeout 300; fastcgi read_timeout 300; fastcgi buffer_size 64k; fastcgi buffers 4 64k; fastcgi busy_buffers_size 128k; fastcgi temp_file_write_size 128k;需要添加的配置字段和信息为: set_real_ip_from IP_address; real_ip_header X-Forwarded-For;说明 如果您要获取代理服务器的地址,可以将代理服务器的网段添加到set_real_ip_from <IP_address>,如负载均衡的IP地址段100.64.0.0/10(100.64.0.0/10 是阿里云保留地址,其他用户无法分配到该网段内,不会存在安全风险)和高防IP地址段。多个IP地址段用逗号分隔。
  4. 执行如下命令,重启Nginx。/alidata/server/nginx/sbin/nginx -s reload

然后获取请求头

//原生
$_SERVER['X-Forwarded-For'];
//laravel
$request->header('X-Forwarded-For');

HTTP监听访问正常但是HTTPS监听访问网址不加载样式

https://help.aliyun.com/document_detail/178368.htm?spm=a2c4g.11186623.2.17.116f7503fHVPRT

【漏洞预警】Laravel <= 8.4.2 Debug模式 _ignition 远程代码执行漏洞

https://help.aliyun.com/noticelist/articleid/1060782748.html

2021年1月13日,阿里云应急响应中心监控到国外某安全研究团队披露了Laravel <= 8.4.2 存在远程代码执行漏洞。

漏洞描述

Laravel 是一个免费的开源 PHP Web 框架,旨在实现的Web软件的MVC架构。2021年1月13日,阿里云应急响应中心监控到国外某安全研究团队披露了 Laravel <= 8.4.2 存在远程代码执行漏洞。当Laravel开启了Debug模式时,由于Laravel自带的Ignition功能的某些接口存在过滤不严,攻击者可以发起恶意请求,通过构造恶意Log文件等方式触发Phar反序列化,从而造成远程代码执行,控制服务器。漏洞细节已在互联网公开。阿里云应急响应中心提醒 Laravel 用户尽快采取安全措施阻止漏洞攻击。

影响版本

Laravel 框架 < 8.4.3

facade ignition 组件 < 2.5.2

安全版本

Laravel 框架 >= 8.4.3

facade ignition 组件 >= 2.5.2

安全建议

建议将 Laravel 框架升级至8.4.3及其以上版本,或者将 facade ignition组件升级至 2.5.2 及其以上版本。

相关链接

https://github.com/facade/ignition/pull/334

https://www.ambionics.io/blog/laravel-debug-rce

字符编码知识 乱码原因

参考

字符编码基础知识其一

字符编码笔记:ASCII,Unicode 和 UTF-8 ——阮一峰

分享一下我所了解的字符编码知识

字符编码详解及由来(UNICODE,UTF-8,GBK)

字符编码详解(基础) ——PHP鸟哥

文件和字符编码

编码方式之ASCII、ANSI、Unicode概述

字节(Byte或byte):计算机系统中用于计量存储容量的一种计量单位, 1B=8bit

字符(Character)是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等,但是一个字符在计算机中占用多少字节是与编码方式有关的,不同的编码方式占用的内存不一样。例如:标点符号+是一个字符,汉字我们是两个字符,在GBK编码中一个汉字占2个字节,在UTF-8编码中一个汉字占3个字节。

mysql(utf8mb4编码)中varchar(255) 255是字符长度不需要考虑不同字符(中文,英文)字节占用问题这是mysql底层处理的

字节字符有什么联系和区别呢?简单来说字节是计算机存储和操作的最小单位,字符是人们阅读的最小单位;字节是存储(物理)概念,字符是逻辑概念;字节代表数据(内涵和本质),字符代表其含义字符由字节组成
举几个例子说明两者区别:“中国”包含2个字符,GBK编码表示需要4个字节,UTF-8编码需要6个字节;数字“1234567890”,包含10个字符,用int32类型表示只需4个字节

编码规范 随着计算机的普及,人们希望能在计算机中显示字符,但是计算机只能显示0和1这样的二进制数,为了显示字符,国际组织就制定了编码规范,希望使用不同的二进制数来表示代表不同的字符,这样电脑就可以根据二进制数来显示其对应的字符。所谓字符集其实就是一套编码规范中的子概念,所以我们通常就称呼其为XX编码,XX字符集。例如:GBK 编码规范,根据这套编码规范,计算机就可以在中文字符和二进制数之间相互转换。而使用GBK编码就可以使计算机显示中文字符。

字库表 一套编码规范不一定包含世界上所有的字符,每套编码规范都有自己的使用场景,而字库表就存储了某种编码规范中能显示的所有字符,计算机就是根据二进制数字库表中找到与之对应的字符然后显示给用户的字库表相当于一个存储字符的数据库。例如:几乎所有汉字都保存在GBK 编码规范的字库表中。所以可以显示汉字,但法语,俄语并不在其字库表中,所以使用GBK编码的文档不能正常显示法语,俄语等不包含在其字库表中的字符。

编码字符集(字符集)在一个字库表中,每一个字符都有一个对应的二进制地址,而编码字符集就是这些地址的集合。字符集定义了字符和二进制的对应关系,为每个字符分配了唯一的编号。可以将字符集理解成一个很大的表格,它列出了所有字符和二进制的对应关系,计算机显示文字或者存储文字,就是一个查表的过程

字符编码(编码方式 )而字符编码规定了如何将字符的编号存储到计算机中,如果使用了类似 GB2312 和 GBK 的变长存储方案(不同的字符占用的字节数不一样),那么为了区分一个字符到底使用了几个字节,就不能将字符的编号直接存储到计算机中,字符编号在存储之前必须要经过转换,在读取时还要再逆向转换一次,这套转换方案就叫做字符编码。

字符集和字符编码的关系

通常特定的字符集采用特定的编码方式(即一种字符集对应一种字符编码(例如:ASCII、IOS-8859-1、GB2312、GBK,都是即表示了字符集又表示了对应的字符编码,但Unicode不是,它采用现代的模型)),因此基本上可以将两者视为同义词
字符和字符编码的异同可参见:https://www.cnblogs.com/lanhaicode/p/11214827.html

粗略总结

  • 解码过程:一个较短的二进制数,通过一种编码方式,转换成编码字符集中正常的地址,然后在字库表中找到一个对应的字符,最终显示给用户。  
  • 编码过程:字库表中的一个文字或符号,在字符集中找到对应的二进制串,然后通过一种编码方式,存储到计算机存储设备中

常见的编码规范及其发展过程

单字节

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),是最早产生的编码规范,共128个字符,用7位二进制表示(00000000-01111111即0x00-0x7F),可以看出ASCII码只需要1个字节的存储空间,它没有特定的编码方式,直接使用地址对应的二进制数来表示,非要说那就叫他ASCII 编码方式。可以表示阿拉伯数字和大小写英文字母,以及一些简单的符号。

EASCII(Extended ASCII),256个字符,用8位二进制表示(00000000-11111111即0x00-0xFF)。当计算机传到了欧洲,国际标准化组织在ASCII的基础上进行了扩展,形成了ISO-8859标准,跟EASCII类似,兼容ASCII,在高128个码位上有所区别。ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性

MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。由此可见,ISO-8859-1只占1个字节,且MySQL数据库默认编码就是ISO-8859-1,有时,tomcat服务器默认也是使用ISO-8859-1编码,然而ISO-8859-1是不支持中文的,有时这就是在浏览器上显示乱码的原因。但是由于欧洲的语言环境十分复杂,所以根据各地区的语言又形成了很多子标准,ISO-8859-1、ISO-8859-2、ISO-8859-3、……、ISO-8859-16。 

双字节  

当计算机传到了亚洲,256个码位就不够用了。于是乎继续扩大二维表,单字节改双字节,16位二进制数,65536个码位。在不同国家和地区又出现了很多编码,中国的GB2312港台的BIG5、日本的Shift JIS,韩国的Euc-kr等等。


GBK全称《汉字内码扩展规范》,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字。GBK字符集中所有字符占2个字节,不论中文英文都是2个字节。 没有特殊的编码方式,习惯称呼GBK 编码。

一般在国内,汉字较多时使用。GBK(Chinese Internal Code Specification)是GB2312的扩展,GBK 向下与 GB 2312 编码兼容,向上支持 ISO 10646.1国际标准,是前者向后者过渡过程中的一个承上启下的产物。ISO 10646 是国际化标准组织 ISO 公布的一个编码标准,即 Universal Multilpe-Octet Coded Character Set(简称UCS),与 Unicode 组织的 Unicode 编码完全兼容。
GBK编码,是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从8140至FEFE(剔除xx7F),共23940个码位,共收录了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字。 

多字节

当互联网席卷了全球,地域限制被打破了,不同国家和地区的计算机在交换数据的过程中,由于之前出现的各种不同的编码方式,文本就会出现乱码的问题,即对同一组二进制数据,不同的编码会解析出不同的字符。而当某个字符集中没有文本中的字符编码时,就会出现乱码。

通用字符集UCS(Universal Character Set)对应两种编码:对每一个字符采用四个8比特字节编码的称为UCS-4,对每一个字符采用两个8比特字节编码的称为UCS-2。

Unicode字符集的出现就是为了解决这个问题。Unicode 是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母AinU+0041表示英语的大写字母AU+4E25表示汉字。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表

需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如,汉字的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。2)Unicode 在很长一段时间内无法推广

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

BOM

BOM(字节顺序标记, byte-order mark)也是我们常见到的名词, 比如我们的代码文件都要求使用UTF-8无BOM形式保存, 不然有可能编译不过, 或者出现一些诡异的事情.
BOM实际上是位于码位U+FEFF的Unicode字符的名称.
对于UTF-16, UCS-2, UTF-32 / UCS-4这类码元不是8位的编码方式来说, 编码后的数据要存储/传输时, 必然会有字节序的问题, BOM出现在字节流的开头, 则用于标识该字节流的字节序. 各编码方案按自己的方式对U+FEFF进行编码, 放在头部即可标志编码该字节流时使用的字节序.
比如, 当我们知道即将读取的字节流以UTF-16编码, 字节序未知, 读到的前两个字节是0xFF, 0xFE, Unicode中U+FFFE则不映射到字符, 而这两个字节必定是编码的U+FEFF, 因此可以判断当前字节流使用小端序, 即UTF-16 LE

对于UTF-8, 由于它使用的是8位的码元, 不存在字节序的问题, 也不建议在头部添加BOM, 因为可能影响到一些工具, 因此使用无BOM的UTF-8成了主流.

ANSI

ANSI全称(American National Standard Institite)美国国家标准学会(美国的一个非营利组织),首先ANSI不是指的一种特定的编码,而是不同地区扩展编码方式的统称,各个国家和地区所独立制定的兼容ASCII,但互相不兼容的字符编码,微软统称为ANSI编码

总结对照

常用的字符编码ASCII,GBK,GB2312,BIG5,UTF-8 英文和中文简繁

Unicode字符集的编码方式有UTF-8,UTF-16,UTF-32

中文乱码产生的原因

PHP获取中文的第一个字符

//多字节字符串 PHP文件编码为UTF-8
$str = '你好PHP';
var_dump($str[0]); //输出结果 b"ä",乱码
var_dump(substr($str, 0, 1));//输出结果 b"ä",乱码
var_dump(substr($str, 0, 3));//输出结果 你 ,utf-8编码中一个汉字是三个字节
var_dump(mb_substr($str, 0, 1));//输出结果 你

PHP处理字符串的方式默认是把字符串作为单字节字符处理的,例如数组方式取字符和普通字符串函数

PHP处理多字节字符串需要用这些扩展 国际化与字符编码支持 常用的有mbstring 扩展和 iconv 函数

mbstring扩展支持的编码 https://www.php.net/manual/zh/mbstring.supported-encodings.php

iconv_get_encoding 获取 iconv 扩展的内部配置变量,

文本文件和二进制的区别

文本文件是二进制文件的一种,底层存储也是0和1;文本文件可读性和移植性好,但表现字符有限;二进制文件数据存储紧凑,无字符编码限制。文本文件基本上只能存放数字、文字、标点等有限字符组成的内容;二进制没有字符约束,可随意存储图像、音视频等数据。

用存储数字的例子可以形象的看出文本文件和二进制文件存储内容上的差异。例如要存储数字1234567890,文本文件要存储0-9这十个数字的ASCII码,对应的十六进制表示为:31 32 33 34 35 36 37 38 39 30,占用10个字节;1234567890对应的二进制为“‭0100 1001 1001 0110 0000 0010 1101 0010‬”,占用4个字节(二进制表示32位,一个字节8位),存储到文件的16进制表示为(大端):49 96 02 D2。

文本文件按字符存放内容,二进制按字节存放,这是两种文件最本质的区别。根据这个特性,可以推断出一些常见结论:二进制文件常常比文本文件紧凑,占用空间少;文本文件更友好易用,能用所见即所得的方式编辑;二进制文件常常需要专用程序打开,等等。

回过头看文本编辑器打开二进制文件常常是乱码的现象。例如一个二进制文件存放了一个整数1234(四个字节),用16进制表示为:00 00 04 D2。文本编辑器打开后逐个字符解释,会发现这几个字节拼不出可显示的字符,只好乱码相待。乱码的原因是文本编辑器不能正确解析字节流,这也是二进制文件需要用专用软件打开的原因。例如jpg文件要用看图软件打开,如果用音乐播放器打开,完蛋!视频文件要用播放器打开,用压缩软件打开,歇菜!有的专用软件会做处理打开不支持的格式后不做反应,有的会报错。

文件格式

Windows按文件拓展名识别文件格式,并调用对应的程序打开文件;

(类)Unix系统,拓展名可有可无,有file命令,这个命令可以告诉我们文件到底是什么格式文件拓展名不是文件格式的本质区别,内容才是。把a.zip改成a.txt/a.jgp/a.mp3,无论什么文件名,file都让其原形毕露:Zip archive data, at least v1.0 to extract。file命令的工作原理可这篇文章

PHP读取txt文本文件获取第一个字符乱码

参考

PHP检测文件BOM头

ANSCII编码对照表

有一个ASCII的编码的文本文件,在linux上打开会显示乱码,因此使用windows记事本打开转成了UTF-8编码,然后用php读取该文本文件第一个字符时出现乱码,原因是windows记事本保存文件时会给文本文件增加bom头。

php检测处理bom头的原理,就是用ord函数检测前三个字符在ASCII编码中的数字是否为239,187,191

if (ord($contents[0]) === 239 && ord($contents[1]) === 187 && ord($contents[2]) == 191)
{
   $contents = substr($contents, 3);
   var_dump($contents[0]);
}

对应的字符如下

用户的操作系统类型是不确定,因此文本文件的字符编码也无法确定 ,需要对用户上传的文本文件转换字符编码

$fileContents = file_get_contents($path);//读取文件字符串,此处可以用框架方法替代
$encoding = mb_detect_encoding($fileContent, ['ASCII', 'GBK', 'GB2312', 'BIG5', 'UTF-8']);//获取文件内容编码
$fileContent = mb_convert_encoding($fileContent, 'UTF-8', $encoding);//转码
//$contents = iconv($encoding, 'UTF-8', $contents);//iconv也可以

PHP解决跨域 常用方法

前置知识

接口站点 nginx 配置 添加请求头 和 自动响应

#表示允许这个域跨域调用(客户端发送请求的域名和端口) 
#$http_origin动态获取请求客户端请求的域,不用*的原因是带cookie的请求不支持*号
add_header Access-Control-Allow-Origin $http_origin;

#表示请求头的字段 动态获取
add_header Access-Control-Allow-Headers $http_access_control_request_headers;

#指定允许跨域的方法,* 代表所有
add_header Access-Control-Allow-Methods "GET,POST,PUT,PATCH,DELETE,OPTIONS";

#带cookie请求需要加上这个字段,并设置为true
add_header Access-Control-Allow-Credentials true;

#预检命令的缓存,如果不缓存每次会发送两次请求 单位s
add_header Access-Control-Max-Age 3600;

#自动响应options请求
if ($request_method = OPTIONS){
    return 204;
}

页面站点 nginx 配置 使用反向代理请求接口

通过代理将页面站点和接口站点变成同域

前置条件:页面的路由不能和接口路由存在冲突,否则路由无法代理到接口

ex: 所有接口的前缀都是/api 且页面路由中不存在 /api前缀

location = /api {
  proxy_pass http://apihost;
}

PHP响应增加请求头

此方法也可用于不支持自动跨域php框架,在框架入口文件 或者 路由文件 最上方增加 header

header("Access-Control-Allow-Origin: *"); //允许请求来源
header("Access-Control-Allow-Credentials : true"); //允许携带cookies
header("Access-Control-Allow-Headers: *");//允许请求头
header("Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS");//允许请求方法
header("Access-Control-Max-Age: 3600");//预检请求缓存时间

Laravel 框架

使用laravel-cors 扩展包

laravel 7.0 以后版本 官方集成了 laravel-cors 扩展包 文档

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'], //触发cors的路由前缀

    'allowed_methods' => ['*'],//允许请求方法

    'allowed_origins' => ['*'], //允许请求来源

    'allowed_origins_patterns' => [],//允许请求来源,正则表达式匹配

    'allowed_headers' => ['*'], //允许请求头

    'exposed_headers' => [], //排除的请求头

    'max_age' => 0, //预检请求缓存时间

    'supports_credentials' => false, //是否允许携带cookie
];

参考

MDN文档 跨域资源共享