TCP-IP学习笔记四:NIO的网络编程-多线程实例

TCP/IP学习笔记四:NIO的网络编程-多线程实例

标签(空格分隔): 网络编程 NIO 多线程


NIO的多线程编程

    对于单线程的程序来说,我们无法达到并行处理,我们要向达到并行处理,必定会使用多线程,但是我们哪些代码使用子线程呢?我们可以对单线程程序进行分析,在程序中最耗时的操作就是I/O操作(读和写)。找到入口就进行改造程序。

服务器端修改代码:

package com.socket.nio3;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * NIO 服务器端
 * 
 * @author MOTUI
 * 
 */
public class NIOServerSocket {

    private static Selector selector = null;

    public static void main(String[] args) throws IOException {

        // 1.创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2.绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8989));
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4.创建通道选择器
        selector = Selector.open();

        /*
         * 5.注册事件类型
         * 
         *  sel:通道选择器
         *  ops:事件类型 ==>SelectionKey:包装类,包含事件类型和通道本身。四个常量类型表示四种事件类型
         *  SelectionKey.OP_ACCEPT 获取报文      SelectionKey.OP_CONNECT 连接
         *  SelectionKey.OP_READ 读           SelectionKey.OP_WRITE 写
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            System.out.println("服务器端:正在监听8989端口");
            // 6.获取可用I/O通道,获得有多少可用的通道
            int num = selector.select();
            if (num > 0) { // 判断是否存在可用的通道
                // 获得所有的keys
                Set selectedKeys = selector.selectedKeys();
                // 使用iterator遍历所有的keys
                Iterator iterator = selectedKeys.iterator();
                // 迭代遍历当前I/O通道
                while (iterator.hasNext()) {
                    // 获得当前key
                    SelectionKey key = iterator.next();
                    // 调用iterator的remove()方法,并不是移除当前I/O通道,标识当前I/O通道已经处理。
                    iterator.remove();
                    // 判断事件类型,做对应的处理
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssChannel.accept();

                        System.out.println("处理请求:"+ socketChannel.getRemoteAddress());

                        // 获取客户端的数据
                        // 设置非阻塞状态
                        socketChannel.configureBlocking(false);
                        // 注册到selector(通道选择器)
                        socketChannel.register(selector, SelectionKey.OP_READ);

                    } else if (key.isReadable()) {

                        //调用读操作工具类
                        RequestProcessor.ProcessorRequest(key,selector);

                    } else if (key.isWritable()) {

                        //调用写操作工具类
                        ResponeProcessor.ProcessorRespone(key);
                    }
                }
            }
        }
    }
}

RequestProcessor:

package com.socket.nio3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 读操作的工具类
 * @author MOTUI
 *
 */
public class RequestProcessor {
    //构造线程池
    private static ExecutorService  executorService  = Executors.newFixedThreadPool(10);

    public static void ProcessorRequest(final SelectionKey key,final Selector selector){
        //获得线程并执行
        executorService.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println("开始读");
                    SocketChannel readChannel = (SocketChannel) key.channel();
                    // I/O读数据操作
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int len = 0;
                    while (true) {
                        buffer.clear();
                        len = readChannel.read(buffer);
                        if (len == -1) break;
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            baos.write(buffer.get());
                        }
                    }
                    System.out.println("服务器端接收到的数据:"+ new String(baos.toByteArray()));

                    System.out.println("开始注册------");
                    //注册写操作
                    readChannel.register(selector, SelectionKey.OP_WRITE);
                    System.out.println("注册完成-------");

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

    }
}

ResponeProcessor:

package com.socket.nio3;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 写操作工具类
 * @author MOTUI
 *
 */
public class ResponeProcessor {
    //构造线程池
    private static ExecutorService  executorService  = Executors.newFixedThreadPool(10);

    public static void ProcessorRespone(final SelectionKey key) {
        //获得线程并执行
        executorService.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println("开始写");
                    // 写操作
                    SocketChannel writeChannel = (SocketChannel) key.channel();
                    //拿到客户端传递的数据
                    //ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();

                    //System.out.println("客户端发送来的数据:"+new String(attachment.toByteArray()));

                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    String message = "你好,我好,大家好!!";
                    buffer.put(message.getBytes());
                    buffer.flip();
                    writeChannel.write(buffer);
                    writeChannel.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }       
            }
        });
    }
}
    两个工具类分别处理读操作和写操作,分别加入了线程

客户端(未做修改):

package com.socket.nio3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 *  NIO 客户端
 */
public class NIOClientSocket {

    public static void main(String[] args) throws IOException {
        //1.创建SocketChannel
        SocketChannel socketChannel=SocketChannel.open();
        //2.连接服务器
        socketChannel.connect(new InetSocketAddress("192.168.0.117",8989));

        //写数据
        String msg="我是客户端~";
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        buffer.put(msg.getBytes());
        buffer.flip();
        socketChannel.write(buffer);
        socketChannel.shutdownOutput();

        //读数据
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int len = 0;
        while (true) {
            buffer.clear();
            len = socketChannel.read(buffer);
            if (len == -1)
                break;
            buffer.flip();
            while (buffer.hasRemaining()) {
                bos.write(buffer.get());
            }
        }

        System.out.println("客户端收到:"+new String(bos.toByteArray()));

        socketChannel.close();

    }

执行结束:

    服务器端:正在监听8989端口
    处理请求:/192.168.0.117:60194
    服务器端:正在监听8989端口
    服务器端:正在监听8989端口
    服务器端:正在监听8989端口
    开始读
    开始读
    ...
    开始写
    开始写
    开始写
    开始写

会出现一个严重的问题:在主线程中,读操作会执行多次。因为在子线程没有执行结束,主线程认为读操作没有结束(在通道选择器中读操作的管道一直存在),会一直开启新的线程去执行读操作,这样就会产生严重的错误。产生这种问题的原因是我们将读操作交给子线程去处理,主线程依然对此通道保留监控,所以会一直执行读操作,我们取消主线程对此通道的监控。将服务器端代码稍作修改(添加:key.cancel();):

else if (key.isReadable()) {
    //取消读事件的监控
    key.cancel();
    //调用读操作工具类
    RequestProcessor.ProcessorRequest(key,selector);
}

执行结果为:

    服务器端:正在监听8989端口
    处理请求:/192.168.0.117:60378
    服务器端:正在监听8989端口
    服务器端:正在监听8989端口
    开始读
    服务器端接收到的数据:我是客户端~
    开始注册------

取消了主线程对通道的监控,但是出现了注册写操作无法执行(无法注册成功)。出现这种问题主要是由于selector.select()和注册操作需要同步。这种方式不能达到目的。换一种解决方式,我们在主线程控制通道的注册会怎样呢?

服务器端:

package com.socket.nio3;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * NIO 服务器端
 * 
 * @author MOTUI
 * 
 */
public class NIOServerSocket {

    //存储SelectionKey的队列
    private static List writeQueen = new ArrayList();
    private static Selector selector = null;

    //添加SelectionKey到队列
    public static void addWriteQueen(SelectionKey key){
        synchronized (writeQueen) {
            writeQueen.add(key);
            //唤醒主线程
            selector.wakeup();
        }
    }

    public static void main(String[] args) throws IOException {

        // 1.创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2.绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8989));
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4.创建通道选择器
        selector = Selector.open();

        /*
         * 5.注册事件类型
         * 
         *  sel:通道选择器
         *  ops:事件类型 ==>SelectionKey:包装类,包含事件类型和通道本身。四个常量类型表示四种事件类型
         *  SelectionKey.OP_ACCEPT 获取报文      SelectionKey.OP_CONNECT 连接
         *  SelectionKey.OP_READ 读           SelectionKey.OP_WRITE 写
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            System.out.println("服务器端:正在监听8989端口");
            // 6.获取可用I/O通道,获得有多少可用的通道
            int num = selector.select();
            if (num > 0) { // 判断是否存在可用的通道
                // 获得所有的keys
                Set selectedKeys = selector.selectedKeys();
                // 使用iterator遍历所有的keys
                Iterator iterator = selectedKeys.iterator();
                // 迭代遍历当前I/O通道
                while (iterator.hasNext()) {
                    // 获得当前key
                    SelectionKey key = iterator.next();
                    // 调用iterator的remove()方法,并不是移除当前I/O通道,标识当前I/O通道已经处理。
                    iterator.remove();
                    // 判断事件类型,做对应的处理
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssChannel.accept();

                        System.out.println("处理请求:"+ socketChannel.getRemoteAddress());

                        // 获取客户端的数据
                        // 设置非阻塞状态
                        socketChannel.configureBlocking(false);
                        // 注册到selector(通道选择器)
                        socketChannel.register(selector, SelectionKey.OP_READ);

                    } else if (key.isReadable()) {
                        //取消读事件的监控
                        key.cancel();
                        //调用读操作工具类
                        RequestProcessor.ProcessorRequest(key);
                    } else if (key.isWritable()) {
                        //取消读事件的监控
                        key.cancel();
                        //调用写操作工具类
                        ResponeProcessor.ProcessorRespone(key);
                    }
                }
            }else{
                synchronized (writeQueen) {
                    while(writeQueen.size() > 0){
                        SelectionKey key = writeQueen.remove(0);
                        //注册写事件
                        SocketChannel channel = (SocketChannel) key.channel();
                        Object attachment = key.attachment();
                        channel.register(selector, SelectionKey.OP_WRITE,attachment);
                    }
                }
            }
        }
    }
}

RequestProcessor类:

package com.socket.nio3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 读操作的工具类
 * @author MOTUI
 *
 */
public class RequestProcessor {

    //构造线程池
    private static ExecutorService  executorService  = Executors.newFixedThreadPool(10);

    public static void ProcessorRequest(final SelectionKey key){
        //获得线程并执行
        executorService.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    SocketChannel readChannel = (SocketChannel) key.channel();
                    // I/O读数据操作
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int len = 0;
                    while (true) {
                        buffer.clear();
                        len = readChannel.read(buffer);
                        if (len == -1) break;
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            baos.write(buffer.get());
                        }
                    }
                    System.out.println("服务器端接收到的数据:"+ new String(baos.toByteArray()));

                    //将数据添加到key中
                    key.attach(baos);
                    //将注册写操作添加到队列中
                    NIOServerSocket.addWriteQueen(key);

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

ResponeProcessor类:

package com.socket.nio3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 写操作工具类
 * @author MOTUI
 *
 */
public class ResponeProcessor {
    //构造线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void ProcessorRespone(final SelectionKey key) {
        //拿到线程并执行
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 写操作
                    SocketChannel writeChannel = (SocketChannel) key.channel();
                    //拿到客户端传递的数据
                    ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();

                    System.out.println("客户端发送来的数据:"+new String(attachment.toByteArray()));

                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    String message = "你好,我好,大家好!!";
                    buffer.put(message.getBytes());
                    buffer.flip();
                    writeChannel.write(buffer);
                    writeChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }               
            }
        });
    }
}

客户端代码:

package com.socket.nio3;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 *  NIO 客户端
 */
public class NIOClientSocket {

    public static void main(String[] args) throws IOException {
        //使用线程模拟用户 并发访问
        for (int i = 0; i < 2; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1.创建SocketChannel
                        SocketChannel socketChannel=SocketChannel.open();
                        //2.连接服务器
                        socketChannel.connect(new InetSocketAddress("192.168.0.117",8989));

                        //写数据
                        String msg="我是客户端"+Thread.currentThread().getId();
                        ByteBuffer buffer=ByteBuffer.allocate(1024);
                        buffer.put(msg.getBytes());
                        buffer.flip();
                        socketChannel.write(buffer);
                        socketChannel.shutdownOutput();

                        //读数据
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        int len = 0;
                        while (true) {
                            buffer.clear();
                            len = socketChannel.read(buffer);
                            if (len == -1)
                                break;
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                bos.write(buffer.get());
                            }
                        }

                        System.out.println("客户端收到:"+new String(bos.toByteArray()));

                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }
    }
}

服务器执行结果:

    服务器端:正在监听8989端口
    处理请求:/192.168.0.117:60792
    服务器端:正在监听8989端口
    处理请求:/192.168.0.117:60791
    服务器端:正在监听8989端口
    服务器端:正在监听8989端口
    服务器端接收到的数据:我是客户端10
    服务器端接收到的数据:我是客户端9
    服务器端:正在监听8989端口
    服务器端:正在监听8989端口
    客户端发送来的数据:我是客户端10
    客户端发送来的数据:我是客户端9

客户端执行结果:

    客户端收到:你好,我好,大家好!!
    客户端收到:你好,我好,大家好!!

总结:

    对于NIO的多线程网络编程:NIO也是使用的阻塞I/O,使用了通道选择器进行选择就绪的通道进行处理I/O,达到非阻塞的目的。
    编程思路:
        1.创建ServerSocketChannel
        2.绑定端口
        3.设置为非阻塞
        4.创建通道选择器
        5.注册事件类型
        6.获取可用I/O通道,获得有多少可用的通道
        7.将耗时的读写操作开启子线程去处理
        8.将注册写操作交给主线程处理
    重点是7、8两步,如何将注册操作交给主线程处理。

    执行流程:
        1:启动TestServiceSocketChannel(服务器),加载服务器端中的代码;进入到第一个while(true){
        停留到int num = selector.select();查询是否有可用通道
    }
        2:启动TestClientSocketChannel(客户端),加载客户端中的代码;
        3:这时客户端有可用的通道,开始向下执行进入下个while(true)循环,然后将这个iterator.remove();标记移除,以为这个请已经被处理,(防止高并发时进行重复处理),
        4:判断当前时间类型并进行处理
            a:key.isAcceptable(),key.channeal拿到一个通道,并用通道接收报文请求,然后进行通道设置为非阻塞的,然后再将读操作注入到通道里。执行完毕后,此时跳出当前while(iterator.hasNext()因为此时测试的环境是一个线程,如果是多个线程则进行循环处理)
            b:因为在步骤1中已经将读取所以进行判断后会进入else if(key.isReadable()){这里边,进入后会将key.cancel();(会将这个key做上标记)调用RequestProcess.request(key);这个类中的方法进行读操作。此时,服务器端的主线程会阻塞。因为此时已经没有要处理的通道或者说是key的时间
            c:进入读操作后,执行代码并将从客户端接受到数据,防止到key的附件中key.attach(bos);然后调用服务器端的TestServiceSocketChannel.addWriteQueen(key);方法,将可以存入WriteQueen集合中,存入后会将已经 阻塞的主线程 进行唤醒(selector.wakeup();) 
            d:唤醒主线程后,以为此时已经有了所对象,会进行注册写入事件和从客户端读取到的数据 放入通道,进行写入操作.进行判断if(key.isWritable())取消key的写标记,调用写方法ResponseProcess.response(key);将写入数据传到客户端,释放通道,完成响应。

你可能感兴趣的:(TCP-IP)