Java NIO Socket 多线程

Java NIO Socket 多线程

java NIO SocketChannel,非阻塞多线程模式应用。

IO模型介绍

1. 阻塞IO
如果数据没有准备就绪,就一直等待,直到数据准备就绪;整个进程会被阻塞。
2. 非阻塞IO
需不断询问内核是否已经准备好数据,非阻塞虽然不用等待但是一直占用CPU。
3. 多路复用 IO NIO
多路复用IO,会有一个线程不断地去轮询多个socket的状态,当socket有读写事件的时候才会调用IO读写操作。
用一个线程管理多个socket,是通过selector.select()查询每个通道是否有事件到达,如果没有事件到达,则会一直阻塞在那里,因此也会带来线程阻塞问题。
4. 信号驱动IO模型
在信号驱动IO模型中,当用户发起一个IO请求操作时,会给对应的socket注册一个信号函数,线程会继续执行,当数据准备就绪的时候会给线程发送一个信号,线程接受到信号时,会在信号函数中进行IO操作。
非阻塞IO、多路复用IO、信号驱动IO都不会造成IO操作的第一步,查看数据是否准备就绪而带来的线程阻塞,但是在第二步,对数据进行拷贝都会使线程阻塞。
5. 异步IO jdk7AIO
异步IO是最理想的IO模型,当线程发出一个IO请求操作时,接着就去做自己的事情了,内核去查看数据是否准备就绪和准备就绪后对数据的拷贝,拷贝完以后内核会给线程发送一个通知说整个IO操作已经完成了,数据可以直接使用了。
同步的IO操作在第二个阶段,对数据的拷贝阶段,都会造成线程的阻塞,异步IO则不会。

异步IO在IO操作的两个阶段,都不会使线程阻塞。

Java NIO的工作原理

  1. 由一个专门的线程(Selector)来处理所有的IO事件,并负责分发。
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
  3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

NIO 三大基本组件

Channel

FileChannel, 从文件中读写数据。
DatagramChannel,通过UDP读写网络中的数据。
SocketChannel,通过TCP读写网络中的数据。
ServerSocketChannel,可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。

Java NIO 的通道类似流,但又有些不同

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。 通道中的数据总是要先读到一个
  • Buffer,或者总是要从一个 Buffer 中写入。 直接输

Buffer

关键的Buffer实现 ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer
Buffer两种模式、三个属性:

capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
监听四种事件

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

select()方法
select()阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。

selectedKeys()方法
调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。 # 如何改变文本的样式

NIO 代码实现

服务端

import java.io.ByteArrayOutputStream;
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;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 目前常见的Socket服务器模型主要有三种:阻塞服务器,并发服务器以及异步服务器。三种形式各有利弊。</br>
 * </br>
 * 开发异步服务器,需要使用Java的NIO才可以,服务使用了ServerSocketChannel以及SocketChannel,而不再是之前ServerSocket以及Socket,异步服务的好处在于,服务没有工作可做的时候,会等在select调用上,不会占用系统资源,而当不同的条件满足时,又可以第一时间被唤醒,执行相应的操作;</br>
 * </br>
 * 所以,无论从资源的利用上,还是从响应的及时性上都优于前两种。 </br>
 * 另外,如果write和read的时间比较长,处理也可以放到线程中处理,这样就结合了并发服务器的优势。 </br>
 * 
 * @author bxji
 *
 */
public class ToUpperTCPNonBlockServer {
    private static Logger logger = LoggerFactory.getLogger(ToUpperTCPNonBlockServer.class);

    public static final Executor executor = Executors.newFixedThreadPool(10);
    // 服务器IP
    final String SERVER_IP = "127.0.0.1";

    // 服务器端口号
    public static final int SERVER_PORT = 10005;

    // 请求终结字符串
    public static final char REQUEST_END_CHAR = '#';

    public void startServer(String serverIP, int serverPort) throws IOException, InterruptedException {

        // 1. 创建ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();

        // 2. 服务器绑定地址及端口
        serverChannel.bind(new InetSocketAddress(serverIP, serverPort));

        // 3. 设置为非阻塞
        serverChannel.configureBlocking(false);

        // 4. 创建通道选择器
        Selector selector = Selector.open();

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

        while (true) {
//            System.out.println("服务器端正在监听端口:" + serverPort);
            // 6.获取可用I/O通道,获得有多少可用的通道, 调用select,阻塞在这里,直到有注册的channel满足条件
            if (selector.select() <= 0) {
                continue; // 不存在可用通道,略过
            }

            // 拿到符合条件的迭代器 keys
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) { // 迭代遍历当前I/O通道
                SelectionKey key = keys.next();
                // 调用iterator的remove()方法,并不是移除当前I/O通道,标识当前I/O通道已经处理。
                keys.remove();

                try {
                    // 判断事件类型,做对应的处理
                    if (key.isAcceptable()) {
                        System.out.println("11111111");
                        // 取得可以操作的channel, 调用accept完成三次握手,返回与客户端可以通信的channel,并设置为非阻塞
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel channel = server.accept();
                        channel.configureBlocking(false);
                        System.out.println("处理请求:" + channel.getRemoteAddress());
                        // 将channel注册到selector(通道选择器),为可读或可写
                        channel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        // 先改变感兴趣事件,防被重复处理
                        key.interestOps(SelectionKey.OP_CONNECT);
                        System.out.println("22222222222");

                        // 利用线程池,启动线程
                        executor.execute(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    // 有channel可读,取出可读的channel
                                    SocketChannel channel = (SocketChannel) key.channel();

                                    // 创建读取缓冲区,一次读取1024字节
                                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                    int len = 0;
                                    while (true) {
                                        buffer.clear();
                                        len = channel.read(buffer);
                                        if (len < 1)
                                            break;
                                        buffer.flip();// 切换为读就绪状态
                                        while (buffer.hasRemaining()) {
                                            baos.write(buffer.get());
                                        }
                                    }

                                    // ############# 业务处理开始 ############
                                    // 英文字符串转大写
                                    String recv = new String(baos.toByteArray()).toUpperCase();
                                    // ############# 业务处理 结束 ############
                                    
                                    // 业务处理结果返回将数据添加到key中
                                    baos.close();
                                    key.attach(recv);
                                    key.interestOps(SelectionKey.OP_WRITE);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        });
                    } else if (key.isWritable()) {
                        // 处理前,先取消对当前key事件监控
                        key.cancel();
                        System.out.println("3333333333");
						// 如果反回写非常耗时也可以添加多线程处理
                        // 有channel可写,取出可写的channel
                        SocketChannel channel = (SocketChannel) key.channel();

                        // 取出可读时设置的值
                        String recv = (String) key.attachment();

                        channel.write(ByteBuffer.wrap(recv.getBytes()));

                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    // 当客户端Socket关闭时,会走到这里,清理资源
                    key.cancel();
                    try {
                        key.channel().close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ToUpperTCPNonBlockServer server = new ToUpperTCPNonBlockServer();
        try {
            server.startServer(SERVER_IP, SERVER_PORT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

普通客户端


public class ToUpperTCPClient {

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;


    // 客户端使用的TCP Socket
    private Socket clientSocket;

    public String toUpperRemote(String serverIp, int serverPort, String str) {
        StringBuilder recvStrBuilder = new StringBuilder();
        try {
            // 创建连接服务器的Socket
            clientSocket = new Socket(serverIp, serverPort);

            // 写出请求字符串
            OutputStream out = clientSocket.getOutputStream();
            out.write(str.getBytes());

            // 读取服务器响应
            InputStream in = clientSocket.getInputStream();
            for (int c = in.read(); c != '#'; c = in.read()) {
                recvStrBuilder.append((char) c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (clientSocket != null) {
                    clientSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return recvStrBuilder.toString();
    }

    public static void main(String[] args) {
        ToUpperTCPClient client = new ToUpperTCPClient();
        String recvStr = client.toUpperRemote(ToUpperTCPNonBlockServer.SERVER_IP, ToUpperTCPNonBlockServer.SERVER_PORT,
                "aaaAAAbbbBBBcccCCC" + ToUpperTCPNonBlockServer.REQUEST_END_CHAR);
        System.out.println("收到:" + recvStr);
    }```
### NIO 客户端

```java
public class NIOClientSocket {
 
    public static void main(String[] args) throws IOException {
        //使用线程模拟用户 并发访问
        for (int i = 0; i < 1; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1.创建SocketChannel
                        SocketChannel socketChannel=SocketChannel.open();
                        //2.连接服务器
                        socketChannel.connect(new InetSocketAddress("localhost",60000));
                        //写数据
                        String msg="我是客户端NNNG"+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();
        }
    }
}

多线程NIO 注意事项

  1. 示例代码仅供学习参考。对于一个已经被监听到的事件,处理前先取消事件(SelectionKey
    .cancel())监控。否则selector.selectedKeys()会一直获取到该事件,但该方法比较粗暴,并且后续register会产生多个SelectionKey。推荐使用selectionKey.interestOps()改变感兴趣事件。
  2. Selector.select()和Channel.register()需同步。
  3. 当Channel设置为非阻塞(Channel.configureBlocking(false))时,SocketChannel.read
    没读到数据也会返回,返回参数等于0。
  4. OP_WRITE事件,写缓冲区在绝大部分时候都是有空闲空间的,所以如果你注册了写事件,这会使得写事件一直处于就就绪,选择处理现场就会一直占用着CPU资源。参考下面的第二个链接。

参考链接:Java 多线程NIO https://blog.csdn.net/zxcc1314/article/details/80918665

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值