程序员

写写WEB中的IO以及JAVA的NIO:P1

2018-03-17  本文已影响0人  namelessEcho

一个最简单的TINYServer

先来看看一个最简单的停等服务器吧!这个例子一般都是教材书给的那种最原始的,但是也是最清晰的web服务器实现。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TinyServer {
       public static void main(String[] args) throws IOException {
           ServerSocket serverSocket = new ServerSocket(2202);
           while(true) {
               //线程会阻塞在这里直到建立连接并返回连接套接字
               Socket socket =serverSocket.accept();
               //处理客户端请求的具体逻辑,这里只是简单的打印到控制台并回送给客户端
               process(socket);
           }
       }
       public static void process(Socket socket) throws IOException{
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            StringBuilder result = new StringBuilder() ;
            String line=reader.readLine();
            while(line!=null){
                System.out.println("server received:"+line);
                // 如果循环内不写入数据到client的话 client的readLine方法会阻塞而导致死锁
                writer.write("server has got your request is "+line+"\r\n");
                writer.flush();

                line=reader.readLine();
            }


            //关闭打开的资源
            reader.close();
            writer.close();
            socket.close();
            
       }
}

相应的client的代码也很类似,也是打开套接字阻塞等待写和读

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    public static void main (String[] args) throws UnknownHostException, IOException {
        Socket  client = new Socket();
        //阻塞等待服务器的连接
        client.connect(new InetSocketAddress("localhost",2202));
        sendAndEcho(client);
    }
    public static void sendAndEcho(Socket client) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
        String line = null;
        writer.write("echo echo\r\n");
        writer.flush();
        line=reader.readLine();
        System.out.println("client received:"+line);


        writer.write("mayday mayday\r\n");
        writer.flush();
        line=reader.readLine();
        System.out.println("client received:"+line);

        writer.close();
        reader.close();
        client.close();
    }
    
}

稍微说一点题外话, 关于reader.readLine()的API,该函数是阻塞式的,在整个流达到EOF才会返回NULL,而对于一个socket流来说,一部分人可能会认为当socket中的数据传输完毕后流就结束了,这种观点是不正确的。事实上只有socket关闭才象征整个流的结束,所以在这时该函数才会返回NULL,所以想要通过readLine返回NULL来说明socket数据传输完毕是不可行的,其实其他的阻塞式的IO,在socket上都存在相应的问题

例如,对于server和client两端都采取这种写法的话,由于两边都阻塞在readLine()上没有办法关闭socket,会导致死锁的产生

//without closing socket
//both blocked here
while(reader.reaLine()!=null){
    // process data here  
}
socket.close();

看起来服务器已经可以正常工作了,客户端确实得到了服务器处理的结果,服务器也完成了自己的任务。但是请考虑一个情况,多个用户线程同时对服务器发起请求。这个时候Tiny服务器明显是不够用的。由于其阻塞的特性,第二个客户端必须等待第一个客户端完成作业,才能得到响应,这就好像是去银行取款,只有一个窗口,大家不得不排队取款,所以效率极差。

利用多线程改进的模型

现代的操作系统大多是多核,而多核意味着可以有多个CPU并行的去处理任务。对于CPU来说,线程是其基本的调度单元,可以将线程抽象成为一个CPU的具体执行逻辑(这么说可能稍微有些问题,因为线程不仅仅只抽象了CPU,不过也没什么大问题)。所以可以充分利用多线程模型去改进之前的TinyServer,对于每次到来的请求,我们都开启一个新的线程来处理他们,这样就不会存在一个client必须等待另一个client完成才能执行的尴尬局面了。
具体的代码如下

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiThreadServer {
       public static void main(String[] args) throws IOException {
           ServerSocket serverSocket = new ServerSocket(2202);
           while(true) {
               //线程会阻塞在这里直到建立连接并返回连接套接字
               Socket socket =serverSocket.accept();
               //处理客户端请求的具体逻辑,这里只是简单的打印到控制台并回送给客户端
               Thread t = new Thread(new Process(socket));
               t.start();
           }
       }
  static class  Process implements Runnable {
          Socket socket ;
          public Process(Socket  socket){
                this.socket=socket;
          }

           @Override
           public void run() {
          //之前的process逻辑在新线程的run方法中执行。
       }
}

看上去比之前好了不少,可以并发的处理多个用户端的请求了。上古时代WEB的请求数量不大的时候,确实是这么做的。比如TOMCAT5的默认CONNECTOR,就是使用的这种方式。

多线程所导致的问题

其实线程对于操作系统来说仍然是很昂贵的,一个线程占去的内存空间可能有500k-1M,对于一台4GB内存的电脑来说,能存在的线程至多不过几千个,而现在的web请求动辄成千上万,所以多线程模型,根本就吃不消高并发。(PS操作系统本身对于进程所能创建的线程个数存在限制,所以之前讨论的线程数量更要打折扣。)另一方面,线程是抢占式的任务,依靠内核来调度,这使得多个线程之间进行上下文切换需要内核管理,在高并发的情况下,大量的CPU时间都要花在上下文切换之中,这显然违背了一开始使用线程提高吞吐的初衷。归根到底,在大量并发的情况下,线程显得过于重量(尽管相比于进程,它已经算是轻的了)

解决之道

非阻塞(Non-Blocking IO)JAVA中的NIO与I/O多路复用(其实就是一个线程管理多个IO链接)或者是基于callback的异步,虽然是日记式的瞎写,但突然发现已经写的有点长了,下篇再更吧:)

上一篇 下一篇

猜你喜欢

热点阅读