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

期刊列表

关于Socket,看我这几篇就够了(一)html


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

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

什么是HTTP

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

特性

无状态

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

这里,可能不少人会有疑问,咱们平时使用的http不是这样的啊,服务器能识别咱们请求的身份啊,要难免登陆怎么作啊?面试

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

从另外一方面也可说,这个特性和http协议自己无关,由于服务器不是从这个协议自己获取对应的状态跨域

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

无链接

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

结构

http 是超文本传输协议,顾名思义,传输的是必定格式的文本,因此,咱们接下来说述一下这个协议的格式

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

请求 Request

请求格式

上图为请求格式

请求行

GET / HTTP/1.1\r\n

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

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

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

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

注:

  1. 所使用连接为https连接,可是其内容与http同样,所以使用该连接作为例子,ssl 将会在接下来的几篇文章中讲述

  2. 请求行的不一样内容须要用 " "空格符 来作分割

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

请求头Request Headers

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

请求头的长度并不固定,咱们能够放置无限多的内容到请求头中。

可是请求头的格式是固定的,咱们能够把它看作是键值对。

格式:

key: value\r\n
复制代码

咱们一般所说的cookie即是请求头中的一项

一些经常使用的http头的定义与做用: blog.csdn.net/philos3/art…

注:

当全部请求头都已经结束(即咱们要发送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介绍: blog.csdn.net/qq_23994787…

到这里,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 服务器自身错误(多是未对操做过程当中的异常进行处理) 联系后台开发人员解决(谁要是说这是客户端问题就去找他理论)

完整错误码请参照网址: baike.baidu.com/item/HTTP状态…

响应头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教程做为本篇文章的测试(www.runoob.com/http/http-t…)(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上,地址:github.com/chouheiwa/S…

总结

越学习越以为本身懂得越少,咱们如今走的每一步,都是在学习。

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

注: 本文原创,若但愿转载请联系做者

参考:

菜鸟教程

百度百科

相关文章
相关标签/搜索