基础知识(1)

1、linux中文件名存在哪里?

答:文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。包括:文件的字节数、文件拥有者的User ID、文件的Group ID、文件的读、写、执行权限、文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。、链接数,即有多少文件名指向这个inode、文件数据block的位置。总之,除了文件名以外的所有文件信息,都存在inode之中。Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。

2、C++纯虚函数及虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。

引入纯虚函数的原因:

       (1)为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

       (2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 

包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。

       虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数。如果父类或者祖先类中函数func()为虚函数,则子类及后代类中,函数func()是否加virtual关键字,都将是虚函数。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
3、内存泄露和内存越界

定义:在编写应用程序的时候,程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针),虽然这些内存被分配出去,但是无法收回,将无法被其他的进程所使用,我们说这块内存泄漏了,被泄漏的内存将在整个程序声明周期内都不可使用。
主要原因:是在使用new或malloc动态分配堆上的内存空间,而并未使用delete或free及时释放掉内存。
情况:

 

1. 不匹配使用new[] 和 delete[]

2. delet void * 的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露;

3. 没有将基类的析构函数定义为虚函数,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数

 4、查找排序算法

  1. 二分查找,复杂度O(logn);
  2. 计数排序,利用地址偏移来进行排序。排序字节串、宽字节串最快的排序算法。而且实现及其简单。最好时间复杂度 O(n)  平均时间复杂度 O(n)  最坏时间复杂度 O(n)  稳定排序
  3. 桶排序:最好时间复杂度 O(n)  平均时间复杂度 O(n)  最坏时间复杂度 O(n)  稳定排序
  4. 堆排序:最好时间复杂度 O(nlogn)  平均时间复杂度 O(nlogn)  最坏时间复杂度 O(nlogn)  非稳定排序
  5. 快速排序:最好时间复杂度 O(nlogn)  平均时间复杂度 O(nlogn)  最坏时间复杂度 O(n^2)  不稳定排序(最坏情况,正序或逆序,导致每次排列后剩余n-1)

排序算法

平均时间复杂度

最坏时间复杂度

最好时间复杂度

空间复杂度

稳定性

冒泡排序

O(n²)

O(n²)

O(n)

O(1)

稳定

直接选择排序

O(n²)

O(n²)

O(n)

O(1)

不稳定

直接插入排序

O(n²)

O(n²)

O(n)

O(1)

稳定

快速排序

O(nlogn)

O(n²)

O(nlogn)

O(nlogn)

不稳定

堆排序

O(nlogn)

O(nlogn)

O(nlogn)

O(1)

不稳定

希尔排序

O(nlogn)

O(ns)

O(n)

O(1)

不稳定

归并排序

O(nlogn)

O(nlogn)

O(nlogn)

O(n)

稳定

计数排序

O(n+k)

O(n+k)

O(n+k)

O(n+k)

稳定

基数排序

O(N*M) 

O(N*M)

O(N*M)

O(M)

稳定

查找 平均时间复杂度 查找条件 算法描述
顺序查找 O(n) 无序或有序队列 按顺序比较每个元素,直到找到关键字为止
二分查找(折半查找) O(logn) 有序数组 查找过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。 如果在某一步骤数组为空,则代表找不到。
二叉排序树查找 O(logn) 二叉排序树 在二叉查找树b中查找x的过程为:
1. 若b是空树,则搜索失败
2. 若x等于b的根节点的数据域之值,则查找成功;
3. 若x小于b的根节点的数据域之值,则搜索左子树
4. 查找右子树。
哈希表法(散列表) O(1) 先创建哈希表(散列表) 根据键值方式(Key value)进行查找,通过散列函数,定位数据元素。
分块查找 O(logn) 无序或有序队列 将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……。然后使用二分查找及顺序查找。

5、c++中的map与python dict(字典)的区别:

  • 以红黑树为基础的数据结构:map、set、TreeMap
  • 基于散列哈希的数据结构:HashMap、dictionary
  • c++中访问不存在的键会返回0,dict中访问不存在的键会报错。但是都可以赋值不存在的键。

6、session与cookie

  1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。

  2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。

  3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。

所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

1,session 在服务器端,cookie 在客户端(浏览器)
2,session 默认被存在在服务器的一个文件里(不是内存)
3,session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
4,session 可以放在 文件、数据库、或内存中都可以。
5,用户验证这种场合一般会用 session 因此,维持一个会话的核心就是客户端的唯一标识,即 session id

 

7、从输入url到展示页面的过程:

一、在浏览器输入URL
URL(Uniform Resource Locator,统一资源定位符)
由一串简单的文本字符组成,他的作用是用于定位互联网上资源的地址。URL通常由以下几部分组成:传输协议、域名、端口、文件路径。
二、DNS域名解析
当你在浏览器中输入URL后,你使用的电脑会发出一个DNS请求(将域名转化为IP地址的请求)。对于浏览器,实际上并不知道"https://www.zhihu.com/people/xiao-lan-miao-37-19"到底是什么东西,需要确认其中域名所在服务器的IP地址才能找到目标网站(浏览器需要对IP地址进行识别,才能够进一步传递网址和信息内容)。那么,将域名解析成对应的服务器IP地址这项工作,就是由DNS服务器来完成。

DNS(Domain Name System,域名系统)

主要进行将主机名和域名转换为IP地址的工作。把便于用户记忆的特定域名转换成为能够被机器直接读取的IP数串。

IP地址(Internet Protocol Address,互联网协议地址)

IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址。在互联网中的每一台主机都有IP地址,IP协议就是使用这个地址在主机之间传递信息,形如 192.168.0.1 。如果没有IP地址,就无法找到相应的主机进行信息交换,这个设备也就无法连接到互联网。

相关问题:

为什么要用域名,不直接使用IP地址?

IP地址不容易记住,特定的域名,更加方便用户记忆或辨识。
IP地址与域名并不是一对一的映射关系,一个域名后面可以对应多台设备的IP地址,一个IP地址也可以绑定多个域名。如果直接使用IP地址的话,可能无法准确的定位你想要访问的网站。


局域网IP与公网IP之间的区别

内网也就是局域网,内网的计算机以NAT(网络地址转换)协议,通过一个公共的网关访问Internet。内网的计算机可向Internet上的其他计算机发送连接请求,但Internet上其他的计算机无法向内网的计算机发送连接请求。
公网IP是处于整个互联网可访问的一个状态当中,公网IP都是需要购买的。
在同样的局域网下,如果知道对方的IP就可以进行访问,公网IP是处于整个互联网可访问的状态中。

域名解析的流程:

查找浏览器缓存——我们日常浏览网站时,浏览器会缓存DNS记录一段时间。如果以前我们访问过该网站,那么在浏览器中就会有相应的缓存记录。因此,我们输入网址后,浏览器会首先检查缓存中是否有该域名对应的IP信息。如果有,则直接返回该信息供用户访问网站,如果查询失败,会从系统缓存中进行查找。
查找系统缓存——从hosts文件中查找是否有存储的DNS信息(MAC端,可在“终端”中输入命令cat etc/hosts找到hosts文件位置),如果查询失败,可从路由器缓存中继续查找。
查找路由器缓存——如果之前访问过相应的网站,一般路由器也会缓存信息。如果查询失败,可继续从 ISP DNS 缓存查找。
查找ISP DNS缓存——从网络服务商(例如电信)的DNS缓存信息中查找。
如果经由以上方式都没找到对应IP的话,则向根域名服务器查找域名对应的IP地址,根域名服务器把请求转发到下一级,逐层查找该域名的对应数据,直到获得最终解析结果或失败的相应。
根域名服务器,根服务器主要用来管理互联网的主目录。是互联网域名解析系统(DNS)中最高级别的域名服务器。

三、服务器处理请求
浏览器通过IP地址找到对应的服务器,要求建立TCP链接,此时服务器开始处理用户请求

服务器如何处理请求?

由服务器上安装的处理请求的应用(Web Server)来处理。
常见的Web服务器有:Apache、Nginx、IIS、Lighttpd。
Web服务器接收用户的Requset交给网站代码或反向代理到其他服务器。

四、网站处理流程
用户输入网址后向服务器发送内容请求,服务器接收到请求后触发Controller(控制器),控制器从Model(模型)和视图(View)中获取各种数据信息进行处理,最后视图(View)将数据渲染为HTML使得页面完整的呈献给用户。



MVC是什么

MVC是一种设计模式,全名是Model View Controller,是模型(Model)- 视图(View)- 控制器(Controller)的缩写。

Model(模型)
是应用程序中用于处理应用程序数据逻辑的部分。
通常模型对象负责在数据苦衷存取数据。
View(视图)
是应用程序中处理数据显示的部分。
通常视图是一句模型数据创建的。
而这一部分也是我们前端工作中很重要的一项内容。
Controller(控制器)
是应用程序中处理用户交互的部分。
通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。




五、浏览器显示页面信息
HTML字符串被浏览器接收后被一句句读取解析。
解析到link标签后重新发送请求获取CSS。
解析到script标签后发送请求获取JS,并执行代码。
解析到img标签后发送请求获取图片资源。
浏览器根据HTML和CSS计算得到渲染书,绘制到屏幕上。
JS会被执行,页面展现。

 8、智能指针

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用

  1. auto_ptr:不要使用,因为拷贝构造函数和复制操作重载函数不够完美,当进行拷贝或者复制时,会将原有auto_ptr失效,表现在其不能作为stl容器的元素,或者auto_ptr作为函数参数时,当调用函数,则原有auto_ptr就会失效。
  2. unique_ptr :独享所有权,无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作。
  3. share_ptr:它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
  4. weak_ptr:当share_ptr进行循环引用时,会导致内存泄露。此时可用weak_ptr,不会增加引用计数。需要用一个shared_ptr实例来初始化weak_ptr。而要使用指针时,可以使用weak_ptr.lock(),如果share_ptr已被释放,则获得空的share_ptr,否则会获得有内容的share_ptr。
  5. 循环引用示例:
    #include <iostream>
    #include <memory>
    #include <vector>
    using namespace std;
    
    class ClassB;
    
    class ClassA
    {
    public:
        ClassA() { cout << "ClassA Constructor..." << endl; }
        ~ClassA() { cout << "ClassA Destructor..." << endl; }
        shared_ptr<ClassB> pb;  // 在A中引用B
    };
    
    class ClassB
    {
    public:
        ClassB() { cout << "ClassB Constructor..." << endl; }
        ~ClassB() { cout << "ClassB Destructor..." << endl; }
        shared_ptr<ClassA> pa;  // 在B中引用A
    };
    
    int main() {
        shared_ptr<ClassA> spa = make_shared<ClassA>();
        shared_ptr<ClassB> spb = make_shared<ClassB>();
        spa->pb = spb;
        spb->pa = spa;
        // 函数结束,思考一下:spa和spb会释放资源么?
    }
    ClassA Constructor...
    ClassB Constructor...
    Program ended with exit code: 0

    weak_ptr:

    class ClassB;
    
    class ClassA
    {
    public:
        ClassA() { cout << "ClassA Constructor..." << endl; }
        ~ClassA() { cout << "ClassA Destructor..." << endl; }
        weak_ptr<ClassB> pb;  // 在A中引用B
    };
    
    class ClassB
    {
    public:
        ClassB() { cout << "ClassB Constructor..." << endl; }
        ~ClassB() { cout << "ClassB Destructor..." << endl; }
        weak_ptr<ClassA> pa;  // 在B中引用A
    };
    
    int main() {
        shared_ptr<ClassA> spa = make_shared<ClassA>();
        shared_ptr<ClassB> spb = make_shared<ClassB>();
        spa->pb = spb;
        spb->pa = spa;
        // 函数结束,思考一下:spa和spb会释放资源么?
    }
    
    ClassA Constructor...
    ClassB Constructor...
    ClassA Destructor...
    ClassB Destructor...
    Program ended with exit code: 0

9、分布式锁

分布式场景,我们可以使用分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式。

在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户发起一个数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插入操作,导致数据重复插入,对于某些不允许有多余数据的业务来说,这就会造成问题。而分布式锁机制就是为了解决类似这类问题,保证多个服务之间互斥的访问共享资源,如果一个服务抢占了分布式锁,其他服务没获取到锁,就不进行后续操作。

1、使用 set key value [EX seconds][PX milliseconds][NX|XX] 命令。SET key value[EX seconds][PX milliseconds][NX|XX]

  • EX seconds: 设定过期时间,单位为秒
  • PX milliseconds: 设定过期时间,单位为毫秒
  • NX: 仅当key不存在时设置值
  • XX: 仅当key存在时设置值
  • value必须要具有唯一性,我们可以用UUID来做,设置随机字符串保证唯一性

假如value不是随机字符串,而是一个固定值,那么就可能存在下面的问题:

1.客户端1获取锁成功

2.客户端1在某个操作上阻塞了太长时间

3.设置的key过期了,锁自动释放了

4.客户端2获取到了对应同一个资源的锁

5.客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题

所以通常来说,在释放锁时,我们需要对value进行验证

2、释放锁的实现

释放锁时需要验证value值,也就是说我们在获取锁的时候需要设置一个value,不能直接用del key这种粗暴的方式,因为直接del key任何客户端都可以进行解锁了,所以解锁时,我们需要判断锁是否是自己的,基于value值来判断

 10、TCP连接池

使用连接池最大的一个好处就是减少连接的创建和关闭,增加系统负载能力,
之前就有遇到一个问题:TCP TIME_WAIT连接数过多导致服务不可用,因为未开启数据库连接池,再加上mysql并发较大,导致需要频繁的创建链接,最终产生了上万的TIME_WAIT的tcp链接,影响了系统性能。链接池中的的功能主要是管理一堆的链接,包括创建和关闭。

因为网络环境是复杂的,中间可能因为防火墙等原因,导致长时间空闲的连接会断开,所以可以通过两个方法来解决:

  • 客户端增加心跳,定时的给服务端发送请求

  • 给连接池中的连接增加最大空闲时间,超时的连接不再使用

远程服务端很有可能重启,那么之前创建的链接就失效了。客户端在使用的时候就需要判断这些失效的连接并丢弃。

11、http1、2、3

1、http1:

连接无法复用:连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对大量小文件请求影响较大(没有达到最大窗口请求就被终止)。

    • HTTP/1.0 传输数据时,每次都需要重新建立连接,增加延迟。
    • HTTP/1.1 虽然加入 keep-alive 可以复用一部分连接,但域名分片等情况下仍然需要建立多个 connection,耗费资源,给服务器带来性能压力。

b、Head-Of-Line Blocking(HOLB):导致带宽无法被充分利用,以及后续健康请求被阻塞。HOLB是指一系列包(package)因为第一个包被阻塞;当页面中需要请求很多资源的时候,HOLB(队头阻塞)会导致在达到最大请求数量时,剩余的资源需要等待其他资源请求完成后才能发起请求。

    • HTTP 1.0:下个请求必须在前一个请求返回后才能发出,request-response对按序发生。显然,如果某个请求长时间没有返回,那么接下来的请求就全部阻塞了。
    • HTTP 1.1:尝试使用 pipeling 来解决,即浏览器可以一次性发出多个请求(同个域名,同一条 TCP 链接)。但 pipeling 要求返回是按序的,那么前一个请求如果很耗时(比如处理大图片),那么后面的请求即使服务器已经处理完,仍会等待前面的请求处理完才开始按序返回。所以,pipeling 只部分解决了 HOLB。

1. 二进制传输

HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。

2. 多路复用

在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。

在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2 中:

    • 同域名下所有通信都在单个连接上完成。
    • 单个连接可以承载任意数量的双向数据流。
    • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。

 

3. Header 压缩

  3、http3:

Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上。

    • 0-RTT

通过使用类似 TCP 快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。

    • 多路复用

虽然 HTTP/2 支持了多路复用,但是 TCP 协议终究是没有这个功能的。QUIC 原生就实现了这个功能,并且传输的单个数据流可以保证有序交付且不会影响其他的数据流,这样的技术就解决了之前 TCP 存在的问题。

    • 加密认证的报文

TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。

    • 向前纠错机制

QUIC 协议有一个非常独特的特性,称为向前纠错 (Forward Error Correction,FEC),每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传,因为数据重传将会消耗更多的时间(包括确认数据包丢失、请求重传、等待新数据包等步骤的时间消耗)

总结

    • HTTP/1.x 有连接无法复用、队头阻塞、协议开销大和安全因素等多个缺陷
    • HTTP/2 通过多路复用、二进制流、Header 压缩等等技术,极大地提高了性能,但是还是存在着问题的
    • QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议

 12、跨域

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
原文地址:https://www.cnblogs.com/zl1991/p/12978680.html