最近写服务器时想到一个问题:用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();
}
}