Java 中的 IO 与 socket 编程 [ 复习 ]

1、Unix IO 与 IPChtml

Unix IO:Open-Read or Write-Closejava

IPC:open socket - receive and send to socket - close socket数据库

 

IPC 全称是 InterProcess Communication。编程

当消息发出后,消息进入 SendQ队列 一直等待 sending socket 处理,才真正发出(一直等待是阻塞的)。当消息到达时,消息进入RecvQ队列 一直等待 receiving socket 处理(同前)。segmentfault

底层的 TCP 协议关联的 RecvQ 或者 SendQ 队列就是一个操做系统缓冲区。数组

 

TCP/UDP 基于 IPC。Udp 是一种 datagram communication protocol,是 connectionless protocol,意味着每次发送 datagrams 时,须要额外发送local socket descriptor 和 receiving socket's address.缓存

Tcp 是一种 stream communication protocol 你能够经过直接构建 Tcp 通道创建链接,在通道中的 socket pairs 能够互相访问。服务器

 

2、Java 中的 IO 编程网络

基于流的多线程

public static void simpleIo() throws IOException {
    InputStream input = new BufferedInputStream(new FileInputStream("/home/lg/Pictures/wallpapers/2047.png"));
    OutputStream out = new BufferedOutputStream(new FileOutputStream("dem.png"));
    byte[] buf = new byte[1024];
    while (input.read(buf) > 0) {
        out.write(buf);
    }
    out.flush();
}

基于字符的

public static void simpleIo() throws IOException {
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    PrintWriter out = new PrintWriter(System.out, true);
    String myLine;
    while ((myLine = input.readLine()) != null) {
        out.println(myLine);
    }
}

// new String(char[] chars, "utf-8") 字节序列到字符串的转换

* PrintWriter 与 BufferedWriter 区别

 1. PrintWriter的print、println方法能够接受任意类型的参数,而BufferedWriter的write方法只能接受字符、字符数组和字符串;

2. PrintWriter的println方法自动添加换行,BufferedWriter须要显示调用newLine方法;

3. PrintWriter的方法不会抛异常,若关心异常,须要调用checkError方法看是否有异常发生;

4. PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);

5. PrintWriter的构造方法更广。 

 

3、Java 中的 socket 编程

服务端

public class Server {

    public static void main(String[] args) {
        {
            ServerSocket myServer = null;
            Socket acceptSocket = null;
            BufferedReader input = null;
            PrintWriter output = null;
            try {
                myServer = new ServerSocket(4000);

                while (true) {
                    
                    acceptSocket = myServer.accept();
                    
                    input = new BufferedReader(new InputStreamReader(acceptSocket.getInputStream()));
                    output = new PrintWriter(acceptSocket.getOutputStream(), true);

                    String rLine;

                    while ((rLine = input.readLine()) != null) {
                        System.out.println("服务器接受到一条消息:\n" + rLine + "\n---------------\n\n");

                        if (rLine.equals("hello server")) {
                            System.out.println("而后打了个招呼\n---------------\n\n");
                            output.println("hello client");
                        } else {
                            System.out.println("而后嘲讽了一波\n---------------\n\n");
                            output.println("233333");
                        }
                    }
                }

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

客户端

public class Client {

    private static Socket myClient;

    //静态初始化时处理异常
    static {
        try {
            myClient = new Socket("localhost", 4000);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void sendMessage() {
        try {
            PrintWriter output = new PrintWriter(myClient.getOutputStream(), true);
            BufferedReader readConsole = new BufferedReader(new InputStreamReader(System.in));

            String cline;

            while ((cline = readConsole.readLine()) != null) {
                output.println(cline);
                System.out.println("消息发送成功!\n---------------\n\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void receiveMessage() {

        try {
            BufferedReader input = new BufferedReader(new InputStreamReader(myClient.getInputStream()));
            // 这里省略掉了 OutputStream(System.out), 直接使用打印替代

            String rLine;
            while ((rLine = input.readLine()) != null) {
                System.out.println("客户端收到来自服务器的消息:\n" + rLine + "\n---------------\n\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(Client::sendMessage).start();
        new Thread(Client::receiveMessage).start();
    }

}

 

说明:

客户端使用了两个线程:一个用于发送消息,另外一个用于接受消息。服务端只考虑了一个 socket 链接,所以并无用多线程。实际上,服务端一般须要同时处理多个 socket 链接,所以可考虑多线程。创建一个线程池,对于每一个 socket 链接,分派一个线程去处理。

 

4、NIO 与 socket 编程

1. 处理流程对比

普通的 BIO,消息要等待处理,须要先放入操做系统的 Socket 缓冲区(阻塞的SendQ与RecvQ队列),这样一直等待,直到 sending socket 或 receiving socket 就位后,才进行处理。

而 NIO,是 消息进入到操做系统的 Socket 缓冲区,直接复制到 Buffer (ByteBuffer.allocate)中,抑或直接进入与系统底层关联的缓冲区(ByteBuffer.allocateDirector)。不用一直等待 sending socket 或 receiving socket 就续就进行处理,是非阻塞的。

须要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操做的阻塞是使用CPU的,真正在"干活",并且这个过程很是快,属于memory copy,带宽一般在1GB/s级别以上,能够理解为基本不耗时。

 

下图是几种常见I/O模型的对比:(图片来自:UNIX网络编程 -- I/O复用:select和poll函数

 

(摘自 Linux IO模式及 select、poll、epoll详解 )

  • BIO,同步阻塞IO,在IO执行的两个阶段都被block了。若是链接少,他的延迟是最低的,由于一个线程只处理一个链接,适用于少链接且延迟低的场景,好比说数据库链接。
  • NIO,同步非阻塞IO,用户进程须要不断的主动询问kernel数据好了没有。
  • 多路复用IO,就是咱们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。注意与 nio 的关系:在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
  • 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
  • 异步IO,用户进程发起read操做以后,马上就能够开始去作其它的事,当数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。

 

2.线程模型对比

NIO一个重要的特色是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操做是同步阻塞的(消耗CPU但性能很是高)。

回忆BIO模型,之因此须要多线程,是由于在进行I/O操做的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即便经过各类估算,算出来操做系统没有能力进行读写,也无法在socket.read()和socket.write()函数中返回,这两个函数没法进行有效的中断。因此除了多开线程另起炉灶,没有好的办法利用CPU。

NIO的读写函数能够马上返回,这就给了咱们不开线程利用CPU的最好机会:若是一个链接不能读写(socket.read()返回0或者socket.write()返回0),咱们能够把这件事记下来,记录的方式一般是在Selector上注册标记位,而后切换到其它就绪的链接(channel)继续进行读写。

 

3.事件模型

NIO的主要事件有几个:读就绪、写就绪、有新链接到来。Selector 主要围绕这几个事件作分发。注册全部感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。 

 服务端

public class Server {

    private static Selector selector;

    static {
        try {
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        sscRegister();
    }

    private static void sscRegister() {

        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();

            ssc.configureBlocking(false);
            ssc.socket().bind(new InetSocketAddress("localhost", 8000));

            ssc.register(selector, SelectionKey.OP_ACCEPT);

            dispatcher(selector);

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

    private static void dispatcher(Selector selector) {
        try {

            while (true) {

                int select = selector.select();

                if (select == 0) {
                    System.out.println("返回 0 表示 SelectedKeys 未更新");
                    continue;
                }

                Iterator<SelectionKey> selectedIterator = selector.selectedKeys().iterator();
                while (selectedIterator.hasNext()) {
                    SelectionKey key = selectedIterator.next();

                    selectedIterator.remove(); // 只是从 SelectedKeys 中移除,而并非从整个选择器的键集中移除。

                    if (!key.isValid()) {
                        continue;
                    }
                    if (key.isConnectable()) {
                        System.out.println(233);
                    }
                    if (key.isAcceptable()) {
                        accept(key);
                    }
                    if (key.isReadable()) {
                        read(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void accept(SelectionKey key) {

        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        try {
            SocketChannel socketChannel;
            if ((socketChannel = serverSocketChannel.accept()) != null) { // 服务端非阻塞时,直接返回 null

                socketChannel.configureBlocking(false);

                socketChannel.register(selector, SelectionKey.OP_READ);
            }


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

    private static void read(SelectionKey key) {

        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try {
            if (channel.read(buffer) > 0) {
                System.out.println("客户端:" + new String(buffer.array()));
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            buffer.clear();
        }

    }
}

  

客户端

public class Client {

    private static void clientSendMsg() {
        try {
            SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", 8000));

            System.out.println("已向服务器发出请求!");

            String msg = "hello" + Thread.currentThread().getName();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

            client.write(buffer);
            buffer.flip();
            Thread.sleep(5);

            client.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(Client::clientSendMsg, "Thread-A").start();
    }

}

 

  

参考文章


 https://tech.meituan.com/nio.html?utm_source=tool.lu

https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html

https://www.bysocket.com/?p=615 (大小端模式)

相关文章
相关标签/搜索