关于Socket,看我这几篇就够了(二)之HTTP

在上一篇中,我们初步的讲述了socket的定义,以及socket中的TCP的简单用法。

这篇我们主要讲的是HTTP相关的东西。

什么是HTTP

HTTP -> Hyper Text Transfer Protocol(超文本传输协议),它是基于TCP/IP协议的一种无状态连接

特性

无状态

无状态是指,在标准情况下,客户端的发出每一次请求,都是独立的,服务器并不能直接通过标准http协议本身获得用户对话的上下文。

这里,可能很多人会有疑问,我们平时使用的http不是这样的啊,服务器能识别我们请求的身份啊,要不免登录怎么做啊?

所以额外解释下,我们说的这些状态,如cookie/session是由服务器与客户端双方约定好,每次请求的时候,客户端填写,服务器获取到后查询自身记录(数据库、内存),为客户端确定身份,并返回对应的值。

从另一方面也可说,这个特性和http协议本身无关,因为服务器不是从这个协议本身获取对应的状态

无状态也可这样理解: 从同一客户端连续发出两次http请求到服务器,服务器无法从http协议本身上获取两次请求之间的关系

无连接

无连接指的是,服务器在响应客户端的请求后,就主动断开连接,不继续维持连接

结构

http 是超文本传输协议,顾名思义,传输的是一定格式的文本,所以,我们接下来讲述一下这个协议的格式

在http中,一个很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回车符 + \n 换行符,它是用来作为识别的字符

请求 Request

请求格式

上图为请求格式

请求行

GET / HTTP/1.1\r\n

首行也叫请求行,是用来告诉服务器,客户端调用的请求类型请求资源路径请求协议类型

请求类型也就是我们常说的(面试官总问的)GETPOST等等发送的位置,它位于请求的最开始

请求资源路径是提供给服务器内部的寻址路径,用来告诉服务器客户端希望访问什么资源,在浏览器中访问 //www.greatytc.com/p/6cfbc63f3a2b (用简书做一波示范了),则我们请求的就是 /p/6cfbc63f3a2b

请求协议类型目前使用最多的是HTTP/1.1说不定在不远的未来,将会被HTTP/2.0所取代

注:

  1. 所使用链接为https链接,但是其内容与http一样,因此使用该链接做为例子,ssl 将会在接下来的几篇文章中讲述

  2. 请求行的不同内容需要用 " "空格符 来做分割

  3. 请求行的结尾需要添加CRLF分割符

请求头Request Headers

请求行之后,一直到请求体(body),之间的部分,被我们成为请求头。

请求头的长度并不固定,我们可以放置无限多的内容到请求头中。

但是请求头的格式是固定的,我们可以把它看做是键值对。

格式:

key: value\r\n

我们通常所说的cookie便是请求头中的一项

一些常用的http头的定义与作用: https://blog.csdn.net/philos3/article/details/76946029

注:

当所有请求头都已经结束(即我们要发送body)的时候,我们需要额外增加一个空行(CRLF) 告诉服务器请求头已经结束

请求体Request Body

如果说header我们没有那么多的使用机会的话,那么body则是几乎每个开发人员都必须接触的了。

通常,当我们进行 POST 请求的时候,我们上传的参数就在这里了。

服务器是如何获得我们上传的完整Body呢?换句话说,就是服务器怎么知道我们的body已经传输完毕了呢?

我们想一下,如果我们在需要实现这个协议的时候,我们会怎么做?

  • 可以约定特殊字节作为终止字符,当读取到指定字符时,即认为读取完毕

  • 发送方肯定知道要发送的数据的大小,直接告诉接收方,接收方只需要在收到指定大小的数据的时候就可以停止接收了

  • 发送方也不知道数据的大小(或者他需要花很大成本才能知道数据的大小),就先告诉接收方,我现在也不知道有多少,等发送的时候看,真正发送的时候告诉接收方,"我这次要发送多少",最后告诉接收方,"我发完了",接收方以此停止接收。‘

也许你会有别的想法,那恭喜你,你可以自己实现类似的接收方法了。

目前,服务器是依靠上述三种方法接收的:

  • 约定特殊字节:

客户端在发送完数据后,就调用关闭socket连接,服务器在收到关闭请求后开始解析数据,并返回结果,最后关闭连接

  • 确定数据大小:

客户端在请求头中给定字段 Content-Length,服务器解析到对应数据后接受body,当body数据达到指定长度后,服务器开始解析数据,并返回结果

  • 不确定数据大小(Http/1.1 可用)

客户端在请求头中给定头 Transfer-Encoding: chunked,随后开始准备发送数据

发送的每段数据都有特定的格式,

格式为:

  1. 长度行:

每段数据的开头的文本为该段真实发送的数据的16进制长度CRLF分割符

  1. 数据行:

真实发送的数据CRLF分割符

例:

12\r\n // 长度行 16进制下的12就是10进制下的 18
It is a chunk data\r\n // 数据行 CRLF 为分割符

结尾段:

用以告诉服务器数据发送完成,开始解析或存储数据。

结尾段格式固定

0\r\n
\r\n 

目前,客户端使用这种方法的不多。

到这里,如何告诉服务器应该接收多少数据的部分已经完成了

接下来就到了,告诉服务器,数据究竟是什么了

同样也是头部定义:Content-Type

Content-Type介绍:
https://blog.csdn.net/qq_23994787/article/details/79044908

到这里,Request的基本格式已经讲完

响应 Response

响应格式

相应结构

其实Response 和 Request 从协议上分析,他们是一样的,但是他们是对Http协议中文本协议的不同的实现。

响应行

HTTP/1.1 200 OK\r\n

首行也叫响应行,是用来告诉客户端当前请求的处理状况的,由请求协议类型服务器状态码对应状态描述构成

请求协议类型 是用来告诉客户端,服务器采用的协议是什么,以便于客户端接下来的处理。

服务器状态码 是一个很重要的返回值,它是用来通知服务器对本次客户端请求的处理结果。

状态码非常多,但是对于我们开发一般用到的是如下几个状态码

状态码 对应状态描述 含义 客户对应操作
200 OK 标志着请求被服务器成功处理
400 Bad Request 标志着客户端请求出现了问题,服务器无法识别,客户端修改后服务器才能进行处理 修改request参数
401 Unauthorized 当前请求需要校验权限,客户端需要在下次请求头部提交对应权限信息 修改Header头并提交对应信息
403 Forbidden 当前请求被服务器拒绝执行(防火墙阻止或其他原因) 等待一段时间后再次发起,无其他解决办法
404 Not Found 服务无法找到对应资源(最为常见的错误码) 修改Request中的资源请求路径
405 Method Not Allowed 客户端当前请求方法不被允许 修改请求方法
408 Request Timeout 客户端请求超时(服务器没有在允许的时间内解析出全部的Request) 重新发起请求
500 Internal Server Error 服务器自身错误(可能是未对操作过程中的异常进行处理) 联系后台开发人员解决(谁要是说这是客户端问题就去找他理论)

完整错误码请参照网址:
https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin

响应头Response Headers响应体Response Body

这些内容与Request中对应部分并无区别,顾不赘述了


我们已经从特性与结构两部分讲述了Http相关的属性,到这里这篇文章的主要内容基本上算是结束了,接下来我要讲讲一些其他的http相关的知识

跨域

作为移动端开发人员,我们对这个的了解不是很多,也几乎用不到,但是我这里还是需要说明。因为现在已经到了前端的时代,万一我们以后需要踏足前端,了解跨域,至少能为我们解决不少事情。

这篇文章不会详细讲解如何解决跨域,只会讲解跨域形成的原因

什么是 跨域

在讲跨域的时候,需要先讲什么是

什么是域

在上一课讲解socket的过程中,我们已经发现了,想建立一个TCP/IP的连接需要知道至少两个事情

  1. 对方的地址(host)
  2. 对方的门牌号(port)

我们只有依靠这两个才能建立TCP/IP 的连接,其中host标明我们该怎么找到对方,port表示,我们应该连接具体的那个端口。

服务器应用是一直在监听着这个端口的,这样才能保证在有连接进入的时候,服务器直接响应对应的信息

向上聊聊吧,我们通常讲的服务器指的是服务器应用,比如常说Tomcat,Apache 等等,他们启动的时候一般会绑定好一个指定的端口(通常不会同时绑定两个端口)。所以呢,作为客户端,就可以用host+port来确定一个指定的服务器应用

由此,的概念就此生成,就是host + port

举个例子: http://127.0.0.1:8056/

这个网址所属的域就是127.0.0.1+8056 也可以写成127.0.0.1:8056

这时候有人就会问了,那localhost:8056127.0.0.1:8056是同一域么,他们实际是等价的啊。

他们不属于同一域,规定的很死,因为他们的host的表示不同,所以不是。

跨域

我们已经知道域了,跨域也就出现了,就是一个访问另一个

我们从http协议中可以发现,服务器并不任何强制规定域,也就是说,服务器并不在乎这个访问是从哪个域访问过来的,同时,作为客户端,我们也并没有域这么一说。

那么跨域究竟是什么呢?

这就要说跨域的来源了,我们日常访问的网站,它实际上就是html代码,服务器将代码下发到了浏览器,由浏览器渲染并展示给我们。

开发浏览器的程序员在开发的时候,也不知道这个网页究竟要做什么,但是他们为了安全着想,不能给网页和客户端(socket)同样的权限,因此他们限制了某些操作,在本的网页的某些请求操作在对方的服务器没有添加允许该的访问权限的时候,访问操作将不会被执行,这些操作会对浏览器的安全性有很大到的影响。

所以跨域就此产生。

跨域从头到尾都只是一个客户端的操作行为,从某种角度上说,它与服务器毫无关系,因为服务器无法得知某次请求是否来自于某一网页(在客户端不配合的情况下),也就无从禁止了

对于我们移动端,了解跨域后我们至少可以说,跨域与我们无关-_-

socket实现简单的http请求

事实上,一篇文章如果没有代码上的支撑,只是纯理念上的阐述,终究还是感觉缺点什么,本文将在上篇文章代码的基础上做些小的改进。

这里就以菜鸟教程网的http教程作为本篇文章的测试(http://www.runoob.com/http/http-tutorial.html)(ip:47.246.3.228:80)

// MARK: - Create 建立
let socketFD = Darwin.socket(AF_INET, SOCK_STREAM, 0)

func converIPToUInt32(a: Int, b: Int, c: Int, d: Int) -> in_addr {
    return Darwin.in_addr(s_addr: __uint32_t((a << 0) | (b << 8) | (c << 16) | (d << 24)))
}
// MARK: - Connect 连接
var sock4: sockaddr_in = sockaddr_in()

sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
// 将ip转换成UInt32
sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)
// 因内存字节和网络通讯字节相反,顾我们需要交换大小端 我们连接的端口是80
sock4.sin_port = CFSwapInt16HostToBig(80)
// 设置sin_family 为 AF_INET表示着这个为IPv4 连接
sock4.sin_family = sa_family_t(AF_INET)
// Swift 中指针强转比OC要复杂
let pointer: UnsafePointer<sockaddr> = withUnsafePointer(to: &sock4, {$0.withMemoryRebound(to: sockaddr.self, capacity: 1, {$0})})

var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
guard result != -1 else {
    fatalError("Error in connect() function code is \(errno)")
}
// 组装文本协议 访问 菜鸟教程Http教程
let sendMessage = "GET /http/http-tutorial.html HTTP/1.1\r\n"
    + "Host: www.runoob.com\r\n"
    + "Connection: keep-alive\r\n"
    + "USer-Agent: Socket-Client\r\n\r\n"
//转换成二进制
guard let data = sendMessage.data(using: .utf8) else {
    fatalError("Error occur when transfer to data")
}
// 转换指针
let dataPointer = data.withUnsafeBytes({UnsafeRawPointer($0)})

let status = Darwin.write(socketFD, dataPointer, data.count)

guard status != -1 else {
    fatalError("Error in write() function code is \(errno)")
}
// 设置32Kb字节存储防止溢出
let readData = Data(count: 64 * 1024)

let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
// 记录当前读取多少字节
var currentRead = 0

while true {
    // 读取socket数据
    let result = Darwin.read(socketFD, readPointer + currentRead, readData.count - currentRead)

    guard result >= 0 else {
        fatalError("Error in read() function code is \(errno)")
    }
    // 这里睡眠是减少调用频率
    sleep(2)
    if result == 0 {
        print("无新数据")
        continue
    }
    // 记录最新读取数据
    currentRead += result
    // 打印
    print(String(data: readData, encoding: .utf8) ?? "")

}

对应代码例子已经放在github上,地址:https://github.com/chouheiwa/SocketTestExample

总结

越学习越觉得自己懂得越少,我们现在走的每一步,都是在学习。

题外话:画图好费劲啊,都是用PPT画的-_-

注: 本文原创,若希望转载请联系作者

参考:

菜鸟教程

百度百科

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

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,898评论 2 89
  • 本文整理自MIN飞翔博客 [1] 1. 概念 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或...
    HoyaWhite阅读 2,637评论 2 20
  • 深入浅出HTTP协议(WEB开发和面试必备) 1.基础概念篇 a.简介 HTTP是Hyper Text Trans...
    半世韶华忆阑珊阅读 1,212评论 0 7
  • 1.TCP报头格式 UDP报头格式 TCP报头格式 UDP报头格式 具体的各部分解释看 TCP报文格式详解 - ...
    杰伦哎呦哎呦阅读 2,434评论 0 5
  • 早上翻看了孩子的试卷,发现各科成绩都不好。早饭后,孩子玩手机,我忍不住说了他的成绩。孩子显得很不好意思。我...
    爱与感赏阅读 261评论 3 6