Java粗浅认识-网络编程(二)

单线程模型

服务端绑定一个端口,然后接收请求,每次请求就处理,后续请求进来时,等待之前的任务处理完成,如果任务处理非常快,也是不会有明显阻塞的。

单线程模型服务端代码

展示文件上传后处理逻辑,在一个while(true)中阻塞等待accept,由于是演示网络通信,这里的文件I/O缓存直接使用的是一个byte[1<<14] = 16k的容量,在项目中可以写成循环使用的方式。

private static void server(int port) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(port));
        System.out.println("服务端启动成功,绑定端口[" + port +"]");
        //这里使用了一个单线程处理,后面讲多线程的时候,会改为多线程版本
        while (true) {
            System.out.println("服务端等待连接中。");
            Socket socket = serverSocket.accept();
            System.out.println("接收到一个客户端连接。");
            InputStream inputStream = socket.getInputStream();
            //16k
            File file = new File("C:\\Users\\baopz\\Desktop\\test\\socket\\server\\1.gif");
            if (!file.exists()) {
                if (!file.createNewFile()) {
                    System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
                    continue;
                }
            }
            OutputStream outputStream = null;
            //这里可以使用java 1.7的资源自动释放写法
            try {
                outputStream = new FileOutputStream(file);
                byte[] bytes = new byte[1 << 14];
                int length;
                while ((length = inputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, length);
                }
                System.out.println("文件接受成功。");
            } finally {
                if (outputStream != null) {
                    outputStream.close();
                }
            }
        }
    }

客户端测试代码

可以在一个for循环中调用,看看会不会等待之前的处理

private static void client() throws IOException {
        Socket socket = new Socket();
        OutputStream outputStream = null;
        try {
            System.out.println("客户端请求连接。");
            socket.connect(new InetSocketAddress("127.0.0.1", 8888));
            outputStream = socket.getOutputStream();
            File file = new File("C:\\Users\\baopz\\Desktop\\test\\socket\\client\\1.gif");
            if (!file.exists()) {
                System.out.println("需要传输的文件不存在。");
                return;
            }
            InputStream inputStream = new FileInputStream(file);
            //16k
            byte[] bytes = new byte[1 << 14];
            int length;
            while((length=inputStream.read(bytes))!=-1){
                outputStream.write(bytes,0,length);
            }
            outputStream.flush();
            inputStream.close();
            System.out.println("文件发送成功。");
        }finally {
            if(outputStream!=null){
                outputStream.close();
            }
            socket.close();
            System.out.println("客户端关闭连接。");
        }
    }

preThreading模式

在服务启动的时候,预先new出一个线程池,让每个连接都用一个线程处理。

基于线程池的服务端

需要注意一下,这里的ThreadFactoryBuilder是来自com.google.guava.guava

/**
     * 基于线程池的服务端
     * @param port
     * @throws IOException
     */
    private static void server(int port) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(port));
        System.out.println("服务器绑定端口[" + port + "]成功");

        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("任务处理线程.%d").build();
        //这里的参数一次含义是,核心线程数量=4,最大线程数量8,空闲等待时间10,空闲等待时间单位s,
        //阻塞队列,LinkedBlockingQueue无界队列,任务量大于了最大线程容量后,会放到这个等待队列当中
        //线程工厂,定义线程名称,方便定位
        //拒绝接受新任务策略,如果新来的任务大于了等待队列上限,那么就会调用这个拒绝策略,拒绝策略可以自己实现,也可以调用
        //系统默认的4中策略,这里用的是AbortPolicy,直接抛出异常,终止线程池
        //这里的线程池可以用自带的线程池工具new出来,如:ExecutorService executorService =  Executors.newCachedThreadPool();
        ExecutorService executorService = new ThreadPoolExecutor(4,
                8,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(16),
                threadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("接收到一个客户端连接,分发任务。");
            //这里文件名递增,每次一个线连接进来,就存放一个新文件
            executorService.execute(new Task(socket,"C:\\Users\\baopz\\Desktop\\test\\socket\\server\\"+atomicInteger.getAndAdd(1)+".gif"));
        }
    }

基于线程池运行的任务代码

接受一个socket和保存路径,进行处理

 /**
     * 服务端的执行任务
     */
    public static class Task implements Runnable {
        private Socket socket;
        private String filename;

        public Task(Socket socket, String filename) {
            this.socket = socket;
            this.filename = filename;
        }

        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在处理任务。");
            //16k
            File file = new File(filename);
            if (!file.exists()) {
                try {
                    if (!file.createNewFile()) {
                        System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
                        return;
                    }
                } catch (IOException e) {
                    System.out.println("创建文件" + file.getAbsolutePath() + "失败。");
                    e.printStackTrace();
                }
            }
            InputStream inputStream = null;
            OutputStream outputStream = null;
            try {
                inputStream = socket.getInputStream();
                outputStream = new FileOutputStream(file);
                byte[] bytes = new byte[1 << 14];
                int length;
                while ((length = inputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, length);
                }
                System.out.println(Thread.currentThread().getName()+"文件接受成功。");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (outputStream != null) {
                    try {
                        outputStream.close();
                        System.out.println(Thread.currentThread().getName()+"关闭连接。");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

测试客户端可以直接使用上面的客户端

Reactor模型服务端(一个Reactor,多个业务处理线程)

服务端分发程序,Reactor,注意,

1.这里在selectionKey.isReadable()里面,做了一个selectionKey.cancel();这个操作,主要是防止在起一个subChannel没有完成时,这个selectionKey依然有效,会重复提交任务的。

2.示例程序展示的是小文件的发送,很不稳定,如果文件大了,你会发现,服务端会接受不到完整的数据。后续在Netty里面会讲怎么处理这个问题,手工处理起来还是比较繁琐的。

   private static void server(int port) throws IOException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        //设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));
        System.out.println("绑定端口成功.");
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            //这里就是在底层做的epoll操作,等待数据就绪
            int completed = selector.select();
            if (completed == 0) {
                System.out.println("监听");
                continue;
            }
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //如果是连接事件,就在当前线程做连接操作,然后把连接得到的socketChannel注册到Selector中,等待下一个select
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverSocketChannel1.accept();
                    System.out.println("得到一个新连接。");
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                //如果是写事件,放到线程池里面处理
                else if (selectionKey.isReadable()) {
                    //
                    SocketChannel subSocketChannel = (SocketChannel) selectionKey.channel();
                    System.out.println("线程提交任务。");
                    
                    ReadTask readTask = new ReadTask(subSocketChannel, "C:\\Users\\baopz\\Desktop\\test\\socket\\server\\" + atomicInteger.getAndAdd(1) + ".gif");
                    executorService.execute(readTask);
                    selectionKey.cancel();
                }
                //如果是读事件
                else if (selectionKey.isWritable()) {
                    SocketChannel subSocketChannel = (SocketChannel) selectionKey.channel();
                    executorService.execute(new WriteTask(subSocketChannel));
                }
                iterator.remove();
            }
        }
    }

Handler处理逻辑

实现了一个读事件,没有实现写事件。

public static class WriteTask implements Runnable {
        private SocketChannel socketChannel;

        public WriteTask(SocketChannel socketChannel) {
            this.socketChannel = socketChannel;
        }

        @Override
        public void run() {
            System.out.println("执行一个读事件。");
            try {
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static class ReadTask implements Runnable {
        private SocketChannel socketChannel;
        private String filename;

        public ReadTask(SocketChannel socketChannel, String filename) {
            this.socketChannel = socketChannel;
            this.filename = filename;
        }

        @Override
        public void run() {
            //16k
            ByteBuffer buffer = ByteBuffer.allocateDirect(1 << 14);
            Path path = Paths.get(filename);
            Path parent = path.getParent();
            if (Files.notExists(parent)) {
                try {
                    Files.createDirectories(parent);
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("创建文件夹失败:" + parent);
                    return;
                }
            }
            if (Files.notExists(path)) {
                try {
                    Files.createFile(path);
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("文件失败:" + path);
                    return;
                }
            }

            FileChannel outFileChannel = null;
            RandomAccessFile randomAccessFile = null;
            try {
                randomAccessFile = new RandomAccessFile(path.toFile(), "rw");
                outFileChannel = randomAccessFile.getChannel();
                //这里buffer读取的时候,可能数据读不全,所以需要精细的设计,在Netty章节,我会详细讲一下它是怎么处理
                while (socketChannel.read(buffer) > 0) {
                    buffer.flip();
                    outFileChannel.write(buffer);
                    buffer.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                System.out.println("执行清理工作");
                if (outFileChannel != null) {
                    try {
                        outFileChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (randomAccessFile != null) {
                    try {
                        randomAccessFile.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (socketChannel != null) {
                    try {
                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

异步网络通信

服务端程序

打开一个AsynchronusServerSocketChannel,绑定到端口上,accept请求。

private static void server(int port) throws IOException, InterruptedException {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("服务处理程序%d").build();
        //线程池
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(4, threadFactory);
        AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(group);

        //这里的backlog可以参考网上资料,这里不详细讲,就是一个tcp/ip的一个参数,不想了解,可以忽略
        serverSocketChannel.bind(new InetSocketAddress(port), 512);
        System.out.println("服务端绑定端口[" + port + "]完成。");
        serverSocketChannel.accept(new Attachment(serverSocketChannel, atomicInteger), new CopyHandler());
    }

完成连接后的回调处理类

注意,这里在CopyHandler中的第一句,把AsynchronousServerSocketChannel.accept(),这里是当前这个serverchannel继续监听下一个请求。

可以用之前的client测试程序,for循环测试一下。

public static class CopyHandler implements CompletionHandler<AsynchronousSocketChannel, Attachment> {
        @Override
        public void completed(AsynchronousSocketChannel result, Attachment attachment) {
            //接受下一次请求
            attachment.getAsynchronousServerSocketChannel().accept(new Attachment(attachment.getAsynchronousServerSocketChannel(), attachment.atomicInteger), this);
            //处理业务逻辑
            try {
                handle(result, attachment.getAtomicInteger());
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void handle(AsynchronousSocketChannel result, AtomicInteger attachment) throws ExecutionException, InterruptedException {
            System.out.println("连接完成。");
            //16k
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1 << 14);
            //这里是阻塞是获取数据,因为需要把数据放到byteBuffer中,所有,需要获取到数据
            result.read(byteBuffer).get();
            Path target = Paths.get("C:\\Users\\baopz\\Desktop\\test\\socket\\server\\" + attachment.getAndAdd(1) + ".gif");
            if (Files.notExists(target)) {
                try {
                    Files.createFile(target);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            //
            try {
                AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(target, StandardOpenOption.WRITE);
                byteBuffer.flip();
                while (!asynchronousFileChannel.write(byteBuffer, 0).isDone()) {
                    TimeUnit.MILLISECONDS.sleep(500);
                }
                asynchronousFileChannel.close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }

            try {
                result.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void failed(Throwable exc, Attachment attachment) {
            exc.printStackTrace();
        }
    }

保存参数的辅助类

在回调函数中需要传递进去一些数据。

public static class Attachment {
        private AsynchronousServerSocketChannel asynchronousServerSocketChannel;
        private AtomicInteger atomicInteger;

        public Attachment(AsynchronousServerSocketChannel serverSocketChannel, AtomicInteger atomicInteger) {
            this.asynchronousServerSocketChannel = serverSocketChannel;
            this.atomicInteger = atomicInteger;
        }

        public AsynchronousServerSocketChannel getAsynchronousServerSocketChannel() {
            return asynchronousServerSocketChannel;
        }

        public void setAsynchronousServerSocketChannel(AsynchronousServerSocketChannel asynchronousServerSocketChannel) {
            this.asynchronousServerSocketChannel = asynchronousServerSocketChannel;
        }

        public AtomicInteger getAtomicInteger() {
            return atomicInteger;
        }

        public void setAtomicInteger(AtomicInteger atomicInteger) {
            this.atomicInteger = atomicInteger;
        }
    }

总结,到这里就把单线程模型,线程池处理,Reactor模型,异步通信模型都讲完了,下一篇讲解Java中的相关容器。

猜你喜欢

转载自blog.csdn.net/bpz31456/article/details/85119356
今日推荐