很多同学知道在大学课程中,我们学习的《计算机网络》一书采用的是OSI七层网络模型(OSI Model),但是OSI 七层模型是一种抽象模型,在操作系统实际实现中,采用的是TCP/IP四层网络模型,四层模型将七层模型合并为了 应用层(Application Layer)、传输(Transport Layer)、网络层(Internet Layer)、链路层(Link Layer) ,使得网络系统在具体实现中更加简化,OSI七层网络模型与TCP/IP四层网络模型以及协议对应关系如下表所示。在计算机系统中,分层是一种很重要的编程思想,分层思想将系统的功能与责任进行了层次化划分,基本上所有系统的架构设计,都是按照层次架构作为基本架构来设计的。在计算机网络中,相同层次具有相同的协议处理方式, 下层协议上层提供服务,上层协议的行为控制着下层的工作状态 ,层层之间责任单一,目的明确。
二 、 Java对于TCP/IP协议的实现
在网络程序开发中,操作系统都为我们提供了全面 方便的应用层网络操作类与接口,使得程序员在使用过程中无需考虑协议栈的细节,而专心于数据的传输处理过程中。当然,操作系统也为程序员提供了可以掌控协议细节的机会,例如使用原始套接字 (Raw Socket) 可以控制TCP的三次握手的细节实现TCP SYN扫描(注意部分 Windows 7系统不支持原始套接字的半开扫描)。但是在大多常规网络应用开发中,我们都直接使用系统提供的应用层接口来实现网络程序。这里我们罗列出在Java中常见的 TCP/IP协议 的实现类或方法,如下表所示:
三 、 TCP协议为什么需要三次握手
首先我们来看一下TCP协议三次握手的具体过程(本图选自网络):
第一次握手: 建立连接。客户端发送连接请求报文段,将 SYN
位置为1, Sequence Number
为x;然后,客户端进入 SYN_SEND
状态,等待服务器的确认;
第二次握手: 服务器收到 SYN
报文段。服务器收到客户端的 SYN
报文段,需要对这个SYN报文段进行确认,设置 Acknowledgment Number
为x+1( Sequence Number
+1) ;同时,自己还要发送 SYN
请求信息,将SYN位置为1, Sequence Number
为y;服务器端将上述所有信息放到一个报文段(即 SYN+ACK
报文段)中,一并发送给客户端,此时服务器进入 SYN_RECV
状态;
第三次握手: 客户端收到服务器的 SYN+ACK
报文段。然后将 Acknowledgment Number
设置为y+1 ,向服务器发送 ACK
报文段,这个报文段发送完毕以后,客户端和服务器端都进入 ESTABLISHED
状态,完成TCP三次握手。
那么,为什么TCP协议需要 三次握手 ?在谢希仁的《计算机网络》中是这样说的:
已失效的连接请求报文段会产生在这样一种情况下:client发出的第一个连接请求报文段并 没有丢失 ,而是在某个网络结点长时间的 滞留 了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就 误认为是client再次发出的一个新的连接请求 。于是就 向client发出确认报文段,同意建立连接 。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用 “三次握手”的办法可以防止上述现象 发生。例如刚才那种情况, client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接 。
换句话说,TCP之所以采用三次握手建立连接的机制,是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 在网络中不能确保数据包的一定可以发送成功,也不能确保数据包的发送顺序和到达顺序一致,三次握手机制避免了客户端与服务器之间在建立连接时可能因丢包而造成的一端 无法感知另一端状态的现象。 关于TCP协议的四次挥手过程与原因与三次握手完全类同,这里就不说明了,关于TCP连接的关闭,后文将分析一个相关问题:TCP半关闭现象(Half-Close),这里我们先来看一下Java中如何使用Socket类建立一个TCP连接的过程。
四、 Java中TCP通信的相关实现
在上节我们分析了 TCP协议建立连接 ,数据传输以及关闭连接的具体实现方式,而在实际的开发中,程序员只需了解TCP协议是一种可靠的传输协议即可实现数据在客户端与服务器之间稳定的传输。在Java中, 提供了Socket与SocketServer类来实现TCP服务器与客户端的相关功能 ,一次正常的TCP通信其大致流程可以分为四步(BIO模式):
服务器端(ServerSocket)绑定监听端口,等待客户端的TCP的连接(ServerSocket.accept())
客户端(Socket)通过IP地址与端口连接服务器监听端口(Socket.Connect()),连接成功后服务器端返回表示该TCP连接的Socket对象。
客户端与服务器通过打开Socket对象的InputStream和OutputStream数据流,实现数据的传输工作(Java将I/O相关的操作都提供了流操作接口,网络接口操作方式也一样)。
客户端与服务器完成数据传输,关闭数据流,关闭TCP连接(Socket.close())。
下图是Socket的通行模型图:
这里有个问题,TCP的三次握手是在Socket的哪一步中实现?
在ServerSocket.accept()与Socket.Connect()的过程中实现的,在客户端通过 Connect()接口 连接服务器时,操作系统底层的TCP/IP协议栈便开始了发送SYN包,回复SYN+1等TCP的三次握手过程,只有三次握手成功,ServerSocket.accept()才会返回一个合法的Scoket对象,客户端Socket.Connect()函数会正常返回,如果三次握手失败,客户端Socket.Connect()会抛出IOException异常。
这里需要注意,具体的三次握手协议细节的实现,也 不是在Java中实现 的, Java只是运行在JVM虚拟机上的语言 ,具体的实现是由 宿主主机的TCP/IP协议栈实现 的,Java只是通过虚拟机调用了这些宿主主机提供的方法而已。
五、 TCP半关闭现象( Half-Close
)
有TCP服务器与客户端应用程序开发经验的同学应该遇到过一个问题,那就是服务器端突然崩溃(kill 掉服务器进程),查看系统中的网络连接时,发现TCP客户端的状态还是处于连接状态( ESTABLISHED
),而该TCP连接实际已经失效了,这就是TCP的Half-Close 现象。 如果应用程序判断与服务器连接状态的方法依赖于TCP的连接状态,客户端将会一直认为与服务器的TCP连接是正常的,只有在客户端向服务器发送数据时,才能发现TCP连接对应的套接字失效了。之所以产生Half-Close现象就是由于客户端与服务器之间没有通过四次挥手的方式关闭TCP连接,在服务器的突然下线会造成客户端无法立即感知的问题。有同学会问,不是有超时时间吗,一旦超时,客户端不就可以感知到服务器已经下线了吗?不错,系统的协议栈实现具有超时时间的机制,但是 在windows系统下,这个超时时间默认是2小时 (作者未考证linux下的keepAlive时间)。
那么如何避免Half-Close现象?
1.首先进行再使用完tcp连接后,一定要将套接字close掉。
2.添加心跳包机制,服务器与客户端之间的连接保持机制不应该依赖套接字的状态,而应该在TCP协议之上设计心跳包机制,例如每5分钟,客户端与服务器之间通过发送心跳包来感知对方的存在。
3.TCP Server应该实现JVM的关闭钩子(Runtime.addShutdownHook()),主动关闭所有TCP连接,清理占用资源,JVM关闭钩子的使用方式如下所: