知 识 点 / 超 人
文章结构:
1.网络七层
2.TCP/IP
3.UDP
4.socket
5.webSocket
6.MQTT
7.XMPP
8.HTTP
网络七层
借用网上找到的一张图
- 应用层:当负责传送数据发送请求时:把需要发送的数据,按照应用的格式标准协议等封装成对应数据。当负责接收数据响应请求时:把数据按照应用的标准格式进行解析。例如在HTTP协议中,发送请求前要封装请求头,而接收数据时要解析响应头,这些解析过程方式是根据应用层的协议格式而定的,每个协议格式式不一样。如果不同应用标准格式不一样,会解析失败,无法正确显示数据内容。例如:A电脑是 Mac电脑用的pages软件写一个文本,然后传给B windows电脑,B电脑接收到的是A电脑的数据,数据格式完全跟A电脑一样。但是B电脑上没有能解析A电脑pages软件数据的工具。所以B电脑用户无法读取该文件。文件之所以无法解析不是因为电脑端不一样,而是因为没有软件解析对应数据格式的应用。 实际上数据格式的基本封装是应用自身完成的,还没有到达应用层这一步。拿HTTP说,当应用封装好需要传输的数据进行传输时(例如我们平时应用中把数据封装在字典里然后转成json字符串,这一步骤就是应用本身完成的封装),应用层会根据HTTP协议对数据进行处理(封装成HTTP的请求头),该协议会在传输的数据前段附加一个首部标签,该首部标签标明了发送数据的内容和发送的地址
- 表示层:数据的转换层。当负责传送数据发送请求时:会将应用层封装的数据转换成网络通用的标准数据格式进行传递(格式化编码)。当负责接收数据响应请求时:会将会话层传入的网络通用标准格式数据转换为对应设备的数据。不同设备对同一比特流数据的解析可能会有不同的结果。表示层与表示层之间为了识别编码格式,也会附加首部信息。
- 会话层:负责网络通信的建立和断开,选择网络通信的连接方式,是GET、POST、长连接,短连接。当负责传送数据发送请求时:把表示层的数据按照一定规律和标准拆分成数据块(每个数据库都有一个单独的附加首部信息,标记接收端和发送端ip)。当负责接收数据响应请求时:负责把比特流数据根据数据的每个节点拼接成完整的数据。会话层会在接收到的数据前端附加首部信息,记录数据的传输顺序信息。
- 传输层:主要是用户负责建立两端节点的通信关系,保证数据的传输安全,传输是直接连接双方节点ip地址,不经过路由处理。如果A发送给B信息,传输过程中出现数据丢失或者网络出现异常只发送了部分信息到达B端,B端会反馈消息,告诉A,只收到了部分消息,A会将未发送的消息重新发送到B,比如迅雷下载资源的时候,可以暂停,下次继续按照上一次的进度下载。(猜测可能是传输结束后会话层中根据附加首部信息判断到数据传输顺序并不完整,会把不完整的顺序反馈给发送端,让附送端把未接收到的数据重新发送过来)
- 网络层:将数据传输到目标地址,目标地址可以是由多个网络或路由连接而成的某个地址,因此改层主要是寻找地址和路由选择。
- 数据链路层:负责物理层上的通信传输,把0、1序列化为有意义的数据帧传给对端。通过Mac地址,目的是识别连接到同一传输介质上的设备。因此,这一把分层中将包含Mac地址信息的首部附加到网络层转发过来的数据上,发送到网络。、
- 物理层:负责将机器语言的0、1转换为电压高低、脉冲光的闪灭输出给物理的传输介质(光纤)。
带宽:数据的物理传输一般用光纤,双绞线电缆等来进行媒介传输,在这些媒介传输过程中,传输的速度是恒定的。如果要增加传输速度,只有增加传输媒介的通道数量,一般低速数据链路是指传输媒介通道较少,同一时间通过的数据少。高速数据链路是指传输媒介通道较多,同一时间可通过的数据多。传输数量又称只为带宽。传输媒介通道越多带宽越大传输的能力越强。带宽单位为bps(bits<比特> Per second<每秒>,每秒的比特流传输速度)
吞吐量:吞吐量的单位与带宽一样都是bps。吞吐量的大小不仅衡量带宽,同时也是衡量主机CPU处理能力,网络拥堵情况,报文中数据字段的占有份额(不计算报文首部,仅计算数据字段本身)等信息
其实现在基本上把会话层、表示层、应用层统一为应用层了。
对于我们而言理解上也就主要是4个层,数据链路层,网络层,传输层,应用层
数据链路层就是高速公路,TCP、UDP就是能在这个高速公路行驶的运货车,而IP就像是这个运货车的司机决定了货车从哪一个站出发到达哪一个站,而HTTP,FTP就是这个车上运载的货物。货物是由客户端与服务端协商定义的,由两端定义好使用同一套协议去识别验收货物,而货物的传输过程是由TCP/IP定义的。
基础补充
bit
:位,比特位,是计算机中表示数据的最小单位。通常bit用来作为数据传输的基本单位,数据链路层的传输是基于二进制的传输。
byte
:字节,1byte = 8bit,1KB = 1024byte即1024B,1M = 1024KB
- 8bit(位)最多表示0-255,因为在二进制中最多表示
0000 0000-1111 1111
,2的8次方-1 - 16bit(位)最多表示0-65535 ,因为在二进制中最多表示
0000 0000 0000 0000 - 1111 1111 1111 1111
,2的16次方-1 - 32bit(位)最多表示0- 4 294 967 295,2的32次方-1
英文字母和中文汉字在不同字符编码下的字节数不一样。
英文字母:
编码类型:GB2312
时字节数为 1
byte
编码类型:GBK
时字节数为 1
byte
编码类型:GB18030
时字节数为 1
byte
编码类型:ISO-8859-1
时字节数为 1
byte
编码类型:UTF-8
时字节数为 1
byte
编码类型:UTF-16
时字节数为 3
byte
编码类型:UTF-16BE
时字节数为 2
byte
编码类型:UTF-16LE
时字节数为 2
byte
中文汉字(包含繁体):
编码类型:GB2312
时字节数为 2
byte
编码类型:GBK
时字节数为 2
byte
编码类型:GB18030
时字节数为 2
byte
编码类型:ISO-8859-1
时字节数为 1
byte
编码类型:UTF-8
时字节数为 3
byte
编码类型:UTF-16
时字节数为 4
byte
编码类型:UTF-16BE
时字节数为 2
byte
编码类型:UTF-16LE
时字节数为 2
byte
TCP/IP
传输控制协议,TCP/IP是互联网协议,Internet互联网的基础,由网络层的IP协议与传输层的TCP协议组成.TCP/IP定义了电子设备如何连入Internet互联网
TCP又叫传输控制层协议位于传输层,基于字节流,数据在互联网之间传输的标准.它可以提供可靠的、较为安全的、面向连接点到点的网络数据传递服务。
TCP报文格式
-
源端口号(sourec port)
:数据发送源的应用进程端口号,占16位(2个字节),端口号最小为1024,最大为65535。因为该字段占16位(16bit),而16bit在十进制中最大表示为65535最小表示为0,所以端口号最大为65535,而由于0-1023是知名端口号一般由系统占用。所以端口号最小从1024开始。
-目的端口号(destination port)
:标明接收数据的端口号。
-顺序号(sequence number)
:这里的顺序号就是TCP的握手和挥手的seq
,它占32位,标识了TCP报文中第一个字节在传输方向中对应的字节序号。当SYN时,SN(sequence number)=ISN(随机值)单位是byte。比如发送端发送的一个TCP包净荷(不包含TCP头)为12byte,SN为5,则发送端接着发送的下一个数据包的时候,SN应该设置为5+12=17。通过序列号,TCP接收端可以识别出重复接收到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠序列号进行重排序,进而对高层提供有序的数据流。另外如果接收的包中包含SYN或FIN标志位,逻辑上也占用1个byte,应答号需加1 -
TCP报头长度(Header Length)
:仅表示报头的长度,占4位(这里按照32位字长位单位计算。 占4位标示4bit,而4bit最大二进制表示为1111,最大显示的值为15, 而单位是32位字长。表示每个单位按照32来计算。每个单位也就是4字节。最大能表示15个单位,就是60字节,TCP报头内容最多60个字节)
。它指出TCP报文段从数据起始处
距离 TCP报文段的起始处
有多远。[这就是为什么上图中 会分为前16位 后16位,因为单位是按照32位字长计算的]
-
保留位(Resy)
:占6位,保留给以后使用,目前必须设置为0 -
编码位
:
URG
:紧急数据标志位
ACK
:确认序号有效
PSH
:请求推送位,接收端应尽快把数据传送给应用层
RST
:重置连接,通常,如果TCP收到的一个分段明显不属于该主机的任何一个连接,则向远程发送一个复位包
SYN
:建立连接,让连接双方同步序列号
FIN
:释放连接 -
窗口大小(Window Size)
:标示从确认号开始,报文的发送源可以接受的字节数为多少。 -
校验和(check sum)
:奇偶校验,此校验和是对整个TCP报文段以16位字进行计算所得,服务端接收后进行验证。
-紧急指针(urgent poiner)
:主要是用作发送端向另一端发送紧急数据的一种方式,只有在URG标志值为1的时候才有效。它是一个正的偏移量,跟顺序号字段中的值相加表示紧急数据最后一个字节的序号。
-选择(options)
:至少1字节的可变长字段,标识哪个选项(如果有的话)有效。如果没有选项,这个字节等于0,说明选项的结束。这个字节等于1表示无需再有操作;等于2表示下四个字节包括源机器的最大长度,最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。注:MSS=TCP报文段长度-TCP首部长度
-
填充(Padding)
:因为选择(options)项长度不一定是32位的整数倍,所以需要填充位,在这个字段中加入额外的0,来保证该字段的位数是32的倍数,保证TCP头是32的整数倍。 -
数据(data)
:TCP所传输的数据,在三次握手和四次挥手时仅有TCP首部,没有该data字段。
TCP粘包拆包
1.从上面的TCP报文来看,我们知道TCP报文中是没有字段表示数据长度的。
2.TCP是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。TCP的Header中有Options标识位,常见的标识为mss(Maximum Segment Size),指的是连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,MSS则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。换算成字节,也就是180多字节。当需要传输的数据大小超过一个TCP段能接收的大小时,TCP就会把数据进行分段传输。这段内容摘取之该文章
而什么是粘包拆包呢。假如一个数据被TCP分段成packet1和packet2两段时。可能会出现以下几种情况:
1.接收端正常收到packet1和packet2,即没有发生拆包和粘包的现象
2.接收端只收到一个数据包packet,由于TCP是不会出现丢包的(不是绝对,但TCP自带丢包重发机制,所以基本不丢包),所以这一个数据包是由packet1和packet粘合在一起的,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
3.接收端收到了两个数据包,但由于某种原因packet2被分离成packet2_1和packet2_2,packet1的尾部和packet2_1的首部连在一起,合成一个大的报文,首先到达接收端端,过后packet2_2才到达,这种情况既发生了粘包,同时也发生了拆包。
TCP为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。
1.当要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2.当待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3.当要发送的数据小于TCP发送缓冲区的大小,TCP会将多次写入到缓冲区的数据一次发送出去,将会发生粘包。
4.当接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
TCP的连接建立过程又称为TCP的三次握手
- 首先客户端向服务端发起一个建立连接的同步(SYN)请求;
- 服务端在收到这个请求后向客户端回复一个同步/确认(SYN/ACK)的应答;
-
客户端收到应答回应后再向服务端发送一个确认(ACK),此时TCP连接成功建立.
TCP的连接断开过程又称之为TCP的四次挥手
首先客户端发送一个FIN消息给服务端,客户端进入FIN_WAIT_1状态。
接着服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态。
服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输的数据是否已经传输完毕,一旦确认数据传输完成,服务端发送一个FIN消息给客户端,服务端进入LAST_ACK状态。
-
最后客户端收到FIN消息后,进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,服务端进入CLOSED状态,完成四次挥手。
这里要单独说下TIME_WAIT状态,它比较特殊,因为处于TIME_WAIT状态的一方还没有释放TCP连接。如果某一方主动发出关闭TCP的请求,那么这一方最后要较长时间(2倍MSL时间)的处于TIME_WAIT状态,因为在最后一个ACK发送出去后,ACK可能会丢失,如果发生丢失,非主动关闭TCP请求的一方就会再次发送FIN请求,而此时主动关闭TCP请求这一方需要再次发送ACK。以确保正常中断双方数据通道,所以必须要让四次ACK都正常接收。
TCP不允许新建立的连接复用TIME_WAIT状态下的。处于TIME_WAIT状态的连接通道,在等待两倍的MSL时间以后将会转变为CLOSED状态。之所以是两倍的MSL,是由于MSL(最长报文段寿命)是一个数据报在网络中单向发出
到认定丢失的时间
,一个数据报有可能在发送途中或是其响应过程中成为残余数据报,确认一个数据报及其响应的丢弃的需要两倍的MSL
,这就意味着,一个成功建立的连接,必然使得先前网络中残余的数据报都丢失了。
TCP将数据流拆分成多个部分,这些部分叫做TCP的数据段,利用IP协议进行数据段的传输。当TCP将把数据段分成多个数据报在IP中进行传输时,由于IP并不能保证接收的数据报的顺序相一致,TCP会在收信端装配TCP数据段并形成一个不间断的数据流
为了确保IP数据报的成功传递TCP主要完成以下工作
- 发送时对大块数据进行分段,接收时对数据段重组。
- 确保正确排序及按顺序传递分段的数据。
- 通过算法校验传输数据的完整性。
- 根据数据段是否接收成功,发送确认消息。通过使用选择性确认,也对没有收到的数据发送否定确认。从而确保数据段拼合后的完整性
IP
IP确保计算机之间通信数据的正确传输,位于网络层
通过IP,将TCP分割的数据段通过因特网在计算机之间传递.IP 负责将每个数据包,路由至它的目的地.
IP路由器,当一个IP数据包从一台计算机被发送,它会到达一个IP路由器.IP路由器负责将这个数据包,路由至它的目的地,直接地或者通过其他路由器的再次传输.
在一个相同的通信中,每个数据包经由的路径可能都不同,因为数据包的传输过程是由IP路由器决定的。路由器负责根据通信量,网络中的错误或者其他参数来进行正确地寻址。
计算机必须有一个IP地址才能够正确的连接互联网中.每个IP数据包必须有一个明确的地址才能够准确发送到目标计算机.
在计算机中IP分为4段,每段的大小都是1个字节,而1个字节是8位(bit)二进制数(即0或1的8位数组合),那么最大的二进制数就是:11111111,化为十进制得:255。最小的就是:00000000,化为十进制得:0,所以每个IP地址由4组0到255之间的数字组合而成,并由点号隔开.例如:192.168.1.192。每个IP地址32bit
UDP
UDP被称之为用户数据报协议位于传输层。提供非面向连接的网络服务,传送数据不需要和服务端建立连接,只需要知道数据需要发送到哪一个IP地址和监听端口即可,该服务传输的数据是不可靠的、可以由一点发送到到多点。这意味着它不保证数据报的到达只负责发送,也不保证所传送数据包的顺序是否正确,正因为UDP协议的控制选项较少,在数据传输过程中延迟较小所以数据传输效率很高,一般用于对可靠性要求不高的功能上,因为它不提供数据包分组、组装和不能对数据包进行排,舍弃了TCP的建立连接等开销较大的步骤。简单的说UDP只管发送数据,过程与结果并不在乎。一般网络质量较差时,采用UDP会造成数据的丢失。从UDP的结构来说,UDP首部采用了16bit大小的内容来标识UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
UDP报文格式
这里UDP的校验和有点特殊。会加一个伪首部进去计算。
UDP长度(Length)
:由于UDP首部由4个字段组成,每个字段16bit即2字节。所以UDP长度最小8字节。UDP长度占16bit,最大表示值65535,而UDP报文占8个字节,IP包头占20个字节。所以UDP最大能表示65535-8-20=65507字节。但由于MTU的限制,实际在以太网中UDP最大可以表示1500-8-20=1472字节。
MTU是什么?1500从哪里来的?
MTU通信术语 最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。
在以太网中,每个以太网帧都有最小的大小64Bytes最大不能超过1518Bytes
,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。
由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes
这个值我们就把它称之为MTU
UDP丢包和无序
丢包原因:
接收端处理包时间过长,接收端处理上一个包的时间过长,导致下个包来的时候并没有去调用recv来接收包。从而导致丢包。 在TCP中有缓冲区,2次调用recv中间收到的包会放在缓冲区中,只有在缓冲区放不下中间发送的包时,会发生丢包,而且丢包TCP会自动重发。但UDP没有缓冲区。
解决方案:
1.可以自己写一个缓冲区去做处理
2.发送端与接收端协议 缓冲区大小和数据包的最大大小,包超过大小需要切割包,包超过缓冲区大小,需要延时发送。
3.也可以增加处理包的效率
4.可以写一个丢包重发机制
这些方法也是TCP之所以不丢包的原因,也是UDP快的原因之一,个人感觉这样做后就跟TCP差不多了,可能唯一不同的就是TCP自行处理的,而UDP需要我们自己写,自己写可操作空间才更大吧。
无序原因:
因为UDP传送数据不需要和服务端建立连接,而网络的稳定是不确定性,所以多个包到达接收端的顺序是无法控制的。因此会造成无序。
没有处理过这种情况,只能猜测解决办法。在数据包中增加顺序标识和完整数据包大小或个数标识
socket
socket又称之为套接字,因为它本身不是一种协议,是位于应用层和传输控制层之间的一组接口。它包含进行网络通信必须的五种信息:
1.连接使用的协议
2.本地主机的IP地址
3.本地进程的协议端口
4.远地主机的IP地址
5.远地进程的协议端口
通过Socket接口,可以让应用层和传输层区分来自不同应用程序进程(端口)或网络连接的通信。建立一个有效的Socket连接需要至少配对两个套接字,一个运行于客户端的套接字,称为ClientSocket,另一个运行于服务器端的套接字,称为ServerSocket。套接字之间的连接过程分为三个步骤:
1.服务器的事件监听
2.客户端数据请求
3.一条双向通道的连接
Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接,UDP连接同理。
TCP/IP与Socket的关系
TCP/IP只是一个协议,就像操作系统的运行机制一样,它是一套规则,必须要把规则具体实现,同时还要对外提供操作接口。像操作系统会提供标准的编程接口,TCP/IP提供给程序员做网络开发所用的接口,这就是Socket编程接口
网络这一块主要是理论所以要耐着性子看,慢慢消化,当你看到这里的时候你就里成功理解网络编程又进一步了,废话不多说直接上代码,从代码里理解socket
客户端
/**
连接socket
@param host 域名ip
@param port 端口号
*/
- (void)connectSocketWithHost:(NSString *)host port:(NSUInteger)port
{
__weak typeof(self) weakSelf = self;
dispatch_async(_socketQueue, ^{//创建一个单独队列来执行连接
//用来接收获取地址信息是否成功的值
int error;
/*
memset作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
第一个参数为指针地址,第二个为设置值,第三个为连续设置的长度(大小)
分配一个hints结构体,把它清零后填写需要的字段,再调用getaddrinfo,然后遍历一个链表逐个尝试每个返回地址
*/
memset(&hints, 0, sizeof(hints));
/*如果指定AF_INET,那么函数九不能返回任何IPV6相关的地址信息;
如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。
AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。
如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回*/
hints.ai_family = PF_UNSPEC;//AF_UNSPEC,ai_family设置网络返回的地址信息类型
/*
AI_V4MAPPED:如果需要dns的host没有v6地址的情况下,getaddinfo会把v4地址转换成v4-mapped ipv6 address,如果有v6地址返回就不会做任何动作
AI_ADDRCONFIG:这个是一个很有用的特性,这个flags表示getaddrinfo会根据本地网络情况,去掉不支持的IP协议地址
AI_DEFAULT:其实就是AI_V4MAPPED|AI_ADDRCONFIG_CFG,也是apple推荐的flags设置方式
*/
hints.ai_flags = AI_DEFAULT;//ai_flags对返回的地址信息处理方式
/*
如果服务支持多个套接口类型,那么每个套接口类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员
指定的服务既可支持TCP也可支持UDP,所以调用者可以把hints结构中的ai_socktype成员设置成自己需要的服务
常用的Socket类型有两种:
流式Socket(SOCK_STREAM)流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;
数据报式Socket(SOCK_DGRAM)数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
*/
hints.ai_socktype = SOCK_STREAM;//ai_socktype设置连接服务类型
/* getaddrinfo解决了把主机名和服务名转换成套接口地址结构的问题
hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。
返回值:0——成功,非0——出错
*/
error = getaddrinfo([host UTF8String], NULL, &hints, &res0);
/*
如果getaddrinfo函数返回成功,那么由res0参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串联起来的addrinfo结构链表。可以导致返回多个addrinfo结构的情形有以下2个:
1. 如果与hostname参数关联的地址有多个,那么适用于所请求地址簇的每个地址都返回一个对应的结构。
2. 如果service参数指定的服务支持多个套接口类型,那么每个套接口类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员。
遍历一个链表逐个尝试每个返回地址。
*/
//判断是否解析成功
if (error != 0) {
NSLog(@"getaddrinfo失败,原因:%s",gai_strerror(error));
[weakSelf sendConnectStatus:HYJSocketStatus_failed];
return;
}
weakSelf.sockerStatus = -1;
for (struct addrinfo *res = res0; res; res= res->ai_next) {
/*
socket() 创建socket
参数1:ai_family网络返回的地址信息类型
参数2:ai_socktype设置连接服务类型
参数3:ai_protocol协议类型。取的值取决于ai_address和ai_socktype的值.一般设置为0
返回值非负描述符成功,返回一个新的套接字描述,出错返回-1
*/
weakSelf.sockerStatus = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (weakSelf.sockerStatus < 0) {//解析失败
continue;
}
/*
ai_addr是一个sockaddr类型的结构体
sa_family是返回的连接类型是IPv4还是IPv6
*/
switch (res->ai_addr->sa_family) {
case AF_INET://IPv4
{
/* sockaddr_in必须在.mm文件中使用 该类型为C++ */
struct sockaddr_in *v4sa = (struct sockaddr_in*)res->ai_addr;//会将struct sockaddr 变量强制转化为struct sockaddr_in 类型
/*
把主机字节序转化为网络字节序 (必须把port和host这些参数转化为网络字节序)
ntohs() 参数16位 short类型 网络字节序转主机字节序
ntohl() 参数32位 long类型 网络字节序转主机字节序
ntohll() 参数64位 long类型 网络字节序转主机字节序
htons() 参数16位 short类型 主机字节序转网络字节序
htonl() 参数32位 long类型 主机字节序转网络字节序
htonll() 参数64位 long类型 主机字节序转网络字节序
以上声明中 n代表netwrok, h代表host ,s代表short,l代表long,ll代表longlong
如果数据是单字节的话,则其没有字节序的说法了
网络字节顺序NBO(Network Byte Order)
按从高到低的顺序存储,在网络上使用同一的网络字节顺序,可避免兼容性问题;
主机字节顺序HBO(Host Byte Order)
不同的机器HBO不相同,与CPU的设计有关,数据的顺序是由CPU决定的,而与操作系统无关;
如Intel x86结构下,short型数0x1234表示为34 12,int型数0x12345678表示为78 56 34 12;
如IBM power PC结构下,short型数0x1234表示为 12 34,int型数0x12345678表示为 12 34 56 78.
由于这个原因,不同体系结构的机器之间不能直接通信,所以要转换成一种约定的顺序,也就是网络字节顺序,其实就是如同power pc那样的顺序。在PC开发中有ntohl和htonl函数可以用来进行网络字节和主机字节的转换
*/
v4sa->sin_port = htons(port);//注意port的数据类型,sin_port是in_port_t类型,而in_port_t是__uint16_t类型。如果一个高位转低位就会出现数据丢失,编译器会报错
/*
注意点
在iOS编程中,经常会使用C或者C++混编的情况。在C和C++编程中,经常将指针转化为int类型。
在32位的系统中,指针占有4个字节(当然有2个字节的指针,别钻牛角)的空间;int也占有4个字节的空间。这样,转化是没有精度损耗的。OK。
在64位操作系统下占有8个字节的空间,而int占有4个字节的空间;这时候,就出问题了,将8字节的指针转化位4个字节的int,当然会发生精度的损耗。
*/
}
break;
case AF_INET6://IPv6
{
struct sockaddr_in6 *v6sa = (struct sockaddr_in6*)res->ai_addr;//会将struct sockaddr 变量强制转化为struct sockaddr_in6 类型,注意类型与IPv4不同
v6sa->sin6_port = htons(port);//配置port
}
break;
}
/*
connect()用来将参数sockfd的socket连至参数serv_addr指定的网络地址.结构sockaddr请参考bind().参数addrlen为sockaddr的结构长度.
返回值:成功则返回0,失败返回-1,错误原因存于errno中.
错误代码:
1、EBADF参数sockfd非合法socket处理代码
2、EFAULT参数serv_addr指针指向无法存取的内存空间
3、ENOTSOCK参数sockfd为一文件描述词,非socket.
4、EISCONN参数sockfd的socket已是连线状态
5、ETIMEDOUT企图连线的操作超过限定时间仍未有响应.
6、ENETUNREACH无法传送数据包至指定的主机.
7、EAFNOSUPPORT sockaddr结构的sa_family不正确.
8、EALREADY socket为不可阻断且先前的连线操作还未完成.
*/
if (connect(weakSelf.sockerStatus, res->ai_addr, res->ai_addrlen) < 0) {
close(weakSelf.sockerStatus);//关闭套接字描述
//socket 设置成非阻塞状态
/*
int fcntl(int fd, int cmd, long arg);
fcntl()针对(文件)描述符提供控制。参数fd是被参数cmd操作(如下面的描述)的描述符。针对cmd的值,fcntl能够接受第三个参数int arg。
fcntl()的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD , F_GETFD , F_GETFL以及F_GETOWN。
F_DUPFD 返回新的文件描述符
F_GETFD 返回相应标志
F_GETFL , F_GETOWN 返回一个正的进程ID或负的进程组ID
fcntl函数有5种功能:
1. 复制一个现有的描述符(cmd=F_DUPFD).
2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5. 获得/设置记录锁(cmd=F_GETLK , F_SETLK或F_SETLKW).
*/
flags = fcntl(weakSelf.sockerStatus,F_GETFL,0);
fcntl(weakSelf.sockerStatus,F_SETFL, flags|O_NONBLOCK);
//socket 设置成非阻塞状态
weakSelf.sockerStatus = -1;
continue;
}
break;/* 连接成功跳出循环 */
}
if (weakSelf.sockerStatus < 0) {
NSLog(@"连接失败");//小于0时连接失败
}else
{
NSLog(@"连接成功");
}
dispatch_async(weakSelf.socketListenQueue, ^{//需要在单独创建一个队列去不断的接收消息,如果不无线循环的去接收消息,那么就只能客户端发一条消息,服务端回一条消息。因为没有新的消息接收器
while (weakSelf.sockerStatus >= 0) {
[weakSelf listenMessage];
}
});
freeaddrinfo(res0);//释放对象
});
}
- (void)listenMessage
{
char *buf[1024];
ssize_t recvLen = recv(_sockerStatus, buf, sizeof(buf), 0);
NSString *recvStr = [[NSString alloc] initWithBytes:buf length:recvLen encoding:NSUTF8StringEncoding];
NSLog(@"收到的消息:%@",recvStr);
}
上面只是Socket客户端的部分代码。建议要更深入了解socket的读者,下载我Git上的Demo,里面有更加详细的注释,并且通过每个流程去深入仔细的了解socket的每个环节,包含客户端和服务端两个工程的代码
socket接口说明
//socket 创建并初始化 socket,返回该 socket 的文件描述符,如果描述符为 -1 表示创建失败。
int socket(int addressFamily, int type,int protocol)
//关闭socket连接
int close(int socketFileDescriptor)
//将 socket 与特定主机地址与端口号绑定,成功绑定返回0,失败返回 -1。
int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)
//接受客户端连接请求并将客户端的网络地址信息保存到 clientAddress 中。
int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength)
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)
//使用 DNS 查找特定主机名字对应的 IP 地址。如果找不到对应的 IP 地址则返回 NULL。
hostent* gethostbyname(char *hostname)
//通过 socket 发送数据,发送成功返回成功发送的字节数,否则返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
//从 socket 中读取数据,读取成功返回成功读取的字节数,否则返回 -1。
int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags)
//通过UDP socket 发送数据到特定的网络地址,发送成功返回成功发送的字节数,否则返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)
//从UDP socket 中读取数据,并保存发送者的网络地址信息,读取成功返回成功读取的字节数,否则返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)
如果想了解心跳机制或更深入了解socket可以看下面这篇文章
iOS即时通讯,从入门到“放弃”?
WebSocket
WebSocket是一种HTML5的协议,与Socket不一样,Socket是位于传输层和应用层之间的抽象接口
,主要是用于方便对TCP/IP协议等使用,而WebSocket是位于应用层的协议
用于对数据的包装解析。
WebSocket 是一个基于 TCP 的协议。建立一个 WebSocket 连接,首先需要客户端向服务器发起一个 HTTP 请求,请求头中包含了一些WebSocket特有的头信息,其中附加头信息Upgrade:WebSocket
表示这是一个特殊的 HTTP 请求,请求的目的就是向服务端申请,将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议。请求头中的Sec-WebSocket-Key1
、Sec-WebSocket-Key2
和[8-byte securitykey]
是客户端向服务器端提供的握手信息,服务器端解析这些信息,并在握手的过程中依据这些信息生成一 个 16 位的安全密钥并返回给客户端,以表明服务器端获取了客户端的请求,同意创建 WebSocket 连接。一旦连接建立,客户端和服务器端就可以通过这个通道双向传输数据了,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
目前iOS最成熟的WebSocket工具是Facebook开源的SocketRocket。
下面是本人基于SocketRocket写的一套客户端代码
@interface HYJWebSocketClientManager()<SRWebSocketDelegate>
{
/* 心跳时间 */
dispatch_source_t webSocketHeartbeatTimer;
/* 重连时间 */
int reConnectTime;
/* 当前需要连接的host */
NSString *_host;
/* 当前需要连接的port */
NSString *_port;
}
/* webSocket管理者 */
@property (nonatomic, strong)SRWebSocket *webSocket;
/* webSocket执行队列 */
@property (nonatomic, strong)NSOperationQueue *webSocketQueue;
/* 心跳队列 */
@property (nonatomic, strong)dispatch_queue_t webSocketHeartbeatQueue;
@end
@implementation HYJWebSocketClientManager
//单例对象
+ (instancetype)sharedManager
{
static HYJWebSocketClientManager *object = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object = [HYJWebSocketClientManager new];
object.status = HYJWebSocketStatus_unInit;
});
return object;
}
#pragma mark - PublicMethod
- (void)connectWebSocketWithHost:(NSString *_Nonnull)host Port:(NSString *_Nonnull)port
{
if (_webSocket) {
[_webSocket close];
_webSocket = nil;
[self sendWebSocketStatus:HYJWebSocketStatus_unInit];
}
if ([self isBlankString:host] || [self isBlankString:port]) {
return;
}
_host = host;
_port = port;
NSURL *url = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"ws://%@:%@",host,port]];
_webSocket = [[SRWebSocket alloc] initWithURL:url];
_webSocket.delegate = self;
_webSocketQueue = [NSOperationQueue new];
[_webSocket setDelegateOperationQueue:_webSocketQueue];
[self sendWebSocketStatus:HYJWebSocketStatus_ing];
[_webSocket open];
}
//发送消息
- (void)sendMessage:(NSString *)message
{
NSString *messageString = [self packageMessageBag:message messageType:@"user"];
[self.webSocket send:messageString];
}
- (void)closeWebSocket
{
if (_webSocket) {
[_webSocket close];
_webSocket = nil;
[self sendWebSocketStatus:HYJWebSocketStatus_unInit];
}
}
#pragma mark - PrivateMethod
//启动心跳时间
- (void)initHeartBeatEvent
{
[self cancelHeartbeatEvent];
dispatch_time_t startTimer = dispatch_time(DISPATCH_TIME_NOW, 3*60*NSEC_PER_SEC);
uint64_t interval = 3*60*NSEC_PER_SEC;
dispatch_source_set_timer(webSocketHeartbeatTimer, startTimer, interval, 0);
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(webSocketHeartbeatTimer, ^{
NSString *heartString = [self packageMessageBag:@"" messageType:@"user"];
[weakSelf.webSocket send:heartString];
});
dispatch_resume(webSocketHeartbeatTimer);
}
//取消心跳时间
- (void)cancelHeartbeatEvent
{
if (webSocketHeartbeatTimer) {
dispatch_source_cancel(webSocketHeartbeatTimer);
webSocketHeartbeatTimer = nil;
}
}
//封装消息
- (NSString *)packageMessageBag:(NSString *)message messageType:(NSString *)type
{
NSMutableDictionary *messageDic = [NSMutableDictionary new];
[messageDic setValue:message forKey:@"message"];
[messageDic setValue:type forKey:@"messageType"];
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:messageDic options:NSJSONWritingPrettyPrinted error:&error];
if (!data || error) {
return nil;
}else
{
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
}
//解析数据包
- (void)analyticalMessage:(NSString *)message
{
if ([self isBlankString:message]) {
//空消息不处理
return ;
}
NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSMutableDictionary *messageDic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
if (!messageDic && error) {
[HYJToast showToastWithText:@"收到一条消息,但解析失败" withToastType:HYJToastType_KeyWindow withToastPointType:HYJToastPointType_Center ForView:nil];
return;
}
NSString *messageString = [messageDic objectForKey:@"message"];
if ([self isBlankString:messageString]) {
[HYJToast showToastWithText:@"收到空消息" withToastType:HYJToastType_KeyWindow withToastPointType:HYJToastPointType_Center ForView:nil];
return;
}else
{
[self backMessage:messageString];
}
}
//是否为空字符串判断 YES表示为空字符串
- (BOOL)isBlankString:(NSString *)string
{
if (string) {
if ([string isKindOfClass:[NSString class]]) {
if ([string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length > 0) {
return NO;
}else
{
return YES;
}
}else
{
return YES;
}
}else
{
return YES;
}
}
//修改并更新socket连接状态
- (void)sendWebSocketStatus:(HYJWebSocketStatus)status
{
if (self.delegate && [self.delegate respondsToSelector:@selector(webSocketConnetStatt:)]) {
self.status = status;
[self.delegate webSocketConnetStatt:status];
}else
{
self.status = status;
}
}
//通过代理返回接收到的数据
- (void)backMessage:(NSString *)message
{
if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveMessage:)]) {
[self.delegate didReceiveMessage:message];
}
}
//重连机制
- (void)reConnect
{
[self closeWebSocket];
if ([self isBlankString:_host] || [self isBlankString:_port]) {
return;
}
//限制重连最长不超过64秒
if (reConnectTime > 64) {
return;
}
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf connectWebSocketWithHost:_host Port:_port];
});
//重连时间2的指数级增长
if (reConnectTime == 0) {
reConnectTime = 2;
}else{
reConnectTime *= 2;
}
}
#pragma mark - SRWebSocketDelegate
//收到消息的回调
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
//没收到一条消息就重新开启心跳时间
[self initHeartBeatEvent];
[self analyticalMessage:message];
}
//连接成功的回调
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
//连接成功后开启心跳时间
[self initHeartBeatEvent];
[self sendWebSocketStatus:HYJWebSocketStatus_success];
}
//连接发生错误的回调
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
//连接失败,这里可以实现掉线自动重连,要注意以下几点
//1.判断当前网络环境,如果断网了就不要连了,等待网络到来,在发起重连
//2.判断调用层是否需要连接,例如用户都没在聊天界面,连接上去浪费流量
//3.连接次数限制,如果连接失败了,重试10次左右就可以了,不然就死循环了。
[self sendWebSocketStatus:HYJWebSocketStatus_fail];
[self reConnect];
//[self cancelHeartbeatEvent];
}
//连接被关闭的回调
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
[self sendWebSocketStatus:HYJWebSocketStatus_unInit];
[self cancelHeartbeatEvent];
}
/*
该函数是接收服务器发送的pong消息,其中最后一个是接受pong消息的,
在这里就要提一下心跳包,一般情况下建立长连接都会建立一个心跳包,
用于每隔一段时间通知一次服务端,客户端还是在线,这个心跳包其实就是一个ping消息,
我的理解就是建立一个定时器,每隔十秒或者十五秒向服务端发送一个ping消息,这个消息可是是空的
*/
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload
{
}
//将收到的消息,是否需要把data转换为NSString,每次收到消息都会被调用,默认YES
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket
{
return YES;
}
#pragma mark LazyLoad
- (dispatch_queue_t)webSocketHeartbeatQueue
{
if (!_webSocketHeartbeatQueue) {
_webSocketHeartbeatQueue = dispatch_queue_create("HYJWebSocketHeartbeatQueue", DISPATCH_QUEUE_SERIAL);
}
return _webSocketHeartbeatQueue;
}
@end
大家可以在我的Git中下载该Demo,熟悉流程。
MQTT
关于MQTT的介绍借用下别人的
没办法网上千篇一律
一、MQTT简介
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,该协议支持所有平台,也就是说不论什么平台都可以使用集成此协议,MQTT被用来当做传感器和致动器的通信协议。MQTT基于订阅发布模式,客户端之间要互相通信,必须都订阅了同一个主题(topic),客户端之间是不能直接单独通讯的,必须通过服务端的发布。当群发消息的时候只需要把消息发布到对应群的主题(topic)中,通过服务端去发布到该主题中的每一个用户,所有订阅了这个topic的客户端就可以接收到消息了。
一、MQTT特点
MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议(简单的说就是数据的传输流量开销很小,三种QOS质量服务能较好保证数据传送),它具有以下主要的几项特性:
1、使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合;
2、对负载内容屏蔽的消息传输;(这句话啥意思?反正我是没太懂)
3、使用 TCP/IP 提供网络连接;
4、有三种QoS消息发布服务质量:
QoS 0
代表,Sender (发送方)最多向Receiver(接收方)发送一次消息,如果发送失败了就依赖于TCP的重发机制,自身不会再次重发,不会关注Receiver是否真的收到消息。
QoS 1
代表,Sender 发送的一条消息,Receiver 至少能收到一次。也就是说 Sender 会确定 Receiver 是否都到消息,如果发送失败,自身会重发,直到 Receiver 明确的告诉Sender收到消息为止。因为是自身重发,底层由基于TCP,TCP也有重发,因此Receiver 有可能会收到重复的消息。
QoS 2
代表,在QoS 1
的基础上增加了重发校验机制,确保消息不会重复发送,Receiver能有且仅收到一次消息。
5、小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量;
- 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。
基本理论差不多就这些,对于客户端来说已经受用。如果要更加深入的了解MQTT可以看看大佬翻译的IBM文档,想做相关优化而不仅仅是使用就耐着性子看完吧。
目前就iOS来说,一般使用MQTT都是用的MQTTKit和MQTTClient这两个开源库。不过MQTTKit确实很久没有更新了,最新一次更新都是2015年9月了。所以似乎大家都用的MQTTClient。
XMPP
XMPP是应用层的协议,是以XML对数据进行封装,基于TCP以数据流方式进行传输的协议。
XMPP中主要有三大角色
客户端:对信息的封装发送和解析接收
服务端:对信息的接收记录转发,因为大多数时候客户端发送数据给另一个客户端不是直接告诉客户端该用户的IP地址,而是userId。而服务端根据userId去查找该用户的IP。想当与一个路由功能
网关:这里的网关与计算机的网关不一样,也是XMPP的特点之一,毕竟XMPP完全开源可扩展,例如邮件方式,假设A用户想和B通信,他们两人的帐号分别在QQ和谷歌的服务器上。A用户客户端将讯息传送到QQ服务器。QQ服务器开启与谷歌服务器的连接。谷歌服务器将讯息寄送给B。两人的服务器是由两家不同的业者所提供的,而他们彼此传讯时,就是依靠网关处理完成发送
在iOS中一般使用XMPP都是用的XMPPFramework。由于本人没有用过这个库,所以代码暂时就不加进去了。等以后对这个有一定深度的掌握后加入代码。
后续会添加MQTT和XMPP的相关代码和Demo。
HTTP
HTTP称之为超文本传输协议(HTTP,HyperText Transfer Protocol),是最为常用的网络协议。所有的WWW文件都必须遵守这个标准。HTTP协议采用了请求/响应模型。客户端向服务器发送一个HTTP请求,HTTP会把内容封装成请求头,请求头中包含请求的方法、URL、协议版本、以及包含请求修饰符、客户信息和内容的类似于MIME的消息结构。服务器以一个状态行作为响应,响应的内容包括消息协议的版本,成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。
客户端向服务端发送请求所包含的内容
一个请求中包含
请求行 -> 通用信息头 -> 请求头 - >实体头 ->报文主体
除了主体,主要信息是 请求行
和请求头
请求行
POST /api/tool/getAreaList HTTP/1.1
第一部分POST是请求方式 常见的就GET 和 POST(Method)
第二部分/api/tool/getAreaList是请求的资源路径
第三部分HTTP/1.1是请求的协议版本 (Protocol)请求头
Host www.baidu.com
Content-Type application/x-www-form-urlencoded
Accept */*
User-Agent ETI/1.0.0 (iPhone; iOS 11.4; Scale/3.00)
Accept-Language zh-Hans-CN;q=1
Content-Length 75
Accept-Encoding gzip, deflate
Connection keep-alive
Host: 访问的主机名
Content-Type:客户端发送的数据类型
Accept: 客户端所能接收的数据类型,/表示任意类型
User-Agent: 客户端的类型,客户端的软件环境
Accept-Language: 客户端的语言环境
Content-Length:请求中内容的大小。这个大小是包含了内容编码的,比如上面请求是采用gzip压缩,那么Content-Length就是压缩后的大小
Accept-Encoding: gzip // 客户端支持的数据压缩格式
Connection:告诉服务器,请求完成后是关闭还是保持链接
Content-Type
Content-Type一般有这三种:
application/x-www-form-urlencoded
:数据被编码为名称/值对。这是标准的编码格式。它会将表单内的数据转换为键值对,比如,name=12&age = 18
multipart/form-data
: 它会将请求表表单中的每条数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来表名文件类型;content-disposition,用来说明字段的一些信息;由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。
text/plain
: 数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符。(RAW)。
form
的enctype
属性为编码方式,常用有两种:application/x-www-form-urlencoded
和multipart/form-data
,默认为application/x-www-form-urlencoded
。
当请求方式为GET
时候,浏览器用x-www-form-urlencoded
的编码方式把form
数据部分转换拼接成一个字符串(例如:name1=value1&name2=value2...),然后把这个字符串追加到url后面,用?分割,加载这个新的url。
当请求方式为POST
的时候,浏览器把form
数据封装到body
中,发送到服务端。 如果没有type=file的控件,用默认的application/x-www-form-urlencoded就可以了。 但是如果有type=file的话,就要用到multipart/form-data了。Content-Type类型是multipart/form-data,浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file),Content-Type(默认为text/plain),name(控件name)等信息,并加上分割符(boundary)。
Accept-Encoding
在Http中,可以采用gzip
编码方式,对body内容进行编码,来从而达到对内容压缩的方式。
首先
客户端发送request
给服务端, request 中有Accept-Encoding
,告诉服务端数据采用gzip编码压缩
接着
服务端接到request后, 生成原始的Response
, 其中有原始的Content-Type
和Content-Length
。
然后
通过服务端通过Gzip,来对Response进行编码, 编码后header中有Content-Type和Content-Length(压缩后的大小), 并且增加了Content-Encoding:gzip. 然后把Response发送给客户端。
最后
客户端接到Response后,会根据Content-Encoding
来对Response 进行解码。 获取到原始response后,再根据数据类型进行解析。
gzip
:表明实体采用GNU zip
编码
compress
:表明实体采用Unix
的文件压缩程序
deflate
表明实体是用zlib
的格式压缩的
identity
表明没有对实体进行编码。当没有Content-Encoding header
时, 就默认为这种情况
gzip, compress, 以及deflate编码都是无损压缩算法,用于减少传输报文的大小,不会导致信息损失。 其中gzip通常效率最高, 使用最为广泛
。
Connection
HTTP协议采用“请求(request
)-应答(response
)”模式,当Connection
为非KeepAliv时,每次请求/应答客户端和服务端都要新建一个连接,完成请求应答之后立即断开连接(此时HTTP协议为无连接的协议);当使用Keep-Alive
(又称持久连接、连接重用)时,Keep-Alive
功能使客户端到服 务器端的连接具有持续有效性,当出现对服务端的后继请求时,Keep-Alive功能避免了建立或者重新建立与服务端的连接,避免了建立/释放连接的开销,它更高效,性能更高
。
在
http 1.0
版本中Keep-Alive
默认是关闭的,需要在http头加入Connection: Keep-Alive
,才能启用Keep-Alive
;http 1.1
中默认启用Keep-Alive
,如果加入Connection: close
,才关闭。目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求了
服务端向客户端返回响应结果所包含的内容
一个响应内容有
状态行 -> 通用信息头 -> 响应头 -> 实体头 -> 报文主体
主要信息是响应行
和响应头
响应行
HTTP/1.1 200 OK
第一部分HTTP/1.1 表示HTTP协议版本号
第二部分200 状态码
第三部分OK 状态英文名称
响应头
Server: nginx/1.12.2
Date: Thu, 16 Aug 2018 06:26:21 GMT
Content-Type: application/json;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.2.8
Content-Encoding: gzip
Server:服务器类型
Date:返回响应的时间
Content-Type:返回的数据类型
Transfer-Encoding:数据传送的方式
Connection:连接方式
X-Powered-By:是用何种语言或框架编写
Content-Encoding: gzip // 服务端对数据的压缩格式
X-Powered-By
声明服务端使用何种语言或者框架编写的
PHP
标准输出是:X-Powered-By: PHP/7.2.8
,可在php.ini中增加或修改 expose_php = Off关闭。
ThinkPHP
标准输出是:X-Powered-By: ThinkPHP 2.0
,可修改相关类文件关闭
.net
标准输出是:X-Powered-By:ASP.NET
,可修改web.config 删除
Transfer-Encoding
服务端返回数据的方式
在HTTP1.1后数据传送方式都采用的
chunked
,分块编码。
分块编码是一种HTTP超文本协议传输数据的一种机制,它表示服务端传送给客户端的数据可以拆分为多个部分进行传输。数据拆分成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。
最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。一般
Content-Encoding
和Transfer-Encoding
二者经常会结合来用,其实就是针对 Transfer-Encoding 的分块再进行 Content-Encoding压缩
以上请求体和响应体只是常规的类型。有部分少见key没有说明。HTTP/1.0 定义了三种请求方法: GET, POST 和 HEAD方法,HTTP/1.1 新增了五种请求方法:OPTIONS、 PUT、DELETE、 TRACE 和 CONNECT 方法。HTTP/2.0仅适用于HTTPS
GET POST区别
- GET使用URL或Cookie传参。而POST将数据放在BODY中。
- GET的URL会有长度上的限制,而POST的数据则可以非常大。
- POST比GET安全,因为数据在地址栏上不可见。(相对而言,这句话并不准确。讨论的点)
HTTP协议明确地指出了,HTTP头和Body都没有长度的要求。而对于URL长度上的限制
使用http协议的GET方式,所有参数必须拼接放在URL的请求中,这个URL请求最大长度限制是2048个字符
http1.1与 http1.0的区别:
1、http1.1添加更多的缓存控制策略(如:Entity tag,If-Match)
2、http1.1支持断点续传
3、http1.1错误状态码的增多
4、http1.1Host头处理:支持Host头域,不在以IP为请求方标志
5、http1.1新增长连接:减少了建立和关闭连接的消耗和延迟。
http2.0与 http1.1的区别:
1、http2.0 使用二进制格式,1.0依然使用基于文本格式
2、http2.0 多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成正常的请求)
3、http2.0 压缩header:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的hearder大小
4、http2.0新增服务端推送
HTTPS
HTTPS是基于TCP传输层与HTTP应用层之间加入了SSL。让HTTP与TCP交互之间多了一层安全控制层。
SSL又叫安全套接字层,通过互相认证、使用数字签名确保完整性、使用加密确保私密性来保证安全通信。SSL分为两层,SSL记录协议层,它建立在TCP之上为高层协议提供数据的封装、压缩、加密等基本功能。SSL握手协议层,它建立在SSL记录层之上,主要是在传递数据之前对通信双方的身份认真并协商加密算法交换加密密钥。在SSL3.0之后,IETF指定了一种新协议,TLS,它是SSL3.0的后续版本。TLS与SSL算法不同,记录的版本号值不同,相比之下,TLS比SSL更加安全。
这里通过一张网上找到的HTTPS抓包图来让大家直观的了解下TLS与TCP的关系
前三行:是TCP常规的三次握手,建立连接。
第四行到最后一行:是TLS的四次握手。之间穿插的TCP的ACK请求是接收方为了确认接收到TLS信息而发送的确认序列,这是TCP常识。
这里详细说明下TLS的四次握手:
第一次握手
:首先客户端会发送一个client hello信息给服务端。
其中包含:
1.客户端支持的最高TLS的版本号
2.加密套件 cipher suites 列表
, 每个加密套件对应前面 TLS 原理中的四个功能的组合:认证算法 Au (身份验证)、密钥交换算法 KeyExchange(密钥协商)、对称加密算法 Enc (信息加密)和信息摘要 Mac(完整性校验)
3.支持的压缩算法
compression methods 列表,用于后续的信息压缩传输。
4.一个用于生成密钥的32字节的随机数
,
5.扩展字段 extensions
,支持协议与算法的相关参数以及其它辅助信息等。
第二次握手
:服务端会向客户端发送一个ServerHello应答
其中包含:
1.基于客户端支持的最高TLS版本号,而选择的通信使用的TLS版本号
。
2.一个用于生成密钥的32字节随机数
。
3.基于客户端的加密套件列表,而选择的加密套件
。
4.基于客户端的压缩算法列表,选择的压缩算法
。
5.server_certificates, 服务器端配置对应的证书链
,用于身份验证与密钥交换。
6.上图第七行,服务端发送 certificateserver hello done 通知客户端 server_hello 信息发送结束。
这里当客户端收到了serverHello信息结束后,客户端会去验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证包括如下:
• 证书链的可信性 trusted certificate path
• 证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同的客户端行为会不同;
• 有效期 expiry date,证书是否在有效时间范围;
• 域名 domain,核查证书域名是否与当前的访问域名匹配,匹配规则后续分析;
验证通过后才会进行第三次握手
第三次握手
:证书的合法性验证通过后发送消息,其中client_key_exchange是,合法性验证通过之后,客户端计算产生随机数字 Pre-master,并用证书公钥加密,发送给服务器,此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数与自己计算产生的 Pre-master,计算得到协商密钥, change_cipher_spec是,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信,encrypted_handshake_message是,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证。
第四次握手
:服务端在发送一个响应。
此时服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数,计算得到协商密钥。计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message是,验证数据和密钥正确性,change_cipher_spec是, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信,encrypted_handshake_message是, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端。