先串讲一下网络,网站输入一个www.baidu.com的过程。
首先是将www.baidu.com这个url地址进行解析,从而生成发给web服务器的信息。
生成http报文后,要发往哪去呢,就需要IP头。
查询IP地址就涉及到DNS域名解析,首先浏览器会查看自己缓存有没有这个域名的IP地址,没有的话操作系统会去检查自己的DNS缓存(hosts文件),如果也没有,就会去查找本地DNS服务器(浏览器配置的公共DNS服务),通过DNS协议查询(涉及到网络传输,几乎是采用UDP,查询过程通过发送接受DNS数据包实现,数据包依然会通过网络中的路由器、交换机转发),本地也没有,就迭代查询,根域名->顶级域名->权威域名。
找到目的地后,需要谁的帮助呢?
首先是需要TCP协议建立连接(UDP直接发)。TCP三次握手建立连接,同时还要留意TCP报文长度限制。此时报错的内容是(TCP头+HTTP头+消息)
但是TCP建立连接发消息都需要封装成IP的网络包进行通讯。IP里有源地址IP(路由表判断哪个网卡做源IP)和目的地IP(DNS找到)。
定位好目的地,并且封装好自己的消息体后,目的地太远了,没法一次到,就需要多次跳跃。
那就需要MAC的出现了,MAC包含接收方MAC地址、发送方MAC地址、协议类型(0800:IP协议、0806:ARP协议)。发送方MAC地址是确定的,那接收方的MAC地址就需要ARP协议来确定了。ARP协议通过查路由表缓存来确定,路由表没有就广播查询,有这个IP地址的路由表就会将MAC地址发给发送方。
上述完成后,就剩将数据在实体中传输。
那就需要网卡,网卡驱动程序将数字信号转为电信号。网卡驱动获得包后,复制到缓冲区,接着在头前加上报头和起始帧分界符,末尾加上检测错误的帧检验序列。最后通过网线发送出去。
距离太远一次传输过去肯定完成不了(线不够长)
那就需要交换机,交换机将电信号转为数字信号,然后检验末尾的帧检验序列,没问题就放入缓冲区,再根据接收方MAC地址再转发出去。(网卡有MAC地址,交换机没有,只管转发)如果没有接受方MAC地址,就除了源端口,全部都转发,接收到响应包后,就将它的MAC地址和端口写入。
最后还需要通过一个路由器进行转发。
路由器的工作和交换机类似,也是检查转发。但它只接受自己地址匹配的包,其他的都丢掉,然后查询转发目标,再由相应端口作为发送方转发出去。到了路由器这块,如果接受方MAC地址就是路由器端口MAC地址,就会去掉MAC头部。然后根据IP头部的内容进行包转发,如果不是,就会通过ARP协议找下一跳的路由器MAC地址。
此时就是路由器->路由器->交换机->目的地。大概这样的过程。最终到目的地。
到达目的地后,就是扒皮,把MAC、IP、TCP都确认一遍。
1.请简要介绍一下Netty是什么,以及它主要解决了什么问题?
Netty是一个基于NIO的,用于快速开发网络服务端和客户端的编程框架。
解决的问题:简化网络编程开发、支持多种协议、提高性能和可靠性。
2.Netty为什么选择NIO这种实现方式?
主要是因为NIO提供了异步非阻塞通信、零拷贝、内存池、高效的Reactor线程模型、无锁化的串行设计理念、高效的并发编程支持。
因为异步非阻塞通信,可以提升IO线程效率,避免因为频繁IO阻塞导致线程挂起。还提高了并发处理能力。一个IO线程可以并发处理多个客户端连接和读写操作,从根本上解决了一对一连接模型的局限性。
零拷贝,实现了缓冲区的二次拷贝,提升读写性能
内存池,Netty提供了基于内存池的缓冲区重用机制,有助于减少内存分配和回收开销。
支持Reactor单线程模型、Reactor多线程模型、主从Reactor多线程模型
无锁化串行,消息的处理尽可能在一个线程内完成,期间不进行线程切换,避免了多线程竞争和同步锁。
并发体现在,正确使用volatil、CAS、原子类
序列化,支持Protobuf,由于高效的序列化、反序列化和数据压缩,有助于提升网络带宽和速度
3.Netty中的EventLoop和EventLoopGroup是如何工作的?它们在网络编程中扮演什么角色?
EventLoop(事件循环)如何工作:
一个EventLoop包含一个或多个的Selector,Selector负责监听通道上的事件,并将就绪的事件通知给EventLoop,EventLoop会将这些事件分发给ChannelPipeline中的ChannelHandler进行处理。
处理任务的时候,可以执行异步任务和定时任务。异步通过execute()方法交给EventLoop,由EventLoop的线程异步执行。定时任务通过schedule()方法提交,设置执行延迟时间和间隔时间。
EventLoopGroup(事件循环组)如何工作:
EventLoopGroup是一组EventLoop的抽象,一个EventLoopGroup当中会包含一个或多个EventLoop。EventLoopGroup提供next()接口,可以从一组EventLoop里面按照一定规则获取其中一个EventLoop来处理任务。
本质上是一个线程池,它负责分配和管理线程,EventLoopGroup默认会根据你的CPU核心数创建两倍数量的线程。当有Channel注册时,Group会为其分配一个Loop,保证了Channel和Loop的一对一绑定关系。
按角色来看:
Loop:事件处理者(处理连接、读写请求)、任务执行器(除了网络事件,还执行异步任务和定时任务)
Group:资源管理者(创建、分配、释放Loop资源)、性能优化者(确保每个Loop都能专注其绑定的Channel上的事件和任务,减少上下文开销和线程竞争)
5.Netty中的Channel、ChannelPipeline、ChannelHandler和ChannelHandlerContext之间的关系是什么?
Channel:一个到实体的开放连接,例如TCP套接字或一个文件,用于读写,连接操作
ChannelPipeline:是ChannelHandlerContext链的容器,负责处理或拦截Channel的入站和出站事件(ChannelHandler)。每个Channel都有一个ChannelPipeline相关联,且永久关联。ChannelPipeline通过ChannelHandlerContext来管理ChannelHandler,确保事件按照正常顺序处理,支持添加删除替换ChannelHandler。
ChannelHandler:处理入站和出站数据的处理器,提供了一系列回调方法。
ChannelHandlerContext:是ChannelHandler和ChannelPipeline之间的关联点,通过它可以调用下一个Handler或者执行特定操作。
关系:
- Channel与ChannelPipeline:每个Channel都有一个ChannelPipeline与之关联,且这个关联是永久性的。ChannelPipeline负责处理Channel上的所有事件,包括数据的读写、连接的建立与断开等。
- ChannelPipeline与ChannelHandler:ChannelPipeline包含了一个或多个ChannelHandler,这些Handler按照特定的顺序排列,共同处理Channel上的事件。ChannelPipeline通过ChannelHandlerContext来管理这些Handler,确保事件能够按照正确的顺序被处理。
- ChannelHandlerContext与ChannelHandler:每个ChannelHandler在注册到ChannelPipeline时,都会生成一个与之关联的ChannelHandlerContext。这个Context提供了与Pipeline交互的API,允许Handler在Pipeline中执行操作、传递事件等。
1.Netty的Reactor模式是如何实现的?请详细解释一下Netty的线程模型。
通过配置EventLoopGroup来完成,由BossGroup(监听连接请求)和WorkerGroup(监听已连接的IO事件)组成。
线程模型:单Reactor单线程(所有事件监听都在一个线程完成)、单Reactor多线程(BossGroup(一个EventLoop)监听和分发,处理由WorkerGroup完成)、多Reactor多线程(BossGroup(多个EventLoop)监听和分发,处理由WorkerGroup完成)
2.在Netty中,如何配置ChannelPipeline来处理入站和出站数据?
ChannelPipeline是由一系列ChannelHandler组成的处理器链。每当有新的Channel创建,Netty都会为它分配新的ChannelPipeline。ChannelHandler有入站处理器(ChannelInboundHandler)和出站处理器(ChannelOutboundHandler)。
通过调用addLast(String name, ChannelHandler handler)来添加Handler到Pipeline中。每个ChannelHandler都会与一个ChannelHandlerContext实例相关联,该实例提供了与ChannelPipeline交互的API,如将事件传递给下一个处理器等。
入站处理器:解码器、XXInboundHandler
出站处理器:编码器、XXOutboundHandler
// 初始化ChannelPipeline
ChannelPipeline pipeline = ch.pipeline();
// 添加入站处理器
pipeline.addLast("decoder", new MyDecoder()); // 解码器,将字节转换为消息对象
pipeline.addLast("inboundHandler", new MyInboundHandler()); // 处理特定类型的消息对象
// 添加出站处理器
pipeline.addLast("encoder", new MyEncoder()); // 编码器,将消息对象转换为字节
pipeline.addLast("outboundHandler", new MyOutboundHandler()); // 处理数据的实际发送
3.解释一下Netty中的ByteBuf及其与Java NIO ByteBuffer的主要区别。
都是处理字节。ByteBuf是ByteBuffer的改进。
区别:
动态扩容:ByteBuf会自动扩容,ByteBuffer分配了容量就无法改变
读写指针:ByteBuf使用readindex和writeindex来控制读写操作,无需模式切换,而ByteBuffer只有一个Position索引,需要读写切换时,调用flip()、rewind()等操作重置position索引。
零拷贝:ByteBuf通过slice、duplicate等方法实现了零拷贝或减少数据拷贝次数。
引用技术:ByteBuf通过引用计数功能,通过ReferenceCounted接口管理内存,减少内存泄漏
池化:ByteBuf通过复用降低内存分配和释放开销
5.Netty是如何处理TCP粘包和拆包问题的?有哪些常见的解码器(Decoder)和编码器(Encoder)可以用于处理这类问题?
解决办法就是通过自定义或者内置的编解码器来解决。
粘包:多个消息被合并
拆包:多个消息被差分
1.Netty中有哪些机制可以帮助提升网络应用的性能?
例如异步非阻塞机制、高效线程模型、内存管理机制(零拷贝(接收发送ByteBuf采用DirectBuffers,即堆外直接内存,减少内存拷贝次数)、内存池)、多种协议和编码器、Pipeline机制、心跳检测机制
2.如何合理配置Netty的线程模型以达到最佳性能?
首先是bossGroup和workerGroup的数量。NioEventLoopGroup(n)
其次可以跳转Direct Memory的大小,设置内存池来优化性能。
以及优化编解码器。
注意不要在ChannelHandler启动用户线程,解码操作放在NIO线程中,复杂操作放在业务线程池。
3.Netty的Backpressure(背压)机制是如何工作的?在什么场景下需要关注背压问题?
Netty的Backpressure(背压)机制主要通过控制数据在网络中的流动速度,来解决生产者和消费者之间数据处理速率不匹配的问题。这一机制通过监控和调节缓冲区(Buffer)的水位(Watermark)来实现。
Netty的写缓冲区可以设置两个水位标记,低水位和高水位,当达到高水位时就会触发背压机制,限制数据进一步写入,知道数据量下降到低水位。
场景:
高并发、实时性要求高、资源受限、流量突发
4.Netty的异常处理机制是怎样的?如何有效地捕获和处理Netty中的异常?
机制:
首先数据时由Pipeline中的Handler来处理的,当发生异常时,异常也会传给Handler来处理。
Handler里面有个exceptionCaught方法用于处理异常。
也可以由ChannelFuture、FutureListener来监听异步的结果
或者通过Promise添加addListener监听结果。
捕获、处理:
重写Handler的exceptionCaught方法,或者专门添加一个ExceptionHandler来处理。
1.Netty支持哪些高级协议或框架的集成?
HTTP/2、WebSocket、SSL/TLS、TCP/UDP
1.为什么它高性能?
其底层的NIO充分利用了操作系统的异步IO特性,能够处理大量并发连接和数据传输,并保持低延迟。(但连接数少、并发低、网络快时,NIO不一定比BIO快)
2.NIO为什么快呢?
NIO之所以快,一方面是允许非阻塞的IO操作,另一方面就是可以通过选择器Selector来管理多个Channel,从而提高整体性能和吞吐量。
3.非阻塞的IO是如何实现的?
三个角色共同作用。
首先是通道Channel,它可以看作是IO设备和内核的桥梁,当向IO设备写入数据时,数据会先写入内核缓冲区Buffer中,然后再从Buffer写入到Channel,再由Channel写入到IO设备。读取IO设备数据也是同理。
然后是内核缓冲区Buffer,一般存在主存(RAM)中,最终数据可能通向磁盘、网络其他节点、其他进程都有可能。
最后是选择器Selector,它是提高吞吐量的关键,它允许单个线程监听多个Channel。多个Channel注册到选择器上,并且每个Channel选择感兴趣的事件(连接就绪、读就绪、写就绪等)。选择器不断轮询这些Channel,检查是否有感兴趣的事件发生,一旦发生,选择器就会通知线程去处理。
4.为什么要先经过Channel呢?
因为Channel可以配置为非阻塞模式,当进行IO操作时,如果没有数据可读,或者无法写入数据,会返回一个特定的值,例如0(没读到数据),或抛出一个异常ClosedChannelException(通道关闭),而不会阻塞在这等待数据。
另一方面是为了给Selector监听,提高了NIO处理并发请求的性能。每个Channel会负责不同的功能,例如连接打开,数据到达,这些都由Channel触发。那么上面返回的0到底是值还是没读到数据,实际上在NIO操作中,会监听Channel,当Channel上有数据可以读时,会通知相关线程,然后线程再从Channel中读取数据。
而且Channel是双向的,不像BIO中的流是单向的。
5.为什么要使用缓冲区?
减少系统调用次数。假设多次IO操作才能写满一个缓冲区,如果不用缓冲区,那就需要多次系统调用写入磁盘,而如果写入一个缓冲区后统一写入磁盘,就仅需要一次系统调用。
减少了磁盘访问次数。磁盘访问是IO操作最耗时的部分之一。缓冲区存的数据也是按块为单位,假设一个缓冲区存3块数据,写入磁盘时,可以一次性将多个块的数据写入磁盘,而不是每次写入一个块的数据。
支持非阻塞IO。当Channel没有准备好读写操作时,线程会处理其他Channel。一旦准备好,线程就可以从缓冲区中读取或写入数据,而不会阻塞在等待Channel读写操作上。