简体   繁体   English

读取 SelectionKey.isReadable 后,Selector.select() 不会阻塞

[英]Selector.select() is not blocking after a SelectionKey.isReadable is read

I have a Socket that connects to a ServerSocketChannel which passes off to another Selector.我有一个连接到 ServerSocketChannel 的套接字,它传递给另一个选择器。 The client socket sends a one time message of 8 bytes, I successfully read it, but then selector which I call selectorIO should block on the select() method, but it immediately returns and then re-reads the same message that was already sent.客户端套接字发送一条 8 字节的一次性消息,我成功读取了它,但是我调用selectorIO的选择器应该在select()方法上阻塞,但它立即返回,然后重新读取已发送的相同消息。

public void readData()
    {
        int numberOfKeys = 0;
        buffer = ByteBuffer.allocate(8);
        buffer.clear();
        
        while(true)
        {
            try
            {
                //This is not blocking anymore?!
                numberOfKeys = selectorIO.select();
                
                Set<SelectionKey> keys = selectorIO.selectedKeys();
                Iterator<SelectionKey> itr = keys.iterator();
                
                while(itr.hasNext())
                {
                    SelectionKey key = itr.next();
                    if(key.isReadable())
                    {
                        SocketChannel channel = (SocketChannel)key.channel();
                        
                        int numread = channel.read(buffer);
                    
                        String s = new String(buffer.array());
                        System.out.println(s);
                        System.out.println(numread);
                        
                        
                        buffer.flip();
                        
                        //channel.write(buffer);
                        int numwrote = channel.write(buffer);
                        System.out.println(numwrote+" Bytes writtent");
                    
                    
                        buffer.flip();
                        
                        //buffer.reset();
                    }   
                    itr.remove();
                }
        
            }
            catch(Exception e)
            {
                System.out.println(e);
            }
        }
    }

When you call the buffer.array() to create the String, the ByteBuffer has no clue that the bytes have been consumed, so the state of the ByteBuffer remains unchanged.当你调用 buffer.array() 创建 String 时,ByteBuffer 并不知道字节已经被消耗,所以 ByteBuffer 的 state 保持不变。 It still contains the read bytes and still wants them to be consumed.它仍然包含读取的字节并且仍然希望它们被消耗。 This causes the rereading of the same message.这会导致重新读取同一消息。

The array returned on ByteBuffer.array(), has no clue about how many useful bytes are available. ByteBuffer.array() 返回的数组不知道有多少可用字节。 If the array has a capacity of 10 and only 8 byte has been set, you are trying to create a String with 2 bogus bytes.如果数组的容量为 10 且仅设置了 8 个字节,则您正在尝试创建一个包含 2 个伪造字节的字符串。 And if only 2 bytes have been read, you try to create as string based on 2 instead of 8 bytes.如果只读取了 2 个字节,则尝试创建基于 2 个字节而不是 8 个字节的字符串。 The string creation approach is incorrect.字符串创建方法不正确。

After you do a channel.write, you should do a compact or clear depending on if data is still available or not.在执行 channel.write 之后,您应该根据数据是否仍然可用进行压缩或清除。

I normally use 2 separate ByteBuffers;我通常使用 2 个独立的 ByteBuffer; one for reading and one for writing.一本用于阅读,一本用于写作。

Multiple issues at work.工作中的多个问题。

Buffer Management缓冲区管理

String creation broken字符串创建中断

You create a ByteBuffer and like all BBs they have a set capacity.您创建一个 ByteBuffer 并且像所有 BB 一样,它们具有固定的容量。 You then read into it ( int numRead = channel.read(buffer) ) and this does one of two things:然后你读入它( int numRead = channel.read(buffer) ),这会做两件事之一:

  1. The capacity of the BB is less than the amount of bytes that can be immediately stuffed into that buffer, copied straight over from your.network card's buffers. BB 的容量小于可以立即塞入该缓冲区的字节数,直接从您的网卡缓冲区复制过来。

In this case, the entire BB is filled ( numRead will be equal to the BB's capacity), and the 'READ READY' status of that channel remains up (because there are still more bytes ready to copy over).在这种情况下,整个 BB 都已填满( numRead将等于 BB 的容量),并且该通道的“READ READY”状态保持不变(因为还有更多字节准备复制)。

Note that bb.array() returns the entire backing array , but in this scenario, given that the whole BB is filled to capacity, that 'works out' so to speak.请注意, bb.array()返回整个后备数组,但在这种情况下,假设整个 BB 已满,可以这么说。

  1. The capacity of the BB is more than the amount of bytes that can be immediately stuffed into that buffer. BB 的容量大于可以立即塞入该缓冲区的字节数。

In this case, numRead will be less than the total capacity of that bytebuffer, and new String(bb.array()) is broken - that would attempt to turn into a string the bytes you read so far and a whole bunch of garbage at the end.在这种情况下, numRead小于该字节缓冲区的总容量,并且new String(bb.array())被破坏- 这将尝试将您到目前为止读取的字节一大堆垃圾转换为字符串结束。

new String(bb.array(), 0, bb.position()) would do the job, but in general this isn't how you're meant to be doing things. new String(bb.array(), 0, bb.position())可以完成这项工作,但总的来说,这不是你应该做的事情。 For starters, you're now charset-confused (You really should be using new String(bb.array(), 0, bb.position(), StandardCharsets.UTF_8) - do not ever convert bytes to chars or vice versa unless you specify which encoding is being used, otherwise the system chooses for you and that's rarely correct, and always confusing).对于初学者,你现在对字符集感到困惑(你真的应该使用new String(bb.array(), 0, bb.position(), StandardCharsets.UTF_8) ——永远不要将字节转换为字符,反之亦然,除非你指定正在使用的编码,否则系统会为您选择,这很少是正确的,而且总是令人困惑)。

No proper resetting没有正确的重置

The general way a buffer is meant to be used is like this:缓冲区的一般使用方式是这样的:

  • fill it (either you fill it, or you call read() on something).填充它(要么你填充它,要么你调用 read() 某物)。
  • flip it.翻转。
  • process with it (either you give it to something that sends the data in it, or you go through the bytes).用它处理(要么你把它交给发送数据的东西,要么你 go 通过字节)。
  • clear it.清除它。
  • repeat.重复。

You fill it ( channel.read() ), then use direct array manipulation instead of flip+reads to 'process' it (by passing the backing array to a string constructor), and then you .flip() it which is the wrong call, you want .clear() .你填充它( channel.read() ),然后使用直接数组操作而不是 flip+reads 来“处理”它(通过将支持数组传递给字符串构造函数),然后你.flip()它是错误的打电话,你想要.clear()

BBs work that way because, well, logic: BB 以这种方式工作,因为,嗯,逻辑:

  • They have a set capacity and you don't necessarily use all of that capacity.它们具有固定容量,您不一定要使用所有这些容量。 Often you use a little less.通常你会少用一点。 So, you first fill it, and then you want that BB to allow injecting data from 0 all the way up to capacity: the 'position' is 0 (and as we fill this thing it updates), the 'limit' is set to the capacity.所以,你首先填充它,然后你希望 BB 允许从 0 一直注入数据到容量:'position' 是 0(当我们填充这个东西时它会更新),'limit' 设置为容量。
  • Then to process it, we want position to be 0 again (we start processing from the beginning of course), but we want limit not to be the end, because maybe it wasn't fully filled up exactly to capacity by whatever process put data into this thing... we want limit to be the position as it was (as that's where the 'process that filled the buffer' left things).然后处理它,我们希望 position 再次为 0(我们当然从头开始处理),但我们希望limit不是结束,因为无论进程放入数据,它都没有完全填满容量进入这个东西......我们希望limit是 position 原样(因为那是“填充缓冲区的进程”留下的东西)。 flip() does this: It sets position back to 0 and limit to where the position was. flip()这样做:它将 position 设置回 0 并限制到 position 所在的位置。

Once you've read data into your buffer and then processed that data, you want clear : You want position back to 0 and the limit back to the capacity, ready for the process that fills the buffer to star filling it again.将数据读入缓冲区然后处理该数据后,您需要清除:您希望 position 回到 0 并且限制回到容量,准备好填充缓冲区的进程再次开始填充它。 clear() does that. clear()就是这样做的。 Your code calls flip() which is wrong.您的代码调用flip()是错误的。

Confusion about selectors选择器的困惑

A selector is set up with certain thing you are 'interested in'.选择器设置了您“感兴趣”的某些东西。 When you ask it to .select() , you're saying:当你问它.select()时,你是在说:

  1. Is any of the stuff I'm interested in possible right now?我现在可能感兴趣的任何东西吗? If yes, return immediately .如果是,立即返回
  2. If not, go to sleep until something I'm interested in might be possible.如果没有,请 go 睡觉,直到我感兴趣的事情成为可能

The thing is, as you process a channel your opinion on what you're interested in changes over time , and you need to update that selector and turn on/off SelectorKey s as needed.问题是,当你处理一个频道时,你对你感兴趣的内容的看法会随着时间的推移而变化,你需要更新该选择器并根据需要打开/关闭SelectorKey

For example, let's say you are writing a simple chat program.例如,假设您正在编写一个简单的聊天程序。 Alice just pasted half of the collected works of shakespeare and your chat program now needs to send all this to Bob. Alice 刚刚粘贴了一半的莎士比亚作品集,您的聊天程序现在需要将所有这些发送给 Bob。 You should now turn on SelectorKey.OP_WRITE on bob's.network channel.您现在应该在 bob's.network 频道上打开SelectorKey.OP_WRITE It should have been off before as you did not have anything to send to bob.它应该在之前关闭,因为您没有任何东西要发送给鲍勃。 But you have something to send now, so turn it on.但是你现在有东西要发送,所以打开它。

You then go to select() which is highly likely to return immediately (the.network card has free buffer space for bob's connection).然后你 go 到select() ,这很可能立即返回(.network 卡有可用的缓冲区空间用于 bob 的连接)。 You start copying those collected works of shakespeare over into the bytebuffer but you won't 'make it' - that buffer's capacity is less than shakespeare's total size.您开始将那些收集的莎士比亚作品复制到字节缓冲区中,但您不会“成功”——该缓冲区的容量小于莎士比亚的总大小。 That's the point, that's fine.这就是重点,这很好。 You then hand that buffer to the.network and go back to selecting while still interested in OP_WRITE because you haven't copied all of shakespeare's collected works yet, you only did like a quarter so far.然后,您将该缓冲区交给 the.network,将 go 返回给选择,同时仍然对OP_WRITE感兴趣,因为您还没有复制所有莎士比亚的作品集,到目前为止您只做了四分之一。

Eventually the.network clears that buffer out through the.network cable, and only then will your selector go: Oh, hey, we're ready for some more writing!最终,.network 会通过 .network 电缆清除该缓冲区,然后您的选择器才会清除 go:哦,嘿,我们准备好进行更多的写作了!

You keep doing this process (add some more of the shakespeare you need to send) until you stuff the last of it in the buffer you then hand to the channel.您继续执行此过程(添加更多您需要发送的莎士比亚),直到将最后一个放入缓冲区,然后将其交给频道。 You should then remove SelectorKey.OP_WRITE because you now no longer care that the.network buffer has room.然后您应该删除SelectorKey.OP_WRITE因为您现在不再关心 .network 缓冲区是否有空间。

Whilst all this was going on, you have a problem: What if Alice keeps sending more and more books, and she sends them faster than bob can receive them?在这一切进行的同时,您遇到了一个问题:如果 Alice 不断发送越来越多的书,而且她发送的速度比 Bob 收到的速度快怎么办? That's possible, of course - maybe Alice is on glass fiber and Bob is on a satellite phone.当然,这是可能的——也许爱丽丝在使用玻璃光纤,而鲍勃在使用卫星电话。 You can of course choose to buffer all of this server side, but all things are limited: There comes a point when Alice has queued up 50GB worth of book content that you still have to send to Bob.您当然可以选择缓冲所有服务器端,但所有事情都是有限的:当爱丽丝排队等候 50GB 的书籍内容时,您仍然必须将其发送给鲍勃。 You can either decide that your server will just crash if Alice does this, or, you're going to have to put in a limit: Once the 'data that alice sent that I have yet to shove into the bob's channel' reaches a certain amount, you have to go: Okay, Alice, no more.您可以决定,如果爱丽丝这样做,您的服务器就会崩溃,或者,您将不得不设置一个限制:一旦“爱丽丝发送但我尚未推入鲍勃频道的数据”达到某个特定值金额,你必须 go:好的,Alice,不说了。

When that happens, you have to deregister the OP_READ key - you know alice has sent you some data that is ready to read, but you don't want to read it, your buffers are full.发生这种情况时,您必须注销OP_READ密钥——您知道 alice 已向您发送了一些可以读取的数据,但您不想读取它,因为您的缓冲区已满。 This is sensible: If Bob has the slower connection and alice is sending heaps of data to bob, you can't process alice's bytes as fast as she can send them.这是明智的:如果 Bob 的连接速度较慢,而 alice 正在向 bob 发送大量数据,那么您无法像 alice 发送的那样快地处理 alice 的字节。

Remember also that .select() is free to return spuriously (for no reason).还请记住.select()可以自由地虚假返回(无缘无故)。 Your code cannot assume 'oh hey select() returned therefore there MUST be at least one thing ready to do here'.您的代码不能假定“哦嘿select()已返回,因此这里必须至少准备好一件事”。 Maybe not.也许不会。 Why does it 'quickly return twice'?为什么它会“快速返回两次”? Cuz.因为。 the JVM is allowed to.允许 JVM。

Low-level async NIO like this tends to cause spinning fans.像这样的低级异步 NIO 往往会导致旋转风扇。 This is the logic:这是逻辑:

  1. Your code loop works very simply: You while (true) {} your way through: Do whatever I can do.您的代码循环的工作方式非常简单:您while (true) {}通过您的方式:尽我所能。 Write whatever I can write, read whatever I can read, and then loop to do it over again forever and ever.写我能写的,读我能读的,然后循环往复,直到永远。
  2. The x.select() call is the only thing stopping a runaway while loop. x.select()调用是唯一阻止失控的 while 循环的方法。 That is the only place you ever 'sleep'.那是你唯一“睡觉”的地方。 It's async: Thread sleeping is the death of your app (in an async model nothing is ever allowed to sleep except when selecting).它是异步的:线程休眠是您应用程序的死亡(在异步 model 中,除了选择时,任何东西都不允许休眠)。
  3. If the selector is misconfigured, for example you have OP_WRITE registered and nothing to write, the selector will always instantly return.如果选择器配置错误,例如您注册了OP_WRITE并且没有任何内容可写,选择器将始终立即返回。
  4. Thus, your code is runaway: It loops forever never sleeping, causing 100% CPU usage, fans turn on, laptops drain their battery in minutes, power is wasted, things get hot, IAAS costs go through the roof.因此,您的代码失控了:它永远循环不休眠,导致 CPU 使用率 100%,风扇打开,笔记本电脑在几分钟内耗尽电池,电力被浪费,东西变热,IAAS 的成本高达 go。

async NIO is rocket science;异步 NIO 是火箭科学; it's really really hard to do it correctly.真的很难正确地做到这一点。 Usually you want to use frameworks that make things easier like grizzly or.netty.通常你想使用让事情变得更容易的框架,比如 grizzly 或 .netty。

Likely: Focusing on the wrong thing可能:关注错误的事情

Writing low-level async code is like writing an app in low-level machine code.编写低级异步代码就像用低级机器代码编写应用程序一样。 People tend to do it because 'they think it will be faster' but the only thing they accomplish is that they make a task that took an hour to program, and made it a thing that takes a week, the end result is a hard to test, buggy mess, and is actually slower because you don't know what you are doing and you underestimate how well all the middle layers (the compiler, the runtime, the OS, and so on) optimize.人们倾向于这样做是因为“他们认为它会更快”,但他们唯一完成的是他们完成了一项需要一个小时才能完成的任务,并把它变成了一个需要一周时间的事情,最终结果是很难测试,bug 乱七八糟,实际上速度更慢,因为你不知道自己在做什么,而且你低估了所有中间层(编译器、运行时、操作系统等)的优化程度。

There are reasons to want to go that low level (when you're writing a kernel driver, for example), but usually you don't.有理由想要go那个低级别(例如,当您编写 kernel 驱动程序时),但通常您不会这样做。

Same here.同样在这里。 Why are you using NIO here, exactly?你为什么在这里使用 NIO? It's not faster except in quite exotic circumstances, and it's definitely a lot harder to code for it.除非在非常奇特的情况下,否则它不会更快,而且为它编写代码肯定要困难得多。 Java suffers (like most languages) from 'the red/blue' problem quite severely. Java(像大多数语言一样)非常严重地遭受“红/蓝”问题

OSes have absolutely no issue handling 5000 threads and can do it quite efficiently.操作系统处理 5000 个线程绝对没有问题,并且可以非常高效地完成。 Yes, 'oh no the context switch', but note that context switching is intrinsic to handling many connections simultaneously: Cache misses are going to be frequent and async doesn't solve that problem.是的,“哦,不,上下文切换”,但请注意,上下文切换是同时处理多个连接所固有的:缓存未命中会很频繁,而异步并不能解决该问题。 The blog posts writing about how async is cool because it avoids 'context switches' all seem to forget that a cache miss due to having to hop to the buffers of another connection are just as much a 'context switch' as a thread hop.写异步有多酷因为它避免了“上下文切换”的博客文章似乎都忘记了由于必须跳到另一个连接的缓冲区而导致的缓存未命中与线程跳一样是“上下文切换”。

The one thing that you need to take care of when writing this code in a threaded fashion which is way, way simpler to write and maintain and test, is that you want to manage the stack sizes of your thread: You both want your threads to use limited stack size (if an exception occurs and the stack trace is a screen ful, that's a problem), and you want to set them up with limited sizes.在以线程方式编写此代码时需要注意的一件事是,编写、维护和测试更简单,您想要管理线程的堆栈大小:你们都希望线程使用有限的堆栈大小(如果发生异常并且堆栈跟踪是一个屏幕,那就是一个问题),并且您希望将它们设置为有限的大小。 You can specify stack sizes when creating threads (the thread constructor allows it, and various things that make threads such as a threaded ExecutorPool let you specify either the stack size, or a closure that makes threads).您可以在创建线程时指定堆栈大小(线程构造函数允许它,并且创建线程的各种事物(例如线程化的ExecutorPool允许您指定堆栈大小或创建线程的闭包)。 Use that and you can just write code that swiftly processes 5000 simultaneous connections using 10,000 threads, and it's all much, much simpler to write than async.使用它,您只需编写代码即可使用 10,000 个线程快速处理 5000 个并发连接,而且编写起来比异步要简单得多。 If you must go with async, use a framework to avoid the complications.如果您必须使用异步 go,请使用框架来避免并发症。

To go back to that alice sends faster than bob can receive model, note how much easier it is:将 go 返回到 alice 发送的速度比 bob 接收 model 的速度快,请注意它要容易得多:

  1. You ask alice's InputStream to fill some byte array.您要求 alice 的InputStream填充一些字节数组。
  2. You then ask bob's OutputStream to send the bytes in that array from 0 to however many you read.然后,您要求 bob 的OutputStream将该数组中的字节从 0 发送到您读取的任意数量。
  3. Go back to 1. Go 回到 1。

That's it.就是这样。 With the async stuff, either alice outpaces bob (in which case you better be turning off OP_READ on alice's connection to gate her input), or, bob outpaces alice (in which case you need to be turning off and on bob's OP_WRITE ), or even that due to vagaries in.network speeds, sometimes alice outpaces bob, and sometimes bob outpaces alice.对于异步的东西,要么 alice 超过 bob(在这种情况下你最好在 alice 的连接上关闭OP_READ以控制她的输入),要么,bob 超过 alice(在这种情况下你需要关闭和打开 bob 的OP_WRITE ),或者即使由于变幻莫测的网络速度,有时爱丽丝超过鲍勃,有时鲍勃超过爱丽丝。

With sync, as above, none of that matters - alice's read() call or bob's write() call, as needed, blocks, and that fixes all.如上所述,有了同步,这些都不重要了——alice 的read()调用或 bob 的write()调用,根据需要,阻塞,然后修复所有问题。 See how much simpler that is?看看这有多简单?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM