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

作者 : 开心源码 本文共6272个字,预计阅读时间需要16分钟 发布时间: 2022-05-12 共170人阅读

在上一篇中,我们初步的讲述了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等等发送的位置,它位于请求的最开始

请求资源路径是提供给服务器内部的寻址路径,用来告诉服务器用户端希望访问什么资源,在浏览器中访问 https://www.songma.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进制下的 18It 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

首行也叫响应行,是用来告诉用户端当前请求的解决状况的,由请求协议类型服务器状态码对应状态形容构成

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

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

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

状态码对应状态形容含义用户对应操作
200OK标志着请求被服务器成功解决
400Bad Request标志着用户端请求出现了问题,服务器无法识别,用户端修改后服务器才能进行解决修改request参数
401Unauthorized当前请求需要校验权限,用户端需要在下次请求头部提交对应权限信息修改Header头并提交对应信息
403Forbidden当前请求被服务器拒绝执行(防火墙阻止或者其余起因)等待一段时间后再次发起,无其余处理办法
404Not Found服务无法找到对应资源(最为常见的错误码)修改Request中的资源请求路径
405Method Not Allowed用户端当前请求方法不被允许修改请求方法
408Request Timeout用户端请求超时(服务器没有在允许的时间内解析出一律的Request)重新发起请求
500Internal 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转换成UInt32sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)// 因内存字节和网络通讯字节相反,顾我们需要交换大小端 我们连接的端口是80sock4.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 = 0while 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上,地址: chouheiwa/SocketTestExample

总结

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

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

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

参考:

菜鸟教程

百度百科

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 关于Socket,看我这几篇就够了(二)之HTTP

发表回复