我这篇文章想讲的是编程时如何正确关闭 tcp 连接。首先给出一个网络上绝大部分的 java nio 代码示例:服务端:1 首先实例化一个多路 I / O 复用器 Selector2 然后实例化一个 ServerSocketChannel3ServerSocketChannel 注册为非阻塞(channel.configureBlocking(false);)4ServerSocketChannel 注册到 Selector,并监听连接事件(serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);)5Selector 开始轮询,如果监听到了 isAcceptable()事件,就建立一个连接,如果监听到了 isReadable()事件,就读数据。6 处理完或者在处理每个事件之前将 SelectionKey 移除出 Selector.selectedKeys()代码:
package qiuqi.main;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class NioServer {
public static void main(String[] args) throws IOException {
startServer();
}
static void startServer() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(999));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (sk.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
System.out.println(“ 读事件!!!”);
SocketChannel channel = (SocketChannel) sk.channel();
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(200);
// 这里只读数据,未作任何处理
channel.read(byteBuffer);
} catch (IOException e) {
// 手动关闭 channel
System.out.println(e.getMessage());
sk.cancel();
if (channel != null)
channel.close();
}
}
}
}
}
}
还有说明一下,为什么在 if (sk.isReadable()){}这个里面加上异常捕捉,因为可能读数据的时候客户端突然断掉,如果不捕捉这个异常,将会导致整个程序结束。而客户端如果使用 NIO 编程,那么和服务端很像,然鹅,我们并不需要使用 NIO 编程,因为这里我想讲的问题和 NIO 或是普通 IO 无关,在我想讲的问题上,他俩是一样的,那么我就用普通 socket 编程来讲解,因为这个好写:)。
直接给代码如下:
package qiuqi.main;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
public class TraditionalSocketClient {
public static void main(String[] args) throws IOException {
startClient();
}
static void startClient() throws IOException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(999));
socket.getOutputStream().write(new byte[100]);
// 要注意这个 close 方法,这是正常关闭 socket 的方法
// 也是导致这个错误的根源
socket.close();
}
}
我们运行客户端和服务端的代码,输出的结果是: 读事件!!! 读事件!!! 读事件!!! 读事件!!! 读事件!!! 读事件!!!…. 读事件!!! 读事件!!! 无限个读事件!!!why???客户端正常关闭,然后显然客户端不可能再给服务端发送任何数据了,服务端怎么可能还有读响应呢?我们现在把客户端代码的最后一行 socket.close(); 这个去掉,再运行一次!输出结果是:读事件!!! 读事件!!! 远程主机强迫关闭了一个现有的连接。
然后。。。就正常了 (当然代码里会有异常提示的),这里的正常指的是不会输出多余的读事件!!! 了。这又是怎么回事?我们知道如果去掉 socket.close(); 那么客户端是非正常关闭,服务端这边会引发 IOException。引发完 IOExpection 之后,我们的程序在 catch{} 语句块中手动关闭了 channel。
既然非正常关闭会引发异常,那么正常关闭呢?什么都不引发?但是这样服务端怎么知道客户端已经关闭了呢?显然服务端会收到客户端的关闭信号(可读数据),而网络上绝大多数代码并没有根据这个关闭信号来结束 channel。那么关闭信号是什么?
channel.read(byteBuffer);
这个语句是有返回值的,大多数情况是返回一个大于等于 0 的值,表示将多少数据读入 byteBuffer 缓冲区。然鹅,当客户端正常断开连接的时候,它就会返回 -1。虽然这个断开连接信号也是可读数据 (会使得 isReadable() 为 true),但是这个信号无法被读入 byteBuffer,也就是说一旦返回 -1,那么无论再继续读多少次都是 -1,并且会引发可读事件 isReadable()。因此,这样写问题就能得到解决,下面的代码在 try 语句块里。
SocketChannel channel = (SocketChannel) sk.channel();
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(200);
int num;
// 这里只读数据,未作任何处理
num = channel.read(byteBuffer);
if(num == -1)
throw new IOException(“ 读完成 ”);
} catch (IOException e) {
System.out.println(e.getMessage());
sk.cancel();
if (channel != null)
channel.close();
}
这里我根据返回值 - 1 来抛出异常,使得下面的 catch 语句块捕捉并关闭连接,也可以不抛出异常,直接在 try{}里处理。