在 等待连接模块流程 中,调用 socket()
函数时 不需要传递 IP 和端口信息,因为 socket()
函数的作用是创建一个套接字。具体的 IP 和端口是在后续的 bind()
函数中进行绑定
服务器部分主要分两个模块
- 等待连接模块
- 与客户端通信模块
如下图
综述
在 等待连接模块流程 中,调用 socket()
函数时 不需要传递 IP 和端口信息 ,因为 socket()
函数的作用是创建一个套接字。具体的 IP 和端口是在后续的 bind()
函数中进行绑定。创建的套接字描述符被存储在服务器端,供后续操作使用。
具体流程
具体流程解释
c
int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字
bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 绑定
listen(listenfd, 5); // 设置为监听状态
上述过程只会创建监听套接字socket,- 它只负责 接收 来自客户端的连接请求。
- 服务器不会用它来进行实际的数据传输。所有的数据传输都将通过新创建的 连接套接字 来完成。
详细解释如下
创建套接字
-
-
socket()
函数 的作用是创建一个套接字描述符。 -
参数:
-
- 第一个参数:协议族(如
AF_INET
表示 IPv4,AF_INET6
表示 IPv6)。 - 第二个参数:套接字类型(如
SOCK_STREAM
表示 TCP,SOCK_DGRAM
表示 UDP)。 - 第三个参数:协议编号(通常传
0
,表示使用默认协议)。
- 第一个参数:协议族(如
-
调用形式示例:
iniint sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
注意:此时还没有涉及 IP 和端口绑定!
-
绑定 IP 和端口
-
-
IP 和端口信息在调用
bind()
函数 时提供,用于将套接字与特定的 IP 地址和端口绑定。 -
调用形式示例:
inistruct sockaddr_in server_addr; server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到本地所有可用 IP server_addr.sin_port = htons(8080); // 端口号,网络字节序 bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
-
-
客户端的 IP 和端口:
-
-
客户端的 IP 和端口信息会在服务器调用
accept()
函数时,通过返回的struct sockaddr_in
结构体获取。 -
示例:
inistruct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
-
在accept函数调用会被阻塞,直到cilent
client 尝试连接
在执行完 accept()
之后,服务器端的套接字才正式为特定的客户端连接服务。以下是更详细的解释:
accept()
函数的作用:
- 等待客户端连接:
-
accept()
会阻塞当前进程,直到有客户端尝试连接服务器的监听套接字。
- 建立连接:
-
- 当有客户端发起连接并通过三次握手成功建立连接时,
accept()
函数会完成: -
- 从监听套接字(
listen
)中抽取该连接的请求。 - 创建一个新的套接字(用于通信的连接套接字)与该客户端进行通信。
- 从监听套接字(
- 当有客户端发起连接并通过三次握手成功建立连接时,
- 返回新的套接字描述符:
-
accept()
会返回一个 新的套接字描述符 (类型为int
),这个描述符指向服务器端与该客户端连接的专用通信通道。- 注意:
-
- 这个新描述符与原来的监听套接字是独立的。
- 原监听套接字仍然负责继续监听其他客户端的连接请求。
accept()
后的行为:
-
新的套接字描述符:
-
-
服务器可以通过这个新套接字描述符,与客户端进行数据的发送与接收(
read/write
或send/recv
)。 -
示例:
scssstruct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); // accept 返回新的套接字描述符 int new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len); if (new_sockfd < 0) { perror("accept failed"); exit(EXIT_FAILURE); } // new_sockfd 可用于与客户端通信
-
-
监听套接字:
-
- 原监听套接字(
sockfd
)不受影响,继续监听其他连接请求。
- 原监听套接字(
为什么还需要描述符?
在套接字刚刚创建好,
还没有建立连接的状态下,这4 种信息是不全的。此外,为了指代一个套
接字,使用一种信息(描述符)比使用4 种信息要简单。出于上面两个原
因,应用程序和协议栈之间是使用描述符来指代套接字的
光信号到网卡:转数字信号
服务器接收到光信号会转换成是数字信号,
网卡的MAC 模块将网络包从信号还原为数字信息
然后传递给操作系统协议栈校验FCS 并存入缓冲区。
然后,网卡需要通过中断将网络包到达的事件通知给CPU。
接下来,CPU 就会暂停当前的工作,并切换到网卡的任务。然后,网卡驱动会开始运行,从网卡缓冲区中将接收到的包读取出来,根据MAC 头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件
当网卡接收到数据帧时,它会查看帧的 MAC(媒体访问控制)头部,以确定数据的协议类型。然后,网卡驱动程序根据该协议类型,将数据传递给对应的协议栈进行处理。 详细解释:
-
MAC头部的结构: 以太网帧的 MAC 头部包含以下字段:
- 目标 MAC 地址(Destination MAC Address): 指明数据帧的接收者。
- 源 MAC 地址(Source MAC Address): 指明数据帧的发送者。
- 以太类型(EtherType): 指示上层协议的类型,例如 IPv4、IPv6 或 ARP。
-
以太类型(EtherType): 该字段用于标识帧中封装的上层协议类型。常见的取值包括:
0x0800
:表示帧携带的是 IPv4 数据包。0x0806
:表示帧携带的是 ARP 协议数据。0x86DD
:表示帧携带的是 IPv6 数据包。
-
网卡驱动的处理流程:
-
接收数据帧: 网卡从物理介质(如以太网)接收数据帧。
-
解析 MAC 头部: 网卡驱动读取帧的 MAC 头部,特别是 EtherType 字段,以确定上层协议类型。
-
协议分发: 根据 EtherType 的值,网卡驱动将数据帧的有效载荷传递给对应的协议栈模块。例如:
- 如果 EtherType 是
0x0800
,则将数据传递给 IPv4 协议栈。 - 如果 EtherType 是
0x0806
,则传递给 ARP 协议处理模块。
- 如果 EtherType 是
-
数字信号传递给协议栈:IP模块
协议栈,最先由IP模块进行处理,,检查IP 头部。
- IP 模块首先会检查IP 头部的格式是否符合规范,
- 然后检查接收方IP 地址 如果不是发给自己的扔掉,或者转发
- 确认包是发给自己的之后,接下来需要检查包有没有被分片
检查IP头部的内容就可以知道是否分片B,如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包;如果没有分片,则直接保留接收时的样子
如何判断IP数据是否被分片
在 IP 数据报的接收过程中,主机需要判断接收到的数据包是否为分片。这是通过检查 IP 头部中的 标志(Flags) 和 片偏移(Fragment Offset) 字段来实现的。
参考文档: blog.csdn.net/u013669912/...
一般来说,我不会希望在IP层进行数据分片,因为IP并没有TCP可靠,所以我们会在TCP进行分片。
IP 头部相关字段:
-
标志(Flags): 该字段占 3 位,其中两位与分片相关:
- DF(Don't Fragment): 禁止分片位。当 DF=1 时,表示不允许对数据报进行分片;当 DF=0 时,表示允许分片。
- MF(More Fragments): 更多分片位。当 MF=1 时,表示后续还有分片;当 MF=0 时,表示这是最后一个分片或数据报未被分片。
-
片偏移(Fragment Offset): 该字段占 13 位,表示当前分片在原始数据报中的相对位置,单位为 8 字节。
判断数据报是否被分片的方法:
- 未分片的数据报: 标志字段中的 MF 位为 0,且片偏移字段为 0。
- 分片的数据报: 标志字段中的 MF 位为 1,或片偏移字段不为 0。
处理流程:
-
检查标志字段:
- 如果 MF=0 且片偏移=0,表示数据报未被分片,可直接处理。
- 如果 MF=1 或片偏移≠0,表示数据报被分片,需要进行重组。
-
分片重组:
- 将所有分片暂存,直到接收到所有分片。
- 根据片偏移字段,将分片按正确顺序组装还原为完整的数据报。
如何防止IP分片产生
以太网(Ethernet),MTU 默认为 1500 字节,这是以太网帧中数据部分(即有效载荷)的最大大小。这个大小不包括以太网帧头部和尾部的开销。
在TCP层面,都会协商双方的MSS来确保尽可能的不要在IP层进行数据分片。
在 TCP 协议中,双方在建立连接时会协商各自的 最大报文段长度(MSS,Maximum Segment Size),以确保传输的数据段大小适合网络路径的 最大传输单元(MTU,Maximum Transmission Unit),从而尽可能避免在 IP 层进行数据分片。
详细说明:
-
MSS 的协商:
- 在 TCP 三次握手过程中,通信双方会在 SYN 报文中包含各自支持的 MSS 值。
- 双方会选择较小的 MSS 值作为该连接的最大报文段长度,确保数据段不会超过路径中最小的 MTU。
-
避免 IP 层分片:
- 通过协商 MSS,TCP 层可以将数据段大小限制在合适范围内,使得封装后的 IP 数据报不会超过链路层的 MTU。
- 这样,IP 层无需对数据报进行分片,减少了分片带来的开销和潜在问题。
补充说明:
-
路径 MTU 发现(Path MTU Discovery):
- 即使通过 MSS 协商,路径中的某些链路可能具有更小的 MTU。
- 为此,TCP 可以使用路径 MTU 发现机制,动态确定路径上的最小 MTU,进一步避免 IP 层分片。
-
如果封装后的 IP 数据报 超过了链路层的 MTU (比如 1500 字节),即使 TCP 层 控制了 MSS ,还是会发生 IP 分片 ,因为 MTU 限制了每个帧的大小。
-
在某些情况下,即使 TCP 层 调整了 MSS ,链路路径中的某个网络设备的 MTU 可能较小,导致 IP 数据报 必须被分片。
处理完上述的步骤,还原一个完整IP报文给TCP ,IP 层在将数据传递给上层协议(如 TCP)时,只传递有效载荷部分,不包括 IP 头部。
接下来需要检查IP 头部的协议号字段,并将包转交给相应的模块。例如,如果协议号为06(十六进制),则将包转交给TCP 模块;如果是11(十六进制),则转交给UDP 模块,下面将会以TCP举例子
下一站 TCP模块
首先 TCP 会先判断这个头部的控制位是不是 SYN 标志,如果是,则会进入等待链接模块建立新的 socket。
client 尝试建立TCP链接;服务器进入等待连接模块
当客户端尝试连接服务器的某个端口(例如 80 端口)时,服务器的处理流程如下:
-
接收 SYN 报文:
客户端发送一个 TCP 报文,其中 SYN 标志位置为 1,表示请求建立连接。
-
检查目标端口:
服务器的 TCP 模块检查该报文的目标端口号(此处为 80),以确定是否有套接字在该端口处于监听(LISTEN)状态。
-
处理逻辑:
- 存在监听套接字:
服务器确认有程序在 80 端口监听,继续进行三次握手,与客户端建立连接。为这个套接字复制一个新的副本,并将发送方 IP 地址、端口号、序号初始值、窗口大小等必要的参数写入这个新的 socket 中,并分配用于发送缓冲区和接收缓冲区的内存空间。 - 不存在监听套接字:
服务器发现 80 端口没有监听套接字,无法处理该连接请求。
服务器发送一个 RST(复位)报文给客户端,通知其连接请求被拒绝,并向客户端返回一个表示接收方端口不存在等待连接的套接字的 ICMP 消息。
- 存在监听套接字:
服务器进入通信模块
之后会进入客户端通信模块
TCP模块会尝试拼接数据,拼合数据块的操作在每次收到数据包时都会进行,而不是等所有数据全部
接受完毕之后再统一拼合的。
当断开操作完成后,套接字会在经过一段时间后被删除,因为当 服务器 在 四次挥手 的 第二步 发送 ACK 报文确认客户端的 FIN 后,客户端 会发送最后一个 ACK 并进入 TIME_WAIT
状态,等待确认是否有丢失的报文。如果此时服务器直接删除套接字,无法再接收来自客户端的最终确认,这会导致 连接关闭不完全。
通过延迟删除套接字,服务器确保在一段时间内保持与客户端的通信状态,从而确保最终的client ACK 报文能够正确到达。如果没有 TIME_WAIT
期间的这段时间, 而恰好有新的连接使用这个端口,但是之前的client延迟的关闭 ACK数据包才到来,会导致这个连接被直接关闭
之后进入服务器应用程序 处理http请求包裹
应用程序处理HTTPS报文
Web 服务器中, 服务器程序会根据收到的请求消息中的内容进行相应的处理,如下图,HTTPS报文包含请求方法GET + 请求 URI
请求消息包括一个称为"方法"的命 令,以及表示数据源的 URI(文件路径名), 服务器程序会根据这些内容向 客户端返回数据,但对于不同的方法和 URI
但是URI并不代表真实的服务器目录, Web 服务器公开的目录其实并不是磁盘上的实际目录, 因为这样 Web 服务器的磁盘内容就全部暴露了,所以,当读取文件时,需要先查询虚拟目录与实际目录的对应关系,并将 URI 转换成实际的文件名后,才能读取文件并返回数据
下面举例子
在 Web 服务器中,虚拟目录是指将客户端请求的路径映射到服务器文件系统中的实际物理路径。这种映射关系通过服务器的配置文件进行设置,不同的 Web 服务器有不同的配置方法。以下是常见 Web 服务器(如 Apache、Nginx 和 Tomcat)中配置虚拟目录与实际目录映射的方法:
- Apache 服务器: 在 Apache 中,可以使用 Alias 指令将虚拟路径映射到实际的文件系统路径。 配置步骤: 打开 Apache 的配置文件(如 httpd.conf)。 添加 Alias 指令,例如:
php
Alias /virtual_path/ "/actual/path/"
<Directory "/actual/path/">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
上述配置将客户端请求的 /virtual_path/
映射到服务器上的 /actual/path/
目录。 保存配置文件并重启 Apache 服务器以使更改生效。
- Nginx 服务器: 在 Nginx 中,可以使用 location 指令结合 alias 或 root 指令来设置虚拟目录映射。 配置步骤: 打开 Nginx 的配置文件(如 nginx.conf)。 在对应的 server 块中,添加 location 配置,例如:
bash
location /virtual_path/ {
alias /actual/path/;
}
这里,/virtual_path/
是客户端请求的路径,/actual/path/
是服务器上的实际目录。 保存配置文件并重启 Nginx 服务器以应用更改。
一般来说,浏览器会将需要程序处理的数据放在 HTTP 请求消息中发送给服务器。这些数据有很多种类,例如购物网站订单表中的品名、数量、发货地址等,搜索引擎中输入的关键字也是一个常见的例子,这些都会在后台直接调用一个程序
浏览器发送给服务器的请求如何包含数据
1 种是在 HTML 文档的 表单中加上 method="GET",通过 HTTP 的 GET 方法,将输入的数据作为 参数添加在 URI 后面发送给服务器
一种方法是在 HTML 文档的表单 中加上 method="POST",将数据放在 HTTP 请求消息的消息体 A 中发送给 服务器
服务器会对请求进行访问控制
Web 服务器的访问控制规则主要有以下 3 种。 (1)客户端 IP 地址 (2)客户端域名 (3)用户名和密码
IP access control
首先是根据客户端 IP 地址设置的规则,这个情况很简单,在调用 accept 接受连接时,就已经知道客户端的 IP 地址了,只要检查其是否允许 访问就可以了
我之前有工单,客户开启了azure上的entra conditioanl access,有针对IP地址进行限制
这有两种方式,第一个是针对 IP地址,第二个是针对client domain
为了保险起见,还需要用这个域名查询一下 IP 地址,看看结果与发送方 IP 地址是否一致, 因为 有一 种在 DNS 服务器上注册假域名的攻击方式,因此我们需要进行双重检查
所以针对client domain 进行访问控制的方式比较慢