Ⅰ 网络层HTTP的概述
  1. 首先是构造场景。在一个闷热的下午,你打算打开浏览器百度一下广州今晚是不是又要降温了。在这里,客户端是你的电脑,而服务端则是百度提供服务的电脑。当你打开浏览器输入网址`www.baidu.com`,并按下回车键后,霎——,就能看到了百度标志性的搜索框了。
  2. 这里发生了什么?首先要知道的是此时此刻,浏览器上展示的网页是由一个html文件渲染的结果,这个html文件是从百度的服务端发送的,显然,获取网页的过程是从按下回车键开始的。浏览器发送了一个请求报文`request`,表示你需要访问这个网页;然后客户端收到后,给浏览器发送了一个响应报文`response`,把网页给寄回来了。
  3. 一个请求报文会长什么样呢?就像是在学校通用的办事申请表格,有统一的样式。请求报文由请求行、首部行和实体主体三部分组成,前两者分别代表了干什么、怎么干,实体主体则是用于有需要将信息发送给服务器的时候,比如在注册账号的时候服务器需要知道你的用户信息。请求行包括了方法,表示希望要做的事,常见的GET方法表示希望获取网页,POST方法则表示希望往服务器发送数据
  4. 响应报文有状态行,首部行和实体主体组成。响应报文与请求报文之间只相差了一个状态行,这是用来告诉浏览器其请求报文是否得到了响应,其中包括了状态码和状态信息。404就是常见的状态码了,表示文件找不到了。
  5. 请求报文和响应报文都属于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);