每天我们都会使用浏览器去访问一些网站页面,但是每次访问时,你是否会想知道,浏览器和服务器到底做了些什么,才让你看到浏览器呈现给你的这些具有样式排版,乃至动画的页面?
本文需要知识前提:HTTP协议了解,TCP/IP协议了解,Socket编程了解。
你可能会回答,浏览器向服务器端发送HTTP
请求,服务器回复HTTP
请求,再经过浏览器内核的渲染和javascript
引擎的解释执行,展现了这一切
可以看到浏览器进行了一次HTTP
请求。
服务器返回的HTTP
报文其实就是一些标签和javascript
代码。
最后浏览器渲染给我们看到的就是一个美观的页面。
对!但并不彻底。
我们知道HTTP
协议是基于TCP
协议的上层协议,那么HTTP
请求说到底,也就是一次TCP
请求。
Java中Socket编程可以进行TCP连接,那么是否使用Java可以模拟HTTP请求的过程呢?
答案是肯定的:熟悉的同学可能会想到,Java中内置的java.net.HttpURLConnection
和java.net.URL
类就可以实现对基于HTTP
协议的URL
进行访问。
比如:
URL url = new URL("https://www.csdn.net/");
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
/**
* URL对象调用openConnection()方法以后并不是直接去创建Socket进行通信
* 而是调用了getContent()方法以后(以及其他的各种获得Header方法等等)
* 所以要在真正建立Socket通信访问服务器80端口以前setRequestMethod
*/
httpURLConnection.setRequestMethod("GET");
BufferedInputStream bufferedInputStream = new BufferedInputStream(httpURLConnection.getInputStream());
// 使用字符流类读取内容,注意编码从二进制转为字符时使用 UTF-8
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream, "utf-8"));
String tem;
while((tem = bufferedReader.readLine())!=null) {
System.out.println(tem);
}
bufferedReader.close();
这段代码会打印出CSDN首页的HTTP源码,也就是说这就是一个最简单的网页爬虫。
但是这还不够,我们想知道,如果直接使用TCP连接去访问对端服务器的80端口会怎么样?
这里我们首先在虚拟机上搭建了HTTP
服务器环境,使用了Nginx + Ubuntu Server
版本。
Socket client = new Socket();
InetSocketAddress inetSocketAddress = new InetSocketAddress("192.168.194.129", 8080);
// 建立TCP连接,虚拟机的地址为192.168.194.129
// Nginx监听的端口设置为8080
client.connect(inetSocketAddress, 1000);
String request = "GET /index.html HTTP/1.1\r\n"+
"Host: 192.168.194.129:8080\r\n";
PrintWriter pWriter = new PrintWriter(client.getOutputStream(),true);
// 这里使用println()方法是因为 HTTP 协议在报头和报文之间,有一行空行
// 如果少了这一行空行,服务器是无法解析报文的,会出现 400 错误
pWriter.println(request);
client.close();
然后使用Socket
编程对服务器发起一次TCP
请求。
这时我们查看Nginx
的访问日志,会发现有一个GET
请求,请求页面是/index.html
。也就是说,我们模拟浏览器发送了一个HTTP
请求。
此时我们需要关注的是
String request = "GET /index.html HTTP/1.1\r\n"+
"Host: 192.168.194.129:8080\r\n";
这是一段HTTP request
报文,可以看到HTTP
请求的方法是GET
方法,请求的路径是/index.html
,使用的协议是HTTP1.1
。然后我们可以自己添加Header
一般浏览器发送HTTP
请求的时候,会自动构造报文,而且会添加Header
其实这些信息,就是我们通过TCP
连接发送给服务器的信息,无论是HTTP
报文,还是HTTP Request Header
,还是发送的消息,最终都是通过Sokcet
传输的TCP
连接。
了解清楚了这些以后,我们就可以自己构造HTTP报文,然后通过TCP连接的形式发送。
那么我们再模拟一下浏览器接收HTTP报文的过程。
Socket client = new Socket();
InetSocketAddress inetSocketAddress = new InetSocketAddress("192.168.194.129", 8080);
// 建立TCP连接,虚拟机的地址为192.168.194.129
// Nginx监听的端口设置为8080
client.connect(inetSocketAddress, 1000);
String request = "GET /index.html HTTP/1.1\r\n"+
"Host: 192.168.194.129:8080\r\n";
PrintWriter pWriter = new PrintWriter(client.getOutputStream(),true);
pWriter.println(request);
String tem;
// 这里要注意二进制字节流转换为字符流编码要使用UTF-8
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream(), "utf-8"));
while((tem = bufferedReader.readLine())!=null) {
System.out.println(tem);
}
client.close();
可以看到控制台打印了以上内容。
我们在回头来看浏览器访问得到的HTTP报文,是不是一模一样呢(除了时间)
看到现在,再回头想想我们一开始使用的HttpURLConnection
类,其实这个类和其依赖的关联类,最终还是需要使用Socket
编程实现HTTP
报文的收发,只不过他内置了一些简单地HTTP Header
和一些方便的方法(比如自动使用UTF-8
解码流)。