SelectableChannel
可选通道的使用大致过程如下:
1. 新建通道
2. 将通道已经通道感兴趣的事件注册到选择器Selector上
3. 通过SelectKey获得需要处理的通道,然后对通道进行处理
SelectableChannel实现类
下图是SelectableChannel的实现类,可以看出主要分为几大块:
- 有关UDP协议的:DatagramChannel
- 有关SCTP协议的:SctpChannel、SctpMultiChannel、SctpServerChannel
- [有关TCP协议的:ServerSocketChannel、SocketChannel
- 有关管道的:SinkChannel、SourceChannel这两个抽象类定义在java.nio.channels.Pipe类中
所以下面从以上四中类型来讲述:
Socket/ServerSocket、SocketChannel/ServerSocketChannel
要说SocketChannel
与ServerSocketChannel
,就避不开Socket
与ServerSocket
这两个类,这两个类位于java.net包下。
- 服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接。
- 客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接。
- 服务器接受到客户端的连接受,再生成一个Socket或者SocketChannel与此客户端通信
Socket和SocketChannel可以通过 socket.channel() SocketChannel.socket() 方法相互转换,同理ServerSocket 和ServerSocketChannel 也可以相互转换
Socket与ServerSocket
- Socket: 套接字是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两个不同计算机上的两个应用可以通过连接发送和接受字节流。也就是在java中通信是在两个Socket之间进行的。
- ServerSocket:服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。
所以二者的关系相当于:
Socket简介
Socket的API如下:
将上面的API函数大致可以分为几部分,上图中的画圈部分并不一定准确,只是大概:
①:是当前Socket的状态
②:是构造Socket,以及与其他Socket进行连接,还有获得当前Socket与连接的Socket的信息
③:有关数据流转的设置,比如超时时间、Nagle算法、窗口大小等
ServerSocket简介
ServerSocket的API如下:
①:ServerSocket的状态属性
②:ServerSocket的构造函数、监听地址端口等
③:数据传输的属性,比如支不支持MulticastSockets等
Socket与ServerSocket使用实例
首先开启一个服务端程序:启动后程序会阻塞在serverSocket.accept()这一行。
然后使用下面两种方式进行访问:
- 使用socket程序
- 使用java程序进行访问
服务端程序:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.CharBuffer;
public class TestServerSocket {
public static void main(String[] args) {
try {
InetAddress inetAddress = InetAddress.getLocalHost(); //主机名为:Q5158E0K1TDIR55
@SuppressWarnings("resource")
ServerSocket serverSocket = new ServerSocket(8089, 20, inetAddress);
while(true) {
Socket socket = serverSocket.accept();
socket.setSoTimeout(10);//设置socket超时时间
//也可以使用线程池,这么弄,差不多就是一个socket连接一个线程。
//当然也可以使用队列,将任务放在队列中,使用线程池来处理
new Thread(new Runnable() {
public void run() {
//获得输入流
try {
//读取数据,当使用命令行telnet时,不输入字符,会一致阻塞在read(b)中
// InputStream inputStream = socket.getInputStream();
// OutputStream outputStream = socket.getOutputStream();
// byte[] b = new byte[100];
// while(-1 != inputStream.read(b)) {
// System.out.println(0);
// //获得输出流,并将其转换为 printStream,并传给客户端“----已收到”
outputStream.write("123".getBytes());
// }
// inputStream.close();
// outputStream.close();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK"));
CharBuffer chars = CharBuffer.allocate(20);
PrintStream printStream = new PrintStream(socket.getOutputStream(), true, "GBK");
String str = "";
while(-1 != (str = bufferedReader.readLine()).length()) {
System.out.println(str);
//获得输出流,并将其转换为 printStream,并传给客户端“----已收到”
printStream.println("-----已收到" + str + "-----");
if("exit".equals(str)) {
break;
}
}
bufferedReader.close();
printStream.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用telnet访问
当我使用telnet进行访问的时候,出现了三个问题
- 一个是输入中文的问题,使用cmd中发送中文字符变为??,且回传的字符也不对。我认为代码是没有问题,假如传输的是gbk字符,那么接受用gbk接受。我认为是当前telnet不支持中文。我下载了一个PuTTY使用telnet进行发送字符,设置telnet连接传输为utf-8,然后java文件中使用utf-8进行接受解析,最后返回显示在PuTTY中不乱码。
- 一个是命令行中输入的字符有时不显示的问题:在输入telnet Q5158E0K1TDIR55 8089后,回车,然后点击
ctrl+]
,然后回车,此时就会回显了。 - 如何从命令行关闭连接:点击
ctrl+]
,然后输入quit
,就会退出
使用socket程序进行访问
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class TestSocket {
public static void main(String[] args) {
try {
Socket socket = new Socket("Q5158E0K1TDIR55", 8089);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK"));
PrintStream printStream = new PrintStream(socket.getOutputStream(), true, "GBK");
printStream.println("我们都是好孩子");
System.out.println(bufferedReader.readLine());
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
SocketChannel/ServerSocketChannel
可选择的通道,通过SelectKey选择准备就绪的通道。
- 创建一个选择器Selector,这个选择器包含(标识符selectkey、准备就绪的通道)的集合
- 创建通道(使用open创建,绑定地址),将(通道、通道感兴趣的事件)注册到Selector上,等待事件的到来
- 遍历触发的事件的集合,获得事件对应的通道,最后处理事件
这两个类都继承自抽象类AbstractSelectableChannel,该类的API函数如下:
SocketChannel与ServerSocketChannel的API函数如下:
代码示例
SocketChannel、ServerSocketChannel对于SelectableChannel来说是同一级别的,都是注册在Selector上。
服务端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* 测试可选通道,
* 1.将通道(包括ServerSocketChannel、SocketChannel)注册到Selector上,
* 2.由Selector获得当前通道状态
* 3.将这些用户感兴趣的通道状态的通道拿出来处理
* @author zt
*/
public class TestServerSocketChannel {
public static void main(String[] args) {
TestServerSocketChannel testServerSocketChannel = new TestServerSocketChannel();
testServerSocketChannel.test();
}
public void test() {
try {
//创建一个选择器,在这个选择器中创建了一个Pipe
Selector selector = Selector.open();
//打开一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置通道为非阻塞模式
serverSocketChannel.configureBlocking(false);
//在通道上绑定一个 ServerSocket 地址
serverSocketChannel.socket().bind(new InetSocketAddress(8090));
//将通道注册到选择器上,并且设置选择器感兴趣的事件是 OP_ACCEPT
//注意:一个通道可以注册到多个选择器上,但是一个同一个通道在同一个选择器中只能注册一次
//另外,不同的通道在选择器中注册的感兴趣事件不一样,也就是第二个参数是有限制的,由Channel类型决定
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//此时相当于服务端已经弄好了,就等连接到来
System.out.println("-------服务端开始等待客户端连接------------");
while(true) { //等待连接到来
//选择器阻塞,等待事件到来,事件不到来,阻塞,事件到来,返回事件到来的通道个数
int i = selector.select();
if(i == 0) {
continue;
}
//遍历感兴趣的事件
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()) {
SelectionKey selectionKey = it.next();
//如果是OP_ACCEPT,表示这个事件是ServerSocketChannel的事件,因为SocketChannel没有这个事件
if(selectionKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
//ServerSocketChannel准备好接受连接,产生一个SocketChannel,用于获得当前准备接受的连接
//建立连接
SocketChannel channel = server.accept();
//将建立的连接设置为准备读,并注册到选择器上
registerChannel(selector, channel, SelectionKey.OP_READ);
//向建立的通道中写入 "Hi there!\r\n"
sayHello(channel);
}
//如果是可读事件,没有在ServerSocketChannel上注册可读,只在SocketChannel上注册了可读
if(selectionKey.isReadable()) {
//从通道中读取数据,
//只有通道中有数据了,才会进入这个if块中
readFromSocket(selectionKey);
}
//此时只是将当前触发的事件list中移除,
//在下一次的select时,还会查看该通道是否有感兴趣的事件发生
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向通道中写入"Hi there!\r\n"
* @param channel
* @throws IOException
*/
private void sayHello(SocketChannel channel) throws IOException {
byteBuffer.clear();
byteBuffer.put("Hi there!\r\n".getBytes());
byteBuffer.flip();
channel.write(byteBuffer);
}
/**
* 将channel注册到选择器上
* @param selector 选择器
* @param channel 通道
* @param opt 感兴趣的事件
* @throws IOException
*/
public void registerChannel(Selector selector, SocketChannel channel, int opt) throws IOException {
if(null == channel) {
return;
}
channel.configureBlocking(false);
channel.register(selector, opt);
}
private ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
/**
* 从selectionKey的通道中读取数据
* @param selectionKey
* @throws IOException
*/
public void readFromSocket(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
byteBuffer.clear();
int count = 0;
//当有数据可读时,第一次读到的数据不应该是0,它会一直读取,直到读取的数据为0,或为-1,就将
while((count = socketChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
//将数据回写到通道中
while(byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
byteBuffer.clear();
}
if(count < 0) {
socketChannel.close();
}
}
}
客户端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class TestSocketChannel {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(true);
System.out.println(socketChannel.connect(new InetSocketAddress("Q5158E0K1TDIR55", 8090)));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true) {
int count = 0;
while((count = socketChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
//这句无法读出正确的值
//System.out.println("收到char:" + byteBuffer.asCharBuffer());
byteBuffer.clear();
}
socketChannel.write(byteBuffer.put("return".getBytes()));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
DatagramSocket与DatagramChannel
DatagramSocket是对UDP的封装,DatagramSocket本身不维护连接的状态,因为UDP协议面向非连接,所以也不会产生IO流,只是用来发送与接收数据报。在java中数据报使用DatagramPacket来表示,所以最有用的方法是send与receive,表示发送与接收报文。可以使用DatagramSocket来收发数据报,也可以使用DatagramChannel来收发数据。
UDP的这种方式,定义服务端与客户端都是DatagramSocket,该类作为两个端点,只是用来接收报文与发送报文。两个DatagramSocket之间的交互使用DatagramPacker来交换信息。由于UDP面向无连接,所以两个端点端不需要持有另一端的地址以及port信息。而两个端点进行交互时使用的DatagramPacker含有该报文发送方的信息。
DatagramChannel,可以充当服务器(监听者)也可以充当客户端(发送者)。如果您希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。
他们的API函数如下所示:
下面是DatagramSocket的函数API:
①:当前DatagramSocket的一些状态
②:初始化DatagramSocket的信息,这里传入的地址与端口,都是自己的。
③:数据的传输以及一些配置
④:获得Channel
下图是DatagramSocketChannel的函数API:
DatagramSocket示例:
服务端,也就是绑定一个自己的端口接收数据
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 开启一个udp端口,等待数据到来。并将传来的数据传回去
* @author zt
*
*/
public class TestDatagramSocketServer {
private static byte[] buf = new byte[1024];
@SuppressWarnings("resource")
public static void main(String[] args) {
try {
//在8090端口等待数据到来
DatagramSocket datagramSocket = new DatagramSocket(8090);
//定义接收数据包
DatagramPacket receiveP = new DatagramPacket(buf , buf.length);
//在此处阻塞,直到有数据到来
datagramSocket.receive(receiveP);
System.out.println("接受到的数据:" + new String(buf, 0, receiveP.getLength()));
//定义发送的数据包
DatagramPacket sendP = new DatagramPacket(buf, buf.length, receiveP.getAddress(), receiveP.getPort());
datagramSocket.send(sendP);
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:发送报文数据
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* 客户端使用SyStem.in作为输入,将数据发送到另一个DatagramSocket中
* @author zt
*
*/
public class TestDatagramSocketClient {
public static void main(String[] args) {
try {
DatagramSocket datagramSocket = new DatagramSocket();
byte[] buf = new byte[1024];
DatagramPacket sendP = new DatagramPacket(new byte[0] , 0, InetAddress.getByName("127.0.0.1"), 8090);
DatagramPacket receiveP = new DatagramPacket(buf, buf.length);
//使用控制台输入作为输入
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
byte[] bytes = scanner.nextLine().getBytes();
sendP.setData(bytes);
datagramSocket.send(sendP);
datagramSocket.receive(receiveP);
System.out.println(new String(buf, 0, receiveP.getLength()));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
DatagramSocketChannel示例(下面例子来自java NIO,使用 DatagramChannel 的时间服务客户端):
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.net.InetSocketAddress;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
/**
* Request time service, per RFC 868. RFC 868
* (http://www.ietf.org/rfc/rfc0868.txt) is a very simple time protocol
* whereby one system can request the current time from another system.
* Most Linux, BSD and Solaris systems provide RFC 868 time service
* on port 37. This simple program will inter-operate with those.
* The National Institute of Standards and Technology (NIST) operates
* a public time server at time.nist.gov.
*
* The RFC 868 protocol specifies a 32 bit unsigned value be sent,
* representing the number of seconds since Jan 1, 1900. The Java
* epoch begins on Jan 1, 1970 (same as unix) so an adjustment is
* made by adding or subtracting 2,208,988,800 as appropriate. To
* avoid shifting and masking, a four-byte slice of an
* eight-byte buffer is used to send/recieve. But getLong( )
* is done on the full eight bytes to get a long value.
*
* When run, this program will issue time requests to each hostname
* given on the command line, then enter a loop to receive packets.
* Note that some requests or replies may be lost, which means
* this code could block forever.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class TestDatagramChannelClient {
private static final int DEFAULT_TIME_PORT = 37;
private static final long DIFF_1900 = 2208988800L;
protected int port = DEFAULT_TIME_PORT;
protected List remoteHosts;
protected DatagramChannel channel;
public TestDatagramChannelClient (String [] argv) throws Exception {
if (argv.length == 0) {
throw new Exception ("Usage: [ -p port ] host ...");
}
parseArgs (argv);
this.channel = DatagramChannel.open( );
}
protected InetSocketAddress receivePacket (DatagramChannel channel, ByteBuffer buffer) throws Exception {
buffer.clear( );
// Receive an unsigned 32-bit, big-endian value
return ((InetSocketAddress) channel.receive (buffer));
}
// Send time requests to all the supplied hosts
protected void sendRequests( ) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate (1);
Iterator it = remoteHosts.iterator( );
while (it.hasNext( )) {
InetSocketAddress sa = (InetSocketAddress) it.next( );
System.out.println ("Requesting time from "
+ sa.getHostName() + ":" + sa.getPort( ));
// Make it empty (see RFC868)
buffer.clear().flip( );
// Fire and forget
channel.send (buffer, sa);
}
}
// Receive any replies that arrive
public void getReplies( ) throws Exception {
// Allocate a buffer to hold a long value
ByteBuffer longBuffer = ByteBuffer.allocate (8);
// Assure big-endian (network) byte order
longBuffer.order (ByteOrder.BIG_ENDIAN);
// Zero the whole buffer to be sure
longBuffer.putLong (0, 0);
// Position to first byte of the low-order 32 bits
longBuffer.position (4);
// Slice the buffer; gives view of the low-order 32 bits
ByteBuffer buffer = longBuffer.slice( );
int expect = remoteHosts.size( );
int replies = 0;
System.out.println ("");
System.out.println ("Waiting for replies...");
while (true) {
InetSocketAddress sa;
sa = receivePacket (channel, buffer);
buffer.flip( );
replies++;
printTime (longBuffer.getLong (0), sa);
if (replies == expect) {
System.out.println ("All packets answered");
break;
}
// Some replies haven't shown up yet
System.out.println ("Received " + replies
+ " of " + expect + " replies");
}
}
// Print info about a received time reply
protected void printTime (long remote1900, InetSocketAddress sa) {
// local time as seconds since Jan 1, 1970
long local = System.currentTimeMillis( ) / 1000;
// remote time as seconds since Jan 1, 1970
long remote = remote1900 - DIFF_1900;
Date remoteDate = new Date (remote * 1000);
Date localDate = new Date (local * 1000);
long skew = remote - local;
System.out.println ("Reply from "
+ sa.getHostName() + ":" + sa.getPort( ));
System.out.println (" there: " + remoteDate);
System.out.println (" here: " + localDate);
System.out.print (" skew: ");
if (skew == 0) {
System.out.println ("none");
} else if (skew > 0) {
System.out.println (skew + " seconds ahead");
} else {
System.out.println ((-skew) + " seconds behind");
}
}
protected void parseArgs (String [] argv) {
remoteHosts = new LinkedList( );
for (int i = 0; i < argv.length; i++) {
String arg = argv [i];
// Send client requests to the given port
if (arg.equals ("-p")) {
i++;
this.port = Integer.parseInt (argv [i]);
continue;
}
// Create an address object for the hostname
InetSocketAddress sa = new InetSocketAddress (arg, port);
// Validate that it has an address
if (sa.getAddress( ) == null) {
System.out.println ("Cannot resolve address: "
+ arg);
continue;
}
remoteHosts.add (sa);
}
}
// --------------------------------------------------------------
public static void main (String [] argv) throws Exception {
TestDatagramChannelClient client = new TestDatagramChannelClient (argv);
client.sendRequests( );
client.getReplies( );
}
}
下面代码是RFC868时间服务,作为时间服务器:
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.net.SocketAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
/**
* Provide RFC 868 time service (http://www.ietf.org/rfc/rfc0868.txt).
* This code implements an RFC 868 listener to provide time
* service. The defined port for time service is 37. On most
* unix systems, root privilege is required to bind to ports
* below 1024. You can either run this code as root or
* provide another port number on the command line. Use
* "-p port#" with TimeClient if you choose an alternate port.
*
* Note: The familiar rdate command on unix will probably not work
* with this server. Most versions of rdate use TCP rather than UDP
* to request the time.
* * @author Ron Hitchens (ron@ronsoft.com)
*/
public class TestDatagramChannelServer {
private static final int DEFAULT_TIME_PORT = 37;
private static final long DIFF_1900 = 2208988800L;
protected DatagramChannel channel;
public TestDatagramChannelServer (int port) throws Exception {
this.channel = DatagramChannel.open( );
this.channel.socket( ).bind (new InetSocketAddress (port));
System.out.println ("Listening on port " + port
+ " for time requests");
}
public void listen( ) throws Exception {
// Allocate a buffer to hold a long value
ByteBuffer longBuffer = ByteBuffer.allocate (8);
// Assure big-endian (network) byte order
longBuffer.order (ByteOrder.BIG_ENDIAN);
// Zero the whole buffer to be sure
longBuffer.putLong (0, 0);
// Position to first byte of the low-order 32 bits
longBuffer.position (4);
// Slice the buffer; gives view of the low-order 32 bits
ByteBuffer buffer = longBuffer.slice( );
while (true) {
buffer.clear( );
SocketAddress sa = this.channel.receive (buffer);
if (sa == null) {
continue; // defensive programming
}
// Ignore content of received datagram per RFC 868
System.out.println ("Time request from " + sa);
buffer.clear( ); // sets pos/limit correctly
// Set 64-bit value; slice buffer sees low 32 bits
longBuffer.putLong (0,
(System.currentTimeMillis( ) / 1000) + DIFF_1900);
this.channel.send (buffer, sa);
}
}
// --------------------------------------------------------------
public static void main (String [] argv) throws Exception {
int port = DEFAULT_TIME_PORT;
if (argv.length > 0) {
port = Integer.parseInt (argv [0]);
}
try {
TestDatagramChannelServer server = new TestDatagramChannelServer (port);
server.listen( );
} catch (SocketException e) {
System.out.println ("Can't bind to port " + port
+ ", try a different one");
}
}
}
Pipe
Pipe的API如下:
Pipe 类创建一对提供环回机制的 Channel 对象。这两个通道的远端是连接起来的,以便任何写在 SinkChannel 对象上的数据都能出现在 SourceChannel 对象上。
Pipe 实例是通过调用不带参数的 Pipe.open( )工厂方法来创建的。Pipe 类定义了两个嵌套的通道类来实现管路。这两个类是 Pipe.SourceChannel(管道负责读的一端)和 Pipe.SinkChannel(管道负责写的一端)。这两个通道实例是在 Pipe 对象创建的同时被创建的,可以通过在 Pipe 对象上分别调用 source( )和 sink( )方法来取回。
代码示例(来之Java nio书):
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.Pipe;
import java.nio.channels.Channels;
import java.util.Random;
/**
* Test Pipe objects using a worker thread.
*
* Created April, 2002
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class TestPipe {
public static void main (String [] argv) throws Exception {
// Wrap a channel around stdout
WritableByteChannel out = Channels.newChannel (System.out);
// Start worker and get read end of channel
ReadableByteChannel workerChannel = startWorker (10);
ByteBuffer buffer = ByteBuffer.allocate (100);
while (workerChannel.read (buffer) >= 0) {
buffer.flip( );
out.write (buffer);
buffer.clear( );
}
}
// This method could return a SocketChannel or
// FileChannel instance just as easily
private static ReadableByteChannel startWorker (int reps) throws Exception {
Pipe pipe = Pipe.open( );
Worker worker = new Worker (pipe.sink( ), reps);
worker.start( );
return (pipe.source( ));
}
//-----------------------------------------------------------------
/**
* A worker thread object which writes data down a channel.
* Note: this object knows nothing about Pipe, uses only a
* generic WritableByteChannel.
*/
private static class Worker extends Thread {
WritableByteChannel channel;
private int reps;
Worker (WritableByteChannel channel, int reps) {
this.channel = channel;
this.reps = reps;
}
//Thread execution begins here
public void run( ) {
ByteBuffer buffer = ByteBuffer.allocate (100);
try {
for (int i = 0; i < this.reps; i++) {
doSomeWork (buffer);
//channel may not take it all at once
while (channel.write (buffer) > 0) {
//empty
}
}
this.channel.close( );
} catch (Exception e) {
//easy way out; this is demo code
e.printStackTrace( );
}
}
private String [] products = {
"No good deed goes unpunished",
"To be, or what?",
"No matter where you go, there you are",
"Just say \"Yo\"",
"My karma ran over my dogma"
};
private Random rand = new Random( );
private void doSomeWork (ByteBuffer buffer) {
int product = rand.nextInt (products.length);
buffer.clear( );
buffer.put (products [product].getBytes( ));
buffer.put ("\r\n".getBytes( ));
buffer.flip( );
}
}
}
SctpChannel、SctpMultiChannel、SctpServerChannel
这三个类是在jdk1.7中添加的。下面的内容来自https://blog.youkuaiyun.com/thirtyfive16/article/details/53124087?locationNum=5&fps=1
SCTP是一个可靠的,面向消息的传输协议。在TCP/IP协议中和UDP/TCP处于同一层。SCTP是基于sesstion的,要先建立端点之间的连接,然后传输数据。
SCTP支持多址的,也就是说一个端点可以由多个地址表示,每个地址可以用来传输数据,从而提供了网络的冗余。端点之间可以在建立连接的时候,交换多个地址。其中一个地址是主地址,也是向对端传输数据的默认地址。一个端口代表一个特定的session。
SCTP是基于消息的。每一个association支持多个独立的逻辑流。每个流代表了一系列顺序消息。每个流都是相互独立的,也就是流的唯一标识和序列号在数据包中存在,从而使得每个流中的消息都是顺序传递的。
SCTP API是基于NIO设计的,因此可以利用非阻塞的复用I/O. 引入了一个新的包com.sun.nio.sctp。包名字不是java.nio.channels.sctp,这意味着这些API是完全公开的。当这些API成熟后,再引入到标准API中。
这个包中的主要类有三个新channel类型。可以分为两个逻辑组。
- 第一个组和TCP类似,包括了sctpChannel 和 SctpServerChannel。一个SctpChannel只能控制一个association,也就是说只能和一个端点发送接受数据。 SctpSeverChannel监听和接受在socket地址上的接入。
- 第二组包括了SctpMultiChannel。这个类可以控制多个association,因此可以和多个端点传送数据。
SCTP是事件驱动的。程序能接收到特定的SCTP事件。这些事件尤其实在SctpMultiChannel有用。SctpMultiChannel可以控制多个association,所以需要跟踪每个association的状态。例如,当收到AssociationChangNotification的时候,表示有个新的连接的association或者断开。如果association支持动态地址配置,PeerAddressChangeNotification表示IP地址在对端刚刚增加或者删除。
例子展示了多个流。服务器端视线了一个时间的协议,它把当前日期时间在一个流中以英语形式传递,一个流中以法语形式传递。为了简便,本程序没有考虑异常处理。
服务端代码如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import com.sun.nio.sctp.MessageInfo;
import com.sun.nio.sctp.SctpChannel;
import com.sun.nio.sctp.SctpServerChannel;
public class DaytimeServer {
static int SERVER_PORT = 3456;
static int US_STREAM = 0;
static int FR_STREAM = 1;
static SimpleDateFormat USformatter = new SimpleDateFormat(
"h:mm:ss a EEE d MMM yy, zzzz", Locale.US);
static SimpleDateFormat FRformatter = new SimpleDateFormat(
"h:mm:ss a EEE d MMM yy, zzzz", Locale.FRENCH);
public static void main(String[] args) throws IOException {
SctpServerChannel ssc = SctpServerChannel.open();
InetSocketAddress serverAddr = new InetSocketAddress(SERVER_PORT);
ssc.bind(serverAddr);
ByteBuffer buf = ByteBuffer.allocateDirect(60);
CharBuffer cbuf = CharBuffer.allocate(60);
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
while (true) {
SctpChannel sc = ssc.accept();
/* get the current date */
Date today = new Date();
cbuf.put(USformatter.format(today)).flip();
encoder.encode(cbuf, buf, true);
buf.flip();
/* send the message on the US stream */
MessageInfo messageInfo = MessageInfo.createOutgoing(null,
US_STREAM);
sc.send(buf, messageInfo);
/* update the buffer with French format */
cbuf.clear();
cbuf.put(FRformatter.format(today)).flip();
buf.clear();
encoder.encode(cbuf, buf, true);
buf.flip();
/* send the message on the French stream */
messageInfo.streamNumber(FR_STREAM);
sc.send(buf, messageInfo);
cbuf.clear();
buf.clear();
sc.close();
}
}
}
客户端代码:
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import com.sun.nio.sctp.AbstractNotificationHandler;
import com.sun.nio.sctp.AssociationChangeNotification;
import com.sun.nio.sctp.AssociationChangeNotification.AssocChangeEvent;
import com.sun.nio.sctp.HandlerResult;
import com.sun.nio.sctp.MessageInfo;
import com.sun.nio.sctp.SctpChannel;
import com.sun.nio.sctp.ShutdownNotification;
public class DaytimeClient {
static int SERVER_PORT = 3456;
static int US_STREAM = 0;
static int FR_STREAM = 1;
public static void main(String[] args) throws IOException {
InetSocketAddress serverAddr = new InetSocketAddress("localhost", SERVER_PORT);
ByteBuffer buf = ByteBuffer.allocateDirect(60);
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
SctpChannel sc = SctpChannel.open(serverAddr, 0, 0);
/* handler to keep track of association setup and termination */
AssociationHandler assocHandler = new AssociationHandler();
/* expect two messages and two notifications */
MessageInfo messageInfo = null;
do {
messageInfo = sc.receive(buf, System.out, assocHandler);
buf.flip();
if (buf.remaining() > 0 &&
messageInfo.streamNumber() == US_STREAM) {
System.out.println("(US) " + decoder.decode(buf).toString());
} else if (buf.remaining() > 0 &&
messageInfo.streamNumber() == FR_STREAM) {
System.out.println("(FR) " + decoder.decode(buf).toString());
}
buf.clear();
} while (messageInfo != null);
sc.close();
}
static class AssociationHandler extends AbstractNotificationHandler<PrintStream>
{
public HandlerResult handleNotification(AssociationChangeNotification not,
PrintStream stream) {
if (not.event().equals(AssocChangeEvent.COMM_UP)) {
int outbound = not.association().maxOutboundStreams();
int inbound = not.association().maxInboundStreams();
stream.printf("New association setup with %d outbound streams" +
", and %d inbound streams.\n", outbound, inbound);
}
return HandlerResult.CONTINUE;
}
public HandlerResult handleNotification(ShutdownNotification not, PrintStream stream) {
stream.printf("The association has been shutdown.\n");
return HandlerResult.RETURN;
}
}
}