面试漫谈(三)- 网络编程篇

Posted by YiBo on December 10, 2020

前记

昨天面试一家公司,所有的题都答的稀碎,也经过一些深思熟虑的动作,决定放弃之前的一家offer,“我的目标是什么?我怎么才能更加接近他?而不是再一次稀里糊涂的入职”

Neety是什么

三点概括

  • 是一个基于NIO的 client-server 框架,可以快速简单的开发网络应用程序
  • 极大简化并又花了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等方面都要更好
  • 支持多种协议如 FTP SMTP HTTP以及各种二进制和基于文本的传统协议

很多项目比如 Dubbo RocketMQ Elasticsearch 都用到了 Netty

他的好处我列一点

  • 支出多种传输类型 阻塞和非阻塞
  • 简单而强大的线程模型
  • 自带编解码器解决TCP粘包、拆包问题
  • 自带各种协议栈
  • 安全性,有完整的SSL/TLS
  • 社区活跃
  • 大型开源项目都用到了

应用场景

主要用来做网络通信

  • RPC
  • http服务器
  • 即时通讯系统
  • 实现消息推送系统

Netty核心组件有哪些,分别有什么用

  1. Channel

    是netty对网络操作抽象类,包含基本的I/O操作,比如bind() connect() read() write()

    常用的channel接口实现类是 NioServerSocketChannel(服务端) NioSocketChaneel(客户端) 大大降低了直接使用Socket的复杂性

  2. EventLoop

    “定义了Netty的核心抽象,用于处理链接的生命周期中所发生的事情”

    主要作用是 负责监听网络事件并调用事件处理器进行相关IO操作的处理

    Channel 为netty网络操作(读写等)的抽象类

    EventLoop 负责处理注册到其上的Channel处理IO操作

  3. ChannelFuture

Netty是异步非阻塞的,所有IO操作都是异步的

通过ChannelFuture接口的addListener()方法注册一个ChannelFutureListener,当操作成果或失败时,监听就会自动出发返回结果

也可以通过ChannelFuture接口的sync()方法让异步操作变成同步

  1. ChannelHandler 和 ChannelPipeline

    ChannelHandler 是消息的具体处理器,负责处理写操作,客户端连接等事件

    ChannelPipeline 为 ChannelHandler 的链,通过 addList() 方法添加一个或多个 ChannelHandler

EventLoopGroup 了解吗 和 EventLoop 啥关系

EventLoopGroup 中包含多个 EventLoop

EventLoop 和 Thread 一对一 , 从而保证了线程安全

EventLoopGroup 分为俩种

  1. Boss EventLoopGroup

    用于接收连接

  2. Worker EventLoopGroup

    用处具体的处理

当客户端通过 connect 方法连接到服务端时, bossGroup 处理客户端请求,然后会将连接提交给 workerGroup 来处理其IO相关操作

Boostrap 和 ServerBootstrap 了解吗

Boostrap 是客户端的启动引导类/辅助类

  • 指定线程模型(NioEventLoopGroup)
  • 尝试建立连接(connet())
  • 优雅的关闭相关线程组资源

ServerBootstrap 是服务端启动引导类/服务类

  • 指定线程模型(boosGroup workerGroup)
  • 绑定端口(bind())
  • 等待连接关闭
  • 优雅的关闭相关线程组资源

源码了解吗? NioEventLoopGroup 默认的构造函数会起多少线程

cpu核心数*2

Netty线程模型了解吗

大部分网络框架都是基于Reactor模式设计开发的

  • Reactor模式基于事件驱动,采用多路复用将事件分发给相应的handler处理,适合海量IO的场景

在Netty主要靠NioEventLoopGroup线程池来实现具体的线程模型的。在实现服务端的时候,会初始化俩个线程组

  • bossGroup 接受连接
  • workerGroup 负责具体的处理,交由对应的Handler处理

Netty的线程模型

  1. 单线程模型

    一个线程需要处理 accept read decode process encode send 事件,对高并发场景不适用

  2. 多线程模型

    一个Acceptor线程只负责监听客户端的连接。一个NIO线程池负责具体处理 accept read decode process encode send事件。遇到并发连接大会成为性能瓶颈

  3. 主从多线程模型

    从一个主线程NIO线程池中选择一个线程作为Acceptor线程,绑定监听端口,接受客户端连接,其他线程负责后续接入认证等工作。连接建立完成后,Sub NIO 线程池负责具体处理IO读写

Neety服务端和客户端的启动过程了解吗

服务端

  1. 创建俩个NioEventLoopGroup对象实例
    • boosGroup 用于处理客户端TCP连接。一般线程数为1
    • workerGroup 负责每一条连接的具体读写数据的处理逻辑,负责IO读写操作,交由对应的Handle处理。一般线程数为cpu*2
  2. 创建 ServerBootstrap 引导类
  3. 通过 .group() 方法给引导类 ServerBootstrap 配置俩大线程组,确定线程模型
  4. 通过 .channel()方法给引导类 ServerBootstrap 指定了IO模型为 NIO
  5. 通过 .childHandler() 给引导类创建一个 ChannelInitializer 然后指定了服务端消息的业务处理逻辑 HelloServerHandler 对象
  6. 调用 ServerBootstrap 类的 bind()方法绑定端口

客户端

  1. 创建一个NioEventLoopGroup 对象实例
  2. 创建客户端引导类 Bootstrap
  3. 通过 .group() 方法给引导类 Bootstrap 配置一个线程组
  4. 通过 channel() 方法给引导类 Bootstrap 指定了IO模型为NIO
  5. 通过 .childHandle() 给引导类创建一个 ChannelInitializer 然后指定了客户端的业务处理逻辑 HelloClientHandler 对象
  6. 调用 Bootstrap 的 connect() 方法进行连接,需要俩个参数 IP Port。 connect方法是Future类型的返回,需要通过 addListener 方法可以监听是否成功

什么是TCP粘包、拆包,有什么解决办法

基于TCP发送数据时,出现了多个字符“粘”在一次或者被“拆”开的问题

解决方法:

  1. 使用Netty自带的解码器

    • LineBashdFrameDecoder 发送端发送数据包的时候,每个数据包之间以换行符作为分割,工作原理就是一次遍历 ByteBuf 中的可读字节,判断是否有换行符
    • DelimiterBasedFrameDecoder 可以自定义分隔符解码器
    • FixedLengthFramDecoder 固定长度解码器
  2. 自定义序列化编解码器

    通常情况下使用 Protostuff Hessian2 json 序列方式

Netty长连接,心跳机制了解吗

TCP进行读写之前,服务端和客户端需要建立一个连接,也就是三次握手,这个过程比较消耗资源。短连接都是读写完成后就关闭连接。长连接不会主动关闭,后续读写操作会继续使用这个链接

TCP长连接过程中,心跳机制是为了防止对方因为网络异常等原因,无法发现对方已经掉线。心跳机制的工作原理:在client和server之间一定时间没有数据交互时,双方中一方就会发送一个特殊的数据包给对方,收到后会回复一个数据报文,就知道对方仍然在线了

Netty零拷贝了解吗

在Netty层面,零拷贝主要体现在对于数据操作的优化

  1. 使用Netty提供的 CompositeByteBuffer类,可以将多个ByteBuffer合并为一个逻辑上的ByteBuffer 避免了各个ByteBuffer之间的拷贝
  2. ByteBuffer 支持 slice 操作,可以将ByteBuffer 分解为多个共享同一个存储区域的 ByteBuf 避免内存的拷贝
  3. 通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,可以直接将文件缓冲区的数据发送到目标 Channel 避免了传统通过循环 write方式导致的内存拷贝问题