关于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
首行也叫请求行,是用来告诉服务器,客户端调用的请求类型,请求资源路径,请求协议类型
请求类型也就是我们常说的(面试官总问的)GET,POST等等发送的位置,它位于请求的最开始
请求资源路径是提供给服务器内部的寻址路径,用来告诉服务器客户端希望访问什么资源,在浏览器中访问 https://www.jianshu.com/p/6cfbc63f3a2b (用简书做一波示范了),则我们请求的就是 /p/6cfbc63f3a2b
请求协议类型目前使用最多的是HTTP/1.1说不定在不远的未来,将会被HTTP/2.0所取代
注:
-
所使用链接为https链接,但是其内容与http一样,因此使用该链接做为例子,ssl 将会在接下来的几篇文章中讲述
-
请求行的不同内容需要用 " "空格符 来做分割
-
请求行的结尾需要添加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
,随后开始准备发送数据
发送的每段数据都有特定的格式,
格式为:
- 长度行:
每段数据的开头的文本为该段真实发送的数据的16进制长度加CRLF分割符
- 数据行:
真实发送的数据加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的连接需要知道至少两个事情
- 对方的地址(host)
- 对方的门牌号(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:8056和127.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画的-_-
注: 本文原创,若希望转载请联系作者
参考: