TCP/IP 阻塞模式与非阻塞模式
UDP/IP 阻塞模式与非阻塞模式
1. 服务端
import Java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.NET.DatagramSocket;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Set;
public class NioUdpServer {
public static void main(String[] args) {
try {
Selector selector = Selector.open();
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
DatagramSocket socket = channel.socket();
socket.bind(new InetSocketAddress(1000));
channel.register(selector, SelectionKey.OP_READ);
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
final int PACKAGE_SIZE = 10;
while(true){
int n = selector.select();
if(n == 0){
continue;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
for(SelectionKey key : readyKeys){
readyKeys.remove(key);
if(key.isReadable()){
DatagramChannel dc = (DatagramChannel)key.channel();
InetSocketAddress client = (InetSocketAddress)dc.receive(receiveBuffer); //接收来自任意一个Client的数据报
key.interestOps(SelectionKey.OP_READ);
System.out.println("client ----> IP: " + client.getAddress().getHostAddress() + ", port: " + client.getPort());
System.out.println("receiveBuffer.position() = " + receiveBuffer.position());
if(receiveBuffer.position() >= PACKAGE_SIZE){
receiveBuffer.flip();
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(receiveBuffer.array()));
System.out.println(dis.readInt());
BufferedReader d = new BufferedReader(new InputStreamReader(dis));
System.out.println(d.readLine());
receiveBuffer.clear();
}else{
dc.register(selector, SelectionKey.OP_READ);
}
}//if
}
}//while
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java NIO学习-UDP的例子
这几天需要实现一个底层基于UDP的协议,该协议底层使用UDP传输但是具有拥塞控制、超时重发、数据确认等功能又比TCP简单 (RUDP,Reliable UDP)。在实现协议底层的UDP服务时准备使用Java的NIO,在网上查资料都是以TCP为例讲的,于是自己研究了一下基于UDP的NIO。
NIO的思路是基于多路选择的,即由原来的每个连接都由一个线程来等待消息,改为每个连接都在选择器上注册,由选择器来等待。当然NIO引入了很多新的概念,如Channel,Buffer、Charset、Selector等,使得编程更简洁、更面向对象化。
下面贴出用NIO API改造成UDP示例代码,注意其中使用Charset来编码解码的过程(当然Charset还支持很多其他编码不仅局限于默认编码)以及Buffer的使用。
Client:
Java异步socket
用异步输入输出流编写Socket进程通信程序在Merlin中加入了用于实现异步输入输出机制的应用程序接口 包:java.nio(新的输入输出包,定义了很多基本类型缓冲(Buffer)),java.nio.channels(通道及选择器等,用于异步输入 输出),java.nio.charset(字符的编码解码)。通道(Channel)首先在选择器(Selector)中注册自己感兴趣的事件,当相应 的事件发生时,选择器便通过选择键(SelectionKey)通知已注册的通道。然后通道将需要处理的信息,通过缓冲(Buffer)打包,编码/解 码,完成输入输出控制。
通道介绍:
这里主要介绍ServerSocketChannel和 SocketChannel.它们都是可选择的(selectable)通道,分别可以工作在同步和异步两种方式下(注意,这里的可选择不是指可以选择两 种工作方式,而是指可以有选择的注册自己感兴趣的事件)。可以用channel.configureBlocking(Boolean )来设置其工作方式。与以前版本的API相比较,ServerSocketChannel就相当于 ServerSocket(ServerSocketChannel封装了ServerSocket),而SocketChannel就相当于 Socket(SocketChannel封装了Socket)。当通道工作在同步方式时,编程方法与以前的基本相似,这里主要介绍异步工作方式。
所 谓异步输入输出机制,是指在进行输入输出处理时,不必等到输入输出处理完毕才返回。所以异步的同义语是非阻塞(None Blocking)。在服务器端,ServerSocketChannel通过静态函数open()返回一个实例serverChl。然后该通道调用 serverChl.socket().bind()绑定到服务器某端口,并调用register(Selector sel, SelectionKey.OP_ACCEPT)注册OP_ACCEPT事件到一个选择器中(ServerSocketChannel只可以注册 OP_ACCEPT事件)。当有客户请求连接时,选择器就会通知该通道有客户连接请求,就可以进行相应的输入输出控制了;在客户端,clientChl实 例注册自己感兴趣的事件后(可以是OP_CONNECT,OP_READ,OP_WRITE的组合),调用 clientChl.connect(InetSocketAddress )连接服务器然后进行相应处理。注意,这里的连接是异步的,即会立即返回而继续执行后面的代码。
选择器和选择键介绍:
选择器 (Selector)的作用是:将通道感兴趣的事件放入队列中,而不是马上提交给应用程序,等已注册的通道自己来请求处理这些事件。换句话说,就是选择器 将会随时报告已经准备好了的通道,而且是按照先进先出的顺序。那么,选择器是通过什么来报告的呢?选择键(SelectionKey)。选择键的作用就是 表明哪个通道已经做好了准备,准备干什么。你也许马上会想到,那一定是已注册的通道感兴趣的事件。不错,例如对于服务器端serverChl来说,可以调 用key.isAcceptable()来通知serverChl有客户端连接请求。相应的函数还 有:SelectionKey.isReadable(),SelectionKey.isWritable()。一般的,在一个循环中轮询感兴趣的事件 (具体可参照下面的代码)。如果选择器中尚无通道已注册事件发生,调用Selector.select()将阻塞,直到有事件发生为止。另外,可以调用 selectNow()或者select(long timeout)。前者立即返回,没有事件时返回0值;后者等待timeout时间后返回。一个选择器最多可以同时被63个通道一起注册使用。
应用实例:
下面是用异步输入输出机制实现的客户/服务器实例程序――程序清单1(限于篇幅,只给出了服务器端实现,读者可以参照着实现客户端代码):
程序类图
小结:
从 以上程序段可以看出,服务器端没有引入多余线程就完成了多客户的客户/服务器模式。该程序中使用了回调模式(CALLBACK)。需要注意的是,请不要将 原来的输入输出包与新加入的输入输出包混用,因为出于一些原因的考虑,这两个包并不兼容。即使用通道时请使用缓冲完成输入输出控制。该程序在 Windows2000,J2SE1.4下,用telnet测试成功。
传统的Socket是阻塞,像ServerSocket在调用accept方法后便处于阻塞状态等待Client端的连接,所以一般会在Server端使用许多线程,对每一个Socket连接分配一个线程。充分利用并发特性来提高性能。
但这样会带来许多问题:
1、Server端创建了许多线程来处理Socket连接,而这些线程大部分的时间都在等待连接,
也就是说这些线程占了资源,真正做事情的时间却不多。也就是说资源利用率比较低,这就会
直接导致一个问题,可伸缩性比较差,当接受1000个连接还可以,但增加到10000个或更多时性能会很快下降。像Web服务器Jetty和Tomcat6.x都是用了异步通信模式,来接受客户端的连接,从而很大程度上提高了系统的伸缩性,提高了性能。
2、由于使用多线程,就会使问题变得复杂,事实如果你敢说精通并发编程,说明你太乐观了,并发编程很容易出错,并且你很难发现问题。并且需要互斥访问一些资源,这往往是个瓶颈,会降低并发性。
异步的Socket可以解决上面的问题,异步的Socket它是非阻塞的,它尝试去连接,但不管是否能够立即建立连接,它都会立即返回,返回之后它便可以做其他的事情了,但连接真正建立成功,就会有相应的事件来通知,这时候你去做连接成功之后的读写操作了.这个过程,整个线程都是处于忙碌状态,所以只需要单个或者很少几个线程,就可以达到阻塞方式的成百上千的线程的性能.
类似异步Socket功能应运而生,但Java在jdk1.4才引入这个功能,考虑以前Socket的已提供的功能,而且接口很难一致,Java并没有单独设计异步Socket,而是在Java nio中引入了SocketChannel之类的通道来处理这个问题,我们会发现这些通道和对应的Socket提供的接口有很大的相似之处,但这些Channel不是关于Socket的抽象,它们并不处理TCP/UDP协议,而是委托给对Socket来处理。
这种新的Channel可以在非阻塞的模式下操作。通过注册Selector,使用一种类似观察者模式。其实是Selector不断的轮询连接的端口,我们可以通过Selector的select()方法,这个方法是阻塞的,他会更新就绪操作集的键的数目,并作为返回值返回。我们会通常在判断这个返回值如果不为零,则可能有我们感兴趣的事件发生,然后我们可以通过
- Iterator it = selector.selectedKeys().iterator();
来得到键的迭代器,这样我们就可以通过遍历这个集合来判断有没有我们感兴趣的事情发生,如果有我们就做一些处理,没有我们可以把这个键remove掉:
- while(it.hasNext()){
- SelectionKey key = (SelectionKey)it.next();
- if(key.isAcceptable()){
- //do something
- }
- if(key.isReadable()){
- //read data
- }
- it.remove();//we remove the key beacause of we don't care about it
- }
一、下面我们介绍一下与Socket相关的各种Channel
与几种Socket对应的提供了一下几种Channel:
ServerSocketChannel,SocketChannel,DatagramChannel.
1、ServerSocketChannel:
- abstract SocketChannel accept()
- // 接受到此通道套接字的连接。
- static ServerSocketChannel open()
- //打开服务器套接字通道。
- abstract ServerSocket socket()
- //获取与此通道关联的服务器套接字。
- int validOps()
- //返回一个操作集,标识此通道所支持的操作。
我们发现这个Channel根本不支持读写操作,叫Channel有点名不副实了,但Java中Channel本身的抽象也不一定支持读写操作的,需要实现WriteableChannel和ReadableChannnel接口才能支持。其实ServerSocketChannel本身也不是用于读写
操作的,它通常通过socket() 方法获取相关的ServerSocket,然后通过ServerSocket 方法bind来绑定端口。ServerSocketChannel提供了open()静态工厂方法来创建ServerSocketChannel对象。同时ServerSocketChannel从AbstractSelectableChannel
继承了:
- SelectableChannel configureBlocking(boolean block)
- //调整此通道的阻塞模式。
- SelectionKey register(Selector sel, int ops)
- //向给定的选择器注册此通道,返回一个选择键。
这两个方法是最常用的了。通过configureBlocking来设置通道的是否为阻塞模式,
通过register向给定的选择器注册此通道。
从上面的过程我们用代码总结一下一般的流程:
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- ServerSocket serverSocket = serverSocketChannel.socket();
- Selector selector = Selector.open();
- serverSocket.bind(new InetSocketAddress(port));
- serverSocketChannel.configureBlocking(false);
- serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
- while (true) {
- int n = selector.select();
- if( n == 0)
- continue;
- Iterator it = selector.selectedKeys().iterator();
- while(it.hasNext()){
- SelectionKey key = (SelectionKey)it.next();
- if(key.isAcceptable()){
- ServerSocketChannel server =
- (ServerSocketChannel) key.channel();
- SocketChannel channel = server.accept();//获取SocketChannel来通信
- registerChannel (selector, channel,
- SelectionKey.OP_READ); doSomething(channel);
- }
- if(key.isReadable()){
- readDataFormSocket(key);
- }
- it.remove();
- }
- }
2、SocketChannel:
SocketChannel通常作为客户端,建立一个对Server端的连接,任何一个SocketChannel
都是和对应的Socket关联的,但一个Socket并不一定有SocketChannel可以获得。
同样的SocketChannel也适用静态工厂方法open()来实例化SocketChannel.
下面来看看最常用的操作:
public abstract boolean connect(SocketAddress remote)
throws IOException连接此通道的套接字。
如果此通道处于非阻塞模式,则调用此方法会发起一个非阻塞连接操作。如果立即建立连接(使用本地连接时就是如此),则此方法返回 true。否则此方法返回 false,并且必须在以后通过调用 finishConnect 方法来完成该连接操作。
我们可以通过
- socketChannel.connect (new InetSocketAddress ("somehost", somePort));
来建立连接,这个过程是异步的,他会立即返回,如果立即建立连接成功则返回true,否则
返回false.随后可以通过finishConnect来完成连接的建立。
另外可通过:
- SocketChannel socketChannel =
- SocketChannel.open (new InetSocketAddress ("somehost", somePort));
建立连接
等价于:
- SocketChannel socketChannel = SocketChannel.open();
- socketChannel.connect (new InetSocketAddress ("somehost", somePort));
下面我们演示一下整个的使用过程:
- InetSocketAddress addr = new InetSocketAddress (host, port);
- SocketChannel sc = SocketChannel.open();
- sc.configureBlocking (false);
- sc.connect (addr);
- while ( ! sc.finishConnect()) {
- doSomethingElse();
- }
- doSomethingWithChannel (sc);
- sc.close();
3、DatagramChannel
这个Channel与DatagramSocket相对应,提供了基于UDP协议的数据包的套接字的通道。
UDP协议是无连接的,DatagramChannelt既可以作为Server端,也可以作为Client端,如果想新创建一个DatagramChannel作为Server端来监听,那么需要绑定到特定的端口或地址和端口的组合,一般过程如下:
- DatagramChannel channel = DatagramChannel.open();
- DatagramSocket socket = channel.socket();
- socket.bind (new InetSocketAddress (portNumber));
但是一个没有绑定特定端口的DatagramChannel仍然是可以接收数据的,事实上会有一个
动态生成的端口分配给他。不管DatagramChannel是否绑定到了一个端口,任何一个包的发送都会包含它的地址,下面是DatagramChannel提供的发送和接收数据的方法:
- SocketAddress receive (ByteBuffer dst) throws
- IOException;
- int send (ByteBuffer src, SocketAddress target)
DatagramChannel并不能保证数据能够发送到目的端,因为UDP协议本身就是不可靠的。
另外我们再看看DatagramChannel提供了以下下几个方法:
- public abstract DatagramChannel connect (SocketAddress remote)
- throws IOException;
- public abstract boolean isConnected();
- public abstract DatagramChannel disconnect() throws IOException;
从名字看起来很让人迷惑,因为DatagramChannel就是基于无连接的,为什么还会有
connect之类的方法呢?
其实这个Connect的语义和基于流的Socket是不一样的,这里的连接只是制定了远程的
地址,这样就可以忽略其他地址发来的数据包了。一但使用完connect方法,就不能像其
他的地址send数据了。但这里的connect和SockectChannel的connect不同,它是可以
随时disconnect,然后去connect其他的地址的。但使用了connect方法之后,就可以
像FileChannel那样read,write数据,而不用指名地址了。