Java网络Bio相关知识点

258 阅读4分钟

相关概念

有关网络io,java提供了阻塞的bio(block io)及非阻塞io(nonblock io)。
阻塞的意思是在做一些io的相关操作时,某些情况下会使当前线程卡住不动,不执行任何代码。

阻塞io

那么为什么bio会阻塞呢或者说哪些地方会阻塞?

io操作是操作系统提供的功能,应用层通过syscall调用操作系统内核,内核完成相关操作。而且由于网络io是从网卡读入数据,当网络中还没有数据传输过来时,操作系统会等待,直到读取到数据了才会返回,所以会发生阻塞。
java中ServerSocket的accept()和Socket中InputStream的read方法都会产生阻塞。

读取数据

怎么处理在读取数据时可能产生的阻塞问题?

在实际使用时,我们不能用read方法返回EOF来判断数据是否读取完成。
这是因为只有当客户端主动关闭连接,服务端才会收到EOF,而当客户端不关闭连接,也不发送数据时,服务端的read()方法会一直阻塞。
要想避免以上问题,有两个简单的方法。

  • 使用nio。
  • 使用短链接或定义协议格式。 本文仅讨论bio,所以,这里只讨论第二种方法。

定义协议格式的意思是客户端和服务端按照规定的格式去发送/读取数据,这样,服务端就知道如何去读取,读取多少数据,而不必等待,也就不会产生阻塞。

以HTTP为例,HTTP协议规定了格式,比如GET请求不含body数据,并且以两个连续的回车换行符结束,这样服务端在读取到两个连续的回车换行符时,就不再读取数据。POST方法由于包含body数据,而且body没有结束标志,但是POST方法提供了Content-Length请求头,该请求头记录了body数据的长度,所以服务端在解析完请求头拿到body数据长度,再以该长度读取数据就不会产生阻塞。

bio为什么不如nio高效?

bio由于阻塞,所以需要一个线程处理一个连接,举个例子:
线程1处理连接1,当线程1等待连接1发送数据的时候,该线程会阻塞,如果这时候连接2数据过来了,我们就没办法读取,所以得使用线程2去读取连接2的数据,这样,就得使用一个连接一个线程这种模式去处理,多个线程会耗费服务端很多资源。
而nio由于不会阻塞且使用基于事件的方式,服务端可以用1个线程去处理连接,如果有网络读写时间了,那么该线程就会把这个连接取出来,交给业务线程去处理,这样,nio是使用一个线程管理所有连接这种模式去处理,只会占用很少的资源。

实现

如何实现一个http bio框架,要注意哪些地方?

使用bio实现http服务时,除了正常的解析协议外,还需要处理请求头Connection: keep-alive
该请求头的目的是为了复用连接,由于tcp在建立连接需要三次握手以及断开连接需要四次挥手,所以使用短连接频繁断开会对性能有一定影响。

所以在处理http连接时,需要记录跟踪http连接的状态。一般情况下,我们使用一个线程接收新连接,使用一个线程池处理新建立的连接,如果处理完业务连接已经关闭,则不需要做处理,如果没有关闭并且包含Connection: keep-alive请求头,则需要把该socket(连接)重新添加到线程池中等待处理。

同时要考虑到空闲连接太多对系统的影响,我们还需要记录socket上次处理完的时间,使用一个线程去监测线程池中空闲时间过长的连接,定期去清理这些连接,保证系统能够正常处理。

结语

本文讨论了使用bio在读取网络数据时需要注意的地方,以及实现http连接处理时需要注意的地方。实现时,要考虑各种问题及各种复杂的场景,对如何实现同时保证性能将会是一个非常大的挑战。