我爱编程

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

2018-03-08  本文已影响0人  GhostInMatrix

回顾上一节,我们介绍了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 /index.html HTTP/1.1

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

http request

重点二: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

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继续进行封装。

上一篇下一篇

猜你喜欢

热点阅读