博客地址首發(fā):http://www.icodeyou.com
之前做web應(yīng)用一直是在本地裝個Apache、Tomcat之類的軟件,然后把做好的網(wǎng)頁文件放在他們的工作目錄下(如Apache的htdocs),然后打開瀏覽器輸入127.0.0.1或localhost就可以直接訪問了,好神奇,可是為什么,怎么實現(xiàn)的呢,早就知道有Socket(套接字)這個東西,可之前就是沒有把這兩方面結(jié)合起來,今天我們就一起來看一看這究竟是為什么。
有同學(xué)說還不懂Socket是什么,這東西很抽象,在計算機網(wǎng)絡(luò)原理里講協(xié)議時才會看到,今天咱們完全忽略太嚴(yán)謹(jǐn)、學(xué)術(shù)的定義,就來看看Socket到底是什么。想象一下,你把電腦的電源插在插座上,你的電腦就可以使用了,為什么?“這不是廢話嗎!”確實,咱們來想一下這個過程,你拿著插頭插在了插座上,然后你的電腦和千里之外的供電廠就能“通信”了,把你的電腦想成是客戶端,把千里之外的供電廠想成是服務(wù)器,通過插座和很長很長的線纜你們就可以勾搭上了,那么Socket在這其中相當(dāng)于什么呢?“插座!”沒錯,就是插座!對于我的電腦來說,我想讓它通電工作,我只需要個插座就行了啊,什么插座是什么材質(zhì)的,線纜是什么型號的,供電廠到底在什么經(jīng)緯度,電力到底怎么傳輸,我管它干嘛呢,都跟我沒關(guān)系!我只要知道我需要的不是整個世界,而是。。。一個插座!讀到這里,想必同學(xué)已經(jīng)對“插座”有了很森的理解了;再舉一個例子,你和基友的電腦通過有線的方式連上了同一個路由器,這個時候你們就可以直接通過內(nèi)網(wǎng)IP地址進行訪問了,在這個過程中,那個方方的接口(RJ45接口)就是“Socket”,反正插上“Socket”就能用,我不用管到底通過Socket怎么能夠?qū)崿F(xiàn)通信。在計算機編程的網(wǎng)絡(luò)世界里,作為應(yīng)用程序,我只需要一個“插座”就可以和任何服務(wù)器通信了,想想都有點小激動呢~~~
接下來要講的就是,電腦電源需要一個socket去插上,那么發(fā)電廠呢,也同樣需要一個插座插上去來給你供電——也就是說,發(fā)電廠需要一個“插座”!。。。廢話,,,,沒錯,確實是這樣,服務(wù)器端也需要一個“插座”,只不過它叫做ServerSocket(這看起來像是繼承自Socket,我也不知道,待查)。
有了“插座”(Socket)的概念之后,我們就可以愉快地讓電腦(客戶端)與發(fā)電廠(服務(wù)器)通信了。無論是客戶端還是服務(wù)器,都需要Socket,鑒于咱們今天的題目是“搭建web服務(wù)器”,所以咱們接下來就來看一下怎么創(chuàng)建服務(wù)器的ServerSocket。說道這里,有同學(xué)就會問到了,“難道客戶端不需要Socket嗎?”,確實需要,因為我們是用瀏覽器訪問本機IP“127.0.0.1”,所以客戶端的Socket就由瀏覽器自己維護了,不需要咱們動手寫的?!翱墒俏疫€是不明白為什么在瀏覽器里輸入127.0.0.1之后就可以看到我的網(wǎng)頁了?求解釋” 好,那咱們慢慢來,先動手編寫一個服務(wù)器端的ServerSocket吧啦啦啦~
創(chuàng)建服務(wù)器端Socket的步驟如下:
1、創(chuàng)建ServerSocket對象
ServerSocket serverSocket = new ServerSocket(“80”);
//這里只需要指明當(dāng)前程序監(jiān)聽80號端口就可以了,至于為什么是80,因為我喜歡!“好霸道。。。”因為我們要監(jiān)聽web請求,默認就是80號端口。其實,1-1024端口被操作系統(tǒng)占用了,1025-65535的端口你隨便用,只要不會和其他應(yīng)用程序沖突就可以(別用什么類似3389這么常用的端口就好了。。。)
2、作為服務(wù)器,我要知道,我的使命就是要等待客戶端發(fā)來請求,也就是客戶端發(fā)來Socket,我首先要把它Hold??!
Socket socket = serverSocket.accept();
//這里需要特別說明一下,accept方法比較特殊,它是一個阻塞方法(block method),因為只要它等不來客戶端發(fā)來的請求(Socket),它就一直等下去而不會繼續(xù)執(zhí)行它下面的代碼。唉,此等癡情人怎么跟我一樣O(∩_∩)O
3、客戶端要向我表白,給我發(fā)來情書,那我作為服務(wù)器只要得到它的輸入就好了
InputStream inputStream = socket.getInputStream();
//注意,客戶端發(fā)來的表白信息都在socket里面,而不是serverSocket里面,這點要是弄錯了,讀不到情書內(nèi)容,活該你單身。(我只有冷笑。。。)
4、收到了情書,我好想知道里面究竟寫了什么啊!迫!不!及!待! 好,開始解析情書內(nèi)容
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//java包裝類,只為讀到寫給我的情書,耶~
String line = “”;
while ( (line = reader.readLine()) != null ){
System.out.println(line);
}
5、組裝前4步的代碼,會要求try catch一下異常,正常捕獲就好 下面貼代碼
public class MultiWebServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(80);
System.out.println("正在等待情書中...");
Socket socket = serverSocket.accept();
System.out.println("收到情書,我要開始解析!");
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = "";
while ( (line = reader.readLine()) != null ){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
好了,服務(wù)器端的代碼咱們寫完了,那接下來干啥?不知道。。。不過還記得剛才提出的問題嗎——“可是我還是不明白為什么在瀏覽器里輸入127.0.0.1之后就可以看到我的網(wǎng)頁了?”那就試試唄,看看咱們?nèi)绻跒g覽器里輸入127.0.0.1或者localhost會怎么樣
首先必須把剛才咱們編寫的服務(wù)器端程序運行起來,然后再打開瀏覽器,記住,必須先運行服務(wù)器端程序,不然情書就發(fā)丟了。。。運行服務(wù)器端程序,如圖:
注意紅圈中的兩點:由于此時沒有客戶端發(fā)來情書,還記得剛才的accept()阻塞方法嗎,它就一直等啊等,等不來我還等,所以紅圈中會顯示“正在等待情書中…”;那么右面那個箭頭指向的是什么意思呢,一個紅色停止的圖標(biāo),也就是說,這個程序現(xiàn)在一直在執(zhí)行著,沒有結(jié)束,就好像死循環(huán)一樣(當(dāng)然這里絕對不是死循環(huán),其實是阻塞,只是死的樣子好像死循環(huán),一會咱們會談到死循環(huán)的,別著急,遲早會死的)
接下來打開瀏覽器,在地址欄輸入127.0.0.1/index.html后回車,看看瀏覽器什么反應(yīng)。。。。。一段時間過去了,瀏覽器居然一點反應(yīng)都沒有,然后告訴我該頁無法顯示。我去。。。難道講了這么多咱們就這么失敗了嗎,我哭。那就打開eclipse看一眼吧,看看服務(wù)器端有沒有什么動靜啊。打開服務(wù)器端一看,臥槽,瞬間世界向我問好了!
注意看紅筆標(biāo)注,我收到了情書!我要開始解析了!那到底情書里是什么內(nèi)容,別問我,繼續(xù)向下看?!昂檬煜さ囊欢螆笪?,我們好像在哪見過,還記得嗎,那是一個春天,你剛發(fā)芽兒。。?!睕]錯,這就是計算機網(wǎng)絡(luò)原理講的http請求報文。沒有學(xué)過計網(wǎng)怎么辦,沒關(guān)系,看前兩行(其實我們一會用到的也只有第一行而已),“我看到了index.html” 是的,這是剛才我們在瀏覽器里面輸入的地址;第二行,“我也看到了127.0.0.1”,是的,也是我們剛剛在瀏覽器里面輸入的。這說明了什么?激動的我無法說出這到底說明了什么,但想必讀者你已經(jīng)揣測出了什么。
寫到這里,作為服務(wù)器的我已經(jīng)收到了從客戶端發(fā)來的情書,那客戶端(瀏覽器)為什么一點反應(yīng)都沒有呢,甚至過了一會就“該頁無法顯示”了。因為啊,人家給你寫了情書,你沒回復(fù)人家,人家等了一會覺得沒戲了就傷心欲絕了!是啊,喜不喜歡人家都要和他說一聲的,給他個答復(fù),哪怕只說:“對不起,你是個好人。。?!?br> 走神了吧?好像說到自己了吧?回來吧,咱們現(xiàn)在的任務(wù)呢,就是怎么給人家個答復(fù)。
怎么給,怎么給,怎么給。。。快想快想,既然人家都指明了想和127.0.0.1里的“index.html”表白,那當(dāng)然就得由index.html來給他答復(fù)嘍。怎么答,怎么答,怎么答。。??煜肟煜耄热籭ndex.html是個文件,那我讀出文件內(nèi)容后直接發(fā)給客戶端不就行了嗎?可是用什么發(fā)?沒錯,是socket!我們用socket把文件的內(nèi)容返回給客戶端就好了。
那么問題來了。。?!罢f的非常好,關(guān)鍵是怎么做!”——首先怎么讀出文件來?
假定咱們的index.html在我電腦的E://課件/計算機網(wǎng)絡(luò)原理/實驗/實驗1/ 文件夾下,并且假定不會跨域訪問,則:
1、定義一個字符串,用來存咱們的工作目錄
String base_url = "E://課件/計算機網(wǎng)絡(luò)原理/實驗/實驗1/";
//這只是我本機的目錄,至于到了你的電腦上,你可以自己更改</span>
2、我怎么通過報文知道客戶端要和index.html表白?看情書第一行 GET /index.html HTTP/1.1,所以只需要獲取情書的第一行字符串并解析出index.html就好辦啦,easy,開始吧
//由于目前只需要第一行,所以咱們就不像上面那樣循環(huán)讀取了,讀一行就夠了
String line = reader.readLine();
//用字符串截取函數(shù),把“index.html”這個字符串給揪出來
String url = line.substring(5, line.indexOf("HTTP") - 1);
3、所以咱們index.html的絕對路徑就是 base_url + url 了,終于把我愛的人從人山人海中找到了,看看她怎么答復(fù)我吧——獲取文件內(nèi)容
inputStream = new FileInputStream(base_url + url);
OutputStream outputStream = socket.getOutputStream(); //我要從服務(wù)器給客戶端答復(fù)了,對于服務(wù)器來說,這是發(fā)出去的內(nèi)容,所以是Out!
byte[] buffer = new byte[4 * 1024]; //定義字節(jié)緩沖區(qū)
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len); //很重要!通過socket的outputStream把咱們解析出來的文件內(nèi)容一字不落的發(fā)出去 如果沒寫這個,導(dǎo)致你愛的跟你表白的抑郁而死,活該你單身
}
outputStream.flush(); //如果最后一次write時沒有把buffer寫滿,是不會自動發(fā)出去的,需要調(diào)用flush方法強制把內(nèi)容從緩沖區(qū)發(fā)出去
好了,文件讀取出來了,也返回給客戶端了,親愛噠他能收到嗎?還是一樣,務(wù)必先運行服務(wù)器端程序,然后打開瀏覽器輸入127.0.0.1/index.html 后回車。我緊張,我激動,能不能收到回復(fù),會給我什么樣的回復(fù)?如圖。。。
為什么會這樣????。。?好吧,看看女神的index.html文件里都寫了些什么。。。
<html>
<head>
<meta charset="utf-8" />
<title>Welcome</title>
</head>
<body>
<h1>王歡,你是個好人... </h1>
</body>
</html>
看到這里,我到底應(yīng)該高興還是欲絕。。。高興的是,我女神給我答復(fù)了;欲絕的是。。。那么問題來了,,,學(xué)表白技術(shù)哪家強?
玩笑歸玩笑,那我們的針對這次的淺談題目是不是就完成了?可以說是的,但是我表白一次失敗就算了?我還要表白第二次?。ㄆ鋵嵨业共皇沁@樣的,這里只能犧牲我的人品來為了大家更好的理解了,呵呵)。好吧,我剛才的工作目錄下還有個another.html,這次我來跟她表白吧!好!繼續(xù)在瀏覽器中輸入127.0.0.1/another.html后回車,期待這次會表白成功??墒俏业劝〉龋瑸g覽器在那里打圈圈,難道瀏覽器都知道我太花心了,拒絕幫我傳遞情書?好吧,我再打開瀏覽器試一下,輸入127.0.0.1/index.html ,嗯?連第一個女神都不理我了?!我靠!為毛!
沖動是魔鬼!冷靜!我打開eclipse控制臺,發(fā)現(xiàn)服務(wù)器根本就沒有“正在等待情書中…”,所以我拜托瀏覽器發(fā)過去的情書當(dāng)然就發(fā)丟了,因為根本沒人在接收啊。(竊喜,還好不是因為我太花心了所以瀏覽器沒有幫我投遞情書)可是為什么呢?
冷靜吧,分析代碼。其實我們可以想到,這段代碼執(zhí)行完一次后不就結(jié)束了嗎,那我第二次給她發(fā)請求她當(dāng)然會收不到了。對啊,那為了解決這個問題,怎么辦呢?跪求紅娘支招!
紅娘說:“給服務(wù)器程序個死循環(huán)吧,讓她反復(fù)在等客戶端的請求就好了?!保ㄆ鋵嵓t娘就一直在死循環(huán)中)
紅娘果然是紅娘(不然是誰。。。),那就按照她的說法試一試唄!改代碼,加入 while (true) 死循環(huán):
public class MultiWebServer {
public static void main(String[] args) {
String base_url = "E://課件/計算機網(wǎng)絡(luò)原理/實驗/實驗1/";
while (true) {
try {
ServerSocket serverSocket = new ServerSocket(80);
System.out.println("正在等待情書中...");
Socket socket = serverSocket.accept();
System.out.println("收到情書,我要開始解析!");
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream));
String line = reader.readLine();
System.out.println(line);
String url = line.substring(5, line.indexOf("HTTP") - 1);
System.out.println("情書解析完畢,我要想想怎么回復(fù)了...");
// 獲取文件內(nèi)容
inputStream = new FileInputStream(base_url + url);
OutputStream outputStream = socket.getOutputStream();
byte[] buffer = new byte[4 * 1024];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
outputStream.flush();
System.out.println("情書請求已發(fā)送給客戶端");
//關(guān)閉對應(yīng)的資源
serverSocket.close();
socket.shutdownInput();
socket.close();
inputStream.close();
reader.close();
outputStream.close();
} catch (Exception e) {
}
}
}
}
這樣,這位紅娘就在這里一直等啊等,來了一個客戶端我就處理他的情書請求,處理完這個繼續(xù)循環(huán)以相同的方式等,處理,等,處理。。。。
好吧,咱們接下來試一下,還是務(wù)必先運行服務(wù)器端程序,然后先和第一個女神表白,即 127.0.0.1/index.html 還是好朋友的話,就別問我返回結(jié)果。。。這個時候打開eclipse的控制臺,有沒有發(fā)現(xiàn)右上角的紅色暫停標(biāo)志可以點擊,那就說明咱們的紅娘還在兢兢業(yè)業(yè)的工作著!好了,抓緊時間趕緊向第二個女神表白,看她怎么說, 瀏覽器輸入 127.0.0.1/another.html ,回車!好快啊,女神給我答復(fù)了。。。
這。。。(她怎么知道不到十分鐘?你是不是突然想到了cookie可以記錄客戶端的信息,不過咱們這里沒用到cookie)還是看看another.html文件里寫了什么吧
<html>
<head>
<meta charset="utf-8" />
<title>Welcome</title>
</head>
<body>
<h1>我記得你剛和別人表白吧,還不過十分鐘,你怎么會是個好人!</h1>
</body>
</html>
好吧,我啥也不說了,親們,我還要向第三個女神表白嗎。。。?瀏覽器主動跟我說:“你表白吧,這次你發(fā)多少封表白信我都能給你送到服務(wù)器那里,因為她一直在等待著我給她發(fā)送呢!”想想,還是算了,人生如此,何須多言。。。
代碼都貼出來了,其實看起來挺簡單的,但是實際操作中會碰到各種各樣的問題。
還有一些要再繼續(xù)嘮叨的邊角料:
- 1、Q:什么是端口?
A:這是一個比較抽象的概念,是為了進程間通信,每一個進程只能占用一個端口,也就是說多個進程絕不能同時占用一個端口
- 2、Q:既然多個進程不能同時占用一個端口,那么咱們常說的web服務(wù)默認使用的是80端口,我電腦有三個瀏覽器,谷歌,360,IE他們卻可以同時上網(wǎng),這不是端口沖突了嗎?
A:常說的web服務(wù)使用80端口指的是服務(wù)器監(jiān)聽web請求的端口,是服務(wù)器,不是你自己的客戶機。一般來說,一個應(yīng)用程序打開后訪問網(wǎng)絡(luò)本地操作系統(tǒng)為其分配的端口號是隨機的,所以三個瀏覽器雖然同時接收web服務(wù)器的回復(fù)報文,由于他們?nèi)齻€各自占用的端口不一樣,所以不會產(chǎn)生沖突。
- 3、Q:既然我的應(yīng)用程序使用的端口都是隨機的,服務(wù)器接收到請求后怎么知道它要把應(yīng)答報文發(fā)給誰?
A:靠Socket!通過剛才的編程實戰(zhàn),在我理解,Socket肯定會至少包括四部分內(nèi)容:IP地址,端口號,輸入流和輸出流。也就是說,從客戶機發(fā)給服務(wù)器的Socket里一定會有客戶機的IP地址和相應(yīng)應(yīng)用程序的端口號,這樣服務(wù)器自然就知道應(yīng)該把應(yīng)答報文發(fā)給誰了。
- 4、Q:非要使用80端口嗎?
A:不一定。我們剛才在編程的時候確實使用的是80端口,所以我們在瀏覽器中輸入127.0.0.1/index.html,瀏覽器會默認認為我們會向127.0.0.1主機的80號端口發(fā)送請求。但是,這個80端口號只是默認的而已,我們完全可以自己改掉,比如在java代碼里把服務(wù)器端的ServerSocket改成 ServerSocket serverSocket = new ServerSocket(3456); 這時候我們在瀏覽器中就要輸入 127.0.0.1:3456/index.html 了,效果是一樣的,可以淺嘗輒止一下。
- 5、Q:誰是客戶端,誰是服務(wù)器?
A:咱們只有一臺電腦,這臺電腦既充當(dāng)著客戶端的角色,又充當(dāng)著服務(wù)器的角色。當(dāng)瀏覽器請求網(wǎng)頁時,它是客戶端;當(dāng)80端口收到請求報文并應(yīng)答時,它就是服務(wù)器。實在不理解,就想想什么是自戀吧,或者,自交也勉強可以。。
- 6、Q:還有什么問題,歡迎留言~
對于此用java編寫的web服務(wù)器的一點簡單說明:此段代碼非常簡單,所以肯定不會是真正web服務(wù)器所用到的代碼,咱們這個只是能夠應(yīng)答最最基本的web請求,不能檢測是否跨域訪問等等。不過最基本的,用的socket編程是肯定的。另外,對于此段程序,只給出了處理輸入輸出流的一種方式。對于輸入流,除了咱們剛才用到的BufferedReader包裝類,還可以直接用InputStream的read()方法等;對于輸出流,除了咱們剛才用的OutputStream的write()方法,還可用BufferedWritter,PrintWritter等,這些都是java IO的基本用法,根據(jù)網(wǎng)絡(luò)環(huán)境,根據(jù)所要讀取的文件大小來時時變通,這就是仁者見仁智者見智了。
文章寫到了最后,不知道該怎么收尾了,安安靜靜做個程序員吧,挺好。
個人github: http://github.com/icodeu
代碼托管地址:https://github.com/icodeu/JavaForWebServer
CSDN博客:http://blog.csdn.net/icodeyou
個人微信號:qqwanghuan 只為技術(shù)交流
