网络

匿名 (未验证) 提交于 2019-12-02 23:57:01

网络适配器接收到数据后(如果目的地址不正确会丢弃),经过 I/O 总线、内存总线,复制到内存(通常是通过 DMA 传送),并触发硬中断来通知 CPU,CPU 会更新硬件寄存器状态(表示数据已经读好了),然后发送软中断信号。

软中断处理程序(ksoftirqd 线程)为该包分配内核中的数据结构 sk_buff,并把包复制到 sk_buff 缓冲区中。内核从缓冲区取出来,通过网络协议栈逐层处理包。传输层里取出 TCP 或 UDP 头后,根据 { 源 IP,源端口,目的 IP,目的端口 } 找出对应的 socket,把数据复制到 socket 的接收队列缓冲区中。应用程序通过 socket 接口获取到该数据。

应用程序通过 socket 系统调用陷入内核,内核把数据放到 socket 的发送缓冲区。网络协议栈取出数据后,按照 TCP/IP 逐层处理。比如传输层、网络层,分别增加 TCP 头、IP 头,执行路由查找下一跳 IP,并按照 MTU 大小进行分片

分片后的网络包,送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后放到发包队列,通过软中断֪ͨ驱动程序

最后,驱动程序通过 DMA,从发包队列中取出网络帧,并通过网卡把它发送出去。

所谓连接,就是两端数据结构是否对得上。符合 TCP 协议规则,就认为连接存在;状态对不上,连接就算断了。

流量控制、拥塞控制:其实就是根据收到的对端的网络包,调整数据结构的状态。TCP 协议的设计理论上认为,这样调整了数据结构的状态,就能进行流量控制、拥塞控制了,其实在通路上是不是真的做到了,谁也管不着。

可靠:所谓可靠也是数据结构做的事情。不丢失其实是数据结构在「点名」,顺序到达其实是数据结构在「排序」,面向数据流其实是数据结构把零散的包,按照顺序捏成一个流,发给应用层。

总之,「连接」这两个字让人误以为功夫在通路,但其实功夫在两端。

socket

内核里,socket 就是一个文件,有对应的文件描述符。

但是与其他文件描述符不同,socket 不是保存在硬盘上的,而是保存在内存中的。

一个 socket 结构里面包含着两个队列:发送队列、接收队列。两个队列都保存着一个缓存 sk_buff,缓存内存的就是

应用层通过 socket 系统调用陷入内核操作传输层

套接字类型的文件,其文件描述符叫做套接字描述符

客户端、服务器可用 socket 函数来创建一个套接字描述符。

客户端通过调用 connect 来发起一个连接请求,此时内核会分配一个临时端口。

服务器调用 bind 函数告诉内核,给这个 socket 赋予一个端口和 IP 地址

socket 函数创建的描述符,默认是对应于主动套接字,是客户端用的。

服务器调用 listen 函数告诉内核,socket 描述符是被服务器使用的,而不是客户端。此时转化为一个监听套接字。该 socket 可接受客户端连接请求。

服务器调用 accept 函数来等待客户端连接请求。内核完成一个连接的建立(三次握手),就会返回,否则一直等待。

连接请求到达监听描述符后,返回一个已连接描述符,该描述符可用来利用 Unix I/O 函数与客户端通信。此时客户端也从 connect 函数返回。之后,客户端、服务器可分别通过读写 clientfd、connfd 来回传数据了。

监听描述符 listenfd:通常被创建一次,存在于服务器的整个生命周期。是作为客户端连接请求的一个端点。

已连接描述符 connfd:服务器每次接受连接请求时,都会创建一次。只存在于服务器为一个客户端服务的过程中。

这样可建立并发服务器,能同时处理多个客户端连接。

{ 本机 IP,本机端口,对端 IP,对端端口 }

客户端 IP 数最多为 2^32,客户端端口数最多为 2^16。所以服务端理论上最多可以有 2^48 个 TCP 连接。

但实际远不能达到理论值,主要原因:文件描述符限制、内存限制。

select 函数去监听(会阻塞)文件描述符是否有变化(如是否可读可写),一旦有变化就会去轮询,依次查看每个描述符是否可读或写,然后返回。

优点:一个线程就可以处理多个 I/O。

缺点:

1)用户每次都要轮询调用 select 去查看数组。

2)每次 select 都需要把 fd_set 从用户态复制到内核态,再让内核去遍历检查这个 fd_set。

3)能监听的描述符数量由 FD_SETSIZE 限制(32 位里是 1024。可修改但是是写死的,所以需要再次编译内核)。

事件通知的方式。当文件描述符发生变化时,操作系统主动通知进行 callback。

需要先注册。在进程 task_struct 的描述符表里,会有 epoll 对象(也是一个文件)对应的文件描述符。epoll 对象里有一个红黑树,保存着这个 epoll 要监听的所有 socket 和对应的 callback。注册时就是把文件描述符放到红黑树。

epoll 只有在 epoll_ctl 注册时会去从用户态拷贝到内核态,之后每次 epoll_wait 不需要再拷贝。

但是如果并发数量小,select 性能会更好。毕竟 epoll 需要一些函数回调、维护红黑树等。

直到用户空间有数据再返回。进程可能会进入睡眠状态。

没有数据时直接返回错误。只有内核有数据时才开始阻塞,成功复制到用户空间时再返回。

所以需要轮询调用。

分两步:1)select 函数阻塞直到内核有数据(描述符)。2)select 返回后再调用函数,从复制开始阻塞,直到成功复制到用户空间。

相对于非阻塞 I/O,优势是 I/O 复用可通过 select 监听多个描述符。

一个 TCP 服务器既要处理监听 socket,又要处理已连接 socket 时,一般就会使用 I/O 复用。或者同时要处理标准 输入、socket 时等情况。

缺点:如果都是在单线程里,select 返回后执行的操作阻塞时间长,会影响到其他 I/O 不能继续处理。解决:使用多线程去处理。比如每个线程去处理一个描述符。

内核准备好数据时通知进程,此时可开始阻塞等待数据成功复制到用户空间。

以上有阻塞的都是同步 I/O。

内核把数据成功复制到用户空间后再返回。这期间进程不会阻塞。

同步异步关注的是消息通信机制。如果我发出信号后,到时候处理返回信号是由我来处理,是同步。而如果不是我来处理,就是异步

阻塞非阻塞关注的是等待消息时的状态。如果返回信号前我可以做其他事,是非阻塞(可以轮询去确认)。如果不能做其他事,是阻塞

BIO

来一个客户端就开一个线程去处理。accept 等待一个连接是一个阻塞的过程。

循环让 selector 去检查是否有新事件发生(accept、readable、writable 等)。

检查到时去处理(继续用当前线程)。

对比 BIO,selector 优势在于可以监听多个连接,而不是像 BIO 需要一个个 accept 后处理。

NIO Reactor ģʽ

响应式编程。observer 设计模式。

(Boss 线程)selector 检查到事件后,让线程池里的线程(Worker 线程)去处理。

Netty 就是用的这种模式,但封装的比较好,比 Java NIO 接口好用。(而且 Netty 的 buffer 比 Java 的 ByteBuffer 更好用)

不需要循环。observer 设计模式。

用户先把 handler 注册到操作系统,操作系统每当连上客户端后调用 handler 去处理。

对客户端分组,每一个组对应一个 threadGroup,一个 threadGroup 可以有多个线程池,线程池里的就是 Worker 线程。

Netty

对于 NIO、BIO 进行了封装。封装成 AIO 的样子。并且对 ByteBuffer 进行了优化。

Boss 在接收连接后交给 Worker 线程,Worker 给该连接(Channel)添加用户设置的 Handler,并调用 Handler 中的方法。

Linux 内核对网络没有成熟的 AIO,只有 Windows 上有,只是应用层有实现 AIO 模型。

所以,AIO、NIO 在 Linux 都是用 epoll 实现的,是没什么区别,只是 AIO 多了封装。

所以 Netty 是对 NIO 进行的封装,但 Netty 提供的接口类似 AIO。

Redis

单线程,多路复用

Nginx

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!