深入理解http协议原理 - Go语言中文社区

深入理解http协议原理


简述

对于Android开发来说,HTTP是网络开发中最为重要的、使用频率最高的手段,也是面试常问到的面试题。因此,深入了解HTTP是必备技能,只有了解它的基本原理才能够更好的运用。

HTTP网络请求原理

HTTP是一种应用层协议,它通过TCP实现了可靠的数据传输,能够保证数据的完整性、正确性。对于移动开发而言,网络应用基本上都是C/S架构。客户端通过向服务器发起特定的请求,服务器返回结果,客户端解析结果,再将结果展现在UI上。详细交互的流程分为如下几步:

  1. 客户端执行网络请求,从URL中解析出服务器的主机名;
  2. 将服务器的主机名转换成服务器的IP地址;
  3. 将端口号从URL中解析出来;
  4. 建立一条客户端与Web服务器的TCP连接;
  5. 客户端通过输出流向服务器发送一条HTTP请求;
  6. 服务器向客户端回送一条HTTP响应报文;
  7. 客户端从输入流获取报文;
  8. 客户端解析报文,关闭连接;
  9. 客户端将结果显示在UI上。

有人可能对上面流程有所疑惑,在开发过程用到的网络框架中从来都只要请求方式(post、get、put、delete等)和传递URL、参数,没有上述流程中的1、2、3步,而且IP+端口号是SOCKET通信俩大必备要素;如果你有如此疑惑,你就该仔细的看看这篇文章,首先给你解惑,我们的后台服务一般不会直接暴露IP和端口号,而是申请域名(如:www.baidu.com),所以我们传递URL首先进入域名服务器,在域名服务器中为我们进行了1-3步操作;HTTP确实是socket实现的。

模拟HTTP过程

在模拟http交互的过程前我们需要了解HTTP报文格式。不同的请求方式,它们的请求格式可能是不一样的,请求格式就是所谓的报文格式。但是通常来说一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成的,如下图:

客户端(post请求)

//这里不展示域名解析,IP和PORT都为解析后的
public class Client {

    //URL通过域名解析后的IP
    public String ip;
    //URL通过域名解析后的PORT,即服务器的端口
    public int port;
    //请求参数
    private Map<String,String>mParamsMap = new HashMap<>();
    //客户端Socket
    Socket mSocket;

    public Client(String ip,int port){
        this.port = port;
        this.ip = ip;
    }

    public void addParam(String key,String value){
        mParamsMap.put(key, value);
    }

    public void execute(){
        try{
            //创建Socket连接
            mSocket = new Socket(ip,port);
            PrintStream outputStream = new PrintStream(mSocket.getOutputStream());
            BufferedReader inputStream = new BufferedReader(new InputStreamReader(
                    mSocket.getInputStream()
            ));
            final String boundary = "my_boundary_123";//用来设置请求边界
            //写入header
            writeHeader(boundary,outputStream);
            //写入参数
            writeParams(boundary,outputStream);
            //等待返回数据
            waitResponse(inputStream);
        }catch (UnknownHostException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                if (mSocket!=null){
                    mSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //代码省略
}

Client构造函数中传入请求的URL地址经DNS解析后成IP+PORT,然后用户调用addParam函数添加普通的文本参数,当用户设置好参数之后就可以通过execute函数执行该请求,在execute函数中客户端首先创建Socket连接,目标地址就是用户执行的URL。连接成功之后客户端就可以获取到输入、输出流,通过输出流客户端可以向服务端发送数据,通过输入流则可以获取服务端返回的数据。之后依次写入header、请求参数、最后等待Response的返回。

我们将header固定做出如下设置:

    /**
     *
     * 模拟header
     */
    private void writeHeader(String boundary,PrintStream outputStream){
        outputStream.println("POST /api/login/ HTTP/1.1");
        outputStream.println("content-length: 123");
        outputStream.println("Host:"+this.ip+":"+this.port);
        outputStream.println("Content-Type:multipart/form-data;boundary = "+boundary);
        outputStream.println("User-Agent:android");
        outputStream.println();//空行
    }

然后,我们将mParamsMap中的所有参数通过输出流传递给服务器:

/**
     *
     * 输入参数格式:
     *  --boundary
     *  Content-Disposition:form-data;name = "参数名"
     *  空行
     *  参数值
     */
    private void writeParams(String boundary,PrintStream outputStream){

        Iterator<String> paramsKeySet = mParamsMap.keySet().iterator();
        while (paramsKeySet.hasNext()){

            String paramName = paramsKeySet.next();
            outputStream.println("--"+boundary);
            outputStream.println("Content-Disposition:form-data;name = "+paramName);
            outputStream.println();//空行
            outputStream.println(mParamsMap.get(paramName));
        }
        //整个参数输入结束符
        outputStream.println("--"+boundary+"--");
    }

等待服务器返回结果:

 //将返回的数据输入到控制台
    private void waitResponse(BufferedReader inputStream) throws IOException {
        System.out.println("请求结果");
        String responseLine = inputStream.readLine();//一行一行的读
        while (responseLine == null || !responseLine.contains("HTTP")){
            responseLine = inputStream.readLine();
        }
        //输入Response
        while ((responseLine = inputStream.readLine())!=null){
            System.out.println(responseLine);
        }
    }

此时客户端的流程就执行完毕了。

服务端

下面看服务端的流程代码:

public class SimpleHttpServer extends Thread {

    public static final int HTTP_POST = 8000;
    static ServerSocket mSocket = null;

    public SimpleHttpServer(){

        try {
            //构造服务端Socket,监听8000端口
            mSocket = new ServerSocket(HTTP_POST);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mSocket == null){
            throw new RuntimeException("服务器Socket初始化失败");
        }
    }

    @Override
    public void run() {
        try{
            while (true){ //无限循环,进入等待连接状态
                System.out.println("等待客户端连接中");
                //一旦接收到连接请求,构建一个线程来处理
                new DeliverThread(mSocket.accept()).start();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

SimpleHttpServer的构造函数中传递一个监听8000端口的服务端Socket,并开启无限循环,在该循环中调用ServerSocket的accept()函数等待客户端的连接,该函数会阻塞,直到客户端进行连接,接收连接后构造一处理线程。

public class DeliverThread extends Thread {

    Socket mClientSocket;
    //输入流
    BufferedReader mInputStream;
    //输出流
    PrintStream mOutputStream;
    //请求方式GET、POST等
    String httpMethod;
    //子路径
    String subPath;
    //分隔符
    String boundary;
    //请求参数
    Map<String,String> mParams = new HashMap<>();
    //是否已经解析完Header
    boolean isParseHeader = false;


    public DeliverThread(Socket socket){
        mClientSocket = socket;
    }

    @Override
    public void run() {
        try {
            //获取输入流
            mInputStream = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));
            //获取输出流
            mOutputStream = new PrintStream(mClientSocket.getOutputStream());
            //解析请求
            parseRequest();
            //返回Response
            handleResponse();

        }catch (IOException e){
            e.printStackTrace();
        }finally {
            //关闭流和Socket
                try {
                    if (mInputStream!=null) {
                        mInputStream.close();
                    }
                    if (mOutputStream!=null) {
                        mOutputStream.close();
                    }
                    if (mClientSocket!=null) {
                        mClientSocket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }
//省略代码
}

上面代码中主要包含如下步骤:

  1. 获取客户端Socket的输入、输出流用于读写数据
  2. 解析请求参数
  3. 处理、返回请求结果
  4. 关闭输入、输出流、客户端Socket

下面我们进行解析请求

 private void parseRequest(){
        String line;
        try{
            int lineNum = 0;
            //从输入流读取数据
            while ((line = mInputStream.readLine())!=null){
                //第一行为请求行
                if (lineNum == 0){
                    parseRequestLine(line);
                }

                //解析header参数
                if (lineNum != 0 && !isParseHeader){
                    parseHeaders(line);
                }
                //判断是否是结束行
                if (isEnd(line)){
                    break;
                }
                //解析请求参数
                if (isParseHeader){
                    parseRequestParams(line);
                }


                lineNum++;
                line = mInputStream.readLine();
            }

        }catch (IOException e){
            e.printStackTrace();
        }

    }

参照之前的Client进行解析,首先解析第一行请求数据,即lineNum为0时:

/**
     *  解析请求行 "POST /api/login/ HTTP/1.1"
     *  tempString = {POST,/api/login/,HTTP/1.1}
     */

    private void parseRequestLine(String lineOne){
        String[] tempString = lineOne.split(" ");
        httpMethod = tempString[0];
        subPath = tempString[1];
        System.out.println("请求方式:"+httpMethod);
        System.out.println("子路径:"+subPath);
        System.out.println("HTTP版本:"+tempString[2]);

    }

通过报文格式理解,请求行分为3部分即请求方式、请求子路径、协议版本,它们之间通过空格进行分割。请求行后面紧跟Header:

   /**
     * 解析header,参数为每个header的字符串
     * 见client中writeHeader方法
     *
     */
    private void parseHeaders(String headerLine){
        //header区域的结束符
        if (headerLine.equals("")){
            isParseHeader = true;
            System.out.println("---------->header解析完成n");
            return;
        }else if (headerLine.contains("boundary")){
            boundary = parseSecondField(headerLine);
            System.out.println("分隔符:"+boundary);
        }else {
            //解析普通header参数
            parseHeaderParam(headerLine);
        }
    }
 /**
     * 解析分隔符 boundary
     * "Content-Type:multipart/form-data;boundary = xxxxxx"
     *
     */
    private String parseSecondField(String line){
        String[] headerArray = line.split(";");
        parseHeaderParam(headerArray[0]);
        if (headerArray.length>1){
            return headerArray[1].split("=")[1].trim();
        }
        return "";
    }
   /**
     * 解析单个header
     * "content-length: 123"
     */
    private void parseHeaderParam(String headerLine){
        String[] keyvalue = headerLine.split(":",0);
        System.out.println("header参数名:"+keyvalue[0].trim()+"参数值:"+headerLine.substring(keyvalue[0].length()+1));

    }

接下来就是参数解析:

 /**
     *解析参数
     * -- boundary
     * Content-Disposition: from-data;name = "参数名"
     * 空行
     * 参数值
     *
     */
    private void parseRequestParams(String paramLine) throws IOException {
         String  r = boundary;
        String t ="--"+r;
        if (paramLine.equals("--"+boundary)){
            //Content-Disposition行
            String contentDisposition = mInputStream.readLine();
            //解析参数名
            String paramName = parseSecondField(contentDisposition);
            //读取参数header与参数的空行
            mInputStream.readLine();
            //读取参数值
            String paramValue = mInputStream.readLine();
            mParams.put(paramName,paramValue);
            System.out.println("参数名:"+paramName+",参数值:"+paramValue);
        }
    }

当参数解析完毕后,跳出服务器数据解析循环:

 private boolean isEnd(String line){
        if (line.equals("--"+boundary+"--")){
            return true;
        }
        return  false;
    }

至此,整个请求的各部分均解析完成。后面就要处理用户请求返回结果:

 private void handleResponse(){
        //模拟处理耗时
        sleep();
        //向客户端提供返回数据
        mOutputStream.println("HTTP/1.1 200 OK");
        mOutputStream.println("Content-Type: application/json");
        mOutputStream.println();
        mOutputStream.println("{"stCode":"success"}");

    }

    private void sleep(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

此时所有的过程展示完成,下面做个测试:

public class TestHttpActivity extends AppCompatActivity implements View.OnClickListener {

    SimpleHttpServer simpleHttpServer;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_http);
        //启动的时候开起服务器
        if (simpleHttpServer==null){
            simpleHttpServer =  new SimpleHttpServer();

        }
        simpleHttpServer.start();//开启服务器

        findViewById(R.id.client).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        //点击请求服务
        Thread thread = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        Client httpPost = new Client("127.0.0.1", 8000);
                        httpPost.addParam("userName","admin");
                        httpPost.addParam("passWord","admin");
                        //执行操作
                        httpPost.execute();
                    }
                }
        );
        thread.start();
    }
}

执行结果

服务端:

客户端:

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/it_yint/article/details/106176903
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-12 19:12:43
  • 阅读 ( 1176 )
  • 分类:Go深入理解

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢