Java 网络编程之TCP(三):基于NIO实现服务端,BIO实现客户端

前面的文章,我们讲述了BIO的概念,以及编程模型,由于BIO中服务器端的一些阻塞的点,导致服务端对于每一个客户端连接,都要开辟一个线程来处理,导致资源浪费,效率低。

为此,Linux 内核系统调用开始支持NIO(non-blocking IO),非阻塞IO,将之前BIO中的一些阻塞点,改为非阻塞,体现在Java API中就是:

服务端:

服务器等待客户端连接的accept方法不阻塞;java api中accept

服务器读取客户端数据不阻塞阻塞;java api中read

Java NIO编程中,主要涉及以下三个主要概念:

1.Channel :IO操作的联结,代表硬件,文件,网络套接字的连接,对应于BIO中的Socket; Channel需要与Buffer结合使用

2.Buffer:用于数据操作的缓冲区,就是一块内存,提供了一些操作,方便使用;

3.Selector:选择器,就是Linux 内核中的IO多路复用器,为了提高网络IO编程的效率,常用的有select, poll, epoll, 可以参考Linux对应系统调用

这三个概念,我们在后面的编程模型都会涉及。

下面我们先基于Channel和Buffer实现一个简单的服务端,用之前的BIO实现一个客户端;

Channel和Buffer对应的API的返回值含义,我都会在代码中注释清楚:

需求:

服务端:基于NIO,可以非阻塞的接收客户端连接,对客户端采用轮询接收数据

客户端:基于BIO,连接服务端,并发送数据

服务端代码:

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * 基于NIO中的Channel和Buffer实现服务端,对客户端采用轮询
 *
 * @author freddy
 */
class NIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 非阻塞
        serverSocketChannel.bind(new InetSocketAddress(9090));

        List<SocketChannel> clients = new LinkedList<>();
        while (true) {
            try {
                Thread.sleep(3000); // 为了方便测试观察
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // accept非阻塞, 没有连接时,返回null
            SocketChannel client = serverSocketChannel.accept();
            if (client != null) {
                // 设置client非阻塞
                client.configureBlocking(false);
                clients.add(client);
            }

            // 遍历处理所有的client,看有没有数据可以读取
            Iterator<SocketChannel> iterator = clients.iterator();
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 共用buffer
            System.out.println("clients size :" + clients.size());
            while (iterator.hasNext()) {
                SocketChannel clientSocket = iterator.next();
                try {
                    int len = clientSocket.read(buffer); // read()返回:>0:读取到数据 0:没读到数据 -1:连接关闭
                    if (len > 0) {
                        // 读取到数据后,进行打印
                        buffer.flip();
                        byte[] bytes = new byte[buffer.limit()];
                        System.out.println(clientSocket + "read data len:" + bytes.length);
                        buffer.get(bytes);
                        System.out.println(clientSocket + " data: " + new String(bytes));
                    } else if (len == 0) {
                        System.out.println(clientSocket + " no data");
                    } else if (len == -1) {
                        // 连接关闭
                        iterator.remove();
                        System.out.println(clientSocket + " close, remove");
                    }
                    buffer.clear();
                } catch (IOException exception) {
                    iterator.remove();
                    System.out.println(clientSocket + " disconnect, remove");
                }
            }
        }
    }
}

客户端代码:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * 基于BIO的TCP网络通信的客户端,接收控制台输入的数据,然后通过字节流发送给服务端
 *
 * @author freddy
 */
class ChatClient {
    public static void main(String[] args) throws IOException {
        // 连接server
        Socket serverSocket = new Socket("localhost", 9090);
        System.out.println("client connected to server");

        // 读取用户在控制台上的输入,并发送给服务器
        new Thread(new ClientThread(serverSocket)).start();

        // 接收服务端发送过来的数据
        try (InputStream serverSocketInputStream = serverSocket.getInputStream();) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = serverSocketInputStream.read(buffer)) != -1) {
                String data = new String(buffer, 0, len);
                System.out.println(
                    "client receive data from server" + serverSocketInputStream + " data size:" + len + ": " + data);
            }
        }

    }
}

class ClientThread implements Runnable {
    private Socket serverSocket;

    public ClientThread(Socket serverSocket) {
        this.serverSocket = serverSocket;
    }

    @Override
    public void run() {
        // 读取用户在控制台上的输入,并发送给服务器
        InputStream in = System.in;
        byte[] buffer = new byte[1024];
        int len;
        try (OutputStream outputStream = serverSocket.getOutputStream();) {
            // read操作阻塞,直到有数据可读,由于后面还要接收服务端转发过来的数据,这两个操作都是阻塞的,所以需要两个线程
            while ((len = in.read(buffer)) != -1) {
                String data = new String(buffer, 0, len);
                System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));
                if ("exit\n".equals(data)) {
                    // 模拟客户端关闭连接
                    System.out.println("client close :" + serverSocket);
                    // 这里跳出循环后,try-with-resources 会自动关闭outputStream
                    break;
                }
                // 发送数据给服务器端
                outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

测试:

先开启服务端,再开启两个客户端发送数据,服务端接受连接后,会打印当前接受到的客户端总数,然后轮询接收数据后打印;

当客户端发送exit后,客户端会关闭连接,服务端会识别到,去除该客户端;

当客户端发进程异常关闭后,客户端会断开连接,服务端会识别到,去除该客户端;

测试日志:

客户端1和2,正常发送数据

图1

客户端1发送exit后,关闭连接

客户端2断开连接

可以直接在Idea中关闭客户端2的程序,或者用nc命令模拟,Ctrl+C关闭nc

bash 复制代码
clients size :1
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:17914] no data
clients size :1
java.nio.channels.SocketChannel[connected local=/127.0.0.1:9090 remote=/127.0.0.1:17914] disconnect, remove
clients size :0
clients size :0

我们在上面的图1中,可以看到,客户端短期内发送的两次内容,是在服务端一次性读到的;这个就是粘包、拆包现象的一种,后面我们会看。

相关推荐
P7进阶路1 小时前
Tomcat异常日志中文乱码怎么解决
java·tomcat·firefox
幽兰的天空1 小时前
介绍 HTTP 请求如何实现跨域
网络·网络协议·http
lisenustc1 小时前
HTTP post请求工具类
网络·网络协议·http
心平气和️1 小时前
HTTP 配置与应用(不同网段)
网络·网络协议·计算机网络·http
小丁爱养花1 小时前
Spring MVC:HTTP 请求的参数传递2.0
java·后端·spring
心平气和️1 小时前
HTTP 配置与应用(局域网)
网络·计算机网络·http·智能路由器
CodeClimb1 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
等一场春雨1 小时前
Java设计模式 九 桥接模式 (Bridge Pattern)
java·设计模式·桥接模式
带刺的坐椅2 小时前
[Java] Solon 框架的三大核心组件之一插件扩展体系
java·ioc·solon·plugin·aop·handler
Mbblovey2 小时前
Picsart美易照片编辑器和视频编辑器
网络·windows·软件构建·需求分析·软件需求