Java Socket(2): 异常处理
1 超时
套接字底层是基于TCP的,所以socket的超时和TCP超时是相同的。下面先讨论套接字读写缓冲区,接着讨论连接建立超时、读写超时以及JAVA套接字编程的嵌套异常捕获和一个超时例子程序的抓包示例。
一旦创建了一个套接字实例,操作系统就会为其分配缓冲区以存放接收和要发送的数据。
(1)socket 读写缓冲区
JAVA可以设置读写缓冲区的大小-setReceiveBufferSize(int size), setSendBufferSize(int size)。向输出流写数据并不意味着数据实际上已经被发送,它们只是被复制到了发送缓冲区队列SendQ,就是在Socket的OutputStream上调用flush()方法,也不能保证数据能够立即发送到网络。真正的数据发送是由操作系统的TCP协议栈模块从缓冲区中取数据发送到网络来完成的。当有数据从网络来到时,TCP协议栈模块接收数据并放入接收缓冲区队列RecvQ,输入流InputStream通过read方法从RecvQ中取出数据。
(2)socket连接建立超时
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内,连接没有建立成功,在TimeoutException异常被抛出。如果timeout的值小于三次握手的时间,那么Socket连接永远也不会建立。
import java.net.*; import java.io.*; public class SocketClientTest { public static final int PORT = 8088; public static void main( String[] args ) throws Exception { InetAddress addr = InetAddress.getByName( "127.0.0.1" ); Socket socket = new Socket(); try { socket.connect( new InetSocketAddress( addr, PORT ), 30000 ); socket.setSendBufferSize(100); BufferedWriter out = new BufferedWriter( new OutputStreamWriter( socket.getOutputStream() ) ); int i = 0; while( true ) { System.out.println( "client sent --- hello *** " + i++ ); out.write( "client sent --- hello *** " + i ); out.flush(); Thread.sleep( 1000 ); } } finally { socket.close(); } } }
(3)socket 读超时
如果输入缓冲队列RecvQ中没有数据,read操作会一直阻塞而挂起线程,直到有新的数据到来或者有异常产生。调用setSoTimeout(int timeout)可以设置超时时间,如果到了超时时间仍没有数据,read会抛出一个SocketTimeoutException,程序需要捕获这个异常,但是当前的socket连接仍然是有效的。如果对方进程崩溃、对方机器突然重启、网络断开,本端的read会一直阻塞下去,这时设置超时时间是非常重要的,否则调用read的线程会一直挂起。TCP模块把接收到的数据放入RecvQ中,直到应用层调用输入流的read方法来读取。如果RecvQ队列被填满了,这时TCP会根据滑动窗口机制通知对方不要继续发送数据,本端停止接收从对端发送来的数据,直到接收者应用程序调用输入流的read方法后腾出了空间。
假设我们需要控制我们的客户端在开始读取数据10秒后还没有读到数据就中断阻塞的话我们可以这样做:
public class Client { public static void main(String args[]) throws Exception { //为了简单起见,所有的异常都直接往外抛 String host = "127.0.0.1"; //要连接的服务端IP地址 int port = 8899; //要连接的服务端对应的监听端口 //与服务端建立连接 Socket client = new Socket(host, port); //建立连接后就可以往服务端写数据了 Writer writer = new OutputStreamWriter(client.getOutputStream()); writer.write("Hello Server."); writer.write("eof\n"); writer.flush(); //写完以后进行读操作 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); //设置超时间为10秒 client.setSoTimeout(10*1000); StringBuffer sb = new StringBuffer(); String temp; int index; try { while ((temp=br.readLine()) != null) { if ((index = temp.indexOf("eof")) != -1) { sb.append(temp.substring(0, index)); break; } sb.append(temp); } } catch (SocketTimeoutException e) { System.out.println("数据读取超时。"); } System.out.println("from server: " + sb); writer.close(); br.close(); client.close(); } }
2 断开连接
TCP Socket连接是双向的,通过四次挥手的方式断开,双方分别调用Socket.close()
方法断开连接。连接断开的过程中,一般一方A先断开连接,另一方B发现A断开连接后,也断开连接。为方便表述,将先断开连接的一方A称为“主动断开连接”;后断开的一方B,则为“被动断开连接”。
在一方B阻塞执行in.readUTF()
方法时,如果对方A主动断开Socket连接,这个方法会抛出异常。从而在B处理异常时,可以被动的断开这边的连接。
为保证主动断开连接的一方不会阻塞在in.readUTF()
方法中,需要先执行socket.shutdownInput()
。所以主动断开连接的代码如下。
socket.shutdownInput();
in.close();
socket.close();
被动断开连接的一方,在捕获到in.readUTF()
的异常后,断开Socket连接。
try { String s = in.readUTF(); } catch (IOException e) { // 连接被断开(被动) try { in.close(); socket.close(); in = null; socket = null; } catch (IOException e) { e.printStackTrace(); } }