写写WEB中的IO以及JAVA的NIO:P1
一个最简单的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的异步,虽然是日记式的瞎写,但突然发现已经写的有点长了,下篇再更吧:)