实习记录-java实现http服务器

  • 使用多线程对到来的请求进行处理

https://github.com/41503257/HttpServerDemo

  • 使用nio非阻塞对到来的请求进行处理

https://github.com/41503257/NIOHttpServerDemo

nio优于多线程的原因

BIO处理客户端请求时,调用socket的字节流的write和read等方法时,会进行阻塞,因此当多个请求来到时会处理非常慢,而多线程使其获得了对多个请求处理的能力,但当大量请求来临时,线程创建过多,上下文切换所带来的开销就会给我们的系统带来巨大的资源消耗。同时在任何时候都有可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这对于我们的系统来说就是一种巨大的资源浪费

而NIO的使用则是将serversockchannel注册到selector选择器上,在调用select方法时,若无请求则进行阻塞,有请求则对请求轮询处理,分别对其进行读写事件的处理。**select会将请求从用户态空间全量复制一份到内核态空间,在内核空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层轮询要高的。**而与BIO最大的不同是,其在read和write方法上直接返回不会进行阻塞。

一些代码细节问题

  • 待处理(?)

(个人理解1)select获取的selectkey应该是指示每个响应的每个注册状态(即每个响应在本项目中有三个key且逐步出现)。本次项目中实现的流程是:当客户端的connect完成后,服务端会准备accpet,得到key为accpet的key,在这个key的处理中进行注册read事件,key为read的处理中read操作后对其注册key为write的key,在key处理中完成write事件,完成write事件后注销write事件。

(个人理解2)select获取的selectkey有每个响应的每个注册状态。本次项目中实现的流程是:当客户端的connect完成后,服务端会准备accpet,有可处理的accpet会进入方法:在这个key的处理中进行注册read事件,有可处理的read事件中read操作后对其注册key为write的key,可处理的write事件,完成write事件后注销write事件。

serversock(port,backlog)的backlog有什么作用

全部参数的构造方法ServerSocket(int port, int backlog, InetAddress bindAddr);其中backlog默认50,bindAddr默认为null

其中执行bind(new InetSocketAddress(bindAddr, port), backlog);此时bindAddr会实现InetAddress.anyLocalAddress()–得到一个本地地址+端口

  1. getImpl().bind(epoint.getAddress(), epoint.getPort());
  2. getImpl().listen(backlog);

以上述两方法中getImpl返回一个SocketImpl绑定监听ip及端口,同时设置监听队列长度。

表示连接请求队列的最大长度,如果不断有新的请求进来,它们会按照先后顺序依次排队,直到这个队列满了,可以设置为SOMAXCONN,由系统来决定请求队列长度。

在通俗的说,假如服务端的队列此时大小是10,如果有10个人向服务端发起请求,而服务端暂时都没有调用accept。这时候在有其他的客户请求则会抛出异常,直到服务端调用accept从这个列队中取出一个,给后续腾出空间。

关于selector的wakeup方法

如果另一个线程当前在调用select()或select(long)方法时被阻塞,则该调用将立即返回。如果当前没有进行选择操作,则下一次调用其中一个方法将立即返回,除非同时调用selectNow()方法。在任何情况下,该调用返回的值都可能是非零的。select()或select(long)方法的后续调用将像往常一样阻塞,除非在此期间再次调用此方法。在两个连续的选择操作之间多次调用此方法与仅调用一次具有相同的效果。

具体原因为:window下selector.open方法调用时会建立自己与自己loopback的tcp链接,linux下会则会创建pipe,这是为了wakeup方法唤醒select的阻塞:实际操作为写入1字节,则唤醒阻塞。

关于write事件的注销

对于socket底层而言,其拥有buffer缓冲区,两个指针,一个指向较上部分,主要对read进行反馈,当buffer中数据到达read时触发readable,这也是tcp粘包问题的来源,即数据到达一定数量才会发出去。一个指向下部分,当buffer中数据没有多少时,会触发writeable,使其写入数据。因此,若不对写事件进行注销,key在select时会一直出发writable使其不断进入流程而不进行阻塞,使得资源浪费。