数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(比如以太网、令牌环等)上的传输。不同的物理网络具有不同的电气特性,网络驱动程序隐藏了这些细节,为上层协议提供一个统一的接口。 封装了物理网络的电气细节
ARP 、RARP(无盘工作站:缺乏存储设备,无盘工作站无法记住自己的IP地址 )
数据链路层使用物理地址寻址一台机器 ,实现IP地址和机器物理地址之间的相互转换
实现数据包的选路和转发 。封装了网络连接的细节
IP协议
IP协议根据数据包的目的IP地址来决定如何投递它 ,寻找一个合适的下一跳路由器,并将数据包交付给该路由器来转发
ICMP协议(因特网控制报文协议)
8位类型 :差错报文-----用来回应网络错误,比如 目标不可到达 和 重定向
查询报文-----查询网络信息 , 比如ping
8位代码 :进一步细分不同的条件,比如重定向报文使用代码值0表示对网络重定向,代码值1表示对主机重定向
16位校验和 :对整个报文(包括头部和内容部分)进行循环冗余校验 ,以检验报文在传输过程中是否损坏
传输层只关心通信的起始端和目的端,而不在乎数据包的中转过程 。封装了一条端到端的逻辑通信链路,它负责数据的收发、链路的超时重连等。
TCP协议:为应用层提供可靠的、面向连接的和基于流的服务
使用超时重传、数据确认等;通信的双方必须先建立TCP连接 ; 数据没有边界限制,发送端可以逐个字节地向数据流中写入数据,接收端也可以逐个字节地将它们读出
UDP协议:提供不可靠、无连接和基于数据报的服务
无法保证数据从发送端正确地传送到目的端,使用UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑; 每次发送数据都要明确指定接收端的地址 ;每个UDP数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断
SCTP协议
为了在因特网上传输电话信号而设计的
处理应用程序的逻辑,在用户空间实现,(数据链路层、网络层和传输层负责处理网络通信细节 ,在内核空间中实现 )。可以通过/etc/services文件查看所有知名的应用层协议,以及它们都能使用哪些传输层服务。
ping(应用程序):它利用ICMP报文检测网络连接 ,可跳过传输层直接使用网络层提供的服务
telnet :远程登录协议
OSPF(开放最短路径优先) : 一种动态路由更新协议,用于路由器之间的通信 ,可能跳过传输层直接使用网络层提供的服务
DNS 机器域名到IP地址的转换
应用程序数据沿着协议栈从上往下依次传递,每层协议都将在上层数据的基础上加上自己的头部信息
经过TCP封装后的数据称为TCP报文段,TCP头部信息和TCP内核缓冲区数据一起构成了TCP报文段,当发送端应用程序使用send(或者write)函数向一个TCP连接写入数据时,内核中的TCP模块首先把这些数据复制到与该连接对应的TCP内核发送缓冲区中,然后TCP模块调用IP模块提供的服务,传递的参数包括TCP头部信息和TCP发送缓冲区中的数据,即TCP报文段 。
经过UDP封装后的数据称为UDP数据报,UDP无须为应用层数据保存副本 ,当一个UDP数据报被成功发送之后,UDP内核缓冲区中的该数据报就被丢弃了。如果应用程序检测到该数据报未能被接收端正确接收,并打算重发这个数据报,则应用程序需要重新从用户空间将该数据报拷贝到UDP内核发送缓冲区中
经过IP封装后的数据称为IP数据报,数据部分就是一个TCP报文段、UDP数据报或者ICMP报文
经过数据链路层封装的数据称为帧,MTU(帧的最大传输单元),以太网帧的MTU是1500字节,过长的IP数据报可能需要被分片
帧到达目的主机时,将沿着协议栈自底向上依次传递,各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序 。帧的头部需要提供某个字段来区分不同协议
IP地址到以太网地址的转换 。原理:主机向自己所在的网络广播一个ARP请求,该请求包含目标机器的网络地址。此网络上的其他机器都将收到这个请求,但只有被请求的目标机器会回应一个ARP应答,其中包含自己的物理地址。
通常,ARP维护一个高速缓存,其中包含经常访问,或最近访问的机器的IP地址到物理地址的映射,避免了重复的ARP请求
将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据,或者是从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据
应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而精细地控制底层通信的行为,如设置TTL
为上层协议提供无状态、无连接、不可靠的服务。
无状态 :所有IP数据报的发送、传输和接收都是相互独立、没有上下文关系的 ,接收端的IP模块无法检测到乱序和重复(IP数据报头部标识字段用来处理IP分片和重组,不是用来指示接收顺序 )
无连接 :IP通信双方都不长久地维持对方的任何信息,每次发送数据需指明对方的IP地址
不可靠 :不能保证IP数据报准确地到达接收端,上层协议需要自己实现数据确认、超时重传等机制
长度为1501字节的IP数据报被拆分成两个IP分片,第一个IP分片长度为1500字节,第二个IP分片的长度为21字节。每个IP分片都包含自己的IP头部(20字节),且第一个IP分片的IP头部设置了MF标志,而第二个IP分片的IP头部则没有设置该标志,因为它已经是最后一个分片了。原始IP数据报中的ICMP头部内容被完整地复制到了第一个IP分片中。第二个IP分片不包含ICMP头部信息,因为IP模块重组该ICMP报文的时候只需要一份ICMP头部信息,重复传送这个信息没有任何益处。1473字节的ICMP报文数据的前1472字节被IP模块复制到第一个IP分片中,使其总长度为1500字节,从而满足MTU的要求;而多出的最后1字节则被复制到第二个IP分片中。
IP模块接收到来自数据链路层的IP数据报时 ,先对该数据报的头部做CRC校验,无误分析其头部 ;如果头部设置了源站选路选项,则IP模块调用数据报转发子模块来处理该数据报 ,如果该IP数据报是发送给本机的,则IP模块就根据数据报头部中的协议字段来决定将它派发给哪个上层应用 ;如果该IP数据报不是发送给本机的,也调用数据报转发子模块来处理该数据报。
数据报转发子模块将首先检测系统是否允许转发,允许,然后将它交给IP数据报输出子模块;IP模块实现数据报路由的核心数据结构是路由表
路由表如何给定数据报的目标IP地:
1、查找路由表中和数据报的目标IP地址完全匹配的主机IP地址。如果找到,就使用该路由项,没找到则转步骤2
2、查找路由表中和数据报的目标IP地址具有相同网路ID的网络IP地址,如果找到,就使用该路由项;没找到则转步骤3
3、选择默认路由项,这通常意味着数据报的下一跳路由是网关
ICMP重定向报文可用于更新路由表,给源端发送一个ICMP重定向报文,以告诉它一个更合理的下一跳路由器。
面向连接:通信的双方先建立连接,为该连接分配必要的内核资源,以管理连接的状态和 连接上数据的传输,一对一通信
字节流:发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,应用程序对数据的发送和接收是没有边界限制的
可靠传输:采用发送应答机制,超时重传机制,TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层
通信的一端可以发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。TCP连接的这种状态称为半关闭
服务器对于客户端发送出的同步报文段没有应答,如果重连仍然无效,则通知应用程序连接超时
在这个状态,客户端连接要等待一段长为2MSL(Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时间 ,标准文档建议2 min。
TIME_WAIT状态存在的原因有两点:
可靠地终止TCP连接。--报文段7丢失 ,那么服务器将重发结束报文段 ,客户端需要停留在某个状态以处理重复收到的结束报文段
保证让迟来的TCP报文段有足够的时间被识别并丢弃。--防止应用程序能够立即建立一个和刚关闭的连接相同的IP地址和端口号 (可能接收到属于原来的连接的应用程序数据 )
TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接 ,3种情况:
1、访问不存在的端口和TIME_WAIT状态的连接
2、异常终止连接
3、客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快
用于迅速通告对方本端发生的重要事件,比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据 。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。
TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式 。根据紧急指针所指的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1字节,称为带外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将覆盖它
TCP服务必须能够重传超时时间内未收到确认的TCP报文段。TCP模块为每个TCP报文段都维护一个重传定时器
TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是所谓的拥塞控制 .四个部分 :慢启动、拥塞避免、快速重传和快速恢复
控制发送端向网络一次连续写入的数据量SWND(发送窗口 ),不过,发送端最终以TCP报文段来发送数据,所以SWND限定了发送端能连续发送的TCP报文段数量。发送端需要合理地选择SWND的大小。如果SWND太小,会引起明显的网络延迟;反之,如果SWND太大,则容易导致网络拥塞。
发送端需要合理地选择SWND的大小。接收方可通过其接收通告窗口(RWND)来控制发送端的SWND 。但这显然不够,所以发送端引入了一个称为拥塞窗口(Congestion Window,CWND)的状态变量。实际的SWND值是RWND和CWND中的较小者
慢启动和拥塞避免
TCP连接建立好之后,CWND将被设置成初始值IW(Initial Window),其大小为2~4个SMSS(TCP报文段的最大长度(仅指数据部分)),其值一般等于MSS,此后发送端每收到接收端的一个确认,增加
当CWND的大小超过慢启动门限该值时,TCP拥塞控制将进入拥塞避免阶段
快速重传和快速恢复
发送端如果连续收到3个重复的确认报文段,就认为是拥塞发生了。后它启用快速重传和快速恢复算法来处理拥塞,过程如下:
1.当收到第3个重复的确认报文段时,按照式(3-3)计算ssthresh,然后立即重传丢失的报文段,并按照式(3-4)设置CWND。
2.每次收到1个重复的确认时,设置CWND=CWND+SMSS。此时发送端可以发送新的TCP报文段(如果新的CWND允许的话)
3.当收到新数据的确认时,设置CWND=ssthresh(ssthresh是新的慢启动门限值,由第一步计算得到)。
快速重传和快速恢复完成之后,拥塞控制将恢复到拥塞避免阶段,这一点由第3步操作可得知。
在HTTP通信链上,客户端和目标服务器之间通常存在某些中转代理服务器,它们提供对目标资源的中转访问。一个HTTP请求可能被多个代理服务器转发,后面的服务器称为前面服务器的上游服务器。
正向代理服务器
客户端自己设置代理服务器的地址。客户的每次请求都将直接发送到该代理服务器,并由代理服务器来请求目标资源。
反向代理服务器
反向代理则被设置在服务器端,因而客户端无须进行任何设置。用代理服务器来接收Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从内部服务器上得到的结果返回给客户端。
透明代理服务器
透明代理只能设置在网关上。 用户访问Internet的数据报必然都经过网关,如果在网关上设置代理,则该代理对用户来说显然是透明的。透明代理可以看作正向代理的一种特殊情况
虽然IP数据报是先发送到路由器,但IP头部的源端IP地址和目的端IP地址在转发过程中是始终不变的,帧头部的源端物理地址和目的端物理地址在转发过程中则是一直在变化的。
通过域名来访问Internet上的某台主机时,需要使用DNS服务来获取该主机的IP地址。但如果我们通过主机名来访问本地局域网上的机器,则可通过本地的静态文件来获得该机器的IP地址
GET http://www.baidu.com/index.html HTTP/1.0 User-Agent:Wget/1.12(linux-gnu) Host:www.baidu.com Connection:close
HTTP/1.0 200 OK Server:BWS/1.0 Content-Length:8024 Content-Type:text/html;charset=gbk SetCookie:BAIDUID=A5B6C72D68CF639CE8896FD79A03FBD8:FG=1;expires=Wed,04- Jul-42 00:10:47 GMT;path=/;domain=.baidu.com Via:1.0 localhost(squid/3.0 STABLE18)
HTTP协议是一种无状态的协议,使用额外的手段来保持HTTP连接状态,常见的解决方法就是Cookie。 Cookie是服务器发送给客户端的特殊信息(通过HTTP应答的头部字段“SetCookie”),客户端每次向服务器发送请求的时候都需要带上这些信息(通过HTTP请求的头部字段“Cookie”)。这样服务器就可以区分不同的客户了。基于浏览器的自动登录就是用Cookie实现的。
#include<arpa/inet.h> in_addr_t inet_addr(const char*strptr); int inet_aton(const char*cp,struct in_addr*inp); char*inet_ntoa(struct in_addr in);
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。 用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。
inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中 ,成功时返回1 ,失败则返回0
inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定目标存储单元的大小。 成功时返回目标存储单元的地址 ,失败则返回NULL
#include<sys/socket.h> int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len); int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。 成功时返回0,失败 返回-1
getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同
struct hostent*gethostbyname(const char*name); struct hostent*gethostbyaddr(const void*addr,size_t len,int type); struct hostent { char*h_name;/*主机名*/ char**h_aliases;/*主机别名列表,可能有多个*/ int h_addrtype;/*地址类型(地址族)*/ int h_length;/*地址长度*/ char**h_addr_list/*按网络字节序列出的主机IP地址列表*/ };
gethostbyname函数根据主机名称获取主机的完整信息, gethostbyaddr函数根据IP地址获取主机的完整信息。
#include<netdb.h> struct servent*getservbyname(const char*name,const char*proto); struct servent*getservbyport(int port,const char*proto); struct servent { char*s_name;/*服务名称*/ char**s_aliases;/*服务的别名列表,可能有多个*/ int s_port;/*端口号*/ char*s_proto;/*服务类型,通常是tcp或者udp*/ };
getservbyname函数根据名称获取某个服务的完整信息, getservbyport函数根据端口号获取某个服务的完整信息。
例子
#include<sys/socket.h> #include<netinet/in.h> #include<netdb.h> #include<stdio.h> #include<unistd.h> #include<assert.h> int main(int argc,char*argv[]){ assert(argc==2); char*host=argv[1]; /*获取目标主机地址信息*/ struct hostent*hostinfo=gethostbyname(host); assert(hostinfo); /*获取daytime服务信息*/ struct servent*servinfo=getservbyname("daytime","tcp"); assert(servinfo); printf("daytime port is%d\n",ntohs(servinfo->s_port)); struct sockaddr_in address; address.sin_family=AF_INET; address.sin_port=servinfo->s_port; /*注意下面的代码,因为h_addr_list本身是使用网络字节序的地址列表,所以使用其 中的IP地址时,无须对目标IP地址转换字节序*/ address.sin_addr=*(struct in_addr*)*hostinfo->h_addr_list; int sockfd=socket(AF_INET,SOCK_STREAM,0); int result=connect(sockfd,(struct sockaddr*)& address,sizeof(address)); assert(result!=-1); char buffer[128]; result=read(sockfd,buffer,sizeof(buffer)); assert(result>0); buffer[result]='\0'; printf("the day tiem is:%s",buffer); close(sockfd); return 0; }
#include<netdb.h> int getaddrinfo(const char*hostname,const char*service,const struct addrinfo*hints,struct addrinfo**result); struct addrinfo { int ai_flags;/*见后文*/ int ai_family;/*地址族*/ int ai_socktype;/*服务类型,SOCK_STREAM或SOCK_DGRAM*/ int ai_protocol;/*见后文*/ socklen_t ai_addrlen;/*socket地址ai_addr的长度*/ 主机的别名char*ai_canonname;/*主机的别名*/ struct sockaddr*ai_addr;/*指向socket地址*/ struct addrinfo*ai_next;/*指向下一个sockinfo结构的对象*/ };
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。
hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)。
service参数可以接收服务名,也可以接收字符串表示的十进制端口号。
hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
例子
struct addrinfo hints struct addrinfo*res; bzero(&hints,sizeof(hints)); hints.ai_socktype=SOCK_STREAM; getaddrinfo("ernest-laptop","daytime",&hints,&res);
getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:
void freeaddrinfo(struct addrinfo*res);
#include<netdb.h> int getnameinfo(const struct sockaddr*sockaddr,socklen_t addrlen,char*host,socklen_t hostlen,char*serv,socklen_t servlen,int flags);
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。