采用Java阻塞IO对已经到达的socket流实现非阻塞完整读取(一个简单的java http server实现)

最近写服务器时想到一个问题:用Java Bio(即Socket)写服务器,怎么一次性完整读取已经到达的Socket流。


对这个需求有很多角度的设定,也有很多解法。我们来一一具化这个需求:

(1)

解法:依赖http协议的content-length。

分析:很直观的想法,可以根据http请求头给定一个固定长度的字节或字符缓存,从中获取content-length,就知道往后要在从流中读多少字节了。

设定:如果不考虑http,设置也不考虑任何定制的协议(流的开头给长度或者用特殊的字符标志留的结尾),仅仅考虑一次socket流的到达,如何才能用完整读取这次到达流的内容。


(2)

解法:上nio上mina上netty。

分析:又直观的想法。不过开篇设定好了用阻塞读来实现非阻塞完整读取到达流。

设定:如何用java阻塞读(bio)达到不阻塞的完整读取一次socket到达流。


(3)

解法:依赖流的结尾返回-1。

分析:更直观的想法,java.io(也就是bio啦)提供的各种XXStream和XXReader都提供read(XX)的函数,并提示如果读到结尾就返回-1。那在服务器端直接不断read(),直到-1就好了。问题是对于文件的读取,到达结尾会返回-1(好像是文件末尾的EOF?具体没研究过)。但是socket流,是没有结束符的(其实也有,一个socket关闭后再read就会返回-1,但是这里对于一个已经到达的流的完整读取,肯定不能依赖于网络对面对socket关闭)。虽然文件流是连续的,但是网络流肯定不能保证,即便通过socket发送一个完整的文件,由于网络原因,这个文件可能分几个部分到达socket,而本文的需求就设定在对一次到达的流,怎么保证完整的读取。


(4)

解决:用大缓存。

分析:更加直观的想法。上个足够大的缓存,一次性把到达流的内容全部读出来,不怕不够,就怕你不够大!但是,多大的缓存算大呢?如果服务器的资源有限呢?如果到达流里的内容就是要胜天半子,比缓存多一个字节呢?

设定:我们可用的缓存有限,肯定比一次性到达的流长度要小。


(5)

解决:用缓存循环从socket流里read(buf),如果缓存读满了,说明流可能还有未读内容。如果缓存未满,哼哼~这次到达流肯定读完了。其实这个方案本质上和不用缓存循环用read()挨个读没区别。伪代码:

while (inputStream.read(buf) == buf.size){}

or

while (inputStream.read() != -1) {}

分析:这个方法有个漏洞——我们在使用阻塞io。假设这样一个场景,我能用的缓存是8个字节,一次流到达了16个字节,那么两次循环下来,我能把流读完。但是第二次循环时判断buf还是读满了,所以可能流还没读完。于是进行第三次流的read(buf)。假设后续没有其他流到达,那么第三次的read(buf)是读不到东西的,也就是阻塞在那里了。用read()挨个读也是一样的,读完流里面最后一个字节,再读取的话会阻塞,而不是返回-1。这与条件设定的非阻塞读完整流冲突。


(6)

问题:如果在读取流的过程中,后续又到达了其他流信息,怎么区分两次到达的流?

分析:超纲了超纲了... 本题设定是没有流的协议信息的...这种情况只能把多次到达的流全部读取过来,或者其他高手在这种玩法下可以解决的话请不吝赐教。


分析到这,清楚这个问题的限定:(1)无传输协议,没有长度信息或者结束标志;(2)用阻塞IO读取;(3)读取一次到达的流内容时不能阻塞;(4)读取时后续有其他流到达不管。


我想到的一个解决的办法就是java.io.XXReader类的ready()函数。这个函数会测试下次调用read()函数时会不会阻塞,如果不阻塞就返回true,阻塞的话返回false。


注意下,ready()函数返回true可以保证进行read()后100%不阻塞,但是返回false的话,进行read()不一定阻塞,也可能读到数据返回。其实这很直观,如果探测到还有未读的数据,返回true,这些未读的数据在没读取之前也不会跑掉,read()的话肯定能读到。但是探测到如果没有数据,和用read()进行读取之间,可能会继续有流到达,此时就不一定会阻塞。也就是为什么用ready()函数解决不了多次到达流区分的问题。


我用java的bio写了一个简单的http server来实现上述需求。虽然用http,只是为了大家从浏览器端跟这个server交互方便而已。其实采用任何协议或者自己写个client用socket随便发送什么来测试ready()的非阻塞读也可以。


这个server接受用户的http请求,然后从请求头把uri提取出来(不包括参数,比如请求行“GET /user?name=XXX HTTP/1.1”,那么提取的uri就是“/user”),然后通过html发回给用户,页面显示用户输入的uri。如果用户输入的请求路径是“/stop”,那么可以从浏览器端关闭服务器,并在页面显示“server close”。


下面直接上代码:


package com.jxshen.example.web.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * A simple http server using bio to read a socket stream
* and response the uri in http request line to client,
* that is uri after method blank and before the blank of protocol/version
* if the uri is "/stop", server close
* just test a method to read a complete arrival socket stream with a very small buffer
* so the buffer should be used repeatedly to join the hold arrival message
* the key point is BufferedRead.ready() function which tell the next read() is guaranteed not to block for input
* but the ready() return false do not guarantee the next read() is 100% block
* * @author jxshen * */ public class SimpleHttpServer { public static final int SMALL_BUF_SIZE = 8; public static final int PORT = 8080; public static final int BACK_LOG = 50; // client can use http get uri to close server, eg: http://localhost:8080/stop private static final String STOP_URL = "/stop"; // if client stop server, the string of response private static final String CLOSE_RESP_STR = "Server Close"; private static volatile boolean stop = false; // The html template of response private static final String HTML = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: %d\r\n" + "\r\n" + "%s"; public static void main(String[] args) { new SimpleHttpServer().run(); } public void run() { ServerSocket server = null; try { server = new ServerSocket(); server.bind(new InetSocketAddress(PORT), BACK_LOG); } catch (IOException e) { e.printStackTrace(); System.exit(1); } Socket client = null; InputStream is = null; OutputStream os = null; while (!stop) { try { client = server.accept(); is = client.getInputStream(); os = client.getOutputStream(); // handle inputStream BufferedReader br = new BufferedReader(new InputStreamReader(is)); StringBuilder reqStr = new StringBuilder(); char[] buf = new char[SMALL_BUF_SIZE]; do { if (br.read(buf) != -1) { reqStr.append(buf); } } // the key point to read a complete arrival socket stream with bio but without block while (br.ready()); // get uri in http request line String respStr = parse(reqStr.toString()); // handle outputStream if (stop = STOP_URL.equalsIgnoreCase(respStr)) { respStr = CLOSE_RESP_STR; System.out.println("client require server to stop"); } // join the html content respStr = "

" + respStr + "

"; os.write(String.format(HTML, respStr.length(), respStr).getBytes()); os.flush(); } catch (IOException e) { e.printStackTrace(); } } try { server.close(); client.close(); } catch (IOException e) { e.printStackTrace(); } } /** * get uri in http request line to client, that is uri after the blank of method and before the blank of protocol/version
* eg: for a http get request, the request line maybe: GET /user?name=jxshen HTTP/1.1
* then the function return "user"
* */ public static String parse(String source) { if(source == null || source.length() == 0) { return new String(); } int startIndex; startIndex = source.indexOf(' '); if (startIndex != -1) { int paramIndex = source.indexOf('?', startIndex + 1); int secondBlankIndex = source.indexOf(' ', startIndex + 1); int endIndex = -1; if (secondBlankIndex > paramIndex) { endIndex = secondBlankIndex; } else { endIndex = paramIndex; } if (endIndex > startIndex) return source.substring(startIndex + 1, endIndex); } return new String(); } }





你可能感兴趣的:(Java,http,IO)