范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Netty源码解析系列(一)NIO原理详解与源码分析

  1 Java NIO三件套
  在NIO中有三个核心对象需要掌握:缓冲区(Buffer)、选择器(Selector)和通道(Channel)。1.1 缓冲区
  1.Buffer操作基本API
  缓冲区实际上是一个容器对象,更直接地说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,它也是写入缓冲区的;任何时候访问NIO中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
  在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示。
  下面是一个简单的使用IntBuffer的例子。
  运行后可以看到如下图所示的结果。
  2.Buffer的基本原理
  在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如果我们使用get()方法从缓冲区获取数据或者使用put()方法把数据写入缓冲区,都会引起缓冲区状态的变化。
  在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪。
  ● position:指定下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。
  ● limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
  ● capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。
  以上三个属性值之间有一些相对大小的关系:0<=position<=limit<=capacity。如果我们创建一个新的容量大小为10的ByteBuffer对象,在初始化的时候,position设置为0,limit和capacity设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其他两个将会随着使用而变化。
  准备一个txt文档,存放在E盘,输入以下内容。
  我们用一段代码来验证position、limit和capacity这三个值的变化过程,代码如下。
  完成后的输出结果如下图所示。
  我们已经看到运行结果,下面对以上结果进行图解,三个属性值分别如下图所示。
  我们可以从通道中读取一些数据到缓冲区中,注意从通道读取数据,相当于往缓冲区写入数据。如果读取4个字己的数据,则此时position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示。
  下一步把读取的数据写入输出通道,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法。该方法将会完成以下两件事情:一是把limit设置为当前的position值。二是把position设置为0。
  由于position被设置为0,所以可以保证在下一步输出时读取的是缓冲区的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入缓冲区的数据,如下图所示。
  现在调用get()方法从缓冲区中读取数据写入输出通道,这会导致position的增加而limit保持不变,但position不会超过limit的值,所以在读取之前写入缓冲区的4字节之后,position和limit的值都为4,如下图所示。
  在从缓冲区中读取数据完毕后,limit的值仍然保持在调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值,如下图所示。
  3.缓冲区的分配
  在前面的几个例子中,我们已经看到,在创建一个缓冲区对象时,会调用静态方法allocate()来指定缓冲区的容量,其实调用allocate()方法相当于创建了一个指定大小的数组,并把它包装为缓冲区对象。或者我们也可以直接将一个现有的数组包装为缓冲区对象,示例代码如下。
  4.缓冲区分片
  在NIO中,除了可以分配或者包装一个缓冲区对象,还可以根据现有的缓冲区对象创建一个子缓冲区,即在现有缓冲区上切出一片作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于现有缓冲区的一个视图窗口。调用slice()方法可以创建一个子缓冲区,下面我们通过例子来看一下。
  在该示例中,分配了一个容量大小为10的缓冲区,并在其中放入了数据0~9,而在该缓冲区基础上又创建了一个子缓冲区,并改变子缓冲区中的内容,从最后输出的结果来看,只有子缓冲区"可见的"那部分数据发生了变化,并且说明子缓冲区与原缓冲区是数据共享的,输出结果如下图所示。
  5.只读缓冲区
  只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的asReadOnlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。具体代码如下。
  如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读缓冲区转换为可写的缓冲区。
  6.直接缓冲区
  直接缓冲区是为加快I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区或者从一个中间缓冲区拷贝数据。要分配直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方法,使用方式与普通缓冲区并无区别,如下面的文件所示。
  7.内存映射
  内存映射是一种读和写文件数据的方法,可以比常规的基于流或者基于通道的I/O快得多。内存映射文件I/O通过使文件中的数据表现为内存数组的内容来完成,这初听起来似乎不过就是将整个文件读到内存中,但事实上并不是这样的。一般来说,只有文件中实际读取或写入的部分才会映射到内存中。来看下面的示例代码。
  1.2 选择器
  传统的Client/Server模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池中线程的最大数量,这又带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。传统的Client/Server模式如下图所示。
  NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,而是注册感兴趣的特定I/O事件,如可读数据到达、新的套接字连接等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象是Selector,Selector是注册各种I/O事件的地方,而且当那些事件发生时,就是Seleetor告诉我们所发生的事件,如下图所示。
  从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
  使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤。
  (1)向Selector对象注册感兴趣的事件。
  (2)从Selector中获取感兴趣的事件。
  (3)根据不同的事件进行相应的处理。
  下面我们用一个简单的示例来说明整个过程。首先是向Selector对象注册感兴趣的事件。
  上述代码中先创建了ServerSocketChannel对象,并调用configureBlocking()方法,配置为非阻塞模式。接下来的三行代码把该通道绑定到指定端口,最后向Selector注册事件。此处指定的参数是OP_ACCEPT,即指定想要监听accept事件,也就是新的连接发生时所产生的事件。对于ServerSocketChannel通道来说,我们唯一可以指定的参数就是OP_ACCEPT。从Selector中获取感兴趣的事件,即开始监听,进入内部循环。
  在非阻塞I/O中,内部循环模式基本都遵循这种方式。首先调用select()方法,该方法会阻塞,直到至少有一个事件发生,然后使用selectedKeys()方法获取发生事件的SelectionKey,再使用迭代器进行循环。
  最后一步就是根据不同的事件,编写相应的处理代码。
  此处判断是接受请求、读数据还是写事件,分别做不同的处理。在Java 1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢;而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,每一个操作在一步中都产生或者消费一个数据库,按块处理数据要比按字节处理数据快得多。1.3 通道
  通道是一个对象,通过它可以读取和写入数据,当然所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道,而是将数据写入包含一个或者多个字节的缓冲区。同样也不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
  NIO提供了多种通道对象,所有的通道对象都实现了Channel接口。它们之间的继承关系如下图所示。
  1.使用NIO读取数据
  前面我们说过,任何时候读取数据,都不是直接从通道读取的,而是从通道读取到缓冲区的。所以使用NIO读取数据可以分为下面三个步骤。
  (1)从FileInputStream获取Channel。
  (2)创建Buffer。
  (3)将数据从Channel读取到Buffer中。
  下面是一个简单的使用NIO从文件中读取数据的例子。
  2.使用NIO写入数据
  使用NIO写入数据与读取数据的过程类似,数据同样不是直接写入通道的,而是写入缓冲区,可以分为下面三个步骤。
  (1)从FileInputStream获取Channel。
  (2)创建Buffer。
  (3)将数据从Channel写入Buffer。
  下面是一个简单的使用NIO向文件中写入数据的例子。
  3.多路复用I/O
  我们试想一下这样的现实场景。
  一个餐厅同时有100位客人到店,到店后要做的第一件事情就是点菜。但是问题来了,餐厅老板为了节约人力成本,目前只有一名大堂服务员拿着唯一的一本菜单给客人提供服务。
  那么最笨(但是最简单)的方法(方法A)是,无论有多少客人等待点餐,服务员都把仅有的一份菜单递给其中一位客人,然后站在客人身旁等待这位客人完成点菜过程。在记录客人的点菜内容后,把点菜记录交给后堂厨师。然后是第二位客人、第三位客人……很明显,没有老板会这样设置服务流程。因为随后的80位客人,在等待超时后就会离店(还会给差评)。
  于是还有一种办法(方法B),老板马上新雇佣99名服务员,同时印制99本新的菜单。每一名服务员手持一本菜单负责一位客人(关键不只在于服务员,还在于菜单。因为没有菜单,客人也无法点菜)。在客人点完菜后,服务员记录点菜内容交给后堂厨师(当然为了更高效,后堂厨师最好也有100名),如下图所示。这样每一位客人享受的都是VIP服务,当然客人也不会走,但是人力成本却很高(必亏无疑)。
  另外一种办法(方法C),就是改进点菜的方式,当客人到店后,自己申请一本菜单。想好自己要点的菜后,就呼叫服务员。服务员站在自己身边记录客人的菜单内容。将菜单递给厨师的过程也要进行改进,并不是每一份菜单记录好以后,都要交给后堂厨师。服务员可以记录好多份菜单后,同时交给厨师就行了,如下图所示。那么这种方式,对于老板来说人力成本是最低的;对于客人来说,虽然不再享受VIP服务,并且要进行一段时间的等待,但是这些都是可以接受的;对于服务员来说,基本上她的时间都没有浪费,最大程度地提高了时间利用率。
  如果你是老板,你会采用哪种方式呢?
  到店情况:并发量。到店情况不理想时,一个服务员一本菜单,当然是足够的。所以不同的老板在不同的场合下,将会灵活选择服务员和菜单的配置。
  客人:客户端请求。
  点餐内容:客户端发送的实际数据。
  老板:操作系统。
  人力成本:系统资源。
  菜单:文件描述符(FD)。操作系统对于一个进程能够同时持有的文件描述符的个数是有限制的,在Linux系统中用$ulimit-n查看这个限制值,当然也是可以(并且应该)进行内核参数调整的。
  服务员:操作系统内核用于I/O操作的线程(内核线程)。
  厨师:应用程序线程(当然厨房就是应用程序进程)。
  方法A:同步I/O。
  方法B:同步I/O。
  方法C:多路复用I/O。
  目前流行的多路复用I/O的实现主要包括四种:select、poll、epoll、kqueue。如下表所示是它们的一些重要特性的比较。
  多路复用I/O技术最适用的是"高并发"场景,所谓"高并发"是指1ms内至少同时有上千个连接请求准备好。其他情况下多路复用I/O技术发挥不出它的优势。另外,使用Java NIO进行功能实现,相对于传统的套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。3.2 NIO源码初探
  说到源码,先得从Selector的open方法开始看,我们看java.nio.channels.Selector类的源码。
  看SelectorProvider.provider()的具体代码。
  其中provider=sun.nio.ch.DefaultSelectorProvider.create()会根据操作系统来返回不同的实现类,Windows平台返回WindowsSelectorProvider;而if(provider!=null)return provider保证了整个Server程序中只有一个WindowsSelectorProvider对象;看WindowsSelectorProvider.openSelector()代码。
  new WindowsSelectorImpl(SelectorProvider)的代码如下。
  其中Pipe.open()是关键,这个方法的调用过程如下。
  在SelectorProvider中,代码如下。
  再看一下PipeImpl()的代码。
  其中IOUtil.makePipe(true)是一个本地方法。
  具体实现代码如下。
  正如下面这段注释所描述的内容。
  高位存放的是通道read端的文件描述符,低32位存放的是write端的文件描述符。所以取得makepipe()返回值后要做移位处理。
  这行代码把返回的pipe的write端的FD放在pollWrapper中(后面会发现这么做是为了实现Selector的wakeup())。
  ServerSocketChannel.open()的实现代码如下。
  SelectorProvider的实现代码如下。
  可见ServerSocketChannelImpl也有WindowsSelectorImpl的引用。
  然后通过serverChannel1.register(selector,SelectionKey.OP_ACCEPT);把Selector和Channel绑定在一起,也就是把新建ServerSocketChannel时创建的FD与Selector绑定在一起。
  到此,Server端已启动完成,主要创建了以下对象。
  (1)WindowsSelectorProvider:为单例对象,实际上是调用操作系统的API…
  (2)WindowsSelectorImpl中包含如下内容。
  ● pollWrapper:保存Selector上注册的FD,包括pipe的write端FD和ServerSocketChannel所用的FD。
  ● wakeupPipe:通道(其实就是两个FD,一个是read端的,一个是write端的)。
  下面来看Selector中的select()方法,selector.select()主要调用了WindowsSelectorImpl中的doSelect()方法。
  其中subSelector.poll()是核心,也就是轮询pollWrapper中保存的FD;具体实现是调用native方法poll0()。
  poll0.()会监听pollWrapper中的FD有没有数据进出,这会造成I/O阻塞,直到有数据读写事件发生。比如,由于pollWrapper中保存的也有ServerSocketChannel的FD,所以只要ClientSocket发一份数据到ServerSocket,那么poll0()就会返回;又由于pollWrapper中保存的也有pipe的write端的FD,所以只要pipe的write端向FD发一份数据,也会造成poll0()返回;如果这两种情况都没有发生,那么poll0()就会一直阻塞,也就是selector.select()会一直阻塞;如果有任何一种情况发生,那么selector.select()就会返回,所以在OperationServer的run()里要用while(true),这样可以保证在Selector接收数据并处理完后继续监听poll()。
  再来看WindowsSelectorImpl.Wakeup()。
  可见wakeup()是通过pipe的write端send(scoutFd,&byte,1,0)发送一个字节1来唤醒poll()的,所以在需要的时候就可以调用selector.wakeup()来唤醒Selector。3.3 反应堆
  现在我们已经对阻塞I/O有了一定了解,知道阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来(或超时)时才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接成功后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通信模型示意如下图所示。
  如果仔细分析,一定会发现阻塞I/O存在一些缺点。根据阻塞I/O通信模型,总结了它的两个缺点。
  (1)当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间。
  (2)阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞I/O就有了它的应用前景。
  Java NIO是从JDK 1.4开始使用的,它既可以说成是"新I/O",也可以说成是"非阻塞I/O"。下面是Java NIO的工作原理。
  (1)由一个专门的线程来处理所有的I/O事件,并负责分发。
  (2)事件驱动机制:事件到的时候触发,而不是同步地去监视事件。
  (3)线程通信:线程之间通过wait、notify等方式通信。保证每次上下文切换都是有意义的,减少无谓的线程切换。
  下面是笔者理解的Java NIO反应堆的工作原理图。
  (注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码和发送响应。)3.4 Netty与NIO3.4.1 Netty支持的功能与特性
  按照定义来说,Netty是一个异步的、事件驱动的、用来做高性能高可靠性的网络应用的框架。下面是其主要的优点。
  (1)框架设计优雅,底层模型随意切换,适应不同的网络协议要求。
  (2)提供了很多标准的协议、安全、编解码的支持。
  (3)解决了很多NIO不易用的问题。
  (4)社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark等。
  Netty支持的功能与特性如下图所示。
  (1)底层核心有:Zero-Copy-Capable Buffer,非常易用的零拷贝Buffer(这个内容很有意思,稍后专门来讲);统一的API;标准可扩展的事件模型。
  (2)传输方面的支持有:管道通信;HTTP隧道;TCP与UDP。
  (3)协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;ProtoBuf编解码;安全认证;HTTP和WebSocket。3.4.2 Netty采用NIO而非AIO的理由
  (1)Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用epoll,没有很好地实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层,不容易深度优化。
  (2)Netty整体架构采用Reactor模型,而AIO采用Proactor模型,混在一起会非常混乱,把AIO也改造成Reactor模型,看起来是把Epoll绕个弯又绕回来。
  (3)AIO还有个缺点是接收数据需要预先分配缓存,而NIO是需要接收时才分配缓存,所以对连接数量非常大但流量小的情况,AIO浪费很多内存。
  (4)Linux上AIO不够成熟,处理回调结果的速度跟不上处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈。

笑也生非喝酒,喝出一部武打片的片段在一家餐馆里上演朋友们聚在一起,到餐馆里喝顿小酒,本是一件快乐的事情。可快乐的事情也有失控的时候。我的一个朋友是一个非常仗义的人。他在做着买卖。他只要挣到钱,就会把他的朋友们聚来,在餐馆里好好地喝真正的友谊是不喧闹的微风爱与孤独周国平先生在他创作的爱与孤独中有这样一段表述一个人又不可能有许多朋友。所谓朋友遍天下,不是一种诗意的夸张,便是一种浅薄的自负。热衷于社交的人往往自诩朋友众多。其实他们心68人遇难,乘客名单公布综合外媒报道,当地时间1月15日,尼泊尔雪人航空公司一架载有72人从加德满都飞往博克拉的客机坠毁。目前部分遗体已找到,搜救正在进行中。空难已致68人遇难乘客名单中没有中国公民记者从向东看,全球投资者热情高涨图为1月3日,在江苏省扬州港的集装箱码头,货轮停在泊位上装卸集装箱。孟德龙摄(人民视觉)北京街头再次变得拥堵,游客们争先恐后预订出国度假旅游,企业预计经济活动将呈现回升趋势最近,出日本从1879年侵占琉球,长达144年,现如今琉球人生活如何?提到琉球或许很多中国人已经没有了印象,琉球群岛从1879年被日本侵占至今,已经有144年。琉球群岛其实是台湾岛和日本九州岛之间一片广阔的群岛,这片区域的地理位置十分优越,自古以来,万恶的旧社会是什么模样?看看晚清时期的童养媳,处境悲惨在封建社会时代,人们的婚姻大多讲究父母之命,媒妁之言,做子女的并没有选择配偶的权利。更有甚者,有一些家庭为了尽早传宗接代,会将年幼的女孩接回家中养着,等到了合适的年龄,就把她们嫁给李峰注册制全面施行在即,从创业板经验看主板变化趋势2月1日中国证监会就全面实行股票发行注册制主要制度规则向全社会公开征求意见,这意味着主板的核准制发行即将成为历史,我国股票市场将迎来全面注册制改革。本次改革主要针对沪深两市主板市场8大血泪经验丨孕妈进产房前一定要知道新手孕妈肯定会查阅很多资料,但是只有自己生产过才会知道,原来还是有这么多坑要踩,今天婴妈就跟大家分享产房的8大血泪经验。还没生产过的一胎妈妈,一定要看完这篇文章。一早点申请无痛分娩想做抽脂手术?抽脂的这5个并发症,早早了解为好说到吸脂手术,大家并不陌生,这是医美行业里非常受欢迎的一种塑形项目。会通过特殊的器械利用负压的方式,在人某个部位里吸取多余的脂肪。随后起到瘦身的效果。脂肪是人肥胖的主要原因。吸脂手我拥有了占据星辰的能力,却失去了与星辰对话的资格头条创作挑战赛作者弈天绝命(陶兴旺)出品单位星空文学社我拥有星星,是因为在我之前没人想到要拥有它们。名著小王子中的这段话曾让我泪目,至今都无法释怀。人世间最无奈的事情莫过于在最无能人过五十,该装就装,不装不行郝有花(图片来自网络)人过五十,生活就进入倒计时身体某些机能开始退化由最初疯狂地折腾慢慢变得沉稳在风风雨雨的行程里好的,坏的,黑的,白的一眼望去,明明白白这个时候,见人能装就装不装
零食垃圾食品!但这些零食你应该拉进黑名单中国天气网广西站讯每当提起零食,大家总会把它和肥胖没营养等词汇挂钩,于是在吃与不吃之间来回纠结其实零食是可以吃的,毕竟会给很多人带来幸福感,而且我国一项最新研究发现吃对了零食反而有25号宇宙实验推演人类未来,准确得令人心惊,人类现在在哪里?大家好,我是文哥。欢迎和我一起探索未解之谜今天给大家介绍一个60年代的神奇实验。神奇在哪里呢?因为实验对象虽然只是一群老鼠,实验结果却准确预言了之后几十年人类的现状。25号宇宙这就NASA又发现两颗超级地球!我们可以很快就要搬家了?近日NASA宣布,TESS任务发现了两个距离我们仅有100光年的超级地球,那他们的这个母星,是一颗红矮星HD260655,这是迄今为止发现离我们最近的系外行星系统之一。那超级地球这中国组建危害地球小行星的防御系统,保护人类家园头号周刊在近日的中国航天日上,中国航天局副局长吴艳华透露,中国将组建小行星防御系统,其目的是防止地球周边太空中的小行星撞击地球,对人类造成灾难,为人类撑起一把保护伞。预计在十四五末空间站内,为何中国航天员穿鞋,而美国宇航员只穿袜子从下图可看到,国际空间站宇航员在舱内活动时,脚上只穿袜子,而中国空间站的航天员却有穿鞋。先科普一下,在太空近地轨道微重力环境下,因为脚底细胞没有受到像在地面行走一样的压力,摩擦力,古代龙是地球上最早的滑翔爬行动物当大多数古代爬行动物在史前栖息地爬行凿沉和跳跃时,一种隐形生物选择了一种空中技术滑翔。微小的龙状Coelurosauravuselivensis(seeluhrohSAWrahvuNASA发现两颗超级地球中国日报9月9日电(记者张宇浩)当地时间7日,美国国家航空航天局的苔丝(TESS)任务宣布发现两颗系外行星,距离地球约100光年,其中一颗或适合生命存活。这两颗系外行星被归类为超级有史以来第一次?重大突破,太阳中日冕被科学家们发现科学家们有史以来第一次观察到太阳的中日冕,这一发现可能会使天气预报更加清晰准确。与我们息息相关!太阳中日冕首次被观察到,或将大幅提升天气预报准确率!环境科学合作研究所(CIRES)6000万年的气候变化推动了爬行动物的进化和多样性在2。5亿多年前的二叠纪末期和三叠纪初期,爬行动物有了一个惊人的出场机会。它们的进化速度和多样性开始爆发,导致了各种令人眼花缭乱的能力身体计划和特征。这有助于牢固确立它们已灭绝的血骆玉仓秦州珍稀水生野生动物大鲵栖息地的守护者新天水记者余碧波陈少娟仲秋时节,走进秦州珍稀水生野生动物国家级自然保护区,河水清澈见底,在阳光的照耀下闪着点点星光,草木郁郁葱葱,山间飞鸟鸣唤,让人身心舒畅,心旷神怡。当记者行至娘是貉!第一次发现,还是国家二级保护动物!长江新区江滩惊现貉长江网讯(记者刘晓双)长江新区江滩湿地惊现金狐狸!9月8日上午,武汉摄影爱好者邹幼勤在长江头条平台发了一条朋友圈,附上张他拍下的金狐狸照片,并发起武汉好生态话题,引起网友纷纷留言,