上一篇文章中,我们分析了通常我们使用的面向字节流的BIO所存在的阻塞问题,那么这篇博客我们来看看java为我们提供的解决BIO的方案,就是NIO了;
在上篇文章的最后,我们提到了,使用了多线程只是能够实现对"业务逻辑处理"的多线程,但是对于数据报文的接收还是需要一个一个来的,也就是我们上面见到的accept以及read方法阻塞问题,多线程是根本解决不了的,那么首先我们来看看accept为什么会造成阻塞,accept方法的作用是询问操作系统是否有新的Socket套接字信息从端口X处发送过来,注意这里询问的是操作系统,也就是说Socket套接字IO模式的支持是基于操作系统的,如果操作系统没有发现有套接字从指定端口X连接进来,那么操作系统就会等待,这样accept方法就会阻塞,他的内部实现使用的是操作系统级别的同步IO,我们从代码层面上看看是怎么回事;
在调用了ServerSocket.accept方法之后,接着进入到accept方法里面;
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}
首先进行的是一些判断,接着创建了一个Socket对象,执行了implAccept方法,来看看implAccept方法:
protected final void implAccept(Socket s) throws IOException {
SocketImpl si = null;
try {
if (s.impl == null)
s.setImpl();
else {
s.impl.reset();
}
si = s.impl;
s.impl = null;
si.address = new InetAddress();
si.fd = new FileDescriptor();
getImpl().accept(si);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccept(si.getInetAddress().getHostAddress(),
si.getPort());
}
} catch (IOException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
} catch (SecurityException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
}
s.impl = si;
s.postAccept();
}
这个方法的关键在于第13行执行了getImpl()的accept方法,那么getImpl是什么呢?其实就是我们Socket的底层实现对应的实体类了,因为不同的操作系统内核是不同的,他们对于Socket的实现当然会各有千秋,我们可以看看Socket的实现到底有几种;
其实是有三种对吧,AbstractPlainSocketImpl只是他们三种公共实现的一个抽象类而已嘛!
我们这里以DualStackPlainSocketImpl为例进行介绍,上面执行了getImpl的accept方法之后,我们首先到DualStackPlainSocketImpl中查看是否存在accept方法,发现根本没有,那肯定就在他的父类AbstractPlainSocketImpl里面了,果不其然:
protected void accept(SocketImpl s) throws IOException {
acquireFD();
try {
socketAccept(s);
} finally {
releaseFD();
}
}
可以看到他调用了socketAccept方法,因为每个操作系统的Socket地城实现都不同嘛,所以这里就执行了我们
DualStackPlainSocketImpl里面的socketAccept方法啦!
void socketAccept(SocketImpl s) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null");
int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) {
newfd = accept0(nativefd, isaa);
} else {
configureBlocking(nativefd, false);
try {
waitForNewConnection(nativefd, timeout);
newfd = accept0(nativefd, isaa);
if (newfd != -1) {
configureBlocking(newfd, true);
}
} finally {
configureBlocking(nativefd, true);
}
}
/* Update (SocketImpl)s' fd */
fdAccess.set(s.fd, newfd);
/* Update socketImpls remote port, address and localport */
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
}
这里第9到22行是关键代码,第10行和第15行执行了accept0方法,这个是native方法,具体来说就是与操作系统交互来实现监听指定端口上是否有客户端接入,正是因为accept0在没有客户端接入的时候会一直处于阻塞状态,所以造成了我们程序级别的accept方法阻塞,当然对于程序界别的阻塞,我们是可以避免的,也就是我们可以将accept方法修改成非阻塞式,但是对于accept0造成的阻塞我们暂时是没法改变的,操作系统级别的阻塞其实就是我们通常所说的同步异步中的同步了;前面说到我们可以在程序级别改变accept的阻塞,具体怎么实现呢?其实就是通过我们上面socketAccept方法中判断timeout的值来实现的啦,在第9行判断timeout的值是否小于等于0,那么直接执行accept0方法,这时候将一直处于阻塞状态,但是如果我们设置了timeout的话,也就是timeout值大于0的话,则程序之后等到我们设置的时间就会返回,注意这里的newfd如果等于-1的话,表示这次accept没有发现有数据从底层返回;那么到底timeout的值是在哪设置的呢?我们可以通过ServerSocket的setSoTimeout方法进行设置,来看看这个方法:
public synchronized void setSoTimeout(int timeout) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_TIMEOUT, new Integer(timeout));
}
执行了getImpl的setOption方法,并且设置了timeout时间,我们这里仍然是以
DualStackPlainSocketImpl为例,查看它里面发现不存在setOption方法,那么需要到他的父类AbstractPlainSocketImpl中查看:
这个方法比较长,我们仅看与我们有关的代码:
case SO_TIMEOUT:
if (val == null || (!(val instanceof Integer)))
throw new SocketException("Bad parameter for SO_TIMEOUT");
int tmp = ((Integer) val).intValue();
if (tmp < 0)
throw new IllegalArgumentException("timeout < 0");
timeout = tmp;
break;
可以看到其实这里仅仅就是将我们
setOption里面传入的timeout值设置到了
AbstractPlainSocketImpl的全局变量timeout里面而已了;
这样的话,我们便可以在程序级别将accept方法设置成为非阻塞式的了,但是read方法现在还是阻塞式的,等会我们还需要改造read方法,将它在程序级别变成非阻塞式;
在正式改造前,我们有必要来解释下同步/异步和阻塞/非阻塞式了;
同步/异步是属于操作系统级别的,指的是操作系统在收到程序请求的IO之后,如果IO资源没有准备好的话,该如何响应程序的问题,同步的话就是不响应,知道IO资源准备好;而异步的话则会返回给程序一个标志,这个标志用于当IO资源准备好后通过事件机制发送的内容应该发到什么地方;
阻塞/非阻塞是属于程序级别的,指的是程序在请求操作系统进行IO操作时,如果IO资源没有准备好的话,程序该怎么处理的问题,阻塞的话就是程序什么都不做,一直等到IO资源准备好,非阻塞的话程序则继续运行,但是会时不时的去查看下IO到底准备好没有呢;
我们通常见到的BIO是同步阻塞式的,同步的话说明操作系统底层是一致等待IO资源准备好的,阻塞的话是程序本身也在一直等待IO资源准备好的,具体来讲程序级别的阻塞就是accept和read造成的,我们可以通过改造将其变成非阻塞式,但是操作系统层次的阻塞我们没法改变,只能使用AIO;
我们的NIO是同步非阻塞式的,其实他的非阻塞实现原理和我们上面的讲解差不多的,就是为了改善accept和read方法带来的阻塞现象,所以引入了通道和缓存的概念;
好了,我们对上一篇的博客实例进行改进,解决accept带来的阻塞问题:
注意我们只修改了服务端代码:
public class BIOServer {
public void initBIOServer(int port)
{
ServerSocket serverSocket = null;//服务端Socket
Socket socket = null;//客户端socket
ExecutorService threadPool = Executors.newCachedThreadPool();
ClientSocketThread thread = null;
try {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(1000);
System.out.println(stringNowTime() + ": serverSocket started");
while(true)
{
try {
socket = serverSocket.accept();
} catch (SocketTimeoutException e) {
//运行到这里表示本次accept是没有收到任何数据的,服务端的主线程在这里可以做一些其他事情
System.out.println("now time is: "+stringNowTime());
continue;
}
System.out.println(stringNowTime() + ": id为" + socket.hashCode()+ "的Clientsocket connected");
thread = new ClientSocketThread(socket);
threadPool.execute(thread);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String stringNowTime()
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return format.format(new Date());
}
class ClientSocketThread extends Thread
{
public Socket socket;
public ClientSocketThread(Socket socket)
{
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
String inputContent;
int count = 0;
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while ((inputContent = reader.readLine()) != null) {
System.out.println("收到id为" + socket.hashCode() + " "+inputContent);
count++;
}
System.out.println("id为" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"读取结束");
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BIOServer server = new BIOServer();
server.initBIOServer(9898);
}
}
主要就是加了第10行代码,为我们的ServerSocket设置了timeout时间,这样的话调用accept方法的时候每隔1s他就会被唤醒一次,而不再是一直在那里,只有有客户端接入才会返回信息;我们运行一下看看结果
可以看到,我们刚开始并没有客户端接入的时候,是会执行第18行的输出语句的,还有一点需要注意的就是,仔细看看上面的输出结果的红色框部分,你会发现下面的红色框部分时间值不是14:57:08:271,原因就在于如果accept正常返回值的话,他是不会执行catch语句部分的;
这样的话,我们就把accept部分改造成了非阻塞式了,那么还有read部分呢?可以改造么?当然可以了,改造方法和accept很类似啦,看下面:
注意也仅仅修改了服务端代码:
public class BIOServer {
public void initBIOServer(int port)
{
ServerSocket serverSocket = null;//服务端Socket
Socket socket = null;//客户端socket
ExecutorService threadPool = Executors.newCachedThreadPool();
ClientSocketThread thread = null;
try {
serverSocket = new ServerSocket(port);
serverSocket.setSoTimeout(1000);
System.out.println(stringNowTime() + ": serverSocket started");
while(true)
{
try {
socket = serverSocket.accept();
} catch (SocketTimeoutException e) {
//运行到这里表示本次accept是没有收到任何数据的,服务端的主线程在这里可以做一些其他事情
System.out.println("now time is: "+stringNowTime());
continue;
}
System.out.println(stringNowTime() + ": id为" + socket.hashCode()+ "的Clientsocket connected");
thread = new ClientSocketThread(socket);
threadPool.execute(thread);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String stringNowTime()
{
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return format.format(new Date());
}
class ClientSocketThread extends Thread
{
public Socket socket;
public ClientSocketThread(Socket socket)
{
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
String inputContent;
int count = 0;
try {
socket.setSoTimeout(1000);
} catch (SocketException e1) {
e1.printStackTrace();
}
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true)
{
try {
while ((inputContent = reader.readLine()) != null) {
System.out.println("收到id为" + socket.hashCode() + " "+inputContent);
count++;
}
} catch (Exception e) {
//执行到这里表示read方法没有获取到任何数据,线程可以执行一些其他的操作
System.out.println("Not read data: "+stringNowTime());
continue;
}
//执行到这里表示读取到了数据,我们可以在这里进行回复客户端的工作
System.out.println("id为" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"读取结束");
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
BIOServer server = new BIOServer();
server.initBIOServer(9898);
}
}
我们增加了第48行代码以及56行的try catch语句,随后运行程序查看输出:
其中的Not read data输出语句部分解决了我们的read阻塞问题,每隔1s会去唤醒我们的read操作,如果在1s内没有读到数据的话就会执行第63行的输出语句,在这里我们就可以进行一些其他操作了,避免了阻塞中当前线程的现象,当我们有数据发送之后,就有了第一个蓝色框的输出了,因为read得到输出了嘛,所以不再会执行catch语句部分了,因此你会发现第二个蓝色框的输出时间是和第一个蓝色框的时间相差1s而不是和之前的15:14:59:301相差一秒;
这样的话,我们就解决了accept以及read带来的阻塞问题了,同时在服务端为每一个客户端都创建了一个线程来处理各自的业务逻辑,这点其实基本上已经解决了阻塞问题了,我们可以理解成是最初版的NIO了,但是呢,为每个客户端都创建一个线程这点确实让人挺烦的,特别是客户端多了的话,很浪费服务器资源,再加上线程之间的切换开销,更是雪上加霜,即使你引入了线程池技术来控制线程的个数,但是当客户端多起来的时候会导致线程池的BlockingQueue队列越来越大,那么,这时候的NIO就可以为我们解决这个问题了,他并不会为每个客户端都创建一个线程,在服务端他只有一个线程,他只会为每个客户端创建一个通道,具体的内容我们下篇博客介绍了;