Java 中的 网络IO (1)

174 阅读4分钟

Socket探究

首先来看一下Java BIO中 Server 和 Client 的建立代码。
BIO Server

public class SocketIOPropertites {


    private static final int BACK_LOG = 2;

    public static void main(String[] args) {

        ServerSocket server = null;
        try {
            server = new ServerSocket();
            //BACK_LOG = 2  后序可以被 留在内核空间不被server程序使用的socket 可以有2个
            // 在netty  中同样可以配置 ,可以在负载均衡中 拒绝连接
            server.bind(new InetSocketAddress( 9090), BACK_LOG);

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("server up use 9090!");
        while (true) {
            try {
                Socket client = server.accept();
                System.out.println("client port: " + client.getPort());

                new Thread(
                        () -> {
                            while (true) {
                                try {
                                    InputStream in = client.getInputStream();
                                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                                    char[] data = new char[1024];
                                    int num = reader.read(data);

                                    if (num > 0) {
                                        System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
                                    } else if (num == 0) {
                                        System.out.println("client readed nothing!");
                                        continue;
                                    } else {
                                        System.out.println("client readed -1...");
                                        client.close();
                                        break;
                                    }

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

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

Client

public class SocketClient {

    public static void main(String[] args) {

        try {
            Socket client = new Socket("192.168.150.11",9090);
			//设置发送缓冲区的大小
            client.setSendBufferSize(20);
            //TcpNoDeylay 表示的是 是否不要延迟发送。
            //因为如果一个一个字节写入的话就有可能一个字节就会打成一个数据包发送,有一点浪费资源,
            //延迟发送的话可以提高资源利用率,等待一堆数据再打包发送,但这样同样会导致响应变慢影响吞吐量。
            client.setTcpNoDelay(true);
            OutputStream out = client.getOutputStream();

            InputStream in = System.in;
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));

            while(true){
                String line = reader.readLine();
                if(line != null ){
                    byte[] bb = line.getBytes();
                    for (byte b : bb) {
                        out.write(b);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

以上就是一个BIO下Server 和 Client 的使用。

思考:Socket 套接字 底层到底是什么呢?

这时候就要去想创建套接字的目的是什么?

原因:在应用程序中想要通过TCP协议获取服务端的数据就要先找到Server程序所在IP 和 Port
当找到后 ,Server 和 Client 进行 TCP三次握手建立连接,此时Server 和 Client都知道了
对方的IP:Port,在这样的情况下
ClientIP:ClientPort -- ServerIP:ServerPort
这么一条记录就是独一无二的。 事实上这个就是socket 的本质 ,是一个记录 CIP:CPort -- SIP:SPort 信息的一个四元组。 当有了这么一个socket 后,在Linux 下 会在内核中为socket 进行其他一些配置,比如 buffer,BACK_LOG....

那么 Java 中Socket 和 Linux内核中的socket 又有什么联系呢?
从别的地方学习到,Java中的socket本质上是一个指向内核socket的一个文件描述符 FD(linux 中一切皆文件) ,每一次对网络流的读写就是通过文件描述符进行IO。

这样子其实是Socket 在底层的一些事情。

NIO

说了BIO下的Socket,在BIO中的代码,每当有一个连接进来就会创建一个线程去一直阻塞监听连接里的信息,这样及其耗费资源,由此NIO出现了。

名词区分 NIO

  • 在JDK中NIO 指的是New IO
  • 在Linux内核中指的是 NoneBlock IO 即非阻塞IO

那么我们先来看看Java 中创建NIO 的代码。

import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketNIO {

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

        LinkedList<SocketChannel> clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); //重点  OS  NONBLOCKING!!!
        
        ss.setOption(StandardSocketOptions.TCP_NODELAY, false);


        while (true) {
            Thread.sleep(1000);
            SocketChannel client = ss.accept(); //不会阻塞?  -1NULL

            if (client == null) {
                System.out.println("null.....");
            } else {
                client.configureBlocking(false);
                int port = client.socket().getPort();
                System.out.println("client...port: " + port);
                clients.add(client);
            }

            ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外

            for (SocketChannel c : clients) {   //串行化!!!!  多线程!!
                int num = c.read(buffer);  // >0  -1  0   //不会阻塞
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }


            }
        }
    }

}

注意 ServerSocketChannel 这个类是在java.nio 这个包下的。 而在这个代码里,我们没有Server像BIo中一样对每个连接生成对应的线程进行处理,而只是用一个主线程就处理了所有事情。对连接的接收,对socket中输入流的读取....

而完成这样功能的原因是 这两句代码
ss.configureBlocking(false); ServerSocketChanne设置为非阻塞
client.configureBlocking(false); SocketChannel 设置为非阻塞。

当这样子设置了以后,线程就不会阻塞在 ss.accept()client.read()上了, 因为无论ss.accept()client.read()中是否有连接或数据都一定会有返回值,也就不会再阻塞住了(BIO中如果accept没有连接进来就没有返回值也就阻塞在那了,read没有数据 也一样阻塞)。
当accept 没有连接 或 read没有数据,就会返回 -1 来表示当前没有数据进入。这样子主线程就会去干别的事情,等下一个循环再来时又会再看一下是否有连接 ,是否有数据。

这样子一来 非阻塞的NIO 比传统BIO 节省了大量的对线程创建 管理 销毁的开销,极大的提高了利用率。

NoneBlocking 在系统层面的实现其实就是对网卡读取时更改了处理方式,BlockIO 在读取网卡时如果没有数据进入就会阻塞在那等网卡中有数据来在继续运行,而None BlockingIO就是如果网卡中没数据就算了不在那阻塞等待,而是直接返回一个-1 表示当前没有数据。

但是现在这样的代码是有问题的,如果同时10个连接进来,只能一次读一个就去干别的事情了,下一次就去用多路复用的方式来进一步调高利用率。