最简单的Java网络编程实例(BIO、NIO、Netty版本对比)

最简单的BIO代码

通过Socket通信,服务端使用ServerSocket最后也是获得socket。
这里有一个坑,在使用Scanner.nextLine()方法时,会吃掉最后的回车符,如果服务端把没有回车符的消息回写到客户端,客户端再使用Scanner.nextLine()时就读不到该行了,会一直阻塞,BufferReader.readLine()也是同样的问题。解决方案,手动加上回车符System.lineSeparator。
还有另外一个坑,客户端代码中需要Scanner键盘输入,而idea编辑器不能在控制台输入,eclipse则可以,因为是idea中使用junit测试时无法使用键盘输入,解决方案是,在idea中把@Test下的方法改为main方法即可。

package com.example.io.bio;

import org.junit.Test;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Scanner;

public class BioEchoExample {

    // 服务器
    @Test
    public void bioEchoServer() throws IOException {

        // 1. bio即普通socket编程,服务端使用ServerSocket,客户端使用Socket
        ServerSocket serverSocket = new ServerSocket();
        // 2. 使用InetSocketAddress绑定地址,客户端是连接地址
        serverSocket.bind(new InetSocketAddress(8899));
        // 3. 死循环持续等待连接
        while (true) {
            // 4. 此处阻塞等待连接,连接成功后获取到与客户端交互的Socket实例
            Socket socket = serverSocket.accept();
            System.out.println("服务端连接建立");
            // 5. 获取成功后交由线程去处理,不同步处理了,这里使用匿名类不重新写Class文件了避免麻烦
            new Thread(new Runnable() {
                @Override
                public void run() {

                    try {
                        while (true){
                            // 6. 因为作用域问题,这里的socket是上面4定义的socket,如果是定义了新的Class线程类,需要在构造器中传入socket
                            // 7. 获取输入流重新写回socket传到客户端
                            Scanner scanner = new Scanner(socket.getInputStream(), "utf8");
                            String msg = scanner.nextLine() + System.lineSeparator();
                            System.out.print("来自客户端的数据:" + msg);
                            OutputStream outputStream = socket.getOutputStream();
                            outputStream.write(msg.getBytes("utf8"));
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        // 8. 流关闭,请自行添加上
    }

    // 客户端
    @Test
    //public static void main(String[] args) throws Exception {
    public void bioEchoClient() throws IOException{

        // 1. 新建socket
        Socket socket = new Socket();
        // 2. 连接服务器, 这里会阻塞
        socket.connect(new InetSocketAddress("localhost", 8899));
        System.out.println("客户端连接建立");
        while (true) {
            Scanner keyBoardScanner = new Scanner(System.in, "utf8");
            String clientMsg = keyBoardScanner.nextLine() + System.lineSeparator();
            // 3. 获取输出流,输出到服务端
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(clientMsg.getBytes(Charset.forName("utf8")));
            Scanner serverScanner = new Scanner(socket.getInputStream(), "utf8");
            String serverMsg = serverScanner.nextLine() + System.lineSeparator();
            System.out.print("来自服务端的消息:" + serverMsg);
        }
    }
}

最简单的NIO代码

通过SocketChannel通信,服务端使用ServerSocketChannel最后也是获得SocketChannel,跟BIO的原理类似。
这里也有一个坑,使用ByteBuffer.wrap()后,如果要读取数据,这时不能使用flip()重置,否者读不到数据。

package com.example.io.bio;

import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.Set;

public class NioEchoExample {

    // 服务器
    @Test
    public void NioEchoServer() throws IOException {

        // 1.新建服务端通道,配置为非阻塞模式
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 2.绑定地址
        serverSocketChannel.bind(new InetSocketAddress(8899));
        // 3.新建通道选择器并注册监听连接事件
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 4. 死循环服务
        try{
            while (true) {
                // 5.select()方法会阻塞直到selector上注册的事件发生了
                selector.select();
                // 6.select()方法有事件发生的话,会返回一个set集合
                Set selectionKeySet = selector.selectedKeys();
                for (SelectionKey s : selectionKeySet) {
                    if (s.isAcceptable()){
                        System.out.println("服务端连接建立");
                        // 7.连接已建立,可以获取SocketChannel进行通信了,此处accept()因为已经有连接进来,不会再堵塞了
                        ServerSocketChannel ssc = (ServerSocketChannel) s.channel();
                        SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        // 8.注册通道事件,监听客户端发来的消息并分配buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        socketChannel.register(selector, SelectionKey.OP_READ, byteBuffer);
                    }else if (s.isReadable()) {
                        // 9.客户端发来了消息
                        System.out.print("服务端收到消息, ");
                        SocketChannel sc = (SocketChannel) s.channel();
                        ByteBuffer byteBuffer = (ByteBuffer) s.attachment();
                        // 清空通道,写入数据到通道
                        byteBuffer.clear();
                        sc.read(byteBuffer);
                        // 要打印日志显示出来,切为读模式
                        byteBuffer.flip();
                        System.out.println("消息内容:" + Charset.forName("utf8").decode(byteBuffer).toString());
                        // 10.客户端发来消息后,监听写模式,写回客户端,把byteBuffer切换为读模式
                        sc.register(selector, SelectionKey.OP_WRITE, byteBuffer);
                    }else if (s.isWritable()) {
                        System.out.print("服务器写回客户端,");
                        SocketChannel sc = (SocketChannel) s.channel();
                        // 将客户端发的消息获取出来
                        ByteBuffer byteBuffer = (ByteBuffer) s.attachment();
                        // 要显示出来,切为读模式
                        byteBuffer.flip();
                        System.out.println("消息内容:" + Charset.forName("utf8").decode(byteBuffer).toString());
                        // 上面显示消息读取完后,position指针在末尾,需要在flip一下
                        byteBuffer.flip();
                        sc.write(byteBuffer);
                        // 重新注册为读
                        sc.register(selector, SelectionKey.OP_READ, byteBuffer);
                    }
                }
                // 事件处理完,清空
                selectionKeySet.clear();
            }
        }catch (Exception e) {
            // nop
        }
    }

    // 客户端
    @Test
    public static void main(String[] args) throws IOException {
    //public void NioEchoClient() throws IOException {
        // 新建SocketChannel,设置为非阻塞并连接服务器
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost", 8899));
        // 新建选择器
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        // 死循环
        while (true) {
            selector.select();
            Set selectionKeySet = selector.selectedKeys();
            for (SelectionKey s : selectionKeySet) {
                if (s.isConnectable()) {
                    System.out.println("客户端已连接");
                    SocketChannel sc = (SocketChannel) s.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // 可连接切换为已连接上,侦听写事件
                    sc.finishConnect();
                    sc.register(selector, SelectionKey.OP_WRITE, byteBuffer);
                }else if (s.isWritable()) {
                    System.out.print("客户端发消息给服务端,请输入:");
                    SocketChannel sc = (SocketChannel) s.channel();
                    Scanner scanner = new Scanner(System.in, "utf8");
                    String msg = scanner.nextLine()+System.lineSeparator();
                    ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes("utf8"));
                    // 显示出来
                    //byteBuffer.flip();wrap构造的字符这里不能flip了,否则输出为空
                    System.out.println("内容为:" + Charset.forName("utf8").decode(byteBuffer).toString());
                    byteBuffer.flip();
                    sc.write(byteBuffer);
                    sc.register(selector, SelectionKey.OP_READ, byteBuffer);
                }else if (s.isReadable()) {
                    System.out.print("客户端收到服务端发来的消息,");
                    SocketChannel sc = (SocketChannel) s.channel();
                    ByteBuffer byteBuffer = (ByteBuffer) s.attachment();
                    byteBuffer.clear();
                    sc.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println("内容为:" + Charset.forName("utf8").decode(byteBuffer).toString());
                    sc.register(selector, SelectionKey.OP_WRITE, byteBuffer);
                }
            }
            selectionKeySet.clear();
        }
    }
}

Netty代码

使用netty的组件EventLoopGroup, Bootstrap, ChannelInitializer, ChannelHandler。ChannelInitializer在初始化时添加一些handler处理器,如编码解码器,其中最后一个为自己的业务处理器。
这里也有个坑,不能使用junit运行启动,需要使用main方法启动。

package com.example.io.bio;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.sctp.SctpNotificationHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.junit.Test;

import java.util.Scanner;

public class NettyEchoExample {


    //@Test
    public static void main(String[] args) throws InterruptedException {
    //public void nettyEchoServer() throws InterruptedException {
        // 新建事件循环组,相当于while死循环使程序不退出,boss负责接收连接,workder复制处理连接
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        // 服务器启动类,设置父子事件循环组,并添加父子处理器
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer() {
                    // 子处理器中Channel初始化时加入业务处理器
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        // 基于行的编码解码器
                        pipeline.addLast(new LineBasedFrameDecoder(1024));
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        // 自己的业务处理代码,数据写回客户端
                        pipeline.addLast(new SimpleChannelInboundHandler() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println("服务端收到消息,内容为:" + s);
                                System.out.println("服务端写回客户端,内容为:" + s);
                                channelHandlerContext.channel().writeAndFlush(s);
                            }
                        });
                    }
                });
        // 绑定端口
        serverBootstrap.bind(8899).sync().channel().closeFuture().sync();
    }

    //@Test
    //public static void main(String[] args) throws InterruptedException {
    public void nettyEchoClient() throws InterruptedException{
        // 新建事件循环组
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        // 只有一个事件循环组,所以只要一个hander即可
        bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                .handler(new ChannelInitializer() {
                    // 通道连接激活时即可发送数据到服务器
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        System.out.println("通道建立,客户端可写,请输入:");
                        Scanner scanner = new Scanner(System.in, "utf8");
                        String msg = scanner.nextLine();
                        ctx.channel().writeAndFlush(msg);
                    }

                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        // 基于行的编码解码器
                        pipeline.addLast(new LineBasedFrameDecoder(1024));
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());
                        // 自己的业务处理代码,发送数据到服务端
                        pipeline.addLast(new SimpleChannelInboundHandler() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                                System.out.println("客户端收到服务端的消息,内容为:" + s);
                            }
                        });
                    }
                });
        bootstrap.connect("localhost", 8899).sync().channel().closeFuture().sync();
    }
}


你可能感兴趣的:(最简单的Java网络编程实例(BIO、NIO、Netty版本对比))