查看原文
其他

TCP 和 UDP 协议有什么区别?

编程导航和鱼友们 面试鸭 2024-01-21

大家好呀,今天是编程导航 30 天面试题挑战的第十九天,一起来看看今天有哪些优质面试题吧。

后端

题目一

TCP 和 UDP 协议有什么区别,分别适用于什么场景?

官方解析

TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol)是两种常用的传输层协议,它们有以下的区别:

1、连接方面

TCP 是面向连接的协议,而 UDP 是无连接的协议。在 TCP 中,发送方和接收方必须先建立连接,然后才能传输数据。UDP 则不需要建立连接,直接发送数据即可。

2、可靠性

TCP 保证数据传输的可靠性,通过序列号、确认应答和重传机制等方式来保证数据的完整性正确性。UDP 则不保证数据传输的可靠性,因为它不提供确认和重传机制。

3、传输速度

因为 TCP 要保证数据传输的可靠性,所以在传输速度方面相对较慢。而 UDP 则不需要进行复杂的传输控制,因此传输速度更快。

4、传输内容

TCP 是一种面向字节流的协议,将数据看作是一连串的字节流,没有明确的消息边界。UDP 则是面向报文的协议,将数据看作是一系列的报文,每个报文是一个独立的单元,具有明确的消息边界。

基于以上的特点,TCP 和 UDP 适用于不同的场景。TCP 适用于对传输可靠性要求比较高的场景,例如网页浏览文件传输邮件等。而 UDP 则适用于对传输可靠性要求较低、传输速度要求较高的场景,例如在线游戏视频直播等。

鱼友的精彩回答

爱吃鱼蛋的回答

TCP 和 UDP 是计算机网络中两种常用的传输层协议,用于实现可靠传输和无连接传输。

TCP(Transmission Control Protocol)是一种面向连接的、可靠的传输协议。它通过三次握手四次挥手进行连接和断开链接,保证数据的可靠性、完整性和顺序性,具有较高的传输效率。

TCP 协议适用于要求可靠传输的场景,如文件传输、电子邮件传输等。

TCP协议的工作流程如下:

  • 客户端向服务器发送连接请求(SYN)。
  • 服务器收到连接请求后,回复确认请求(SYN+ACK)。
  • 客户端收到确认请求后,回复确认(ACK),完成连接。
  • 数据传输完成后,客户端和服务器分别发送关闭连接请求(FIN)。
  • 对方收到关闭请求后,回复确认(ACK)。
  • 双方都收到对方的关闭请求和确认后,关闭连接。

UDP(User Datagram Protocol)是一种无连接的、不可靠的传输协议。它不需要建立连接和维护连接状态,具有较高的传输速度和实时性,但不保证数据的完整性和顺序性。

UDP 协议适用于实时性要求高、数据量小、丢失数据不会影响结果的场景,如视频直播语音通话等。 UDP协议工作流程:

  • 客户端向服务器发送数据报。
  • 服务器收到数据报后,直接处理数据并回复确认。
  • 客户端收到确认后,继续发送下一个数据报。
  • 如果数据报丢失或损坏,客户端不会重传,而是直接忽略。

两者的区别主要如下:

  1. 连接方式:TCP 是面向连接的协议,UDP 是无连接的协议。
  2. 可靠性:TCP 提供可靠的传输,保证数据的完整性顺序性,而 UDP 不保证数据的完整性和顺序性。
  3. 速度UDP 比 TCP 更快,因为它不需要建立连接和维护连接状态。
  4. 传输方式:TCP 是基于字节流的传输方式,UDP 是基于数据报的传输方式。

针对于 TCP 的特点,其应用场景主要有:

  1. 文件传输:通过 TCP 协议传输文件时,确保文件的完整性安全性
  2. 邮件传输:通过 TCP 协议传输邮件时,确保邮件的完整性可靠性
  3. 网页浏览:通过 TCP 协议传输网页时,确保网页的完整性正确性

针对UDP的特点,其应用场景主要有:

  1. 视频流传输:通过 UDP 协议传输视频流时,要求实时性高,允许数据的丢失和重复。
  2. 语音通话:通过 UDP 协议传输语音时,要求实时性高,允许数据的丢失和重复。
  3. 游戏应用:通过 UDP 协议传输游戏数据时,要求实时性高,允许数据的丢失和重复。

猫十二懿的回答

TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是两种常用的传输层协议,两者的区别比较如下:

HeiHei 的回答

区别:

1. 连接:

TCP 是面向连接的传输层协议,传输数据前要先建立连接

UDP是不需要连接,即刻传输数据

2. 服务对象

TCP 是一对一的两点服务,即一条连接只有两个端点。

UDP 支持一对一、一对多、多对多的交互通信。

3. 可靠性

TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达

UDP 是尽最大努力交付,不保证可靠交付数据

4. 流量控制、拥塞控制

TCP 有拥塞控制流量控制机制,保证数据传输的安全性

UDP则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率

5. 首部开销

TCP 首部长度较长,会有一定的开销,首部在没有使用[选项]字段时是 20

UDP 首部只有 8 个字节,并且是固定不变的,开销较小

6. 传输方式

TCP 是流式传输没有边界,但保证顺序和可靠

UDP是一个包一个包地发送,是有边界的,但可能会丢包和乱序

7. 分片不同

TCP 的数据大小如果大于 MSS(最大报文段长度) 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片

UDP 的数据大小如果大于 MTU(最大传输单元) 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应小于 MTU

8. 首部字段

TCP 有可变长的[选项]字段

UDP 头部长度则是不会变化的,无需多一个字段区记录 UDP 的首部长度

9. 包长度字段

TCP 无。其中 IP 总长度和 IP 首部长度,在 IP 首部格式是已知的。TCP 首部长度,则是在 TCP 首部格式已知的,所以就可以求得 TCP 数据的长度:TCP 数据的长度 = IP 总长度 - IP 首部长度 - TCP 首部长度

UDP 有 2 字节。为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍

应用场景:

TCP 应用场景适用于对效率要求低,对准确性要求高或者要求有链接的场景,而 UDP 适用场景为对效率要求高,对准确性要求低的场景

题目二

什么是分布式的 CAP 理论?

官方解析

分布式的 CAP 理论是指在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)这三个指标无法同时满足的问题。具体来说:

  • 一致性(Consistency):指多个副本之间数据保持一致,即在一个副本上的写操作会立即同步到其他所有副本,所有副本的数据都是最新的,保持强一致性
  • 可用性(Availability):指系统在任何时候都能对外提供服务,即系统随时能够响应用户请求,不会因为节点故障或其他原因而导致服务中断。
  • 分区容错性(Partition Tolerance):指系统在出现网络分区节点之间失去联系)时,仍能够继续工作,保证数据的一致性和可用性。

CAP 理论指出,一个分布式系统只能同时满足其中的两个指标,无法同时满足三个

例如,当出现网络分区时,如果要保证一致性,就必须停止对外服务,从而失去可用性;如果要保证可用性,就必须放弃一致性,从而可能导致不同节点之间数据不一致

因此,在设计分布式系统时,需要根据具体的场景和需求来选择合适的权衡方案,比如选择 CP(一致性和分区容错性)或者选择 AP(可用性和分区容错性)

需要注意的是,CAP 理论只是一种理论框架,不能直接应用于实际的分布式系统设计。在实际应用中,还需要考虑系统的具体业务需求、数据访问模式、节点规模和部署环境等因素,综合权衡之后再选择合适的分布式架构和技术方案。

鱼友的精彩回答

Gianhing 的回答

CAP 理论是指一个分布式系统中,不可能同时满足以下三个条件:

  • 一致性(Consistency):所有节点在同一时间的看到的数据是一致的,即写数据操作时要同时更新相关副本,保证强一致性
  • 可用性(Availability):系统中能正常接收请求的节点都能在合理时间内返回结果,即系统在某些节点失效下仍能对外提供服务
  • 分区容错性(Partition Tolerance):什么是分区?分布式系统中存在很多节点,这些节点之间通过网络进行通信,当节点间的通信出了问题(如网络故障、机器故障等),就称系统出现了分区。而分区容错性就是出现分区问题时,系统还能继续对外提供服务

根据 CAP 理论,分布式系统只能满足其中的两个特性。然而实际上,分区容错性是一定要满足的,因为不可能只要出现分区问题时整个系统就完全无法使用。因此,在分布式系统中,我们需要考虑的是当出现分区问题时,选择的是一致性还是可用性即 CP 还是 AP

  • CP 架构:当系统出现分区故障时,客户端发送的任意请求都会被卡死或超时,保证数据的强一致性。如 Zookeeper
  • AP 架构:当系统出现分区故障时,客户端依旧能获取数据但有的是新数据,有的是旧数据。如 Eureka

鱼皮评论:不错

猫十二懿的回答

CAP 原则又称 CAP 定理,指的是在一个分布式系统中,Consistency(一致性)Availability(可用性)Partition tolerance(分区容错性)这三个基本需求,最多只能同时满足其中的 2 个。

一致性 :数据在多个副本之间能够保持一致的特性。

可用性:系统提供的服务一直处于可用的状态,每次请求都能获得正确的响应。

分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

CAP 三者不可同得,那么必须得做一些权衡。

CA without P

如果不要求 P(不允许分区),则 C(强一致性)和 A(可用性)是可以保证的。但是对于分布式系统,分区是客观存在的,其实分布式系统理论上是不可选 CA 的

CP without A

如果不要求 A(可用),相当于每个请求都需要在 Server 之间强一致,而 P(分区)会导致同步时间无限延长,如此 CP 也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

AP wihtout C

要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的 NoSQL 都属于此类。

mos 的回答

CAP即:

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance(分区容忍性)

这三个性质对应了分布式系统的三个指标: 而 CAP 理论说的就是:一个分布式系统,不可能同时做到这三点。如下图:

一致性:对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。换句话说,一致性是站在分布式系统的角度,对访问本系统的客户端的一种承诺:要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确

可用性任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错

分区容忍性:由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。换句话说,分区容忍性是站在分布式系统的角度,对访问本系统的客户端的再一种承诺:我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉

CAP 理论指出,一个分布式系统只能同时满足其中的两个指标,无法同时满足三个

例如,当出现网络分区时,如果要保证一致性,就必须停止对外服务,从而失去可用性;如果要保证可用性,就必须放弃一致性,从而可能导致不同节点之间数据不一致。因此,在设计分布式系统时,需要根据具体的场景和需求来选择合适的权衡方案,比如选择 CP(一致性和分区容错性)或者选择 AP(可用性和分区容错性)。

需要注意的是,CAP 理论只是一种理论框架,不能直接应用于实际的分布式系统设计。在实际应用中,还需要考虑系统的具体业务需求、数据访问模式、节点规模和部署环境等因素,综合权衡之后再选择合适的分布式架构和技术方案。

9915-java-木木的回答

在一个分布式系统中,分布式的 CAP 理论体现在一下三个基本需求,最多只能同时满足其中的两个:

  • 一致性(C:Consistency):所有节点在同一时间具有相同的数据
  • 可用性(A:Availability):每个请求都能得到一个响应,无论成功或失败
  • 分区容错性(P:Partitiontolerance):系统能够容忍网络分区的情况,即节点之间无法通信。

根据 CAP 理论,一个分布式系统只能在以下三种情况中选择其需要满足的两个需求:

  • CP:满足一致性和分区容错性,但牺牲可用性。例如:当网络发生故障时,为了保证数据不出现不一致,就拒绝服务请求。
  • AP:满足可用性和分区容错性,但牺牲一致性。例如:当网络发生故障时,为了保证服务不中断,就允许数据出现不一致。
  • CA:满足一致性和可用性,但牺牲分区容错性。例如:在没有网络故障的情况下,保证数据始终一致和服务始终可用。

常见的例子

  • CP:Zookeeper、HBase、Redis Cluster等。这些系统在遇到网络分区的情况下,就回牺牲可用性,来保证数据的一致性。
  • AP:Eureka、Cassandra、MingoDB等,这些系统在遇到网络分区的情况下,会牺牲一致性,可以摆正服务的可用性。
  • CA:单机数据库、单机缓存等,这些系统在没有网络分区的情况下,可以保证数据的一致性和服务的可用性。

CA只是一个理想化的状态,在实际的分布式系统中,网络分区是一个不可避免的问题,所以只有CP和AP是真正有意义的选择。

题目三

如何用 Redis 实现分布式 Session?

官方解析

在分布式系统中,通常会将 Session 存储在 Redis 中来实现分布式 Session,这样就可以在多台服务器之间共享 Session 数据。

实现分布式 Session 的方式有多种,其中一种常用的方式是使用 Redis 的数据结构 Hash。具体实现步骤如下:

  1. 在用户登录成功后,将 Session 数据存储在 Redis 中。
  2. 将 Redis 中的 Session 数据的 Key 设置为一个全局唯一的 ID,一般使用类似于“session:token”这样的格式,其中 token 是一个随机生成的字符串,用来标识这个 Session 数据。
  3. 在客户端返回响应的同时,将 Session ID(即 token)以 Cookie 的形式返回给客户端。客户端在后续的请求中都会携带这个 Cookie。
  4. 在后续的请求中,服务器会从客户端传递过来的 Cookie 中获取 Session ID,然后根据这个 ID 从 Redis 中获取对应的 Session 数据。如果 Redis 中没有找到对应的 Session 数据,那么就表示这个请求无法通过认证。
  • 在用户退出登录或 Session 失效时,需要将 Redis 中的对应 Session 数据删除。

可以使用 Redis 的 EXPIRE 命令来设置 Session 数据的过期时间,这样可以自动删除已经过期的 Session 数据。

同时,还需要注意保护 Redis 中的 Session 数据不被恶意攻击者窃取,一般可以通过设置 Session 数据的前缀和使用随机的 Session ID 等方式来提高安全性。

鱼皮的补充:这题可以结合 spring-session-data-redis 的实现去说

鱼友的精彩回答

yes.的回答

分布式 session 指在多个服务器间共享 session,我们可以使用 redis 来存储 session 来实现该功能。

在 redis 中我们通常使用 Hash 来存储 session。

具体的步骤如下:

  1. 用户登录成功后,将 Session 存到 redis 中
  2. 将key设置为一个全局 id,格式可以采用“session:token”,其中 token 为 sessiond 的唯一标识。
  3. 将 session 的唯一标识 token 以 cookie 的形式返回给客户端,客户端在后续请求中都会携带这个 cookie。
  4. 后续请求中,服务器拿到客户端传来的 cookie,并根据它的值,也就是 token,去 redis 找对应的 session 数据。
  5. 用户退出登录后,将 session 删除。

9915-java-木木的回答

在 web 开发中,我们会把用户的登录信息存储在 session 中,而 session 是依赖于 cookie 的,即服务器创建 session 时,会自动分配一个唯一的 ID,并且的响应时创建一个 cookie 用于存储该 sessionId。当客户端收到这个 cookie 后,就会自动保存这个 sessionId,并且在下次访问时自动携带这个 sessionId。这时服务器就可以通过这个 sessionId 得到与之对应的 session,从而识别用户的身份。

目前的大部分应用,基本都是采用分布式部署的方式,即将应用程序部署在多台服务器上,并通过 Nginx 做统一的请求分发。但是服务器与服务器之间是相互隔离的,他们的 session 是不共享的,这就存在了 session 同步的问题了,如图:

如果客户端第一次访问服务器,请求被分发到了服务器 A,则服务器 A 就会为该客户端创建 session。如果客户端再次访问服务器,请求被分发到了服务器B,由于服务器B上没有这个 session,所以,用户的身份就无法得到验证,从而产生了不一致的问题。

解决这个问题的办法很多,比如可以协调多个服务器,让他们的 session 保持同步,也可以在分发请求时做绑定处理,即将某一个 IP 固定分配给同一个服务器。但是这些方式都比较麻烦,而且在性能上也有一定的消耗。更加合理的方式就是使用 Redis 高性能缓存服务器,来实现分布式session

从上面的分析中,不难看出,其实要想做到多台服务器 session 共享,无非就是两件事:

  • 保存用户信息
  • 验证用户信息

具体实现如下:

  1. 创建令牌:用户初次访问服务器时,给他创建一个 唯一的身份标识,并且使用cookie封装这个标识后,再发送给客户端。当客户端再次访问服务器时,就回自动携带这个身份标识了,这个和单机版本的sessionId是一样的。在返回令牌之前,需要将他存储起来,以便于后续的验证。而这个令牌不能保存在服务器本地,因为多台服务器之间相互隔离。所以,这个令牌就可以使用Redis进行存储。

  2. 验证令牌:用户再次访问服务器时,获取到之前的身份标识,通过Redis进行查询便知结果。

航仔丶的回答

Java 实现

1.配置Maven依赖和Redis连接信息

添加Spring Boot和Spring Session依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
</dependencies>

配置Redis连接信息:

spring:
  redis:
    cluster:
      nodes:
        - 148.70.153.63:9426
        - 148.70.153.63:9427
        - 148.70.153.63:9428
        - 148.70.153.63:9429
        - 148.70.153.63:9430
        - 148.70.153.63:9431
    password: password
    timeout: 60000

2.配置 Spring Session 和 Redis

在Spring Boot应用程序的入口点上添加@EnableRedisHttpSession注释,并指定 session 的最大不活动时间:

@SpringBootApplication
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisSessionApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisSessionApplication.class, args);
    }
}

配置Redis作为Spring Session的存储:

spring:
  session:
    store-type: redis

3.编写代码并测试

创建一个RESTful API,以获取当前Session的ID:

@RestController
public class SessionController {
    @RequestMapping("/getSessionId")
    public String getSessionId(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        return sessionId;
    }
}

在多个 Tomcat 实例中启动应用程序,并使用不同的浏览器访问 API。您应该会看到相同的 Session ID,这表明 Session 数据已在不同的Tomcat实例之间共享。

总结: 使用Redis实现分布式 Session 需要将 Session 数据存储在 Redis 中,并在多个 Tomcat 实例之间共享这些数据。可以使用 Spring Session 和 Spring Boot 自动配置来实现这一目标,并在代码中访问 Session 数据。

迷。的回答

讲一下整个 redis 实现共享 session 的业务流程:

  • 在发送验证码的时候将手机号和对应验证码以 key value 形式存储到 redis 中
  • 在对比验证码是否一致时,需要从 redis 里面取出手机号对应的 code
  • 会使用 UUID 创建一个登录令牌 token
  • 将 User 对象转为 HashMap,并与 token 令牌一起以 hash 键值对形式存储
  • 设置一个 token 的有效期并返回给前端
  • 设置一个新的拦截器,用于刷新 token,由于 LoginInterceptor 没有交给 Spring 进⾏管理,因此 StringRedisTemplate 不能通过@Resource⾃动注⼊。需要在配置⽂件中进⾏构造器注⼊。
public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        //如果不符合就返回错误信息
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }

        //符合就发送验证码
        //胡图工具类的使用
        String code = RandomUtil.randomNumbers(6);
        //保存验证码到redis  设置一个验证码有效时长   key-⼿机号 value-验证码
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //发送验证码,返回ok(这里假装发一下验证码,实际上要不调用云平台的服务
        log.debug("短信验证码为:" + code);
        return Result.ok();
    }
 public Result login(LoginFormDTO loginForm, HttpSession session) {
        //校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        //从redis里获取并校验验证码

        String cashCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);//找到key对应的value
        String code = loginForm.getCode();
        if (cashCode == null || !cashCode.equals(code)) {
            return Result.fail("验证码错误");
        }
        User user = query().eq("phone", phone).one();//用mybatis-plus的框架查询
        if (user == null) {//用户不存在就创建出来
            user = createUserWithPhone(phone);
        }
        //保存在redis中
        //随机生成一个token作为登录的令牌
        String token= UUID.randomUUID().toString(true);
        //将User对象转为HashMap存储
        UserDTO userDTO =BeanUtil.copyProperties(user,UserDTO.class);//用户脱敏
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        String tokenKey=LOGIN_USER_KEY+token;
        //用哈希结构存储 多个字段有多个属性 可以存好几个键值对
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //设置token的有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//30分钟
        //返回token
        return Result.ok(token);
    }
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

前端

题目一

为什么 JS 要被设计为单线程?

官方解析

JS 被设计为单线程的主要原因是为了避免多线程编程所带来的复杂性。如果 JS 是多线程的,那么在处理并发问题时,需要考虑锁、同步等一系列复杂的问题,这会增加代码的复杂度和开发难度。

此外,JS 最初是为了解决网页交互的问题而诞生的,而网页交互的需求大部分是基于用户事件的,比如点击按钮、输入文本等。这些操作的响应速度要求很高,如果在响应事件的同时还要处理其他任务,可能会导致网页卡顿、响应变慢等用户体验不佳的问题。

因此,为了避免多线程所带来的复杂性和降低开发难度,并且满足网页交互的高响应速度需求,JS 被设计为单线程。虽然单线程有局限性,但是可以通过异步编程、事件循环机制等技术手段来实现高效的并发处理。

鱼友的精彩回答

淡云的回答

因为js主要用于用户交互,单线程可以使用户的操作响应迅速;

HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

你还费解吗的回答

JS 的单线程

JS 是单线程的,这表示它同一时间只能执行一个任务,其他任务都必须在后面排队等待。

它之所以被设计为单线程,而非多线程,和历史有关。JS 最初只是用于做一些简单的输入验证处理,开发者并没有想法要将它应用于复杂的场景。另外,如果采用多线程,则需要共享资源,并且有可能修改彼此的运行结果。比如,假设 JS 同时有两个线程,一个要修改某个 DOM 节点,另一个要删除它,这时浏览器应该以哪个线程为准呢?当然可以通过锁来解决上面的问题。但为了避免因引入了锁而带来更大的复杂性,JS 在最初就选择了单线程执行。

尽管单线程实现起来比较简单,执行环境相对单纯,但它还是存在明显的缺点:只要一个任务耗时非常长,后面的任务就必须排队等待,这会严重拖慢整个程序的执行。比如,执行代码时遇到一段死循环,浏览器就会不断地执行,在此期间,不能与页面进行任何交互,页面会出现一种卡死的现象。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JS 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JS 单线程的本质。

浏览器的多进程架构

补充一下,“JS 是单线程的”指的是执行 JS 的线程只有一个,是浏览器提供的 JS 引擎线程(主线程)。如今的主流浏览器都是多进程架构的,以 Chrome 为例,它包含了 1 个 浏览器主进程、1个 GPU 进程、1 个网络进程、多个渲染进程或多个插件进程。作为前端开发者,应重点关注其渲染进程,渲染进程的核心任务是将 HTML、CSS 和 JavaScript 转换为可交互的网页,排版引擎 Blink 和 JS 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。一个渲染进程通常由以下线程组成:

JS 引擎线程(主线程):

JavaScript 引擎,也称为 JS 内核,负责处理 JS 脚本,执行代码。

当主线程空闲且任务队列不为空时,会依次取出任务执行。

注意,该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JS 时间过长,将导致页面渲染的阻塞。

GUI 渲染线程:

主要负责页面的渲染,解析 HTML、CSS,构建DOM树,布局和绘制等。

当界面需要重绘或者由于某种操作引发重排时,将执行该线程。

注意:该线程与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 线程会被挂起,当任务队列空闲时,主线程才会去执行 GUI 渲染。

事件触发线程:

用于控制事件循环,将准备好的事件交给 JS 引擎线程执行。

当主线程遇到异步任务,如 setTimeOut(或 ajax 请求、鼠标点击事件),会将它们交由对应的线程处理,处理完毕后,事件触发线程会把对应的事件添加到任务队列的尾部,等待 JS 引擎的处理。

注意:由于 JS 的单线程关系,队列中的待处理事件都得排队等待,只有在 JS 引擎空闲时才能被执行。

定时器触发线程:

负责执行定时器一类函数的线程,如 setTimeout,setInterval 等。

主线程依次执行代码时,遇到定时器,会将定时器交由该线程进行计时,当计时结束,事件触发线程会将定时器的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执行。

异步 http 请求线程:

负责执行异步请求一类的函数的线程,如 Promise,axios,ajax 等。 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理。当监听到状态码变更,如果设置有回调函数,事件触发线程会将相应的回调函数添加到任务队列的尾部,等待 JS 引擎空闲后执行。

题目二

TypeScript 中的 Declare 关键字有什么用?

官方解析

TypeScript 中的 declare 关键字用于声明一个变量、函数、类等的类型信息,但不实现其具体实现。它主要用于在编译时进行静态类型检查,并在编译后移除声明的代码,以减少 JavaScript 文件的大小。

使用 declare 关键字声明的类型信息可以是任何类型,如对象、函数、类、变量、命名空间等。常见的使用场景有:

声明全局变量或模块

// 声明全局变量
declare const jQuery: (selector: string) => any;

// 声明模块
declare module "lodash" {
  export function debounce(fn: Function, wait: number): Function;
}

声明外部的 JavaScript 库

declare module "some-library" {
  export function someFunction(): void;
  export const someVariable: number;
}

声明命名空间

declare namespace MyNamespace {
  function myFunction(): void;
}

通过使用 declare 关键字,可以让 TypeScript 在编译时检查代码的类型信息,并避免一些类型错误。同时,也可以提高代码的可读性和维护性。

题目三

什么是 Node.js 中的 process?它有哪些方法和应用场景?

官方解析

在 Node.js 中,process 是一个全局变量,它提供了与当前 Node.js 进程相关的信息和控制。process 对象是 EventEmitter 的一个实例,因此它可以使用 EventEmitter 的 API,例如注册事件监听器和触发事件。

process 对象的一些常用方法和属性:

  • process.argv:返回一个数组,其中包含命令行参数。第一个元素是 Node.js 可执行文件的路径,第二个元素是正在执行的 JavaScript 文件的路径,后面的元素是命令行参数。
  • process.env:返回一个包含当前 Shell 环境变量的对象。
  • process.exit([code]):终止 Node.js 进程。如果指定了 code,那么进程将以 code 退出。
  • process.cwd():返回当前工作目录的路径。
  • process.chdir(directory):将 Node.js 进程的工作目录更改为 directory。
  • process.pid:返回 Node.js 进程的进程 ID。
  • process.nextTick(callback[, arg1][, arg2][, ...]):将 callback 添加到下一个 tick 队列。callback 会在当前操作完成后、事件循环继续之前调用。
  • process.on(event, listener):注册事件监听器。常用的事件包括 "exit"、"uncaughtException"、"SIGINT" 等。

process 对象的应用场景:

  • 监听进程退出事件,执行资源清理操作。
  • 通过 process.argv 读取命令行参数。
  • 通过 process.env 读取环境变量。
  • 通过 process.cwd 和 process.chdir 修改 Node.js 进程的工作目录。
  • 通过 process.pid 获取进程 ID。
  • 通过 process.nextTick 将某个操作放到下一个 tick 队列中,以实现异步执行。

总之,process 对象提供了与 Node.js 进程相关的许多信息和控制,是 Node.js 编程中不可或缺的一部分。

鱼皮补充:可以说下自己在项目中是如何用 process 的,比较常见的就是 process.env 读取环境变量

星球活动

1.欢迎参与 30 天面试题挑战活动 ,搞定高频面试题,斩杀面试官!

2.欢迎已加入星球的同学 免费申请一年编程导航网站会员

3.欢迎学习 鱼皮最新原创项目教程,手把手教你做出项目、写出高分简历!

加入我们

欢迎加入鱼皮的编程导航知识星球,鱼皮会 1 对 1 回答您的问题、直播带你做出项目、为你定制学习计划和求职指导,还能获取海量编程学习资源,和上万名学编程的同学共享知识、交流进步。

💎 加入星球后,您可以:

1)添加鱼皮本人微信,向他 1 对 1 提问,帮您解决问题、告别迷茫!点击了解详情

2)获取海量编程知识和资源,包括:3000+ 鱼皮的编程答疑和求职指导、原创编程学习路线、几十万字的编程学习知识库、几十 T 编程学习资源、500+ 精华帖等!点击了解详情

3)找鱼皮咨询求职建议和优化简历,次数不限!点击了解详情

4)鱼皮直播从 0 到 1 带大家做出项目,已有 50+ 直播、完结 3 套项目、10+ 项目分享,帮您掌握独立开发项目的能力、丰富简历!点击了解详情

外面一套项目课就上千元了,而星球内所有项目都有指导答疑,轻松解决问题

星球提供的所有服务,都是为了帮您更好地学编程、找到理想的工作。诚挚地欢迎您的加入,这可能是最好的学习机会,也是最值得的一笔投资!

长按扫码领优惠券加入,也可以添加微信 yupi1085 咨询星球(备注“想加星球”):

往期推荐

什么是单例模式?

怎么解决跨域问题?

synchronized 是什么,有什么作用?

Redis 的持久化机制有哪些?

如何使用 Redis 实现一个排行榜?

继续滑动看下一个

TCP 和 UDP 协议有什么区别?

编程导航和鱼友们 面试鸭
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存