刚开始感到很奇怪,大文件的复制不都是这样做的么,怎么还会出错,在网上搜了一下,socket在close后,才会发送给另一端结束符EOF,从而才会read到流结尾信息而返回-1。
以前写java聊天功能的时候其实遇到过这样的问题的,要退出聊天发一个特定的字符,然后在break出循环,接着会close掉socket,这样另一端的会由于这端的socket被close掉也跳出循环。只是现在由于只写服务端就没想到。
因为无法知道远程的socket是否还有没有东西要发送。所以read一直不会返回。
read的文档说明大致是:如果因已到达流末尾而没有可用的字节,则返回值 -1。在输入数据可用、检测到流的末尾或者抛出异常前,此方法一直阻塞。
socket和文件不一样,从文件中读,读到末尾就到达流的结尾了,所以会返回-1或null,循环结束,但是socket是连接两个主机的桥梁,一端无法知道另一端到底还有没有数据要传输。
socket如果不关闭的话,read之类的阻塞函数会一直等待它发送数据,就是所谓的阻塞。
当然这里我们可以将缓冲buffer调整的大一点,这样不用while循环,只读一次即可,然而其他的场景比如发送的数据很大一次读不完那么就只能while循环来处理了。这种场景下的解决方案方案见下面。
四种途径解决:
1.调用socke的shutdownOutput方法关闭输出流,该方法的文档说明为,将此套接字的输出流置于“流的末尾”,这样另一端的输入流上的read操作就会返回-1。不能调用socket.getInputStream().close()。这样会导致socket被关闭。
2.约定结束标志,当读到该结束标志时退出不再read。
3.设置超时,会在设置的超时时间到达后抛出SocketTimeoutException异常而不再阻塞。
4.在头部约定好数据的长度。当读取到的长度等于这个长度时就不再继续调用read方法。
总之tcp方式会经常由于阻塞函数等read/readLine和流处理的函数如刷新缓冲导致代码出现问题。一定要小心!
方式一一般用在通信双方均由开发者掌控。方式二有一定的局限,并且双方还要沟通好标结束志。方式三总感觉不好,超时应该用在其他更有意义的地方,如网络不好时的时间限制。方式四应该是最好的方式,并且大多数的情况都是这样做的。
显然我们这里不能使用方式一。
于是我立刻想到了一个问题:HTTP协议的结束标志是什么?
貌似就搜到了几个地方有人讨论该问题,见:
1.主题:学习Spring必学的Java基础知识(9)—-HTTP报文(系列全) 里面提到的结束标志我测试了也不对。
2.http包结束的标志
我没有研究过HTTP协议的具体细节,只知道它是对Socket的封装和一些协议的格式,其他的还不太清楚,不过就目前看到的来看应该没有让服务器端知道数据结束的标志。
于是另一个问题又在我脑海产生了:tomcat源代码是怎么解析HTTP协议的头信息呢?
我最初猜想应该是通过第四种方式因为包含了Content-Length字段,很容易能得到总的大小。大致翻看了一下源代码,貌似还不是这样,其采用的是NIO Socket实现的,
在解析HTTP的头时是一个字节一个字节解析的,不过代码太长,只是看了个大概,比较了解的可以和我交流学习,不胜感激。