美文网首页我爱编程
网络那些事(2)——基于Socket简单实现HTTP请求

网络那些事(2)——基于Socket简单实现HTTP请求

作者: GhostInMatrix | 来源:发表于2018-03-08 16:58 被阅读0次

回顾上一节,我们介绍了Socket是啥,如何建立C/S模式下的双向通信。
这次我们来看一看HTTP协议是什么样的,如何基于Socket建立HTTP请求。

基于Kotlin,我推荐使用spring-boot搭建server端,方便快捷。搭建方式不是终点,附上链接有兴趣的话可以自行查看:https://projects.spring.io/spring-boot/#quick-start

重点一:HTTP请求格式
一次完整的HTTP请求过程,从TCP三次握手的建立成功后开始,客户端按照指定的数据格式向服务端发送请求数据(即HTTP请求),服务端接受请求后,解析这些数据,处理完成业务逻辑,最后返回一个HTTP的响应给客户端。HTTP的响应内容同样是有标准的格式。无论是什么客户端或服务端,只要遵循该HTTP规范组织数据,它一定是通用的。

HTTP请求格式主要有四部分组成,分别是:请求行请求头空行消息体。下面我们以GET方法为例,说明每一部分的数据格式和字段意义。

  • 请求行:是请求消息的第一行,由三部分组成,分别是:请求方法(GET/POST/DELETE/PUT/HEAD)、请求资源的URI路径、HTTP的版本号。

GET /index.html HTTP/1.1

  • 请求头:请求头中的信息有跟缓存相关的头(Cache-Control,If-Modified-Since)、客户端身份信息(User-Agent, Cookie)等。例如:

Cache-Control: max-age=0
Cookie:id=0x1123;stoken=fr9hfr87w7e68932%&*();ptoken=&fdospajpfejwp89@@#!
User-Agent: Mozilla/3.0

  • 消息体:请求体是客户端发给服务端的请求数据,比如一些参数等,该部分不是必须的。
  • 空行:属于协议结构的一部分,专门用来区分请求头和消息体。在编码中的体现就是\r\n
http request

重点二:HTTP响应格式

服务器接收处理完请求后会返回一个HTTP响应消息给客户端。HTTP响应消息的格式包括:状态行、响应头、空行、消息体。

  • 状态行: 位于响应消息的第一行,有HTTP协议版本号,状态码和状态说明三部分构成。

HTTP/1.1 200 OK

  • 响应头:服务器传递给客户端用于说明服务器的一些信息,以及将来继续访问该资源时的策略。

Connection:keep-alive
Content-Type: application/json;charset=UTF-8
Date: Thu, 08 Mar 2018 07:49:14 GMT
Content-Length: 35

  • 响应体:服务端返回给客户端的数据部分,比如:视频流、图片、json字符串等。
  • 空行:专门用于区分响应头和响应体的协议,编码中同样体现为\r\n
http response

下面我们基于上一节所讲的Socket基础,实现简单的get请求获取数据。


import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import java.net.Socket
import java.nio.charset.Charset

class SfSocket : Runnable {
    override fun run() {
        sendSocket()
    }
    
    val HOST = "10.59.47.206"
    val PORT = 8001
    fun sendSocket() {
        val socket = Socket(HOST, PORT)
        val path = "/hello"
        val pw = PrintWriter(socket.getOutputStream())
        val input = socket.getInputStream()
        val sb = StringBuilder()
        
        /**
         * 为了成为一个合法的HTTP请求,我们需要做如下的组装,构造请求头及空行。
         */
        val request = sb.append("GET $path HTTP/1.1\r\n")
                .append("Host: $HOST\r\n")
                .append("Connection: Keep-Alive\r\n")
                .append("Accept-Encoding: gzip\r\n")
                .append("Accept: application/json\r\n")
                .append("User-Agent: sfhttp/0.0.1\r\n")
                .toString()  //请求头构造结束
        
        pw.write("$request\r\n")//请求头下增加空行,标志请求头到此结束。
        pw.flush()
        
        var line = ""
        var contentLength = 0
        do {
            line = readLine(input)
            //如果有Content-Length消息头时取出
            if (line.startsWith("Content-Length")) {
                contentLength = Integer.parseInt(line.split(":")[1].trim())
            }
            //打印响应头部信息
            Log.e("sfhttp:", "Header---$line")
            //如果遇到了一个单独的回车换行(空行),则表示响应头结束。
        } while (line != "\r\n")
        
        val bodyStr = readBody(socket.getInputStream(), contentLength)
        Log.e("sfhttp:", "Body---$bodyStr")
        
        input.close()
        pw.close()
        socket.close()
    }
    
    @Throws(IOException::class)
    fun readBody(inputstream: InputStream, contentLength: Int): String {
        var byte: Byte = 0
        var list = ArrayList<Byte>()
        var total = 0
        do {
            byte = inputstream.read().toByte()
            list.add(byte)
            total++
        } while (total < contentLength)
        return String(list.toByteArray(), Charset.forName("UTF-8"))
    }
    
    
    @Throws(IOException::class)
    private fun readLine(`is`: InputStream): String {
        val lineByteList = ArrayList<Byte>()
        var readByte: Byte
        do {
            readByte = `is`.read().toByte()
            lineByteList.add(java.lang.Byte.valueOf(readByte))
        } while (readByte.toInt() != 10)
        val byteArr = lineByteList.toByteArray()
        return String(byteArr,  Charset.forName("UTF-8"))
    }
}

顺便贴一下server端的核心代码:


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by ghostinmatrix on 2018/3/5.
 */
@RestController
class HelloController {
    @GetMapping(value = "/hello",produces="application/json;charset=UTF-8")
    @ResponseBody
    public String hello(HttpServletResponse rsp) throws IOException {
        System.out.println("in hello");
        return "{\"url\":\"hello  from spring-boot\"}";
    }
}

日志打印出来的结果可以看出,Response 成功200,数据格式为json,数据长度为33,最后包含一个空行作为响应头的结束标志。Body内为我们根据Content-Length读出的数据。

03-08 16:50:24.617 27716-31350/com.sf.sfhttp E/sfhttp:: Header---HTTP/1.1 200
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Content-Type: application/json;charset=UTF-8
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Content-Length: 33
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Date: Thu, 08 Mar 2018 08:50:25 GMT
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Body---{"url":"hello from spring-boot"}

总结:
1.明确了HTTP 协议规则,空行\r\n的意义是区分请求/响应头和请求/响应体而专门设计的。
2.试验了,只要按照上述HTTP协议格式组织请求数据,就能够作为真正的HTTP请求得到响应。
3.说明了,市面上所存在的这些框架(Okhttp、UrlConnection、HttpClient等),其根本都是基于Socket和HTTP协议进行的封装。只不过,我们的demo非常简单,没有任何的验证措施和安全保障。但我们可以基于已有的demo继续进行封装。

相关文章

网友评论

    本文标题:网络那些事(2)——基于Socket简单实现HTTP请求

    本文链接:https://www.haomeiwen.com/subject/eueyfftx.html