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

来源:互联网 发布:数据比对公式vlookup 编辑:程序博客网 时间:2024/05/20 20:48

最近写服务器时想到一个问题:用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<br>  * and response the uri in http request line to client, <br> * that is uri after method blank and before the blank of protocol/version<br> * if the uri is "/stop", server close<br> * just test a method to read a complete arrival socket stream with a very small buffer<br> * so the buffer should be used repeatedly to join the hold arrival message<br> * the key point is BufferedRead.ready() function which tell the next read() is guaranteed not to block for input<br> * but the ready() return false do not guarantee the next read() is 100% block<br> *  * @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 = "<h1>" + respStr + "</h1>";                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<br>     * eg: for a http get request, the request line maybe: GET /user?name=jxshen HTTP/1.1<br>     * then the function return "user"<br>     *      */    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();    }}





原创粉丝点击