三次输错密码后,系统是怎么做到不让我继续尝试的?

三次,密码,系统,怎么,做到,不让,继续,尝试 · 浏览次数 : 3082

小编点评

**类** * JacksonRedisSerializer * RedisTemplate **方法** * serialize() * deserialize() * limitCount() * getLimitCountKey() **方法说明** * limitCount() 方法用于限制登录接口的次数,并返回限制次数的 key * getLimitCountKey() 方法用于获取限制登录接口的 key * deserialize 方法用于解析登录接口的 JSON 字符串 * serialize 方法用于解析登录接口的 JSON 字符串 **测试** * LoginController 中的 login 方法用于测试登录接口的限制 * 测试方法中使用 limitCount() 方法限制登录接口的次数

正文

故事背景

忘记密码这件事,相信绝大多数人都遇到过,输一次错一次,错到几次以上,就不允许你继续尝试了。

但当你尝试重置密码,又发现新密码不能和原密码重复:

相信此刻心情只能用一张图形容:

虽然,但是,密码还是很重要的,顺便我有了一个问题:三次输错密码后,系统是怎么做到不让我继续尝试的?

我想了想,有如下几个问题需要搞定

  1. 是只有输错密码才锁定,还是账户名和密码任何一个输错就锁定?
  2. 输错之后也不是完全冻结,为啥隔了几分钟又可以重新输了?
  3. 技术栈到底麻不麻烦?

去网上搜了搜,也问了下ChatGPT,找到一套解决方案:SpringBoot+Redis+Lua脚本。
这套方案也不算新,很早就有人在用了,不过难得是自己想到的问题和解法,就记录一下吧。

顺便回答一下上面的三个问题:

  1. 锁定的是IP,不是输入的账户名或者密码,也就是说任一一个输错3次就会被锁定
  2. Redis的Lua脚本中实现了key过期策略,当key消失时锁定自然也就消失了
  3. 技术栈同SpringBoot+Redis+Lua脚本

那么自己动手实现一下

前端部分

首先写一个账密输入页面,使用很简单HTML加表单提交

<!DOCTYPE html>
<html>
<head>
<title>登录页面</title>
<style>
body {
background-color: #F5F5F5;
}
form {
width: 300px;
margin: 0 auto;
margin-top: 100px;
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
label {
display: block;
margin-bottom: 10px;
}
input[type="text"], input[type="password"] {
border: none;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
width: 100%;
box-sizing: border-box;
font-size: 16px;
}
input[type="submit"] {
background-color: #30B0F0;
color: white;
border: none;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
width: 100%;
font-size: 16px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #1C90D6;
}
</style>
</head>
<body>
<form action="http://localhost:8080/login" method="get">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
<input type="submit" value="登录">
</form>
</body>
</html>

效果如下:

后端部分

技术选型分析

首先我们画一个流程图来分析一下这个登录限制流程

从流程图上看,首先访问次数的统计与判断不是在登录逻辑执行后,而是执行前就加1了;
其次登录逻辑的成功与失败并不会影响到次数的统计;
最后还有一点流程图上没有体现出来,这个次数的统计是有过期时间的,当过期之后又可以重新登录了。

那为什么是Redis+Lua脚本呢?

Redis的选择不难看出,这个流程比较重要的是存在一个用来计数的变量,这个变量既要满足分布式读写需求,还要满足全局递增或递减的需求,那Redis的incr方法是最优选了。
那为什么需要Lua脚本呢?流程上在验证用户操作前有些操作,如图:

这里至少有3步Redis的操作,get、incr、expire,如果全放到应用里面来操作,有点慢且浪费资源。

Lua脚本的优点如下:

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

最后为了增加功能的复用性,我打算使用Java注解的方式实现这个功能。

代码实现

项目结构如下

配置文件

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>LoginLimit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>LoginLimit</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--切面依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.properties

## Redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=1000
## Jedis配置
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.max-active=2000
spring.redis.jedis.pool.max-wait=10000

注解部分

LimitCount.java
package com.example.loginlimit.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 次数限制注解
* 作用在接口方法上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitCount {
/**
* 资源名称,用于描述接口功能
*/
String name() default "";

/**
* 资源 key
*/
String key() default "";

/**
* key prefix
*
* @return
*/
String prefix() default "";

/**
* 时间的,单位秒
* 默认60s过期
*/
int period() default 60;

/**
* 限制访问次数
* 默认3次
*/
int count() default 3;
}
核心处理逻辑类:LimitCountAspect.java
package com.example.loginlimit.aspect;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;

import com.example.loginlimit.annotation.LimitCount;
import com.example.loginlimit.util.IPUtil;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Slf4j
@Aspect
@Component
public class LimitCountAspect {

private final RedisTemplate<String, Serializable> limitRedisTemplate;

@Autowired
public LimitCountAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}

@Pointcut("@annotation(com.example.loginlimit.annotation.LimitCount)")
public void pointcut() {
// do nothing
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();

MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
LimitCount annotation = method.getAnnotation(LimitCount.class);
//注解名称
String name = annotation.name();
//注解key
String key = annotation.key();
//访问IP
String ip = IPUtil.getIpAddr(request);
//过期时间
int limitPeriod = annotation.period();
//过期次数
int limitCount = annotation.count();

ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix() + "_", key, ip));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
log.info("IP:{} 第 {} 次访问key为 {},描述为 [{}] 的接口", ip, count, keys, name);
if (count != null && count.intValue() <= limitCount) {
return point.proceed();
} else {
return "接口访问超出频率限制";
}
}

/**
* 限流脚本
* 调用的时候不超过阈值,则直接返回并执行计算器自加。
*
* @return lua脚本
*/
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}

}

获取IP地址的功能我写了一个工具类IPUtil.java,代码如下:

package com.example.loginlimit.util;

import javax.servlet.http.HttpServletRequest;

public class IPUtil {

private static final String UNKNOWN = "unknown";

protected IPUtil() {

}

/**
* 获取 IP地址
* 使用 Nginx等反向代理软件, 则不能通过 request.getRemoteAddr()获取 IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,
* X-Forwarded-For中第一个非 unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}

}

另外就是Lua限流脚本的说明,脚本代码如下:

private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}

这段脚本有一个判断, tonumber(c) > tonumber(ARGV[1])这行表示如果当前key 的值大于了limitCount,直接返回;否则调用incr方法进行累加1,且调用expire方法设置过期时间。

最后就是RedisConfig.java,代码如下:

package com.example.loginlimit.config;

import java.io.IOException;
import java.io.Serializable;
import java.time.Duration;
import java.util.Arrays;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

@Value("${spring.redis.password}")
private String password;

@Value("${spring.redis.timeout}")
private int timeout;

@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;

@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;

@Value("${spring.redis.database:0}")
private int database;

@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
if (StringUtils.isNotBlank(password)) {
return new JedisPool(jedisPoolConfig, host, port, timeout, password, database);
} else {
return new JedisPool(jedisPoolConfig, host, port, timeout, null, database);
}
}

@Bean
JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
redisStandaloneConfiguration.setDatabase(database);

JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration
.builder();
jedisClientConfiguration.connectTimeout(Duration.ofMillis(timeout));
jedisClientConfiguration.usePooling();
return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration.build());
}

@Bean(name = "redisTemplate")
@SuppressWarnings({"rawtypes"})
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用 fastjson 序列化
JacksonRedisSerializer jacksonRedisSerializer = new JacksonRedisSerializer<>(Object.class);
// value 值的序列化采用 fastJsonRedisSerializer
template.setValueSerializer(jacksonRedisSerializer);
template.setHashValueSerializer(jacksonRedisSerializer);
// key 的序列化采用 StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

template.setConnectionFactory(redisConnectionFactory);
return template;
}

//缓存管理器
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory);
return builder.build();
}

@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
public KeyGenerator wiselyKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
Arrays.stream(params).map(Object::toString).forEach(sb::append);
return sb.toString();
};
}

@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

class JacksonRedisSerializer<T> implements RedisSerializer<T> {
private Class<T> clazz;
private ObjectMapper mapper;

JacksonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
this.mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
}

@Override
public byte[] serialize(T t) throws SerializationException {
try {
return mapper.writeValueAsBytes(t);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}

@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes.length <= 0) {
return null;
}
try {
return mapper.readValue(bytes, clazz);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

LoginController.java
package com.example.loginlimit.controller;

import javax.servlet.http.HttpServletRequest;

import com.example.loginlimit.annotation.LimitCount;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class LoginController {

@GetMapping("/login")
@LimitCount(key = "login", name = "登录接口", prefix = "limit")
public String login(
@RequestParam(required = true) String username,
@RequestParam(required = true) String password, HttpServletRequest request) throws Exception {
if (StringUtils.equals("张三", username) && StringUtils.equals("123456", password)) {
return "登录成功";
}
return "账户名或密码错误";
}

}
LoginLimitApplication.java
package com.example.loginlimit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LoginLimitApplication {

public static void main(String[] args) {
SpringApplication.run(LoginLimitApplication.class, args);
}

}

演示一下效果

上面这套限流的逻辑感觉用在小型或中型的项目上应该问题不大,不过目前的登录很少有直接锁定账号不能输入的,一般都是弹出一个验证码框,让你输入验证码再提交。我觉得用我这套逻辑改改应该不成问题,核心还是接口尝试次数的限制嘛!刚好我还写过SpringBoot生成图形验证码的文章:SpringBoot整合kaptcha实现图片验证码功能,哪天再来试试这套逻辑~

与三次输错密码后,系统是怎么做到不让我继续尝试的?相似的内容:

三次输错密码后,系统是怎么做到不让我继续尝试的?

# 故事背景 忘记密码这件事,相信绝大多数人都遇到过,输一次错一次,错到几次以上,就不允许你继续尝试了。 但当你尝试重置密码,又发现新密码不能和原密码重复: ![](https://img2023.cnblogs.com/blog/1127399/202305/1127399-20230511195

聊聊基于Alink库的推荐系统

概述 Alink提供了一系列与推荐相关的组件,从组件使用得角度来看,需要重点关注如下三个方面: 算法选择 推荐领域有很多算法,常用的有基于物品/用户的协同过滤、ALS、FM算法等。对于不同的数据场景,算法也会在计算方式上有很大的变化。 推荐方式 输入信息可以有多种选择,输入结果也有多种情况。 同时输

[转帖]“三次握手,四次挥手”你真的懂吗?

https://cloud.tencent.com/developer/article/1953965?areaSource=104001.110&traceId=7WZNP412yK3vh7ebw4th0 记得刚毕业找工作面试的时候,经常会被问到:你知道“3次握手,4次挥手”吗?这时候我会“胸有成

能将三次握手讲到这个程度,不给你offer给谁!

摘要:在后端相关岗位的入职面试中,三次握手的出场频率非常的高,甚至说它是必考题也不为过。 本文分享自华为云社区《能将三次握手理解到这个深度,面试官拍案叫绝~》,作者:龙哥手记。 在后端相关岗位的入职面试中,三次握手的出场频率非常的高,甚至说它是必考题也不为过。一般的答案都是说客户端如何发起 SYN

自己理解的TCP三次握手

### TCP 三次握手过程是怎样的? TCP的建立连接是通过三次握手来进行的。三次握手的过程如下图: 说实话这个很好理解,我称之为N字型 首先我们理解到建立连接是一个虚的概念了对吧?那么我们来设计一个可靠的TCP,首先建立连接是必须的吧?相当于我们打电话,总要先说一句喂 wei?(面向连接正是这个

[转帖]TCP三次握手详解,滑动窗口,拥塞窗口,网络包路由过程,全连接队列,半连接队列

众所周知,网络分层有传统的OSI七层模型和后来的基于TCP/IP的四层模型: 那么在一次网络的传输过程中具体的流程是怎么样的,我们先从一个数据包的传输说起(以TCP为例): TCP协议根据上层应用提供的信息生成TCP报文 TCP报文在交由下面的IP层(网络层)进行处理,委托IP模块将TCP报文封装成

[转帖]linux tcp 半连接队列和全连接队列

TCP建立连接的“三次握手”过程 上图就是tcp建联的三次握手过程。 Server端需要先调用bind()方法,绑定ip和端口号,再调用listen()方法,然后就可以等待来自Client连接了Client 调用connect()后,就会发送SYN包到Server,此时Client端处理SYN_SE

[转帖]计算机网络【TCP的序号 确认号详解 TCP三次握手 和 四次挥手】

文章目录 初始TCP三次握手--建立连接再聊TCP的序号和确认号TCP建立连接--三次握手为什么需要三次握手,二次握手为什么不行?假如第三次握手失败,是如何处理的?TCP释放连接--四次挥手为什么断开连接需要4次挥手TCP释放连接--状态解读 初始TCP三次握手–建立连接 在发送方和接收方方收发TC

TCP连接的关键之谜:揭秘三次握手的必要性

在这篇文章中,我们将深入探讨TCP连接建立过程中的关键步骤——三次握手。三次握手是确保客户端和服务端之间建立可靠连接的重要过程。通过三次握手,双方可以确认彼此的接收和发送能力,并同步双方的初始序列号,从而确保连接的稳定性和可靠性。文章还解释了三次握手的原因,它可以避免历史重复连接的初始化,确保双方都...

后端每日一题 1:说一下三次握手

本文首发于公众号:腐烂的橘子 三次握手的流程 第 1 步 - 初始连接请求 SYN(Synchronize) 服务端状态 LISTEN,客户端向服务端发送一个 SYN 标志位的报文段(TCP segment) 这个报文段包含初始序列号 x,以及最大报文段大小等字段 客户端发送报文后,状态设置为 SY