分层模型
OSI7层模型
-
物理层主要定义物理设备标准, 如网线的接口类型, 光纤的接口类型, 各种传输介质的传输速率等. 它的主要作用是传输比特流(就是1/0转化为电流强弱来进行传输, 达到目的地后再转化为1/0, 也就是我们常说的数模转换与模数转换), 这一层的数据叫做比特. -
数据链路层定义了如何让格式化数据以帧为单位进行传输, 以及如何让控制对物理介质的访问. 这一层通畅还提供错误检测和纠正, 以确保数据的可靠传输. 如: 串口通信中使用到的115200, 8, N, 1 -
网络层在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择, Intener的发展使得从世界各站点访问信息的用户数大大增加, 而网络层正式管理这种连接的层 -
传输层定义了一些传输数据的协议和端口号(WWW端口80等), 如: TCP(传输控制协议, 传输效率低, 可靠性强, 用于传输可靠性要求高, 数据量大的数据); UDP(用户数据报协议, 与TCP特性相反, 用户传输可靠性要求不高, 数据量小的数据. 如视频聊天). 主要是将从下层接收的数据进行分段和传输, 到达目的地址后再进行重组, 这一层的数据通常被称为"段" -
会话层通过传输层(端口号-传输端口与接受端口)建立数据传输的通路, 主要在你的系统之间发起会话或者接受会话请求(设备之间要互相认识, 可以是IP也可以是MAC或者是主机名) -
表示层可确保一个系统的应用层所发送的信息可以被另一个系统的应用程序读取. 例如PC程序与另一台计算机来进行通信, 其中一台计算机使用扩展二一十进制交换码(EBCDIC), 而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符, 如有必要, 表示层会通过使用一种通格式来实现多种数据格式之间的转换 -
应用层是最靠近用户的OSI层, 这一层为用户的应用程序提供网络服务
TCP/IP四层模型
-
应用层应用程序, 如Telnet, FTP和email等. -
传输层TCP和UDP协议. -
网络层IP,ICMP和IGMP协议. -
设备驱动程序及接口卡
TCP/IP的通信过程
两台计算机通过TCP/IP协议通讯的过程如下所示
上图对应两台计算机在同一网段中的情况, 如果两台计算机在不同的网段中, 那么数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器, 如下图所示
-
链路层有以太网, 令牌环网等标准,链路层负责网卡设备的驱动, 帧同步(即从网线上检测到什么信号算作新帧的开始), 冲突检测(如果检测到冲突就自动重发), 数据差错校验等工作. 交换机是工作在链路层的网络设备, 可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间, 以太网和令牌环网之间),由于不同链路层的帧格式不同, 交换机要将进来的数据包拆掉链路层首部重新封装之后再转发. -
网络层的IP协议是构成Inernet的基础.Internet上的主机通过ip地址来标识, Internet上有大量路由器负责根据ip地址选择合适的路径转发数据包, 数据包从Internet上的源主机到目的主机往往要经过十多个路由器. 路由器是工作在第三层的网络设备, 同时兼有交换机的功能, 可以在不同的链路层接口之间转发数据包, 因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装.ip协议不保证传输的可靠性, 数据包在传输过程中可能丢失, 可靠性可以在上层协议或应用程序中提供支持.
网络层负责点到点的传输, 而传输层负责端到端的传输
-
tcp是一种面向连接的, 可靠的协议.tcp传输的双方需要首先建立连接, 之后由tcp协议保证数据收发的可靠性, 丢失的数据包自动重发, 上层应用程序收到的总是可靠的数据流, 通讯之后关闭连接. -
udp是无连接的传输协议, 不保证可靠性,使用udp协议的应用程序需要自己完成丢包重发, 消息排序等工作.
协议格式
传输层及其以下的机制由内核提供, 应用层程序由用户提供, 应用程序对通讯数据的含义进行解释, 而传输层及其以下处理通讯的细节, 将数据从一台计算机通过一定的路径发到另一台计算机. 应用层数据通过协议栈发到网络上时, 每层协议都要加上一个数据首部(header), 称为封装.
以太网帧格式
其中的源地址和目的地址是指网卡的硬件地址(mac), 长度是48位, 是在网卡出厂时固化的. 可以在shell中使用ifconfig查看. 协议字段有三种值, 分别对应ip, apr, rapr. 帧尾是crc校验码.
以太网帧中的数据长度规定最小46字节, 最大1500字节, arp和rarp数据包的长度不够46字节, 要在后面补填充位. 最大值1500称为以太网的最大传输单元(MTU), 不同的网络类型有不同的MTU, 如果一个数据包从以太网路由到拨号链路上, 数据包长度大于拨号链路的MTU, 则需要对数据包进行分片. ifconfig命令输出中也有"MTU:1500". 注意: MTU这个概念指数据帧中有效载荷的最大长度, 不包括帧头长度.
IP段格式
ip数据包的首部长度和数据长度都是可变长的, 但是总是4字节的整数倍. 对于IPv4, 4位版本字段是4, 4位首部长度的数值是以4字节为单位的, 最小值为5, 也就是说首部长度最小是4 * 5 = 20字节, 不带任何选项的ip首部, 4位能表示的最大值是15, 也就是说首部长度最大是60字节. 8位TOS字段有3个位用来指定ip数据报的优先级(目前已废弃), 还有4个位表示可选的服务类型(最小延迟, 最大吞吐量, 最大可靠性, 最小成本), 还有一个位总是0. 总长度是整个数据报的字节数. 每传一个ip数据报, 16位的标志+1, 可用于分片和重新组装数据报. 3位标志和13位片偏移用于分片. TTL(time to live)是这样用的: 源主机为数据包设定一个生存时间, 比如64, 每过一个路由器就把该值-1, 如果减到0就表示路由器已经太长了仍然找不到目的的主机网络, 就丢弃该包, 因此这个生存时间的单位不是秒, 而是跳(hop). 协议字段指示上层协议是tcp, udp, icmp, igmp. 然后是校验和, 只校验ip首部, 数据的校验由更高层协议负责. IPv4的ip地址长度为32位.
TCP数据报格式
16位源端口号和16位目的端口号, 通讯的双方由IP地址和端口号标识. 32位序号, 32位确认序号, 16位滑动窗口, 4位首部长度: 可以标识首部长度最大是15, 而又以4字节为单位, 因此首部长度最大是4 * 15 = 60字节. 如果没有选项字段, TCP协议头最短20字节(32位源目的端口号 + 32位序号 + 32位确认序号 + 16位滑动窗口 + 4位首部长度 + 6位unused + 6位控制位 + 16位校验和 + 16位紧急指针 = 160位 = 20字节). URG, ACK, PSH, RST, SYN, FIN是6个控制位. 16位校验和将tcp协议头和数据都计算在内.
TCP协议
TCP通信时序
在这个例子中, 首先客户端主动发起连接, 发送请求, 然后服务器端响应请求, 然后客户端主动关闭连接. 由于数据从一端到另一端也需要时间, 因此图中箭头都应该是斜的. 双方发送的段按时间顺序编号为1-10, 各段中的主要信息在箭头上标出. 例如段2的箭头上标着SYN 8000(0), ACK1001, 表示该段中的syn位置1, 32位序号是8000, 该段不携带任何有效载荷(数据为0字节), ack位置1, 32位确认序号是1001, 带有一个mss(Maximum Segment Size, 最大报文长度)选项值为1024.
三次握手
客户端发送一个带SYN标识的TCP报文到服务器. 对应上图的段1: (SYN 1000(0) <mss:1460>
客户端发出段1, SYN位表示连接请求, 序号是1000, 这个序号在网络通讯中用作临时的地址, 每发送1个数据字节, 这个序号就+1, 这样在接收端可以根据序号排除数据包的正确顺序, 也可以发现丢包的情况, 另外, 规定SYN位和FIN位也要占一个序号, 这次虽然没发送数据, 但是由于发送了SYN位, 因此下次再发送应该用序号1001. mss:1460表示最大段尺寸, 如果一个段太大, 封装成帧后超过了链路层的最大帧长度, 就必须在IP层分片了, 为了避免这种情况, 客户端声明自己的最大段尺寸, 建议服务器发来的段不要超过这个长度.
服务器端回应客户端, 对应上图的段2: (SYN 8000(0), ACK 1001, <mss:1024>), 同时带ACK标识和SYN标识. 它标识对刚才客户端SYN的回应, 同时又发送SYN给客户端, 询问客户端是否准备好进行数据通讯.
服务器发出段2, 也带有SYN位, 同时置ACK位表示确认, 确认序号是1001, 表示自己已经接收到序号1000及以前所有的段, 告知客户端下次发送序号为1001的段. 段2这个操作就表明服务器端已经答应了客户端的连接请求, 同时也给客户端发出一个连接请求, 同时声明最大尺寸为1024(mss:1024).
客户端必须再次回应服务器端一个ACK报文, 对应上图段3: (ACK 8001)
客户端发出段3, 对服务器的连接请求进行应答, 确认序号是8001. 在这个过程中, 客户端和服务器端分别给对方发了连接请求, 也应答了对方的连接请求. 其中服务器的请求和应答在一个段中发出, 因此一共有3个段用于建立连接, 称为三次握手. 在建立连接的同时, 双方协商了一些信息, 例如双方发送序号的初始值, 最大段尺寸等.
RST位置段
在TCP通讯中, 如果一方收到另一方发来的段, 读出其中的目的端口号, 发现本机并没有任何进程使用这个端口, 就应答一个包含RST位的段给对方. 例如服务器并没有任何进程使用8000端口, 但是却使用telnet客户端去连接8000, 服务器收到客户端发来的SYN段就会应答一个RST段, 客户端的telnet程序收到RST段后报告错误: Connection refused.
数据传输过程
-
客户端发出段4, 包含从序号1001开始的20个字节数据: (1001(20) ACK 8001)
-
服务器端发出段5, 确认序号为1021, 对序号为1001-1020的数据表示确认收到: (8001(10), ACK 1021), 同时请求发送序号1021开始的数据, 服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据.
-
客户端发出段6, 对服务器发来的序号为8001-8010的数据表示确认收到(ACK 8011), 请求发送序号8011开始的数据.
在数据传输的过程中, ACK和确认序号是非常重要的, 应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中, 发出数据包给对方之后, 只有收到对方应答的ACK段才知道该数据包确实发送成功了, 可以从发送缓冲区中释放掉了, 如果因为网络故障丢失了数据包或者丢失了对方发挥的ACK段, 经过等待超时后TCP协议自动将发送缓冲区中的数据包重新发送.
四次挥手关闭连接
由于TCP连接是全双工的, 因此每个方向都必须单独进行关闭. 这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接. 收到一个FIN只意味着这一方向上没有数据流动, 一个TCP连接在收到一个FIN后仍能发送数据. 首先进行关闭的一方将执行主动关闭, 另一方执行被动关闭.
-
客户端发出段7, FIN位表示关闭连接的请求
-
服务器发出段8, 应答客户端的关闭连接请求
-
服务器发出段9, 其中也包含FIN位, 向客户端发送关闭连接请求
-
客户端发出段10, 应答服务器的关闭连接请求
建立连接的过程是三次握手, 而关闭连接需要4个段, 服务器的应答和关闭连接请求通畅不合在一起并成一个段, 因为有连接半关闭的情况, 这种情况下客户端关闭连接之后就不能再发送数据给服务器了, 但是服务器还可以发送数据给客户端, 直到服务器也关闭连接为止.
滑动窗口(TCP流量控制)
如果发送端发送数据的速度较快, 接收端收到数据后处理的速度较慢, 而接收缓冲区的大小是固定的, 就会丢失数据. TCP协议通过滑动窗口机制解决了这个问题.
-
发送端发起连接, 声明最大段尺寸是1460, 初始序号是0,
窗口大小4096, 表示发送端的接收缓冲区还有4096个字节空闲, 告知服务器程序发的数据不要超过4096字节. 接收端应答连接请求,声明最大段尺寸是1024, 初始序号是8000, 窗口大小是6144字节. 发送端应答, 三次握手结束, -
发送端发出段4-9, 每个段携带1024的数据包, 发送端根据窗口大小知道了接收端的缓冲区满了, 因此停止发送数据.
-
接收端的应用程序读取2048字节的数据, 接收缓冲区释放了2k空间, 于是这2k空间空闲出来了, 接收端发出段10, 在应答已收到6k数据的同时, 声明窗口大小为2k.
-
接收端应用程序又读取了2k的数据, 此时接收端的接收缓冲区就有4k的空闲了, 于是发送端11, 重新声明窗口大小为4k.
-
发送端发出段12-13, 每个段带2k数据, 段13同时还包含FIN位.
-
接收端应答收到了2k的数据(6145-8192), 再加上FIN位占一个序号, 因此应答序号是8194, 连接处于半关闭状态, 接收端同时声明窗口大小为2k.
-
接收端的应用程序读取2k数据, 接收端重新声明窗口大小为4k.
-
接收端的应用程序读取剩下的2k数据, 接收缓冲区全控, 接收端重新声明窗口大小为6k.
-
接收端的应用程序在读取完全部数据后, 发出段17包含FIN位, 发送端应答, 连接完全关闭.
从这个例子可以看出, 由于三次握手的时候, 接收端告知了发送端它的mss是1024, 因此发送端是1k1k地发送数据, 而接收端的应用程序可以2k2k地读取数据, 当然也有可能是3k4k地读取数据. 也就是说, 应用程序所看到的的数据是一个整体, 或者说是一个流(stream), 在底层通讯中这些数据可能被拆成很多数据包来发送, 但是一个数据包有多少个字节对应用程序来说是不可见的, 因此TCP协议是面向流的协议. 而UDP是面向消息的协议, 每个UDP段都是一条消息, 应用程序必须以消息为单位去读取数据, 不能一次读取任意字节的数据, 这一点和TCP是很不同的.
半关闭
当TCP连接中A发送FIN请求关闭, B端回应ACK后, A段会进入FIN_WAIT_2状态, B没有立即发送给A时, A方处于半关闭状态, 此时A可以接收B发送的数据, 但是A已经不能再向B发送数据.
这是由于TCP协议是一个全双工的, 一个TCP连接, 在内核中有发送缓冲区和接收缓冲区这两块内核内存空间, 当A端主动关闭连接时, B端回应ACK后, A端将会释放自己的发送缓冲区内存, 只剩下了接收缓冲区, B端将会释放自己的接收缓冲区, 这时候A就只能接收数据了(这里的数据指真正载荷的数据, 并不代表不能发送ACK响应了), B也就只能发送数据了. 然后B再发送FIN, 得到A的ACK应答之后, A的接收缓冲区也会被释放, B的发送缓冲区也会被释放, 这样一个连接就关闭了.
从程序的角度, 可以使用api来控制实现半连接状态:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能, 此选项将不允许sockfd进行读操作.
该套接字**不再接受数据**, 任何当前在套接字接受缓冲区的数据将被无声的丢弃掉.
SHUT_WR(1): 关闭sockfd的写功能, 此选项将不允许sockfd进行写操作. 进程不能在对此套接字发出写操作.
SHUT_RDWR(2): 关闭sockfd的读写功能. 相当于调用shutdown两次: 首先是以SHUT_RD, 然后以SHUT_WR.
使用close终止一个连接, 但它只是减少描述符的引用计数, 并不是直接关闭连接, 只有当描述符的引用计数为0时, 才会真正关闭连接.
注意:
-
如果有多个进程共享一个套接字, close每调用一次, 计数-1, 直到计数为0时, 也就是所用进程都调用了close, 套接字将被释放. -
在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)之后, 其它的进程将无法进行通信. 但是如果一个进程close(sfd)将不会影响其它进程.