0%

Netty学习笔记

本文主要包括:

学习背景

最近部门接了一个新项目,客户要求把接口从http协议转成TCP协议,原因是http的并发没有TCP协议支持的好
并把计算压力从Oracle迁移到数据中台(Starrocks),并要求高并发,毫秒级响应
因此,这里首先要先确认一个问题,TCP协议确实比Http协议在高并发场景性能优越吗?
项目开始之前,会议沟通中,客户提到了以下几个名词,大概的意思是,之前使用的是http,它的高并发性能比较差
这里首先了解以下关系:

Websocket与Http

什么是WebSocket

WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

  1. WebSocket是一种通信协议
  2. 区别于HTTP协议,HTTP协议只能实现客户端请求,服务端响应的这种单项通

WebSocket 是一种全双工通信协议,允许客户端和服务器之间进行双向通信。与传统的 HTTP 请求-响应模型不同,WebSocket 的连接通常是长时间保持的,而不是请求-响应后就断开。

为什么需WebSocket?

因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

由于http协议只能由客户端发起通信,如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用”轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。Websocket在解决这样的需求上非常方便。
如果不使用WebSocket与服务器实时交互,一般有两种方法。AJAX轮询和Long Polling长轮询

上边这两种方式都有个致命的弱点,开销太大,被动性。假设并发很高的话,这对服务端是个考验
AJAX轮询: AJAX轮询也就是定时发送请求,也就是普通的客户端与服务端通信过程,只不过是无限循环发送,这样,可以保证服务端一旦有最新消息,就可以被客户端获取。
Long Polling长轮询: Long Polling长轮询是客户端和浏览器保持一个长连接,等服务端有消息返回,断开。然后再重新连接,也是个循环的过程,无穷尽也,客户端发起一个Long Polling,服务端如果没有数据要返回的话,会hold住请求,等到有数据,就会返回给客户端。客户端又会再次发起一次Long Polling,再重复一次上面的过程。

WebSocket特点

最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL

所以,springboot如果使用的是:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

springboot就没有使用websocket,这时候,如果服务器有连续的状态变化,这时候只能通过AJAX轮询或者Long Polling长轮询来获取服务器的状态,但是这种两种方式,在高并发的情况下,都会有性能瓶颈。如果要解决这种问题,可以使用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

注意

Websocket不适合多线程或多进程服务器。
原因: 传统的 Web 服务器,比如处理 HTTP 请求的服务器,通常采用多线程或多进程的方式来同时处理多个连接。每个请求都被一个新的线程或进程处理,这样服务器能够同时服务多个客户端。
由于 WebSocket 连接的长时间保持,以及可能有频繁的消息交换,传统的多线程或多进程模型可能会面临效率和资源管理的问题。因此,实现 WebSocket 的服务器通常需要使用异步的方式来处理连接,以便能够在一个连接上同时处理多个事件,而不是为每个连接创建一个独立的线程或进程。
简而言之,传统的多线程或多进程服务器模型可能不太适合处理 WebSocket 连接,因为WebSocket连接的特性要求服务器能够有效地处理长时间保持的连接和频繁的双向通信。因此,实际的 WebSocket 服务器端实现通常采用异步的方式,以提高效率和资源利用率。

Http与Tcp

TCP/IP即传输控制/网络协议,也叫作网络通讯协议,它是在网络的使用中的最基本的通信协议。Http是一个简单的请求-响应协议,它通常运行在TCP之上。Socket是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。是支持TCP/IP协议的路通信的基本操作单元。

OSI 网络分层(7 层)

Open Systems Interconncection 开放系统互联:
OSI网络分层

协议 描述
第七层 应用层 支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信。主要的协议有:http、ftp、dns、telnet、smtp、pop3等。
第六层 表示层 把数据转换为合适、可理解的语法和语义
第五层 会话层 维护网络中的连接状态,即保持会话和同步,有 SSL
第四层 传输层 负责为信源和信宿提供应用程序进程间的数据传输服务,这一层上主要定义了两个传输协议,即传输控制协议TCP和用户数据报协议UDP。
第三层 网络层 负责将数据报独立地从信源发送到信宿,主要解决路由选择、拥塞控制和网络互联等问题。IP 在这一层
第二层 数据链路层 负责将IP数据报封装成合适在物理网络上传输的帧格式并传输,或将从物理网络接收到的帧解封,取出IP数据报交给网络层。
第一层 物理层 负责将比特流在结点间传输,即负责物理传输。该层的协议既与链路有关也与传输介质有关。

HTTP 是应用层协议,而 TCP 是传输层协议

TCP

TCP、UDP都是是传输层协议:

  • 用户数据报协议 UDP(User Datagram Protocol):
  • 无连接;
  • 尽最大努力的交付;
  • 面向报文;
  • 无拥塞控制;
  • 支持一对一、一对多、多对一、多对多的交互通信;
  • 首部开销小(只有四个字段:源端口、目的端口、长度、检验和)。
  • 传输控制协议 TCP(Transmission Control Protocol):
  • 面向连接;
  • 每一个TCP连接只能是点对点的(一对一);
  • 提供 可靠交付 服务;
  • 提供 全双工 通信;
  • 面向字节流。

另外,UDP是面向报文的传输方式是应用层交给UDP多长的报文,UDP发送多长的报文,即一次发送一个报文。因此,应用程序必须选择合适大小的报文

应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应该程序传送的数据块太长,TCP就可以把它划分短一些再传送

当网络通信时采用 TCP 协议时,在真正的读写操作之前,客户端与服务器端之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时可以释放这个连接。连接的建立依靠“三次握手”,而释放则需要“四次握手”,所以每个连接的建立都是需要资源消耗和时间消耗的

TCP连接过程(三次握手)

TCP连接过程(三次握手)

  • 第一次握手
    客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

  • 第二次握手
    服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

  • 第三次握手
    当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

为什么需要三次握手,2次不行吗?

喂喂喂,我是A,你听的到吗?B:在在在,我能听到,我是B,你能听到我吗? A:(听到了,老子不想理你) B:喂喂喂?听不听到?我X,对面死了,我挂了。。
如果只有 2 次的话,B 并不清楚 A 是否收到他发过去的信息。

TCP断开链接(四次挥手)

TCP断开链接(四次挥手)

  • 第一次挥手
    若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

  • 第二次挥手
    B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

  • 第三次挥手
    B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

  • *PS:** 通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

  • 第四次挥手
    A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最长报文段寿命,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

Http

HTTP 是建立在 TCP 上的应用层协议,超文本传送协议。
HTTP 连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

http1.0 : 客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

http1.1 : 可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后就可以再发送一个新的请求

http2.0 : 支持多路复用,一个 TCP 可同时传输多个 http 请求,头部数据还做了压缩

http3.0 : 使用了 QUIC,开启多个 TCP 连接,在出现丢包的情况下,只有丢包的 TCP 等待重传,剩余的 TCP 连接还可以正常传输数据

HTTP特点

  • 无状态: 协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作。
  • 无连接: HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。基于请求和响应:基本的特性,由客户端发起请求,服务端响应。
  • 简单快速、灵活。
  • 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性。

HTTP 与 TCP 区别

TCP 协议对应于传输层,而 HTTP 协议对应于应用层,从本质上来说,二者没有可比性:

  • HTTP 对应于应用层,TCP 协议对应于传输层
  • HTTP 协议是在 TCP 协议之上建立的,HTTP 在发起请求时通过 TCP 协议建立起连接服务器的通道,请求结束后,立即断开 TCP 连接
  • HTTP 是无状态的短连接,而 TCP 是有状态的长连接
  • TCP是传输层协议,定义的是数据传输和连接方式的规范,HTTP是应用层协议,定义的是传输数据的内容的规范
    说明:从HTTP/1.1起,默认都开启了Keep-Alive,保持连接特性,简单地说,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。

Netty简介

基本常识

在了解Netty之前,我们非常有必要简要了解一下Java网络编程模型的基本常识,具体说也就是BIO、NIO和AIO这3个技术概念。
BIO、NIO和AIO这三个概念分别对应三种通讯模型:阻塞、非阻塞、非阻塞异步,具体这里就不详细写了。网上好多博客说Netty对应NIO,准确来说,应该是既可以是NIO,也可以是AIO,就看你怎么实现。
这三个概念的区别如下:

  1. BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理,线程开销大。
  2. NIO:一个请求一个线程,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到该连接有I/O请求时才启动一个线程进行处理;
  3. AIO:一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

通俗地概括一下就是:

  1. 向流的,NIO是面向缓冲区的;
  2. BIO的各种流是阻塞的,而NIO是非阻塞的;
  3. BIO的Stream是单向的,而NIO的channel是双向的。

NIO的的显著特点:
事件驱动模型、单线程处理多任务、非阻塞I/O,I/O读写不再阻塞,而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。基于Reactor线程模型。

基本介绍

Netty是一个Java NIO技术的开源异步事件驱动的网络编程框架,用于快速开发可维护的高性能协议服务器和客户端。

往通俗了讲,可以将Netty理解为:一个将Java NIO进行了大量封装,并大大降低Java NIO使用难度和上手门槛的超牛逼框架。

技术特征

Netty的优点,概括一下就是:

  1. 使用简单;
  2. 功能强大;
  3. 性能强悍。

Netty的特点:

  1. 高并发:基于 NIO(Nonblocking IO,非阻塞IO)开发,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高;
  2. 传输快:传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输;
  3. 封装好:封装了 NIO 操作的很多细节,提供了易于使用调用接口。

Netty的优势:

  1. 使用简单:封装了 NIO 的很多细节,使用更简单;
  2. 功能强大:预置了多种编解码功能,支持多种主流协议;
  3. 扩展性强:可以通过 ChannelHandler 对通信框架进行灵活地扩展;
  4. 性能优异:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优;
  5. 运行稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身;
  6. 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。

Netty高性能表现在哪些方面?

  1. IO 线程模型:同步非阻塞,用最少的资源做更多的事;
  2. 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输;
  3. 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况;
  4. 串形化处理读写:避免使用锁带来的性能开销;
  5. 高性能序列化协议:支持 protobuf 等高性能序列化协议。

可以参考史上最通俗Netty入门长文:基本介绍、环境搭建、动手实战

代码实现-Spring + Netty + Mybatis

这里的代码主要是在spring-netty-demo 的基础之上修改的。
具体代码已经推到githup ,地址为:spring-netty
这里有几个注意点:

  1. 如果想要打印mybatis的debug日志,即想看执行的哪个sql,可以在yml里设置:
    logging:
      # mybatis sql 打印
      level:
        root: INFO  # 设置全局日志级别为DEBUG
        com.digiwin.ltgx.core: debug
        com.digiwin.ltgx.mapper: DEBUG
      file: /root/ltgx-api/netty-server.log
  2. CustomServerHandler类不能用spring维护,这里添加@Component(“customServerHandler”),之后使用BeanFactoryContext.findBeanByName,会报空指针异常,不知道因为什么
  3. 根据spring-netty-demo默认的代码,netty-client收到server的反馈后,不会自动断开连接,正常的业务应该是,server端发出反馈后,就应该把channel关闭,然后客户端收到server的反馈后,也应该正常关闭
    // server端应该添加以下代码:
    ctx.write(responseJson + Constants.delimiter).addListener(ChannelFutureListener.CLOSE);
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
    // client端应该添加如下代码:
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().closeFuture();
        super.channelReadComplete(ctx);
    }
    // 需要注意的是,client其实也是一个服务,是一直启动着的,虽然这里close了channal,但是,client的代码不会停止,如果要停止,需要在client端添加以下代码:
    group.shutdownGracefully();