请帮我弄清楚这个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或设置套接字超时。