满满干货之Linux网络编程
Written with StackEdit中文版.
列举一下OSI协议的各种分层。说说你最熟悉的一层协议的功能
-
七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
-
五层划分为:应用层、传输层、网络层、数据链路层、物理层。
-
四层划分为:应用层、传输层、网络层、网络接口层。(TCP/IP协议对应模型)
各层功能
传输层 | 为通信双方的主机提供端到端的服务,有两个不同的传输协议TCP和UDP,TCP提供可靠交付,而UDP并不能保证可靠交付。 |
网络层 | 处理分组在网络中的活动,例如分组的选路。 |
网络接口层 | 处理与电缆(或其他任何传输媒介)的物理接口细节。 |
IP地址转换成物理地址的协议?反之?
- 将IP地址转换成物理地址的协议是ARP
- 反之是RARP
ARP协议工作流程: ①首先,每台主机都会在自己的ARP缓冲区( ARP Cache )中建立一个ARP列表,以表示IP地址和MAC地址的对应关系。
②当源主机需要将一个数据包要发送到目的主机时,会首先检查自己ARP列表中是否存在该IP地址对应的MAC地址,如果有﹐就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。
③网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个ARP响应数据包,告诉对方自己是它需要查找的MAC地址;
④源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
应用程序ping发出的是什么报文?
**答案:**应用程序ping发出的是ICMP请求报文。
**解读:**ping的原理是利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,通过对方回复的数据包来确定两台网络机器是否连接相通,时延是多少。
socket编程流程
服务器流程
socket() | 创建套接字 |
bind() | 绑定本地IP地址和端口号 |
listen() | 设置监听队列长度 |
accpet() | 等待连接,从连接队列里取出已经完成三次握手的连接,会创建另一个套接字 |
recv() | 接受消息,从TCP关联的内核空间接收缓冲区中取出数据到程序定义的缓冲区 |
close() | 关闭套接字 |
客户端流程
socket() | 创建套接字 |
connect() | 发送连接请求 |
send() | 发送消息,将要发送的数据从用户空间拷贝到TCP关联的内核发送缓冲区 |
close() | 关闭套接字 |
int listen(int sockfd,int backlog);
- backlog:描述sockfd的等待连接队列能够达到的最大值,内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个backlog告诉内核使用这个数值作为队列的上限。
socket在内核中的实现
在Linux中,socket网络通信是通过内核提供的 Socket API 实现的。内核中的网络通信模块主要由三个部分组成:网络协议栈、套接字层和网络设备驱动。
-
- 网络协议栈:网络协议栈是内核中负责处理网络通信的核心模块,它实现了各种网络协议,如TCP/IP、UDP、ICMP等。网络协议栈负责将应用程序的数据进行分片、封装、解封装和路由等操作,以便在网络中传输。
-
- 套接字层:套接字层是内核中与应用程序进行交互的接口,它提供了一组系统调用和数据结构,用于创建、连接、发送和接收数据等操作。应用程序通过调用套接字层提供的系统调用,如socket()、bind()、listen()、accept()等,来创建和控制套接字。
-
- 网络设备驱动:网络设备驱动是内核中负责与物理网络设备进行通信的模块。它通过与网卡设备进行交互,将数据从内核中的套接字层传输到物理网络中,或从物理网络中接收数据并传递给套接字层。网络设备驱动通过网络协议栈中的协议栈层来处理数据的封装和解封装等操作。
当应用程序调用套接字层提供的系统调用时,内核会根据调用参数进行相应的操作。例如,当应用程序调用socket()系统调用来创建一个套接字时,内核会为该套接字分配一个文件描述符,并在套接字层中创建一个对应的数据结构来保存套接字的相关信息。当应用程序调用send()系统调用发送数据时,内核会将数据从应用程序空间复制到内核空间,并通过网络协议栈进行封装和路由等操作,最终通过网络设备驱动发送到目标主机。
在接收数据时,内核会通过网络设备驱动接收数据,并通过网络协议栈进行解封装和路由等操作,最终将数据复制到套接字层中的接收缓冲区,应用程序可以通过调用recv()系统调用来从接收缓冲区中读取数据。
总之,socket网络通信在Linux中是通过内核中的网络协议栈、套接字层和网络设备驱动相互配合实现的。套接字层提供了应用程序与内核之间的接口,网络协议栈负责处理数据的封装和解封装等操作,网络设备驱动负责与物理网络设备进行通信。这些模块共同工作,实现了Linux中的socket网络通信功能。
socket和fd的区别
和普通的FD类似,Socket也可以通过系统调用来进行读写操作、设置选项、关闭连接等。Socket和普通的FD在底层操作系统的层面上是类似的,都是通过文件描述符来表示和操作。
然而,Socket在应用层面上有一些特殊的操作和属性,例如建立连接、监听端口、发送和接收网络数据等。这些操作是针对网络通信而设计的,使得Socket在使用上有一些区别于普通的文件描述符。
针对TCP的网络编程
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将 socket 绑定在指定的 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
accept,connect发生在三次握手的哪次
- 客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务端的 SYN 包进行应答,应答数据为 server_isn+1; - ACK 应答包到达服务端后,服务端的 TCP 连接进入 ESTABLISHED 状态,同时服务端协议栈使得
accept
阻塞调用返回,这个时候服务端到客户端的单向连接也建立成功。至此,客户端与服务端两个方向的连接都建立成功。 - 客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
客户端close会发生什么
- 客户端调用
close
,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态; - 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符
EOF
到接收缓冲区中,应用程序可以通过read
调用来感知这个 FIN 包。这个EOF
会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态; - 接着,当处理完数据后,自然就会读到
EOF
,于是也调用close
关闭它的套接字,这会使得服务端发出一个 FIN 包,之后处于 LAST_ACK 状态; - 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
- 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
- 客户端经过
2MSL
时间之后,也进入 CLOSE 状态;
没有accpet,能建立TCP连接吗
- accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。
TCP半连接队列和全连接队列
- 半连接队列,也称 SYN 队列;
- 全连接队列,也称 accept 队列;
- 服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
- 超过限制时,内核会直接丢弃,或返回 RST 包。
三次握手
-
一开始,客户端和服务端都处于
CLOSE
状态。先是服务端主动监听某个端口,处于LISTEN
状态 -
客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。 -
服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。 -
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。
为什么是三次握手?不是两次、四次?
-
首要原因是为了防止旧的重复连接初始化造成混乱
- 在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接
-
同步双方初始序列号
- 可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的
SYN
报文的时候,需要服务端回一个ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
- 可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的
-
避免资源浪费
- 如果只有「两次握手」,当客户端发生的
SYN
报文在网络中阻塞,客户端没有接收到ACK
报文,就会重新发送SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的ACK
报文,所以服务端每收到一个SYN
就只能先主动建立一个连接,这会造成什么情况呢?如果客户端发送的SYN
报文在网络中阻塞了,重复发送多次SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
- 如果只有「两次握手」,当客户端发生的
四次挥手
-
客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 -
服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSE_WAIT
状态。 -
客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 -
等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 -
客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 -
服务端收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。 -
客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。
为什么挥手需要四次?
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
-
关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 -
服务端收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。
epoll是什么?
-
epoll是Linux网络编程中用于处理大批量文件描述符的机制,是对select/poll的改进。
-
select监听的fd是有上限的,32位处理器一般为1024;且select/poll每次调用会遍历所有fd,时间复杂度为O(n),效率太低。而epoll监听的fd数量没有限制,且能在O(1)的时间复杂度内完成操作。
-
epoll相关的系统调用有:epoll_creat、epoll_ctl、epoll_wait/epoll_pwait(可屏蔽特定信号),分别用来创建一个epoll文件描述符、添加/删除/修改需要侦听的文件描述符及事件、接收被侦听描述符的IO事件。epoll文件描述符用完之后直接close关闭即可。
TCP、UDP的区别?
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接。
- UDP 是不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点。
- UDP 支持一对一、一对多、多对多的交互通信
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议,具体可以参见这篇文章:如何基于 UDP 协议实现可靠传输?(opens new window)
4. 拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
5. 首部开销
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的。 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7. 分片不同
- TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
TCP如何保证可靠传输?
-
校验和:发送数据报的二进制相加然后取反,检测数据在传输过程中的变化,有差错则丢弃。
-
确认应答:接收方收到正确的报文就会确认。
-
超时重传:发送方等待一定时间后没有收到确认报文则重传。
-
序列号:发送方对每一个数据包编号,接收方对数据包排序,保证不乱序、不重复。
-
窗口机制(流量控制):双方会协调发送的数据包大小,保证接收方能及时接收。
-
拥塞控制机制:如果网络拥塞,发送方会降低发送速率,降低整个网络的拥塞程度。
边学习边总结的嵌入式各种知识,八股,面经,量大管饱,最重要:免费开放,希望大家能共同进步。