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

回顾上一节,我们介绍了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继续进行封装。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • 一、概念(载录于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434阅读 8,295评论 6 152
  • 网络请求是iOS项目的一个大部分,而且大部分的iOS的项目的网络请求是根据AFN进行的二次封装,我们查看返回的结果...
    FR_Zhang阅读 6,730评论 15 46
  • Http协议详解 标签(空格分隔): Linux 声明:本片文章非原创,内容来源于博客园作者MIN飞翔的HTTP协...
    Sivin阅读 5,154评论 3 82
  • 2系列200 OK请求已成功,请求所希望的响应头或数据体将随此响应返回。201 Created请求已经被实现,而且...
    Y像梦一样自由阅读 3,457评论 1 5
  • 周而复始,单调平淡的日子一天天过,与同事一拍即合,相约周末青岛旅游。 周五晚出发,周六早上到青岛,一夜的长途大巴,...
    郑红阅读 321评论 0 0