山东大学Netty学习小组NIO网络编程应用实例-群聊系统与NIO 零拷贝 2021SC@SDUSC

2021SC@SDUSC

SelectionKey

1)SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:

int OP_ACCEPT:有新的网络连接可以 accept,值为 16 int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4
源码中:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;

2)SelectionKey 相关方法
selectkey

ServerSocketChannel

1)ServerSocketChannel 在服务器端监听新的客户端 Socket 连接
2)相关方法如下在这里插入图片描述
在这里插入图片描述

SocketChannel

在这里插入图片描述
SocketChannel实现的接口更多,更重要的是对于数据的读写,而ServerSocketChannel是有一个连接来了,生成一个SocketChannel,分工不同。
1)SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

2)相关方法如下
在这里插入图片描述

NIO 网络编程应用实例-群聊系统

实例要求:
1)编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以监测用户上线,离线,并实现消息转发功能
4)客户端:通过 channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
5)目的:进一步理解 NIO 非阻塞网络编程机制
6)示意图分析和代码

图
1.先编写服务器
1.1服务器启动并监听6667,先启动服务器端
1.2服务器接收客户端消息,并实现转发【转发注意要排除自己处理上线和离线】
2.编写客户器
2.1连接服务器
2.2发送消息
2.3接收服务器消息

服务器端


package com.shandonguniversity.nio.groupchat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class GroupChatServer {
    //定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;//用于监听
    private static final int PORT = 6667;

    //构造器
    //初始化工作
    public GroupChatServer() {

        try {

            //得到选择器
            selector = Selector.open();
            //ServerSocketChannel
            listenChannel =  ServerSocketChannel.open();
            //绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞模式
            listenChannel.configureBlocking(false);
            //将该listenChannel 注册到selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);

        }catch (IOException e) {
            e.printStackTrace();
        }

    }

    //监听
    public void listen() {
        System.out.println("监听线程: " + Thread.currentThread().getName());
        try {
            //循环处理
            while (true) {

                int count = selector.select();
                if(count > 0) {//有事件处理

                    //遍历得到selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        //取出selectionkey
                        SelectionKey key = iterator.next();

                        //监听到accept
                        if(key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            //将该 sc 注册到seletor
                            sc.register(selector, SelectionKey.OP_READ);

                            //提示
                            System.out.println(sc.getRemoteAddress() + " 上线 ");

                        }
                        if(key.isReadable()) { //通道发送read事件,即通道是可读的状态
                            //处理读 (专门写方法,读取客户端消息)
                            readData(key);
                        }
                        //当前的key 删除,防止重复处理
                        iterator.remove();
                    }

                } else {
                    System.out.println("等待....");
                }
            }

        }catch (Exception e) {
            e.printStackTrace();

        }finally {
            //发生异常处理....

        }
    }

    //读取客户端消息,从相关联的通道读取,
    //通过获得SelectionKey key,反向读取对应的channel
    private void readData(SelectionKey key) {

        //取到关联的channle
        SocketChannel channel = null;
        try {
           //得到channel,强转为SocketChannel
            channel = (SocketChannel) key.channel();
            //创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int count = channel.read(buffer);
            //根据count的值做处理
            if(count > 0) {
                //把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                //输出该消息
                System.out.println("from 客户端: " + msg);
                //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }

        }catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            }catch (IOException e2) {
                e2.printStackTrace();;
            }
        }
    }

    //转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{

        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        //遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for(SelectionKey key: selector.keys()) {
            //通过 key  取出对应的 SocketChannel
            Channel targetChannel = key.channel();

            //排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                //转型
                SocketChannel dest = (SocketChannel)targetChannel;
                //将msg 存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer 的数据写入 通道
                dest.write(buffer);
            }
        }

    }

    public static void main(String[] args) {

        //创建服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

//可以写一个Handler
class MyHandler {
    public void readData() {

    }
    public void sendInfoToOtherClients(){

    }
}


客户端

package com.shandonguniversity.nio.groupchat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class GroupChatClient {

    //定义相关的属性
    private final String HOST = "127.0.0.1"; // 服务器的ip
    private final int PORT = 6667; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //构造器, 完成初始化工作
    public GroupChatClient() throws IOException {

        selector = Selector.open();
        //连接服务器
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");

    }

    //向服务器发送消息
    public void sendInfo(String info) {
        info = username + "说:" + info;

        try {
        //把消息发送过去
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取从服务器端回复的消息
    public void readInfo() {
        try {

            int readChannels = selector.select();
            if(readChannels > 0) {//有可以用的通道
            
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        //得到相关的通道
                       SocketChannel sc = (SocketChannel) key.channel();
                       //得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());//去掉头尾空格
                    }
                }
                iterator.remove(); //删除当前的selectionKey, 防止重复操作
            } else {
                //System.out.println("没有可以用的通道...");

            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {

        //启动我们客户端
        GroupChatClient chatClient = new GroupChatClient();

        //启动一个线程, 每隔3秒,读取从服务器发送数据
        new Thread() {
            public void run() {
                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.currentThread().sleep(3000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

NIO 与零拷贝

1.零拷贝基本介绍

1)零拷贝是网络编程的关键,很多性能优化(比如文件传输)都离不开。

2)在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile方法。那么,他们在 OS 里,到底是怎么样的一个的设计?我们分析 mmap 和 sendFile 这两个零拷贝

3)另外我们看下 NIO 中如何使用零拷贝

2.传统IO 数据读写劣势

Java 传统 IO 和 网络编程的一段代码
初学 Java 时,我们在学习 IO 和 网络编程时,会使用以下代码:
在这里插入图片描述
byte[] arr字节数组大小和文件大小保持一致,通过raf的read方法把文件的数据读入到arr字节数组,拿到ServerSocket,通过socket得到输出流对象,实际上就是文件读写的过程。

3.传统IO 模型

DMA: direct memory access 直接内存拷贝(不使用 CPU)
蓝色是用户态,粉色是内核态
我们会调用 read 方法读取 index.html 的内容—— 变成字节数组,然后调用 write 方法,将 index.html 字节流写到 socket 中,那么,我们调用这两个方法,在 OS 底层发生了什么呢?我这里借鉴了一张其他文字的图片,尝试解释这个过程。
清晰
上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:
1.read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
2.发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
3.发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
4.第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
5.write 方法返回,再次从内核态切换到用户态。

先把硬件上的数据进行DMA拷贝到内核缓存(kernel buffer),然后CPU拷贝到用户缓存(user buffer),数据在用户缓存进行修改,再用CPU拷贝到socket buffer,再用DMA拷贝到协议栈(protocol engine)。共经过4次拷贝,3次切换。拷贝次数很多,代价很高。于是就有了memory map(内存映射)优化。

如你所见,复制拷贝操作太多了。如何优化这些流程?

4.mmap 优 化

1)mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
2)mmap 示意图
在这里插入图片描述
如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。

先把硬件上的数据进行DMA拷贝到内核缓存(kernel buffer),内核缓存(kernel buffer)和用户缓存(user buffer)共享数据,数据可以在内核缓存进行修改,再通过CPU拷贝到socket buffer,再用DMA拷贝到协议栈(protocol engine)。拷贝次数减少为3次,但切换次数(上边蓝粉转化)仍为3次。

5.sendFile 优 化

1)Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换

2)示意图和小结
在这里插入图片描述

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用,然后掉一共 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为在一个用户空间。
最后,数据从 Socket 缓冲区进入到协议栈。
此时,数据经过了 3 次拷贝,2 次上下文切换。
那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?

3)提示:零拷贝从操作系统角度,是没有 cpu 拷贝
4)Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,实现了零拷贝,直接拷贝到协议栈, 从而再一次减少了数据拷贝。具体如下图和小结:
在这里插入图片描述
现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。
等一下,不是说零拷贝吗?为什么还是要 2 次拷贝?
答:首先我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。
而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

共经过2次拷贝,2次切换。

5)这里其实有 一次 cpu 拷贝
kernel buffer -> socket buffer
但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略

零拷贝的再次理解

1)我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。

2)零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

mmap 和 sendFile 的 区 别

1)mmap 适合小数据量读写,sendFile 适合大文件传输。

2)mmap 需要 3 次上下文切换,3 次数据拷贝;sendFile 需要 2次上下文切换,最少 2 次数据拷贝。

3)sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NIOS2 设计方案开发技术资料FPGA设计文档开发应用例子资料69个合集: Nios 2 小册子.pdf NIOS 2 开发常见问题.pdf Nios 2 软件代码优化方法.pdf NIOS 2常见问题总结.pdf NIOS 2应用开发ABC.pdf NIOS 2熟悉开发环境.pdf Nios2 的Boot过程分析.pdf NIOS 2熟悉开发环境.pdf NIOS2 FLASHStep_by_step.pdf Nios2之常见问题解答.pdf Nios2之软件调试技巧.pdf Nios2系统开发流程.pps Nios2软件调试技巧.pdf Nios_Starter.pdf Nio2集成开发环境问世.pdf NIOS入门.pdf Nios2入门实验程序问题整理.pdf Nios2嵌入式处理器的CPU结构.pdf NIOS2嵌入式系统的可扩展性分析.pdf Nios2教材.pdf NIOS2软核处理器的Linux引导程序U_boot设计.pdf 基于Altera Nios平台的信号高速采集系统.pdf 基于ARM+FPGA的大屏幕显示器控制系统设计.pdf 基于DSP与FPGA的LED显示屏控制系统的设计.pdf 基于DSP与FPGA的LED显示屏控制系统的设计2.pdf 基于DSP与FPGA的LED显示屏控制系统的设计3.pdf 基于DSP与FPGA的LED显示屏控制系统的设计4.pdf 基于DSP与FPGA的LED显示屏控制系统的设计5.pdf 基于DSP与FPGA的LED显示屏控制系统的设计6.pdf 基于DSP与FPGA的LED显示屏控制系统的设计7.pdf 基于FPGA乒乓球比赛游戏机的设计.pdf 基于FPGA圆阵超声自适应波束形成的设计.pdf 基于FPGA的AGWN信号生成器.pdf 基于FPGA的DDR SDRAM控制器在高速数据采集系统中的应用.pdf 基于FPGA的FFTIFFT处理器的实现.PDF 基于FPGA的PCB测试机硬件电路设计.pdf 基于FPGA的VGA时序彩条信号实现方法及其应用.pdf 基于FPGA的VGA显示模块设计.pdf 基于FPGA的嵌入式系统设计.pdf 基于FPGA的快速傅立叶变换.PDF 基于FPGA的总线型LVDS通信系统设计.pdf 基于FPGA的简易微型计算机结构.pdf 基于FPGA的简易数字存储示波器设计.pdf 基于FPGA的远程图像采集系统设计.pdf 基于FPGA的高速数字隔离型串行ADC应用.pdf 基于Nios 的通用数字调制器设计与实现.pdf 基于Nios的DDS技术在电磁无损检测中的应用.pdf 基于Nios的LED显示屏控制系统.pdf 基于Nio和eCos的串口通信程序开发.pdf 基于NIO嵌入式软核处理器的LCD控制方法研究.pdf 基于Nio的USB接口模块设计.pdf 基于Nio的纤维自动识别系统设计.pdf 基于NIOS处理器的SOPC应用系统研究与设计.pdf 基于NIOS处理器的面阵CCD采集系统设计.pdf 基于Nios嵌入式软核处理器的液晶显示屏控制.pdf 基于NIOS嵌入式软核的硬盘录像机的设计.pdf 基于Nios的DDS信号源实现.pdf 基于Nios的DDS高精度信号源实现.pdf 基于Nios的SOPC系统设计以及程序引导.pdf 基于NIOS的SOPC设计.pdf 基于Nios的掌纹鉴别系统设计与实现.pdf 基于NIOS的无中心电话系统中噪声的分析与抑制.pdf 基于Nios的通用编译码器的设计.pdf 基于NIOS软核CPU技术的多路电话计费系统的设计与实现.pdf 基于Nios软核的音频效果器.pdf 基于SYSTEM C的FPGA设计方法.pdf 基于声卡的LabVIEW数据采集与分析系统设计.pdf 基于多种EDA工具的FPGA设计.pdf 基于模糊控制的迟早门同步器及其FPGA实现.pdf

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值