网络1 HTTP的概述

Ⅰ 网络层HTTP的概述

​ 首先是构造场景。在一个闷热的下午,你打算打开浏览器百度一下广州今晚是不是又要降温了。在这里,客户端是你的电脑,而服务端则是百度提供服务的电脑。当你打开浏览器输入网址www.baidu.com,并按下回车键后,霎——,就能看到了百度标志性的搜索框了。

​ 这里发生了什么?首先要知道的是此时此刻,浏览器上展示的网页是由一个html文件渲染的结果,这个html文件是从百度的服务端发送的,显然,获取网页的过程是从按下回车键开始的。浏览器发送了一个请求报文request,表示你需要访问这个网页;然后客户端收到后,给浏览器发送了一个响应报文response,把网页给寄回来了。
在这里插入图片描述

​ 一个请求报文会长什么样呢?就像是在学校通用的办事申请表格,有统一的样式。请求报文由请求行、首部行和实体主体三部分组成,前两者分别代表了干什么、怎么干,实体主体则是用于有需要将信息发送给服务器的时候,比如在注册账号的时候服务器需要知道你的用户信息。请求行包括了方法,表示希望要做的事,常见的GET方法表示希望获取网页,POST方法则表示希望往服务器发送数据

​ 响应报文有状态行,首部行和实体主体组成。响应报文与请求报文之间只相差了一个状态行,这是用来告诉浏览器其请求报文是否得到了响应,其中包括了状态码和状态信息。404就是常见的状态码了,表示文件找不到了。

​ 请求报文和响应报文都属于HTTP的报文,那么为什么网络连接使用了HTTP协议,则是因为Web的应用层协议就是HTTP

在这里插入图片描述

Ⅱ 探索

​ 最近在看网络的书,看到了其使用Socket进行网络连接的部分,想到了之前在第一行代码的书中也涉及了网络连接的部分,然后重新读了一下,发现是用HttpURLConnection实现的,所以试着看了它的源码。

​ 这段代码最终运行结果是将get_data.json的内容输出在terminal里面,服务端和客户端都是自己的电脑,使用Windows自带的IIS作为服务器。

HttpURLConnection connection = null;
BufferedReader reader = null;
try {
    URL url = new URL("http://127.0.0.1/get_data.json");
    connection = (HttpURLConnection)url.openConnection();
    connection.setRequestMethod("GET");
    connection.setConnectTimeout(8000);
    connection.setReadTimeout(8000);
    InputStream in = connection.getInputStream();
    reader = new BufferedReader(new InputStreamReader(in));
    StringBuilder response = new StringBuilder();
    String line;
    while((line = reader.readLine())!=null){
        response.append(line);
    }
    System.out.println(response.toString());
} catch (Exception e) {
    e.printStackTrace();
} finally{
    if(reader!=null){
        try {
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    if(connection!=null)connection.disconnect();
}

​ 网络连接的代码以HttpURLConnection和BufferedReader作为主角,前者负责指挥连接,后者则作为数据流,从Socket读入数据。不难看出,代码从InputStream in = connection.getInputStream()才开始向网络获取数据,所以直接在这个位置设置断点,并进行debug。

​ 代码首先进入到了HttpURLConnection.getInputStream()方法,注意到这个HttpURLConnection是在包sun.net.www.protocol.http下的,它继承了同名抽象类java.net.HttpURLConnection(一开始我被这两个类搞混了)。在本次debug过程下,在这个方法中并没有做什么事,随后便进入到了getInputStream0()

getInputStream0()占了400多行,说明了它的戏份很足,重头戏被一个try-catch语句包住了,而try代码块中则是一个循环,循环条件是redirects < maxRedirects,redirect是重定向的意思,不关心。同样值得注意的是全方法唯一的return语句在循环内,但在执行return语句之前,会先执行finally代码块,这是try-catch语句的特性。

try {
    do {
		// ...
        return inputStream;
    } while (redirects < maxRedirects);
    throw new ProtocolException("Server redirected too many times ("+ redirects + ")");
} catch (RuntimeException e) {
    // ...
} catch (IOException e) {
    // ...
} finally {
    // ...
}

​ 接下来把目光投到getInputStream0()的循环内,我按照个人理解圈定了一些比较重要的代码,准则是本次debug过程中能被执行的方法,不包括各种标志位。说实话,这么长一段代码还是看得我脑壳痛,但往下拉,看到了200、203等一系列数字——巧了,这是状态码。所以我打算从状态码的获取入手分析,进入到getResponseCode()的内部查看。最终确定了状态码是从responses对象获取的,显然responses对象就是响应报文,在IDE中的变量表中,也可以看到详细的响应报文内容。

respCode = getResponseCode();

// ...
if (respCode == 200 || respCode == 203 || respCode == 206 || respCode == 300 || respCode == 301 || respCode == 410) {
    // ...
}

​ 接下来要做的是往getInputStream0()的上游追踪,找到responses对象是什么时候填入数据的。这个过程同样可以通过观察IDE的变量表做到,最终发现填充过程是在http.parseHTTP(responses, pi, this)执行后完成的,所以点进去看一下是什么名堂

if (!checkReuseConnection())
    connect();
// ...
ps = (PrintStream)http.getOutputStream();

if (!streaming()) {
    writeRequests();
}
http.parseHTTP(responses, pi, this);
// ...
inputStream = http.getInputStream();

​ 这里的http对象属于HttpClient类,点进来方法后,看到了前几步都在准备BufferedInputStream,那么看来是在最后一步才把流填到responses内了。那么问题来了,在parseHttp()这个方法执行的时候,究竟是已经网络交换数据结束了还是没有?从函数名看,parse是解析的意思,所以这里先盲猜一手还没有。

serverInput = serverSocket.getInputStream();
if (capture != null) {
    serverInput = new HttpCaptureInputStream(serverInput, capture);
}
serverInput = new BufferedInputStream(serverInput);
return (parseHTTPHeader(responses, pi, httpuc));

​ 继续往上游看,看到了PrintStream类。之前在Head First Java里面了解过PrintWriter,它是用来向通信对方发送数据的,所以很难不把两者联想到一起。在java文档上搜索,可以看到它们的区别:PrintStream用于将文本转换为bytes,而PrintWriter则用于将文本转换为characters。PrintStream为程序和Socket之间架起了桥梁。

// Head First Java里的例子
Socket chatSocket = new Socket("127.0.0.1",5000);
PrintWriter.writer = new PrintWriter(chatSocket.getOutputStream());
writer.println("message to send");
writer.print("another message");

writeRequests()显然这个方法是用来向服务端发送请求报文request的,这个方法提供的注释也是这么写的。在这个方法内部,首先是填充requests的数据。requests和responses同样是MessageHeader类的对象,可以在IDE的变量表中看到其填充的过程。按照注释的说法,requests对象最终会给到PrintStream,之后应该会给到Socket发送出去
在这里插入图片描述

/* adds the standard key/val pairs to reqests if necessary & write to given PrintStream
 */
private void writeRequests() throws IOException {//...

​ 在HttpClient.writeRequests()中,看到了发送的过程

public void writeRequests(MessageHeader head,
                          PosterOutputStream pos) throws IOException {
    requests = head;
    requests.print(serverOutput);
    poster = pos;
    if (poster != null)
        poster.writeTo(serverOutput);
    serverOutput.flush();
}

​ 这大概就是网络连接的全过程了吧。其实之前还想着可能会看到等待获取响应报文的过程的,但是可能看的深度不够,所以没有找到这个过程。在使用HttpURLConnection,先是用OutputStream与Socket连接,发送requests,然后用InputStream与Socket连接,获取responses,最后获得了想要访问的文件。不过我对这一次的探索还是满意的,毕竟我在计算机网络的书中找到了TCP连接的代码,在这次源码探索中都遇到了。

// 连接
Socket clientSocket = new Socket("hostname", 6789);
DataOuputStream outToServer = new DataOutputStream(
	clientSocket.getOutputStream();
);
BufferedReader inFromServer = new BufferedReader(
	new InputStreamReader(
        clientSocket.getInputStream());
);
// 用户输入与结果输出
String sentence;
String modifiedSentence;
BufferedReader inFromUser = new BufferedReader(
	new InputStreamReader(System.in);
);
sentence = inFromUser.readLine();
outToServer.writeBytes(sentence+'\n');
modifiedSentence = inFromServer.readLine();
System.out.println(modifiedSentence);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值