Java I/O之NIO Socket

时间:2022-09-28 22:57:19

PS:本文简单介绍下旧I/O和NIO下的Socket通讯,仅以UDP来示例。

TCP/IP协议

首先简单回顾下TCP/IP协议
Java I/O之NIO Socket

Application:应用程序;Socket:套接字;Host:主机;Channel:通信信道;Ethernet:以太网;Router:路由器;Network Layer:网络层;Transport Layer:传输层。

在TCP/IP协议族中,底层由基础的通信信道构成,如以太网或调制解调器拨号连接。这些信道由网络层(network layer)使用,而网络层则完成将分组报文传输到它们的目的地址的工作(也就是路由器的功能)。TCP/IP协议族中属于网络层的唯一协议是IP协议,它使两个主机间的一系列通信信道和路由器看起来像是一条单一的主机到主机的信道。

IP协议提供了一种数据报服务:每组分组报文都由网络独立处理和分发,就像信件或包裹通过邮政系统发送一样。为了实现这个功能,每个IP报文必须包含一个保存其目的地址(address)的字段,就像你所投递的每份包裹都写明了收件人地址。(我们随即会对地址进行更详细的说明。)尽管绝大部分递送公司会保证将包裹送达,但IP协议只是一个"尽力而为"(best-effort)的协议:它试图分发每一个分组报文,但在网络传输过程中,偶尔也会发生丢失报文,使报文顺序被打乱,或重复发送报文的情况。

IP协议层之上称为传输层(transport layer)。它提供了两种可选择的协议:TCP协议和UDP协议。这两种协议都建立在IP层所提供的服务基础上,但根据应用程序协议(application protocols)的不同需求,它们使用了不同的方法来实现不同方式的传输。TCP协议和UDP协议有一个共同的功能,即寻址。回顾一下,IP协议只是将分组报文分发到了不同的主机,很明显,还需要更细粒度的寻址将报文发送到主机中指定的应用程序,因为同一主机上可能有多个应用程序在使用网络。TCP协议和UDP协议使用的地址叫做端口号(port numbers),都是用来区分同一主机中的不同应用程序。TCP协议和UDP协议也称为端到端传输协议(end-to-end transport protocols),因为它们将数据从一个应用程序传输到另一个应用程序,而IP协议只是将数据从一个主机传输到另一主机。

TCP协议能够检测和恢复IP层提供的主机到主机的信道中可能发生的报文丢失、重复及其他错误。TCP协议提供了一个可信赖的字节流(reliable byte-stream)信道,这样应用程序就不需要再处理上述的问题。TCP协议是一种面向连接(connection-oriented)的协议:在使用它进行通信之前,两个应用程序之间首先要建立一个TCP连接,这涉及到相互通信的两台电脑的TCP部件间完成的握手消息(handshake messages)的交换。使用TCP协议在很多方面都与文件的输入输出(I/O, Input/Output)相似。实际上,由一个程序写入的文件再由另一个程序读取就是一个TCP连接的适当模型。另一方面,UDP协议并不尝试对IP层产生的错误进行修复,它仅仅简单地扩展了IP协议"尽力而为"的数据报服务,使它能够在应用程序之间工作,而不是在主机之间工作。因此,使用了UDP协议的应用程序必须为处理报文丢失、顺序混乱等问题做好准备。

旧I/O的Socket示例

//服务器端
DatagramSocket servSocket = null;
byte[] buf = new byte[1024];
DatagramPacket datapkg = new DatagramPacket(buf, buf.length);
try {
servSocket = new DatagramSocket(5008);
while (true) {
servSocket.receive(datapkg);
//一般使用多线程来处理....
System.out.println("服务器接收了客户端:" + datapkg.getAddress()
+ " 的数据:" + new String(buf, 0, datapkg.getLength()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
servSocket.close();
}
//客户端
DatagramSocket clientSocket=new DatagramSocket();
byte[] msg="this is a test message".getBytes();
DatagramPacket datapkg=new DatagramPacket(msg, msg.length, new InetSocketAddress("127.0.0.1", 5008));
clientSocket.send(datapkg);
clientSocket.close();

上面只是简单地示例,实际用应用中需要多线程,特别是服务器端,接收到数据报文后一般需要使用多线程来处理的。

NIO的Socket

旧I/O Socket是阻塞式的,我们通过上例可以看到使用了while(true)在那忙等(很傻吧),当然你可以使用多线程来避开阻塞,但这个解决办法会产生它自己的问题 ― 即线程开销,线程开销同时影响性能和可伸缩性。
新NIO Socket可以使用非阻塞式的(如果你一定要把NIO Socket用作阻塞式的,也不会有人拦你的^_^),实际上是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,就会帮我们记录下来,当我们需要的时候再取出来,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。

而这个非阻塞模式就是由NIO中的类Selector来完成的,它类似一个观察者:只要我们把需要探知的channel事件注册到Selector上,我们接着做别的事情,当channel有事件发生时就会主动通知selector,我们可以轮询selector就知道那些channel可操作,然后我们再从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。

NIO中的通道和缓冲器在前面一篇博文中有介绍,我们来看下非阻塞式的代码示例

// 服务器端
DatagramChannel servChannel = DatagramChannel.open();
servChannel.socket().bind(new InetSocketAddress("127.0.0.1", 5008));
// 如果使用selector,此处必须设置为非阻塞式的
servChannel.configureBlocking(false); ByteBuffer buf = ByteBuffer.allocate(1024); Selector selector = Selector.open();
// 注册事件OP_ACCEPT和OP_CONNECT用于TCP,OP_READ和OP_WRITE可用于UDP和TCP
servChannel.register(selector, SelectionKey.OP_READ); // 轮询selector
while (true) {
if (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isReadable()) {
buf.clear();
SocketAddress clientAdrress = ((DatagramChannel) key
.channel()).receive(buf);
buf.flip();
System.out.println("服务器接收了客户端:" + clientAdrress
+ " 的数据:" + buf.asCharBuffer());
}
it.remove();
}
}
}

虽然说得天花乱坠,但到目前为止我还是没太明白使用这种观察者模式的好处,最后还是避不开忙等。(PS:关于I/O Socket和NIO Socket的对比优势可参考这里。)

推荐一本书:Java Tcp/Ip Socket编程
Java I/O之NIO Socket