2014年10月22日 星期三

Java,InputStream,Socket阻塞.(关于HTTP请求的IO问题自我总结)

http://ansonlai.iteye.com/blog/556287

前言:

由于项目的需求,需要实现以下流程:
1. Client发送HTTP请求到Server.
2. Server接收HTTP请求并显示出请求的内容(包含请求的头及Content的内容)

服务端实现: 

Server部分代码如下:
Java代码  收藏代码
  1. import java.net.Socket;  
  2. import java.net.ServerSocket;  
  3. import java.net.InetAddress;  
  4. import java.io.InputStream;  
  5. import java.io.OutputStream;  
  6. import java.io.IOException;  
  7.   
  8. /** 
  9.  *   
  10.  * @author Anson 
  11.  */  
  12. public class Server{  
  13.   
  14.     private String ServerIP;  
  15.     private int ServerPort;  
  16.     private boolean shutdown = false;  
  17.   
  18.     /** 
  19.        * Init Server 
  20.        */  
  21.     public void init(){  
  22.           
  23.         ServerIP = "192.168.0.1";  
  24.         ServerPort = 8082;  
  25.     }  
  26.       
  27.     /** 
  28.      *   
  29.      */  
  30.     public void await(){  
  31.           
  32.         ServerSocket serverSocket = null;  
  33.           
  34.         try {  
  35.             serverSocket =  new ServerSocket(ServerPort, 1, InetAddress.getByName(ServerIP));  
  36.         }catch (IOException e) {  
  37.             e.printStackTrace();  
  38.             System.exit(1);  
  39.         }  
  40.           
  41.         // Loop waiting for a request  
  42.         while(!shutdown){  
  43.             Socket          socket  = null;  
  44.             InputStream     input   = null;  
  45.             OutputStream    output  = null;  
  46.   
  47.             try {  
  48.                 socket  = serverSocket.accept();  
  49.                 input   = socket.getInputStream();  
  50.                 output  = socket.getOutputStream();  
  51.                   
  52.                 this.parse(input);  
  53.                    
  54.                
  55.                  // Close the socket  
  56.                 socket.close();  
  57.                   
  58.             }catch (Exception e) {  
  59.                 e.printStackTrace();  
  60.                 continue;  
  61.             }  
  62.         }  
  63.     }  
  64.     public void parse(InputStream input) {  
  65.         // Read a set of characters from the socket  
  66.         StringBuffer request = new StringBuffer(2048);  
  67.         int i;  
  68.         byte[] buffer = new byte[2048];  
  69.         try {  
  70.             i = input.read(buffer);  
  71.         }catch (IOException e) {  
  72.             e.printStackTrace();  
  73.             i = -1;  
  74.         }  
  75.         for (int j=0; j<i; j++) {  
  76.             request.append((char) buffer[j]);  
  77.         }  
  78.         System.out.println(request.toString());  
  79.     }  
  80.            
  81. }  
 再编写StartServer.java
Java代码  收藏代码
  1. ....  
  2. public class StartServer{  
  3.     public static void main(String[] args){  
  4.         Server server = new Server();  
  5.         server.init();  
  6.         server.await();  
  7.         .....  
  8.     }  
  9. }  
跟着,客户端的代码做法有很多,
可以用最简单的Socket,
也可以用HttpClient,
或其它客户端如:OC
具体做法,网上有很多,在此不细述.
我用的是OC,
当然,可能有些做法,并不会出现这样的问题,
毕竟,我,并非一个高手.
我只描述我遇到的状况,
成功实现的可以忽略.

问题描述:

当客户端发送的数据量小于一定长度(如2048byte)的时候,
Server运行正常,即可以正常打印出请求的内容.
如:GET index HTTP/1.1
    ........
请求的头加上一些Content.
但当数据量大于2048的时候,奇怪的问题出现了.
有时会发现数据读不全.
例如,在请求的头下面,Content的内容是一串XML的String:
<?xml version="1.0" encoding="UTF-8" ?><label1>label1></label1>......
当请求的头加上XML的String的总长度超过2048,那么,可能出现在情况就是超过的部分丢失.

解决办法:

可能已经有人发觉,在上面的Server里面的parse(InputStream input)这里的处理有问题.
因为很大程度上这个2048在StringBuffer的定义时就出现了.
所以,第一个失败的尝试便是修改2048.
第一次变成10*2048
跟着是100*2048....
最后是1000*2048....
但结果可以发现:数据量的限制并不会随着这个数值的成倍增长而以相同倍数增长.有时可能是为了增加一倍,
而这个数值必需增加20倍.....
最后发现这个办法不可能,
于是,改变了读取的办法.
用了其它的如StreamBufferReader还有很多其它的来尝试读取,
结果却令人失望,
甚至于这时候出现了阻塞.
即,在读取的过停中,卡住了,
直到等到客户端的请求超时,才跳出来.
所以这些方法也失败了.
具体的情况也记不大清楚了.

之后的另一个方法:
         String request = "";
           //如果不先读一次,那么下面的input.available()有可能是0值.
           char firstbyte = (char) input.read();
            request += firstbyte;
            int ava = 0;
            while ((ava = input.available()) > 0) {
                try {
                    // 在此睡眠0.1秒,很重要
                    Thread.sleep(100);
                } catch (Exception t) {
                    t.printStackTrace();
                }
                byte[] bufferedbyte = new byte[ava];
                input.read(bufferedbyte, 0, ava);
                request += new String(bufferedbyte, "UTF-8");
            }
 这是一个成功解决的方法,
 之所以要睡眠0.1秒,等待高手帮忙解答,
这也许跟网络有关系,
当然,数据量越大,睡眠的时应该有小小的加长.(缓冲时间).
虽然以上的做法成功实现了,但对于服务器来说,效率是个问题,所以只能寻找更优的办法.

第三次更改:

Java代码  收藏代码
  1.           int readInt = 0;  
  2. int availableCount = 0;  
  3. char requestHead = (char)(input.read());  
  4. request += requestHead;  
Java代码  收藏代码
  1. //取得请求的类型是PUT或GET  
  2.             if(requestHead == 'P') {//主要是PUT的方法里面会带有XML.  
  3.                 while((readInt=input.read()) != -1) {  
  4.                     request += (char)readInt;  
  5.                     if(request.indexOf("</endXML>") != -1){  
  6.                         break;  
  7.                     }  
  8.                 }  
  9.                 System.out.println("this is put\n" +request);  
  10.             } else if(requestHead=='G') {//GET的方法内容比较少,用以下的方法可以正常实现  
  11.                 while((availableCount=input.available()) > 0){  
  12.                     byte[] requestbuffer = new byte[availableCount];  
  13.                     input.read(requestbuffer);  
  14.                     request += new String(requestbuffer, "UTF-8");  
  15.                 }   
*代码并不完整.
这种做法,我在读取XML的String里加上了一个结束的判断即对"</endXML>"的判断(当然,这是在知道Content内容的基础上这种做法才可行).
虽然暂时解决了问题,但仍然不能完美解决存在的问题.

第四次更改:
这也是最后一次更改,办法是:
在PUT的请求里面,先取得一个Content-Length:的请求头的值length.
这里的大小就是Content的长度.
在这个基础上,
先判断是否开始读取Content:
if(requestString.indexOf("\r\n\r\n") != -1)
.....

之后再循环读取:
    for(int i=0; i < length; i++){
        requestString += (char)input.read();
    }
当读完length后,跳出循环,
并跳出读取input的读取.
.......
问题在此告一段落.

若有哪位朋友懂得其中原理,恳请告知,
知其然而不知其所以然,心里不免有个结.

结言:

以上做法,仅供参考.
若有错误,请不啬指出.

欢迎转载
转载进请注明转自:http://ansonlai.iteye.com

分享到:  
评论
13 楼 A_L_85 2014-07-29  
a_bin 写道
垃圾,说了那么多又那么乱,没个主题,死吧你

你死了吗?
我可还活着!
12 楼 a_bin 2014-05-27  
垃圾,说了那么多又那么乱,没个主题,死吧你
11 楼 A_L_85 2012-05-24  
macemers 写道
同在学习中,楼主能给出客户端的代码么?

哈,你想要什么代码,这是我两年前的代码了
10 楼 bluedest 2012-05-21  
A_L_85 写道
bluedest 写道
基本上,我认为这是
read(byte b[])方法造成的结果。
1.底层windows在网络缓冲区开辟一块内存,这块内存用于接收别人发给自己的数据包(网络层的packet)。
2.网络数据包是有大小的,根据各自的网络情况,可能实际情况有所不同。
3.read(byte b[])向底层操作系统读数据包时,只是取到了一个瞬间在缓冲区中的字节,然后就返回到Java(应用层)。所以实际上你的数据包还没有收完。
4.可以做一个测试,客户端发数据,服务端收数据。客户端连接发10字节就睡5秒,在服务端你读取read(byte b[1024]),虽然定义得很长,但是服务端只是读到10字节就返回了,一样的道理。

在第4点的测试中,如果以这种情况下,会不会出现服务器读到10字节之的后便处在不断等待数据包的情况?




不会的,你可以试一下.read(byte[])方法将会立即返回,不阻塞
9 楼 macemers 2012-05-21  
同在学习中,楼主能给出客户端的代码么?
8 楼 A_L_85 2011-03-09  
charseller 写道
Java6支持httpserver,其中的httpExchange.getRequestBody()得到的InputStream的同样处理就不存在这个问题

GOOD!
7 楼 charseller 2011-03-06  
Java6支持httpserver,其中的httpExchange.getRequestBody()得到的InputStream的同样处理就不存在这个问题
6 楼 charseller 2011-03-05  
也按照1楼同学的提示,对一个socket的先发一段,睡10秒,100秒再发后面一段,服务器都可以正常接收,客户端也正常收到返回数据
5 楼 charseller 2011-03-05  
在我的程序中同样出现了这个问题,搜索到此。3楼的方案虽然解决了问题,仍然没有说明问题所在。用中文搜索没找到满意解决,只得英文搜索,搜索到:http://stackoverflow.com/questions/611760/java-inputstream-read-blocking

其中有段话有启发:It returns -1 if it's end of stream. If stream is still open (i.e. socket connection) but no data has reached the reading side (server is slow, networks is slow,...) the read() blocks.

最后发现其实有关 read()本身没问题,而是client端没有做好。

在socket.getOutputStream().write(data)后,尝试socket.getOutputStream().close()。服务器端没问题了,read()会返回-1咯。。。呵呵,但是(最怕但是),客户端close OutputStream后等带服务器的回应的InputSteam.read()出现了:socket closed Exception

最后发现Socket有 shutdownOutput()方法!哈哈,在客户端write,flush后再调用shutdownOutput(),成了!后面的inputStream仍然可以。。。

当然,如果客户端和服务器程序是各自开发,楼主及3楼的方法还是必要。

这次我碰到的问题其实说明这个现在还是普便存在的:本来用http实现通信的,用httpserver的httpExchange.getRequestBody()得到的InputStream处理就没有出现这个问题。后来到现场才发现服务器的开发单位最终是用socket实现的通信,还专门提出了socket的头6个字节表示长度,俺这么一写,才发现对方可能也是碰到这个问题,采取了长度方法来处理。(我是在做服务器端simulator,同时也要做这个simulator的测试,所以客户端simulator也做,说的没听糊涂吧:)

shutdownOutput
public void shutdownOutput()
                    throws IOException
Disables the output stream for this socket. For a TCP socket, any previously written data will be sent followed by TCP's normal connection termination sequence. If you write to a socket output stream after invoking shutdownOutput() on the socket, the stream will throw an IOException.

4 楼 A_L_85 2011-01-10  
 特此感谢3楼兄弟~~
受益良多啊...
3 楼 xiaod0510 2010-12-19  

首先在你的parse(InputStream input)方法里
下面的代码段
#  byte[] buffer = new byte[2048]; 
#         try { 
#             i = input.read(buffer); 
#         }catch (IOException e) { 
#             e.printStackTrace(); 
#             i = -1; 
#         } 

buffer是定长的,A_L_85也意识到这个问题
其实可以用ByteArrayOutputStream解决的

ByteArrayOutputStream可以作为一个变相的byte动态数组,A_L_85可以看一下相关API文档

代码修改如下

ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
try {
while ((i = input.read(buffer)) != -1) {
bos.write(buffer,0,i);
}
} catch (IOException e) {
e.printStackTrace();
}
//toByteArray后会将内存中的数据片段连接返回一个Byte[] 
//类似StringBuffer
buffer = bos.toByteArray();


然后再说第二个问题
当Socket.getInputStream()的缓冲区内没有数据时
调用read(...)方法会出现阻塞现象
1楼说的意思大致是正确的
当你读取"InputStream缓冲区"的速度大于网络传输速度时
会出现阻塞现象,其实这是因为java程序在读完缓冲区内的数据后,无法判断客户端是不是还在写入数据,所以会在那里等着.

这也就是下面的代码为什么需要sleep,其实是在等待客户端向缓冲区写数据

try {
    // 在此睡眠0.1秒,很重要
    Thread.sleep(100);
} catch (Exception t) {
   t.printStackTrace();
}


这个问题可以如下解决的

int before = input.available();//缓冲区内可读数据
while(true){
    Thread.sleep(100);//try代码省略 sleep时间可以按需要调整
    int ava = input.available();
    /**相隔一段时间后 缓冲区内的可读数据没变的话说明客户端已经写入完成 退出循环 一次性将剩余数据取出*/
    if(ava==before){
        break;
    }
    before = ava;
}

byte[] buffer = new byte[input.available()];
input.read(buffer);

第一,二个问题相结合就能正确的读取缓冲区内的数据了

还有就是看到作者这样的代码

    for(int i=0; i < length; i++){
        requestString += (char)input.read();
    }
着实吓了我一跳,真的...




2 楼 A_L_85 2010-02-10  
bluedest 写道
基本上,我认为这是
read(byte b[])方法造成的结果。
1.底层windows在网络缓冲区开辟一块内存,这块内存用于接收别人发给自己的数据包(网络层的packet)。
2.网络数据包是有大小的,根据各自的网络情况,可能实际情况有所不同。
3.read(byte b[])向底层操作系统读数据包时,只是取到了一个瞬间在缓冲区中的字节,然后就返回到Java(应用层)。所以实际上你的数据包还没有收完。
4.可以做一个测试,客户端发数据,服务端收数据。客户端连接发10字节就睡5秒,在服务端你读取read(byte b[1024]),虽然定义得很长,但是服务端只是读到10字节就返回了,一样的道理。

在第4点的测试中,如果以这种情况下,会不会出现服务器读到10字节之的后便处在不断等待数据包的情况?
1 楼 bluedest 2010-02-09  
基本上,我认为这是
read(byte b[])方法造成的结果。
1.底层windows在网络缓冲区开辟一块内存,这块内存用于接收别人发给自己的数据包(网络层的packet)。
2.网络数据包是有大小的,根据各自的网络情况,可能实际情况有所不同。
3.read(byte b[])向底层操作系统读数据包时,只是取到了一个瞬间在缓冲区中的字节,然后就返回到Java(应用层)。所以实际上你的数据包还没有收完。
4.可以做一个测试,客户端发数据,服务端收数据。客户端连接发10字节就睡5秒,在服务端你读取read(byte b[1024]),虽然定义得很长,但是服务端只是读到10字节就返回了,一样的道理。