浏览器输入网址后的完整流程(大白话版)
一、URL解析 - 看看你要去哪
首先浏览器会分析你输入的网址,比如 https://www.taobao.com/product/detail?id=123
它会拆分成:
- 协议:https(说明要加密传输)
- 域名:www.taobao.com
- 路径:/product/detail
- 参数:id=123
就像你要去一个地方,得先知道是坐飞机还是坐火车(协议),去哪个城市(域名),具体哪条街(路径)。
二、DNS解析 - 把域名翻译成IP地址
为什么需要这步?
因为计算机只认识IP地址(比如 192.168.1.1),不认识域名。就像你说"去王府井",但导航需要具体的经纬度。
查找顺序(从近到远):
-
浏览器缓存:先看看浏览器记不记得这个域名对应的IP
比如你刚访问过淘宝,浏览器会记住:www.taobao.com → 47.96.123.45 -
操作系统缓存:浏览器不记得,问问操作系统
-
本地hosts文件:看看有没有手动配置过
/etc/hosts 或 C:\Windows\System32\drivers\etc\hosts 127.0.0.1 localhost -
路由器缓存:问问路由器记不记得
-
DNS服务器查询:以上都没有,就要去DNS服务器问了
- 本地DNS服务器(一般是运营商提供的)
- 根DNS服务器 → .com域的DNS → taobao.com的DNS
- 最终拿到IP地址:47.96.123.45
三、建立TCP连接 - 三次握手
拿到IP地址后,要和服务器建立连接。这就是著名的三次握手。
为什么要三次握手?
打个比方,你要和朋友约饭:
你: "明天中午有空吗?" (第一次握手 - SYN)
朋友:"有空,你也有空吗?" (第二次握手 - SYN+ACK)
你: "有空,那就这么定了!" (第三次握手 - ACK)
三次才能确认双方都能收发消息。
技术角度:
客户端 → 服务器:SYN(我想和你建立连接)
服务器 → 客户端:SYN+ACK(好的,我同意,你收到了吗?)
客户端 → 服务器:ACK(收到了,开始吧)
四、TLS/SSL握手 - HTTPS加密(如果是HTTPS)
如果是HTTPS,还要多一步加密握手。这步比较复杂,但目的很简单:协商一个只有你和服务器知道的密码。
通俗理解:
就像你和朋友要说悄悄话,得先确认一个暗号。
具体步骤:
第一步:打招呼,交换信息
客户端说:"我支持这些加密方式:AES、RSA..."
服务器说:"好,我选AES,这是我的证书(身份证)"
第二步:验证身份
客户端检查服务器的证书:
- 是不是正规CA机构颁发的?(就像检查身份证是不是公安局发的)
- 有没有过期?
- 是不是这个网站的证书?
第三步:生成密钥
双方各自生成一个随机数
然后一起算出一个对称密钥(共享密码)
后面所有通信都用这个密钥加密
为什么这么复杂?
为了防止中间人攻击。就像你和朋友说悄悄话,得先确认对方不是冒充的。
五、发送HTTP请求 - 告诉服务器你要什么
连接建立好了,可以发请求了!
请求长这样:
GET /api/user/info?userId=123 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Chrome浏览器)
Cookie: sessionId=abc123xyz (登录凭证)
Accept: application/json
Connection: keep-alive
通俗理解:
- GET:我要查询数据
- POST:我要提交数据
- Host:告诉服务器是访问哪个网站(一台服务器可能部署多个网站)
- Cookie:身份凭证,证明你是谁(已登录)
六、服务器处理请求 - 后端的活(重点)
这是我们Java后端最熟悉的部分!
6.1 请求到达服务器
用户请求 ↓
DNS解析 ↓
【负载均衡器】(Nginx/LVS)↓
根据策略分发到某台应用服务器↓
【防火墙/WAF】安全检查↓
到达应用服务器
负载均衡的作用:
就像银行有很多窗口,负载均衡器负责把客户分配到不同窗口,避免某个窗口排队太长。
6.2 Web容器接收(Tomcat/Jetty)
Tomcat接收到HTTP请求
创建HttpServletRequest对象
交给应用处理
6.3 Spring MVC处理流程(核心)
这是我们写代码的地方!
// ========== 第1步:DispatcherServlet(前端控制器)==========
// 所有请求的入口,统一接收// ========== 第2步:HandlerMapping(找到对应的Controller)==========
// 根据URL找到处理的方法
// GET /api/user/info → UserController.getUserInfo()// ========== 第3步:拦截器前置处理 ==========
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 检查是否登录String token = request.getHeader("token");if (StringUtils.isEmpty(token)) {response.setStatus(401);return false; // 拦截,不继续执行}return true; // 放行}
}// ========== 第4步:执行Controller ==========
@RestController
@RequestMapping("/api/user")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/info")public Result getUserInfo(@RequestParam Long userId) {System.out.println("接收到请求,查询用户ID:" + userId);// 调用Service层UserInfo userInfo = userService.getUserInfoById(userId);return Result.success(userInfo);}
}// ========== 第5步:Service业务处理 ==========
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic UserInfo getUserInfoById(Long userId) {// 1. 先查缓存(Redis)String cacheKey = "user:info:" + userId;UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {System.out.println("缓存命中,直接返回");return userInfo;}// 2. 缓存没有,查数据库System.out.println("缓存未命中,查询数据库");userInfo = userMapper.selectById(userId);// 3. 放入缓存if (userInfo != null) {redisTemplate.opsForValue().set(cacheKey, userInfo, 30, TimeUnit.MINUTES);}return userInfo;}
}// ========== 第6步:DAO层查询数据库 ==========
@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {// MyBatis会自动生成SQL:// SELECT * FROM user_info WHERE id = #{userId}
}// ========== 第7步:拦截器后置处理 ==========
// 记录日志、统计耗时等// ========== 第8步:统一异常处理 ==========
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public Result handleException(Exception e) {log.error("系统异常", e);return Result.error("系统异常,请稍后重试");}
}// ========== 第9步:响应数据序列化(JSON) ==========
// Spring Boot默认用Jackson把对象转成JSON
// UserInfo对象 → {"id": 123, "name": "张三"}
6.4 涉及的中间件交互
Redis缓存:
// 为什么要用缓存?
// 数据库查询慢(几十毫秒),Redis查询快(1-2毫秒)
// 减轻数据库压力// 缓存策略
1. 先查缓存,有就直接返回
2. 没有再查数据库,然后放入缓存
3. 设置过期时间,避免数据过期
数据库连接池(Druid/HikariCP):
// 为什么要连接池?
// 每次建立数据库连接很慢(几百毫秒)
// 连接池提前创建好连接,用的时候直接拿// 配置
spring:datasource:hikari:maximum-pool-size: 20 # 最多20个连接minimum-idle: 5 # 最少保持5个空闲连接connection-timeout: 3000 # 获取连接超时时间
消息队列(RabbitMQ/Kafka):
// 为什么要用MQ?
// 1. 异步处理:用户下单后,发送短信通知可以异步做
// 2. 削峰填谷:秒杀时,订单先进队列,慢慢处理@Service
public class OrderService {@Autowiredprivate RabbitTemplate rabbitTemplate;public void createOrder(Order order) {// 保存订单orderMapper.insert(order);// 发送消息到MQ,异步发送短信rabbitTemplate.convertAndSend("order.exchange", "order.created", order);}
}
6.5 微服务架构下的调用
如果是微服务,可能还要调用其他服务:
@Service
public class OrderService {@Autowiredprivate UserServiceClient userServiceClient; // Feign客户端@Autowiredprivate ProductServiceClient productServiceClient;public OrderInfo createOrder(Long userId, Long productId) {// 1. 调用用户服务,检查用户信息UserInfo user = userServiceClient.getUserById(userId);if (user == null) {throw new BusinessException("用户不存在");}// 2. 调用商品服务,检查库存ProductInfo product = productServiceClient.getProductById(productId);if (product.getStock() <= 0) {throw new BusinessException("库存不足");}// 3. 扣减库存(RPC调用)productServiceClient.decreaseStock(productId, 1);// 4. 创建订单Order order = new Order();order.setUserId(userId);order.setProductId(productId);orderMapper.insert(order);return order;}
}
七、返回HTTP响应 - 把结果发回去
服务器处理完后,要把结果返回给浏览器:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 156
Set-Cookie: sessionId=xyz789; HttpOnly; Secure
Cache-Control: max-age=3600
Access-Control-Allow-Origin: *{"code": 200,"message": "success","data": {"userId": 123,"username": "张三","avatar": "https://cdn.example.com/avatar/123.jpg"}
}
响应头的含义:
- 200 OK:成功(还有404未找到、500服务器错误等)
- Content-Type:返回的是JSON格式数据
- Set-Cookie:设置Cookie(下次请求会自动带上)
- Cache-Control:缓存控制,3600秒内可以用浏览器缓存
八、浏览器接收响应 - 拿到结果
浏览器接收到响应后:
- 解析响应头:看看状态码、Content-Type等
- 处理Cookie:保存Cookie,下次自动带上
- 解析响应体:把JSON数据解析成JavaScript对象
// 前端JavaScript代码
fetch('/api/user/info?userId=123').then(response => response.json()).then(data => {console.log('用户信息:', data);// 然后更新页面显示document.getElementById('username').textContent = data.data.username;});
九、关闭TCP连接 - 四次挥手
请求完成后,可能会关闭连接(如果不用keep-alive的话):
客户端 → 服务器:FIN(我说完了)
服务器 → 客户端:ACK(知道了)
服务器 → 客户端:FIN(我也说完了)
客户端 → 服务器:ACK(知道了,再见)
为什么是四次?
因为TCP是全双工的,双方都要确认关闭。就像打电话,双方都要说"我挂了"。
HTTP Keep-Alive:
为了性能优化,现在大多保持连接不关闭
一个TCP连接可以发多个HTTP请求
Connection: keep-alive
十、性能优化点(面试加分项)
1. DNS优化
- DNS缓存:减少DNS查询次数
- DNS预解析:<link rel="dns-prefetch" href="//cdn.example.com">
- 使用CDN:让用户访问离他最近的服务器
2. 连接优化
- HTTP/2:一个连接多路复用,并行请求
- Keep-Alive:连接复用
- 减少重定向:每次重定向都要额外请求
3. 后端优化(我们能做的)
缓存优化:
// 多级缓存
1. 本地缓存(Caffeine):最快,但不能共享
2. 分布式缓存(Redis):共享,稍慢
3. 数据库:最慢@Cacheable(value = "userInfo", key = "#userId")
public UserInfo getUserInfoById(Long userId) {return userMapper.selectById(userId);
}
数据库优化:
// 1. 索引优化
CREATE INDEX idx_user_id ON orders(user_id);// 2. 避免N+1查询问题
// 错误示例:循环查询
for (Long userId : userIds) {UserInfo user = userInfoApi.getUserInfoById(userId); // 查N次
}// 正确示例:批量查询
List<UserInfo> users = userInfoApi.getUserInfoListByIds(userIds); // 查1次
Map<Long, UserInfo> userMap = users.stream().collect(Collectors.toMap(UserInfo::getId, Function.identity()));// 3. 读写分离:查询走从库,写入走主库
// 4. 分库分表:数据量大了,拆分表
异步处理:
@Async
public CompletableFuture<UserInfo> getUserInfoAsync(Long userId) {UserInfo userInfo = userMapper.selectById(userId);return CompletableFuture.completedFuture(userInfo);
}// 并行查询多个接口
CompletableFuture<UserInfo> userFuture = getUserInfoAsync(userId);
CompletableFuture<OrderList> orderFuture = getOrderListAsync(userId);CompletableFuture.allOf(userFuture, orderFuture).join();
限流降级(高并发场景):
// 使用Sentinel限流
@SentinelResource(value = "getUserInfo", blockHandler = "handleBlock")
public UserInfo getUserInfo(Long userId) {return userMapper.selectById(userId);
}public UserInfo handleBlock(Long userId, BlockException e) {// 限流后返回默认值或提示return UserInfo.getDefault();
}
4. 响应优化
- Gzip压缩:减少传输大小
- 静态资源CDN:JS、CSS、图片等放CDN
- HTTP缓存:设置Cache-Control,减少请求
十一、完整时间线(实际耗时)
DNS解析: 20-120ms
TCP握手: 30-50ms(1个RTT)
TLS握手: 60-100ms(2个RTT,TLS 1.2)
HTTP请求: 10-30ms
服务器处理: 50-200ms(取决于业务复杂度)- 缓存命中: 1-5ms- 数据库查询: 10-50ms- RPC调用: 10-100ms
HTTP响应: 10-30ms
-------------------
总计: 180-530ms
优化后(理想情况):
DNS缓存命中: 0ms
HTTP/2复用: 0ms(连接已建立)
Redis缓存命中: 1-2ms
总计: 10-30ms
十二、常见面试追问
Q1:为什么TCP是三次握手,不是两次?
两次握手的问题:
如果第一次的SYN包在网络中延迟了,客户端以为丢了,又发了一次
服务器两次都响应,建立了两个连接,浪费资源三次握手可以防止这种情况
Q2:HTTPS一定安全吗?
不一定,可能有这些风险:
1. 证书伪造(中间人攻击)
2. SSL降级攻击
3. 证书过期或不受信任
4. 协议漏洞(如早期的心脏滴血漏洞)所以要:
- 使用正规CA颁发的证书
- 使用最新的TLS版本
- 配置强加密套件
Q3:如何优化接口响应时间?
1. 加缓存(Redis):毫秒级响应
2. 异步处理:不需要立即返回的操作异步做
3. 数据库优化:索引、批量查询
4. 减少RPC调用:能合并的合并
5. 使用CDN:静态资源加速
6. 负载均衡:分散压力
Q4:如何保证接口的幂等性?
// 场景:用户重复点击提交按钮,如何避免重复下单?@Service
public class OrderService {@Autowiredprivate RedisTemplate redisTemplate;public OrderInfo createOrder(String idempotentKey, OrderDTO orderDTO) {// 使用Redis的SETNX实现幂等Boolean success = redisTemplate.opsForValue().setIfAbsent("order:idempotent:" + idempotentKey, "1", 5, TimeUnit.MINUTES);if (!success) {throw new BusinessException("请勿重复提交");}// 创建订单Order order = new Order();// ... 订单逻辑return order;}
}
Q5:如何应对高并发?
1. 缓存:减少数据库压力
2. 限流:保护系统不被打垮
3. 降级:非核心功能暂时关闭
4. 熔断:下游服务挂了,快速失败
5. 异步:MQ削峰填谷
6. 扩容:加机器(弹性伸缩)
7. 数据库:读写分离、分库分表
8. CDN:静态资源加速
总结
完整流程:
- URL解析
- DNS解析(域名→IP)
- TCP三次握手
- TLS握手(HTTPS)
- 发送HTTP请求
- 服务器处理(负载均衡 → Web容器 → Spring MVC → Service → DAO → 数据库/缓存)
- 返回HTTP响应
- TCP四次挥手(或保持连接)
关键点:
- 理解整体流程
- 知道每个环节的作用和可能的优化点
- 能结合实际项目经验讲解
- 注重性能优化和高并发处理
面试建议:
- 先讲主流程,面试官感兴趣会深入问
- 结合自己项目经验,说说实际遇到的问题和解决方案
- 提到性能优化点可以加分
- 不知道的不要硬编,诚实说不清楚,但可以说说自己的理解
附录:常用命令
# 查看DNS解析
nslookup www.baidu.com
dig www.baidu.com# 查看路由
traceroute www.baidu.com# 抓包看HTTP请求
tcpdump -i any -X port 80# 查看TCP连接
netstat -an | grep ESTABLISHED# 测试接口响应时间
curl -w "@curl-format.txt" -o /dev/null -s "http://example.com/api"
curl-format.txt:
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_starttransfer: %{time_starttransfer}\n
time_total: %{time_total}\n