最近的一次面试中,遇到了 “浏览器中输入网址回车后底层发生了什么?”。在基于网络相关知识做了回答后,面试官向一个很有趣的方向进行了深挖 —— 报文抵达服务端后发生了什么?
当时大致讲了 socket 相关的一些知识,但是比较零散,逻辑有点混乱,因此去学习了 java网络编程的一些知识。在这里对这个过程做一个比较详细的小结整理:
明晰一些概念
Socket
我们知道进程分为 内核态 和 用户态,我们开发的服务端应用程序运行在用户态,而和网络通信相关的功能只能由内核态去执行。Socket 正是为了让用户态应用程序能够调用内核态网络相关功能而设计出来的,也就是说,Socket 是为用户态提供的一套 API,屏蔽了底层操作系统复杂的网络操作细节,使得用户态程序能够方便地调用内核态中 TCP/IP 协议栈相关的网络功能,充当了两者之间的桥梁。
Socket 是一种编程接口,它抽象了网络通信中的端点。一个 socket 对应一个 ip 地址和端口号的组合,用于唯一标识网络上的一个应用程序的通信端点。这一概念主要体现在传输层(例如 TCP 和 UDP),因为它们负责端到端的数据传输。不管是无连接的 UDP 还是面向连接的 TCP 协议,Socket 都是支持的。
两种 Socket 类型
我们根据 C/S 架构的分工,将 socket 分为下面两类:
1 ClientSocket
客户端的 Socket 的主要任务是与 ServerClient 建立连接,向 socket 中写入信息。
主要对应 Linux 中的方法:socket()、connect()、send()、close()等
2 ServerSocket
服务器端的 Socket 主要任务是监听指定端口,维护连接到该端口的 clientSocket,并从这些 clientSocket 中读取字符。
主要对应 Linux 中的方法:socket()、bind、listen()、accept()、recv()、close()等
下面是一张经典的 socket 函数流程图
服务端应用程序启动发生了什么?
在讨论报文抵达服务端后发生什么的问题之前,我们先捋一下在服务端应用程序启动时,是怎么做到对指定接口的监听的。
假设我们的应用程序是 springboot 应用程序,内嵌 tomcat 服务器,并通过配置文件指定端口。
在启动时,会读取到监听的端口,调用操作系统底层的 socket 接口:
- 用户态服务端程序在启动时,通过调用系统调用
socket()
创建一个或多个监听 socket,这个 socket 是操作系统提供的 TCP / IP 协议栈面向用户态的接口。 - 调用
socket.bind()
将 socket 和指定的端口绑定。 - 调用
socket.listen()
,socket 进入监听状态。 - 不断调用
socket.accept()
,阻塞等待接收客户端的连接请求。
到这里,我们的 springboot 应用程序通过内嵌服务器调用 socket 接口,完成了对指定接口的监听。
浏览器点击回车后发生了什么?
在浏览器点击了跳转后,会执行下面操作:
- 组装 HTTP 请求报文
- DNS 域名解析
- TCP 的三次握手
会用目标 ip 和端口生成 socket(socket()
),调用 connect()
尝试和目标建立连接。
在三次握手后,浏览器端的 socket 就已经和服务器端的 serverSocket 成功建立了连接。
后续就会通过这个 socket 通道,调用 write()
将之前的 http 请求报文发送到服务器端 socket。
报文抵达服务器端发送了什么?
之前提到 springboot 服务器端的内嵌服务器通过调用 accept()
阻塞等待建立连接的 clientSocket。现在获取到浏览器那边的 socket 后,通过循环调用 read()
去读取发送过来的 http 请求报文。
获取到报文后,内嵌服务器会对报文进行解析,解析成 HTTP 请求,并将 HTTP 请求分发给 Spring 的 DispatcherServlet。DispatcherServlet 根据配置的路由(如 @RequestMapping 注解)调用相应的 Controller 处理业务逻辑。因为本文的重点在操作系统的操作上,就不详细解释了。
处理结束后的 http 响应发送给内嵌服务器,内嵌服务器将其组装成如下图符合要求的 http 响应报文。调用 socket.send()
将响应报文发送回浏览器端。
完成✅
小结
这次面试的问题在操作系统层面解释了这个问题,确实纠正了之前对 socket 网络编程的一些误解。通过这 OS 层级捋这个流程,深入解答了网络服务器是如何接收到对应的报文并转化为请求的问题。