Java 网络编程之TCP(五):分析服务端注册OP_WRITE写数据的各种场景(二)

这篇具有很好参考价值的文章主要介绍了Java 网络编程之TCP(五):分析服务端注册OP_WRITE写数据的各种场景(二)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

接上文

二、注册OP_WRITE写数据

服务端代码:

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

/**
 * 基于NIO实现服务端,通过Selector基于事件驱动客户端的读取
 * 服务端接收到数据后,缓存,注册OP_WRITE事件,收到状态转发数据
 *
 */
class NIOSelectorOpWriteServer1 {
    Selector selector;

    public static void main(String[] args) throws IOException {
        NIOSelectorOpWriteServer1 server = new NIOSelectorOpWriteServer1();
        server.start(); // 开启监听和事件处理
    }

    public void start() {
        initServer();
        // selector非阻塞轮询有哪些感兴趣的事件到了
        doService();
    }

    private void doService() {
        if (selector == null) {
            System.out.println("server init failed, without doing read/write");
            return;
        }
        try {
            while (true) {
                while (selector.select() > 0) {
                    Set<SelectionKey> keys = selector.selectedKeys(); // 感兴趣且准备好的事件
                    Iterator<SelectionKey> iterator = keys.iterator(); // 迭代器遍历处理,后面要删除集合元素
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove(); // 删除当前元素,防止重复处理
                        // 下面根据事件进行分别处理
                        if (key.isAcceptable()) {
                            // 客户端连接事件
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            // 读取客户端数据
                            readHandler(key);
                        } else if (key.isWritable()) {
                            // 为了避免重复写,需要先去除OP_WRITE注册状态
                            key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

    private void initServer() {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(9090));

            // 此时在selector上注册感兴趣的事件
            // 这里先注册OP_ACCEPT: 客户端连接事件
            selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("server init success");
        } catch (IOException exception) {
            exception.printStackTrace();
            System.out.println("server init failied");
        }
    }

    public void acceptHandler(SelectionKey key) {
        ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取客户端的channel
        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false); // 设置client非阻塞
            System.out.println("server receive a client :" + client);
            // 注册OP_READ事件,用于从客户端读取数据
            // 给Client分配一个buffer,用于读取数据,注意buffer的线程安全
            ByteBuffer buffer = ByteBuffer.allocate(1024); // buffer这个参数注册的时候也可以不用
            client.register(key.selector(), SelectionKey.OP_READ, buffer);
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        System.out.println("read handler");
        SocketChannel client = (SocketChannel) key.channel(); // 获取客户端的channel
        ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取Client channel关联的buffer
        buffer.clear(); // 使用前clear

        // 防止数据分包,需要while循环读取
        try {
            while (true) {
                int readLen = client.read(buffer);
                if (readLen > 0) {
                    // 读取到数据了
                    buffer.flip();
                    byte[] data = new byte[buffer.limit()];
                    buffer.get(data);
                    System.out.println("server read data from " + client + ", data is :" + new String(data));
                    // 给其他客户端注册OP_WRITE;
                    registerWrite(client, data);
                } else if (readLen == 0) {
                    // 没读到数据
                    System.out.println(client + " : no data");
                    break;
                } else if (readLen < 0) {
                    // client 关闭连接
                    // 当客户端主动连接断开时,为了让服务器知道断开了连接,会产生OP_READ事件。所以需要判断读取长度,当读到-1时,关闭channel。
                    System.out.println(client + " close");
                    client.close();
                    break;
                }
            }
        } catch (IOException exception) {
            exception.printStackTrace();
            // client 关闭连接
            System.out.println(client + " disconnect");
            // todo:disconnect 导致一直有read事件,怎么办?
            try {
                client.close();
            } catch (IOException ex) {
                System.out.println("close ex");
            }
        }
    }

    public void writeHandler(SelectionKey key) {
        System.out.println("write handler");
        SocketChannel client = (SocketChannel) key.channel(); // 获取客户端的channel
        ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取Client channel关联的buffer,此时处于读模式

        try {
            while (buffer.hasRemaining()) {
                client.write(buffer);
            }
        } catch (IOException exception) {
            System.out.println("write error");
            exception.printStackTrace();
        }
    }

    private void registerWrite(SocketChannel myself, byte[] data) throws IOException {
        Set<SelectionKey> keys = selector.keys();
        // read/write 对应同一个key,同一个client不会发送两遍
        for (SelectionKey key : keys) {
            SelectableChannel channel = key.channel();
            if (channel instanceof SocketChannel && channel != myself) {
                key.interestOps(key.interestOps() + SelectionKey.OP_WRITE);
                ByteBuffer attachment = (ByteBuffer) key.attachment();
                attachment.clear();
                attachment.put(data);
                attachment.flip();
            }
        }
    }
}

这里有几个注意项:

1.在注册OP_WRITE时,需要给所有其他客户端注册;

2.注册OP_WRITE时:是使用key.interestOps(key.interestOps() + SelectionKey.OP_WRITE);避免对原来的OP_READ事件进行覆盖;在OP_WRITE事件来的时候,要把先把OP_WRITE事件去掉,key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); 防止重复写事件状态发生;

3.注册OP_WRITE时,要写的数据,直接给到了原来channel对应的attachment里了;在OP_WRITE事件来的时候,可以直接用;

到此,我们一定有个疑问:既然服务端关不关注OP_WRITE事件,都可以给客户端发送数据,意义何在?

那我们就要看下OP_WRITE事件的具备条件:send-queue是否有空间

而服务端要写数据要关注:服务端数据是否准备好了 + send-queue是否有空间

服务端一般都是在自己数据准备好了后,再注册对客户端的OP_WRITE事件。

但是上面的代码中,在给客户端写数据时,是一直写,直到数据写完,但是 send-queue空间有限,当 send-queue写满后,写操作就会阻塞,导致单线程下业务阻塞。。。

此时,OP_WRITE的优势就体现出来了

我们可以对OP_WRITE的使用方式稍微调整,就可以解决上面的问题:

当我们收到OP_WRITE事件,开始给客户端写数据后,当我们发现该客户端对应的send-queue写满,SocketChannel.write(buffer)会返回已经写出去的字节数,此时为0;我们根据此标志知道,此时send-queue满,不能再写了,此时我们记录下没有写的数据,再次给该客户端注册一个OP_WRITE事件,结束本次写过程;让业务线程继续处理其他事件,等到该客户端对应的send-queue有空闲的时候,会再次收到收到OP_WRITE事件,我们就可以继续写数据了;这样就是解决了写数据满导致业务阻塞的问题了。文章来源地址https://www.toymoban.com/news/detail-857524.html

到了这里,关于Java 网络编程之TCP(五):分析服务端注册OP_WRITE写数据的各种场景(二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 网络编程(一)TCP单进程服务器编程详解

    想要学习socket网络编程的读者一定要首先学好计算机网络的理论知识,包括 1)osi网络七层模型与ip四层模型 2)套接字含义 3)局域网通信过程 4)广域网通信过程 5)tcp,udp通信协议,在这两个协议中的连接建立,数据封装,传输过程,传输中可能遇到的问题的处理(差错控

    2024年02月15日
    浏览(34)
  • 【网络编程】demo版TCP网络服务器实现

    UDP和TCP的区别: 对于TCP协议有几个特点: 1️⃣ 传输层协议 2️⃣ 有连接(正式通信前要先建立连接) 3️⃣ 可靠传输(在内部帮我们做可靠传输工作) 4️⃣ 面向字节流 对于UDP协议有几个特点: 1️⃣ 传输层协议 2️⃣ 无连接 3️⃣ 不可靠传输 4️⃣ 面向数据报 可以看到

    2024年02月06日
    浏览(41)
  • 【网络编程】TCP流套接字编程(TCP实现回显服务器)

    Socket(既能给客户端使用,也能给服务器使用) 构造方法 基本方法: ServerSocket(只能给服务器使用) 构造方法: 基本方法: 客户端代码示例: 服务器代码示例: 运行结果: 代码执行流程: 服务器启动,阻塞在accept,等待客户端建立连接. 客户端启动.这里的new操作会触发和服务器之间建立连

    2024年04月25日
    浏览(52)
  • QT TCP网络编程——服务端的实现

    温馨提示:初来乍到,请多关照,文章如有不足,还请不吝赐教。本程序是仿照通讯助手(NetAssist.exe)进行界面设计以及基本功能实现的,主要实现的功能有监听服务器端口、发送、接收数据。使用到的工具及编程语言为:visual studio 2015、QT、C++,其中代码仅供参考 提示:以

    2024年02月04日
    浏览(31)
  • Java网络编程 - TCP通信

    快速入门(一发一收) TCP协议回顾 : TCP是一种面向连接,安全、可靠的传输数据的协议 传输前,采用“三次握手”方式,点对点通信,是可靠的 在连接中可进行大数据量的传输 TCP通信模式 : 在java中只要是使用java.net.Socket类实现通信,底层即是使用了TCP协议 编写客户端代码

    2024年01月19日
    浏览(37)
  • 【Java学习笔记】 68 - 网络——TCP编程、UDP编程

    https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter21/src 目录 项目代码 网络 一、网络相关概念 1.网络通讯 2.网络 3.IP地址 4.域名 5.端口号 6.网络通讯协议 TCP协议:传输控制协议 UDP协议: 二、InetAddress类 1.相关方法 三、Socket 1.基本介绍 2.TCP网络通信编程 基本介绍 应用案例

    2024年02月04日
    浏览(31)
  • Java网络编程(UDP、TCP)

    在网络通信协议下,不同计算机上运行的程序,进行的数据传输。 应用场景:即时通信、网游对战、金融证券、国际贸易、邮件、等等。 不管是什么场景,都是计算机跟计算机之间通过网络进行数据传输。 Java中可以使用java.net包下的技术轻松开发出常见的网络应用程序。

    2024年02月05日
    浏览(50)
  • 【Java】--网络编程:基于TCP协议的网络通信

    TCP协议(Transmission Control Protocol),即传输控制协议,是一种 面向连接 的, 可靠 的,基于 字节流 的传输层通信协议。数据大小无限制。 建立连接的过程需要 三次握手 。 断开连接的过程需要 四次挥手 。 使用TCP协议的通信双方分别为 客户端 和 服务器端 。 客户端负责向服务

    2024年01月23日
    浏览(47)
  • 网络编程(8.14)TCP并发服务器模型

    作业: 1. 多线程中的newfd,能否修改成全局,不行,为什么? 2. 多线程中分支线程的newfd能否不另存,直接用指针间接访问主线程中的newfd,不行,为什么? 多线程并发服务器模型原代码: 1.将newfd改成全局变量效果:  答:不行,因为newfd是全局变量的话,客户端连接后生成

    2024年02月13日
    浏览(32)
  • 网络编程:编写一个TCP客户端与服务端

    用的系统是Ubuntu。 socket用来创建套接字。这个函数服务端与客户端都要使用。 第一个参数用来制定地址族规范,比如 AF_INET(PF_INET) 表示IPv4地址, AF_INET6(PF_INET6) 表示IPv6地址。 第二个参数用来制定套接字的类型规范,如 SOCK_STREAM 表示面向连接的套接字, SOCK_DGRAM 表示面

    2024年02月01日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包