尝试做出一些改进,解决不能读取图片的问题。

一、不要把头信息都写在一团

优雅一点:

二、一些问题

我开始觉得实现服务器是一个超级复杂的工作了,必须要非常了解HTTP协议。

(1)Content-Length的必要性

下面是我的理解:

首先,对于静态内容(比如一张图片),因为内容的大小已经决定了,所以有必要计算Content-Length。Content-Length是浏览器对页面进行处理的重要依据。

关于这个问题,详见:https://my.oschina.net/xishuixixia/blog/93185

Content-Length必须真实反映实体长度。但是在实际应用中,有些时候实体长度并没有那么好获得。对于一些动态内容(比如一个查询接口,不知道查询结果如何),或者一些网络文件,只能开一个buffer数组,通过计算数组长度,从而获得Content-Length的值。这样做一方面需要更大的内存开销,另一方面也会让客户端等更久(处理需要更长时间)。

为了解决这个问题,需要一种新的机制:Transfer-Encoding为chunked。

如果服务端设置了Transfer-Encoding为chunked,HTTP请求头中就不会有Content-Length属性了。如果Transfer-Encoding为chunked,就意味着内容会被分成一块一块的发送,接收者不需要等到内容都传输完毕了才读取其中的内容。

这种情况下,Content-Length无法被计算,所以可以不用设置,就算设置了,也会被无视。

如果客户端在请求头加上Connection:keep-alive,服务器的response就会采用Transfer-Encoding:chunked的形式,通知页面数据是否接收完毕。

我尝试使用swagger向spring mvc客户端发送请求(swagger默认请求的设置是Connection:keep-alive),返回数据的形式就是Transfer-Encoding:chunked。

关于Transfer-Encoding的详细内容,详见:https://imququ.com/post/transfer-encoding-header-in-http.html

(2)我的一些疑问

1.缓冲区的大小

如果这样设置header:

在这里content的长度为1024,因为:

设置string时,bytes的长度为1024,所以string的长度也为1024,append之后content的长度也为1024。

但是,index.html中内容的实际长度只有110字节,bytes中剩下的都是无效内容(全都是空字节),这就造成了传输时的浪费。

这就要求我们仔细考虑缓冲区的大小:

  1. 如果增大缓冲区,可以减少写IO的次数,但是最后传输的bytes中可能出现无效内容,浪费传输带宽。同时,因为加载到内存中的内容变多,将会更多地消耗内存,可能增加GC次数。
  2. 如果减小缓冲区,可以减少无效内容,减少带宽的浪费,加载到内存中的内容也会更少,GC的次数可能会减少。但是会增加写IO的次数,效率较低。
  3. 在设置缓冲区大小时,需要更多的经验。根据网上的说法,BUFFER_SIZE设为1024应该是比较好的选择。

2.Content-Length的值

如果改动Content-Length的值,在95以上,页面都是没有问题的。但是,只要少于95,内容就开始不全了(比如只要设置为94,就会少加载一个感叹号)。就算有很多标签没有加载,加载的只有:

浏览器也会自动会把标签补全(我用的是chrome),所以不会有显示问题。

这有可能是一个奇技淫巧(其实完全没有必要),说不定以后服务端可以少传一些标签(反正浏览器会自动补全),从而减少需要传输的数据量。

如果改成1025,浏览器就会报出ERR_CONTENT_LENGTH_MISMATCH错误,一直处于等待状态,页面无法读取。

这是因为outputStream传的bytes只有1024byte,如果把Content-Length设成1025,浏览器认为内容还没有传输完,就会一直处于等待状态,直到再传1个byte,才会加载整个页面。

综上,如果Content-length存在并且有效,就必须和消息内容的传输长度完全一致。否则将引发各种问题。

3.如果不设置Content-Length

如果不设置Content-Length,客户端如何判断数据是否接收完成呢?

  1. 静态资源:当客户端向服务器请求一个静态页面或者一张图片时,服务器可以很清楚的知道内容大小,然后通过Content-length消息首部字段告诉客户端需要接收多少数据。
  2. 动态资源:如果是动态资源,服务器不可能预先知道内容大小,这时就可以使用“Transfer-Encoding:chunk”的模式来传输数据。如果要一边产生数据,一边发给客户端,服务器就需要使用“Transfer-Encoding:chunked”这样的方式来代替Content-Length。

我测试了一下,如果服务端从一开始就不设置Content-Length(同时也没有设置Transfer-Encoding),页面反而能够正常加载。

我个人猜想:如果不设置Content-Length,那么浏览器就会一直读取数据,坚持到连接关闭为止,保证页面能够正常显示。

后来我试验了一下,如果服务端不使用close方法关闭链接,那么浏览器会一直处于读取状态,证实了我的猜想。

4.一种攻击方式

HTTP 1.1支持TCP长连接,当服务端接收请求时需要获取请求头中的content-length,确定接下来还需要获取的body长度,从而切分同一个连接上的不同请求。

这时,如果伪造Content-Length,使其大于实际body长度,服务器将持续等待还未接收的字节流,从而导致链接一直被占用。

如果保持的链接多了,自然会意向服务端的性能。

为了防范此种攻击,服务端应该具备“限制链接数”、“超时关闭”的机制。

  1. 首先应该限制服务端的最大链接数,避免超出负载极限。
  2. 当链接数到达上限时,如果有新的链接,应该主动关闭长时间等待的旧链接/拒绝新链接。
  3. 如果某些链接超时,应该主动关闭,腾出资源。

三、读取不了图片文件

直接运行上篇文章中的代码,会有如下两个问题:

(1)java.net.SocketException

存在这样一个错误:java.net.SocketException: Connection reset by peer: socket write error

这是Content-Length导致的错误。当浏览器接收一定字节的数据后,就认为已经获取完了,主动关闭socket链接。这时候服务端正在输出数据,还没输出完,socket就已经关闭了,所以报出链接重置错误。

只要服务端不设置Content-Length或者设置正确数值的Content-Length,就能解决这个问题。

(2)读取的数据为乱码

这是Content-Type的类型错误所导致的。如果设置为text/html,浏览器会把字节解析为文本,导致乱码。

实验时我使用了png格式的图片,所以应设置为image/png,浏览器将自动把字节解析为图片。

实际上,设置为image/jpg(或者其他图片格式)也可以解析为图片(即使原图片文件是png格式)。

为了根据不同文件格式设置不同的header,作出如下调整:

在接下来的改进中,应该根据Content-Type对照表设置header,正确解析文件格式。

四、总结

具体实现起来,感觉还是有很多可以加强的地方,希望能继续保持下去。

发表评论

电子邮件地址不会被公开。 必填项已用*标注