先想想需要達到怎樣的要求:
本來這是一個很小的課程設計作業,老師也是要求能達到簡單的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();
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/237691.html
標籤:其他
上一篇:C語言編程>第五周 ⑦ 求1-1000中能被7或能被11,但不能同時被7和11整除的數。每10個為一行顯示。
下一篇:作業系統IO模式知識整理
