HTTP首部——分块传输和持久连接

18 篇文章 1 订阅
9 篇文章 0 订阅

前言:
最近写了几篇博客,讲解了几个HTTP首部,感觉自己对于知识的理解还行。现在让我们来了解以下分块传输和持久连接的概念吧。本来,我是不准备写关于持久连接的部分的,因为我都是使用socket模拟的短连接,基本用不上持久连接。但是当我使用分块传输的时候,它有一个特点。因为不使用分块连接的话,默认是有一个Content-Length首部的,但是使用了分块传输的话,就没有这个首部了。这样我的程序就很难终止了(但是也是有方法的)。所以,索性就一起都写入博客了!

分块传输

分块编码是属于传输编码这个范围的,不过我也只是见过分块编码这一种传输编码方式。
先来聊一聊分块传输的应用场景,通常如果请求一个静态资源。例如一张图片,它的大小是固定的。所以报文首部可以携带一个Content-Length用来表示图片的大小。这样,程序才方便进行解析操作。
但是有两类类场景:
1.未知的尺寸
在发送的时候,我们并不能很方便的获取到需要发送的数据的大小。例如数据,是随机产生的,这就不能使用Content-Length了。因为,你根本不知道一个确定的Content-Length。所以,就产生了分块传输。
2.传输的安全性
把数据打乱了传输,不过现在有了TLS这样的协议,也就不需要依靠这种方式了。

分块传输的原理是:
分块编码把报文分割成若干个大小已知的块。块之间是紧挨着发送的,这样就不需要知道整体的大小了,并且它是作用于整个报文的。分块传输很容易理解,例如将文件分割成3块,每次发送的时候,先告诉接收方分块的大小,再告诉接收方分块的内容,这样接收方就可以很容易接收到数据了。

我前几次的博客中,自己模拟的TCP长连接和它的方法很相似。如果你理解了那篇博客的内容,对于这里要介绍的分块传输基本上也就没问题了。但是那篇博客中的文件是一个接一个接的发送,其实和这里的分块传输还是有区别的!

TCP长连接和短连接代码及其比较

分块和持久连接

先来一些预备知识:
HTTP1.0默认是短连接,即传输完数据就会自动断开,可以使用Connection: keep-alive启用长连接。
HTTP1.1默认是长连接,即传输完数据不会自动断开,可以使用Connection: close关闭长连接。
上面这两条,以及如果启用长连接和关闭都要知道,因为待会的模拟测试需要用到,不然就会很难受了!

若客户端和服务器之间不是持久连接, 客户端就不需要知道它正在读取的主体的长 度, 而只需要读到服务器关闭主体连接为止。 当使用持久连接时,在服务器写主体之前, 必须知道它的大小并在 ContentLength 首部中发送。 如果服务器动态创建内容, 就可能在发送之前无法知道主体 的长度。 分块编码为这种困难提供了解决方案, 只要允许服务器把主体逐块发送, 说明每块 的大小就可以了。 因为主体是动态创建的,服务器可以缓冲它的一部分, 发送其大 小和相应的块, 然后在主体发送完之前重复这个过程。 服务器可以用大小为 0 的块 作为主体结束的信号,这样就可以继续保持连接, 为下一个响应做准备。 备。 分块编码是相当简单的。 图 15-6 展示了一个分块编码报文的基本结构。 它由起始的 HTTP 响应首部块开始, 随后就是一系列分块。 每个分块包含一个长度值和该分块 的数据。 长度值是十六进制形式并将 CRLF 与数据分隔开。 分块中数据的大小以字 节计算, 不包括长度值与数据之间的 CRLF 序列以及分块结尾的 CRLF 序列。 最后一个块有点特别, 它的长度值为 0, 表示“主体结束”。 客户端也可以发送分块的数据给服务器。 因为客户端事先不知道服务器是否接受分块编码(这是因为服务器不会在给客户端的响应中发送 TE 首部), 所以客户端必须 做好服务器用 411 Length Required(需要 Content-Length 首部) 响应来拒绝分块 请求的准备。

在这里插入图片描述

首先是完整的首部,然后是第一块数据,第二块数据,…,最后一块数据。这个最后一块其实很有趣,它的作用也是很大的。千万不要忽略它,它其实是一个边界条件。对于程序来说,通常边界条件都是很重要的!

注意:关于这个拖挂,这里就不做说明了,因为我也没有遇到过,它是一些可选的信息。

相关首部

HTTP协议中规定了两个首部用来描述和控制传输编码(目前我只知道分块编码!)。

首部作用
Transfer-Encoding告知接收方为了可靠地传输报文,已经对其进行了何种编码。
TE用在请求首部,告知服务器可以使用哪些传输编码扩展。

举一个实际地例子:
测试URL:摄图网

在浏览器抓包测试或者其它方式抓包也行:

请求报文头

GET /?sem=1&sem_kid=33480&sem_type=1&bd_vid=8358880229459218471 HTTP/1.1
Host: 699pic.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36 Edg/84.0.522.59
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

响应报文头

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 16 Aug 2020 14:59:47 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip

注:
1.我没有看到请求报文中地TE首部,不知道是为什么?当是响应报文中确实是有Transfer-Encoding: chunked首部的,说明了这是分块传输!我今天看到一篇博客说,因为目前只有chunked这一种分块传输的方式,所以可以不带TE首部,但是还是不明白,如果客户端不支持的话,应该也是不行的。所以,应该是有首部告知服务器是否返回分块传输的报文。
HTTP的传输编码(Transfer-Encoding:chunked) 这篇博客写得很详细,可以去看看。

2.这里请求头和响应头都是做了一些删改,去掉了一些不重要或者我不知道的首部。

代码实测

Talk is cheap, show me the code.

下面我们来实际编程体验以下,理论的知识需要通过实践去夯实!

完整响应报文

我们先看一下完整的响应报文,来一个直观的感受:

/**
	 * 分块传输下载方法:获取完整的原始响应报文!
	 * 
	 * @param ip 请求连接的ip地址
	 * @param port 端口号
	 * @param suffix 下载资源的后缀名 
	 * 
	 * */
	public void chunkedRaw(String host, int port, String path, String query, String suffix){
		try (Socket socket = new Socket(host, port)) {
			socket.setSoTimeout(TIMEOUT);   // 设置超时时间
			// 获取输出流和输入流
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			// 使用输出流发送请求数据
			InputStream input = new BufferedInputStream(socket.getInputStream());
			
			StringBuilder msgBuilder = new StringBuilder();
			// 构造简单的请求报文,这里报文非常简单
			msgBuilder.append("GET").append(BLANK)                                  // 但是 1.0 又不支持 chunked 编码,所以还是需要 HTTP/1.1
			.append(path+"?"+query).append(BLANK).append("HTTP/1.1").append(CRLF)   // HTTP/1.1 默认是持久连接,导致我的程序卡死超时,我这里缓存老古董 HTTP/1.0
			.append("Host").append(":").append(BLANK).append(host).append(CRLF)
			.append("TE").append(":").append(BLANK).append("chunked").append(CRLF)
			.append("Connection").append(":").append(BLANK).append("close").append(CRLF)        // 关闭HTTP/1.1的默认的持久连接,防止我的程序卡死!
			.append("User-Agent").append(":").append(BLANK).append("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
					+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36").append(CRLF)
			.append(CRLF);
			// 请求报文转成字符串
			String msg = msgBuilder.toString();
			
			// 查看请求报文格式
			System.out.println(msg);
			
			// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
			// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
			byte[] request = msg.getBytes(Charset.forName("UTF-8"));
			// 发送请求报文,这里其实可以一步到位的,但是为了表达清晰,还是分步来写
			output.write(request);			
			output.flush();  // 刷新输出流,不然未发送请求,导致无法接收到响应

			// 获取整个响应报文,这里使用 HTTP/1.1 它是默认的长连接,所以关闭
			// 其实是服务器主动的。我这里功能太简单了,无法实现关闭的功能!
			String fileName = UUID.randomUUID().toString() + suffix;
			try (OutputStream outputFile = new BufferedOutputStream(new FileOutputStream(new File("D:/DBC/socketPic", fileName)))) {
	        	int len = 0;
	        	byte[] b = new byte[1024];
	        	
	        	// 返回 -1 说明连接断开了,但是由于使用了持久连接,导致连接不会被服务器主动关闭。
	        	// 这里读取数据实际上会卡住,知道程序超时异常而退出当前的循环!
	        	while ((len = input.read(b)) != -1) {
	    			outputFile.write(b, 0, len);
	        	}
			}
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("网络资源已经下载完成了!");
	}

这里是部分代码,不过我写了很多注释,可以先理解一下。这里是通过告诉服务器使用分块传输发送响应报文,然后完整的接受它。
使用分块传输和短连接
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

说明: 这里的报文已经是分块传输了,注意看第16行即是报文的第一块,长度为:17c58。
报文的第4651行即是报文的最后一块,长度为0。这里的CRLF、LF都是控制字符,默认是不显示的,我使用了notepad++的查看所有字符功能。

使用分块传输和长连接
注释掉此行,再次运行。

.append("Connection").append(":").append(BLANK).append("close").append(CRLF) 

注: 这里超时是正常现象,因为使用了分块传输编码。所以客户端不会自动结束,我这里客户端逻辑很简单,它就是需要服务器主动关闭才行。如果有Content-Length首部,也是可以进行判断的。但是这里我也没有Content-Length首部,所以数据传输完了,就卡住了或者专业一点——程序阻塞了。

如果采用其它方式,其实也很麻烦。所以我干脆就关闭长连接,使用短连接即可达成目的了。
Connection: close 关闭默认的长连接即可,这里我不使用HTTP/1.0默认的短连接是因为它好像不支持给HTTP/1.0发送分块编码的内容。

在这里插入图片描述
在这里插入图片描述


说明:一开始两种方式下载的html文件大小不一样,总是差了几个字节。我以为是代码有问题,后来才发现,使用不同的首部,服务器响应首部也是不一样的。所以差的字节数就是首部的差异。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

响应报文中的实体

对于整个报文是程序需要关注的事情,但是用于只是关注报文中的实体部分而已。所以,现在的目标是从报文中解析处实体。我们先来对分块的报文进行一个直观的了解(我个人的理解)。

报文分块:
首先分块传输的首部和正常的报文首部是一样的,具体可以看上面的截图。然后是分块的部分,这里我们来看一个简化的模型:
在这里插入图片描述
一个块的结构:首先是16进制表示的长度,紧接着一个CRLF,然后就是对应长度的数据部分了,最后再以一个CRLF结尾。
若干个这样的块就构成了完整的数据体部分了,当然了还要考虑最后一个块的,它的长度是0。即中间的数据部分是不存在的,但是CRLF是有的。我们需要它来结束当前报文的读取,不然不知道结束也是没有用的。



所以,我的思路就是:
首先单独处理报文的首部(复用以前的代码,最近的博客中基本上都涉及到了它),然后对应分块报文中的每一块。首先读取出长度,并且读取完最后的CRLF,然后开始读取数据后最后的CRLF,然后将数据写入文件中,在写入文件中的时候,丢弃最后的两个字符CRLF。

代码

package com.dragon;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 分块编码
 * */
public class ChunkedTransfer {
	
	private static final int TIMEOUT = 10*1000; // 超时时间
	private static final String BLANK = " ";    // 空格
	private static final String CRLF = "\r\n";  // 回车换行符
	private static final char CR = '\r';
	private static final char LF = '\n';
	private static final int LENGTH = 1024;
	
	public static void main(String[] args) throws Exception {
		ChunkedTransfer transfer = new ChunkedTransfer();
		long start = System.currentTimeMillis();
		transfer.download("http://699pic.com/?sem=1&sem_kid=33480&sem_type=1&bd_vid=8358880229459218471", ".html");
		//transfer.download("http://api.qingyunke.com/api.php?key=free&appid=0&msg="+URLEncoder.encode("虎", "UTF-8"), ".txt");
		System.out.println("耗时:" + (System.currentTimeMillis()-start) + "ms");
	}
	
	/**
	 * 对于下面这条url:
	 * https://www.baidu.com/
	 * 
	 * 以下各值分别为(快速回答):
	 * schema/protocol
	 * host
	 * port
	 * path
	 * 
	 * 我发现这是一个有趣的问题,感兴趣的同学可以自测一下,
	 * 看看自己对于网络的一些基础知识的掌握。
	 * @throws UnsupportedEncodingException 
	 * */
	public void download(String urlstr, String suffix) throws Exception {
		System.out.println(urlstr);
		URL url = new URL(urlstr);
		String protocol = url.getProtocol();   // 协议
		String host = url.getHost();           // 主机
		String path = url.getPath();           // 路径
		int port = url.getPort();              // 端口,注意这里默认端口的话,它的值是 -1,所以需要自己默认端口号。
		String query = url.getQuery();         // 这里查询字符串是根据 ? 来确定的,所以这里 path只是path,不带后面的查询参数的!
	
		if (protocol.equals("http")) {
			this.chunked(host, port != -1 ? port : 80, path, query, suffix);
		} else {
			// 这里抛出一个异常,不支持这个协议
			throw new NotSupportProtocolException("只支持http协议!");
		}
	}
	
	/**
	 * 分块传输下载方法
	 * 
	 * @param ip 请求连接的ip地址
	 * @param port 端口号
	 * @param suffix 下载资源的后缀名 
	 * 
	 * */
	public void chunked(String host, int port, String path, String query, String suffix){
		try (Socket socket = new Socket(host, port)) {
			socket.setSoTimeout(TIMEOUT);   // 设置超时时间
			// 获取输出流和输入流
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			// 使用输出流发送请求数据
			InputStream input = new BufferedInputStream(socket.getInputStream());
			
			StringBuilder msgBuilder = new StringBuilder();
			// 构造简单的请求报文,这里报文非常简单
			msgBuilder.append("GET").append(BLANK)                            
			.append(path+"?"+query).append(BLANK).append("HTTP/1.1").append(CRLF)  
			.append("Host").append(":").append(BLANK).append(host).append(CRLF)
			.append("TE").append(":").append(BLANK).append("chunked").append(CRLF)
			.append("User-Agent").append(":").append(BLANK).append("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
					+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36").append(CRLF)
			.append(CRLF);
			
			// 请求报文转成字符串
			String msg = msgBuilder.toString();
			// 查看请求报文格式
			System.out.println(msg);
			
			// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
			// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
			byte[] request = msg.getBytes(Charset.forName("UTF-8"));
			output.write(request);			
			output.flush();  // 刷新输出流,不然未发送请求,导致无法接收到响应

			char ch;                           
			Map<String, String> headMap = new HashMap<>();  // 定义一个map结构,用于存储报文头部
			StringBuilder statusLine = new StringBuilder(); // 响应报文的首行
 			StringBuilder key = new StringBuilder();        // 存储键
			StringBuilder value = new StringBuilder();      // 存储键
			
			boolean flag = true;   // 定义一个标志标量,用于读取首行
			// 读取响应报文头部数据
			dragon:	while (true) {
				// 响应报文的首行需要单独处理
				if (flag) {
					while (true) {
						ch = (char) input.read();
						if (ch == '\n') {
							flag = false;  // 标志位置为假
							break;
						}
						statusLine.append(ch);
					}
				}
				// 读取一个 key
				while (true) {
					ch = (char) input.read();
					if (ch == ':') {
						break;
					}
					key.append(ch);
					// 如果 ch 是\n字符,那么就认为读取到了头部和数据部分的分隔符 \r\n,
					// 即头部已经读取完毕了,直接退出循环即可。
					if (ch == '\n') {  
						break dragon;
					}
				}
				// 读取一个键
				while (true) {
					ch = (char) input.read();
					if (ch == '\n') {
						break;
					}
					value.append(ch);
				}
				// 存储读取的key和value,并去除左右空格
				headMap.put(key.toString().trim(), value.toString().trim());
				// 清空 key 和 value,为下一次读取做准备。
				key.delete(0, key.length());   
				value.delete(0, value.length());
			}
			// 如果Transfer-Encoding首部不为空,即表示使用了分块传输
			headMap.forEach((k, v)->{
				System.out.println(k + " --> " + v);
			});
			System.out.println();
			// 对于 Chunked 方法的读取也不是什么难事,主要头脑够灵活就行了,多思考一下就行了!
	
			// 定义一个 ArrayOutputStream 对象,来存储读取到的数据。
			ByteArrayOutputStream out = new ByteArrayOutputStream(), store = new ByteArrayOutputStream();
			byte b;  //因为响应体通常是含有中文的,所以不能使用 char 来存储了,头部按理说也是可以的,但是严格的是不能含有中文的!
			boolean isEnd = false;
			while (!isEnd) {
				while ((b = (byte) input.read()) != LF) {  // 读取CRLF中的LF为止,直到把长度读取出来为止!
					if (b != CR) {
						out.write(b);
					}
				}
				String lenStr = out.toString("UTF-8");   // 这里使用完,需要清空 out,否则会导致字节数据累计,长度也不对了。
				out.reset();          // 重置流,便于再次使用!

				// 转化成 int 型的长度
				int length = Integer.parseInt(lenStr, 16); // 这里的长度是16进制的,不是十进制!
				System.out.println(lenStr + " --> " + length);
				if (length == 0) {  // 如果读取到的长度为0,说明可以准备退出循环了,
					isEnd = true;   // 但是不是立刻退出,还有一些控制字符(CRLF)需要读取完毕。
				}
				
				length += 2;  //读取分块结尾的CRLF,它是不计算入长度的。但是对于整个报文的解析不能忽略它!
				int hasRead = 0;
            	int len = 0;
            	byte[] data = new byte[LENGTH];
            	
            	// 如果需要读取的长度大于等于 LENGTH,那就读取 LENGTH,否则只是读取需要的长度!
            	int size = length >= LENGTH ? LENGTH : length;
            	while ((hasRead != length) && (len = input.read(data, 0, size)) != -1) {
        			hasRead += len;
        			int remain = length - hasRead;
        			if (remain == 0) {
        				store.write(data, 0, len-2);
        			} else {
        				store.write(data, 0, len);
        				size = remain >= LENGTH ? LENGTH : remain;
        			}		
            	}
//            	System.out.println("hasRead: " + hasRead);
//            	String fileName = UUID.randomUUID().toString() + suffix;
//    			store.writeTo(new FileOutputStream(new File("D:/DBC/socketPic", fileName)));
//    			store.reset();
			}
			String fileName = UUID.randomUUID().toString() + suffix;
			store.writeTo(new FileOutputStream(new File("D:/DBC/socketPic", fileName)));
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("网络资源已经下载完成了!");
	}
	
	/**
	 * 分块传输下载方法:获取完整的原始响应报文!
	 * @param ip 请求连接的ip地址
	 * @param port 端口号
	 * @param suffix 下载资源的后缀名 
	 * */
	public void chunkedRaw(String host, int port, String path, String query, String suffix){
		// 它的代码在最上面呢!
	}
		
	public class NotSupportProtocolException extends Exception {
		private static final long serialVersionUID = 1L;

		public NotSupportProtocolException(String msg) {
			super(msg);
		}
	}
}
难点说明
// 如果需要读取的长度大于等于 LENGTH,那就读取 LENGTH,否则只是读取需要的长度!
int size = length >= LENGTH ? LENGTH : length;
while ((hasRead != length) && (len = input.read(data, 0, size)) != -1) {
	hasRead += len;
	int remain = length - hasRead;
	if (remain == 0) {
		store.write(data, 0, len-2);
	} else {
		store.write(data, 0, len);
		size = remain >= LENGTH ? LENGTH : remain;
	}		
}

这部分是处理分块数据的主要逻辑,也是我认为最麻烦的了。因为这里不能按照每次读取固定长度的字节了,例如1024。由于两个块之间是紧挨着的,如果我还是读取1024字节,那么就会导致把后面的块的数据读取进来。如果发生了这种情况,那么程序就无法正常工作了。如果必须要判断,当前需要读取的字节数是否大于一个标准长度(例如1024),如果大于,那就继续读取一个标准长度,如果不足一个标准长度,那就按照当前块剩余的字节读取。这样才能独立的读取每一块的数据,而不至于产生干扰!


测试结果

下载测试:
在这里插入图片描述


下载的html文件,已经去除了分块的头和尾。
在这里插入图片描述


还可以单独下载每一块的数据
这里我每次读取一块就存将一块的信息存入一个文件,也是可以的,也很简单。只要将此处被注释的代码取消注释,没注释的代码注释掉即可。

//            	System.out.println("hasRead: " + hasRead);
//            	String fileName = UUID.randomUUID().toString() + suffix;
//    			store.writeTo(new FileOutputStream(new File("D:/DBC/socketPic", fileName)));
//    			store.reset();
			}
			String fileName = UUID.randomUUID().toString() + suffix;
			store.writeTo(new FileOutputStream(new File("D:/DBC/socketPic", fileName)));

在这里插入图片描述


在命令行中查看每一块文件的大小和总的文件的大小:

在这里插入图片描述

另一个测试

上面的文件太大了,而且由于是html文件,它似乎一直在更新,所以我的不使用分块下载的文件的大小和分块取出来的文件的大小,总是有一些细小的差距,不过文件本身是没有问题的!
这里我换一个URL进行测试,http://api.qingyunke.com/api.php?key=free&appid=0&msg=狗。
它就是返回一句话,结果比较直观,一眼就能看出来了。
在这里插入图片描述
将主函数改为如下,再次执行程序即可!

public static void main(String[] args) throws Exception {
		ChunkedTransfer transfer = new ChunkedTransfer();
		long start = System.currentTimeMillis();
		// transfer.download("http://699pic.com/?sem=1&sem_kid=33480&sem_type=1&bd_vid=8358880229459218471", ".html");
		transfer.download("http://api.qingyunke.com/api.php?key=free&appid=0&msg="+URLEncoder.encode("虎", "UTF-8"), ".txt");
		System.out.println("耗时:" + (System.currentTimeMillis()-start) + "ms");
	}

测试结果:
在这里插入图片描述
文件的内容:
在这里插入图片描述

原始的完整报文结构:
在这里插入图片描述

总结

这篇博客介绍了TE、Transfer-Encoding、Connection这几个首部,它们都是具有不同的特点的,并且经过实际的编程使用,相信你读完之后可以对其有一个深刻的认识。分块的知识其实是很有趣的,而且它也已经产生如此之久了,不去了解一下真是太可惜了!
推荐阅读书籍 《HTTP权威指南》,书中有更加相信的理论知识描述部分。当然了,我这里的代码也是很重要的,毕竟我也是花了时间写的,同时也改了不少bug!不过在错误中学习,在学习中错误,这是不可避免地。这些都是比较基础的知识,比较网络的话,对于我们应用软件程序员来说,掌握HTTP协议是最重要的!

PS:我并没有在实际抓包中看到TE首部,不知道是为什么?如果有知道的,可以在评论中告知我,谢谢。

补充 2021.3.14

某位博友想要看一下请求报文的分块传输,这里我就临时补充一个简单的示例吧!

服务端代码
这里简单采用SpringBoot创建了一个处理请求的Controller,只是接收并响应请求。

@RestController
public class TestController {
	
	@PostMapping("/test")
	public String test(@RequestBody String text) {
		System.out.println("接收到的客户端请求:" + text);
		return "I love you yesterday and today!";
	}
	
}

客户端代码

package test;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Test {
	
	private static final int TIMEOUT = 10*1000; // 超时时间
	private static final String BLANK = " ";    // 空格
	private static final String CRLF = "\r\n";  // 回车换行符
	private static final char CR = '\r';
	private static final char LF = '\n';
	private static final int LENGTH = 1024;
	
	public static void main(String[] args) throws IOException {
		String host = "127.0.0.1";
		int port = 8000;
		String path = "/test";
		
		try (Socket socket = new Socket(host, port)) {
			socket.setSoTimeout(TIMEOUT);   // 设置超时时间
			// 获取输出流和输入流
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			// 使用输出流发送请求数据
			InputStream input = new BufferedInputStream(socket.getInputStream());
			
			StringBuilder msgBuilder = new StringBuilder();
			// 构造简单的请求报文,这里报文非常简单
			msgBuilder.append("POST").append(BLANK)                            
			.append(path).append(BLANK).append("HTTP/1.1").append(CRLF)  
			.append("Host").append(":").append(BLANK).append(host).append(CRLF)
			.append("Transfer-encoding").append(":").append(BLANK).append("chunked").append(CRLF)   // 分块传输
			.append("User-Agent").append(":").append(BLANK).append("Alfred").append(CRLF)           // 自定义UA
			.append(CRLF);
			
			// 请求报文头转成字符串
			String msg = msgBuilder.toString();
			// 查看请求报文格式
			System.out.println(msg);
						
			String[] messages = new String[] {
					"I love you yesterday and today!",
					"Time and tidy wait for no man.",
					"有一美人兮,见之不忘。一日不见兮,思之如狂。凤飞翱翔兮,四海求凰。无奈佳人兮,不在东墙。",
					""   // 结束信息,其长度为0,表示数据已经发送完毕了。
			};
			
			// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
			// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
			byte[] request = msg.getBytes(StandardCharsets.UTF_8);
			output.write(request);			
			output.flush();  // 刷新输出流,不然未发送请求,导致无法接收到响应
			
			for (String message : messages) {
				int size = message.getBytes(StandardCharsets.UTF_8).length;               // 传输部分内容的长度。
				String len = Integer.toHexString(size);  // 报文中的长度,为16进制
				String content = len + CRLF + message + CRLF;  // 发送的信息格式为:16进制长度 + CRLF + message + CRLF
				request = content.getBytes(StandardCharsets.UTF_8);
				
				output.write(request);    // 写入
				output.flush();           // 刷新
				
				System.out.println(content);
			}
					
			// 简单接收输入
			byte[] in = new byte[LENGTH];
			int length = input.read(in);
			System.out.println("接收到的服务端响应:" + new String(in, 0, length));
			System.out.println("执行结束了!");
		}
	}
}

测试截图
注意:需要先启动服务端,再启动客户端。

客户端
在这里插入图片描述
服务器端
在这里插入图片描述
说明:请求报文中的信息采用了分块传输的方式,但是服务器端也是可以正常接收的。分块传输是HTTP协议中的一种传输方式,特别适用于在不了解具体发送报文的大小的情况下使用

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值