1.網(wǎng)絡(luò)編程
1.1計算機網(wǎng)絡(luò)概述
網(wǎng)絡(luò)編程的實質(zhì)就是兩個(或多個)設(shè)備(例如計算機)之間的數(shù)據(jù)傳輸。
按照計算機網(wǎng)絡(luò)的定義,通過一定的物理設(shè)備將處于不同位置的計算機連接起來組成的網(wǎng)絡(luò),這個網(wǎng)絡(luò)中包含的設(shè)備有:計算機、路由器、交換機等等。
其實從軟件編程的角度來說,對于物理設(shè)備的理解不需要很深刻,就像你打電話時不需要很熟悉通信網(wǎng)絡(luò)的底層實現(xiàn)是一樣的,但是當深入到網(wǎng)絡(luò)編程的底層時,這些基礎(chǔ)知識是必須要補的。
路由器和交換機組成了核心的計算機網(wǎng)絡(luò),計算機只是這個網(wǎng)絡(luò)上的節(jié)點以及控制等,通過光纖、網(wǎng)線等連接將設(shè)備連接起來,從而形成了一張巨大的計算機網(wǎng)絡(luò)。
網(wǎng)絡(luò)最主要的優(yōu)勢在于共享:共享設(shè)備和數(shù)據(jù),現(xiàn)在共享設(shè)備最常見的是打印機,一個公司一般一個打印機即可,共享數(shù)據(jù)就是將大量的數(shù)據(jù)存儲在一組機器中,其它的計算機通過網(wǎng)絡(luò)訪問這些數(shù)據(jù),例如網(wǎng)站、銀行服務(wù)器等等。
如果需要了解更多的網(wǎng)絡(luò)硬件基礎(chǔ)知識,可以閱讀《計算機網(wǎng)絡(luò)》教材,對于基礎(chǔ)進行強化,這個在基礎(chǔ)學習階段不是必須的,但是如果想在網(wǎng)絡(luò)編程領(lǐng)域有所造詣,則是一個必須的基本功。
對于網(wǎng)絡(luò)編程來說,最主要的是計算機和計算機之間的通信,這樣首要的問題就是如何找到網(wǎng)絡(luò)上的計算機呢?這就需要了解IP地址的概念。
為了能夠方便的識別網(wǎng)絡(luò)上的每個設(shè)備,網(wǎng)絡(luò)中的每個設(shè)備都會有一個唯一的數(shù)字標識,這個就是IP地址。在計算機網(wǎng)絡(luò)中,現(xiàn)在命名IP地址的規(guī)定是IPv4協(xié)議,該協(xié)議規(guī)定每個IP地址由4個0-255之間的數(shù)字組成,例如10.0.120.34。每個接入網(wǎng)絡(luò)的計算機都擁有唯一的IP地址,這個IP地址可能是固定的,例如網(wǎng)絡(luò)上各種各樣的服務(wù)器,也可以是動態(tài)的,例如使用ADSL撥號上網(wǎng)的寬帶用戶,無論以何種方式獲得或是否是固定的,每個計算機在聯(lián)網(wǎng)以后都擁有一個唯一的合法IP地址,就像每個手機號碼一樣。
但是由于IP地址不容易記憶,所以為了方便記憶,有創(chuàng)造了另外一個概念——域名(Domain Name),例如sohu.com等。一個IP地址可以對應(yīng)多個域名,一個域名只能對應(yīng)一個IP地址。域名的概念可以類比手機中的通訊簿,由于手機號碼不方便記憶,所以添加一個姓名標識號碼,在實際撥打電話時可以選擇該姓名,然后撥打即可。
在網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù),全部是以IP地址作為地址標識,所以在實際傳輸數(shù)據(jù)以前需要將域名轉(zhuǎn)換為IP地址,實現(xiàn)這種功能的服務(wù)器稱之為DNS服務(wù)器,也就是通俗的說法叫做域名解析。例如當用戶在瀏覽器輸入域名時,瀏覽器首先請求DNS服務(wù)器,將域名轉(zhuǎn)換為IP地址,然后將轉(zhuǎn)換后的IP地址反饋給瀏覽器,然后再進行實際的數(shù)據(jù)傳輸。
當DNS服務(wù)器正常工作時,使用IP地址或域名都可以很方便的找到計算機網(wǎng)絡(luò)中的某個設(shè)備,例如服務(wù)器計算機。當DNS不正常工作時,只能通過IP地址訪問該設(shè)備。所以IP地址的使用要比域名通用一些。
IP地址和域名很好的解決了在網(wǎng)絡(luò)中找到一個計算機的問題,但是為了讓一個計算機可以同時運行多個網(wǎng)絡(luò)程序,就引入了另外一個概念——端口(port)。
在介紹端口的概念以前,首先來看一個例子,一般一個公司前臺會有一個電話,每個員工會有一個分機,這樣如果需要找到這個員工的話,需要首先撥打前臺總機,然后轉(zhuǎn)該分機號即可。這樣減少了公司的開銷,也方便了每個員工。在該示例中前臺總機的電話號碼就相當于IP地址,而每個員工的分機號就相當于端口。
有了端口的概念以后,在同一個計算機中每個程序?qū)?yīng)唯一的端口,這樣一個計算機上就可以通過端口區(qū)分發(fā)送給每個端口的數(shù)據(jù)了,換句話說,也就是一個計算機上可以并發(fā)運行多個網(wǎng)絡(luò)程序,而不會在互相之間產(chǎn)生干擾。
在硬件上規(guī)定,端口的號碼必須位于0-65535之間,每個端口唯一的對應(yīng)一個網(wǎng)絡(luò)程序,一個網(wǎng)絡(luò)程序可以使用多個端口。這樣一個網(wǎng)絡(luò)程序運行在一臺計算上時,不管是客戶端還是服務(wù)器,都是至少占用一個端口進行網(wǎng)絡(luò)通訊。在接收數(shù)據(jù)時,首先發(fā)送給對應(yīng)的計算機,然后計算機根據(jù)端口把數(shù)據(jù)轉(zhuǎn)發(fā)給對應(yīng)的程序。
有了IP地址和端口的概念以后,在進行網(wǎng)絡(luò)通訊交換時,就可以通過IP地址查找到該臺計算機,然后通過端口標識這臺計算機上的一個唯一的程序。這樣就可以進行網(wǎng)絡(luò)數(shù)據(jù)的交換了。
但是,進行網(wǎng)絡(luò)編程時,只有IP地址和端口的概念還是不夠的,下面就介紹一下基礎(chǔ)的網(wǎng)絡(luò)編程相關(guān)的軟件基礎(chǔ)知識。
1.2網(wǎng)絡(luò)編程概述
網(wǎng)絡(luò)編程中有兩個主要的問題,一個是如何準確的定位網(wǎng)絡(luò)上一臺或多臺主機,另一個就是找到主機后如何可靠高效的進行數(shù)據(jù)傳輸。在TCP/IP協(xié)議中IP層主要負責網(wǎng)絡(luò)主機的定位,數(shù)據(jù)傳輸?shù)穆酚桑蒊P地址可以唯一地確定Internet上的一臺主機。而TCP層則提供面向應(yīng)用的可靠的或非可靠的數(shù)據(jù)傳輸機制,這是網(wǎng)絡(luò)編程的主要對象,一般不需要關(guān)心IP層是如何處理數(shù)據(jù)的。
按照前面的介紹,網(wǎng)絡(luò)編程就是兩個或多個設(shè)備之間的數(shù)據(jù)交換,其實更具體的說,網(wǎng)絡(luò)編程就是兩個或多個程序之間的數(shù)據(jù)交換,和普通的單機程序相比,網(wǎng)絡(luò)程序最大的不同就是需要交換數(shù)據(jù)的程序運行在不同的計算機上,這樣就造成了數(shù)據(jù)好換的復雜。雖然通過IP地址和端口號可以找到網(wǎng)絡(luò)上運行的一個程序,但是如果需要進行網(wǎng)絡(luò)編程,則需要了解網(wǎng)絡(luò)通訊的過程。
網(wǎng)絡(luò)通訊基于“請求—響應(yīng)”模型。在網(wǎng)絡(luò)通訊中,第一次主動發(fā)起通訊的程序被稱為客戶端(client)程序,簡稱客戶端,而第一次通訊中等待鏈接的程序被稱為服務(wù)器端(Server)程序,簡稱服務(wù)器。一旦通訊建立,則客戶端和服務(wù)器端完全一樣,沒有本質(zhì)區(qū)別。
由此,網(wǎng)絡(luò)編程中的兩種程序就分別是客戶端和服務(wù)器端,例如QQ程序,每個QQ用戶安裝的都是QQ客戶端程序,而QQ服務(wù)器端程序則在騰訊公司的機房中,為大量的QQ用戶提供服務(wù)。這種網(wǎng)絡(luò)編程的結(jié)構(gòu)被稱為客戶端/服務(wù)器結(jié)構(gòu),也叫Client/Serverj結(jié)構(gòu),簡稱C/S結(jié)構(gòu)。
使用C/S結(jié)構(gòu)的程序,在開發(fā)時需要分別開發(fā)客戶端和服務(wù)器端,這種結(jié)構(gòu)的優(yōu)勢在于客戶端是專門開發(fā)的,所以根據(jù)需要實現(xiàn)各種效果,專業(yè)點的說就是表現(xiàn)力豐富,而服務(wù)器端也需要專門進行開發(fā)。但是這種結(jié)構(gòu)也存在著很多不足,例如通用性差,幾乎不能通用,也就是說一種程序的客戶端只能和對應(yīng)的服務(wù)器端通訊,而不能和其他服務(wù)器端通訊,在實際維護中,也需要維護專門的客戶端和服務(wù)器端,維護的壓力比較大。
其實在運行很多程序時,沒有必要使用專門的客戶端,而需要使用通用的客戶端,例如瀏覽器,使用瀏覽器作為客戶端的結(jié)構(gòu)稱為瀏覽器/服務(wù)器結(jié)構(gòu),也叫做Browser/Server結(jié)構(gòu),簡稱B/S結(jié)構(gòu)。
使用B/S結(jié)構(gòu)的程序,在開發(fā)時只需要開發(fā)服務(wù)器端即可,這種優(yōu)勢在于開發(fā)壓力比較小,不需要維護客戶端,但是這種結(jié)構(gòu)也存在這很多不足,例如瀏覽器的限制比較大,表現(xiàn)了不強,不能進行系統(tǒng)級別的操作等。
總之C/S結(jié)構(gòu)和B/S結(jié)構(gòu)是現(xiàn)在網(wǎng)絡(luò)編程中常見的兩種結(jié)構(gòu),B/S結(jié)構(gòu)其實也就是一種特殊的C/S結(jié)構(gòu)。
另外簡單的介紹一下P2P(Point to Point)程序,常見的如BT、電驢等。P2P程序是一種特殊的程序,應(yīng)該一個P2P程序中既包含客戶端程序,也包含服務(wù)器端程序,例如BT,使用客戶端程序部分連接其它的種子(服務(wù)器端),而使用服務(wù)器端向其它的BT客戶端傳輸數(shù)據(jù)。如果這個還不是很清楚,其實P2P程序和手機是一樣的,當手機撥打電話時就是使用客戶端的作用,而手機處于待機狀態(tài)時,可以接收到其它用戶撥打的電話則起的就是服務(wù)器端的功能,只是一般的手機不能同時使用撥打電話和接聽電話的功能,而P2P程序?qū)崿F(xiàn)了該功能。
最后介紹一下網(wǎng)絡(luò)編程中最重要的,也是最復雜的概念——協(xié)議(protocol)。按照前面的介紹,網(wǎng)絡(luò)編程就是運行在不同計算機中兩個程序之間的數(shù)據(jù)交換。在實際進行數(shù)據(jù)交換時,為了讓接收端理解該數(shù)據(jù),計算機比較笨,什么都不懂的,那么久需要規(guī)定該數(shù)據(jù)的格式,這個數(shù)據(jù)的格式就是協(xié)議。
如果沒有理解協(xié)議的概念,那么再舉一個例子,記得有個電影叫《永不消逝的電波》,講述的是地下黨通過電臺發(fā)送情報的故事,這里我們不探討電影的劇情,而只關(guān) 心電臺發(fā)送的數(shù)據(jù)。在實際發(fā)報時,需要首先將需要發(fā)送的內(nèi)容轉(zhuǎn)換為電報編碼,然后將電報編碼發(fā)送出去,而接收端接收的是電報編碼,如果需要理解電報的內(nèi)容 則需要根據(jù)密碼本翻譯出該電報的內(nèi)容。這里的密碼本就規(guī)定了一種數(shù)據(jù)格式,這種對于網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)格式在網(wǎng)絡(luò)編程中就被稱作協(xié)議。
那么如何編寫協(xié)議格式呢?答案是隨意。只要按照這種協(xié)議格式能夠生成唯一的編碼,按照該編碼可以唯一的解析出發(fā)送數(shù)據(jù)的內(nèi)容即可。也正因為各個網(wǎng)絡(luò)程序之間協(xié)議格式的不同,所以才導致了客戶端程序都是專用的結(jié)構(gòu)。
在實際的網(wǎng)絡(luò)編程中,最麻煩的內(nèi)容不是數(shù)據(jù)的發(fā)送和接受,因為這個功能在幾乎所有編程語言中都提供了封裝好的API進行調(diào)用,最麻煩的內(nèi)容就是協(xié)議的設(shè)計及協(xié)議的生產(chǎn)和解析,這個才是網(wǎng)絡(luò)編程最核心的內(nèi)容。
1.3網(wǎng)絡(luò)通訊方式
在現(xiàn)有的網(wǎng)絡(luò)中,網(wǎng)絡(luò)通訊的方式主要有兩種:
1.TCP(傳輸控制協(xié)議)方式。
2.UDP(用戶數(shù)據(jù)協(xié)議)方式。
為了方便理解這兩種方式,還是先來看個例子。大家使用手機時,向別人傳遞信息時有兩種方式:撥打電話和發(fā)送短信。使用撥打電話的方式可以保證該信息傳遞給別人,因為別人接電話時本身就確認收到了該信息。而發(fā)送短信的方式價格低廉,使用方便,但是接受人可能收不到。
在網(wǎng)絡(luò)通訊中,TCP方式就類似于撥打電話,使用該種方式進行網(wǎng)絡(luò)通訊時,需要建立專門的虛擬連接,然后進行可靠的數(shù)據(jù)傳輸,如果數(shù)據(jù)發(fā)送失敗,則客戶端會自動重發(fā)該數(shù)據(jù),而UDP方式就類似于發(fā)送短信,使用這種方式進行網(wǎng)絡(luò)通訊時,不需要建立專門的虛擬連接,傳輸也不是很可靠,如果發(fā)送失敗則客戶端無法獲得。
這兩種傳輸方式都是實際的網(wǎng)絡(luò)編程中進行使用,重要的數(shù)據(jù)一般使用TCP方式進行數(shù)據(jù)傳輸,而大量的非核心數(shù)據(jù)則都通過UDP方式進行傳遞,在一些程序中甚至結(jié)合使用這兩種方式進行數(shù)據(jù)的傳遞。
由于TCP需要建立專用的虛擬連接以及確認傳輸是否正確,所以使用TCP方式的速度稍微慢一些,而且傳輸時產(chǎn)生的數(shù)據(jù)量要比UDP稍微大一些。
關(guān)于網(wǎng)絡(luò)編程的基礎(chǔ)知識就介紹這么多,如果需要深入了解相關(guān)知識請閱讀專門的計算機網(wǎng)絡(luò)書籍,下面開始介紹Java語言中網(wǎng)絡(luò)編程的相關(guān)技術(shù)。
1.3網(wǎng)絡(luò)編程步驟
按照前面的基礎(chǔ)知識介紹,無論使用TCP方式還是UDP方式進行網(wǎng)絡(luò)通訊,網(wǎng)絡(luò)編程都是由客戶端和服務(wù)器端組成,所以,下面介紹網(wǎng)絡(luò)編程的步驟時,均以C/S結(jié)構(gòu)為基礎(chǔ)進行介紹。
1.3.1客戶端網(wǎng)絡(luò)編程步驟
客戶端是指網(wǎng)絡(luò)編程中首先發(fā)起連接的程序,客戶端一般實現(xiàn)程序界面和基本邏輯實現(xiàn),在進行實際的客戶端編程時,無論客戶端復雜還是簡單,以及客戶端實現(xiàn)的方式,客戶端的編程主要由三個步驟實現(xiàn):
1.建立網(wǎng)絡(luò)連接
客戶端網(wǎng)絡(luò)編程的第一步都是建立網(wǎng)絡(luò)連接。在建立網(wǎng)絡(luò)連接時需要指定連接的服務(wù)器的IP地址和端口號,建立完成以后,會形成一條虛擬的連接,后續(xù)的操作就可以通過該連接實現(xiàn)數(shù)據(jù)交換了。
2.交換數(shù)據(jù)
連接建立以后,就可以通過這個連接交換數(shù)據(jù)了,交換數(shù)據(jù)嚴格要求按照請求響應(yīng)模型進行,由客戶端發(fā)送一個請求數(shù)據(jù)到服務(wù)器,服務(wù)器反饋一個響應(yīng)數(shù)據(jù)后給客戶端,如果客戶端不發(fā)送請求則服務(wù)器就不響應(yīng)。
根據(jù)邏輯需要,可以多次交換數(shù)據(jù),但是還是必須遵循請求響應(yīng)模型。
3.關(guān)閉網(wǎng)絡(luò)連接
在數(shù)據(jù)交換完成后,關(guān)閉網(wǎng)絡(luò)連接,釋放程序占用的端口、內(nèi)存等系統(tǒng)資源,結(jié)束網(wǎng)絡(luò)編程。
最基本的步驟一般都是這三個步驟,在實際實現(xiàn)時,步驟2會出現(xiàn)重復,在進行代碼組織時,由于網(wǎng)絡(luò)編程是比較耗時的操作,所以一般開啟專門的現(xiàn)場進行網(wǎng)絡(luò)通訊。
1.4服務(wù)器端網(wǎng)絡(luò)編程步驟
服務(wù)器是指網(wǎng)絡(luò)編程中被等待連接的程序,服務(wù)器端一般實現(xiàn)程序的核心邏輯以及數(shù)據(jù)存儲等核心功能。服務(wù)器端的編程步驟和客戶端不同,是由四個步驟實現(xiàn),依次是:
1.監(jiān)聽端口
服務(wù)器端屬于被動等待連接,所以服務(wù)器端啟動以后,不需要發(fā)起連接,而只需要監(jiān)聽本地計算機的某個固定端口即可。這個端口就是服務(wù)器端開放給客戶端的端口,服務(wù)器端程序運行的本地計算機的IP地址就是服務(wù)器端程序的IP地址。
2.獲得連接
當客戶端連接到服務(wù)器端時,服務(wù)器端就可以獲得一個連接,這個連接包含客戶端信息,例如客戶端IP地址等,服務(wù)器端和客戶端通過該連接進行數(shù)據(jù)交換。
一般在服務(wù)器端編程中,當獲得連接時,需要開啟專門的線程處理該連接,每個連接都由獨立的線程實現(xiàn)。
3.交換數(shù)據(jù)
服務(wù)器端通過獲得的連接進行數(shù)據(jù)交換。服務(wù)器端的數(shù)據(jù)交換步驟是首先接收客戶端發(fā)送過來的數(shù)據(jù),然后進行邏輯處理,再把處理以后的結(jié)果數(shù)據(jù)發(fā)送給客戶端。簡單來說,就是先接收再發(fā)送,這個和客戶端的數(shù)據(jù)交換順序不同。
其實,服務(wù)器端獲得的連接和客戶端的連接是一樣的,只是數(shù)據(jù)交換的步驟不同。當然,服務(wù)器端的數(shù)據(jù)交換也是可以多次進行的。在數(shù)據(jù)交換完成以后,關(guān)閉和客戶端的連接。
4.關(guān)閉連接
當服務(wù)器程序關(guān)閉時,需要關(guān)閉服務(wù)器端,通過關(guān)閉服務(wù)器端使得服務(wù)器監(jiān)聽的端口以及占用的內(nèi)存可以釋放出來,實現(xiàn)了連接的關(guān)閉。
其實服務(wù)器端編程的模型和呼叫中心的實現(xiàn)是類似的,例如移動的客服電話10086就是典型的呼叫中心,當一個用戶撥打10086時,轉(zhuǎn)接給一個專門的客服人員,由該客服實現(xiàn)和該用戶的問題解決,當另外一個用戶撥打10086時,則轉(zhuǎn)接給另一個客服,實現(xiàn)問題解決,依次類推。
在服務(wù)器端編程時,10086這個電話號碼就類似于服務(wù)器端的端口號碼,每個用戶就相當于一個客戶端程序,每個客服人員就相當于服務(wù)器端啟動的專門和客戶端連接的線程,每個線程都是獨立進行交互的。
這就是服務(wù)器端編程的模型,只是TCP方式是需要建立連接的,對于服務(wù)器端的壓力比較大,而UDP是不需要建立連接的,對于服務(wù)器端的壓力比較小罷了。
總之,無論使用任何語言,任何方式進行基礎(chǔ)的網(wǎng)絡(luò)編程,都必須遵循固定的步驟進行操作,在熟悉了這些步驟以后,可以根據(jù)需要進行邏輯上的處理,但是還是必須遵循固定的步驟進行。
其實,基礎(chǔ)的網(wǎng)絡(luò)編程本身不難,也不需要很多的基礎(chǔ)網(wǎng)絡(luò)知識,只是由于編程的基礎(chǔ)功能都已經(jīng)由API實現(xiàn),而且需要按照固定的步驟進行,所以在入門時有一定的門檻,希望下面的內(nèi)容能夠?qū)⒛憧焖俚膸刖W(wǎng)絡(luò)編程技術(shù)的大門。
2.Java網(wǎng)絡(luò)編程技術(shù)
和網(wǎng)絡(luò)編程有關(guān)的基本API位于Java.NET包中,該包中包含了基本的網(wǎng)絡(luò)編程實現(xiàn),該包是網(wǎng)絡(luò)編程的基礎(chǔ)。該包既包含基本的網(wǎng)絡(luò)編程類,也包含封裝后的專門處理WEB相關(guān)的處理類。
首先來介紹一下基礎(chǔ)的網(wǎng)絡(luò)類-InetAddress類。該類的功能是代表一個IP地址,并且將IP地址和域名相關(guān)的操作方法包含在該類的內(nèi)部。關(guān)于該類的使用,下面通過一個基礎(chǔ)的代碼演示該類的使用。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressDemo {
public static void main(String[] args) {
try {
InetAddress inet1 = InetAddress.getByName("www.163.com");
System.out.println(inet1);
InetAddress inet2=InetAddress.getByName("127.0.0.1");
System.out.println(inet2);
InetAddress inet3=InetAddress.getLocalHost();
System.out.println(inet3);
String host =inet3.getHostName();
System.out.println("域名:"+host);
String ip=inet3.getHostAddress();
System.out.println("IP:"+ip);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
在該示例代碼中,演示了InetAddress類的基本使用,并使用了該類的幾個常用方法,該代碼的執(zhí)行結(jié)果是:
www.163.com/202.201.14.182
/127.0.0.1
DESKTOP-HRHF03J/192.168.1.196
域名:DESKTOP-HRHF03J
IP:192.168.1.196
說明:由于該代碼中包含一個互聯(lián)網(wǎng)的網(wǎng)址,所以運行該程序時需要聯(lián)網(wǎng),否則將產(chǎn)生異常。
在后續(xù)的使用中,經(jīng)常包含需要使用InetAddress對象代表IP地址的構(gòu)造方法,當然,該類的使用補水必須的,也可以使用字符串來代表IP地址。
3.TCP編程
在Java語言中,對于TCP方式的網(wǎng)絡(luò)編程提供了良好的支持,在實際實現(xiàn)時,以java.net.socket類代表客戶端連接,以java.net.ServerSocket類作為服務(wù)器端連接。在進行網(wǎng)絡(luò)編程時,底層網(wǎng)絡(luò)通訊的細節(jié)已經(jīng)實現(xiàn)了比較高的封裝,所以在程序員實際編程時,只需要指定IP地址和端口號就可以建立連接了。正是由于這種高度的封裝,一方面,簡化了Java語言網(wǎng)絡(luò)編程的難度,另外也使得使用Java語言進行網(wǎng)絡(luò)編程無法深入到網(wǎng)絡(luò)的底層,所以使用Java語言進行網(wǎng)絡(luò)底層系統(tǒng)編程很困難,具體點說,Java語言無法事先底層的網(wǎng)絡(luò)嗅探以及獲得IP包結(jié)構(gòu)等消息。但是由于Java語言的網(wǎng)絡(luò)編程比較簡答,所以還是獲得了廣泛的使用。
在使用TCP方式進行網(wǎng)絡(luò)編程時,需要按照前面介紹的網(wǎng)絡(luò)編程的步驟進行,下面分別介紹一下在Java語言中客戶端和服務(wù)器端的實現(xiàn)步驟。在客戶端網(wǎng)絡(luò)編程中,首先需要建立連接,在Java API中以及java.net.socket類的對象代表網(wǎng)絡(luò)連接,所以建立客戶端網(wǎng)絡(luò)連接,也就是創(chuàng)建Socket類型的對象,該對象代表網(wǎng)絡(luò)連接,示例如下:
Socket socket1=new Socket(“192.168.1.103”,10000); Socket socket2=new Socket(“www.sohu.com”,80);
上面的代碼中,socket1實現(xiàn)的是連接到IP地址是192.168.1.103的計算機的10000號端口,而socket2實現(xiàn)的是連接到域名是www.sohu.com的計算機的80號端口,至于底層網(wǎng)絡(luò)如何實現(xiàn)建立連接,對于程序員來說是完全透明的。如果建立連接時,本機網(wǎng)絡(luò)不通,或服務(wù)器端程序未開啟,則會拋出異常。
連接一旦建立,則完成了客戶端編程的第一步,緊接著的步驟就是按照“請求-響應(yīng)”模型進行網(wǎng)絡(luò)數(shù)據(jù)交換,在Java語言中,數(shù)據(jù)傳輸功能由Java IO實現(xiàn),也就是說只需要從連接中獲得輸入流和輸出流即可,然后將需要發(fā)送的數(shù)據(jù)寫入連接對象的輸出流中,在發(fā)送完成后從輸入流中讀取數(shù)據(jù)即可。示例代碼如下:
OutputStream os=socket1.getOutputStream();
InputStream is=socket1,getInputStream();
上面的代碼中,分別從socket1這個連接對象獲得了輸出流和輸入流對象,在整個網(wǎng)絡(luò)編程中,后續(xù)的數(shù)據(jù)交換就變成了IO操作,也就是遵循“請求-響應(yīng)”模式的規(guī)定,先向輸出流中寫入數(shù)據(jù),這些數(shù)據(jù)會被系統(tǒng)發(fā)送出去,然后再從輸入流中讀取服務(wù)器端的反饋信息,這樣就完成了一次數(shù)據(jù)交換工作,當然這個數(shù)據(jù)交換可以多次進行。
這里獲得的只是最基本的輸出流和輸入流對象,還可以根據(jù)前面學習到的IO知識,使用流的嵌套將這些獲得的基本流對象轉(zhuǎn)換成需要的裝飾流對象,從而方便數(shù)據(jù)的操作。
最后當數(shù)據(jù)交換完成以后,關(guān)閉網(wǎng)絡(luò)連接,釋放網(wǎng)絡(luò)連接占用的系統(tǒng)端口和內(nèi)存等資源,完成網(wǎng)絡(luò)操作,示例代碼如下:
socket1.close();
這就是最基本的網(wǎng)絡(luò)編程功能介紹,下面是一個簡單的網(wǎng)絡(luò)客戶端程序示例,該程序的作用是向服務(wù)器發(fā)送一個字符串“Hello”,并將服務(wù)器端的反饋顯示到控制臺,數(shù)據(jù)交換只進行一次,當數(shù)據(jù)交換完成以后關(guān)閉網(wǎng)絡(luò)連接,程序結(jié)束,實現(xiàn)的代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 簡單的Socket客戶端
* 功能為:發(fā)送字符串“Hello”到服務(wù)器端,并打印出服務(wù)器端的反饋
*/
public class SimpleSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務(wù)器端IP地址
String serverIP = "127.0.0.1";
//服務(wù)器端端口號
int port = 10000;
//發(fā)送內(nèi)容
String data = "Hello";
try {
//建立連接
socket = new Socket(serverIP,port);
//發(fā)送數(shù)據(jù)
os = socket.getOutputStream();
os.write(data.getBytes());
//接收數(shù)據(jù)
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//輸出反饋數(shù)據(jù)
System.out.println("服務(wù)器反饋:" + new String(b,0,n));
} catch (Exception e) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關(guān)閉流和連接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
在該示例代碼中建立了一個連接到IP地址為127.0.0.1,端口號為10000的TCP類型的網(wǎng)絡(luò)連接,然后獲得連接的輸出流對象,將需要發(fā)送的字符串“Hello”轉(zhuǎn)換為波耶特數(shù)組寫入到輸出流中,由系統(tǒng)自動完成將輸出流中的數(shù)據(jù)發(fā)送出去,如果需要強制發(fā)送,可以調(diào)用輸出流對象中的flush方法實現(xiàn)。在數(shù)據(jù)發(fā)送出去后,從連接對象的輸入流中讀取服務(wù)器端的反饋信息,讀取時可以使用IO中的各種讀取方法進行讀取,這里使用最簡單的方法進行讀取。從輸入流中讀取到的內(nèi)容就是服務(wù)器端的反饋,并將讀取到的內(nèi)容在客戶端的控制臺進行輸出,最后依次關(guān)閉打開的流對象和網(wǎng)絡(luò)連接對象。
這是一個簡單的功能示例,在該示例中演示了TCP類型的網(wǎng)絡(luò)客戶端基本方法的使用,該代碼只起演示目的,還無法達到實用的級別。
如果需要在控制臺下面編譯和運行該代碼,需要首先在控制臺下切換到源代碼所在的目錄,然后依次輸入編譯和運行命令:
javac –d . SimpleSocketClient.java
java tcp.SimpleSocketClient
和下面將要介紹的SimpleSocketServer服務(wù)器端組合運行時,程序的輸出結(jié)果為:
服務(wù)器反饋:Hello
介紹完一個簡單的客戶端編程的示例,下面接著介紹一下TCP類型的服務(wù)器端的編寫。首先需要說明的是,客戶端的步驟和服務(wù)器端的編寫步驟不同,所以在學習服務(wù)器端編程時注意不要和客戶端混淆起來。
在服務(wù)器端程序編程中,由于服務(wù)器端實現(xiàn)的是被動等待連接,所以服務(wù)器端編程的第一個步驟是監(jiān)聽端口,也就是監(jiān)聽是否有客戶端連接到達。實現(xiàn)服務(wù)器端監(jiān)聽的代碼為:
ServerSocket ss = new ServerSocket(10000);
該代碼實現(xiàn)的功能是監(jiān)聽當前計算機的10000號端口,如果在執(zhí)行該代碼時,10000號端口已經(jīng)被別的程序占用,那么將拋出異常。否則將實現(xiàn)監(jiān)聽。
服務(wù)器端編程的第二個步驟是獲得連接。該步驟的作用是當有客戶端連接到達時,建立一個和客戶端連接對應(yīng)的Socket連 接對象,從而釋放客戶端連接對于服務(wù)器端端口的占用。實現(xiàn)功能就像公司的前臺一樣,當一個客戶到達公司時,會告訴前臺我找某某某,然后前臺就通知某某某, 然后就可以繼續(xù)接待其它客戶了。通過獲得連接,使得客戶端的連接在服務(wù)器端獲得了保持,另外使得服務(wù)器端的端口釋放出來,可以繼續(xù)等待其它的客戶端連接。 實現(xiàn)獲得連接的代碼是:
Socket socket = ss.accept();
該代碼實現(xiàn)的功能是獲得當前連接到服務(wù)器端的客戶端連接。需要說明的是accept和前面IO部分介紹的read方法一樣,都是一個阻塞方法,也就是當無連接時,該方法將阻塞程序的執(zhí)行,直到連接到達時才執(zhí)行該行代碼。另外獲得的連接會在服務(wù)器端的該端口注冊,這樣以后就可以通過在服務(wù)器端的注冊信息直接通信,而注冊以后服務(wù)器端的端口就被釋放出來,又可以繼續(xù)接受其它的連接了。
連接獲得以后,后續(xù)的編程就和客戶端的網(wǎng)絡(luò)編程類似了,這里獲得的Socket類型的連接就和客戶端的網(wǎng)絡(luò)連接一樣了,只是服務(wù)器端需要首先讀取發(fā)送過來的數(shù)據(jù),然后進行邏輯處理以后再發(fā)送給客戶端,也就是交換數(shù)據(jù)的順序和客戶端交換數(shù)據(jù)的步驟剛好相反。這部分的內(nèi)容和客戶端很類似,所以就不重復了,如果還不熟悉,可以參看下面的示例代碼。
最后,在服務(wù)器端通信完成以后,關(guān)閉服務(wù)器端連接。實現(xiàn)的代碼為:ss.close();
這就是基本的TCP類型的服務(wù)器端編程步驟。下面以一個簡單的echo服務(wù)實現(xiàn)為例子,介紹綜合使用示例。echo的意思就是“回聲”,echo服務(wù)器端實現(xiàn)的功能就是將客戶端發(fā)送的內(nèi)容再原封不動的反饋給客戶端。實現(xiàn)的代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* echo服務(wù)器
* 功能:將客戶端發(fā)送的內(nèi)容反饋給客戶端
*/
public class SimpleSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監(jiān)聽端口號
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
//獲得連接
socket = serverSocket.accept();
//接收客戶端發(fā)送內(nèi)容
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//輸出
System.out.println("客戶端發(fā)送內(nèi)容為:" + new String(b,0,n));
//向客戶端發(fā)送反饋內(nèi)容
os = socket.getOutputStream();
os.write(b, 0, n);
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關(guān)閉流和連接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中建立了一個監(jiān)聽當前計算機10000號端口的服務(wù)器端Socket連接,然后獲得客戶端發(fā)送過來的連接,如果有連接到達時,讀取連接中發(fā)送過來的內(nèi)容,并將發(fā)送的內(nèi)容在控制臺進行輸出,輸出完成以后將客戶端發(fā)送的內(nèi)容再反饋給客戶端。最后關(guān)閉流和連接對象,結(jié)束程序。
在控制臺下面編譯和運行該程序的命令和客戶端部分的類似。
這樣,就以一個很簡單的示例演示了TCP類型的網(wǎng)絡(luò)編程在Java語言中的基本實現(xiàn),這個示例只是演示了網(wǎng)絡(luò)編程的基本步驟以及各個功能方法的基本使用,只是為網(wǎng)絡(luò)編程打下了一個基礎(chǔ),下面將就幾個問題來深入介紹網(wǎng)絡(luò)編程深層次的一些知識。
為了一步一步的掌握網(wǎng)絡(luò)編程,下面再研究網(wǎng)絡(luò)編程中的兩個基本問題,通過解決這兩個問題將對網(wǎng)絡(luò)編程的認識深入一層。
1、如何復用Socket連接?
在前面的示例中,客戶端中建立了一次連接,只發(fā)送一次數(shù)據(jù)就關(guān)閉了,這就相當于撥打電話時,電話打通了只對話一次就關(guān)閉了,其實更加常用的應(yīng)該是撥通一次電話以后多次對話,這就是復用客戶端連接。
那么如何實現(xiàn)建立一次連接,進行多次數(shù)據(jù)交換呢?其實很簡單,建立連接以后,將數(shù)據(jù)交換的邏輯寫到一個循環(huán)中就可以了。這樣只要循環(huán)不結(jié)束則連接就不會被關(guān) 閉。按照這種思路,可以改造一下上面的代碼,讓該程序可以在建立連接一次以后,發(fā)送三次數(shù)據(jù),當然這里的次數(shù)也可以是多次,示例代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 復用連接的Socket客戶端
* 功能為:發(fā)送字符串“Hello”到服務(wù)器端,并打印出服務(wù)器端的反饋
*/
public class MulSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務(wù)器端IP地址
String serverIP = "127.0.0.1";
//服務(wù)器端端口號
int port = 10000;
//發(fā)送內(nèi)容
String data[] ={"First","Second","Third"};
try {
//建立連接
socket = new Socket(serverIP,port);
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
byte[] b = new byte[1024];
for(int i = 0;i < data.length;i++){
//發(fā)送數(shù)據(jù)
os.write(data[i].getBytes());
//接收數(shù)據(jù)
int n = is.read(b);
//輸出反饋數(shù)據(jù)
System.out.println("服務(wù)器反饋:" + new String(b,0,n));
}
} catch (Exception e) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關(guān)閉流和連接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
該示例程序和前面的代碼相比,將數(shù)據(jù)交換部分的邏輯寫在一個for循環(huán)的內(nèi)容,這樣就可以建立一次連接,依次將data數(shù)組中的數(shù)據(jù)按照順序發(fā)送給服務(wù)器端了。
如果還是使用前面示例代碼中的服務(wù)器端程序運行該程序,則該程序的結(jié)果是:
java.net.SocketException: Software caused connection abort: recv failed
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at java.net.SocketInputStream.read(SocketInputStream.java:90)
at tcp.MulSocketClient.main(MulSocketClient.java:30)
服務(wù)器反饋:First
顯然,客戶端在實際運行時出現(xiàn)了異常,出現(xiàn)異常的原因是什么呢?如果仔細閱讀前面的代碼,應(yīng)該還記得前面示例代碼中的服務(wù)器端是對話一次數(shù)據(jù)以后就關(guān)閉了連接,如果服務(wù)器端程序關(guān)閉了,客戶端繼續(xù)發(fā)送數(shù)據(jù)肯定會出現(xiàn)異常,這就是出現(xiàn)該問題的原因。
按照客戶端實現(xiàn)的邏輯,也可以復用服務(wù)器端的連接,實現(xiàn)的原理也是將服務(wù)器端的數(shù)據(jù)交換邏輯寫在循環(huán)中即可,按照該種思路改造以后的服務(wù)器端代碼為:
package tcp;
import java.io.*;
import java.net.*;
/**
* 復用連接的echo服務(wù)器
* 功能:將客戶端發(fā)送的內(nèi)容反饋給客戶端
*/
public class MulSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監(jiān)聽端口號
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
System.out.println("服務(wù)器已啟動:");
//獲得連接
socket = serverSocket.accept();
//初始化流
is = socket.getInputStream();
os = socket.getOutputStream();
byte[] b = new byte[1024];
for(int i = 0;i < 3;i++){
int n = is.read(b);
//輸出
System.out.println("客戶端發(fā)送內(nèi)容為:" + new String(b,0,n));
//向客戶端發(fā)送反饋內(nèi)容
os.write(b, 0, n);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關(guān)閉流和連接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,也將數(shù)據(jù)發(fā)送和接收的邏輯寫在了一個for循環(huán)內(nèi)部,只是在實現(xiàn)時硬性的將循環(huán)次數(shù)規(guī)定成了3次,這樣代碼雖然比較簡單,但是通用性比較差。
以該服務(wù)器端代碼實現(xiàn)為基礎(chǔ)運行前面的客戶端程序時,客戶端的輸出為:
服務(wù)器反饋:First
服務(wù)器反饋:Second
服務(wù)器反饋:Third
服務(wù)器端程序的輸出結(jié)果為:
服務(wù)器已啟動:
客戶端發(fā)送內(nèi)容為:First
客戶端發(fā)送內(nèi)容為:Second
客戶端發(fā)送內(nèi)容為:Third
在該程序中,比較明顯的體現(xiàn)出了“請求-響應(yīng)”模型,也就是在客戶端發(fā)起連接以后,首先發(fā)送字符串“First”給服務(wù)器端,服務(wù)器端輸出客戶端發(fā)送的內(nèi)容“First”,然后將客戶端發(fā)送的內(nèi)容再反饋給客戶端,這樣客戶端也輸出服務(wù)器反饋“First”,這樣就完成了客戶端和服務(wù)器端的一次對話,緊接著客戶端發(fā)送“Second”給服務(wù)器端,服務(wù)端輸出“Second”,然后將“Second”再反饋給客戶端,客戶端再輸出“Second”,從而完成第二次會話,第三次會話的過程和這個一樣。在這個過程中,每次都是客戶端程序首先發(fā)送數(shù)據(jù)給服務(wù)器端,服務(wù)器接收數(shù)據(jù)以后,將結(jié)果反饋給客戶端,客戶端接收到服務(wù)器端的反饋,從而完成一次通訊過程。
在該示例中,雖然解決了多次發(fā)送的問題,但是客戶端和服務(wù)器端的次數(shù)控制還不夠靈活,如果客戶端的次數(shù)不固定怎么辦呢?是否可以使用某個特殊的字符串,例如quit,表示客戶端退出呢,這就涉及到網(wǎng)絡(luò)協(xié)議的內(nèi)容了,會在后續(xù)的網(wǎng)絡(luò)應(yīng)用示例部分詳細介紹。下面開始介紹另外一個網(wǎng)絡(luò)編程的突出問題。
2、如何使服務(wù)器端支持多個客戶端同時工作?
前面介紹的服務(wù)器端程序,只是實現(xiàn)了概念上的服務(wù)器端,離實際的服務(wù)器端程序結(jié)構(gòu)距離還很遙遠,如果需要讓服務(wù)器端能夠?qū)嶋H使用,那么最需要解決的問題就是——如何支持多個客戶端同時工作。
一個服務(wù)器端一般都需要同時為多個客戶端提供通訊,如果需要同時支持多個客戶端,則必須使用前面介紹的線程的概念。簡單來說,也就是當服務(wù)器端接收到一個連接時,啟動一個專門的線程處理和該客戶端的通訊。
按照這個思路改寫的服務(wù)端示例程序?qū)⒂蓛蓚€部分組成,MulThreadSocketServer類實現(xiàn)服務(wù)器端控制,實現(xiàn)接收客戶端連接,然后開啟專門的邏輯線程處理該連接,LogicThread類實現(xiàn)對于一個客戶端連接的邏輯處理,將處理的邏輯放置在該類的run方法中。該示例的代碼實現(xiàn)為:
package tcp;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 支持多客戶端的服務(wù)器端實現(xiàn)
*/
public class MulThreadSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
//監(jiān)聽端口號
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
System.out.println("服務(wù)器已啟動:");
while(true){
//獲得連接
socket = serverSocket.accept();
//啟動線程
new LogicThread(socket);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關(guān)閉連接
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,實現(xiàn)了一個while形式的死循環(huán),由于accept方法是阻塞方法,所以當客戶端連接未到達時,將阻塞該程序的執(zhí)行,當客戶端到達時接收該連接,并啟動一個新的LogicThread線程處理該連接,然后按照循環(huán)的執(zhí)行流程,繼續(xù)等待下一個客戶端連接。這樣當任何一個客戶端連接到達時,都開啟一個專門的線程處理,通過多個線程支持多個客戶端同時處理。
下面再看一下LogicThread線程類的源代碼實現(xiàn):
package tcp;
import java.io.*;
import java.net.*;
/**
* 服務(wù)器端邏輯線程
*/
public class LogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public LogicThread(Socket socket){
this.socket = socket;
start(); //啟動線程
}
public void run(){
byte[] b = new byte[1024];
try{
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
for(int i = 0;i < 3;i++){
//讀取數(shù)據(jù)
int n = is.read(b);
//邏輯處理
byte[] response = logic(b,0,n);
//反饋數(shù)據(jù)
os.write(response);
}
}catch(Exception e){
e.printStackTrace();
}finally{
close();
}
}
/**
* 關(guān)閉流和連接
*/
private void close(){
try{
//關(guān)閉流和連接
os.close();
is.close();
socket.close();
}catch(Exception e){}
}
/**
* 邏輯處理方法,實現(xiàn)echo邏輯
* @param b 客戶端發(fā)送數(shù)據(jù)緩沖區(qū)
* @param off 起始下標
* @param len 有效數(shù)據(jù)長度
* @return
*/
private byte[] logic(byte[] b,int off,int len){
byte[] response = new byte[len];
//將有效數(shù)據(jù)拷貝到數(shù)組response中
System.arraycopy(b, 0, response, 0, len);
return response;
}
}
在該示例代碼中,每次使用一個連接對象構(gòu)造該線程,該連接對象就是該線程需要處理的連接,在線程構(gòu)造完成以后,該線程就被啟動起來了,然后在run方法內(nèi)部對客戶端連接進行處理,數(shù)據(jù)交換的邏輯和前面的示例代碼一致,只是這里將接收到客戶端發(fā)送過來的數(shù)據(jù)并進行處理的邏輯封裝成了logic方法,按照前面介紹的IO編程的內(nèi)容,客戶端發(fā)送過來的內(nèi)容存儲在數(shù)組b的起始下標為0,長度為n個中,這些數(shù)據(jù)是客戶端發(fā)送過來的有效數(shù)據(jù),將有效的數(shù)據(jù)傳遞給logic方法,logic方法實現(xiàn)的是echo服務(wù)的邏輯,也就是將客戶端發(fā)送的有效數(shù)據(jù)形成以后新的response數(shù)組,并作為返回值反饋。
在線程中將logic方法的返回值反饋給客戶端,這樣就完成了服務(wù)器端的邏輯處理模擬,其他的實現(xiàn)和前面的介紹類似,這里就不在重復了。
這里的示例還只是基礎(chǔ)的服務(wù)器端實現(xiàn),在實際的服務(wù)器端實現(xiàn)中,由于硬件和端口數(shù)的限制,所以不能無限制的創(chuàng)建線程對象,而且頻繁的創(chuàng)建線程對象效率也比較低,所以程序中都實現(xiàn)了線程池來提高程序的執(zhí)行效率。
這里簡單介紹一下線程池的概念,線程池(Thread pool)是池技術(shù)的一種,就是在程序啟動時首先把需要個數(shù)的線程對象創(chuàng)建好,例如創(chuàng)建5000個線程對象,然后當客戶端連接到達時從池中取出一個已經(jīng)創(chuàng)建完成的線程對象使用即可。當客戶端連接關(guān)閉以后,將該線程對象重新放入到線程池中供其它的客戶端重復使用,這樣可以提高程序的執(zhí)行速度,優(yōu)化程序?qū)τ趦?nèi)存的占用等。