Netty总结

传统 HTTP服务器

  • 创建一个 ServerSocket ,监听并绑定一个端口
  • 一系列客户端来请求这个端口
  • 服务器使用 Accept ,获得一个来自客户端的socket连接对象
  • 启动一个新线程处理连接
    • 读socket,得到字节流
    • 解码协议,得到http请求对象
    • 处理http请求,得到一个结果,封装成一个httpResponse对象
    • 编码协议,将结果序列化字节流 写socket , 将字节流发给客户端

NIO处理的HTTP服务器

NIO 代表的一个词汇叫做 IO多路复用。它是由操作系统提供的系统调用,早起这个操作系统调用的名字是 select ,但是性能低下,后来渐渐演化成了 liunx下的 epoll 和 mac下的kqueue.

说NIO 之前先说一下 BIO(Blocking IO),如何理解这个Blocking呢?

  • 客户端监听时,Accept是阻塞的,只有新连接来了,Accept 才会返回,主线程才能继续
  • 读写 sockett时,Read 是阻塞的,只有请求消息来了,Read 才能返回 ,子线程才能继续处理
  • 读写socket 时 ,Write是阻塞的,只有客户端把消息收了,Write 才能返回,子线程才能继续读取下一个请求
while true {
    events = takeEvents(fds)  // 获取事件,如果没有事件,线程就休眠
    for event in events {
        if event.isAcceptable {
            doAccept() // 新链接来了
        } elif event.isReadable {
            request = doRead() // 读消息
            if request.isComplete() {
                doProcess()
            }
        } elif event.isWriteable {
            doWrite()  // 写消息
        }
    }
}

Netty 执行流程

Netty服务端创建时序图

ServerBootstrap serverBootStrap = new ServerBootstrap();

		// boss 用于接受新连接的线程 主要负责创建新连接
		//worker 用于负责读取数据的线程 主要用于读取数据以及业务逻辑处理
		/**
		 * 首先创建两个 NioEventLoopGroup 这两个线程可以看做是传统IO 编程模型的两大线程组
		 *  boosGroup 表示监听端口 accpet 新连接的线程组
		 *  workGroup 表示处理每一条连接的数据读写的线程组
		 *  *** 用生活中的例子来讲就是 一个工厂要运作 必然要有一个老板负责从外面接活  老板就是bossGroup
		 *  *** 然后有很多员工,负责具体干活 ,员工就是 workerGroup bossGroup接收完连接,扔给workerGroup 处理
		 *
		 */
		NioEventLoopGroup boss = new NioEventLoopGroup();
		NioEventLoopGroup worker = new NioEventLoopGroup();

		/**
		 *接下来 我们创建了一个 引导类 ServerBootstrap 这个类将引导我们进行服务端的启动工作 直接new 出来就行了
		 *  我们通过 .group(bossGroup ,workerGroup) 给引导类配置两大线程组 这个引导类的线程模型也就定型了
		 *然后 我们指定我们服务端的IO模型为 NIO ,我们通过 .channel(NioServerSocketChannel.class) 来指定 IO模型
		 *  然,这里也有其他选择,如果我们想指定 IO模型为 BIO ,那么这里配置上 OioServerSocketChannel.class 类型即可
		 *  然通常我们也不会这么做,因为 Netty 的优势就在于 NIO
		 *接着,我们调用 childHandler() 方法给引导类创建一个 ChannelInitializer ,这里主要就是定义后续每条连接的数据读写 ,
		 * 	业务处理 ,不理解没关系,后面继续详解
		 * 	ChannelInitializer 这个类中,我们注意到有一个泛型参数 NioSocketChannel ,
		 * 	这个类呢就是 Netty 对NIO类型的连接的抽象,而我们前面 NioServerSocketChannel 也是对NIO类型的连接的抽象
		 * 	NioServerSocketChannel 和 NioSocketChannel 的概念都可以和 BIO 编程模型中的 ServerSocket 和 Socket 两个概念对应上
		 *
		 * 我们的最小化参数配置到这里就完成了
		 *
		 * !!! 总结一下 !!!
		 *  要启动一个 Netty 服务端,必须要指定 三类属性:
		 *  分别是线程模型 IO 模型 连接读写处理逻辑
		 *  有了这三者,之后在调用 bind(port) 就可以在本地绑定一个 port 端口启动起来
		 *
		 *
		 */
		serverBootStrap
				.group(boss, worker)
				.channel(NioServerSocketChannel.class)
				.childHandler(new ChannelInitializer<NioSocketChannel>() {
					@Override
					protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
						nioSocketChannel.pipeline().addLast(new FirstServerHandler())
							.addLast(new SecondServerHandler());

					}
				});
		// 可以和 childHandler() 对应起来, childHandler() 用于指定处理新连接数据的读写处理逻辑 Handler() 用于指定在服务端启动过程中的
		//一些逻辑 通常情况下,我们用不着这个方法
		serverBootStrap.handler(new ChannelInitializer<NioServerSocketChannel>() {
			@Override
			protected void initChannel(NioServerSocketChannel nioServerSocketChannel) throws Exception {
				System.out.println("服务器启动中...");
			}
		});
		/** attr() 方法可以给服务端的 channel ,也就是NioServerSocketChannel 指定一些自定义属性
		 *  然后可以通过 channel.attr() 这个方法取出这个属性 比如这里给服务端channel 指定了一个 severName ,属性值为 nettyName
		 *  说白了 就是给 NioServerSocketChannel 维护了一个Map 通常情况下 我们也用不上这个方法
		 */
		serverBootStrap.attr(AttributeKey.newInstance("serverName"), "nettyName");
		/**
		 * attr() 是给 服务端 NioServerSocketChannel 指定一些属性
		 * childAttr() 是给每一条连接 NioSocketChannel 然后我们可以通过 channel.attr 取出该属性
		 */
		serverBootStrap.childAttr(AttributeKey.newInstance("cilentKey"), "cilentName");

		/**
		 *  childOption() 可以给每条连接设置一些 TCP 底层相关的属性,
		 *  比如 ChannelOption.SO_KEEPALIVE 表示是否开启 TCP底层心跳机制 true 为开启
		 *  ChannelOption.TCP_NODELAY 表示是否开启 Nagle 算法 true 表示关闭 false 表示开启
		 *  通俗地说 如果实时性要求高 有数据就马上发送 就关闭 ; 如果要减少发送次数减少网络交互 就开启
		 */
		serverBootStrap.childOption(ChannelOption.SO_KEEPALIVE, true)
				.childOption(ChannelOption.TCP_NODELAY, true);
		/**
		 * 除了给每个连接设置一系列属性之外 还可以给服务端 channel 设置一些属性
		 * 最常见的就是 so_backlog
		 * 表示系统用于临时存放已经完成三次握手的请求的队列的最大长度
		 * 如果连接建立频繁 服务器创建新连接较慢,可以适当调大这个参数
		 */
		serverBootStrap.option(ChannelOption.SO_BACKLOG, 1024);
		bind(serverBootStrap, 8000);
				//.bind(8000)
		User user = new User();
		//user.getName();
	}


	private static void bind(final ServerBootstrap serverBootstrap, final int port) {
		serverBootstrap.bind(port)
				.addListener(new GenericFutureListener<Future<? super Void>>() {
					@Override
					public void operationComplete(Future<? super Void> future) throws Exception {
						if (future.isSuccess()) {
							System.out.println("端口: " + port +"绑定成功");
						} else {
							System.err.println("端口: " + port +"绑定失败");
							bind(serverBootstrap, port + 1);
						}
					}
				});
	}
  • 服务端创建流程
    • 创建 ServerBootStrap 实例
    • 创建并设置 Reactor 线程池:EventLoopGroup ,EventLoop 就是处理所有注册到本线程的Selector 上面的 channel
    • 设置并绑定服务端的 channel
    • 创建处理网络事件的 ChannelPipeline 和 handler ,网络事件以流的形式在其中流转
    • hander 完成多数的功能定制
    • 绑定并启动监听端口
    • 当轮训到准备就绪的channel 后,由 Reactor 线程:NioEventLoop 执行 pipeline中的方法,最终调度并执行 channelHandler
  • 客户端流程

		 Bootstrap bootStrap = new Bootstrap();
		NioEventLoopGroup group = new NioEventLoopGroup();

		bootStrap
				//指定线程模型
				.group(group)
				//指定 IO类型为 NIO
				.channel(NioSocketChannel.class)
				// IO处理逻辑
				.handler(new ChannelInitializer<Channel>() {
					@Override
					protected void initChannel(Channel channel) throws Exception {
						// ch.pipeline() 返回的是和这条连接相关的逻辑处理链 采用了责任链模式
						channel.pipeline()
								/** addLast() 方法添加一个逻辑处理器 这个逻辑处理器为的就是在客户端建立连接成功之后
								 * 向服务端写数据 下面编写这个逻辑处理器相关的代码
								 */
								//.addLast(new StringEncoder())
								.addLast(new FirstClientHandler());
						//指定 连接数据读写逻辑
					}
				});
		// 常规建立连接
		//bootStrap.connect("127.0.0.1", 8000)
		//		.addListener(future -> {
		//			if (future.isSuccess()) {
		//				System.out.println("连接成功");
		//			} else {
		//				System.out.println("连接失败");
		//			}
		//		});

		String host = "127.0.0.1";
		int port = 8000;

		// 添加自动重连的连接
		//ChannelFuture channelFuture = connect(bootStrap, host, port);

		//智能化的自动重连机制
		ChannelFuture channelFuture = connect(bootStrap, host, port, 5);
		Channel channel = channelFuture.channel();


	}

	/**
	 * @Description: 支持自动重连的方式建立连接
	 * @Param: [bootstrap, host, port]
	 * @return: void
	 * @Author: Administrator
	 * @Date: 2021/8/23
	 */
	private static ChannelFuture connect(Bootstrap bootstrap, String host, int port) {
		return bootstrap.connect(host, port).addListener( future -> {
					if (future.isSuccess()) {
						System.out.println("连接成功");
					} else {
						System.out.println("连接失败,开始重连");
						connect(bootstrap, host, port);
					}
				});
	}

	/**
	 * @Description: 智能化的自动重连,超过失败次数就不再连接
	 *  连接建立失败不会立即重连,而是会通过一个指数退避的方法 比如 1秒 2秒 4秒 8秒,以2的幂次来建立连接
	 *  然后达到一定次数之后就放弃连接
	 * @Param: [bootstrap, host, port, retry]
	 * @return: void
	 * @Author: Administrator
	 * @Date: 2021/8/23
	 */
	private static ChannelFuture connect(Bootstrap bootstrap, String host, int port, int retry) {
		return bootstrap.connect(host, port).addListener(future -> {
			if (future.isSuccess()) {
				System.out.println("连接成功");
			} else if (retry ==0 ) {
				System.err.println("重连次数已用完,放弃连接!");
			} else {
				//第几次连接
				int order = (MAX_RETRY - retry) +1;
				//本次重连的间隔
				int delay = 1 << order;
				System.err.println(new Date() + ": 连接失败, 第" + order + "次重连");
				// 返回 BootstrapCofig 这是对Bootsrap配置参数的抽象
				bootstrap.config()
						// bootsrap.config().group() 返回的就是我们在一开始的时候配置的线程模型 workerGroup
						.group()
						//调用 workerGroup 的schedule 就可以实现定时任务逻辑
						.schedule(()->
								connect(bootstrap, host, port, retry - 1), delay , TimeUnit.SECONDS
						);
			}
		});
	}

相关概念和常见事件

socket里的零拷贝

1. `File.read(bytes)`
2. `Socket.send(bytes)`
这种方式需要四次数据拷贝和四次上下文切换:
  • 具体流程
    • 数据从磁盘读取到内核 read buffer
    • 数据从内核缓冲区拷贝到用户缓冲区
    • 数据从用户缓冲区拷贝到内核的 socket buffer
    • 数据从内核的socket buffer 拷贝到网卡接口/硬件的 缓冲区

零拷贝的概念

上面的第二步和第三步是没必要的,通过 java 的 FileChannel.transferTo 方法,直接在通道之间传递数据,可以避免上面两次多余的拷贝

  • 具体流程

    • 调用 transferTo 数据从文件由DMA引擎拷贝到内核 readerBuffer
    • 接着DMA从内核reader Buffer 将数据拷贝到网卡接口 bufer
    • 上面两次操作都不需要CPU参与 ,所以就达到了零拷贝

    Netty中的零拷贝

    主要体现在三个方面:

    • bytebuffer

      • Netty发送和接收消息主要使用 bytebuffer ,bytebuffer 使用堆外内存/直接内存 进行socket读写 (如果使用传统的堆内存进行socket读写,JVM会将堆内存buffer拷贝一份到直接内存再写入socket 多了一次缓冲区的拷贝,DirectMemory中可以直接通过DMA发送到网卡接口)
    • composite Buffers

      • 传统的ByteBuffer ,如果需要将两个byteBuffer中的数据组合到一起,我们需要首先创建一个size = size 1 + size2 大小的新数组,然后将两个数组中的数据拷贝到新的数组中。但是netty中的compositeByteBuf并没有真正将多个buffer组合起来,而是保存了他们的引用,从而避免了数据的拷贝,实现了零拷贝
    • FileChannel. transferTo

    • 该方法依赖于操作系统实现零拷贝

TCP 3次握手 和 四次挥手

  • 握手

    • 客户端发建立连接请求,第一次握手
    • 服务端收到请求,给客户端发个确认,第二次握手
    • 客户端收到服务器的确认,给服务端发个请求,用来确认客户端收到了服务端的确认 第三次握手
  • 四次挥手

    TCP 双工通讯,每个方向都必须单独进行关闭
    以客户端主动发起关闭为例:

    • 客户端发请求,跟服务端说准备分手 这是第一次挥手
    • 服务端收到请求后,就回了个滚进行确认 这是第二次挥手
    • 服务端准备拆 服务端到客户端这条链路这是第三次挥手
    • 客户端收到服务端的确认后,再给服务端发送最后一个确认,然后关闭客户端到服务端的链路,服务端收到之后马上也关闭服务端到客户端的链路 这就是第四次挥手

长链接和短连接

  • 短连接

    • 连接 -> 传输数据 ->关闭连接
    • 短连接指socket 连接,发送数据,接收数据后,马上断开连接
  • 长连接

    • 连接 -> 传输数据 ->保持连接 ->传输数据 … 关闭连接
    • 建立socket连接后,不管是否使用,一致保持连接

半包 和 粘包 分包

在TCP中只有流的概念,没有包的概念。
TCP是一种流协议,这就意味着数据是以字节流的形式传递给接收者的,没有固定的报文或者报文边界的概念。从这方面来讲,读取TCP数据就像从串口端口中读取数据一样,无法预知在一次读取中会返回多少个字节。

  • 发送方粘包

TCP为了提高效率,发送方往往要收集到足够多的数据才发送一包数据,若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一个包后一次发送出去,这样接收方就收到了粘包数据。
这么做的有点也很明显,就是为了减少广域网的小分组数目,从而减小网络阻塞的出现。总的来说就是:发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取

  • 接收方粘包
    接收方用户进程不及时接收,从而导致的粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区读取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次性取到了多包数据。

-半包
接收方没有接收到一个完整的包,只接受了部分。
这种情况是因为TCP为了提高传输效率,将一个包分配的足够大,导致接收方并不能一次接收完。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值