TCP/IP 阻塞模式与非阻塞模式
package concurrentTest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* TCP/IP的阻塞模式
*/
public class TCPBIO {
/**
* 客户端监听事件
* @param ipStr IP地址
* @param portNum 端口号
* */
public void clientListen(String ipStr, int portNum)
{
try {
Socket socket = new Socket(ipStr, portNum);
//创建读取服务器端返回流的BufferReader
BufferedReader in = new BufferedReader(new
InputStreamReader(socket.getInputStream()));
//创建向服务器写入流的PrintWriter
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
//向服务器发送字符串信息,此处即使写失败也不会抛出异常,会一直阻塞到
//写入操作系统或者网络IO出现异常为止
out.println("hello java");
//阻塞读取服务器端的返回信息,以下代码会阻塞到服务器端返回信息或者网络IO出现异常为止
//若希望过段时间后不阻塞,则在socket创建后加入socket.setSoTimeout(time);
in.readLine();
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 服务器端监听事件
* @param portNum 端口号
* */
public void serverListen(int portNum)
{
ServerSocket ss;
try {
ss = new ServerSocket(portNum);
//通过Scoket.getInputStream和Socket.getOutputStream进行读写操作,此方法
//会一直阻塞到有客户端发送建立请求
Socket socket = ss.accept();
} catch (IOException e) {
// 网络IO异常
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
<PRE class=java name="code">package concurrentTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Set;
/**
* TCP/IP的 非阻塞模式
*/
public class TCPNIO {
/**
* 客户端监听事件
* @param ipStr IP地址
* @param portNum 端口号
* */
public void clientListen(String ipStr, int portNum, int blockTime)
{
try {
SocketChannel channel = SocketChannel.open();
//设置为非阻塞模式
channel.configureBlocking(false);
//对于非阻塞模式,立刻返回false,表示连接正在建立中
Selector selector = Selector.open();
//向channel注册selector和感兴趣事件
channel.register(selector, SelectionKey.OP_CONNECT);
//阻塞至有感兴趣的IO事件发生,或者到达超时时间,如果希望一直等待到有感兴趣的
//事件发生,可调用无参数的select方法,如果希望不阻塞直接返回目前是否有感兴趣的
//事件发生,可调用selectNow方法
int nKeys = selector.select(blockTime);
SelectionKey sKey = null;
//nKeys大于0,说明有感兴趣的事件发生
if(nKeys > 0)
{
Set<SelectionKey> keys = selector.selectedKeys();
for(SelectionKey key:keys)
{
//对于发生连接事件
if(key.isConnectable())
{
SocketChannel sc = (SocketChannel)key.channel();
sc.configureBlocking(false);
//注册感兴趣的IO事件,通常不直接注册写事件,在发送缓冲区未满的情况下
//一直是可写的,所以如果注册了写事件,而又不写数据,则很容易造成CPU消耗100%
sKey = sc.register(selector, SelectionKey.OP_READ);
//完成连接的建立
sc.finishConnect();
}
else if(key.isReadable())//有流可读取
{
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel sc = (SocketChannel)key.channel();
int readBytes = 0;
try {
int ret = 0;
try {
//读取目前可读的流,sc.read(buffer)返回的是成功复制到bytebuffer中
//的字节数,为阻塞操作,值可能为0,若到流结尾,返回-1
while ((ret = sc.read(buffer)) > 0) {
readBytes += ret;
}
} finally {
buffer.flip();
}
} finally{
if(buffer != null)
{
buffer.clear();
}
}
}
else if(key.isWritable())//可写入流
{
//取消对OP_WRITE事件的注册
ByteBuffer buffer = ByteBuffer.allocate(1024);
key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE));
SocketChannel sc = (SocketChannel)key.channel();
//此步为阻塞操作,知道写入操作系统发送缓冲区或者网络IO出现异常
//返回的为成功写入的字节数,若缓冲区已满,返回0
int writeenedSize = sc.write(buffer);
//若未写入,继续注册感兴趣的OP_WRITE事件
if(writeenedSize == 0)
{
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}
}
}
selector.selectedKeys().clear();
}
/*
* 也可以直接调用channel.write完成写
* 只有在写入未成功时才注册OP_WRITE事件
ByteBuffer buffer = ByteBuffer.allocate(1024);
int wSize = channel.write(buffer);
if(wSize == 0)
{
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}*/
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 服务器端监听事件
* @param portNum 端口号
* */
public void serverListen(int portNum, int blockTime)
{
ServerSocketChannel ssc;
try {
ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
serverSocket.bind(new InetSocketAddress(portNum));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
int nKeys = selector.select(blockTime);
if(nKeys > 0)
{
Set<SelectionKey> keys = selector.selectedKeys();
for(SelectionKey key:keys)
{
if(key.isAcceptable())
{
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel sc = server.accept();
if(sc == null)
{
continue;
}
sc.configureBlocking(false);
//注册感兴趣的连接建立事件
sc.register(selector, SelectionKey.OP_READ);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
</PRE><BR>
<BR>
<PRE></PRE>
<BR>
UDP/IP 阻塞模式与非阻塞模式
package concurrentTest;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
/**
* UDP/IP阻塞模式
* */
public class UDPBIO {
/**
* 服务器端和客户端类似
* @param ipStr IP地址
* @param portNum 端口号
* */
public void udpListen(String ipStr, int portNum)
{
try {
//若希望是全双工模式,则启动一个监听端口,承担服务器的职责
//若不能绑定到指定端口,则抛出SocketException
DatagramSocket serverSocket = new DatagramSocket(portNum);
InetAddress server = InetAddress.getByName(ipStr);
byte[] buffer = new byte[65507];
DatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(buffer,buffer.length,server,portNum);
//阻塞发送packet到指定的服务器和端口
//网络IO异常,抛出IOExceprion
//连不上IP和端口,抛出PortUnreachableException
socket.send(packet);
//阻塞并同步读取流信息,如接收到的信息流比packet长,则删除更长的信息
serverSocket.receive(receivePacket);
} catch (SocketException e) {
// 若不能绑定到指定端口
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
package concurrentTest;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
/**
* UDP/IP非阻塞模式
* */
public class UDPNIO {
/**
* 服务器端和客户端类似
* @param ipStr IP地址
* @param portNum 端口号
* */
public void udpListen(String ipStr, int portNum)
{
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
DatagramChannel reveiveChannel = DatagramChannel.open();
reveiveChannel.configureBlocking(false);
DatagramSocket socket = reveiveChannel.socket();
socket.bind(new InetSocketAddress(portNum));
Selector selector = Selector.open();
reveiveChannel.register(selector, SelectionKey.OP_READ);
//之后可采取和TCP/IP NIO中对selector遍历方式进行流信息的读取
DatagramChannel sendChannel = DatagramChannel.open();
sendChannel.configureBlocking(false);
SocketAddress target = new InetSocketAddress(InetAddress.getLocalHost(), portNum);
sendChannel.connect(target);
//阻塞写入流,若发送缓冲区已满,返回0,此时可继续注册OP_WRITE事件
sendChannel.write(buffer);
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 组播
* */
public void multicaseListen(String ipStr, int portNum)
{
try {
InetAddress groupAddress = InetAddress.getByName(ipStr);
MulticastSocket server = new MulticastSocket(portNum);
//加入组播,若地址为非组播地址,抛出IOException
//若不希望再发送数据到组播地址或者不希望再读取数据,可调用server.leaveGroup(组播地址)
server.joinGroup(groupAddress);
MulticastSocket client = new MulticastSocket();
client.joinGroup(groupAddress);
//
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// 若地址为非组播地址
e.printStackTrace();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
服务器代码:
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
public class server
{
ServerSocketChannel ssc ;
public void start()
{
try
{
Selector selector = Selector.open();
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocket ss=ssc.socket();
InetSocketAddress address = new InetSocketAddress(55555);
ss.bind(address);
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("端口注册完毕!");
while(true)
{
selector.select();
Set<SelectionKey> selectionKeys=selector.selectedKeys();
Iterator<SelectionKey> iter=selectionKeys.iterator();
ByteBuffer echoBuffer=ByteBuffer.allocate(20);
SocketChannel sc;
while(iter.hasNext())
{
SelectionKey key=iter.next();
if((key.readyOps()&SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT)
{
ServerSocketChannel subssc=(ServerSocketChannel)key.channel();
sc=subssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
iter.remove();
System.out.println("有新连接:"+sc);
}
else if((key.readyOps()&SelectionKey.OP_READ)==SelectionKey.OP_READ)
{
sc=(SocketChannel) key.channel();
while(true)
{
echoBuffer.clear();
int a;
try
{
a=sc.read(echoBuffer);
}
catch(Exception e)
{
e.printStackTrace();
break;
}
if(a==-1) break;
if(a>0)
{
byte[] b=echoBuffer.array();
System.out.println("接收数据: "+new String(b));
echoBuffer.flip();
sc.write(echoBuffer);
System.out.println("返回数据: "+new String(b));
}
}
sc.close();
System.out.println("连接结束");
System.out.println("=============================");
iter.remove();
}
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
客户端代码:
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
public class client
{
public void start()
{
try
{
SocketAddress address = new InetSocketAddress("localhost",55555);
SocketChannel client=SocketChannel.open(address);
client.configureBlocking(false);
String a="asdasdasdasddffasfas";
ByteBuffer buffer=ByteBuffer.allocate(20);
buffer.put(a.getBytes());
buffer.clear();
int d=client.write(buffer);
System.out.println("发送数据: "+new String(buffer.array()));
while(true)
{
buffer.flip();
int i=client.read(buffer);
if(i>0)
{
byte[] b=buffer.array();
System.out.println("接收数据: "+new String(b));
client.close();
System.out.println("连接关闭!");
break;
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
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的使用。
package sinpo.usagedemo;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author 徐辛波(sinpo.xu@hotmail.com) Oct 19, 2008
*/
public class UDPServer extends Thread {
public void run() {
Selector selector = null;
try {
DatagramChannel channel = DatagramChannel.open();
DatagramSocket socket = channel.socket();
channel.configureBlocking(false);
socket.bind(new InetSocketAddress(5057));
selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
} catch (Exception e) {
e.printStackTrace();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(65536);
while (true) {
try {
int eventsCount = selector.select();
if (eventsCount > 0) {
Set selectedKeys = selector.selectedKeys();
Iterator iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey sk = (SelectionKey) iterator.next();
iterator.remove();
if (sk.isReadable()) {
DatagramChannel datagramChannel = (DatagramChannel) sk
.channel();
SocketAddress sa = datagramChannel
.receive(byteBuffer);
byteBuffer.flip();
// 测试:通过将收到的ByteBuffer首先通过缺省的编码解码成CharBuffer 再输出
CharBuffer charBuffer = Charset.defaultCharset()
.decode(byteBuffer);
System.out.println("receive message:"
+ charBuffer.toString());
byteBuffer.clear();
String echo = "This is the reply message from 服务器。";
ByteBuffer buffer = Charset.defaultCharset()
.encode(echo);
datagramChannel.write(buffer);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new UDPServer().start();
}
}
Client:
package sinpo.usagedemo;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author 徐辛波(sinpo.xu@hotmail.com) Oct 19, 2008
*/
public class UDPClient extends Thread {
public void run() {
DatagramChannel channel = null;
Selector selector = null;
try {
channel = DatagramChannel.open();
channel.configureBlocking(false);
SocketAddress sa = new InetSocketAddress("localhost", 5057);
channel.connect(sa);
} catch (Exception e) {
e.printStackTrace();
}
try {
selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
channel.write(Charset.defaultCharset().encode("Tell me your time"));
} catch (Exception e) {
e.printStackTrace();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
while (true) {
try {
int eventsCount = selector.select();
if (eventsCount > 0) {
Set selectedKeys = selector.selectedKeys();
Iterator iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey sk = (SelectionKey) iterator.next();
iterator.remove();
if (sk.isReadable()) {
DatagramChannel datagramChannel = (DatagramChannel) sk
.channel();
datagramChannel.read(byteBuffer);
byteBuffer.flip();
// TODO 将报文转化为RUDP消息并调用RUDP协议处理器来处理
System.out.println(Charset.defaultCharset()
.decode(byteBuffer).toString());
byteBuffer.clear();
datagramChannel.write(Charset.defaultCharset()
.encode("Tell me your time"));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
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(限于篇幅,只给出了服务器端实现,读者可以参照着实现客户端代码):
程序类图
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
public class NBlockingServer {
int port = 8000;
int BUFFERSIZE = 1024;
Selector selector = null;
ServerSocketChannel serverChannel = null;
HashMap clientChannelMap = null;// 用来存放每一个客户连接对应的套接字和通道
public NBlockingServer(int port) {
this.clientChannelMap = new HashMap();
this.port = port;
}
public void initialize() throws IOException {
// 初始化,分别实例化一个选择器,一个服务器端可选择通道
this.selector = Selector.open();
this.serverChannel = ServerSocketChannel.open();
this.serverChannel.configureBlocking(false);
InetAddress localhost = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(localhost, this.port);
this.serverChannel.socket().bind(isa);// 将该套接字绑定到服务器某一可用端口
}
// 结束时释放资源
public void finalize() throws IOException {
this.serverChannel.close();
this.selector.close();
}
// 将读入字节缓冲的信息解码
public String decode(ByteBuffer byteBuffer) throws CharacterCodingException {
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(byteBuffer);
String result = charBuffer.toString();
return result;
}
// 监听端口,当通道准备好时进行相应操作
public void portListening() throws IOException, InterruptedException {
// 服务器端通道注册OP_ACCEPT事件
SelectionKey acceptKey = this.serverChannel.register(this.selector,
SelectionKey.OP_ACCEPT);
// 当有已注册的事件发生时,select()返回值将大于0
while (acceptKey.selector().select() > 0) {
System.out.println("event happened");
// 取得所有已经准备好的所有选择键
Set readyKeys = this.selector.selectedKeys();
// 使用迭代器对选择键进行轮询
Iterator iter = readyKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
if (key.isReadable()) {// 如果是通道读准备好事件
System.out.println("Readable");
// 取得选择键对应的通道和套接字
SelectableChannel nextReady = (SelectableChannel) key
.channel();
Socket socket = (Socket) key.attachment();
// 处理该事件,处理方法已封装在类ClientChInstance中
this.readFromChannel(socket.getChannel(),
(ClientChInstance) this.clientChannelMap
.get(socket));
} else if (key.isWritable()) {// 如果是通道写准备好事件
System.out.println("writeable");
// 取得套接字后处理,方法同上
Socket socket = (Socket) key.attachment();
SocketChannel channel = (SocketChannel) socket.getChannel();
this.writeToChannel(channel, "This is from server!");
}
}
}
}
// 对通道的写操作
public void writeToChannel(SocketChannel channel, String message)
throws IOException {
ByteBuffer buf = ByteBuffer.wrap(message.getBytes());
int nbytes = channel.write(buf);
}
// 对通道的读操作
public void readFromChannel(SocketChannel channel,
ClientChInstance clientInstance) throws IOException,
InterruptedException {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFERSIZE);
int nbytes = channel.read(byteBuffer);
byteBuffer.flip();
String result = this.decode(byteBuffer);
// 当客户端发出”@exit”退出命令时,关闭其通道
if (result.indexOf("@exit") >= 0) {
channel.close();
} else {
clientInstance.append(result.toString());
// 读入一行完毕,执行相应操作
if (result.indexOf("/n") >= 0) {
System.out.println("client input" + result);
clientInstance.execute();
}
}
}
// 该类封装了怎样对客户端的通道进行操作,具体实现可以通过重载execute()方法
public class ClientChInstance {
SocketChannel channel;
StringBuffer buffer = new StringBuffer();
public ClientChInstance(SocketChannel channel) {
this.channel = channel;
}
public void execute() throws IOException {
String message = "This is response after reading from channel!";
writeToChannel(this.channel, message);
buffer = new StringBuffer();
}
// 当一行没有结束时,将当前字窜置于缓冲尾
public void append(String values) {
buffer.append(values);
}
}
// 主程序
public static void main(String[] args) {
NBlockingServer nbServer = new NBlockingServer(8000);
try {
nbServer.initialize();
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
try {
nbServer.portListening();
} catch (Exception e) {
e.printStackTrace();
}
}
}
小结:
从 以上程序段可以看出,服务器端没有引入多余线程就完成了多客户的客户/服务器模式。该程序中使用了回调模式(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数据,而不用指名地址了。