前言: 前几天有同学在postman中发送get请求时,把请求参数放入了请求体中。然后后端使用了@RequestBody来接收请求参数,它确实是可以这样用的!postman测试API的响应也是正常的。但是前端使用get方式传递参数,却报 400 错误。所以最终还是把@RequestBody换成了@RequestParam。或者也可以把请求方法改成post。不过第一次见到get请求方式这样使用,我感觉很古怪。因为按照我以前的学习来看,它是不行的,至少不符合规范。如果使用html的表单,是无法做到的,所以前面的前端发送的请求无法被正确接收,导致了400,但是在postman中,它目前是可行的(以前的版本是不支持的)。也就是说,如果我们不是使用网页(浏览器)来发送get请求那它应该就是可以使用的。 但是这样的解释,不够具体,我们可以利用所学知识,来深入理解为什么?不过在那之前,先来了解一下基础知识吧!
HTTP请求报文
HTTP请求报文的格式如下,不同请求方式的报文都满足这一格式,但是具体上会有一些差异。
这里提一下,为什么要有结构呢?这实际上是协议规定的,因为这样程序才可以解析,如果一个没有规律的报文,尽管可以发送,但是那是无法正确解析的。
HTTP请求方式主要有:get、post、put、delete等,这里以get和post最为常用。
我们这里就只简单介绍get和post的一些特点:
get方式:
1.get方式是发送请求得到实体(得到服务器的数据),它应该响应一个资源。
2.get方式请求的参数会跟在URL后面,以?来分隔URL和参数,如果有多个参数,那么参数之间使用&连接。并且,整个URL加参数需要使用url encode编码方式编码,get请求方式的参数可以在浏览器的输入框看到。
3.get请求方式的url长度有所限制,有人说是2KB,有人说是4KB。但是这个只是浏览器的规定(因为浏览器的输入框的长度是有限制的,它不可能非常长的。),所以实际上,如果不经过浏览器来发送get请求,应该是没有这个限制的,但是应该也没有人会发送一个100M的请求参数吧,哈哈。
这应该是比较权威的解释了:Http get方法提交的数据大小长度并没有限制,Http协议规范没有对URL长度进行限制。目前说的get长度有限制,是特定的浏览器及服务器对它的限制。
post方式:
1.post方式是向服务器发送请求,上传实体。
2.post方式的请求数据是放入请求体中的,所以用户无法看到,通常认为它比较安全。(但是这也是针对浏览器的,如果不使用浏览器,即使是get请求用户也是看不到的请求参数的。)
3.post请求方式的长度也是没有限制的,但是它实际受限制于服务器的性能限制。
报文到底长啥样?——有一美人兮,见之不忘。
前面光是理论的介绍,还是很难让人认识到具体的区别,顶多就是死记硬背一下那几点总结了。但是计算机网络是分层的,站在HTTP层看HTTP并不能看到所有的信息,让我们再往下一层,站在TCP的角度来看吧。让我们来一窥它的真面目吧!
提供一个demo
package dragon.net;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
public class Server {
private final String BLANK = " "; //空格符
private final String CRLF = "\r\n"; //回车换行符
public static void main(String[] args) {
Server server = new Server();
server.start();
}
public void start() {
System.out.println("服务启动。。。");
try (ServerSocket server = new ServerSocket(8899)){
while (true) {
Socket client = server.accept();
InputStream in = new BufferedInputStream(client.getInputStream()); //获取HTTP请求报文
byte[] b = new byte[5*1024]; //这里因为网络流读取不是以 -1 来判断结束,处理比较复杂,
int len = in.read(b); //所以我使用一个大点的字节数组,直接读取整个报文
//不对请求报文进行处理,直接响应一个固定的数据。
OutputStream out = new BufferedOutputStream(client.getOutputStream()); //发送HTTP响应报文
Charset UTF_8 = Charset.forName("UTF-8"); //使用 UTF-8 作为字符集
String json = "{\"name\":\"龙林夕\", \"words\":\"I love you yesterday and today\"}";
byte[] responseBody = json.getBytes(UTF_8);
StringBuilder header = new StringBuilder();
byte[] responseHeader = header.append("HTTP/1.0").append(BLANK).append(200).append(BLANK).append("OK").append(CRLF) // 响应头部
.append("Server:"+"CrazyDragon").append(CRLF)
.append("Date:").append(BLANK).append(this.getDate()).append(CRLF)
.append("Content-Type:").append(BLANK).append("application/json").append(CRLF)
.append("Content-Length:").append(BLANK).append(responseBody.length).append(CRLF).append(CRLF)
.toString()
.getBytes(UTF_8);
//发送响应报文
out.write(responseHeader);
out.write(responseBody);
out.flush(); //刷新输出流。
//打印请求报文
System.out.println(new String(b, 0, len, UTF_8));
//关闭客户端连接
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("服务结束!");
}
//获取时间
private String getDate() {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT")); // 设置时区为GMT
return format.format(date);
}
}
说明: 这个程序启动之后,通过浏览器访问 localhost:8899 即可看到下面图的结果,就会返回一个响应报文,报文数据是一个json,并打印出请求报文。
运行结果
浏览器上面的结果:(注意get请求的参数)
控制台上面的结果:
这里可以看到有两个请求,但是我们不需要管第二个,它是浏览器自动发送的,目的是请求网站的图标。 第一个请求是我们需要关注的,可以看到整个完整的HTTP报文。其实这里不太能看出来它的结构,可以使用notepad++来打开(显示所有字符)观看。
注意: 划线处,我访问的路径是 / ,并且请求参数是在URL后面的,但是它已经被 url 编码了,所以看起来很奇怪,但是只要解码就能看出来很上面的请求参数是一致的。
在打印报文下面添加如下语句,并且修改报文的路径(确保该文件夹存在),运行即可在相应路径下看到报文,使用notepad++打开,并且显示全部字符可看到报文的结构,注意那个黄色的小点是表示空格。
//把报文写入文件,并且使用追加的方式,否则请求图片的报文会覆盖我的请求参数的报文
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("D:/DragonFile/DBC/msg.txt", true), UTF_8))) {
writer.write(new String(b, 0, len, UTF_8));
}
注意:浏览器发送的get请求是没有请求体的,所以最后一个 CRLF 之后报文就结束了。
让我们跳出浏览器的规范,使用其它方式发送请求
现在,我要使用postman来发送请求了,但是不知为何,post发送的请求,我无法接收,如果我尝试打印请求,那么会报一个错误,程序也就崩溃了。
这个似乎表明,我并没有发送请求!但是当我注释打印请求的代码段时,它就可以工作正常了。那这样我就看不到报文了!我已经写到这里了,没有想会遇到这个问题,我怎么能放弃呢!幸好我还有最后一招!—— 抓包。只要请求发送出去,必有数据包!抓住它,不久一目了然了嘛!
因为最近准备抓csdn的blink,学着试了一下抓包,但是望着抓到的4000多个包,只剩下一脸懵逼!app抓包这个东西,似乎不是那么简单的,我也不会分析这些网络包,完全浪费了我一天的时间。 但是我简单尝试了使用 Fiddler抓包。使用 Fiddler 抓包的方法,可以去看一篇博客,基本上就可以掌握最基础的抓包方法了,这里我也是刚学,就不多介绍了。似乎也可以通过HttpClient来做到,但是这里还是软件使用起来比较顺手,尽管因为上面那个问题我差点放弃了。
Fiddler 抓包
但是,我发现只要我打开了Fiddler抓包,我就可以正常使用 postman 了,这是一个问题,不过暂时我不需要管它。
所以,Fiddler 只管开启抓包,我还是使用postman发送请求来打印报文。
抓包软件的结果,可以发现前三个请求路径有参数,body里面也有数据,总之,它不是正常的请求参数,至少web上是很少见的!
postman 测试
注意,这里的前提是 Fiddler 开启抓包,虽然原因是什么暂时还不清楚。
1.get方式,把参数加在请求体的位置上。
2.post方式,由于我同时选择了Params和Body两个选项,所以发送的报文居然变成了get和post的混合形式。
3.post方式,去掉Body部分后,post请求参数加在url后面的形式。注意最下面的 Content-Length:0,虽然没有请求体了,但是它还是要有的,只是值为0。
说明
可以发现,这似乎全部乱了套了,但是它是可以的,web包括的范围不止浏览器那一块,应该也包含其它领域,毕竟使用HTTP协议的地方还是很多的。感觉今天的一番探索之后,收获了很多,但是我们可以更进一步,发送更加自定义的报文吗?
更进一步
下面我提供了一个小的demo,通过它和前面的那个demo,一起来进行简单的测试。
这里我把这个请求方法的名字给改了,然后经过测试,也是可以打印结果的。但是这个明显是不规范的,因为不可能有这种请求方法的,哈哈!
提供一个demo
package dragon.net;
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.Charset;
public class Client {
private static final String BLANK = " "; //空格符
private static final String CRLF = "\r\n"; //回车换行符
public static void main(String[] args) {
try (Socket socket = new Socket("127.0.0.1", 8899)) {
InputStream in = new BufferedInputStream(socket.getInputStream()); //获取输入流
OutputStream out = new BufferedOutputStream(socket.getOutputStream()); //获取输出流
StringBuilder msg = new StringBuilder();
msg.append("FUCK").append(BLANK).append("/name=%E9%BE%99%E6%9E%97%E5%A4%95").append("HTTP/1.1").append(CRLF)
.append("User-Agent").append(": ").append("CrazyDragon").append(CRLF)
.append("accept").append(": ").append("*/*").append(CRLF)
.append("Host").append(": ").append("localhost:8899").append(CRLF)
.append("Content-Type").append(": ").append("application/x-www-form-urlencoded").append(CRLF)
.append("Content-Length").append(": ").append("32").append(CRLF)
.append(CRLF)
.append("name=%E9%BE%99%E6%9E%97%E5%A4%95");
Charset UTF_8 = Charset.forName("UTF-8");
byte[] data = msg.toString().getBytes(UTF_8);
out.write(data);
out.flush(); //刷新输出流
byte[] b = new byte[5*1024]; //我这个值其实设置的偏大,对于文本数据来说。
int len = in.read(b);
System.out.println(new String(b, 0, len, UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结
HTTP报文从TCP的角度来说,只是一串字节流。TCP—传输控制协议,提供的是面向连接、可靠的字节流服务。既然是字节流,虽然协议是那样规定的,但是确实是可以有一些不常规的使用方法。
虽然上面那个自定义一定是非法的,但是感觉很有意思!get和post两种方式是我们最常使用的,所以形成了一些特定的用法,如get方式不携带请求体。但是我刚才查阅资料发现,似乎协议也没规定不能这样用啊!但是还是保持默认的规则比较适合,否则可能会出现一些奇怪的问题。正如我们开头所提到的那个问题。
协议是灵活的,你完全可以自己定义一套简单的协议。反正基于TCP层面,也不止HTTP一种协议,它也不一定是最好的,只是对于某些场景很适合。