NIOTCP客户端代码:
package NIOtcpudp3;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOTCPClientNonblocking {
public static void main(String args[]) throws Exception{
if ((args.length < 2) || (args.length > 3))
throw new IllegalArgumentException("参数不正确");
//第一个参数作为要连接的服务端的主机名或IP
String server = args[0];
//第二个参数为要发送到服务端的字符串
byte[] argument = args[1].getBytes();
//如果有第三个参数,则作为端口号,如果没有,则端口号设为7
int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
//创建一个信道,并设为非阻塞模式
SocketChannel clntChan = SocketChannel.open();
clntChan.configureBlocking(false);
//向服务端发起连接
if (!clntChan.connect(new InetSocketAddress(server, servPort))){
//不断地轮询连接状态,直到完成连接
while (!clntChan.finishConnect()){
//在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
//这里为了演示该方法的使用,只是一直打印"-",这里根本就打印不出来,不知道原博主是什么个意思
System.out.print("-");
}
}
//为了与后面打印的"."区别开来,这里输出换行符
System.out.print("\n");
//分别实例化用来读写的缓冲区
ByteBuffer writeBuf = ByteBuffer.wrap(argument);
ByteBuffer readBuf = ByteBuffer.allocate(argument.length);
//接收到的总的字节数
int totalBytesRcvd = 0;
//每一次调用read()方法接收到的字节数
int bytesRcvd;
//循环执行,直到接收到的字节数与发送的字符串的字节数相等
while (totalBytesRcvd < argument.length){
//如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
if (writeBuf.hasRemaining()){
clntChan.write(writeBuf);
}
//如果read()接收到-1,表明服务端关闭,抛出异常
if ((bytesRcvd = clntChan.read(readBuf)) == -1){
throw new SocketException("Connection closed prematurely");
}
//计算接收到的总字节数
totalBytesRcvd += bytesRcvd;
//在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性
//这里为了演示该方法的使用,同样只是一直打印"."
System.out.print(".");
}
//打印出接收到的数据
System.out.println("Received: " + new String(readBuf.array(), 0, totalBytesRcvd));
//关闭信道
clntChan.close();
}
}
NIOUDP客户端代码:
package NIOtcpudp3;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class NIOUDPClientNonblocking {
private static final int TIMEOUT = 3000; // Resend timeout (milliseconds)
private static final int MAXTRIES = 1024; // Maximum retransmissions
public static void main(String args[]) throws Exception {
byte[] bytesToSend = "Hello world!".getBytes();
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.socket().setSoTimeout(TIMEOUT);
datagramChannel = datagramChannel.connect(new InetSocketAddress("localhost", 7777));
/**若想改成客户端NIO发送TCP数据只需将整个代码中的DatagramChannel改为SocketChannel即可,并且将上一行的代码改成下面这样的
socketChannel.connect(new InetSocketAddress("127.0.0.1", 7788));
socketChannel.finishConnect();
*/
int totalBytesRcvd = 0; // Total bytes received so far
int bytesRcvd; // Bytes received in last read
ByteBuffer writeBuf = ByteBuffer.wrap(bytesToSend);
ByteBuffer readBuf = ByteBuffer.allocate(MAXTRIES);
while (totalBytesRcvd < bytesToSend.length) {
if (writeBuf.hasRemaining()) {
datagramChannel.write(writeBuf);
//当传输数据为UDP时,上面的这行代码还可以这样发送数据包,而TCP的SocketChannel则只有write方法
// int bytesSent = datagramChannel.send(writeBuf, new InetSocketAddress("localhost", 7777));
}
if ((bytesRcvd = datagramChannel.read(readBuf)) == -1) {
throw new SocketException("Connection closed prematurely");
}
totalBytesRcvd += bytesRcvd;
System.out.print("."); // Do something else
}
System.out.println("Received: " + new String(readBuf.array(), 0, totalBytesRcvd));
datagramChannel.close();
/**上面是用了两个ByteBuffer,一个读一个写,也可以用一个ByteBuffer,服务端也可以这样写
String newData = "Hello world!";
ByteBuffer buf=ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while (totalBytesRcvd < bytesToSend.length) {
if (buf.hasRemaining()) {
datagramChannel.write(buf);
}
buf.flip();
if ((bytesRcvd = datagramChannel.read(buf)) == -1) {
throw new SocketException("Connection closed prematurely");
}
totalBytesRcvd += bytesRcvd;
System.out.print(".");
}
System.out.println("Received: " + new String(buf.array(), 0, totalBytesRcvd));
datagramChannel.close();
*/
}
}
NIO服务端代码:
ServerSelector.java(主程序):
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
public class ServerSelector {
private static final int BUFSIZE = 1024;
// private static final int TIMEOUT1 = 3000;
private static final long TIMEOUT = 1;
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
Selector selector1 = Selector.open();
ServerSocketChannel listnChannel = ServerSocketChannel.open();
listnChannel.socket().bind(new InetSocketAddress(7788));
listnChannel.configureBlocking(false);
listnChannel.register(selector, SelectionKey.OP_ACCEPT);
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(7777));
channel.register(selector1, SelectionKey.OP_READ, new UDPSelectorProtocol.ClientRecord());
TCPSelectorProtocol protocol = new TCPSelectorProtocol(BUFSIZE);
UDPSelectorProtocol protocol1 = new UDPSelectorProtocol();
while (true){
//一直等待,直至有信道准备好了I/O操作。感觉使用selectNow()==0更好,因为他是非阻塞的,但实际效果却是使用TIMEOUT = 1更好,因为我在使用selectNow()==0时我的笔记本电脑风扇呜呜的转,说明它很消耗资源。而TIMEOUT = 1时风扇则没转(可能我这个判断太奇葩了。。。)
if (selector.select(TIMEOUT)==0 && selector1.select(TIMEOUT)==0){
// if(selector.selectNow()==0 && selector1.selectNow()==0){
//在等待信道准备的同时,也可以异步地执行其他任务,这里只是简单地打印"."
// System.out.print(".");
continue;
}
//获取准备好的信道所关联的Key集合的iterator实例
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
Iterator<SelectionKey> keyIter1 = selector1.selectedKeys().iterator();
//循环取得集合中的每个键值
while (keyIter.hasNext()){
SelectionKey key = keyIter.next();
//如果服务端信道感兴趣的I/O操作为accept
if (key.isAcceptable()){
protocol.handleAccept(key);
}
//如果客户端信道感兴趣的I/O操作为read
if (key.isReadable()){
protocol.handleRead(key);
}
//如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write
if (key.isWritable()) {
protocol.handleWrite(key);
}
//这里需要手动从键集中移除当前的key
keyIter.remove();
}
while (keyIter1.hasNext()) {
SelectionKey key1 = keyIter1.next();
if (key1.isReadable())
protocol1.handleRead1(key1);
if (key1.isValid() && key1.isWritable())
protocol1.handleWrite1(key1);
keyIter1.remove();
}
}
}
}
注意1:
若客户端强制关闭,服务器会报“java.io.IOException: 远程主机强迫关闭了一个现有的连接。”,并且服务器会在报错后停止运行,错误的意思就是客户端关闭了,但是服务器还在从这个套接字通道读取数据,便抛出IOException,导致这种情况出现的原因就是,客户端异常关闭后,服务器的选择器会获取到与客户端套接字对应的套接字通道SelectionKey,并且这个key的兴趣是OP_READ,执行从这个通道读取数据时,客户端已套接字已关闭,所以会出现“java.io.IOException: 远程主机强迫关闭了一个现有的连接”的错误。解决这种问题也很简单,就是服务器在读取数据时,若发生异常,则取消当前key并关闭通道,如下代码:
try{
protocol.handleRead(key);
}catch(Exception e){
key.cancel();
listnChannel.socket().close();
listnChannel.close();
}
本来上面的代码是参考http://blog.csdn.net/abc_key/article/details/29295569这篇文章的,可是引用到我这个案例中还需要改进一下,否则虽然当客户端强制关闭后不会报原来的那个错误,但是再连一个正常的TCP客户端又报Connection refused: connect的错误,所以需要改进一下,改进后的代码:
TCP:
try{
protocol.handleRead(key);
}catch(Exception e){
key.cancel();
}
UDP:
try{
protocol1.handleRead1(key1);
}catch(Exception e){
key1.cancel();
}
后来经测试,在写的那块也必须得加异常处理,否则客户端异常退出会报Exception in thread "main" java.io.IOException: Connection reset by peer,修改如下:
TCP:
try{
protocol.handleWrite(key);
}catch(Exception e){
key.cancel();
}
UDP:
try{
protocol1.handleWrite1(key1);
}catch(Exception e){
key1.cancel();
}
合在一起:
TCP:
try{
if (key.isReadable()){
protocol.handleRead(key);
}
if (key.isValid() && key.isWritable()) {
protocol.handleWrite(key);
}
}catch(Exception e){
key.cancel();
}
UDP:
try{
if (key1.isReadable())
protocol1.handleRead1(key1);
if (key1.isValid() && key1.isWritable())
protocol1.handleWrite1(key1);
}catch(Exception e){
key1.cancel();
}
注意2:
因在catch中取消了key,readMsg返回后,run方法继续往下走,之前的代码会报“java.nio.channels.CancelledKeyException”错误,所以需要判断当前key是否有效,之前的代码:
if (key.isAcceptable()){
......
}
if (key.isReadable()){
......
}
if (key.isWritable()) {
......
}
修复后的代码:
if (key.isValid() && key.isAcceptable()){
......
}
if (key.isValid() && key.isReadable()){
......
}
if (key.isValid() && key.isWritable()) {
......
}
经试验没必要这三个判断都加key.isValid(),只需在第三个writeable上加就可以达到同样的效果
TCPProtocol.java:
import java.io.IOException;
import java.nio.channels.SelectionKey;
public interface TCPProtocol{
//accept I/O形式
void handleAccept(SelectionKey key) throws IOException;
//read I/O形式
void handleRead(SelectionKey key) throws IOException;
//write I/O形式
void handleWrite(SelectionKey key) throws IOException;
}
TCPSelectorProtocol.java:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class TCPSelectorProtocol implements TCPProtocol {
private static int bufSize; // 缓冲区的长度
public TCPSelectorProtocol(int bufSize){
this.bufSize = bufSize;
}
//服务端信道已经准备好了接收新的客户端连接
public void handleAccept(SelectionKey key) throws IOException {
SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
clntChan.configureBlocking(false);
//将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ,同时为该信道指定关联的附件
clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
}
public void handleRead(SelectionKey key) throws IOException {
SocketChannel clntChan = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = clntChan.read(buf);
//将ByteBuffer转换为String
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buf.asReadOnlyBuffer());
// System.out.println(charBuffer.toString());
if (bytesRead == -1){
clntChan.close();
}else if(bytesRead > 0){
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
public void handleWrite(SelectionKey key) throws IOException {
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.flip();
SocketChannel clntChan = (SocketChannel) key.channel();
clntChan.write(buf);
if (!buf.hasRemaining()){
key.interestOps(SelectionKey.OP_READ);
}
buf.compact();
}
}
UDPProtocol.java:
import java.io.IOException;
import java.nio.channels.SelectionKey;
public interface UDPProtocol{
//accept I/O形式
void handleAccept1(SelectionKey key) throws IOException;
//read I/O形式
void handleRead1(SelectionKey key) throws IOException;
//write I/O形式
void handleWrite1(SelectionKey key) throws IOException;
}
UDPSelectorProtocol.java:
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
public class UDPSelectorProtocol implements UDPProtocol {
private static int bufSize = 1024; // 缓冲区的长度
static class ClientRecord {
public SocketAddress clientAddress;
public ByteBuffer buffer = ByteBuffer.allocate(bufSize);
}
public void handleAccept1(SelectionKey key) throws IOException {
}
public void handleRead1(SelectionKey key) throws IOException {
DatagramChannel channel = (DatagramChannel) key.channel();
ClientRecord clntRec = (ClientRecord) key.attachment();
clntRec.buffer.clear();
clntRec.clientAddress = channel.receive(clntRec.buffer);
if (clntRec.clientAddress != null) {
key.interestOps(SelectionKey.OP_WRITE);
}
}
public void handleWrite1(SelectionKey key) throws IOException {
DatagramChannel channel = (DatagramChannel) key.channel();
ClientRecord clntRec = (ClientRecord) key.attachment();
clntRec.buffer.flip();
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.defaultCharset();
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(clntRec.buffer.asReadOnlyBuffer());
String messages = charBuffer.toString().replaceAll("\r|\n", "");
// System.out.println(messages);
int bytesSent = channel.send(clntRec.buffer, clntRec.clientAddress);
if (bytesSent != 0) {
key.interestOps(SelectionKey.OP_READ);
}
}
}
注意3:
后来在实际中如果UDP通道发包含二进制数据(比如发的json中包含图片,图片就是二进制形式,如{\"id\": 0, \"data\": { \"stamp\": 1511779291, \"hlen\": 42, \"hctx\": \"(Q2\\u00053?\\u0000#$???\\b\\u0000E\\u0000\\u0001\\u0001?@\\u0000@\\u0011'???f\\u001d??f\\u001f?<\\u001ea\\u0000??*\" }})的时候服务端报错java.nio.charset.MalformedInputException: Input length = 1
解决:修改UDPSelectorProtocol.java中的代码为
try{
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.defaultCharset();
// CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = charset.decode(clntRec.buffer.asReadOnlyBuffer());
String messages = charBuffer.toString().replaceAll("\r|\n", "");
// System.out.println(clntRec.buffer);
// System.out.println(messages);
}catch(Exception e){
e.printStackTrace();
}
补充:像上面客户端发来的数据中包含有转义字符\,如果想去掉的话可以参考我的这篇文章http://blog.csdn.net/m0_37739193/article/details/78657155
将代码导入到myeclipse中:
运行结果如下:
参考:
http://blog.csdn.net/ns_code/article/details/15545057
http://kingxss.iteye.com/blog/2098818
http://blog.csdn.net/itbuluoge/article/details/39552397