首先呢,我们从代码的层面看一下,一个Web服务器是如何运行的。这里的Web服务器实际应该指的是一个运行在机器上的程序,现在各种文章里面“服务器”乱叫,都让人搞不清到底在说啥。从大的层面嘛,服务器就是一台电脑。一台电脑可以做服务器,也可以做客户端,比如我的电脑可以同时做服务器和客户端。而Web服务器就有些含糊,需要根据语境来判断到底是指一台电脑还是在电脑上运行的程序。本文中,Web服务器特指运行在电脑上的程序。
两台电脑通过网络通信,本质上是两支分别运行在各个电脑上的程序(更具体点是进程)在通信,而进程是通过套接字(socket)把数据发送到网络中去的。形象的说,套接字是一支运行中的程序在网络中的代表,因为它包含了两个必要的属性:IP和端口号。IP用来定位主机,端口号用来定位进程。每个需要进行网络通信的进程,其进程ID必然和是某个套接字的端口号绑定的。套接字是操作系统的一种资源,可以由操作系统分配和回收。
public class WebClient {
private Socket socket = null;
public WebClient(Socket socket) {
this.socket = socket;
}
...
}
我们需要借助Java语言已经封装好的Socket类,由他负责与底层的操作系统交互。在展示类的构造方法中,要传入一个在main方法(见下面)中构造好的Socket对象,设定了参数IP和端口号,这样Java虚拟机就会向操作系统申请一个套接字,这个套接字就代表了我们运行时的WebClient类在网络中的位置。
然后是接收和发送数据的方法:
public void send() throws IOException {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("GET /index.html HTTP/1.1");
out.println("Host: localhost:8080");
out.println();
}
以上代码明显是输出一个标准的HTTP请求头,这跟浏览器的工作原理是一致的。
public void accept() throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuffer sb = new StringBuffer(8096);
while (true) {
if (in.ready()) {
int i=0;
while (i != -1) {
i = in.read();
sb.append((char) i);
}
}
System.out.println(sb.toString());
socket.close();
}
}
这是接收服务器发来的数据,展示然后关闭连接。这里的展示显然只是简单的以字符串的形式打印到控制台,但是浏览器会去解析这些数据,比如常见的HTML代码会被解析成结构化的文档,图片也会被还原。
写个测试:
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8081);
WebClient webClient = new WebClient(socket);
webClient.send();
webClient.accept();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
以上还只是客户端的代码,服务器会有所不同。为啥呢?因为二者角色不同。服务器是提供服务的,必须二十四小时待命。但是客户端不同,它可以随时随地发起连接,收到回应后就关闭。一句话,客户端不是固定的,然而服务器却必须是固定的,只有这样才能为客户端提供服务。这也是为啥服务器的IP和端口号是已知的、公开的。这就是传统B/S结构的来源。
为了能时刻监听来自客户端的请求,服务器端需要一个ServerSocket类,一旦收到请求,这个类会新建一个Socket类与客户端通信。
它的一个构造方法如下:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
然后我们的例子是:
public class WebServer {
private ServerSocket serverSocket = null;
public WebServer(int port, String host) {
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName(host));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
...
}
到此只是初始化了serverSocket,给它一个绑定的IP地址以及监听端口,重点是下面的:
public void response() {
while (true) {
Socket socket = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
output = socket.getOutputStream();
byte[] bytes = new byte[1024];
FileInputStream fis = null;
try {
File file = new File("E:\\index.html");
if(file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, 1024);
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, 1024);
}
} else {
String errorMessage = "Sorry, file does not exist!";
output.write(errorMessage.getBytes());
}
} catch(IOException e) {
e.printStackTrace();
}
socket.close();
}
catch (Exception e) {
e.printStackTrace ();
continue;
}
}
}
重点关注标红部分,如果文件存在则发送该文件的字节流;否则直接在代码中生成一个字符串发回去。所以,返回给客户端的数据是可以直接在代码中动态生成的,代码中可以设置条件,根据条件来生成不同的代码。但是对于内容都已经写死的HTML文件这恐怕是非常难办到的,总不能每个条件分支都对应一个HTML文件吧。。而且其实每个条件下,变动非常小,比如整体页面不变,只有上面的名字或电话号码变了,难道值当为此重复写N个HTML文件?
所以这时用代码来搞事情的优势就很明显了,我们在代码里面想让它怎么输出就怎么输出。说白了,反正最终都是发给套接字一堆数据流,至于数据流从静态的HTML文件中来还是由代码生成,不必care。