先想想需要达到怎样的要求:
本来这是一个很小的课程设计作业,老师也是要求能达到简单的socket应答就行了。但是我还是觉得有必要自己手撸一个HTTP服务器,毕竟这样更炫酷。
在开始写之前,我们先想想应该达到一个怎样的效果,我自己罗列了一下:
- 能在浏览器访问网页,比如:http://localhost:8000/index.html,这样子能解析自己预先准备的index.html:
- 能读取文本信息:比如:http://localhost:8000/17.xml,这样子能直接读取文本显示。
- 能处理请求异常:比如:http://localhost:8000/we.html,我们不支持这个地址,就会弹出File not found
- 持续接受用户的请求(无论正确/错误)
根据我自己对于HTTP服务器的理解,设计出HTTP服务器的生命周期如下:
- Step1-
read
:读取socket数据流 - Step2-
parser
:解析数据流,分析报头,得到客户给予服务器的命令语义 - Step3-
process
:处理客户给的命令语义,返回处理结果 - Step4-
response
:把处理结果打包,增加报头 - Step5-
write
:写入socket数据流
以上的五步,我们完全可以把他作为一个线程独立出来,我希望:每一次客户向服务器发起请求的时候,我们就独立开辟一条线程去处理客户的需求,说白了,用子线程去独立执行以上的过程。
开始撸码
你应该遵循一下函数设计的规范:
我个人的编码规范,对于一个有返回对象的函数,应该需要设计如下:
public Object functionExample(Object params){
Object object1 = null;
Obejct object2 = null;
try{
object1 = new Object();
object2 = new Object();
//Do something
return object1;//Here is the return
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//Close object1
//CLose object2
}catch(Exception e){
e.printStackTrace();
}
}
return null;
}
部分代码如果想偷懒偶尔不写close
,我们也可以用这个
@SuppressWarnings("resource") // Warnings iggnore
先写服务器的主线程框架:
package csdn_fake_server;
import java.io.*;
import java.util.logging.Logger;
import java.net.*;
public class Server extends Thread{
int watchPort = 8000;
Logger log = Logger.getLogger("SERVER-LOG-JT");//Open log
public Server(){};
/**
* Server
* @param port: The server listen port
*/
public Server(int port) {
watchPort = port;
}
//Class httpServer{} 这里将会实现我们上面提到的子线程
@Override
public void run() {
super.run();
try {
@SuppressWarnings("resource") // Warnings iggnore
ServerSocket serverSocket = new ServerSocket(watchPort);
while (true) { // Waitting for clients
Socket socket = serverSocket.accept();// Thread Join here
//Child Thread here
new httpServer(socket).start();// If connected, start a new server thread
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
以上是我写出来一个自己脑海里服务器的线程框架,该类里面你应该要配备一个测试的函数main(String[] args)
。
我们在启动服务器线程之后,我们将利用ServerSocket
的accept()
通过线程拥塞来监听端口的信息,因此,我们一旦收到服务器的套接字信息之后,就可以开启我们的子线程去处理客户的需求了。
接着写服务器子线程框架实现
class httpServer extends Thread {
Socket currentSocket = null; //Client Socket
InputStream inputStream = null;//Use for get header
OutputStream outputStream = null;//Use for response
final static int sucessStatus = 200;//HTTP response header status code
final static int noResourceStatus = 404;//HTTP response header status code
int resultStatus = 404; //Our response code
long responseLength = 20; //@Attention!!! Error info length, Important
public httpServer(Socket socket) {
try {
currentSocket = socket; //Get socket bridge
inputStream = socket.getInputStream(); // Get inputstream
outputStream = socket.getOutputStream(); // Get outputstream
} catch (Exception e) {
log.info("Connection aborted");
}
}
// Read=>parser=>process=>response=>write
@Override
public void run() {
try {
String rawString = read(); // Get raw header
String serverCommand = parser(rawString); // Parser the raw header
String resultString = process(serverCommand); // Process command
String rawResultString = response(resultString); // Pack the result
write(rawResultString);// Write in responsed stream
currentSocket.close();// CLose the client socket
//End thread
} catch (Exception e) {
log.info("Failed connection");
e.printStackTrace();
}
}
private String read(){//Step 1
try{
//Read Interface
return null;
}catch(Exception e){
log.info("Read faild");
e.printStackTrace();
}
return null;
}
/**
*
* @param rawString: raw String from read()
* @return server command from client
*/
private String parser(String rawString) { //Step 2
try{
//Parser Interface
return null;
}catch(Exception e){
log.info("Parser faild");
e.printStackTrace();
}
return null;
}
/**
*
* @param commandString: command string from parser
* @return
*/
private String process(String commandString) { //Step 3
try{
//Process Interface
return null;
}catch(Exception e){
log.info("Process failed");
e.printStackTrace();
}finally{
try{
//Close IO
}catch(Exception e){
log.info("Bad IO");
e.printStackTrace();
}
}
return null;
}
private String response(String resultString) { // Step 4
try{
// response Interface
return null;
}catch(Exception e){
log.info("response failed");
e.printStackTrace();
}
return null;
}
private void write(String rawResultString) { //Step 5
try{
//Write Interface
return;
}catch(Exception e){
log.info("write faild");
e.printStackTrace();
}
}
}
好了,至此,我们的子线程的框架机搭建好了,具体的五个函数还没有实现。
- read
- parser
- process
- response
- write
我们来好好思考一下每一步需要完成些什么工作。
第一步:读取原始的报文数据
private String read();//Step 1
读取报文之前,我们应该需要知道Http的报文格式,我们尝试打开现在的代码,然后试一下在浏览器向我们这个还没完成的服务器发送请求:http://localhost:8000/
如果我们在浏览器发出的请求一般都是GET请求了,对报文数据是在下一步,我们不关心客户给我们发来的数据是什么,我们在这个步骤值考虑把我们的数据流读入进服务器就是了。
因为HTTP的报文数据以“\r\n”作为分割,我们可以选择读取完所有的报文之后,再处理,也可以我们简单地读取第一行就行了,我们可以选择用BufferedReader的IO工具来处理,因为以行作为单位这样的话就更加方便了。
我们写出我们的read接口:
private String read(){//Step 1
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
try{
String infoRead = bufferedReader.readLine(); // We only get the line1 as: GET /index.html HTTP/1.1
log.info(infoRead);
return infoRead;
}catch(Exception e){
log.info("Read faild");
e.printStackTrace();
}
return null;
}
我这里偷懒没有Close IO,为了控制博文的长度,还是希望以后能加上。
第二步:解析报文
因为我们在上一步读取数据的时候偷懒只读一行了,因此,我们处理解析的时候也是一笔带过就是了,用空格分开GET /index.html HTTP/1.1
/**
* @param rawString: raw String from read()
* @return server command from client
*/
private String parser(String rawString) { //Step 2
try{
String[] split = rawString.split(" ");
if (split.length != 3) { //@example as: GET /index.html HTTP/1.1
throw new NullPointerException();
}
return split[1]; // Get path
}catch(Exception e){
log.info("Parser faild");
e.printStackTrace();
}
return null;
}
第三步:处理客户语义
通过上面的解析报文,我们已经知道了客户需要请求一个地址了。因此,我们根据可以给出的地址来从后台读取文件:
/**
* @param commandString: command string from parser
* @return
*/
private String process(String commandString) { //Step 3
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try{
log.info(commandString);//Show request in server panel
if(commandString.equals("/")){ //Default get resource
commandString = "index.html";
}
File file = new File("src/simple_webserver_lab/template/"+commandString);
fileReader = new FileReader(file);
bufferedReader = new BufferedReader(fileReader);
StringBuffer stringBuffer = new StringBuffer();
String line;
while((line = bufferedReader.readLine())!=null){
stringBuffer.append(line+"\r\n");
}
resultStatus = 200; //Success
responseLength = file.length(); // This is must be added for HTPP Header
String result = stringBuffer.toString();
return result;
}catch(Exception e){
resultStatus = 404;
log.info("Process failed");
e.printStackTrace();
}finally{
try{
bufferedReader.close();
fileReader.close();
}catch(Exception e){
resultStatus = 404;
log.info("Bad IO");
e.printStackTrace();
}
}
return null;
}
第四步:(巨坑)响应客户返回数据包
HTTP报头的content length是一个大坑
- 需要两次的\r\n
- 长度必须匹配!!!!
看这里:
<h1>File not found...</h1>
为毛后面写这些点,就是为了填补上不够数的字节,如果你差一个字节,都会不出来结果的,浏览器会报错的。
我们给出的数据只能比要求的content length要长!
我在前面定义了一个全局的响应长度:
long responseLength = 20; //@Attention!!! Error info length, Important
像上面如果你返回:<h1>Error</h1>
就会空白,因为你的数据不满足http报文的数据长度,因此我们会丢弃掉这个分组的数据了。
还有就是:HTTP/1.1 之间不要空格,否则 Firefox 系列的浏览器检查会出错哦。
响应部分的代码:
/**
* @param resultString
* @return raw response result
*/
private String response(String resultString) { // Step 4
try{
StringBuffer responseInfo = new StringBuffer();
switch(resultStatus){
case sucessStatus:{
responseInfo.append("HTTP/1.1 200 ok \r\n");
responseInfo.append("Content-Type:text/html \r\n");
responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
responseInfo.append(resultString);
break;
}
case noResourceStatus:{
responseInfo.append("HTTP/1.1 "+Integer.toString(noResourceStatus)+" file Not Found \r\n");
responseInfo.append("Content-Type:text/html \r\n");
responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
responseInfo.append("<h1>File not found...</h1>");
break;
}default:{
break;
}
}
return responseInfo.toString();
}catch(Exception e){
log.info("response failed");
e.printStackTrace();
}
return null;
}
第五步:写入Socket的数据流
这个就没啥好说的了
/***
* @param rawResultString
*/
private void write(String rawResultString) { //Step 5
try{
outputStream.write(rawResultString.getBytes());
outputStream.flush();
outputStream.close();
return;
}catch(Exception e){
log.info("write faild");
e.printStackTrace();
}
}
测试结果:
启动服务器:
public static void main(String[] args) {
Server server = new Server();
server.start(); //服务器的主线程启动
}
前提你要有一个index.html在你的指定目录。
假如我们访问根和指定访问index.html,都会显示:
后台打印:
假如,我们访问目录下的文本文件:17.xml,将会显示文本内容:
终端:
假如我们访问一个不存在的路径:
后台打印:
至此,我们撸完了一个简易的 Http 服务器了。
完整代码:
项目文件夹:
Server.java
package simple_webserver_lab;
import java.io.*;
import java.util.logging.Logger;
import java.net.*;
/**
* @author:JintuZheng
* @version: 1.0.00
*/
public class Server extends Thread {
int watchPort = 8000;
Logger log = Logger.getLogger("SERVER-LOG-JT");
public Server(){};
/**
* Server
* @param port: The server listen port
*/
public Server(int port) {
watchPort = port;
}
/**
* @apiNote: The child httpServer
*/
class httpServer extends Thread {
Socket currentSocket = null; //Client Socket
InputStream inputStream = null;//Use for get header
OutputStream outputStream = null;//Use for response
final static int sucessStatus = 200;//HTTP response header status code
final static int noResourceStatus = 404;//HTTP response header status code
int resultStatus = 404; //Our response code
long responseLength = 20; //@Attention!!! Error info length, Important
public httpServer(Socket socket) {
try {
currentSocket = socket; //Get socket bridge
//log.info(socket.getInetAddress().toString()); //Show ip address
inputStream = socket.getInputStream(); // Get inputstream
outputStream = socket.getOutputStream(); // Get outputstream
} catch (Exception e) {
log.info("Connection aborted");
}
}
// Read=>parser=>process=>response=>write
@Override
public void run() {
try {
String rawString = read(); // Get raw header
String serverCommand = parser(rawString); // Parser the raw header
String resultString = process(serverCommand); // Process command
String rawResultString = response(resultString); // Pack the result
write(rawResultString);// Write in responsed stream
currentSocket.close();// CLose the client socket
//End thread
} catch (Exception e) {
log.info("Failed connection");
e.printStackTrace();
}
}
private String read(){//Step 1
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
try{
String infoRead = bufferedReader.readLine(); // We only get the line1 as: GET /index.html HTTP/1.1
log.info(infoRead);
return infoRead;
}catch(Exception e){
log.info("Read faild");
e.printStackTrace();
}
return null;
}
/**
*
* @param rawString: raw String from read()
* @return server command from client
*/
private String parser(String rawString) { //Step 2
try{
String[] split = rawString.split(" ");
if (split.length != 3) { //@example as: GET /index.html HTTP/1.1
return null;
}
return split[1]; // Get path
}catch(Exception e){
log.info("Parser faild");
e.printStackTrace();
}
return null;
}
/**
*
* @param commandString: command string from parser
* @return
*/
private String process(String commandString) { //Step 3
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try{
log.info(commandString);//Show request in server panel
if(commandString.equals("/")){ //Default get resource
commandString = "index.html";
}
File file = new File("src/simple_webserver_lab/template/"+commandString);
fileReader = new FileReader(file);
bufferedReader = new BufferedReader(fileReader);
StringBuffer stringBuffer = new StringBuffer();
String line;
while((line = bufferedReader.readLine())!=null){
stringBuffer.append(line+"\r\n");
}
resultStatus = 200;
responseLength = file.length(); // This is must be added for HTPP Header
String result = stringBuffer.toString();
return result;
}catch(Exception e){
resultStatus = 404;
log.info("Process failed");
e.printStackTrace();
}finally{
try{
bufferedReader.close();
fileReader.close();
}catch(Exception e){
resultStatus = 404;
log.info("Bad IO");
e.printStackTrace();
}
}
return null;
}
/**
*
* @param resultString
* @return raw response result
*/
private String response(String resultString) { // Step 4
try{
StringBuffer responseInfo = new StringBuffer();
switch(resultStatus){
case sucessStatus:{
responseInfo.append("HTTP/1.1 200 ok \r\n");
responseInfo.append("Content-Type:text/html \r\n");
responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
responseInfo.append(resultString);
break;
}
case noResourceStatus:{
responseInfo.append("HTTP/1.1 "+Integer.toString(noResourceStatus)+" file Not Found \r\n");
responseInfo.append("Content-Type:text/html \r\n");
responseInfo.append("Content-Length:"+Long.toString(responseLength)+" \r\n\r\n");
responseInfo.append("<h1>File not found...</h1>");
break;
}default:{
break;
}
}
return responseInfo.toString();
}catch(Exception e){
log.info("response failed");
e.printStackTrace();
}
return null;
}
/***
*
* @param rawResultString
*/
private void write(String rawResultString) { //Step 5
try{
outputStream.write(rawResultString.getBytes());
outputStream.flush();
outputStream.close();
return;
}catch(Exception e){
log.info("write faild");
e.printStackTrace();
}
}
}
@Override
public void run() {
super.run();
try {
@SuppressWarnings("resource") // Warnings iggnore
ServerSocket serverSocket = new ServerSocket(watchPort);
while (true) { // Waitting for clients
Socket socket = serverSocket.accept();// Thread Join here
new httpServer(socket).start();// If connected, start a new server thread
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}