请帮我弄清楚这个Web代理代码有什么问题

我想为练习编写一个Web代理,这是我到目前为止的代码:

// returns a map that contains the port and the host def parseHostAndPort(String data) { def objMap // this has host and port as keys data.eachLine { line -> if(line =~ /^(?i)get|put|post|head|trace|delete/) { println line def components = line.split(" ") def resource = components[1] def colon = resource.indexOf(":") if(colon != -1) { URL u = new URL(resource) def pHost = u.host def pPort = u.port return (objMap = [host:pHost,port:pPort]) } else { return (objMap = [host:resource,port:80]) } } } return objMap } // reads a http request from a client def readClientData(Socket clientSocket) { def actualBuffer = new StringBuilder() InputStream inStream = clientSocket.inputStream while(true) { def available = inStream.available() if(available == 0) break; println "available data $available" def buffer = new byte[available] def bytesRead = inStream.read(buffer,0,available) actualBuffer < println "got a client" def data = readClientData(cli) def parsed = parseHostAndPort(data) def host = parsed["host"] def port = parsed["port"] println "got from client $data" def nsock = new Socket(host,port) nsock << data // send data received from client to the socket nsock.outputStream.flush() def datax = readClientData(nsock) println "got back $datax" cli << datax // send the client the response cli.outputStream.flush() cli.close() } } 

现在,它所做的只是:

  • 读取我的浏览器发送的HTTP请求

  • 解析主机和端口

  • 连接到该主机,并写入从客户端收到的数据

  • 向客户端发回从主机收到的数据

但是……它不会一直有效。 有时它会提出一个好的请求,有时候不是。 我认为这是一个缓冲问题,我不确定。 问题是,我添加了flush呼叫,但仍然没有。

你能发现我做错了什么吗?

编辑:

  • 我注意到,如果我添加一些sleep调用,代理似乎“工作”了更多的请求,但不是全部。
  • 收集赏金,帮我找出我做错了什么。 用于Web代理的常规“算法”是什么? 我在哪里偏离它? 谢谢!

首先,很难知道这里到底出了什么问题 – “有时它会提出一个好的请求,有时候不会。” 并没有真正描述问题发生时发生的事情!!

也就是说,我仍然能够找出你出了什么问题。

正如您已经说过的那样,您正在寻找能够始终如一地工作的最基本的解决方案,因此我将避免任何不必要的事情或者提高代码的效率或其他方面。 另外,我先给你答案,然后描述导致问题的原因(它很长,但值得一读:)

您的问题的简单答案是您需要进行一些HTTP协议解析,以确定客户端是否已发送所有数据,而不依赖于available()read()返回的内容。 这有多少取决于您希望支持HTTP协议的完全程度。 为了支持GET请求,它非常简单。 支持指定内容长度的POST有点困难。 支持“其他”编码类型要困难得多(例如,chunked或multipart / byteranges,请参阅http://tools.ietf.org/html/rfc2616#section-4.4 )。

无论如何,我假设你只是想让GET工作,所以要做到这一点,你必须知道HTTP标题和bodys被一个“空行”分开,HTTP的行分隔符是\ r \ n而GETs那样做没有身体。 因此,客户端在传输\ r \ n \ r \ n时已完成发送GET请求。

像这样的一些代码应该一致地为你处理GET(代码未经测试但它应该让你至少达到90%):

 def readClientData(Socket clientSocket) { def actualBuffer = new StringBuilder() def eof = false; def emptyLine = ['\r', '\n', '\r', '\n'] def lastEmptyLineChar = 0 InputStream inStream = clientSocket.inputStream while(!eof) { def available = inStream.available() println "available data $available" // try to read all available bytes def buffer = new byte[available] def bytesRead = inStream.read(buffer,0,available) // check for empty line: // * iterate through the buffer until the first element of emptyLine is found // * continue iterating through buffer checking subsequent elements of buffer with emptyLine while consecutive elements match // * if any element in buffer and emptyLine do not match, start looking for the first element of emptyLine again as the iteration through buffer continues // * if the end of emptyLine is reached and matches with buffer, then the emptyLine has been found for( int i=0; i < bytesRead && !eof; i++ ) { if( buffer[i] == emptyLine[lastEmptyLineChar] ){ lastEmptyLineChar++ eof = lastEmptyLineChar >= emptyLine.length() } else { lastEmptyLineChar = 0 } } // changed this so that you avoid any encoding issues actualBuffer << new String(buffer, 0, bytesRead, Charset.forName("US-ASCII")) } return actualBuffer.toString() } 

对于POST,您需要通过查找字符串“Content-length:”并在此之后解析值来添加。 此值是八进制中 HTTP主体的大小(即标头标记的/ r / n / r / n结尾之后的位)。 因此,当您遇到标题的结尾时,您只需要计算该字节的八进制数,并且您知道POST请求已完成传输。

您还需要确定请求的类型(GET,POST等) - 您可以通过检查在第一个空格之前传输的字符来完成此操作。

问题

您的问题是您的readClientData函数并不总是读取客户端发送的所有数据。 因此,您有时会向服务器发送部分请求,并返回某种错误。 如果更换,您应该看到打印到标准输出的不完整请求

 println(new String(buffer)) 

 println(avaliable) 

readClientData函数中。

为什么会这样? 这是因为available()只告诉你当前可以从InputStream读取的内容,而不是客户端是否发送了它要发送的所有数据。 一个InputStream,就其本质而言,实际上无法判断是否会有更多数据(例外情况是,如果没有更多的底层数据要读取 - 例如,套接字已关闭,数组或文件的末尾有已达到等等 - 这是read()返回-1(即EOF)的唯一时间。 相反,它取决于更高级别的代码来决定它是否应该从流中读取更多数据,并根据特定于应用程序的规则做出此决定,这些规则适用于由InputStream读取的特定于应用程序的数据。

在这种情况下,应用程序是HTTP,因此您需要了解HTTP协议的基础知识才能使其正常工作(cmeerw,您在正确的轨道上)。

当客户端发出HTTP请求时,客户端会打开一个到服务器的套接字并发送请求。 客户端在超时或基础网络连接断开时关闭套接字,或者响应需要关闭套接字的用户操作(应用程序关闭,页面刷新,停止按钮等)。 否则,在发送请求后,它只是等待服务器发送响应。 服务器发送响应后,服务器将关闭连接[1]。

在您的代码成功的地方,客户端快速且一致地提供数据,以便InputStream在您调用read()和随后的循环的下一次迭代调用available()之间接收额外的数据(请记住, InputStream是正在为您的代码调用其read()方法提供数据“并行”。 现在在另一种情况下,你的代码失败了,还没有数据提供给InputStream ,所以当你的代码调用available()InputStream正确地返回0,因为你没有提供更多的数据,因为你调用了read()因此它有0个字节可供您read() 。 这是Johnathan谈论的竞争条件。

你的代码假定当available()返回0时,客户端发送了所有数据,实际上有时它已经发送,有时它没有(因此有时你得到一个“好请求”而其他时候没有:)。

因此,您需要比available()更好的东西来确定客户端是否发送了所有数据。

调用read()时检查EOF(参见R4an的答案[2])也不合适。 应该清楚为什么会这样 - read()应该返回EOF(-1)的唯一时间是套接字关闭时。 在您将请求转发到目标代理,收到响应并将响应发送到客户端之前,这不应该发生,但我们知道它也可以由客户端exception关闭。 事实上,当您运行示例代码时,您会看到此行为 - 代理会挂起,直到在浏览器中单击停止按钮,导致客户端过早关闭连接。

您现在知道的正确答案是对HTTP进行一些解析并使用它来确定连接的状态。

笔记
[1]它超出了概念代理的certificate,但由于它已被触及,如果HTTP连接是“保持活动”,服务器将保持连接打开并等待来自客户端的另一个请求
[2]此代码中存在一个错误,导致readClientData损坏数据:

 byte[] buffer = new byte[16 * 1024]; while((bytesRead = inStream.read(buffer)) >= 0) { // -1 on EOF def bytesRead = inStream.read(buffer,0,bytesRead); actualBuffer << new String(buffer) } 

第二个inStream.read()调用完全覆盖第一次调用inStream.read()读取的数据。 此处还重新定义了bytesRead(不熟悉Groovy以了解这是否是一个错误)。 这行应该是:

 bytesRead = bytesRead + inStream.read(buffer,bytesRead,buffer.length()-bytesRead); 

或完全删除。

乔纳森走在正确的轨道上。 问题部分在于你使用available()available的方法并没有说“它完成了吗?” 它说“目前有没有可用的数据?”。 因此,在您提出请求后,将立即没有任何可用数据,并且取决于处理过程中可能发生的网络时间,但这并不意味着不再有任何数据可用,因此您的break还为时过早。

此外, 始终允许InputStream.read(byte[] ...)系列方法返回比您要求的更少的字节。 数组长度或偏移量,长度对约束最大值 ,但总是可以减少。 所以,你的这个代码:

  def buffer = new byte[available] def bytesRead = inStream.read(buffer,0,available) actualBuffer << new String(buffer) 

可以创建一个大数组,但只能在读取中获得一半的数据,但仍然将完整的缓冲区(带有其未删除的数组元素)附加到String上。

这是一个依赖于InputStream.read(...)将永远不会返回的事实的修订版,除非它的流结束或者有一些数据可用(但不一定像你要求的那样多)。

 // reads a http request from a client def readClientData(Socket clientSocket) { def actualBuffer = new StringBuilder() InputStream inStream = clientSocket.inputStream int bytesRead = 0; byte[] buffer = new byte[16 * 1024]; while((bytesRead = inStream.read(buffer)) >= 0) { // -1 on EOF def bytesRead = inStream.read(buffer,0,bytesRead); // only want newly read bytes actualBuffer << new String(buffer) } return actualBuffer.toString() } 

也就是说,你还有其他一些问题:

  • 当你应该将字节泵循环直接复制到客户端的响应输出流中时,你将整个响应拉入内存(如果它是一个多千兆字节的响应会发生什么)
  • 你正在使用字符串来存储二进制数据 - 这假设所有字节在默认的CharacterEncoding中工作正常,这在UTF-8或US-ASCII中可能是正确的,但是不适用于其他语言环境

Ry4an提出了一些好处。 如果你想看看如何构建一个小但完美形成的代理,看看用Python编写的Tiny HTTP Proxy – 你可以看到所有需要解决的问题,将代码移植到Groovy是相当简单的。 我已经使用代理进行测试,效果很好。

我建议你熟悉HTTP协议规范 。 HTTP比单独的TCP连接上的单个请求响应更复杂 – 即如果客户端或服务器尝试使用持久连接,则实现将失败。

readClientData(Socket)中是否存在竞争条件? 看起来您正在立即检查数据是否可用,但有可能尚未收到数据; 你只需退出循环而不是等待接收第一个数据。

客户端套接字是否阻塞? 如果是这样,您可能需要尝试非阻塞I / O或设置套接字超时。