网络编程中Socket和TCP的断开过程—学习笔记

目录

  • 一小段引言
    • 什么是socket网络编程
    • TCP:传输控制协议
    • TCP连接的建立
    • 为什么要三次握手
    • 利用tcpdump命令和nc命令观察tcp连接建立过程
    • TCP断开连接的过程
    • 关于 TIME_WAIT 状态的说明
    • close()函数和优雅关闭连接shutdown()函数
    • close()函数
    • shutdown()函数
    • 总结

一小段引言

学习socket网络编程之前,对于所学的计算机网络知识就是一知半解。甚至一度怀疑学计算机网络有什么用?自今年3月起,我面临找实习的阶段。开始时我还迟迟未定,我到底要从事什么岗位?前端?还是后台?算法肯定不会…,后来发现对于计算机图形学、HTML、Js都不熟悉。算了,只能后台了。于是开始了socket网络编程的学习之路。

此篇博文是学习微信公众号—编程珠玑的文章总结所得,以下内容均为作者见解。

什么是socket网络编程

通俗来讲,可以认为网络编程是两台或者多台主机(应用)之间进行数据交换或传输的技术手段。socket套接字(文件描述符),是不同主机的进程间通信的机制,它工作在计算机网络的协议栈中,但却不是TCP/IP模型的某一层。socket位于传输层和应用层之间,我理解为是用户进程和操作系统内核交流的通道。
网络编程中Socket和TCP的断开过程—学习笔记_第1张图片
socket实现两个功能:将应用程序的数据从用户缓冲区复制到TCP/UDP内核的发送缓冲区,或者从内核的接受缓冲区将数据复制到用户缓冲区以读取数据。可以通过它修改内核当中各层协议的某些头部信息或者其他的数据结构,精细控制底层通信行为。
网络编程几乎是每一门编程语言都会涉及的内容,虽然各种语言调用的方式可能不一样,但它们背后的原理支持都是一样的。

TCP:传输控制协议

而数据交换需要按照一定的规则,而这种规则就是协议。只有按照约定的规则,双方之间才能正确地进行数据交换。而TCP就是这些协议的一种,它提供一种面向连接的,可靠的字节流服务。

面向连接:两个使用TCP的应用在交换数据之前必须先建立一个TCP连接

可靠的:TCP有很多机制来尽可能的保证数据不丢失。例如:滑动窗口协议、拥塞控制算法等

字节流:不区分是ASCII字符还是二进制数据,数据解释交给应用层

TCP连接的建立

关于TCP的三次握手建立连接的过程,和socket网络编程中对于TCP三次握手的状态处理,可以参考我的另一篇博客:https://blog.csdn.net/La745739773/article/details/91385275

在这里补充一些计算机网络的基础知识:

为什么要三次握手

这几乎是面试中必问的一个问题。一个TCP连接是全双工的,即数据在两个方向上能同时传输。因此,建立连接的过程也就必须确认双方的收发能力都是正常的。

四次握手是否可以呢?完全可以!但是没有必要!在服务端收到SYN之后,它可以先回ACK,再发送SYN,但是这两个信息可以一起发送出去,因此没有必要。

两次握手是否可以呢?想象这样一种情况,客户端发起了一个连接请求在网络中滞留了很长时间,以至于在连接建立好且断开连接后,它才到达服务端,此时如果采用两次握手,那么服务端就会认为这个报文是新的连接请求,于是建立连接,等待客户端发送数据,但是实际上客户端根本没有发出建立请求,也不会理睬服务端,因此导致服务端空等而浪费资源。

为什么服务器会认为这个迟到的报文是新的连接请求?因为如果采用两次握手机制,那么服务端无法通过SYN来判断这是一个迟到或者重复的报文,还是正常到达的报文,但是对于三次握手,即便出现这样的情况,也不会在服务端建立起真正的连接。

利用tcpdump命令和nc命令观察tcp连接建立过程

待补充…

TCP断开连接的过程

TCP3次握手建立连接后的双方处于ESTABLISED状态,客户端调用close后,进行如下过程:
网络编程中Socket和TCP的断开过程—学习笔记_第2张图片

  1. 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

  2. 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
    注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要做完未完成的事情才可以断开。

  3. 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。

  4. 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。

  5. 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。

  6. 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

close()函数和优雅关闭连接shutdown()函数

TCP是全双工的通信机制已经概述过了,全双工连接分为读通道和写通道,如果这两个通道都关闭了,则这个连接不能继续通信。

(1)假设server和client 已经建立了连接,server调用了close,关闭了自身的socket, 发送FIN 段给client,此时server不能再通过socket发送和接收数据。
(2)client接收到FIN后,根据4次挥手的过程,client还可以向server发送数据write 。write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错。
(3)而server收到数据后应答一个RST段,表示服务器已经不能接收数据,连接重置,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。
(4)如果client再次调用write发数据给server,由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序。

通过上述场景,可以体会close关闭连接和优雅关闭连接的区别。

close()函数

#include 
 int close(int fd);

关闭本进程(自身)的socket id,但连接还是未断开的,用这个socket id的其它进程还能用这个连接,能读或写这个socket id。使用close中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。

在多进程的并发场景,假设客户端有两个进程(注意是客户端),父进程和子进程,子进程是在父进程和服务器建立连接之后fork出来的,因此客户端的socket id的引用次数+1。
我们期望实现这样的功能:
(1)子进程将数据写入套接字后close,并退出。
(2)服务端接收完数据,直到检测到EOF(接收到FIN标志),关闭连接并退出。
(3)父进程读取完服务端响应的数据,也退出。
如果子进程使用close的话,并不会发生4次挥手的过程,只是引用计数减1,服务端是接收不到EOF的(客户端是不会发送FIN的),这时就需要使用优雅关闭了。

shutdown()函数

 #include 
 int shutdown(int sockfd, int how);

shutdown 可以选择关闭某个方向或者同时关闭两个方向,shutdown how = 0 or how = 1 or how = 2 (SHUT_RD or SHUT_WR or SHUT_RDWR),后两者可以保证对等方接收到一个EOF字符(即发送了一个FIN段),而不管其他进程是否已经打开了这个套接字。而close不能保证,只有当某个sockfd的引用计数为0,close 才会发送FIN段,否则只是将引用计数减1而已。也就是说只有当所有进程(可能fork多个子进程都打开了这个套接字)都关闭了这个套接字,close 才会发送FIN 段。
---------------------

所以说,如果是调用shutdown how = 1 ,则意味着往一个已经发送出FIN的套接字中写是允许的,接收到FIN段仅代表对方不再发送数据,但对方还是可以读取数据的,可以让对方可以继续读取缓冲区剩余的数据。

总结

现在总结一下shutdown()和close()的主要区别:
1)对应的系统调用不同
2)shutdown()只能用于套接字文件,close()可以用于所有文件类型
3)shutdown()只是关闭连接,并没有释放文件描述符,close()可以
4)shutdown()不能用于TCP_CLOSE状态的套接字,否则会返回ENOTCONN错误
5)shutdown()可以选择关闭读通道或写通道,close()不能。

你可能感兴趣的:(Socket网络编程)