43 预习:Socket通信之网络协议基本原理
网络协议:一台机器将内容按照约定好的格式发送出去,另外一台机器收到后能按照约定格式解析,获取到信息。
两种网络协议,
osi标准七层模型:物理层 数据链路层 网络层 传输层 会话层 表示层 应用层
TCP/IP模型:物理层 mac层 ip层 传输层 应用层(dns http)
具体如下图:
为什么要分层呢?
同一套网络协议栈通过切分成多个层次和组合,来满足不同服务器和设备的通信需求。
网络协议层次介绍
物理层:即物理设备,如连着电脑的网线、wifi
数据链路层:又称mac层,因为每个网卡都有唯一的硬件地址。在该层IP地址经过ARP协议得到对应的mac地址,在网络内服务器匹配上广播包对应的MAC地址就接收。
网络层:也就IP层,网络包从一个起始的IP地址,沿着路由协议指的道儿,经过多个网络,通过多次路由器转发,到达目标IP地址。
传输层:协议包括UDP/TCP,TCP是可靠协议,通过重传、编号等手段避免了丢包等问题。
应用层:如咱们在浏览器里面输入的 HTTP,Java 服务端写的 Servlet
应用层是用户态的,二层到四层都是在Linux内核里面处理的。内核对于网络包的处理不区分内核应用,应用层则进行区分,如通过端口,nginx监听80,tomcat监听8080.
用户态的应用层和内核2~4层的互通,就是通过socket系统调用。
数据包的传输过程
网络分完层后,对于数据包的发送和接收就是层层封装与解封装的过程。
在 Linux服务器B上部署的服务端Nginx, Linux 服务器 A 上的客户端,打开一个 Firefox 连接 Ngnix。如下图:
那么网络传输的具体流程:
1)在服务器A上,浏览器请求封装为HTTP协议,通过Socket发送到A的内核。传输层将http包加上TCP头发送给IP层,IP层加上IP头发送给mac层,mac层加上mac头,从硬件网卡发出去。
2)到达交换机,因只会处理到第二层所以被称为二层设备。会将网络包的MAC头拿下来,发现目标MAC是在自己右面的网口,于是就从这个网口发出去。
3)到达路由器,它左面的网卡会收到网络包,发现 MAC 地址匹配,就交给 IP 层,在 IP 层根据 IP 头中的信息,在路由表中查找,然后从合适的网口发出去。常把路由器称为三层设备,因为它只会处理到第三层
4)交换机,还是会经历一次二层的处理,转发到交换机右面的网口。
5)linux服务器B,它发现 MAC 地址匹配,就将 MAC 头取下来,交给上一层。IP 层发现 IP 地址匹配,将 IP 头取下来,交给上一层。TCP 层会获取TCP 头中的信息,内核会根据 TCP 头中的端口号,将网络包发给相应的应用。
6)应用会解析HTTP 层的头和正文,处理后获取结果,然后仍然封装为http的网络包,通过一系列过程返回给客户端。
43 socket通信
TCP/UDP的区别
1)TCP是面向连接的。UDP无连接
2)TCP通过编号、重试等是可靠传输,且提供流量控制和拥塞控制。UDP不可靠
3)TCP效率较低,
4)TCP是基于流的,UDP基于数据报
说明:所谓的连接,就是两端数据结构状态的协同,两边的状态能够对得上。可靠也是两端的数据结构做的事情,如不丢失其实是数据结构在“点名”,顺序到达其实是数据结构在“排序”,面向数据流其实是数据结构将零散的包,按照顺序捏成一个流发给应用层。
socket操作tcp或udp,都要调用socket函数,如下:
int socket(int domain, int type, int protocol);
参数说明:
domain:表示使用什么 IP 层协议。AF_INET 表示 IPv4,AF_INET6 表示 IPv6。
type:表示 socket 类型。SOCK_STREAM,顾名思义就是 TCP 面向流的,SOCK_DGRAM 就是 UDP 面向数据报的,SOCK_RAW 可以直接操作 IP 层,或者非 TCP 和 UDP 的协议。例如 ICMP。
protocol 表示的协议,包括 IPPROTO_TCP、IPPTOTO_UDP。
函数功能:在内核中,创建一个 socket 的文件描述符,后续的操作都会用到。
TCP下socket编程
整体流程如下图:
0)服务端和客户端分别调用socket函数,获取socket文件描述符
1)服务端调用bind函数,监听一个端口,给socket赋一个ip地址和端口。这样后面客户端可以通过ip和端口访问服务端,客户端则不需要bind
说明:服务端所在的服务器可能有多个网卡、多个地址,可以选择监听在一个地址,也可以监听 0.0.0.0 表示所有的地址都监听。服务端一般要监听在一个众所周知的端口上,例如,Nginx 一般是 80,Tomcat 一般是 8080。
2)服务端调用listen进入LISTEN状态,等待客户端连接。
3)服务端调用 accept,等待内核完成了至少一个连接的建立,才返回。如果有多个客户端发起连接,并且在内核里面完成了多个三次握手,建立了多个连接,这些连接会被放在一个队列里面,循环调用accpet进行处理。
4)客户端通过connect函数发起连接,指明要连接的 IP 地址和端口号,然后发起三次握手。握手成功后,服务端的 accept 就会返回另一个 socket。
监听的 socket 和真正用来传送数据的 socket,是两个 socket,一个叫作监听 socket,一个叫作已连接 socket。
5)双方开始通过 read 和 write 函数来读写数据
上面提到的三次握手如下图:
为什么是三次握手,不是2次或四次?tcp的报文序号,三次握手和四次挥手_进化的深山猿-CSDN博客
UDP下的socket编程
如下图:
UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因而也需要 bind。
每次通信时,调用 sendto 和 recvfrom,都要传入 IP 地址和端口。
44 socket内核数据结构
socket函数解析
Socket系统调用会调用sock_create创建一个struct socket结构,然后通过sock_map_fd和文件描述符对应起来。
参数:
先分配struct socket 结构。
family,表示地址族。 net_families 数组中以该参数为下标,找到对应的 struct net_proto_family,net_proto_family中会指定create函数的指向。
type,也即 Socket 的类型。类型是比较少的,包括SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW。type作为inetsw数组下标,找到type对应的列表,根据protocol最终得到用户指定的 family->type->protocol 的 struct inet_protosw *answer 对象,其ops赋给struct socket *sock 的 ops 成员变量。
然后创建一个 struct sock *sk 对象。说明:socket 是用于负责对上给用户提供接口,并且和文件系统关联。而 sock,负责向下对接内核网络协议栈。
struct inet_protosw *answer 结构的 tcp_prot 赋值给了 struct sock *sk 的 sk_prot 成员。
protocol,是协议。协议数目是比较多的,也就是说,多个协议会属于同一种类型。会循环对应的链表,找到对应类型下对应协议的inetsw
解析bind函数
sockfd_lookup_light 会根据 fd 文件描述符,找到 struct socket 结构。然后将 sockaddr 从用户态拷贝到内核态,然后调用 struct socket 结构里面 ops 的 bind 函数。根据前面创建 socket 的时候的设定,调用的是 inet_stream_ops 的 bind 函数,即调用inet_bind,检查端口是否冲突,是否可以绑定。可以则进行绑定。
解析listen函数
即调用 inet_listen。如果socket还不在TCP_LISTEN状态,会调用inet_csk_listen_start进入监听状态。
在内核中,为每个Socket维护两个队列:
1)一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;即icsk_accept_queue
2)一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。
初始化完之后,将 TCP 的状态设置为 TCP_LISTEN,再次调用 get_port 判断端口是否冲突。
解析accept函数
即调用inet_accept
原来的 socket 是监听 socket,这里我们会找到原来的 struct socket,并基于它去创建一个新的 newsock。这才是连接socket。除此之外,我们还会创建一个新的 struct file 和 fd,并关联到 socket。
如果 icsk_accept_queue 为空,则调用 inet_csk_wait_for_connect 进行等待;等待的时候,调用 schedule_timeout,让出 CPU,并且将进程状态设置为 TASK_INTERRUPTIBLE。
如果再次 CPU 醒来,我们会接着判断 icsk_accept_queue 是否为空,同时也会调用 signal_pending 看有没有信号可以处理。一旦 icsk_accept_queue 不为空,就从 inet_csk_wait_for_connect 中返回,在队列中取出一个 struct sock 对象赋值给 newsk。
解析connect函数
三次握手结束后,icsk_accept_queue 才不为空。下面分析三次握手的过程。
客户端发送SYN:
三次握手由客户端调用connect函数发起,调用inet_stream_connect。
如果socket处于SS_UNCONNECTED 状态,调用tcp_v4_connect函数,主要工作如下:
1)ip_route_connect 其实是做一个路由的选择。SYN 包了,这就要凑齐源地址、源端口、目标地址、目标端口。
2)发送 SYN 之前,我们先将客户端 socket 的状态设置为 TCP_SYN_SENT。然后初始化 TCP 的 seq num
3)调用 tcp_connect 进行发送SYN,会有重试机制。
服务端接受SYN:
通过struct net_protocol 结构中的 handler 进行接收,调用的函数是 tcp_v4_rcv。接下来的调用链为 tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process。
服务端处于TCP_LISTEN状态,接收到SYN.
回复一个 SYN-ACK,调用 icsk->icsk_af_ops->conn_request 函数,其实调用的是 tcp_v4_conn_request-->tcp_conn_request
回复完毕后,服务端处于 TCP_SYN_RECV状态
客户端接收SYN-ACK网络包了:
都是 TCP 协议栈,所以过程和服务端没有太多区别,还是会走到 tcp_rcv_state_process 函数的,此时客户端目前处于 TCP_SYN_SENT 状态,所以tcp_rcv_synsent_state_process 会调用 tcp_send_ack,发送一个 ACK-ACK,发送后客户端处于 TCP_ESTABLISHED 状态。
服务端接收ACK-ACK网络包
又tcp_rcv_state_process 函数处理。由于服务端目前处于状态 TCP_SYN_RECV 状态,因而又走了另外的分支。当收到这个网络包的时候,服务端也处于 TCP_ESTABLISHED 状态,三次握手结束。
45-46 发送数据包
这个过程分成几个层次:
VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。
IP 层:ip_route_output_ports 函数里面会调用 fib_lookup 查找路由表。FIB 全称是 Forwarding Information Base,转发信息表,也就是路由表。
在 IP 层里面要做的另一个事情是填写 IP 层的头。
在 IP 层还要做的一件事情就是通过 iptables 规则。
MAC 层:IP 层调用 ip_finish_output 进行 MAC 层。
MAC 层需要 ARP 获得 MAC 地址,因而要调用 ___neigh_lookup_noref 查找属于同一个网段的邻居,他会调用 neigh_probe 发送 ARP。
有了 MAC 地址,就可以调用 dev_queue_xmit 发送二层网络包了,它会调用 __dev_xmit_skb 会将请求放入队列。
设备层:网络包的发送会触发一个软中断 NET_TX_SOFTIRQ 来处理队列中的数据。这个软中断的处理函数是 net_tx_action。
在软中断处理函数中,会将网络包从队列上拿下来,调用网络设备的传输函数 ixgb_xmit_frame,将网络包发到设备的队列上去。
47-48 接收数据包
整个过程可以分成以下几个层次:
硬件网卡接收到网络包之后,通过 DMA 技术,将网络包放入 Ring Buffer;
硬件网卡通过中断通知 CPU 新的网络包的到来;
网卡驱动程序会注册中断处理函数 ixgb_intr;
中断处理函数处理完需要暂时屏蔽中断的核心流程之后,通过软中断 NET_RX_SOFTIRQ 触发接下来的处理过程;
NET_RX_SOFTIRQ 软中断处理函数 net_rx_action,net_rx_action 会调用 napi_poll,进而调用 ixgb_clean_rx_irq,从 Ring Buffer 中读取数据到内核 struct sk_buff;
调用 netif_receive_skb 进入内核网络协议栈,进行一些关于 VLAN 的二层逻辑处理后,调用 ip_rcv 进入三层 IP 层;
在 IP 层,会处理 iptables 规则,然后调用 ip_local_deliver 交给更上层 TCP 层;
在 TCP 层调用 tcp_v4_rcv,这里面有三个队列需要处理,如果当前的 Socket 不是正在被读取,则放入 backlog 队列,如果正在被读取,不需要很实时的话,则放入 prequeue 队列,其他情况调用 tcp_v4_do_rcv;
在 tcp_v4_do_rcv 中,如果是处于 TCP_ESTABLISHED 状态,调用 tcp_rcv_established,其他的状态,调用 tcp_rcv_state_process;
在 tcp_rcv_established 中,调用 tcp_data_queue,如果序列号能够接的上,则放入 sk_receive_queue 队列;如果序列号接不上,则暂时放入 out_of_order_queue 队列,等序列号能够接上的时候,再放入 sk_receive_queue 队列。
至此内核接收网络包的过程到此结束,接下来就是用户态读取网络包的过程,这个过程分成几个层次。
VFS 层:read 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_read_iter 函数。sock_read_iter 函数调用 sock_recvmsg 函数。
Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_recvmsg 函数。
Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_recvmsg 函数。
TCP 层:tcp_recvmsg 函数会依次读取 receive_queue 队列、prequeue 队列和 backlog 队列。