HTTP协议之:TCP连接详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/grassroots2011/article/details/50376035

TCP连接

HTTP请求连接过程

HTTP是一个应用层的协议,在这个层的协议,是一种网络交互需要遵守的一种协议规范。
1. 浏览器解析出主机名
2. 浏览器查询这个主机名的IP地址(DNS)
3. 浏览器获得端口号
4. 浏览器发起到目标IP:端口的连接,建立一个socket连接。因为socket是通过ip和端口建立的,所以,之前则还有一个DNS解析过程。如把www.baidu.com变成一个ip,如果url不包含端口号,则会使用该协议的默认端口号,HTTP协议的默认端口号为80。
5. 浏览器向服务器发送一条HTTP报文(这个请求一般是GET或POST请求。)
6. 浏览器从服务器读取HTTP报文(包括:HTTP头信息,体信息)。
7. 浏览器关闭连接(当应答结束后,web浏览器与web服务器必须断开,以保证其它web浏览器能够与web服务器建立连接。)
这里写图片描述

HTTP和HTTPS网络协议

这里写图片描述

HTTP和HTTPS区别:

  • https协议需要到ca申请证书,一般免费证书很少,需要交费。
  • http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议 http和https使用的是完全不同的连接方式用的端口也不一样:前者是80,后者是443。
  • http的连接很简单,是无状态的 HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全

HTTP要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的TCP连接按序传输。
TCP收到数据流后,将数据流砍成被称作段的小数据块,并将段封装在IP分组中,通过网络传输,如上图。
每个TCP段都是由IP分组承载,从一个IP地址发送到另一个IP地址的。
每个IP分组中都包括:
* 一个IP分组首部(通常为20字节)
* 一个TCP段首部(通常为20字节)
* 一个TCP数据块(0个或多个字节)

TCP套接字编程

这里写图片描述

TCP编程接口

套接字API调用 描述
s = sokect(< parameters>) 创建一个新的,未命名,未关联的套接字
bind(s,< local IP:port>) 向套接字赋一个本地端口号和接口
connect(s,< remote IP:port>) 创建一条连接本地套接字与远程主机及端口的连接
listen(s,…) 标识一个本地套接字,使其可以合法接受连接
s2 = accept(s) 等待某人建立一条到本地端口的连接
n = read(s, buffer, n) 尝试从套接字向缓冲区读取n个字节
n = write(s, buffer, n) 尝试从缓冲区中向套接字写入n个字节
close(s) 完全关闭TCP连接
shutdown(s, < side>) 只关闭TCP连接的输入或输出端
getsokect(s, …) 读取某个内部套接字配置选项的值
setsokect(s, …) 修改某个内部套接字配置选项的值

socket函数

套接字是通信端点的抽象,实现端对端之间的通信。与应用程序要使用文件描述符访问文件一样,访问套接字需要套接字描述符。任何套接字编程都必须调用socket 函数获得套接字描述符,这样才能对套接字进行操作。以下是该函数的描述:

/* 套接字 */   

/*  
 * 函数功能:创建套接字描述符;  
 * 返回值:若成功则返回套接字非负描述符,若出错返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   

int socket(int family, int type, int protocol);   
/*  
 * 说明:  
 * socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;  
 * family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:  
 * (1)AF_INET         IPv4因特网域  
 * (2)AF_INET6        IPv6因特网域  
 * (3)AF_UNIX         Unix域  
 * (4)AF_ROUTE        路由套接字  
 * (5)AF_KEY          密钥套接字  
 * (6)AF_UNSPEC       未指定  
 *  
 * type确定socket的类型,常用类型如下:  
 * (1)SOCK_STREAM     有序、可靠、双向的面向连接字节流套接字  
 * (2)SOCK_DGRAM      长度固定的、无连接的不可靠数据报套接字  
 * (3)SOCK_RAW        原始套接字  
 * (4)SOCK_SEQPACKET  长度固定、有序、可靠的面向连接的有序分组套接字  
 *  
 * protocol指定协议,常用取值如下:  
 * (1)0               选择type类型对应的默认协议  
 * (2)IPPROTO_TCP     TCP传输协议  
 * (3)IPPROTO_UDP     UDP传输协议  
 * (4)IPPROTO_SCTP    SCTP传输协议  
 * (5)IPPROTO_TIPC    TIPC传输协议  
 *  
 */   

connect函数

在处理面向连接的网络服务时,例如 TCP ,交换数据之前必须在请求的进程套接字和提供服务的进程套接字之间建立连接。TCP 客户端可以调用函数connect 来建立与 TCP 服务器端的一个连接。该函数的描述如下:

/*  
 * 函数功能:建立连接,即客户端使用该函数来建立与服务器的连接;  
 * 返回值:若成功则返回0,出错则返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);   
/*  
 * 说明:  
 * sockfd是系统调用的套接字描述符,即由socket函数返回的套接字描述符;  
 * servaddr是目的套接字的地址,该套接字地址结构必须包含目的IP地址和目的端口号,即想与之通信的服务器地址;  
 * addrlen是目的套接字地址的大小;  
 *  
 * 如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址,即内核会确定源IP地址,并选择一个临时端口号作为源端口号;  
 */   

TCP 客户端在调用函数 connect 前不必非得调用 bind 函数,因为内核会确定源 IP 地址,并选择一个临时端口作为源端口号。若 TCP 套接字调用connect 函数将建立 TCP 连接(执行三次握手),而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:

  • 若 TCP 客户端没有收到 SYN 报文段的响应,则返回 ETIMEOUT 错误;
  • 若客户端的 SYN 报文段的响应是 RST (表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接。只是一种硬错误,客户端一接收到 RST 就立即返回ECONNERFUSED 错误;
  • RST 是 TCP 在发生错误时发送的一种 TCP 报文段。产生 RST 的三个条件时:
    • 目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器
    • TCP 想取消一个已有连接
    • TCP 接收到一个不存在的连接上的报文段
  • 若客户端发出的 SYN 在中某个路由器上引发一个目的地不可达的 ICMP 错误,这是一个软错误。客户端主机内核保存该消息,并在一定的时间间隔继续发送 SYN (即重发)。在某规定的时间后仍未收到响应,则把保存的消息(即 ICMP 错误)作为EHOSTUNREACH 或ENETUNREACH 错误返回给进行。

bind函数

调用函数 socket 创建套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它与一个地址相关联,可以调用函数bind 使其与地址绑定。客户端的套接字关联的地址一般可由系统默认分配,因此不需要指定具体的地址。若要为服务器端套接字绑定地址,可以通过调用函数 bind 将套接字绑定到一个地址。下面是该函数的描述:

/* 套接字的基本操作 */   

/*  
 * 函数功能:将协议地址绑定到一个套接字;其中协议地址包含IP地址和端口号;  
 * 返回值:若成功则返回0,若出错则返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);   
/*  
 * 说明:  
 * sockfd 为套接字描述符;  
 * addr是一个指向特定协议地址结构的指针;  
 * addrlen是地址结构的长度;  
 */   
  • 若 TCP 客户端或服务器端不调用bind 函数绑定一个端口号,当调用connect 或 listen 函数时,内核会为相应的套接字选择一个临时端口号。
  • 一般 TCP 客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind 函数将端口号与相应的套接字绑定。
  • 对于 TCP 客户端,这就为在套接字上发送的 IP 数据报指派了源 IP 地址
  • 对于 TCP 服务器端,这就限定该套接字只接收那些目的地为这个 IP 地址的客户端连接
  • TCP 客户端一般不把 IP 地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器端所需的路径
  • 若 TCP 服务器端没有把 IP 地址捆绑到它的套接字上,内核就把客户端发送的 SYN 的目的 IP 地址作为服务器端的源 IP 地址

在地址使用方面有下面一些限制:

    在进程所运行的机器上,指定的地址必须有效,不能指定其他机器的地址;
    地址必须和创建套接字时的地址族所支持的格式相匹配;
    端口号必须不小于1024,除非该进程具有相应的特权(超级用户);
    一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定;

listen函数

在编写服务器程序时需要使用监听函数 listen 。服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。listen 函数描述如下:

/*  
 * 函数功能:接收连接请求;  
 * 函数原型:  
 */   
#include <sys/socket.h>   

int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;   
/*  
 * sockfd是套接字描述符;  
 * backlog是该进程所要入队请求的最大请求数量;  
 */   
  • 当 socket 函数创建一个套接字时,若它被假设为一个主动套接字,即它是一个将调用connect 发起连接的客户端套接字。listen 函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求;
  • listen 函数的第二个参数规定内核应该为相应套接字排队的最大连接个数;
  • listen 函数一般应该在调用socket 和bind 这两个函数之后,并在调用 accept 函数之前调用。 内核为任何一个给定监听套接字维护两个队列:
    • 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于 SYN_REVD 状态
    • 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态

accept函数

accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。该函数描述如下:

/* 函数功能:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠;  
 * 函数原型:  
 */   
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//返回值:若成功返回套接字描述符,出错返回-1;   
/*  
 * 说明:  
 * 参数 cliaddr 和 addrlen 用来返回已连接的对端(客户端)的协议地址;  
 *  
 * 该函数返回套接字描述符,该描述符连接到调用connect函数的客户端;  
 * 这个新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接,  
 * 而是继续保持可用状态并接受其他连接请求;  
 * 若不关心客户端协议地址,可将cliaddr和addrlen参数设置为NULL,否则,在调用accept之前,应将参数cliaddr设为足够大的缓冲区来存放地址,  
 * 并且将addrlen设为指向代表这个缓冲区大小的整数指针;  
 * accept函数返回时,会在缓冲区填充客户端的地址并更新addrlen所指向的整数为该地址的实际大小;  
 *  
 * 若没有连接请求等待处理,accept会阻塞直到一个请求到来; 

fork和exec函数

/* 函数功能:创建子进程;  
 * 返回值:  
 * (1)在子进程中,返回0;  
 * (2)在父进程中,返回新创建子进程的进程ID;  
 * (3)若出错,则范回-1;  
 * 函数原型:  
 */   
#include <unistd.h>   
pid_t fork(void);   
/* 说明:  
 * 该函数调用一次若成功则返回两个值:  
 * 在调用进程(即父进程)中,返回新创建进程(即子进程)的进程ID;  
 * 在子进程返回值是0;  
 * 因此,可以根据返回值判断进程是子进程还是父进程;  
 */   

/* exec 序列函数 */   

/*  
 * 函数功能:把当前进程替换为一个新的进程,新进程与原进程ID相同;  
 * 返回值:若出错则返回-1,若成功则不返回;  
 * 函数原型:  
 */   
#include <unistd.h>   
int execl(const char *pathname, const char *arg, ...);   
int execv(const char *pathnam, char *const argv[]);   
int execle(const char *pathname, const char *arg, ... , char *const envp[]);   
int execve(const char *pathnam, char *const argv[], char *const envp[]);   
int execlp(const char *filename, const char *arg, ...);   
int execvp(const char *filename, char *const argv[]);   
/*  6 个函数的区别如下:  
 * (1)待执行的程序文件是 文件名 还是由 路径名 指定;  
 * (2)新程序的参数是 一一列出 还是由一个 指针数组 来引用;  
 * (3)把调用进程的环境传递给新程序 还是 给新程序指定新的环境;  
 */   

当要求一个服务器同时为多个客户服务时,需要并发服务器。TCP 并发服务器,它们为每个待处理的客户端连接调用 fork 函数派生一个子进程。当一个连接建立时,accept 返回,服务器接着调用 fork 函数,然后由子进程服务客户端,父进程则等待另一个连接,此时,父进程必须关闭已连接套接字。

close/shutdown

/* 函数功能:关闭套接字,若是在 TCP 协议中,并终止 TCP 连接;  
 * 返回值:若成功则返回0,若出错则返回-1;  
 * 函数原型:  
 */   
#include <unistd.h>   
int close(int sockfd);   

/*  
 * 函数功能:关闭套接字上的输入或输出;  
 * 返回值:若成功则返回0,若出错返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   
int shutdown(int sockfd, int how);   
/*  
 * 说明:  
 * sockfd表示待操作的套接字描述符;  
 * how表示具体操作,取值如下:  
 * (1)SHUT_RD     关闭读端,即不能接收数据  
 * (2)SHUT_WR     关闭写端,即不能发送数据  
 * (3)SHUT_RDWR   关闭读、写端,即不能发送和接收数据  
 *  
 */  

getsockname/getpeername

为了获取已绑定到套接字的地址,我们可以调用函数 getsockname 来实现:

/*  
 * 函数功能:获取已绑定到一个套接字的地址;  
 * 返回值:若成功则返回0,若出错则返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);   
/*  
 * 说明:  
 * 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;  
 * 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错;  
 */   
/*  
 * 函数功能:获取套接字对方连接的地址;  
 * 返回值:若成功则返回0,若出错则返回-1;  
 * 函数原型:  
 */   
#include <sys/socket.h>   

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);   
/*  
 * 说明:  
 * 该函数除了返回对方的地址之外,其他功能和getsockname一样;  
 */   

这里写图片描述

TCP性能

HTTP紧挨着TCP,位于其上层,所以HTTP事务的性能在很大程度上取决于底层TCP通道的性能。

HTTP事务的时延

几个主要原因:
1. 客户端首先需要根据URI确定WEB服务器的IP地址和端口号。如果最近没有对URI中的主机名进行访问,通过DNS解析系统讲URI中的主机名转换成一个IP地址可能要花费数十秒的时间。
2. 接下来,客户端会向服务器发送一条TCP连接请求,并等待服务器回送一个请求接受应答。每条新的TCP连接都会有连接建立时延。这个值通常最多只有一两秒,但如果有数百个HTTP事务的话,这个值会快速叠加上去。
3. 一旦连接建立起来了,客户端就会通过新建立的TCP管道来发送HTTP请求。数据到达时,WEB服务器会从TCP连接中读取请求报文,并对请求进行处理。
4. 然后,WEB服务器会回送HTTP响应,这也需要花费时间。
这些TCP网络时延的大小取决于硬件速度,网络速度和服务器的负载,请求和报文响应的尺寸,以及客户端和服务器之间的距离。TCP协议的技术复杂性也会对时延产生巨大的影响。

常见TCP相关时延

  • TCP连接建立握手
  • TCP慢启动拥塞控制
  • 数据聚集的Nagle算法
  • 用于捎带确认的TCP延迟确认算法
  • TIME_WAIT时延和端口耗尽

TCP连接的握手时延

建立一条新的TCP连接时,甚至是在发送任意数据之前,TCP软件之间会交换一系列IP分组,对连接的有关参数进行沟通,如果连接只用来传输少量数据,这些交换过程会严重降低HTTP的性能。

TCP连接握手步骤

这里写图片描述
* 请求新的TCP连接时,客户端要向服务器发送一个小的TCP分组(通常是40 ~ 60个字节)。这个分组中设置了一个特殊的SYN标记,说明这是一个连接请求。
* 如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,这个分组中的SYN和ACK标记被置位,说明连接请求已被接受
* 最后,客户端向服务器回送一条确认信息,通知它连接已成功建立,现代的TCP栈都允许客户端在这个确认分组中发送数据。

通常HTTP事务都不会交换太多数据,此时,SYN/SYN+ACK握手会产生一个可测量的时延。TCP连接的ACK分组通常都足够大,可以承载整个HTTP请求报文,而且很多HTTP服务器响应报文都可以放入一个IP分组中去(比如,响应是包含了装饰性图片的小型HTML文件,或者是对浏览器高速缓存请求产生的304 Not Modified响应)

最后的结果是,小的HTTP事务可能会在TCP建立上花费50%,或更多时间。

关于详细资料可参阅:
http://blog.163.com/xychenbaihu@yeah/blog/static/13222965520118139252103/

TCP慢启动

TCP数据传输的性能还取决于TCP连接的使用期(age)。TCP连接会随着时间进行自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐被称为TCP慢启动(show start),用于防止因特网的突然过载和拥塞。

TCP慢启动限制了一个TCP端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个HTTP事务有大量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确认;然后可以发送两个分组,每个分组都必须被确认,这样就可以发送四个分组了,以此类推。

Nagle算法/TCP_NODELAY

  • Nagle算法试图在发送一个分组之前,将大量TCP数据绑定在一起,以提高网络效率。
  • Nagle鼓励发送全尺寸(LAN上最大尺寸的分组大约是1500字节,在因特网上是几百字节)的段。只有当所有其他分组都被确认之后,Nagle算法才允许发送非全尺寸的分组。如果其他分组任然在传输过程中,就将那部分数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。
  • Nagle算法会引发几种HTTP性能问题
    • 小的HTTP报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。
    • 与延迟确认之间的交互存在问题—Nagle算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟100 ~ 200毫秒

TCP_NODELAY参数设置,禁用Nagle算法

TIME_WAIT积累与端口耗尽

  • 当某个TCP端点关闭TCP连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的IP地址和端口号。
  • 这类信息会维持一小段时间,通常是所估计的最大分段使用期的2倍(称为2MSL,通常为2分钟)左右
  • 以确保在这段时间内不会创建具有相同地址和端口号的新连接。
  • 连接率就被限制在了60000/120 = 500次/秒。

猜你喜欢

转载自blog.csdn.net/grassroots2011/article/details/50376035