目录
传输层
端口号
UDP协议(User Datagram Protocol, 用户数据报协议)
UDP报文格式
UDP的特点
协议实现(原理) / 特性对于上层应用层代码编写的影响 (我们用UDP协议时该注意什么?)
TCP协议(Transmission Control Protocol, 传输控制协议)
TCP报文格式
TCP三次握手与四次挥手
close()shutdown()的区别及使用场景
三次挥手和四次挥手中的一些问题
TCP特点
面向字节流
可靠传输
传输层负责应用程序之间的数据传输, 也就是负责数据能够从发送端传输到接收端 .
一个发送的数据在网络中, 用IP地址来确定这条数据来自哪台主机(源IP地址), 要发往哪台主机(目的IP地址). 但当一台主机接收到一条网络数据时, 主机里有不止一个应用程序在运行着, 谁来接收这条网络数据呢 ? 所以还需要一个信息来标识, 这条数据是哪个应用程序的, 这个标识就是端口号.
源IP地址和目的IP地址是在传输层下层的网络层IP协议中封装在头部的. 而端口号封装在传输层协议的头部.
这是为什么呢? 因为传输层的作用就是负责应用程序之间的数据传输, 在网络上中数据时如何传输是下层网络层该操心的事, 当数据到达对端主机时, 哪个应用程序接收才是传输层关心的事, 所以IP地址封装在网络层, 端口号封装在传输层.
在传输层的上层应用层中, 用一些常用协议搭建的服务器都有一些固定端口号, 如下:
在Linux中, 在 /etc/services 中保存着这些知名端口, 我们自己写的程序要避开这些知名端口
端口号的划分范围
问题
1. 一个进程是否可以bind 多个端口号? (bind指bind()接口, 用于给socket套接字绑定自己的地址信息(包括源IP地址和源端口号))
答 : 一个进程可以bind多个端口
2.一个端口号是否可以被多个进程 bind?
答 : 一个端口号只能被一个进程bind
注 : 在Linux中, 这里的一个进程可以理解为一个PCB, 因为Linux中的线程是轻量级进程, 所以线程也有相同特性, 所也就能说是一个PCB可以bind多个端口号, 一个端口号只能被一个PCB所bind.
相关命令
netstat : 用于查看Linux中网络状态
pidof : 用于查找指定名称的进程的进程id
语法 : pidof 进程名
1. 由于UDP报头中只用了16位2字节来标识整个UDP报文的大小, 所以, UDP报文的大小是有限制的, 16位最大能表示的数是65535, 即UDP报文最大是65535个字节, 即64K, 但报文中头部还占了8字节, 所以, UDP所能发送的数据最大是64k - 8. 但由于传输层协议在下层网络层中还要进行封装, 而网络层的IP协议中, 整个IP报文最大也只能是64K, 而IP协议报文头部最小也要20字节, 这样一来, UDP报文中数据最大也只能是64k - 20 - 8, 如下图:
注意 : 值得注意的是, 这里限制UDP报文大小的根本原因不是UDP长度标识是16位的数据, 如果是这样, 那要想数据再长些给更多位不就好了. 其实UDP报文不能太大的根本原因是UDP协议是面向数据报传输, 也就是在调用sendto()接口发送数据时, socket会将数据直接交给内核封装UDP报头然后一次性发送出去 (注意 : UDP是没有真正意义上的发送缓冲区的), 如果这个数据很大, 那么发送失败的几率就很大, 所以, UDP报文数据不能太大的原因就是因为其面向数据报传输, 如果数据很大, 发送失败的几率就很大
刚说到, UDP没有真正意义上的缓冲区, 什么意思呢? 因为UDP是不可靠传输, 其发送失败也不会保存应用程序的数据拷贝(比如说, 应用程序产生了一个临时的数据需要发送, 之后就会销毁这个数据, 此时UDP发送失败了, 那么这条数据就没有了, 因为这个数据是在应用程序中被销毁, 而UDP也没有将其先放在发送缓冲区中).
2. 如果发送的数据大于64K-20-8, 则不能一次性发送, 需要用户在上层应用层手动进行分包操作(将一个大数据截断为多个小数据,分别发送). 由于UDP协议是不可靠传输, 发送的数据并不能有序到达, 还可能会丢失, 所以需要我们程序员手动在应用层进行包序管理.
3. 在使用UDP协议时, 由于收发都是整个报文一次性收发, 所以用户的接收缓冲区要定义的足够大, 否则recvfrom()接收缓冲区满了之后之后的数据就会接被丢弃.
三次握手建立连接
第一次握手 :
client : 客户端向服务端发送连接请求SYN包(发送连接请求)(SYN=1, 同时选择一个初始序号seq=x)后, 客户端进入SYN-SENT状态, 等待服务器确认回复.
server :当服务端还没有接收到客户端的连接请求时, 服务端处于LISTEN状态.
第二次握手 :
server : 当服务端收到客户端的连接请求时(收到syn包), 为新的连接请求创建新的通信socket, 此时服务端必须确认客户端的SYN请求(回复确认序号ack = x + 1), 确认序号有效ACK=1, 因为连接是双向的, 所以服务端也向客户端发送连接请求SYN包(SYN = 1, 为自己选择一个初始序号seq = y), 即服务端向客户端发送 ACK+SYN 包, 服务端进入SYN_RCVD状态. 当第二次握手完成, 还没进行第三次握手时, 此时TCP连接的状态称之为半连接状态.
第三次握手 :
client : 当客户端收到服务端回复的SYN+ACK包时, 确认建立连接(客户端这边已经没什么问题了, 可以通信了), 并回复给服务端确认信息ACK包(seq = x + 1, ack = y+1), 客户端的进入ESTABLISHED状态, 完成连接
server : 当服务端收到客户端发送的ACK包后, 确认客户端连接就绪, 可以开始通行, 进入ESTABLISHED状态
四次挥手断开连接
图片来源于网络第一次挥手 :
client : 当客户端确定不再需要发送数据时, 调用 close(sockfd) / shutdown(sockfd, SHUT_WR) (两者的区别以及用法下面说). 客户端会向服务端发送FIN包(FIN=1, seq = u)(u就是客户端之前收到的数据的最后一个字节的序号+1), 客户端进入FIN_WAIT1状态. (注意 : TCP协议规定, FIN报文段就算没有数据, 也需要消耗一个序号)
server : 当服务端未收到客户端发送的FIN包时, 一直处于ESTABLISHED状态
第二次挥手 :
server : 当服务端收到客户端发来的FIN包后, 知道客户端不会再发送数据了, 也就不需要接受, 先调用close(sockfd) / shutdown(sockfd, SHUT_RD), 再确认回复客户端, 即(ACK=1, ack = u+1), 并且带上自己的序列号seq=v, 此时服务端的进入了CLOSE_WAIT状态. TCP服务端就通知高层的应用进程, 客户端不会再向服务端发送数据了, 此时TCP通信的连接状态就称为半关闭状态,即客户端已经没有数据要发送了, 但是服务器若发送数据, 客户端依然要接受. 这个状态还要持续一段时间, 也就是整个CLOSE_WAIT状态持续的时间.
client : 当客户端收到服务器的确认请求(ACK包)后, 此时, 客户端就进入FIN_WAIT2状态, 等待服务器发送FIN (在这之前还需要接收服务器发送的最后的数据)
第三次挥手 :
server : 服务端将最后的数据发送完毕后, 再不需要发送数据了, 就调用shutdown(sockfd, SHUT_WR) , 再向客户端发送FIN包, 由于在半关闭状态, 服务器很可能又向客户端发送了一些数据, 假定此时的序列号为seq=w,即FIN包(ACK=1, seq=w, ack=u+1). 此时, 服务端就进入了LAST_ACK(最后确认)状态, 等待客户端的确认.
第四次挥手 :
client : 当客户端收到服务端的连接释放请求(FIN包)时, 必须发出确认, ACK=1,ack=w+1, 而自己的序列号是seq=u+1, 此时,客户端就进入了TIME_WAIT状态. 注意 : 此时TCP连接还没有释放, 必须经过2倍的MSL(最长报文段寿命)的时间后, 当客户端释放连接后, 才进入CLOSED状态.
server : 服务端只要收到了客户端发出的确认(ACK包), 立即进入CLOSED状态. 同样, 释放TCB连接后, 就结束了这次的TCP连接. 可以看到, 服务端结束TCP连接的时间要比客户端早一些.
注意 : 需要注意的是, 四次挥手可以是由客户端首先发送FIN包触发, 也可以由服务端首先发送FIN包触发.
由于TCP是全双工双的, 所以连接的断开也需要单独将两个通道拆除, 四次挥手所做的事情就是拆除两条通道和释放资源.
那么已经完成三次握手的TCP连接, 什么时候会触发四次握手呢 ? 从逻辑上, 是通信结束, 不需要这个TCP连接时, 从具体操作上, 就涉及到两个系统调用接口close()和shutdown()
对于close(), 当调用close(sockdfd)只会造成套接字文件描述符的引用计数减一, 当引用计数为0时, 调用方主动发送FIN包, 触发四次挥手. 例如在多进程服务器中, 父子进程共享socket文件描述符, 当父进程或者某个子进程调用close(sockfd) 时, socket文件描述的引用计数减一, 直到父进程和所有的子进程都调用close(sockfd)后(引用计数为0), 才会发生四次挥手, 释放资源.
对于shutdown(sockfd, how), 是拆分四次挥手过程, 在设置how参数为SHUT_WR或者SHUT_RDWR时, 会立即发送FIN, 触发四次挥手. 无论socket文件描述符引用计数是多少, 只要任一进程调用该接口都会破坏所有进程的连接, 任一进程在该描述符上读取数据都会收到EOF结束符, recv()返回0, 写数据时会收到SIGPIPE信号. 因为其并不释放资源, 所以最后还需要调用close().
对于FIN包, shutdown()和close()都能发送FIN包, 对于发送FIN包, 是想要告诉对方, 我不再向你发送数据了
close()和shutdown()的使用场景 : 多进程服务器, 就不能用shutdown()只能用close(), 客户端可以用shutdown(), 也可以用close(). 多进程服务器端, 每监听到一个连接就会创建一个子进程, shutdown会关闭服务器监听工作, 也会关闭其他正在与不同客户端通信的进程. 客户端可以使用shutdown(), 因为客户端和服务器端一般只有一条连接,可以使用shutdown(). 当客户端与服务器端有多条连接且是在同一个进程中, 最好用close(), 以免影响到其他连接通信.
int shutdown(int sockfd, int how)
头文件 : sys/socket.h
功能 : 关闭socket的全部或部分全双工连接, 只关闭其读写功能, 并不释放资源
参数 : sockfd : socket的操作句柄, 即文件描述符
how : 有三种选择 : SHUT_RD : 值为0, 关闭读端
SHUT_WR : 值为1, 关闭写端
SHUT_RDWR : 值为2, 关闭读端和写端由于TCP是全双工通信, 所以会存在半连接状态. 即当主动端关闭写端代表不再发送数据, 被动端关闭读端代表不再接收数据(但还是能对收到的数据进行确认回复).
返回值 : 正确执行返回0, 错误返回-1, 并设置errno
int close(int fd)
头文件 : unistd.h
参数 : fd : 文件描述符
返回值 : 正确执行返回0, 错误返回-1, 并设置errno
1. 为什么不是两次/四次握手而是三次握手?
答 : 两次不安全, 四次没必要; 三次握手完成两个重要功能, 一是确保通信双方都准备好了(双方各自都知道对方准备好了), 二是双方就初始序列号进行协商(这个序列号在握手过程中被发送和确认).
两次不安全 : 假设一个客户端像一个服务端发送连接请求报文段(SYN包), 服务器端收到SYN包并回复确认应答报文段(ACK+SYN), 如果是两次握手那么此时连接已建立, 可以开始通信(发送数据报文段). 如果有以下可能, 就会造成不安全的情况. 如 : 服务端的ACK包在传输过程中丢失, 那么客户端就不知道服务端是否已经准备好, 不知道服务端建议用什么样的序号用于客户端和服务端之间的传输, 也不知道服务器是否同意自己发送的初始序列号, 甚至怀疑服务端是否收到自己发送的的SYN包
在这种情况下, 客户端既不会向服务端发送数据报文段, 也不会接收服务端发送来的数据报文段(就算服务端发送了, 客户端还以为没建立连接, 当然不会接收), 服务端发给客户端的数据报文段没有得到客户端的确认应答, 服务端就开始只等待接收客户端的确认应答报文段了, 而等到服务器怀疑自己发出的数据报文段没有被客户端收到时, 就会重复发送同样的数据报文段(超时重传机制, 下面说), 就形成了死锁. 或者当客户端像服务端发出SYN包之后就关闭了(断网断电...), 服务端会出现相同的问题.
四次没必要 : 由于握手要确保通信双方都准备好了(双方各自都知道对方准备好了), 不仅仅客户端要向服务端发送连接请求SYN包, 服务端确认应答回复给客户端ACK包. 还要服务端向客户端发送连接请求SYN包, 客户端在确认应答回复给服务端ACK包, 这样双方都就放心了, 但中间服务端给客户端先回复ACK包, 再次向客户端发送请求SYN包, 这就有点憨了, 何不一起置1, 一次发送呢? 所以四次可以, 但没必要.
2. 为什么不是三次挥手而是四次挥手?
疑问 : 建立连接时需要三次握手, 那断开连接反着来不就好了, 不也只需要三次吗?
答 : 前面说到, FIN包的发送并不表示发送方不发送数据也不接收数据了, 而是告诉对方我不再给你发送数据报文段了, 但不代表发送发不再接收数据报文段了, 所以当客户端向服务端发送FIN包后, 服务端需要确认回复ACK包, 但不能像三次握手一样, ACK和FIN同时置1回复, 因为客户端(虽然不能发送数据报文段了, 但)还可能要接收服务端发送的的数据报文段, 所以服务端要先给客户端回复ACK包, 再向客户端发送FIN包, 在服务端发送ACK包和FIN包之间服务端还可能会向客户端发送数据报文段. 所以在客户端回复ACK包之后, 确定自己不再向客户端送数据报文段了时, 才会向客户端发送SYN包.
3. 为什么握手三次, 挥手四次?
答 : 也就是前两个问题, 因为当服务端端收到客户端的SYN连接请求报文(SYN包)后, 可以直接发送SYN+ACK报文. 其中ACK报文是用来应答的, SYN报文是用来请求连接的. 但是关闭连接时, 当服务端收到FIN报文时, 很可能并不会立即关闭SOCKET, 所以只能先回复一个ACK报文, 告诉客户端, "你发的FIN报文我收到了". 只有等到服务端所有的报文都发送完了, 才能向客户端发送FIN报文, 因此不能一起发送. 所以需要四次挥手.
4. 三次握手失败了, 服务端如何处理?
1. 如果是客户端发送的SYN包没有到达服务端, 服务器根本不知道有这个请求, 因此服务器没什么要处理的
2. 客户端发送了SYN后服务端收到并进行了SYN+ACK报文的响应, 但没有收到客户端应答的ACK包. 服务端等待最后一个ACK超时后, 则会给客户端发送RST重置连接报文, 要求对方重新发起连接请求, 释放为本次连接请求创建的socket, 而并非重传SYN+ACK
5. 为什么FIN包最先发送方发送了ACK包后, 并没有直接进入CLOSED状态释放资源, 而是进入TIME_WAIT状态呢?
(为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSED状态?) (TIME_WAIT的作用?)
答 : 讲道理, FIN包先发送方在ACK包发送完了之后, 就可以直接进入CLOSED状态, 释放资源了, 但我们的网络不是一定可靠的, 有可能最后一个ACK包会丢失, 假设没有TIME_WAIT状态, 客户端在发送最后一个ACK包之后直接进如CLOSED状态释放资源, 客户端关闭. 而这个ACK包丢失在网络中, 服务端因为没有收到客户端ACK包, 向客户端超时重传了FIN包, 在服务端向已经关闭的客户端重传FIN包之前将客户端再重启. 如果重启后的客户端地址信息与之前关闭的客户端相同, 有两种情况 :
1. 刚启动的客户端就会收到一个服务端的FIN包. 就会对接下来客户端与服务端的连接造成影响.
2. 重启的客户端向服务端请求新连接发送SYN包, 服务端正在四次挥手, 处于LAST_ACK状态. 突然客户端来个SYN包, 服务端就会认为状态错误, 向客户端发送RST连接重置报文(RST包), 要求客户端重新建立连接. (分手分到一半, 对方说发个消息说做我女(男)朋友好吗, 懵不懵?, 卑微的我就心里想 : "我就当你脑子抽风了". 然后告诉对方, 我们重新来过吧)
所以, 在FIN包先发送方在ACK包发送完了之后, 进入TIME_WAIT状态进过两个MSL时间后再进入CLOSED状态, 就是为在这段时间内处理服务端可能重传的SYN包, 为了本次连接的所有数据都被处理或消失于网络之中, 不会对后续新连接造成影响.
MSL : 报文最大生存周期, 任何报文在网络上存在的最长时间, 超过这个时间报文将被丢弃(一个报文在网络中总不能一直传一直传, 可能造成占用大量带宽等问题, MSL就是限制报文在网络中一直传输的一种方式)
总结 : TIME_WAIT的作用 : 1. 可靠地实现TCP全双工连接的终止 2. 让"老"的数据在网络中被丢弃
5. 服务器上出现了大量的CLOSE_WAIT状态是什么原因?
CLOSE_WAIT的危害在于, 在一个端口上打开的文件描述符超过一定数量, (在linux上默认是1024, 可修改), 新来的socket连接就无法建立了.
答 : CLOSE_WAIT是被动方收到FIN包进行回复后还没向主动方发送FIN时的状态, 等待调用close(sockfd) / shutdown(sockfd, SHUT_WR). 服务器中出现大量的CLOSE_WAIT很大可能是对于大量的套接字连接断开之后, 没有调用clsoe()关闭, 释放资源, 是代码不够健壮造成的
6. 服务器出现大量的TIME_WAIT是什么原因?
答 : TIME_WAIT是主动关闭方, 在发送最后一个ACK包之后的状态, 意味着服务器上大量主动的关闭了连接, 通常出现在爬虫服务器上. 解决方法如下 :
1. 开启地址复用 : 如果服务器重启时需要对端口号以及socket地址进行复用,从而避免了TIME_WAIT状态(接口 : setsockopt() )
2. 设置MSL时间, 设置的更短一些.
7. SYN 泛洪(flood)攻击
攻击方的客户端只发送SYN包发送给服务器,然后对服务器发回来的SYN+ACK什么也不做, 直接忽略掉, 也就是不发送ACK包给服务器. 当有大量的SYN flood 攻击时, 半连接队列就会被占满, 会导致正常的客户端连接无法连接上服务器
SYN flood攻击的方式其实也分两种, 第一种, 攻击方的客户端一直向服务端发送SYN包, 对于服务器回应的SYN+ACK什么也不做. 也就是不给服务端回复ACK包. 第二种,攻击方的客户端发送SYN包时, 将源IP改为一个虚假的IP, 然后服务器将SYN+ACK发送到虚假的IP, 这样当然永远也得不到ACK的回应.
8. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
答 : TCP还设有一个保活机制, 其中有一个保活计时器. 显然, 客户端如果出现故障,服务器不能一直等下去,白白浪费资源. 服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为7200s, 即2小时, 若两小时还没有收到客户端的任何数据,服务器就会每隔75秒发送一个探测报文段, 若一连发送9个探测报文, 客户端仍然没反应,服务器就认为客户端出了故障, 断开连接,上层(应用层)体现, recv()返回0, send()触发异常
有连接上面已经写完, 接下来再看面向字节流和可靠传输 .
可靠的, 有序的, 双向的, 基于连接的字节流传输方式
字节流传输 : 发送的数据都会放到发送缓冲区中进行缓冲, 字节流传输不关心缓冲区有多少数据, 会根据自己实际情况选择一次发送的数据大小, 然后从缓冲区中取出合适大小的数据进行封装.
面向字节流传输 : 传输灵活, 并不限制上层发送的数据大小以及接收的数据大小. (字节流就像生活中的水流一样)
字节流的弊端 : 粘包
粘包就是多条数据粘连在一起被TCP作为一条数据进行发送或交付给上层(接收), 粘包可能发生在接收端或发送端.
本质原因 : TCP对上层数据边界并不敏感(因为其不关心上层是什么数据, 有多少数据, 只管从缓冲区中取出合适大小的数据进行发送(给网络层)或交付(给应用层)). 当发送端发送的数据较小, 需要等多个数据填满缓冲区才发送出去, 就会造成粘包, 或者接收方不及时接收缓冲区的包, 造成多个包接收在缓冲区中
不是所有的粘包现象都需要处理, 若传输的数据为不带结构的连续流数据(如文件传输), 则不必把粘连的包分开(分包). 但在实际工程应用中, 传输的数据一般为带结构的数据,这时就需要做分包处理.
解决方案: 既然TCP在传输层并不不对数据进行边界管理, 那么就需要在应用层我们程序员自己进行边界管理,
注意 : UDP不会产生粘包现象, 因为其传输方式是面向数据报的, 并且其头部中有数据长度
写在另一篇中: 戳链接( ̄︶ ̄)↗ : https://blog.csdn.net/qq_41071068/article/details/105474889