大家好,又见面了,我是你们的朋友全栈君。
TCP / IP模型中有5层结构:应用层、传输层、网络层、数据链路层以及物理层。其中IP协议是位于网络层的,TCP协议是位于传输层的,通过IP协议可以使两台计算机使用同一种语言,从而允许Internet上连接不同类型的计算机和不同的操作系统的网络,IP协议只能保证计算机能够接收和发送分组数据,当计算机要和远程的计算机建立连接时,TCP协议会让它们建立连接:用于发送和接收数据的虚拟电路。
在Java中,我们使用Socket、ServerSocket类创建一个套接字连接,从套接字得到的结果就是一个InputStream以及OutputStream对象,以便将连接作为一个IO流对象对待。通过IO流可以从流中读取数据或者写数据到流中。
套接字是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。Java有两个基于数据流的套接字类:ServerSocket,服务器用它监听进入的连接,Socket,客户端用它初始一次连接。监听套接字只能接收新的连接请求,不能接收实际的数据包,即ServerSocket不能接收实际的数据包。
套接字是基于TCP / IP实现的,它是用来提供一个访问TCP的服务接口,即Socket是TCP的应用编程接口API,通过它应用层就可以访问TCP提供的服务。
Socket简介
Socket字面意思就是套接字,包含了源IP地址、源端口、目的IP地址和目的端口。
Socket是应用层与TCP / IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP / IP协议族隐藏在Socket接口后面。
Socket底层数据结构
对于套接字结构,是指底层实现(包括JVM和TCP / IP,但通常是后者)的数据结构集合,包含了特定Socket所关联的信息。套接字结构包含:
该套接字所关联的本地和远程互联网地址和端口
一个FIFO队列用于存放接收到的等待分配的数据(RecvQ),以及一个用于存放等待传输数据的队列(SendQ)
打开和关闭TCP握手相关的额外协议状态信息
image.png
由于TCP提供了一种可信赖的字节流服务,任何写入Socket和OutputStream的数据副本都没有保留,直到连接的另一端将这些数据成功接收,向输出流写数据并不意味着数据实际上已经被发送—,它们只是被复制到本地缓冲区,就算在Socket的OutputStream上进行flush操作,也不能保证数据能够立即发送到信道。此外,字节流服务的自身属性决定了其无法保留输入流中消息的边界消息
Socket数据传输的底层实现
一旦创建了一个套接字实例,操作系统就会为其分配缓冲区以存放接收和要发送的数据。
Java可以设置读写缓冲区的大小:
setReceiveBufferSize(int size)
setSendBufferSize(int size
向输出流写数据并不意味着数据实际上已经被发送,它们只是被复制到了发送缓冲队列SendQ,就是在Socket的OutputStream上调用flush方法,也不能保证数据能够立即发送到网络。真正的数据发送是由操作系统的TCP协议栈模块从缓冲区中取数据发送到网络来完成的。当有数据从网络来到时,TCP协议栈模块接收数据并放入缓冲区队列RecvQ,输入流InputStream通过read方法从RecvQ中取出数据。
Socket超时
Scoke连接建立超时
Socket连接建立是基于TCP的连接建立过程。TCP的连接需要通过3次握手报文来完成,开始建立TCP连接时需要发送同步SYN报文,然后等待确认报文SYN + ACK,最后再发送确认报文ACK。TCP连接的关闭通过4次挥手来完成,主动关闭TCP连接的一方发送FIN报文,等待对方的确认报文,被动关闭的一方也发送FIN报文,然后等待确认报文。
正在等待TCP连接请求的一端有一个固定长度的连接队列,该队列中的连接已经被TCP接受(即三次握手已经完成),但还没有被应用层所接受。TCP接受一个连接是将其放入这个连接队列,而应用层接受连接是将其从该队列中移出。应用层可以通过设置backlog变量来指明该连接队列的最大长度,即已被TCP接受而等待应用层接受的最大连接数。
当一个连接请求SYN到达时,TCP确定是否接受这个连接。如果队列中还有空间,TCP模块将对SYN进行确认并完成连接的建立,但应用层只有在三次握手中的第三个报文收到后才会知道这个新连接,如果队列没有空间,TCP将不理会收到的SYN。如果应用层不能及时接受已被TCP接受的连接,这些连接可能会占满整个连接队列,新的连接请求可能不会响应而会超时。如果一个连接请求SYN发送后,一段时间后没有收到确认SYN + ACK,TCP会重传这个连接请求SYN两次,每次重传的时间间隔加倍,在规定的时间内仍然没有收到SYN + ACK,TCP将放弃这个连接请求,连接请求就超时了。
Java Socket连接建立超时和TCP是相同的,如果TCP建立连接时三次握手超时,那么导致Socket连接建立也就超时了,可以设置Socket连接建立的超时时间:
connect(SocketAddress endpoint,int timeout)
如果在timeout内,连接没有建立成功,TimeoutExpection异常被抛出,如果timeout的值小于三次握手的时间,那么Socket连接永远不会建立。
不同应用层有不同的连接建立过程,Socket的连接建立和TCP一样,仅仅需要三次握手就完成连接,但有些应用程序需要交互很多信息后才能成功建立连接,比如Telnet协议,在TCP三次握手完成后,需要进行选项协商之后,Telnet连接才建立完成。
2、Socket读超时
如果输入缓冲队列RecvQ中没有数据,read操作会一直阻塞而挂起线程,直到有新的数据到来或者有异常产生,调用setSoTimeout(int timeout)可以设置超时时间,如果到了超时时间仍没有数据,read会抛出一个SocketTimeoutExpection,程序需要捕获这个异常,但是当前的Socket连接仍然是有效的。
如果对方进程奔溃、对方机器突然重启、网络断开,本端的read会一直阻塞下去,这是设置超时时间是非常重要的,否则调用read的线程会一直挂起。
TCP模块把接受到的数据放入RecvQ中,直到应用层调用输入流的read方法来读取。如果RecvQ队列被填满了,这时TCP会根据滑动窗口机制通知对方不要继续发送数据,本端停止接收从对端发送来的数据,直到接受者应用程序调用输入流的read方法后腾出空间。
3、Socket写超时
Socket的写超时是基于TCP的超时重传。超时重传是TCP保证数据可靠性传输的一个重要机制,其原理是在发送一个数据报文后就开启一个计时器,在一定时间内如果没有得到发送报文的确认ACK,那么就重新发送报文。如果重新发送多次之后,仍没有确认报文,就发送一个复位报文RST,然后关闭TCP连接。首次数据报文发送与复位报文传输之间的时间差大约为9分钟,也就是说如果9分钟内没有得到确认报文,就关闭连接,但是这个值是根据不同的TCP协议栈实现而不同。
如果发送端调用write持续地写出数据,直到SendQ队列被填满。如果在SendQ队列已满时调用write方法,则write将被阻塞,直到SendQ有新的空闲空间为止,也就是说直到一些字节传输到了接受者套接字的RecvQ中,如果此时RecvQ队列也已经被填满,所有操作都将被停止,直到接收端调用read方法将一些字节传输到应用程序。
Socket写超时是基于TCP协议栈的超时重传机制,一般不需要设置write的超时时间,也没有提供这种方法。
关闭服务端连接
在客户端和服务端的数据交互完成后,一般需要关闭网络连接。对于服务端来说,需要关闭Socket和ServerSocket。
在关闭Socket后,客户端并不会马上感知自已的Socket已经关闭,也就是说,在服务端的Socket关闭后,客户端的Socket的isClosed和isConnected方法仍然会分别得到false和true。但对已关闭的Socket的输入输出流进行操作会抛出一个SocketException异常。下面的代码演示了在服务端关闭Socket后,客户端是所何反应的。
package server;
import java.net.*;
class Client
{
public static void main(String[] args) throws Exception
{
Socket socket = new Socket(“127.0.0.1”, 1234);
Thread.sleep(1000);
// socket.getOutputStream().write(1);
System.out.println(“read() = ” + socket.getInputStream().read());
System.out.println(“isConnected() = ” + socket.isConnected());
System.out.println(“isClosed() = ” + socket.isClosed());
}
}
public class CloseSocket
{
public static void main(String[] args) throws Exception
{
ServerSocket serverSocket = new ServerSocket(1234);
while (true)
{
Socket socket = serverSocket.accept();
socket.close();
}
}
}
read() = -1
isConnected() = true
isClosed() = false
从上面的运行结果可以看出例程Client并未抛出SocketException异常。而在012行的read方法返回了-1。如果将socket.close去掉,客户端的read方法将处于阻塞状态。这是因为Java在发现无法从服务端的Socket得到数据后,就通过read方法返回了-1。如果将011行的注释去掉,Client就会抛出一个SocketException异常。在write时候并不会抛异常, write再去read就会抛异常,因为服务端进程不会向客户端发送ACK 报文,而是发送了一个RST 报文请求将处于异常状态的连接复位。客户端接收到了这个抛异常,
RST (Reset)
TCP连接的断开有两种方式:
连接正常关闭时双方会发送FIN,经历4次挥手过程;
通过RST包异常退出,此时会丢弃缓冲区内的数据,也不会对RST响应ACK。
java中,调用Socket#close()可以关闭Socket,该方法类似Unix网络编程中的close方法,将Socket的 读写 都关闭,已经排队等待发送的数据会被尝试发送,最后(默认)发送FIN。考虑一个典型的网络事务,A向B发送数据,A发送完毕后close(),FIN发送出去;B一直read直到返回了-1,也通过close()发送FIN,4次挥手,连接关闭,一切都很和谐。
那什么时候会用RST而非FIN关闭连接呢?
Socket#setSoLinger(true,0),则close时会发送RST
如果主动关闭方缓冲区还有数据没有被应用层消费掉,close会发送RST并忽略这些数据
A向B发送数据,B已经通过close()方法关闭了Socket,虽然TCP规定半关闭状态下B仍然可以接收数据,但close动作关闭了该socket上的任何数据操作,如果此时A继续write,B将返回RST,A的该次write无法立即通知应用层(因为write仅把数据写入发送缓冲区),只会把状态保存在tcp协议栈内,下次read时才会抛出SocketException。
2. 对已关闭socket读写会产生的异常
2.1 主动关闭方
close()后,无论是发送FIN/RST关闭的,之后再读写均会抛java.net.SocketException:socket is closed.
2.2 被动关闭方
被FIN关闭
写(即向”已被对方关闭的Socket”写)
如上所说,第一次write得到RST响应但不抛异常,第二次write抛异常,ubuntu下是broken pipe (断开的管道),win7下是Software caused connection abort: socket write error
读 – 始终返回 -1
被RST关闭
读写都会抛出异常:connection reset (by peer)
重点在于:
connection reset:另一端用RST主动关闭连接
broken pipe / Software caused connection abort: socket write error : 对方已调用Socket#close()关闭连接,己方还在写数据
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/156542.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...