我們經常使用别人的伺服器進行建構網站,現在我們就自己來寫一個自己的服務來使用。
準備工作:下載下傳所需的題材及文檔
注:完整項目下載下傳
一、request請求擷取
1、了解request請求
在寫伺服器之前,我們需要知道用戶端發送給我們哪些資訊?以及要求我們傳回哪些資訊?經過測試我們能夠知道使用者用戶端發送的資訊有以下幾點:
用戶端發送到伺服器端的請求消息,我們稱之為請求(request),其實就是一個按照http協定的規則拼接而成的字元串,Request請求消息包含三部分: 請求行 消息報頭 請求正文
第一部 請求行
格式:
Method Request-URI HTTP-Version CRLF
各部分分别為:
Method表示請求方法;一般為GET或者POST ;Request-URI是一個統一資源辨別符; HTTP-Version表示請求的HTTP協定版本; CRLF表示回車和換行
例如:
GET /test.html HTTP/1.1
第二部 消息報頭 http header
例如:
GET /test.html HTTP/1.1
Host: 127.0.0.1:9999
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,en;q=0.8,zh;q=0.5,en-US;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
第三部 請求正文 http body
請求頭和請求正文之間是一個空行,這個行非常重要,它表示請求頭已經結束,接下來的是請求正文。請求正文中可以包含客戶送出的字元串資訊
注意:在第二部分header和第三部分body之間有個空行,除非沒有請求正文(如果你想要親自看到效果,請參考:浏覽器中GET和POST的差別),這是因為使用者在浏覽網頁時送出給伺服器的資訊是不同的
2、實作
經過以上分析,我們就能夠清楚的知道,用戶端發送給伺服器的請求,請求資訊有使用的協定、請求的方法、請求的資源路徑、請求的消息報頭、判斷請求的内容是否為靜态資源、判斷請求的内容是否為動态資源、判斷是否為空請求,為了使用的友善,我們需要将其封裝起來,總不能使用一次讀取一次吧,這樣做實在是太浪費系統資源與時間了,如下代碼,就是一個接口類,用于擷取用戶端發送過來的屬性
package com.sample.http;
import java.util.Map;
// http協定的請求
public interface HttpRequest {
//獲得請求的協定
public String getProtocol();
//獲得請求的方法
public String getRequestMethod();
//獲得請求的路徑
public String getRequestPath();
//獲得請求的消息報頭
public Map<String,String> getRequestHeader();
//根據參數的名字獲得請求帶過來的參數值
public String getParameter(String parameterName);
//判斷目前請求的否是靜态資源
public boolean isStaticResource();
//判斷目前請求的否是動态資源
public boolean isDynamicResource();
//判斷目前請求的否是為空請求(有些浏覽器會自動發送空請求)
public boolean isNullRequest();
}
有了接口類之後,我們就可以建立類進行實作,下面就是實作類,用于對各個方法進行處理:
package com.sample.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class HttpRequestImpl implements HttpRequest{
//用戶端的Socket
private Socket s;
private InputStream is=null;//輸入流
private BufferedReader br=null;
private HashMap<String,String> hmHeader=new HashMap<String,String>();//消息報頭
private HashMap<String,String> hmparameters=new HashMap<String, String>();//參數集合
private boolean isNullRequest=false;//是否為空請求,預設false
private String protocol=null;//請求協定
private String requestMethod=null;//請求方法
private String requestPath=null;//請求路徑
public HttpRequestImpl(Socket s) {
this.s=s;
getInfos();//調用方法
}
private void getInfos()//定義一個方法,用于處理擷取的用戶端資訊
{
try {
is=s.getInputStream();
br=new BufferedReader(new InputStreamReader(is));
//解析第一行
String str;
str=br.readLine();//readLine使用回車換行判斷一行是否結束
if(str==null)
{
isNullRequest=true;
return;
}
parseRequestMethodPathProtocol(str);//調用自己建立在本類裡邊的方法處理第一行資訊,方法在後面
//解析第二行---第八行
String header=null;
//判斷是否結束,如果結束就退出,這裡的判斷按較為饒人
//首先應該明确br.readLine()的内容,當為true是對應的情況
//也就是說當“”(中間沒有空格)與br.readLine()相等時,就進入到while中
while(!"".equals((header=br.readLine()))){
parseRequestHeader(header);
}
//post和get
if(br.ready())//post//POST送出方式判斷,如果還有下一行就繼續讀取資訊
{
char[] buf=new char[1024];
int len=br.read(buf);//使用位元組進行讀取,因為這一行沒有回車換行,readLine無法判斷是否結束
String parameter=new String(buf,0,len);
parseRequestParamByPost(parameter);//調用自己建立在本類裡邊的方法處理POST方式送出的正文資訊
}
else
{//get,get參數的位置在第一行連接配接處
parseRequestParamByGet(requestPath);//調用自己建立在本類裡邊的方法處理GET方式送出的正文資訊
}
} catch (Exception e) {
e.printStackTrace();
}
}
//GET方法處理
private void parseRequestParamByGet(String requestPath2) {
String []str1=requestPath2.split("[?]");//使用“?”分割字元串
if(str1.length==2)
{
parseRequestParamByPost(str1[1]);
}
this.requestPath=str1[0];
}
//POST方法處理
private void parseRequestParamByPost(String parameter) {
String[] strs=parameter.split("&");
if(strs.length>=1)
{
for(String str:strs)
{
String [] sp=str.split("=");
hmparameters.put(sp[0],sp[1]);
}
}
}
//解析第二行到第八行
private void parseRequestHeader(String header) throws Exception{
String[] headHost=header.split(": ");
if(headHost.length!=2)
{
throw new Exception("消息報頭異常,請重新送出");
}
hmHeader.put(headHost[0],headHost[1]);
}
//解析第一行
private void parseRequestMethodPathProtocol(String str) throws Exception {
String[] protocolMethodPath=new String[3];//由于第一行含有三個内容,分割後需要三個String存儲
protocolMethodPath=str.split(" ");//使用空格分割
if(protocolMethodPath.length==3)
{
requestMethod=protocolMethodPath[0];
requestPath=protocolMethodPath[1];
protocol=protocolMethodPath[2];
}
else
{
throw new Exception("首行參數不合适,請重新送出");
}
}
//獲得請求的協定
public String getProtocol()
{
return protocol;
}
//獲得請求的方法
public String getRequestMethod(){
return requestMethod;
}
//獲得請求的路徑
public String getRequestPath(){
return requestPath;
}
//獲得請求的消息報頭
public Map<String,String> getRequestHeader(){
return this.hmHeader;
}
//根據參數的名字獲得請求帶過來的參數值
public String getParameter(String parameterName){
return hmparameters.get(parameterName);
}
//判斷目前請求的否是靜态資源
public boolean isStaticResource(){
return true;
}
//判斷目前請求的否是動态資源
public boolean isDynamicResource(){
return true;
}
//判斷目前請求的否是為空請求(有些浏覽器會自動發送空請求)
public boolean isNullRequest(){
return isNullRequest;
}
}
以上内容是對用戶端(浏覽器)請求内容的處理,即如何進行包裝用戶端請求的資訊,并且将其包裝成一個統一的整體,既然能夠擷取浏覽器的内容,那麼,我們就必須采取一定的措施告訴浏覽器我們找到了你想要的檔案,并且将傳回給用戶端,下面我們就來實作如何傳回給用戶端想要的資訊
二、response響應處理
1、了解response響應
伺服器在接收和解釋用戶端的請求消息後,伺服器會傳回給用戶端一個HTTP響應消息,我們稱之為響應(response)。其實也是一個按照http協定的規則拼接而成的一個字元串
HTTP響應也是由三個部分組成,分别是: 響應狀态行、消息報頭、響應正文
第一部分 響應狀态行
格式如下:
HTTP-Version Status-Code Reason-Phrase CRLF
例如:
HTTP/1.1 200 OK
各部分分别為:
HTTP-Version表示伺服器HTTP協定的版本;
Status-Code表示伺服器發回的響應狀态代碼;
Reason-Phrase表示狀态代碼的文本描述。
CRLF表示回車和換行
第二部分 消息報頭
HTTP消息報頭包括普通報頭、請求報頭、響應報頭、實體報頭這四大類。
每一個 報頭域 都是由 名字+冒号+空格+值 組成,消息報頭域的名字不區分大小寫。它們的作用是描述 用戶端或者伺服器 的屬性
1.普通報頭:即可用于請求,也可用于響應,是作為一個整體而不是特定資源與事務相關聯。
2.請求報頭:允許用戶端傳遞關于自身資訊和希望的響應形式。
3.響應報頭:允許伺服器傳遞關于自身資訊的響應。
4.實體報頭:定義被傳送資源的資訊。即可用于請求,也可用于響應。
什麼是 MIME Type?
首先,我們要了解浏覽器是如何處理内容的。在浏覽器中顯示的内容有 HTML、有 XML、有 GIF、還有 Flash ……那麼,浏覽器是如何區分它們,決定什麼内容用什麼形式來顯示呢?答案是 MIME Type,也就是該資源的媒體類型。媒體類型通常是通過 HTTP 協定,由 Web 伺服器告知浏覽器的,更準确地說,是通過響應的消息報頭裡面的屬性 Content-Type 來表示的,例如:Content-Type: text/HTML表示内容是 text/HTML 類型,也就是超文本檔案。為什麼是“text/HTML”而不是“HTML/text”或者别的什麼?MIME Type 不是個人指定的,是經過 ietf 組織協商,以 RFC 的形式作為建議的标準釋出在網上的,大多數的 Web 伺服器和使用者代理都會支援這個規範 (順便說一句,Email 附件的類型也是通過 MIME Type 指定的)。
通常隻有一些在網際網路上獲得廣泛應用的格式才會獲得一個 MIME Type,如果是某個用戶端自己定義的格式,一般隻能以 application/x- 開頭。XHTML 正是一個獲得廣泛應用的格式,是以,在 RFC 3236 中,說明了 XHTML 格式檔案的 MIME Type 應該是 application/xHTML+XML。當然,處理本地的檔案,在沒有人告訴浏覽器某個檔案的 MIME Type 的情況下,浏覽器也會做一些預設的處理,這可能和你在作業系統中給檔案配置的 MIME Type 有關。比如在 Windows 下,打開系統資料庫的“HKEY_LOCAL_MACHINESOFTWAREClassesMIMEDatabaseContent Type”主鍵,你可以看到所有 MIME Type 的配置資訊
每個MIME類型由兩部分組成,前面是資料的大類别,例如聲音audio、圖象image等,後面定義具體的種類。
常見的MIME類型
超文本标記語言文本 .html,.html text/html
普通文本 .txt text/plain
RTF文本 .rtf application/rtf
GIF圖形 .gif image/gif
JPEG圖形 .ipeg,.jpg image/jpeg
au聲音檔案 .au audio/basic
MIDI音樂檔案 mid,.midi audio/midi,audio/x-midi
RealAudio音樂檔案 .ra, .ram audio/x-pn-realaudio
MPEG檔案 .mpg,.mpeg video/mpeg
AVI檔案 .avi video/x-msvideo
GZIP檔案 .gz application/x-gzip
TAR檔案 .tar application/x-tar
第三部分 響應正文
響應正文就是伺服器傳回的資源的内容
2、實作
首先,我們需要進行抽象,即将浏覽器想要的資訊,即如下内容包裝起來,如下所示,我們将其包裝成一個接口,在抽象時我們必須認識到使用者可能會犯的錯誤,是以盡量使用重載的方法進行避免,在下面的接口中,使用了重載進行處理部分方法:
package com.sample.http;
import java.io.OutputStream;
import java.io.PrintWriter;
//http協定的響應
public interface HttpResponse {
//獲得一個指向用戶端的位元組流
public OutputStream getOutputStream()throws Exception;
//獲得一個指向用戶端的字元流
public PrintWriter getPrintWriter()throws Exception;
//設定響應的狀态行 參數為String類型
public void setStatusLine(String statusCode);
//設定響應的狀态行 參數為int類型
public void setStatusLine(int statusCode);
//設定響應消息報頭
public void setResponseHeader(String hName,String hValue);
//設定響應消息報頭中Content-Type屬性
public void setContentType(String contentType);
//設定響應消息報頭中Content-Type屬性 并且同時設定編碼
public void setContentType(String contentType,String charsetName);
//設定CRLF 回車換行 \r\n
public void setCRLF();
//把設定好的響應狀态行、響應消息報頭、固定空行這三部分寫給浏覽器
public void printResponseHeader();
//把響應正文寫給浏覽器
public void printResponseContent(String requestPath);
}
在接口中,我們能夠看到詳細的解釋,下面我們就來實作接口中的方法:
package com.sample.http;
<pre name="code" class="java">import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import com.sample.utils.ConfigUtils;
import com.sample.utils.StatusCodeUtils;
//http協定的響應
public class HttpResponseImpl implements HttpResponse {
//聲明初始變量
Socket s;//用戶端socket
OutputStream os;//輸出端位元組流
BufferedWriter bw;//輸出端字元流
PrintWriter pw;
StringBuffer sbuffer;//放狀态行,\r\n ,
FileInputStream fis;
File file;
//構造器
public HttpResponseImpl(Socket s) {
this.s=s;
System.out.println("HttpRequestImpl(Socket s)");
os=null;
bw=null;
pw=null;
sbuffer=new StringBuffer();//初始化,記得,否則以下的操作會遇見空指針異常
fis=null;
file=null;
getInfos();
}
private void getInfos() {
try {
os=s.getOutputStream();
bw=new BufferedWriter(new OutputStreamWriter(os,"GBK"));
pw=new PrintWriter(bw);
} catch (Exception e) {
e.printStackTrace();
}
}
//獲得一個指向用戶端的位元組流
public OutputStream getOutputStream()throws Exception
{
return os;
}
//獲得一個指向用戶端的字元流
public PrintWriter getPrintWriter()throws Exception
{
return pw;
}
//設定響應的狀态行 參數為String類型
public void setStatusLine(String statusCode)
{
String str=StatusCodeUtils.getStatusCodeValue(statusCode);
//System.out.println(str+"------"+str.length());
sbuffer.append("HTTP/1.1 "+statusCode+" "+str);
setCRLF();
}
//設定響應的狀态行 參數為int類型
public void setStatusLine(int statusCode)
{
setStatusLine(statusCode+"");//将int類型轉化為String類型
}
//設定響應消息報頭
public void setResponseHeader(String hName,String hValue)
{
sbuffer.append(hName+": "+hValue);
setCRLF();
}
//設定響應消息報頭中Content-Type屬性
public void setContentType(String contentType)
{
setResponseHeader("Content-Type",contentType);
}
//設定響應消息報頭中Content-Type屬性 并且同時設定編碼
public void setContentType(String contentType,String charsetName)
{//text/html;charset=utf-8
setContentType(";charset= "+charsetName);
}
//設定CRLF 回車換行 \r\n
public void setCRLF()
{
sbuffer.append("\r\n");
}
//把設定好的響應狀态行、響應消息報頭、固定空行這三部分寫給浏覽器
public void printResponseHeader()
{
//設定setResponseLine,setResponseHeader,setResponseType
String res=sbuffer.toString();
pw.print(res);
pw.flush();
}
//把響應正文寫給浏覽器
public void printResponseContent(String requestPath)
{
//響應正文
String getPath= requestPath;//客戶請求的位址
String webHome=(new ConfigUtils()).getConfigValue("rootPath");
System.out.println("配置檔案中目錄:"+webHome);//輸出從配置檔案中擷取的位址
file=new File(webHome+getPath);
if(file.exists())//如果檔案存在就執行
{
try {
fis=new FileInputStream(file);
byte[] buf=new byte[1024];
int len=-1;
while((len=fis.read(buf))!=-1)
{
//String str=buf.toString();
//bw.write(str);//字元流寫過去是一個位址,因為寫過去之後需要浏覽器解析,如果是圖檔或者其他(圖檔或視訊是位元組流)的該怎麼解析呢?
//System.out.println(str);
os.write(buf, 0, len);
}
bw.flush();
os.flush();//os要不要關???
} catch (IOException e) {
e.printStackTrace();
}finally
{
try {
if(bw!=null)
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
在Eclipse中寫完以上代碼我們會發現,其中有多處錯誤資訊,其原因是我們沒有進行建立以上代碼所要求的類,現在我們進行建立,其使用方法請參見:java.util 類 Properties ,使用參考中的方法,我們能夠進行對所需要的資訊進行配置,在以上代碼中使用的地方有兩處,分别是【注意:這樣做的好處是增減了項目的靈活性,使用者能夠在不檢視代碼的情況下随時更改配置檔案等一些檔案的資訊,】:
(1)設定狀态行處
//設定響應的狀态行 參數為String類型
public void setStatusLine(String statusCode)
{
String str=StatusCodeUtils.getStatusCodeValue(statusCode);
//System.out.println(str+"------"+str.length());
sbuffer.append("HTTP/1.1 "+statusCode+" "+str);
setCRLF();
}
其中StatusCodeUtils類建立如下所示,而對于status_code.properties檔案存放在下載下傳的準備檔案中的/webservlet/project/中,直接複制到項目中即可:
package com.sample.utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class StatusCodeUtils {
private static Properties p;
static
{
InputStream in=null;
p=new Properties();
try {
//讀了xx.properties檔案
in=StatusCodeUtils.class.getResourceAsStream("status_code.properties");
//放置到p中,即放properties檔案中的key,value
p.load(in);
} catch (IOException e) {
e.printStackTrace();
}
finally
{
if(in!=null)
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String getStatusCodeValue(String status)
{
return p.getProperty(status);
}
public static String getStatusCodeValue(int status)
{
return getStatusCodeValue(status+"");//沒有空格
}
/*public static void main(String[] args) {//輸出測試
//Properties p=new Properties();
// p.setProperty("rootPath","ddd");
//System.out.println(p.get("rootPath"));
System.out.println(getStatusCodeValue("304"));
System.out.println(getStatusCodeValue("200"));
}*/
}
(2)響應正文處:
//響應正文
String getPath= requestPath;//客戶請求的位址
String webHome=(new ConfigUtils()).getConfigValue("rootPath");
System.out.println("配置檔案中目錄:"+webHome);//輸出從配置檔案中擷取的位址
file=new File(webHome+getPath);
在響應正文中使用了ConfigUtils類進行了項目路徑的擷取,代碼如下所示,對于config.properties(注意:此檔案中檔案路徑應該注意,我使用的是Linux系統,檔案結構是/home/***,而對于windows系統,目錄結構為:"C://webapps/*****,最好在位址欄複制位址,寫到配置中")檔案也在準備檔案中,請自行下載下傳,然後複制到項目中:就是下面這個東西,路徑配置合适,然後你就可以将自己的項目放在webapps目錄下,讓自己的電腦作為伺服器供其他人通路自己的網站了

ConfigUtils路徑配置類,用于擷取項目檔案目錄位置
package com.sample.utils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
public class ConfigUtils {
private static Properties p;
static
{
InputStream in=null;
OutputStream on=null;
p=new Properties();
try {
//讀了xx.properties檔案
in=ConfigUtils.class.getResourceAsStream("config.properties");
//放置到p中,即放properties檔案中的key,value
p.load(in);
} catch (IOException e) {
e.printStackTrace();
}
finally
{
if(in!=null)
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String getConfigValue(String config)
{
return p.getProperty(config);
}
public static void main(String[] args) {//輸出測試
// Properties p=new Properties();
// p.setProperty("rootPath","ddd");
// System.out.println(p.get("rootPath"));
System.out.println(getConfigValue("rootPath"));
}
}
到此為止,我們已經實作了伺服器的主要任務,接受請求和處理請求,下面我們進行測試:
寫一個測試類如下:
package com.sample.http;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class ServerTest {
public static void main(String[] args) {
//聲明變量
ServerSocket ss=null;
Socket s=null;
boolean flag=true;
try {
int port=10002;
System.out.println("Server Port:"+port);
ss=new ServerSocket(port);
//while(flag)
{
//接受用戶端發送過來的Socket
s=ss.accept();
HttpRequestImpl request=new HttpRequestImpl(s);
// 用于測試收到的資訊
System.out.println("擷取的路徑:"+request.getRequestPath());
System.out.println("擷取的:"+request.getProtocol());
System.out.println("擷取的:"+request.getRequestMethod());
System.out.println(request.getParameter("name"));
System.out.println(request.getParameter("id"));
Map<String,String> m=request.getRequestHeader();
Set<Entry<String,String>> set=m.entrySet();
Iterator it=set.iterator();
while(it.hasNext())
{
Entry entry=(Entry<String, String> )it.next();
System.out.println(entry.getKey()+"----"+entry.getValue());
}
//寫響應給浏覽器
/*
* 封裝:
* */
//輸出流
HttpResponseImpl response=new HttpResponseImpl(s);
response.setStatusLine(200);
response.setContentType("text/html");
response.printResponseHeader();
response.setCRLF();
response.printResponseHeader();
response.printResponseContent(request.getRequestPath());
//用于輸出資訊
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在浏覽器中輸入位址回車:http://127.0.0.1:10002/test.html?id=1212&name=suguniang ,能夠看到浏覽器解析後的界面,當其他電腦通路時(其他電腦指的是同一個域内的),隻要将127.0.0.1修改為本地的ip位址即可
此時控制台上也輸出相應的資訊: