JAVA Socket编程学习7--NIO同时接收TCP和UDP数据

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小强签名设计

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值