Http & Socket & Tcp/Ip
三者之间的关系以及在网络层中的位置
网络层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
Http:简单对象访问协议,对应于应用层,基于TCP连接实现。HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。
Socket:是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。
Socket是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
他们再网络层中的位置:
两个总结的点:
我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容。如果想要使传输的数据有意义,则必须使用到应用层协议。应用层提供了封装或者显示数据的具体形式。
Socket跟TCP/IP协议没有必然的联系。 Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象。TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。
Socket详解
Socket可以基于TCP实现,也可以基于UDP实现。
下面以TCP协议通信为例,流程大致如下:
网络中进程之间如何通信?
如何表示网络中的一个进程?
- 在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。
- 在网路中,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
Socket只是个编程接口
TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。
Socket是一组编程接口(API), 是对TCP/IP协议的封装和应用。应用层不必了解TCP/IP协议细节。直接通过对Socket接口函数的调用完成数据在IP网络的传输。
Socket的基本操作
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
所以Socket可以理解成一种特殊的文件,是“open-write/read-close”模式的一种实现。
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
- domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
- 流套接字(SOCK_STREAM):流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP。
- 数据报套接字(SOCK_DGRAM):数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
- 原始套接字(SOCK_RAW):原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
- SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数把一个地址族中的特定地址赋给socket。否则就当调用connect()、listen()时系统会自动随机分配一个端口。
- sockfd就是上面的Socket描述符。
int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
int accept(int sockfd, struct sockaddr addr, socklen_t addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
read()、write()等函数
int close(int fd)
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
socket中TCP的三次握手建立连接详解
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
- 客户端向服务器发送一个SYN J
- 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
- 客户端再想服务器发一个确认ACK K+1
socket中TCP的四次握手释放连接详解
- 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
- 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
- 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
- 接收到这个FIN的源发送端TCP对它进行确认。
Http详解
Http Header
Request
被称作“first line”的第一行包含三个部分:
- “method” 表明这是何种类型的请求. 最常见的请求类型有 GET, POST 和 HEAD.
- “path” 体现的是主机之后的路径.
- “protocol” 包含有 “HTTP” 和版本号, 现代浏览器都会使用1.1.
比较常见的Header参数:
Host:一个HTTP请求会发送至一个特定的IP地址,但是大部分服务器都有在同一IP地址下托管多个网站的能力,那么服务器必须知道浏览器请求的是哪个域名下的资源。
User-Agent:这个头部可以携带如下几条信息:
- 浏览器名和版本号.
- 操作系统名和版本号.
- 默认语言.
这就是某些网站用来收集访客信息的一般手段。例如,你可以判断访客是否在使用手机访问你的网站,然后决定是否将他们引导至一个在低分辨率下表现良好的移动网站。
Accept-Language:这个信息可以说明用户的默认语言设置。如果网站有不同的语言版本,那么就可以通过这个信息来重定向不同的网址。
Accept-Encoding:大部分的现代浏览器都支持gzip压缩,并会把这一信息报告给服务器。这时服务器就会压缩过的HTML发送给浏览器。这可以减少近80%的文件大小,以节省下载时间和带宽。
If-Modified-Since:如果一个页面已经在你的浏览器中被缓存,那么你下次浏览时浏览器将会检测文档是否被修改过,那么它就会发送这样的头部:
If-Modified-Since: Sat, 28 Nov 2009 06:38:19 GMT
如果自从这个时间以来未被修改过,那么服务器将会返回“304 Not Modified”,而且不会再返回内容。浏览器将自动去缓存中读取内容
Cookie:顾名思义,他会发送你浏览器中存储的Cookie信息给服务器。
- Referer:先前网页的地址,当前请求网页紧随其后,即来路
更详细的一个表格:
Response
Cache-Control:缓存的控制
Cache-Control: max-age=3600, public
“public”意味着这个响应可以被任何人缓存,“max-age” 则表明了该缓存有效的秒数。允许你的网站被缓存降大大减少下载时间和带宽,同时也提高的浏览器的载入速度。
Content-Type:这个头部包含了文档的”mime-type”。浏览器将会依据该参数决定如何对文档进行解析。
浏览器可以通过mime-type来决定使用外部程序还是自身扩展来打开该文档。如下的例子降调用Adobe Reader:
Content-Type: application/pdf
直接载入,Apache通常会自动判断文档的mime-type并且添加合适的信息到头部去。并且大部分浏览器都有一定程度的容错,在头部未提供或者错误提供该信息的情况下它会去自动检测mime-type。
Content-Length:当内容将要被传输到浏览器时,服务器可以通过该头部告知浏览器将要传送文件的大小(bytes)。对于文件下载来说这个信息相当的有用。这就是为什么浏览器知道下载进度的原因。
Etag:这是另一个为缓存而产生的头部信息。它看起来会是这样:
Etag: "pub1259380237;gz"
服务器可能会将该信息和每个被发送文件一起响应给浏览器。该值可以包含文档的最后修改日期,文件大小或者文件校验和。浏览会把它和所接收到的文档一起缓存。下一次当浏览器再次请求同一文件时将会发送如下的HTTP请求:
If-None-Match: "pub1259380237;gz"
如果所请求的文档Etag值和它一致,服务器将会发送304状态码,而不是2oo。并且不返回内容。浏览器此时就会从缓存加载该文件。
Last-Modified:最后修改时间
Location:这个头部是用来重定向的。如果响应代码为 301 或者 302 ,服务器就必须发送该头部。
header('Location: http://net.tutsplus.com/');
默认会发送302状态码,如果你想发送301,就这样写:
header('Location: http://net.tutsplus.com/', true, 301);
Set-Cookie:当一个网站需要设置或者更新你浏览的cookie信息时。每个cookie会作为单独的一条头部信息。注意,通过js设置cookie将不会体现在HTTP头中。
更详细的response:
Http断点续传的实现
HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下:
- 客户端下载一个1024K的文件,已经下载了其中512K
- 网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段:Range:bytes=512000-这个头通知服务端从文件的512K位置开始传输文件
- 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes 512000-/1024000并且此时服务端返回的HTTP状态码应该是206,而不是200。
但是在实际场景中,会出现一种情况,即在终端发起续传请求时,URL对应的文件内容在服务端已经发生变化,此时续传的数据肯定是错误的。如何解决这个问题了?显然此时我们需要有一个标识文件唯一性的方法。在RFC2616中也有相应的定义,比如实现Last-Modified来标识文件的最后修改时间,这样即可判断出续传文件时是否已经发生过改动。同时RFC2616中还定义有一个ETag的头,可以使用ETag头来放置文件的唯一标识,比如文件的MD5值。
终端在发起续传请求时应该在HTTP头中申明If-Match 或者If-Modified-Since 字段,帮助服务端判别文件变化。
Http1.0无法实现断点续传功能。
TCP协议缺陷
三次握手
据说已经有两次握手的解决方案。TCP Fast Open (TFO)扩展机制。
慢启动(拥塞控制)
为了防止网络的拥塞现象,TCP提出了一系列的拥塞控制机制,即慢启动。通过观察到新分组进入网络的速率与另一端返回ACK的速率是否相同。
慢启动为TCP的发送方增加了一个拥塞窗口(cwnd),当连接建立时,拥塞窗口被初始化为一个报文段大小,每收到一个ACK,拥塞窗口就会增加一个报文段,发送方取拥塞窗口与通过窗口的最小值作为发送的上限。
具体来说,当新建连接时,cwnd初始化为1个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd就增加1倍MSS大小。这样cwnd的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长。
如果带宽为W,那么经过RTT*log2W时间就可以占满带宽。
所以这个的问题就是,需要经过一定的时间后才能完全的利用整个的带宽。
滑动窗口协议(不是缺陷~~)
发送方的窗口大小由接受方确定,目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。
线头阻塞(Head-of-line blocking, HOL)
TCP协议数据传输需要按序传输,可以理解为FIFO先进先出队列,当前面数据传输丢失后,后续数据单元只能等待,除非已经丢失的数据被重传并确认接收以后,后续数据包才会被发送。
解决方案:
虚拟输出队列(Virtual Output Queues)总体的想法十分朴素:在输入端口将发送到不同端口的数据包虚拟成不同的队列,并且彼此互不影响,这样一来即使队头数据包被阻塞也将不会影响发送到其他端口的数据包的发送。
四次摆手
两端连接成功建立之后,需要关闭时,需要产生四次交互,这在移动互联网环境下,显得有些多余。快速关闭,快速响应,冗余交互导致网络带宽被占用。
终端IP漫游
手机终端经常在2G/3G/4G和WIFI之间切换,导致IP地址频繁发生改变。这样造成的后果就是已有的网络请求-响应被放弃和终止,需要人工干预或重新发起请求,存在资源浪费现象。
支持Multipath TCP的终端设备,可以同时利用 2G/3G/4G 和 WiFi 建立Mutlpath连接,通过多点优化网络下载,且互为备份。可以很好解决多个网络共存的情况下,一个网络中断不会导致全局请求处理中断,在设备的连接稳定和可靠性方面有所增强。
当然,服务器之间也可以利用Multipath TCP的多个网络增强网络吞吐量。
实例说明:
假如我手中的 iPhone 同时开启了 3G 和 WiFi 连接(大多数情况也是这样),这个时候我通过 App Store 更新一个 100M 的软件。按照以往的情况,App Store 的软件更新是会优先通过 WiFi 进行的,3G 在此刻是闲置状态。
但是在 Multipath TCP 的支援下,尽管只通过 App Store 更新一个软件,建立起了一个网络连接,但是它却可以同时利用 3G 和 WiFi 建立 Mutlpath 连接,通过多点优化网络下载,且互为备份。
假如这个时候 WiFi 断了,以前的情况是,App Store 更新中断,需要人工干预恢复或重新下载。而在 Mutlipath TCP 的优化下,只要 3G 没断,App Store 就能继续更新下载。除非 3G 也断了,才宣告此次连接失败。
Http 1.0
- HTTP 1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。
- 连接无法复用,导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。
- 线头阻塞,head of line blocking问题,会导致健康的请求会被不健康的请求影响
Http 1.1
HTTP 1.1支持持久连接(HTTP/1.1的默认模式使用带流水线的持久连接),在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输,但每个单独的网页文件的请求和应答仍然需要使用各自的连接。
HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间。
- request和reponse头中都有可能出现一个connection的头,此header的含义是当client和server通信时对于长链接如何进行处理。client和server都是默认对方支持长链接的,如果不想使用长链接,则只要将connection设置为close即可,不论request还是response的header中包含了值为close的connection,都表明当前正在使用的tcp链接在当天请求处理完毕后会被断掉。
- HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。HTTP/1.0不支持文件断点续传,RANGE:bytes是HTTP/1.1新增内容,HTTP/1.0每次传送文件都是从文件头开始,即0字节处开始。
总结
- 是否支持长连接
- host头域:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname).但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址. HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request).此外,服务器应该接受以绝对路径标记的资源请求.
- 请求方法的扩充:HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT这些Request方法.
- 新增status code:HTTP1.0没有定义任何具体的1xx status code, HTTP1.1有2个等等
- 是否支持断点续传
- 请求是否需要等待响应才能发下个请求,即PipeLining管道。
- 网络请求的内容长度更加准确:通常,HTTP应答消息中发送的数据是整个发送的,Content-Length消息头字段表示数据的长度.数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始.但是在一些动态网页中,由于网页是动态生成的,所以没法计算出准确的Content-Length,这样导致的后果是:如果 Content-Length 比实际长度短,会造成内容被截断;如果比实体内容长,会造成 pending,浏览器一直转圈圈. 所以在HTTP1.1中引入了Transfer-Encoding,如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束. ps:如果同时设置了Content-Length 和Transfer-Encoding,那么Transfer-Encoding的优先级更高,Content-Length会被忽略.
- 缓存的灵活性:为了使caching机制更加灵活,HTTP/1.1增加了Cache-Control头域(请求消息和响应消息都可使用),它支持一个可扩展的指令子集:例如max-age指令支持相对时间戳;private和no-store指令禁止对象被缓存;no-transform阻止Proxy进行任何改变响应的行为.
http 2.0
多路复用 (Multiplexing)
允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。与1.1不同的是,2.0的response不必跟请求顺序一致,保证了更快的返回。
通过 Stream ID 标识,所有的请求和响应都可以欢快的同时跑在一条 TCP 链接上了。
二进制分帧
在应用层与传输层之间增加一个二进制分帧层,以此达到“在不改动HTTP的语义,HTTP 方法、状态码、URI及首部字段的情况下,突破HTTP1.1的性能限制,改进传输性能,实现低延迟和高吞吐量。”
http1.x诞生的时候是明文协议,其格式由三部分组成:start line(request line或者status line),header,body。要识别这3部分就要做协议解析,http1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑http2.0的协议解析决定采用二进制格式,实现方便且健壮。
在二进制分帧层上,HTTP2.0会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。
头部压缩
HTTP/1.1并不支持 HTTP 首部压缩,为此 SPDY 和 HTTP/2 应运而生, SPDY 使用的是通用的 DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法。
前面提到过http1.x的header由于cookie和user agent很容易膨胀,而且每次都要重复发送。http2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。
如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新。
请求优先级
多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
服务端推送
http1.x只能由客户端发起请求,然后服务器被动的发送response。开启server push之后,server通过X-Associated-Content header
(X-开头的header都属于非标准的,自定义header)告知客户端会有新的内容推送过来。在用户第一次打开网站首页的时候,server将资源主动推送过来可以极大的提升用户体验。
服务端暗示
和server push不同的是,server hint并不会主动推送内容,只是告诉有新的内容产生,内容的下载还是需要客户端主动发起请求。server hint通过X-Subresources header
来通知,一般应用场景是客户端需要先查询server状态,然后再下载资源,可以节约一次查询请求。
移动网络的困难
一段时间内的(15s)连接复用对PC端浏览器的体验帮助很大,因为大部分的请求在集中在一小段时间以内。但对移动app来说,成效不大,app端的请求比较分散且时间跨度相对较大。
移动网络延迟高的原因
唤醒延迟
手机要接入网络,必须先向RPC控制平台发送申请。只有当控制平台将手机切换到Active状态,手机才能进行通信,这一过程在3G网络下消耗的时间一般在500-2500ms之间,我们称之为唤醒延迟。这是一个造成移动网络延迟的原因。
HTTP延迟
优化策略
- 不用域名,用 IP 直连
- 连接复用
- 减小请求数据大小
- CDN 缓存静态资源
- 减小返回数据大小
- 精简数据格式
- 增量更新
- 数据缓存
- 预取
- 分优先级、延迟部分请求
- 针对不同网络的特殊化处理