JAVA的网络编程【转】
轉(zhuǎn)自?http://www.cnblogs.com/springcsc/archive/2009/12/03/1616413.html
網(wǎng)絡(luò)編程
?????????網(wǎng)絡(luò)編程對于很多的初學(xué)者來說,都是很向往的一種編程技能,但是很多的初學(xué)者卻因?yàn)楹荛L一段時(shí)間無法進(jìn)入網(wǎng)絡(luò)編程的大門而放棄了對于該部分技術(shù)的學(xué)習(xí)。
?????????在 學(xué)習(xí)網(wǎng)絡(luò)編程以前,很多初學(xué)者可能覺得網(wǎng)絡(luò)編程是比較復(fù)雜的系統(tǒng)工程,需要了解很多和網(wǎng)絡(luò)相關(guān)的基礎(chǔ)知識,其實(shí)這些都不是很必需的。首先來問一個(gè)問題:你 會打手機(jī)嗎?很多人可能說肯定會啊,不就是按按電話號碼,撥打電話嘛,很簡單的事情啊!其實(shí)初學(xué)者如果入門網(wǎng)絡(luò)編程的話也可以做到這么簡單!
?????????網(wǎng)絡(luò)編程就是在兩個(gè)或兩個(gè)以上的設(shè)備(例如計(jì)算機(jī))之間傳輸數(shù)據(jù)。程序員所作的事情就是把數(shù)據(jù)發(fā)送到指定的位置,或者接收到指定的數(shù)據(jù),這個(gè)就是狹義的網(wǎng)絡(luò)編程范疇。在發(fā)送和接收數(shù)據(jù)時(shí),大部分的程序設(shè)計(jì)語言都設(shè)計(jì)了專門的API實(shí)現(xiàn)這些功能,程序員只需要調(diào)用即可。所以,基礎(chǔ)的網(wǎng)絡(luò)編程可以和打電話一樣簡單。
?????????下面就開始Java語言的網(wǎng)絡(luò)編程技術(shù)學(xué)習(xí)吧。
13.1?網(wǎng)絡(luò)概述
?????????網(wǎng) 絡(luò)編程技術(shù)是當(dāng)前一種主流的編程技術(shù),隨著聯(lián)網(wǎng)趨勢的逐步增強(qiáng)以及網(wǎng)絡(luò)應(yīng)用程序的大量出現(xiàn),所以在實(shí)際的開發(fā)中網(wǎng)絡(luò)編程技術(shù)獲得了大量的使用。本章中以淺 顯的基礎(chǔ)知識說明和實(shí)際的案例使廣大初學(xué)者能夠進(jìn)入網(wǎng)絡(luò)編程技術(shù)的大門,至于以后的實(shí)際修行就要閱讀進(jìn)階的書籍以及進(jìn)行大量的實(shí)際練習(xí)。
13.1.1?計(jì)算機(jī)網(wǎng)絡(luò)概述
?????????網(wǎng)絡(luò)編程的實(shí)質(zhì)就是兩個(gè)(或多個(gè))設(shè)備(例如計(jì)算機(jī))之間的數(shù)據(jù)傳輸。
?????????按照計(jì)算機(jī)網(wǎng)絡(luò)的定義,通過一定的物理設(shè)備將處于不同位置的計(jì)算機(jī)連接起來組成的網(wǎng)絡(luò),這個(gè)網(wǎng)絡(luò)中包含的設(shè)備有:計(jì)算機(jī)、路由器、交換機(jī)等等。
?????????其實(shí)從軟件編程的角度來說,對于物理設(shè)備的理解不需要很深刻,就像你打電話時(shí)不需要很熟悉通信網(wǎng)絡(luò)的底層實(shí)現(xiàn)是一樣的,但是當(dāng)深入到網(wǎng)絡(luò)編程的底層時(shí),這些基礎(chǔ)知識是必須要補(bǔ)的。
?????????路由器和交換機(jī)組成了核心的計(jì)算機(jī)網(wǎng)絡(luò),計(jì)算機(jī)只是這個(gè)網(wǎng)絡(luò)上的節(jié)點(diǎn)以及控制等,通過光纖、網(wǎng)線等連接將設(shè)備連接起來,從而形成了一張巨大的計(jì)算機(jī)網(wǎng)絡(luò)。
?????????網(wǎng)絡(luò)最主要的優(yōu)勢在于共享:共享設(shè)備和數(shù)據(jù),現(xiàn)在共享設(shè)備最常見的是打印機(jī),一個(gè)公司一般一個(gè)打印機(jī)即可,共享數(shù)據(jù)就是將大量的數(shù)據(jù)存儲在一組機(jī)器中,其它的計(jì)算機(jī)通過網(wǎng)絡(luò)訪問這些數(shù)據(jù),例如網(wǎng)站、銀行服務(wù)器等等。
?????????如果需要了解更多的網(wǎng)絡(luò)硬件基礎(chǔ)知識,可以閱讀《計(jì)算機(jī)網(wǎng)絡(luò)》教材,對于基礎(chǔ)進(jìn)行強(qiáng)化,這個(gè)在基礎(chǔ)學(xué)習(xí)階段不是必須的,但是如果想在網(wǎng)絡(luò)編程領(lǐng)域有所造詣,則是一個(gè)必須的基本功。
?????????對于網(wǎng)絡(luò)編程來說,最主要的是計(jì)算機(jī)和計(jì)算機(jī)之間的通信,這樣首要的問題就是如何找到網(wǎng)絡(luò)上的計(jì)算機(jī)呢?這就需要了解IP地址的概念。
?????????為了能夠方便的識別網(wǎng)絡(luò)上的每個(gè)設(shè)備,網(wǎng)絡(luò)中的每個(gè)設(shè)備都會有一個(gè)唯一的數(shù)字標(biāo)識,這個(gè)就是IP地址。在計(jì)算機(jī)網(wǎng)絡(luò)中,現(xiàn)在命名IP地址的規(guī)定是IPv4協(xié)議,該協(xié)議規(guī)定每個(gè)IP地址由4個(gè)0-255之間的數(shù)字組成,例如10.0.120.34。每個(gè)接入網(wǎng)絡(luò)的計(jì)算機(jī)都擁有唯一的IP地址,這個(gè)IP地址可能是固定的,例如網(wǎng)絡(luò)上各種各樣的服務(wù)器,也可以是動(dòng)態(tài)的,例如使用ADSL撥號上網(wǎng)的寬帶用戶,無論以何種方式獲得或是否是固定的,每個(gè)計(jì)算機(jī)在聯(lián)網(wǎng)以后都擁有一個(gè)唯一的合法IP地址,就像每個(gè)手機(jī)號碼一樣。
?????????但是由于IP地址不容易記憶,所以為了方便記憶,有創(chuàng)造了另外一個(gè)概念——域名(Domain Name),例如sohu.com等。一個(gè)IP地址可以對應(yīng)多個(gè)域名,一個(gè)域名只能對應(yīng)一個(gè)IP地址。域名的概念可以類比手機(jī)中的通訊簿,由于手機(jī)號碼不方便記憶,所以添加一個(gè)姓名標(biāo)識號碼,在實(shí)際撥打電話時(shí)可以選擇該姓名,然后撥打即可。
?????????在網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù),全部是以IP地址作為地址標(biāo)識,所以在實(shí)際傳輸數(shù)據(jù)以前需要將域名轉(zhuǎn)換為IP地址,實(shí)現(xiàn)這種功能的服務(wù)器稱之為DNS服務(wù)器,也就是通俗的說法叫做域名解析。例如當(dāng)用戶在瀏覽器輸入域名時(shí),瀏覽器首先請求DNS服務(wù)器,將域名轉(zhuǎn)換為IP地址,然后將轉(zhuǎn)換后的IP地址反饋給瀏覽器,然后再進(jìn)行實(shí)際的數(shù)據(jù)傳輸。
?????????當(dāng)DNS服務(wù)器正常工作時(shí),使用IP地址或域名都可以很方便的找到計(jì)算機(jī)網(wǎng)絡(luò)中的某個(gè)設(shè)備,例如服務(wù)器計(jì)算機(jī)。當(dāng)DNS不正常工作時(shí),只能通過IP地址訪問該設(shè)備。所以IP地址的使用要比域名通用一些。
???????? IP地址和域名很好的解決了在網(wǎng)絡(luò)中找到一個(gè)計(jì)算機(jī)的問題,但是為了讓一個(gè)計(jì)算機(jī)可以同時(shí)運(yùn)行多個(gè)網(wǎng)絡(luò)程序,就引入了另外一個(gè)概念——端口(port)。
?????????在介紹端口的概念以前,首先來看一個(gè)例子,一般一個(gè)公司前臺會有一個(gè)電話,每個(gè)員工會有一個(gè)分機(jī),這樣如果需要找到這個(gè)員工的話,需要首先撥打前臺總機(jī),然后轉(zhuǎn)該分機(jī)號即可。這樣減少了公司的開銷,也方便了每個(gè)員工。在該示例中前臺總機(jī)的電話號碼就相當(dāng)于IP地址,而每個(gè)員工的分機(jī)號就相當(dāng)于端口。
?????????有了端口的概念以后,在同一個(gè)計(jì)算機(jī)中每個(gè)程序?qū)?yīng)唯一的端口,這樣一個(gè)計(jì)算機(jī)上就可以通過端口區(qū)分發(fā)送給每個(gè)端口的數(shù)據(jù)了,換句話說,也就是一個(gè)計(jì)算機(jī)上可以并發(fā)運(yùn)行多個(gè)網(wǎng)絡(luò)程序,而不會在互相之間產(chǎn)生干擾。
?????????在硬件上規(guī)定,端口的號碼必須位于0-65535之間,每個(gè)端口唯一的對應(yīng)一個(gè)網(wǎng)絡(luò)程序,一個(gè)網(wǎng)絡(luò)程序可以使用多個(gè)端口。這樣一個(gè)網(wǎng)絡(luò)程序運(yùn)行在一臺計(jì)算上時(shí),不管是客戶端還是服務(wù)器,都是至少占用一個(gè)端口進(jìn)行網(wǎng)絡(luò)通訊。在接收數(shù)據(jù)時(shí),首先發(fā)送給對應(yīng)的計(jì)算機(jī),然后計(jì)算機(jī)根據(jù)端口把數(shù)據(jù)轉(zhuǎn)發(fā)給對應(yīng)的程序。
?????????有了IP地址和端口的概念以后,在進(jìn)行網(wǎng)絡(luò)通訊交換時(shí),就可以通過IP地址查找到該臺計(jì)算機(jī),然后通過端口標(biāo)識這臺計(jì)算機(jī)上的一個(gè)唯一的程序。這樣就可以進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的交換了。
?????????但是,進(jìn)行網(wǎng)絡(luò)編程時(shí),只有IP地址和端口的概念還是不夠的,下面就介紹一下基礎(chǔ)的網(wǎng)絡(luò)編程相關(guān)的軟件基礎(chǔ)知識。
13.1. 2?網(wǎng)絡(luò)編程概述
?????????按照前面的介紹,網(wǎng)絡(luò)編程就是兩個(gè)或多個(gè)設(shè)備之間的數(shù)據(jù)交換,其實(shí)更具體的說,網(wǎng)絡(luò)編程就是兩個(gè)或多個(gè)程序之間的數(shù)據(jù)交換,和普通的單機(jī)程序相比,網(wǎng)絡(luò)程序最大的不同就是需要交換數(shù)據(jù)的程序運(yùn)行在不同的計(jì)算機(jī)上,這樣就造成了數(shù)據(jù)交換的復(fù)雜。雖然通過IP地址和端口可以找到網(wǎng)絡(luò)上運(yùn)行的一個(gè)程序,但是如果需要進(jìn)行網(wǎng)絡(luò)編程,則還需要了解網(wǎng)絡(luò)通訊的過程。
?????????網(wǎng)絡(luò)通訊基于“請求-響應(yīng)”模型。為了理解這個(gè)模型,先來看一個(gè)例子,經(jīng)常看電視的人肯定見過審訊的場面吧,一般是這樣的:
???????????????????警察:姓名
???????????????????嫌疑犯:XXX
???????????????????警察:性別
???????????????????嫌疑犯:男
???????????????????警察:年齡
???????????????????嫌疑犯:29
???????????????????……
?????????在這個(gè)例子中,警察問一句,嫌疑犯回答一句,如果警察不問,則嫌疑犯保持沉默。這種一問一答的形式就是網(wǎng)絡(luò)中的“請求-響應(yīng)”模型。也就是通訊的一端發(fā)送數(shù)據(jù),另外一端反饋數(shù)據(jù),網(wǎng)絡(luò)通訊都基于該模型。
?????????在網(wǎng)絡(luò)通訊中,第一次主動(dòng)發(fā)起通訊的程序被稱作客戶端(Client)程序,簡稱客戶端,而在第一次通訊中等待連接的程序被稱作服務(wù)器端(Server)程序,簡稱服務(wù)器。一旦通訊建立,則客戶端和服務(wù)器端完全一樣,沒有本質(zhì)的區(qū)別。
?????????由此,網(wǎng)絡(luò)編程中的兩種程序就分別是客戶端和服務(wù)器端,例如QQ程序,每個(gè)QQ用戶安裝的都是QQ客戶端程序,而QQ服務(wù)器端程序則運(yùn)行在騰訊公司的機(jī)房中,為大量的QQ用戶提供服務(wù)。這種網(wǎng)絡(luò)編程的結(jié)構(gòu)被稱作客戶端/服務(wù)器結(jié)構(gòu),也叫做Client/Server結(jié)構(gòu),簡稱C/S結(jié)構(gòu)。
?????????使用C/S結(jié) 構(gòu)的程序,在開發(fā)時(shí)需要分別開發(fā)客戶端和服務(wù)器端,這種結(jié)構(gòu)的優(yōu)勢在于由于客戶端是專門開發(fā)的,所以根據(jù)需要實(shí)現(xiàn)各種效果,專業(yè)點(diǎn)說就是表現(xiàn)力豐富,而服 務(wù)器端也需要專門進(jìn)行開發(fā)。但是這種結(jié)構(gòu)也存在著很多不足,例如通用性差,幾乎不能通用等,也就是說一種程序的客戶端只能和對應(yīng)的服務(wù)器端通訊,而不能和 其它服務(wù)器端通訊,在實(shí)際維護(hù)時(shí),也需要維護(hù)專門的客戶端和服務(wù)器端,維護(hù)的壓力比較大。
?????????其實(shí)在運(yùn)行很多程序時(shí),沒有必要使用專用的客戶端,而需要使用通用的客戶端,例如瀏覽器,使用瀏覽器作為客戶端的結(jié)構(gòu)被稱作瀏覽器/服務(wù)器結(jié)構(gòu),也叫做Browser/Server結(jié)構(gòu),簡稱為B/S結(jié)構(gòu)。
?????????使用B/S結(jié)構(gòu)的程序,在開發(fā)時(shí)只需要開發(fā)服務(wù)器端即可,這種結(jié)構(gòu)的優(yōu)勢在于開發(fā)的壓力比較小,不需要維護(hù)客戶端。但是這種結(jié)構(gòu)也存在著很多不足,例如瀏覽器的限制比較大,表現(xiàn)力不強(qiáng),無法進(jì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)其實(shí)也就是一種特殊的C/S結(jié)構(gòu)。
?????????另外簡單的介紹一下P2P(Point to Point)程序,常見的如BT、電驢等。P2P程序是一種特殊的程序,應(yīng)該一個(gè)P2P程序中既包含客戶端程序,也包含服務(wù)器端程序,例如BT,使用客戶端程序部分連接其它的種子(服務(wù)器端),而使用服務(wù)器端向其它的BT客戶端傳輸數(shù)據(jù)。如果這個(gè)還不是很清楚,其實(shí)P2P程序和手機(jī)是一樣的,當(dāng)手機(jī)撥打電話時(shí)就是使用客戶端的作用,而手機(jī)處于待機(jī)狀態(tài)時(shí),可以接收到其它用戶撥打的電話則起的就是服務(wù)器端的功能,只是一般的手機(jī)不能同時(shí)使用撥打電話和接聽電話的功能,而P2P程序?qū)崿F(xiàn)了該功能。
?????????最后再介紹一個(gè)網(wǎng)絡(luò)編程中最重要,也是最復(fù)雜的概念——協(xié)議(Protocol)。按照前面的介紹,網(wǎng)絡(luò)編程就是運(yùn)行在不同計(jì)算機(jī)中兩個(gè)程序之間的數(shù)據(jù)交換。在實(shí)際進(jìn)行數(shù)據(jù)交換時(shí),為了讓接收端理解該數(shù)據(jù),計(jì)算機(jī)比較笨,什么都不懂的,那么就需要規(guī)定該數(shù)據(jù)的格式,這個(gè)數(shù)據(jù)的格式就是協(xié)議。
?????????如 果沒有理解協(xié)議的概念,那么再舉一個(gè)例子,記得有個(gè)電影叫《永不消逝的電波》,講述的是地下黨通過電臺發(fā)送情報(bào)的故事,這里我們不探討電影的劇情,而只關(guān) 心電臺發(fā)送的數(shù)據(jù)。在實(shí)際發(fā)報(bào)時(shí),需要首先將需要發(fā)送的內(nèi)容轉(zhuǎn)換為電報(bào)編碼,然后將電報(bào)編碼發(fā)送出去,而接收端接收的是電報(bào)編碼,如果需要理解電報(bào)的內(nèi)容 則需要根據(jù)密碼本翻譯出該電報(bào)的內(nèi)容。這里的密碼本就規(guī)定了一種數(shù)據(jù)格式,這種對于網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)格式在網(wǎng)絡(luò)編程中就被稱作協(xié)議。
?????????那么如何來編寫協(xié)議格式呢?答案是隨意。只要按照這種協(xié)議格式能夠生成唯一的編碼,按照該編碼可以唯一的解析出發(fā)送數(shù)據(jù)的內(nèi)容即可。也正因?yàn)楦鱾€(gè)網(wǎng)絡(luò)程序之間協(xié)議格式的不同,所以才導(dǎo)致了客戶端程序都是專用的結(jié)構(gòu)。
?????????在實(shí)際的網(wǎng)絡(luò)程序編程中,最麻煩的內(nèi)容不是數(shù)據(jù)的發(fā)送和接收,因?yàn)檫@個(gè)功能在幾乎所有的程序語言中都提供了封裝好的API進(jìn)行調(diào)用,最麻煩的內(nèi)容就是協(xié)議的設(shè)計(jì)以及協(xié)議的生產(chǎn)和解析,這個(gè)才是網(wǎng)絡(luò)編程中最核心的內(nèi)容。
?????????關(guān)于網(wǎng)絡(luò)編程的基礎(chǔ)知識,就介紹這里,深刻理解IP地址、端口和協(xié)議等概念,將會極大的有助于后續(xù)知識的學(xué)習(xí)。
13.1.3?網(wǎng)絡(luò)通訊方式
?????????在現(xiàn)有的網(wǎng)絡(luò)中,網(wǎng)絡(luò)通訊的方式主要有兩種:
1、?TCP(傳輸控制協(xié)議)方式
2、?UDP(用戶數(shù)據(jù)報(bào)協(xié)議)方式
為 了方便理解這兩種方式,還是先來看一個(gè)例子。大家使用手機(jī)時(shí),向別人傳遞信息時(shí)有兩種方式:撥打電話和發(fā)送短信。使用撥打電話的方式可以保證將信息傳遞給 別人,因?yàn)閯e人接聽電話時(shí)本身就確認(rèn)接收到了該信息。而發(fā)送短信的方式價(jià)格低廉,使用方便,但是接收人有可能接收不到。
在網(wǎng)絡(luò)通訊中,TCP方式就類似于撥打電話,使用該種方式進(jìn)行網(wǎng)絡(luò)通訊時(shí),需要建立專門的虛擬連接,然后進(jìn)行可靠的數(shù)據(jù)傳輸,如果數(shù)據(jù)發(fā)送失敗,則客戶端會自動(dòng)重發(fā)該數(shù)據(jù)。而UDP方式就類似于發(fā)送短信,使用這種方式進(jìn)行網(wǎng)絡(luò)通訊時(shí),不需要建立專門的虛擬連接,傳輸也不是很可靠,如果發(fā)送失敗則客戶端無法獲得。
這兩種傳輸方式都是實(shí)際的網(wǎng)絡(luò)編程中進(jìn)行使用,重要的數(shù)據(jù)一般使用TCP方式進(jìn)行數(shù)據(jù)傳輸,而大量的非核心數(shù)據(jù)則都通過UDP方式進(jìn)行傳遞,在一些程序中甚至結(jié)合使用這兩種方式進(jìn)行數(shù)據(jù)的傳遞。
由于TCP需要建立專用的虛擬連接以及確認(rèn)傳輸是否正確,所以使用TCP方式的速度稍微慢一些,而且傳輸時(shí)產(chǎn)生的數(shù)據(jù)量要比UDP稍微大一些。
?????????關(guān)于網(wǎng)絡(luò)編程的基礎(chǔ)知識就介紹這么多,如果需要深入了解相關(guān)知識請閱讀專門的計(jì)算機(jī)網(wǎng)絡(luò)書籍,下面開始介紹Java語言中網(wǎng)絡(luò)編程的相關(guān)技術(shù)。
?
?
13.2?網(wǎng)絡(luò)編程技術(shù)
?????????前面介紹了網(wǎng)絡(luò)編程的相關(guān)基礎(chǔ)知識,初步建立了網(wǎng)絡(luò)編程的概念,但是實(shí)際學(xué)習(xí)網(wǎng)絡(luò)編程還必須使用某種程序設(shè)計(jì)語言進(jìn)行代碼實(shí)現(xiàn),下面就介紹一下網(wǎng)絡(luò)編程的代碼實(shí)現(xiàn)。
13.2.1?網(wǎng)絡(luò)編程步驟
?????????按照前面的基礎(chǔ)知識介紹,無論使用TCP方式還是UDP方式進(jìn)行網(wǎng)絡(luò)通訊,網(wǎng)絡(luò)編程都是由客戶端和服務(wù)器端組成。當(dāng)然,B/S結(jié)構(gòu)的編程中只需要實(shí)現(xiàn)服務(wù)器端即可。所以,下面介紹網(wǎng)絡(luò)編程的步驟時(shí),均以C/S結(jié)構(gòu)為基礎(chǔ)進(jìn)行介紹。
?????????說明:這里的步驟實(shí)現(xiàn)和語言無關(guān),也就是說,這個(gè)步驟適用于各種語言實(shí)現(xiàn),不局限于Java語言。
13.2.1.1?客戶端網(wǎng)絡(luò)編程步驟
?????????客戶端(Client)是指網(wǎng)絡(luò)編程中首先發(fā)起連接的程序,客戶端一般實(shí)現(xiàn)程序界面和基本邏輯實(shí)現(xiàn),在進(jìn)行實(shí)際的客戶端編程時(shí),無論客戶端復(fù)雜還是簡單,以及客戶端實(shí)現(xiàn)的方式,客戶端的編程主要由三個(gè)步驟實(shí)現(xiàn):
1、?建立網(wǎng)絡(luò)連接
客戶端網(wǎng)絡(luò)編程的第一步都是建立網(wǎng)絡(luò)連接。在建立網(wǎng)絡(luò)連接時(shí)需要指定連接到的服務(wù)器的IP地址和端口號,建立完成以后,會形成一條虛擬的連接,后續(xù)的操作就可以通過該連接實(shí)現(xiàn)數(shù)據(jù)交換了。
2、?交換數(shù)據(jù)
連接建立以后,就可以通過這個(gè)連接交換數(shù)據(jù)了。交換數(shù)據(jù)嚴(yán)格按照請求響應(yīng)模型進(jìn)行,由客戶端發(fā)送一個(gè)請求數(shù)據(jù)到服務(wù)器,服務(wù)器反饋一個(gè)響應(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ò)編程。
?????????最基本的步驟一般都是這三個(gè)步驟,在實(shí)際實(shí)現(xiàn)時(shí),步驟2會出現(xiàn)重復(fù),在進(jìn)行代碼組織時(shí),由于網(wǎng)絡(luò)編程是比較耗時(shí)的操作,所以一般開啟專門的現(xiàn)場進(jìn)行網(wǎng)絡(luò)通訊。
13.2.1.2?服務(wù)器端網(wǎng)絡(luò)編程步驟
?????????服務(wù)器端(Server)是指在網(wǎng)絡(luò)編程中被動(dòng)等待連接的程序,服務(wù)器端一般實(shí)現(xiàn)程序的核心邏輯以及數(shù)據(jù)存儲等核心功能。服務(wù)器端的編程步驟和客戶端不同,是由四個(gè)步驟實(shí)現(xiàn),依次是:
1、?監(jiān)聽端口
服務(wù)器端屬于被動(dòng)等待連接,所以服務(wù)器端啟動(dòng)以后,不需要發(fā)起連接,而只需要監(jiān)聽本地計(jì)算機(jī)的某個(gè)固定端口即可。
這個(gè)端口就是服務(wù)器端開放給客戶端的端口,服務(wù)器端程序運(yùn)行的本地計(jì)算機(jī)的IP地址就是服務(wù)器端程序的IP地址。
2、?獲得連接
當(dāng)客戶端連接到服務(wù)器端時(shí),服務(wù)器端就可以獲得一個(gè)連接,這個(gè)連接包含客戶端的信息,例如客戶端IP地址等等,服務(wù)器端和客戶端也通過該連接進(jìn)行數(shù)據(jù)交換。
一般在服務(wù)器端編程中,當(dāng)獲得連接時(shí),需要開啟專門的線程處理該連接,每個(gè)連接都由獨(dú)立的線程實(shí)現(xiàn)。
3、?交換數(shù)據(jù)
服務(wù)器端通過獲得的連接進(jìn)行數(shù)據(jù)交換。服務(wù)器端的數(shù)據(jù)交換步驟是首先接收客戶端發(fā)送過來的數(shù)據(jù),然后進(jìn)行邏輯處理,再把處理以后的結(jié)果數(shù)據(jù)發(fā)送給客戶端。簡單來說,就是先接收再發(fā)送,這個(gè)和客戶端的數(shù)據(jù)交換數(shù)序不同。
其實(shí),服務(wù)器端獲得的連接和客戶端連接是一樣的,只是數(shù)據(jù)交換的步驟不同。
當(dāng)然,服務(wù)器端的數(shù)據(jù)交換也是可以多次進(jìn)行的。
在數(shù)據(jù)交換完成以后,關(guān)閉和客戶端的連接。
4、?關(guān)閉連接
當(dāng)服務(wù)器程序關(guān)閉時(shí),需要關(guān)閉服務(wù)器端,通過關(guān)閉服務(wù)器端使得服務(wù)器監(jiān)聽的端口以及占用的內(nèi)存可以釋放出來,實(shí)現(xiàn)了連接的關(guān)閉。
?????????其實(shí)服務(wù)器端編程的模型和呼叫中心的實(shí)現(xiàn)是類似的,例如移動(dòng)的客服電話10086就是典型的呼叫中心,當(dāng)一個(gè)用戶撥打10086時(shí),轉(zhuǎn)接給一個(gè)專門的客服人員,由該客服實(shí)現(xiàn)和該用戶的問題解決,當(dāng)另外一個(gè)用戶撥打10086時(shí),則轉(zhuǎn)接給另一個(gè)客服,實(shí)現(xiàn)問題解決,依次類推。
?????????在服務(wù)器端編程時(shí),10086這個(gè)電話號碼就類似于服務(wù)器端的端口號碼,每個(gè)用戶就相當(dāng)于一個(gè)客戶端程序,每個(gè)客服人員就相當(dāng)于服務(wù)器端啟動(dòng)的專門和客戶端連接的線程,每個(gè)線程都是獨(dú)立進(jìn)行交互的。
?????????這就是服務(wù)器端編程的模型,只是TCP方式是需要建立連接的,對于服務(wù)器端的壓力比較大,而UDP是不需要建立連接的,對于服務(wù)器端的壓力比較小罷了。
13.2.1.3?小結(jié)
?????????總之,無論使用任何語言,任何方式進(jìn)行基礎(chǔ)的網(wǎng)絡(luò)編程,都必須遵循固定的步驟進(jìn)行操作,在熟悉了這些步驟以后,可以根據(jù)需要進(jìn)行邏輯上的處理,但是還是必須遵循固定的步驟進(jìn)行。
?????????其實(shí),基礎(chǔ)的網(wǎng)絡(luò)編程本身不難,也不需要很多的基礎(chǔ)網(wǎng)絡(luò)知識,只是由于編程的基礎(chǔ)功能都已經(jīng)由API實(shí)現(xiàn),而且需要按照固定的步驟進(jìn)行,所以在入門時(shí)有一定的門檻,希望下面的內(nèi)容能夠?qū)⒛憧焖俚膸刖W(wǎng)絡(luò)編程技術(shù)的大門。
13.2.2 Java網(wǎng)絡(luò)編程技術(shù)
???????? Java語言是在網(wǎng)絡(luò)環(huán)境下誕生的,所以Java語言雖然不能說是對于網(wǎng)絡(luò)編程的支持最好的語言,但是必須說是一種對于網(wǎng)絡(luò)編程提供良好支持的語言,使用Java語言進(jìn)行網(wǎng)絡(luò)編程將是一件比較輕松的工作。
?????????和網(wǎng)絡(luò)編程有關(guān)的基本API位于java.net包中,該包中包含了基本的網(wǎng)絡(luò)編程實(shí)現(xiàn),該包是網(wǎng)絡(luò)編程的基礎(chǔ)。該包中既包含基礎(chǔ)的網(wǎng)絡(luò)編程類,也包含封裝后的專門處理WEB相關(guān)的處理類。在本章中,將只介紹基礎(chǔ)的網(wǎng)絡(luò)編程類。
?????????首先來介紹一個(gè)基礎(chǔ)的網(wǎng)絡(luò)類——InetAddress類。該類的功能是代表一個(gè)IP地址,并且將IP地址和域名相關(guān)的操作方法包含在該類的內(nèi)部。
?????????關(guān)于該類的使用,下面通過一個(gè)基礎(chǔ)的代碼示例演示該類的使用,代碼如下:
?????????????????? package inetaddressdemo;
import java.net.*;
/**
?*?演示InetAddress類的基本使用
?*/
public class InetAddressDemo {
???????? public static void main(String[] args) {
?????????????????? try{
??????????????????????????? //使用域名創(chuàng)建對象
??????????????????????????? InetAddress inet1 = InetAddress.getByName("www.163.com");
??????????????????????????? System.out.println(inet1);
??????????????????????????? //使用IP創(chuàng)建對象
??????????????????????????? InetAddress inet2 = InetAddress.getByName("127.0.0.1");
??????????????????????????? System.out.println(inet2);
??????????????????????????? //獲得本機(jī)地址對象
??????????????????????????? InetAddress inet3 = InetAddress.getLocalHost();
??????????????????????????? System.out.println(inet3);
??????????????????????????? //獲得對象中存儲的域名
??????????????????????????? String host = inet3.getHostName();
??????????????????????????? System.out.println("域名:" + host);
??????????????????????????? //獲得對象中存儲的IP
??????????????????????????? String ip = inet3.getHostAddress();
??????????????????????????? System.out.println("IP:" + ip);
?????????????????? }catch(Exception e){}
???????? }
}
?????????在該示例代碼中,演示了InetAddress類的基本使用,并使用了該類中的幾個(gè)常用方法,該代碼的執(zhí)行結(jié)果是:
???????????????????www.163.com/220.181.28.50
/127.0.0.1
chen/192.168.1.100
域名:chen
IP:192.168.1.100
?????????說明:由于該代碼中包含一個(gè)互聯(lián)網(wǎng)的網(wǎng)址,所以運(yùn)行該程序時(shí)需要聯(lián)網(wǎng),否則將產(chǎn)生異常。
?????????在后續(xù)的使用中,經(jīng)常包含需要使用InetAddress對象代表IP地址的構(gòu)造方法,當(dāng)然,該類的使用不是必須的,也可以使用字符串來代表IP地址進(jìn)行實(shí)現(xiàn)。
13.2.3 TCP編程
?????????按照前面的介紹,網(wǎng)絡(luò)通訊的方式有TCP和UDP兩種,其中TCP方式的網(wǎng)絡(luò)通訊是指在通訊的過程中保持連接,有點(diǎn)類似于打電話,只需要撥打一次號碼(建立一次網(wǎng)絡(luò)連接),就可以多次通話(多次傳輸數(shù)據(jù))。這樣方式在實(shí)際的網(wǎng)絡(luò)編程中,由于傳輸可靠,類似于打電話,如果甲給乙打電話,乙說沒有聽清楚讓甲重復(fù)一遍,直到乙聽清楚為止,實(shí)際的網(wǎng)絡(luò)傳輸也是這樣,如果發(fā)送的一方發(fā)送的數(shù)據(jù)接收方覺得有問題,則網(wǎng)絡(luò)底層會自動(dòng)要求發(fā)送方重發(fā),直到接收方收到為止。
?????????在Java語言中,對于TCP方式的網(wǎng)絡(luò)編程提供了良好的支持,在實(shí)際實(shí)現(xiàn)時(shí),以java.net.Socket類代表客戶端連接,以java.net.ServerSocket類代表服務(wù)器端連接。在進(jìn)行網(wǎng)絡(luò)編程時(shí),底層網(wǎng)絡(luò)通訊的細(xì)節(jié)已經(jīng)實(shí)現(xiàn)了比較高的封裝,所以在程序員實(shí)際編程時(shí),只需要指定IP地址和端口號碼就可以建立連接了。正是由于這種高度的封裝,一方面簡化了Java語言網(wǎng)絡(luò)編程的難度,另外也使得使用Java語言進(jìn)行網(wǎng)絡(luò)編程時(shí)無法深入到網(wǎng)絡(luò)的底層,所以使用Java語言進(jìn)行網(wǎng)絡(luò)底層系統(tǒng)編程很困難,具體點(diǎn)說,Java語言無法實(shí)現(xiàn)底層的網(wǎng)絡(luò)嗅探以及獲得IP包結(jié)構(gòu)等信息。但是由于Java語言的網(wǎng)絡(luò)編程比較簡單,所以還是獲得了廣泛的使用。
?????????在使用TCP方式進(jìn)行網(wǎng)絡(luò)編程時(shí),需要按照前面介紹的網(wǎng)絡(luò)編程的步驟進(jìn)行,下面分別介紹一下在Java語言中客戶端和服務(wù)器端的實(shí)現(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實(shí)現(xiàn)的是連接到IP地址是192.168.1.103的計(jì)算機(jī)的10000號端口,而socket2實(shí)現(xiàn)的是連接到域名是www.sohu.com的計(jì)算機(jī)的80號端口,至于底層網(wǎng)絡(luò)如何實(shí)現(xiàn)建立連接,對于程序員來說是完全透明的。如果建立連接時(shí),本機(jī)網(wǎng)絡(luò)不通,或服務(wù)器端程序未開啟,則會拋出異常。
連接一旦建立,則完成了客戶端編程的第一步,緊接著的步驟就是按照“請求-響應(yīng)”模型進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)交換,在Java語言中,數(shù)據(jù)傳輸功能由Java IO實(shí)現(xiàn),也就是說只需要從連接中獲得輸入流和輸出流即可,然后將需要發(fā)送的數(shù)據(jù)寫入連接對象的輸出流中,在發(fā)送完成以后從輸入流中讀取數(shù)據(jù)即可。示例代碼如下:
???????? OutputStream os = socket1.getOutputStream();?//獲得輸出流
???????? InputStream is = socket1.getInputStream();???? //獲得輸入流
上面的代碼中,分別從socket1這個(gè)連接對象獲得了輸出流和輸入流對象,在整個(gè)網(wǎng)絡(luò)編程中,后續(xù)的數(shù)據(jù)交換就變成了IO操作,也就是遵循“請求-響應(yīng)”模型的規(guī)定,先向輸出流中寫入數(shù)據(jù),這些數(shù)據(jù)會被系統(tǒng)發(fā)送出去,然后在從輸入流中讀取服務(wù)器端的反饋信息,這樣就完成了一次數(shù)據(jù)交換過程,當(dāng)然這個(gè)數(shù)據(jù)交換過程可以多次進(jìn)行。
這里獲得的只是最基本的輸出流和輸入流對象,還可以根據(jù)前面學(xué)習(xí)到的IO知識,使用流的嵌套將這些獲得到的基本流對象轉(zhuǎn)換成需要的裝飾流對象,從而方便數(shù)據(jù)的操作。
最后當(dāng)數(shù)據(jù)交換完成以后,關(guān)閉網(wǎng)絡(luò)連接,釋放網(wǎng)絡(luò)連接占用的系統(tǒng)端口和內(nèi)存等資源,完成網(wǎng)絡(luò)操作,示例代碼如下:
???????? socket1.close();
這就是最基本的網(wǎng)絡(luò)編程功能介紹。下面是一個(gè)簡單的網(wǎng)絡(luò)客戶端程序示例,該程序的作用是向服務(wù)器端發(fā)送一個(gè)字符串“Hello”,并將服務(wù)器端的反饋顯示到控制臺,數(shù)據(jù)交換只進(jìn)行一次,當(dāng)數(shù)據(jù)交換進(jìn)行完成以后關(guān)閉網(wǎng)絡(luò)連接,程序結(jié)束。實(shí)現(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) {}
?????????????????? }
???????? }
}
在該示例代碼中建立了一個(gè)連接到IP地址為127.0.0.1,端口號碼為10000的TCP類型的網(wǎng)絡(luò)連接,然后獲得連接的輸出流對象,將需要發(fā)送的字符串“Hello”轉(zhuǎn)換為byte數(shù)組寫入到輸出流中,由系統(tǒng)自動(dòng)完成將輸出流中的數(shù)據(jù)發(fā)送出去,如果需要強(qiáng)制發(fā)送,可以調(diào)用輸出流對象中的flush方法實(shí)現(xiàn)。在數(shù)據(jù)發(fā)送出去以后,從連接對象的輸入流中讀取服務(wù)器端的反饋信息,讀取時(shí)可以使用IO中的各種讀取方法進(jìn)行讀取,這里使用最簡單的方法進(jìn)行讀取,從輸入流中讀取到的內(nèi)容就是服務(wù)器端的反饋,并將讀取到的內(nèi)容在客戶端的控制臺進(jìn)行輸出,最后依次關(guān)閉打開的流對象和網(wǎng)絡(luò)連接對象。
這是一個(gè)簡單的功能示例,在該示例中演示了TCP類型的網(wǎng)絡(luò)客戶端基本方法的使用,該代碼只起演示目的,還無法達(dá)到實(shí)用的級別。
如果需要在控制臺下面編譯和運(yùn)行該代碼,需要首先在控制臺下切換到源代碼所在的目錄,然后依次輸入編譯和運(yùn)行命令:
???????? javac –d . SimpleSocketClient.java
???????? java tcp.SimpleSocketClient
和下面將要介紹的SimpleSocketServer服務(wù)器端組合運(yùn)行時(shí),程序的輸出結(jié)果為:
?????????服務(wù)器反饋:Hello
介紹完一個(gè)簡單的客戶端編程的示例,下面接著介紹一下TCP類型的服務(wù)器端的編寫。首先需要說明的是,客戶端的步驟和服務(wù)器端的編寫步驟不同,所以在學(xué)習(xí)服務(wù)器端編程時(shí)注意不要和客戶端混淆起來。
在服務(wù)器端程序編程中,由于服務(wù)器端實(shí)現(xiàn)的是被動(dòng)等待連接,所以服務(wù)器端編程的第一個(gè)步驟是監(jiān)聽端口,也就是監(jiān)聽是否有客戶端連接到達(dá)。實(shí)現(xiàn)服務(wù)器端監(jiān)聽的代碼為:
???????? ServerSocket ss = new ServerSocket(10000);
該代碼實(shí)現(xiàn)的功能是監(jiān)聽當(dāng)前計(jì)算機(jī)的10000號端口,如果在執(zhí)行該代碼時(shí),10000號端口已經(jīng)被別的程序占用,那么將拋出異常。否則將實(shí)現(xiàn)監(jiān)聽。
服務(wù)器端編程的第二個(gè)步驟是獲得連接。該步驟的作用是當(dāng)有客戶端連接到達(dá)時(shí),建立一個(gè)和客戶端連接對應(yīng)的Socket連 接對象,從而釋放客戶端連接對于服務(wù)器端端口的占用。實(shí)現(xiàn)功能就像公司的前臺一樣,當(dāng)一個(gè)客戶到達(dá)公司時(shí),會告訴前臺我找某某某,然后前臺就通知某某某, 然后就可以繼續(xù)接待其它客戶了。通過獲得連接,使得客戶端的連接在服務(wù)器端獲得了保持,另外使得服務(wù)器端的端口釋放出來,可以繼續(xù)等待其它的客戶端連接。 實(shí)現(xiàn)獲得連接的代碼是:
???????? Socket socket = ss.accept();
該代碼實(shí)現(xiàn)的功能是獲得當(dāng)前連接到服務(wù)器端的客戶端連接。需要說明的是accept和前面IO部分介紹的read方法一樣,都是一個(gè)阻塞方法,也就是當(dāng)無連接時(shí),該方法將阻塞程序的執(zhí)行,直到連接到達(dá)時(shí)才執(zhí)行該行代碼。另外獲得的連接會在服務(wù)器端的該端口注冊,這樣以后就可以通過在服務(wù)器端的注冊信息直接通信,而注冊以后服務(wù)器端的端口就被釋放出來,又可以繼續(xù)接受其它的連接了。
連接獲得以后,后續(xù)的編程就和客戶端的網(wǎng)絡(luò)編程類似了,這里獲得的Socket類型的連接就和客戶端的網(wǎng)絡(luò)連接一樣了,只是服務(wù)器端需要首先讀取發(fā)送過來的數(shù)據(jù),然后進(jìn)行邏輯處理以后再發(fā)送給客戶端,也就是交換數(shù)據(jù)的順序和客戶端交換數(shù)據(jù)的步驟剛好相反。這部分的內(nèi)容和客戶端很類似,所以就不重復(fù)了,如果還不熟悉,可以參看下面的示例代碼。
最后,在服務(wù)器端通信完成以后,關(guān)閉服務(wù)器端連接。實(shí)現(xiàn)的代碼為:
???????? ss.close();
這就是基本的TCP類型的服務(wù)器端編程步驟。下面以一個(gè)簡單的echo服務(wù)實(shí)現(xiàn)為例子,介紹綜合使用示例。echo的意思就是“回聲”,echo服務(wù)器端實(shí)現(xiàn)的功能就是將客戶端發(fā)送的內(nèi)容再原封不動(dòng)的反饋給客戶端。實(shí)現(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){}
?????????????????? }
???????? }
}
在該示例代碼中建立了一個(gè)監(jiān)聽當(dāng)前計(jì)算機(jī)10000號端口的服務(wù)器端Socket連接,然后獲得客戶端發(fā)送過來的連接,如果有連接到達(dá)時(shí),讀取連接中發(fā)送過來的內(nèi)容,并將發(fā)送的內(nèi)容在控制臺進(jìn)行輸出,輸出完成以后將客戶端發(fā)送的內(nèi)容再反饋給客戶端。最后關(guān)閉流和連接對象,結(jié)束程序。
在控制臺下面編譯和運(yùn)行該程序的命令和客戶端部分的類似。
這樣,就以一個(gè)很簡單的示例演示了TCP類型的網(wǎng)絡(luò)編程在Java語言中的基本實(shí)現(xiàn),這個(gè)示例只是演示了網(wǎng)絡(luò)編程的基本步驟以及各個(gè)功能方法的基本使用,只是為網(wǎng)絡(luò)編程打下了一個(gè)基礎(chǔ),下面將就幾個(gè)問題來深入介紹網(wǎng)絡(luò)編程深層次的一些知識。
?為了一步一步的掌握網(wǎng)絡(luò)編程,下面再研究網(wǎng)絡(luò)編程中的兩個(gè)基本問題,通過解決這兩個(gè)問題將對網(wǎng)絡(luò)編程的認(rèn)識深入一層。
1、如何復(fù)用Socket連接?
在前面的示例中,客戶端中建立了一次連接,只發(fā)送一次數(shù)據(jù)就關(guān)閉了,這就相當(dāng)于撥打電話時(shí),電話打通了只對話一次就關(guān)閉了,其實(shí)更加常用的應(yīng)該是撥通一次電話以后多次對話,這就是復(fù)用客戶端連接。
那 么如何實(shí)現(xiàn)建立一次連接,進(jìn)行多次數(shù)據(jù)交換呢?其實(shí)很簡單,建立連接以后,將數(shù)據(jù)交換的邏輯寫到一個(gè)循環(huán)中就可以了。這樣只要循環(huán)不結(jié)束則連接就不會被關(guān) 閉。按照這種思路,可以改造一下上面的代碼,讓該程序可以在建立連接一次以后,發(fā)送三次數(shù)據(jù),當(dāng)然這里的次數(shù)也可以是多次,示例代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
?*?復(fù)用連接的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ù)交換部分的邏輯寫在一個(gè)for循環(huán)的內(nèi)容,這樣就可以建立一次連接,依次將data數(shù)組中的數(shù)據(jù)按照順序發(fā)送給服務(wù)器端了。
???????????????????如果還是使用前面示例代碼中的服務(wù)器端程序運(yùn)行該程序,則該程序的結(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
顯然,客戶端在實(shí)際運(yùn)行時(shí)出現(xiàn)了異常,出現(xiàn)異常的原因是什么呢?如果仔細(xì)閱讀前面的代碼,應(yīng)該還記得前面示例代碼中的服務(wù)器端是對話一次數(shù)據(jù)以后就關(guān)閉了連接,如果服務(wù)器端程序關(guān)閉了,客戶端繼續(xù)發(fā)送數(shù)據(jù)肯定會出現(xiàn)異常,這就是出現(xiàn)該問題的原因。
按照客戶端實(shí)現(xiàn)的邏輯,也可以復(fù)用服務(wù)器端的連接,實(shí)現(xiàn)的原理也是將服務(wù)器端的數(shù)據(jù)交換邏輯寫在循環(huán)中即可,按照該種思路改造以后的服務(wù)器端代碼為:
???????? package tcp;
import java.io.*;
import java.net.*;
/**
?*?復(fù)用連接的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ù)器已啟動(dòng):");
??????????????????????????? //獲得連接
??????????????????????????? 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ā)送和接收的邏輯寫在了一個(gè)for循環(huán)內(nèi)部,只是在實(shí)現(xiàn)時(shí)硬性的將循環(huán)次數(shù)規(guī)定成了3次,這樣代碼雖然比較簡單,但是通用性比較差。
???????????????????以該服務(wù)器端代碼實(shí)現(xiàn)為基礎(chǔ)運(yùn)行前面的客戶端程序時(shí),客戶端的輸出為:
????????????????????????????服務(wù)器反饋:First
服務(wù)器反饋:Second
服務(wù)器反饋:Third
???????服務(wù)器端程序的輸出結(jié)果為:
???????????服務(wù)器已啟動(dòng):
客戶端發(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”,從而完成第二次會話,第三次會話的過程和這個(gè)一樣。在這個(gè)過程中,每次都是客戶端程序首先發(fā)送數(shù)據(jù)給服務(wù)器端,服務(wù)器接收數(shù)據(jù)以后,將結(jié)果反饋給客戶端,客戶端接收到服務(wù)器端的反饋,從而完成一次通訊過程。
在該示例中,雖然解決了多次發(fā)送的問題,但是客戶端和服務(wù)器端的次數(shù)控制還不夠靈活,如果客戶端的次數(shù)不固定怎么辦呢?是否可以使用某個(gè)特殊的字符串,例如quit,表示客戶端退出呢,這就涉及到網(wǎng)絡(luò)協(xié)議的內(nèi)容了,會在后續(xù)的網(wǎng)絡(luò)應(yīng)用示例部分詳細(xì)介紹。下面開始介紹另外一個(gè)網(wǎng)絡(luò)編程的突出問題。
2、如何使服務(wù)器端支持多個(gè)客戶端同時(shí)工作?
?????????前面介紹的服務(wù)器端程序,只是實(shí)現(xiàn)了概念上的服務(wù)器端,離實(shí)際的服務(wù)器端程序結(jié)構(gòu)距離還很遙遠(yuǎn),如果需要讓服務(wù)器端能夠?qū)嶋H使用,那么最需要解決的問題就是——如何支持多個(gè)客戶端同時(shí)工作。
?????????一個(gè)服務(wù)器端一般都需要同時(shí)為多個(gè)客戶端提供通訊,如果需要同時(shí)支持多個(gè)客戶端,則必須使用前面介紹的線程的概念。簡單來說,也就是當(dāng)服務(wù)器端接收到一個(gè)連接時(shí),啟動(dòng)一個(gè)專門的線程處理和該客戶端的通訊。
?????????按照這個(gè)思路改寫的服務(wù)端示例程序?qū)⒂蓛蓚€(gè)部分組成,MulThreadSocketServer類實(shí)現(xiàn)服務(wù)器端控制,實(shí)現(xiàn)接收客戶端連接,然后開啟專門的邏輯線程處理該連接,LogicThread類實(shí)現(xiàn)對于一個(gè)客戶端連接的邏輯處理,將處理的邏輯放置在該類的run方法中。該示例的代碼實(shí)現(xiàn)為:
?????????????????? package tcp;
import java.net.ServerSocket;
import java.net.Socket;
/**
?*?支持多客戶端的服務(wù)器端實(shí)現(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ù)器已啟動(dòng):");
??????????????????????????? while(true){
???????????????????????????????????? //獲得連接
???????????????????????????????????? socket = serverSocket.accept();
???????????????????????????????????? //啟動(dòng)線程
???????????????????????????????????? new LogicThread(socket);
??????????????????????????? }
?????????????????? } catch (Exception e) {
??????????????????????????? e.printStackTrace();
?????????????????? }finally{
??????????????????????????? try{
???????????????????????????????????? //關(guān)閉連接
???????????????????????????????????? serverSocket.close();
??????????????????????????? }catch(Exception e){}
?????????????????? }
???????? }
}
?????????在該示例代碼中,實(shí)現(xiàn)了一個(gè)while形式的死循環(huán),由于accept方法是阻塞方法,所以當(dāng)客戶端連接未到達(dá)時(shí),將阻塞該程序的執(zhí)行,當(dāng)客戶端到達(dá)時(shí)接收該連接,并啟動(dòng)一個(gè)新的LogicThread線程處理該連接,然后按照循環(huán)的執(zhí)行流程,繼續(xù)等待下一個(gè)客戶端連接。這樣當(dāng)任何一個(gè)客戶端連接到達(dá)時(shí),都開啟一個(gè)專門的線程處理,通過多個(gè)線程支持多個(gè)客戶端同時(shí)處理。
?????????下面再看一下LogicThread線程類的源代碼實(shí)現(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(); //啟動(dòng)線程
???????? }
????????
???????? 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){}
???????? }
????????
???????? /**
???????? ?*?邏輯處理方法,實(shí)現(xiàn)echo邏輯
???????? ?* @param b?客戶端發(fā)送數(shù)據(jù)緩沖區(qū)
???????? ?* @param off?起始下標(biāo)
???????? ?* @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è)連接對象構(gòu)造該線程,該連接對象就是該線程需要處理的連接,在線程構(gòu)造完成以后,該線程就被啟動(dòng)起來了,然后在run方法內(nèi)部對客戶端連接進(jìn)行處理,數(shù)據(jù)交換的邏輯和前面的示例代碼一致,只是這里將接收到客戶端發(fā)送過來的數(shù)據(jù)并進(jìn)行處理的邏輯封裝成了logic方法,按照前面介紹的IO編程的內(nèi)容,客戶端發(fā)送過來的內(nèi)容存儲在數(shù)組b的起始下標(biāo)為0,長度為n個(gè)中,這些數(shù)據(jù)是客戶端發(fā)送過來的有效數(shù)據(jù),將有效的數(shù)據(jù)傳遞給logic方法,logic方法實(shí)現(xiàn)的是echo服務(wù)的邏輯,也就是將客戶端發(fā)送的有效數(shù)據(jù)形成以后新的response數(shù)組,并作為返回值反饋。
?????????在線程中將logic方法的返回值反饋給客戶端,這樣就完成了服務(wù)器端的邏輯處理模擬,其他的實(shí)現(xiàn)和前面的介紹類似,這里就不在重復(fù)了。
?????????這里的示例還只是基礎(chǔ)的服務(wù)器端實(shí)現(xiàn),在實(shí)際的服務(wù)器端實(shí)現(xiàn)中,由于硬件和端口數(shù)的限制,所以不能無限制的創(chuàng)建線程對象,而且頻繁的創(chuàng)建線程對象效率也比較低,所以程序中都實(shí)現(xiàn)了線程池來提高程序的執(zhí)行效率。
?????????這里簡單介紹一下線程池的概念,線程池(Thread pool)是池技術(shù)的一種,就是在程序啟動(dòng)時(shí)首先把需要個(gè)數(shù)的線程對象創(chuàng)建好,例如創(chuàng)建5000個(gè)線程對象,然后當(dāng)客戶端連接到達(dá)時(shí)從池中取出一個(gè)已經(jīng)創(chuàng)建完成的線程對象使用即可。當(dāng)客戶端連接關(guān)閉以后,將該線程對象重新放入到線程池中供其它的客戶端重復(fù)使用,這樣可以提高程序的執(zhí)行速度,優(yōu)化程序?qū)τ趦?nèi)存的占用等。
?????????關(guān)于基礎(chǔ)的TCP方式的網(wǎng)絡(luò)編程就介紹這么多,下面介紹UDP方式的網(wǎng)絡(luò)編程在Java語言中的實(shí)現(xiàn)。
?
?
?
???????網(wǎng)絡(luò)通訊的方式除了TCP方式以外,還有一種實(shí)現(xiàn)的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用戶數(shù)據(jù)報(bào)協(xié)議,方式類似于發(fā)短信息,是一種物美價(jià)廉的通訊方式,使用該種方式無需建立專用的虛擬連接,由于無需建立專用的連接,所以對于服務(wù)器的壓力要比TCP小很多,所以也是一種常見的網(wǎng)絡(luò)編程方式。但是使用該種方式最大的不足是傳輸不可靠,當(dāng)然也不是說經(jīng)常丟失,就像大家發(fā)短信息一樣,理論上存在收不到的可能,這種可能性可能是1%,反正比較小,但是由于這種可能的存在,所以平時(shí)我們都覺得重要的事情還是打個(gè)電話吧(類似TCP方式),一般的事情才發(fā)短信息(類似UDP方式)。網(wǎng)絡(luò)編程中也是這樣,必須要求可靠傳輸?shù)男畔⒁话闶褂肨CP方式實(shí)現(xiàn),一般的數(shù)據(jù)才使用UDP方式實(shí)現(xiàn)。
???????? UDP方式的網(wǎng)絡(luò)編程也在Java語言中獲得了良好的支持,由于其在傳輸數(shù)據(jù)的過程中不需要建立專用的連接等特點(diǎn),所以在Java API中設(shè)計(jì)的實(shí)現(xiàn)結(jié)構(gòu)和TCP方式不太一樣。當(dāng)然,需要使用的類還是包含在java.net包中。
?????????在Java API中,實(shí)現(xiàn)UDP方式的編程,包含客戶端網(wǎng)絡(luò)編程和服務(wù)器端網(wǎng)絡(luò)編程,主要由兩個(gè)類實(shí)現(xiàn),分別是:
l?DatagramSocket
DatagramSocket類實(shí)現(xiàn)“網(wǎng)絡(luò)連接”,包括客戶端網(wǎng)絡(luò)連接和服務(wù)器端網(wǎng)絡(luò)連接。雖然UDP方式的網(wǎng)絡(luò)通訊不需要建立專用的網(wǎng)絡(luò)連接,但是畢竟還是需要發(fā)送和接收數(shù)據(jù),DatagramSocket實(shí)現(xiàn)的就是發(fā)送數(shù)據(jù)時(shí)的發(fā)射器,以及接收數(shù)據(jù)時(shí)的監(jiān)聽器的角色。類比于TCP中的網(wǎng)絡(luò)連接,該類既可以用于實(shí)現(xiàn)客戶端連接,也可以用于實(shí)現(xiàn)服務(wù)器端連接。
l?DatagramPacket
DatagramPacket類實(shí)現(xiàn)對于網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)封裝,也就是說,該類的對象代表網(wǎng)絡(luò)中交換的數(shù)據(jù)。在UDP方式的網(wǎng)絡(luò)編程中,無論是需要發(fā)送的數(shù)據(jù)還是需要接收的數(shù)據(jù),都必須被處理成DatagramPacket類型的對象,該對象中包含發(fā)送到的地址、發(fā)送到的端口號以及發(fā)送的內(nèi)容等。其實(shí)DatagramPacket類的作用類似于現(xiàn)實(shí)中的信件,在信件中包含信件發(fā)送到的地址以及接收人,還有發(fā)送的內(nèi)容等,郵局只需要按照地址傳遞即可。在接收數(shù)據(jù)時(shí),接收到的數(shù)據(jù)也必須被處理成DatagramPacket類型的對象,在該對象中包含發(fā)送方的地址、端口號等信息,也包含數(shù)據(jù)的內(nèi)容。和TCP方式的網(wǎng)絡(luò)傳輸相比,IO編程在UDP方式的網(wǎng)絡(luò)編程中變得不是必須的內(nèi)容,結(jié)構(gòu)也要比TCP方式的網(wǎng)絡(luò)編程簡單一些。
?????????下面介紹一下UDP方式的網(wǎng)絡(luò)編程中,客戶端和服務(wù)器端的實(shí)現(xiàn)步驟,以及通過基礎(chǔ)的示例演示UDP方式的網(wǎng)絡(luò)編程在Java語言中的實(shí)現(xiàn)方式。
???????? UDP方式的網(wǎng)絡(luò)編程,編程的步驟和TCP方式類似,只是使用的類和方法存在比較大的區(qū)別,下面首先介紹一下UDP方式的網(wǎng)絡(luò)編程客戶端實(shí)現(xiàn)過程。
UDP客戶端編程涉及的步驟也是4個(gè)部分:建立連接、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)和關(guān)閉連接。
首先介紹UDP方式的網(wǎng)絡(luò)編程中建立連接的實(shí)現(xiàn)。其中UDP方式的建立連接和TCP方式不同,只需要建立一個(gè)連接對象即可,不需要指定服務(wù)器的IP和端口號碼。實(shí)現(xiàn)的代碼為:
?????????????????? DatagramSocket ds = new DatagramSocket();
?????????這樣就建立了一個(gè)客戶端連接,該客戶端連接使用系統(tǒng)隨機(jī)分配的一個(gè)本地計(jì)算機(jī)的未用端口號。在該連接中,不指定服務(wù)器端的IP和端口,所以UDP方式的網(wǎng)絡(luò)連接更像一個(gè)發(fā)射器,而不是一個(gè)具體的連接。
?????????當(dāng)然,可以通過制定連接使用的端口號來創(chuàng)建客戶端連接。
?????????????????? DatagramSocket ds = new DatagramSocket(5000);
?????????這樣就是使用本地計(jì)算機(jī)的5000號端口建立了一個(gè)連接。一般在建立客戶端連接時(shí)沒有必要指定端口號碼。
?????????接著,介紹一下UDP客戶端編程中發(fā)送數(shù)據(jù)的實(shí)現(xiàn)。在UDP方式的網(wǎng)絡(luò)編程中,IO技術(shù)不是必須的,在發(fā)送數(shù)據(jù)時(shí),需要將需要發(fā)送的數(shù)據(jù)內(nèi)容首先轉(zhuǎn)換為byte數(shù)組,然后將數(shù)據(jù)內(nèi)容、服務(wù)器IP和服務(wù)器端口號一起構(gòu)造成一個(gè)DatagramPacket類型的對象,這樣數(shù)據(jù)的準(zhǔn)備就完成了,發(fā)送時(shí)調(diào)用網(wǎng)絡(luò)連接對象中的send方法發(fā)送該對象即可。例如將字符串“Hello”發(fā)送到IP是127.0.0.1,端口號是10001的服務(wù)器,則實(shí)現(xiàn)發(fā)送數(shù)據(jù)的代碼如下:
???????? ???????? String s = “Hello”;
?????????????????? String host = “127.0.0.1”;
?????????????????? int port = 10001;
???????? ???????? //將發(fā)送的內(nèi)容轉(zhuǎn)換為byte數(shù)組
?????????????????? byte[] b = s.getBytes();
?????????????????? //將服務(wù)器IP轉(zhuǎn)換為InetAddress對象
?????????????????? InetAddress server = InetAddress.getByName(host);
?????????????????? //構(gòu)造發(fā)送的數(shù)據(jù)包對象
?????????????????? DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);
?????????????????? //發(fā)送數(shù)據(jù)
?????????????????? ds.send(sendDp);
?????????在該示例代碼中,不管發(fā)送的數(shù)據(jù)內(nèi)容是什么,都需要轉(zhuǎn)換為byte數(shù)組,然后將服務(wù)器端的IP地址構(gòu)造成InetAddress類型的對象,在準(zhǔn)備完成以后,將這些信息構(gòu)造成一個(gè)DatagramPacket類型的對象,在UDP編程中,發(fā)送的數(shù)據(jù)內(nèi)容、服務(wù)器端的IP和端口號,都包含在DatagramPacket對象中。在準(zhǔn)備完成以后,調(diào)用連接對象ds的send方法把DatagramPacket對象發(fā)送出去即可。
?????????按照UDP協(xié)議的約定,在進(jìn)行數(shù)據(jù)傳輸時(shí),系統(tǒng)只是盡全力傳輸數(shù)據(jù),但是并不保證數(shù)據(jù)一定被正確傳輸,如果數(shù)據(jù)在傳輸過程中丟失,那就丟失了。
???????? UDP方式在進(jìn)行網(wǎng)絡(luò)通訊時(shí),也遵循“請求-響應(yīng)”模型,在發(fā)送數(shù)據(jù)完成以后,就可以接收服務(wù)器端的反饋數(shù)據(jù)了。
?????????下面介紹一下UDP客戶端編程中接收數(shù)據(jù)的實(shí)現(xiàn)。當(dāng)數(shù)據(jù)發(fā)送出去以后,就可以接收服務(wù)器端的反饋信息了。接收數(shù)據(jù)在Java語言中的實(shí)現(xiàn)是這樣的:首先構(gòu)造一個(gè)數(shù)據(jù)緩沖數(shù)組,該數(shù)組用于存儲接收的服務(wù)器端反饋數(shù)據(jù),該數(shù)組的長度必須大于或等于服務(wù)器端反饋的實(shí)際有效數(shù)據(jù)的長度。然后以該緩沖數(shù)組為基礎(chǔ)構(gòu)造一個(gè)DatagramPacket數(shù)據(jù)包對象,最后調(diào)用連接對象的receive方法接收數(shù)據(jù)即可。接收到的服務(wù)器端反饋數(shù)據(jù)存儲在DatagramPacket類型的對象內(nèi)部。實(shí)現(xiàn)接收數(shù)據(jù)以及顯示服務(wù)器端反饋內(nèi)容的示例代碼如下:
?????????????????? //構(gòu)造緩沖數(shù)組
?????????????????? byte[] data = new byte[1024];
?????????????????? //構(gòu)造數(shù)據(jù)包對象
?????????????????? DatagramPacket received = new DatagramPacket(data,data.length);
?????????????????? //接收數(shù)據(jù)
?????????????????? ds.receive(receiveDp);
?????????????????? //輸出數(shù)據(jù)內(nèi)容
?????????????????? byte[] b = receiveDp.getData();?//獲得緩沖數(shù)組
?????????????????? int len = receiveDp.getLength(); //獲得有效數(shù)據(jù)長度
?????????????????? String s = new String(b,0,len);
?????????????????? System.out.println(s);
?????????在該代碼中,首先構(gòu)造緩沖數(shù)組data,這里設(shè)置的長度1024是預(yù)估的接收到的數(shù)據(jù)長度,要求該長度必須大于或等于接收到的數(shù)據(jù)長度,然后以該緩沖數(shù)組為基礎(chǔ),構(gòu)造數(shù)據(jù)包對象,使用連接對象ds的receive方法接收反饋數(shù)據(jù),由于在Java語言中,除String以外的其它對象都是按照地址傳遞,所以在receive方法內(nèi)部可以改變數(shù)據(jù)包對象receiveDp的內(nèi)容,這里的receiveDp的功能和返回值類似。數(shù)據(jù)接收到以后,只需要從數(shù)據(jù)包對象中讀取出來就可以了,使用DatagramPacket對象中的getData方法可以獲得數(shù)據(jù)包對象的緩沖區(qū)數(shù)組,但是緩沖區(qū)數(shù)組的長度一般大于有效數(shù)據(jù)的長度,換句話說,也就是緩沖區(qū)數(shù)組中只有一部分?jǐn)?shù)據(jù)是反饋數(shù)據(jù),所以需要使用DatagramPacket對象中的getLength方法獲得有效數(shù)據(jù)的長度,則有效數(shù)據(jù)就是緩沖數(shù)組中的前有效數(shù)據(jù)長度個(gè)內(nèi)容,這些才是真正的服務(wù)器端反饋的數(shù)據(jù)的內(nèi)容。
???????? UDP方式客戶端網(wǎng)絡(luò)編程的最后一個(gè)步驟就是關(guān)閉連接。雖然UDP方式不建立專用的虛擬連接,但是連接對象還是需要占用系統(tǒng)資源,所以在使用完成以后必須關(guān)閉連接。關(guān)閉連接使用連接對象中的close方法即可,實(shí)現(xiàn)的代碼如下:
?????????????????? ds.close();
?????????需要說明的是,和TCP建立連接的方式不同,UDP方式的同一個(gè)網(wǎng)絡(luò)連接對象,可以發(fā)送到達(dá)不同服務(wù)器端IP或端口的數(shù)據(jù)包,這點(diǎn)是TCP方式無法做到的。
?????????介紹完了UDP方式客戶端網(wǎng)絡(luò)編程的基礎(chǔ)知識以后,下面再來介紹一下UDP方式服務(wù)器端網(wǎng)絡(luò)編程的基礎(chǔ)知識。
???????? UDP方式網(wǎng)絡(luò)編程的服務(wù)器端實(shí)現(xiàn)和TCP方式的服務(wù)器端實(shí)現(xiàn)類似,也是服務(wù)器端監(jiān)聽某個(gè)端口,然后獲得數(shù)據(jù)包,進(jìn)行邏輯處理以后將處理以后的結(jié)果反饋給客戶端,最后關(guān)閉網(wǎng)絡(luò)連接,下面依次進(jìn)行介紹。
?????????首先UDP方式服務(wù)器端網(wǎng)絡(luò)編程需要建立一個(gè)連接,該連接監(jiān)聽某個(gè)端口,實(shí)現(xiàn)的代碼為:
???? ????? DatagramSocket ds = new DatagramSocket(10010);
由于服務(wù)器端的端口需要固定,所以一般在建立服務(wù)器端連接時(shí),都指定端口號。例如該示例代碼中指定10010端口為服務(wù)器端使用的端口號,客戶端端在連接服務(wù)器端時(shí)連接該端口號即可。
接著服務(wù)器端就開始接收客戶端發(fā)送過來的數(shù)據(jù),其接收的方法和客戶端接收的方法一直,其中receive方法的作用類似于TCP方式中accept方法的作用,該方法也是一個(gè)阻塞方法,其作用是接收數(shù)據(jù)。
接收到客戶端發(fā)送過來的數(shù)據(jù)以后,服務(wù)器端對該數(shù)據(jù)進(jìn)行邏輯處理,然后將處理以后的結(jié)果再發(fā)送給客戶端,在這里發(fā)送時(shí)就比客戶端要麻煩一些,因?yàn)榉?wù)器端需要獲得客戶端的IP和客戶端使用的端口號,這個(gè)都可以從接收到的數(shù)據(jù)包中獲得。示例代碼如下:
???? //獲得客戶端的IP
???? InetAddress clientIP = receiveDp.getAddress();
???????? //獲得客戶端的端口號
???????? Int clientPort = receiveDp.getPort();
?????使用以上代碼,就可以從接收到的數(shù)據(jù)包對象receiveDp中獲得客戶端的IP地址和客戶端的端口號,這樣就可以在服務(wù)器端中將處理以后的數(shù)據(jù)構(gòu)造成數(shù)據(jù)包對象,然后將處理以后的數(shù)據(jù)內(nèi)容反饋給客戶端了。
?????最后,當(dāng)服務(wù)器端實(shí)現(xiàn)完成以后,關(guān)閉服務(wù)器端連接,實(shí)現(xiàn)的方式為調(diào)用連接對象的close方法,示例代碼如下:
???????? ds.close();
?????介紹完了UDP方式下的客戶端編程和服務(wù)器端編程的基礎(chǔ)知識以后,下面通過一個(gè)簡單的示例演示UDP網(wǎng)絡(luò)編程的基本使用。
?
該示例的功能是實(shí)現(xiàn)將客戶端程序的系統(tǒng)時(shí)間發(fā)送給服務(wù)器端,服務(wù)器端接收到時(shí)間以后,向客戶端反饋?zhàn)址癘K”。實(shí)現(xiàn)該功能的客戶端代碼如下所示:
??????? package udp;
import java.net.*;
import java.util.*;
/**
?*?簡單的UDP客戶端,實(shí)現(xiàn)向服務(wù)器端發(fā)生系統(tǒng)時(shí)間功能
?*/
public class SimpleUDPClient {
??????????? public static void main(String[] args) {
???????? ??????? ????DatagramSocket ds = null;?//連接對象
???????? ??????? ????DatagramPacket sendDp; //發(fā)送數(shù)據(jù)包對象
??????????? ????DatagramPacket receiveDp; //接收數(shù)據(jù)包對象
???????? ??????? ????String serverHost = "127.0.0.1"; //服務(wù)器IP
??????????? ????int serverPort = 10010;?//服務(wù)器端口號
???????? ??????? ????try{
???????? ?????????? ????//建立連接
???????? ?????????? ????ds = new DatagramSocket();
???????? ?????????? ????//初始化發(fā)送數(shù)據(jù)
???????? ?????????? ????Date d = new Date(); //當(dāng)前時(shí)間
???????? ????? ?????????String content = d.toString(); //轉(zhuǎn)換為字符串
???????? ?????????? ????byte[] data = content.getBytes();
???????? ?????????? ????//初始化發(fā)送包對象
???????? ?????????? ????InetAddress address = InetAddress.getByName(serverHost);
???????? ?????????? ????sendDp = new DatagramPacket(data,data.length,address,serverPort);
???????? ?????????? ????//發(fā)送
???????? ?????????? ????ds.send(sendDp);
??????????????????????????????????????????????????????????????????????????
???????? ?????????? ????//初始化接收數(shù)據(jù)
???????? ?????????? ????byte[] b = new byte[1024];
???????? ?????????? ????receiveDp = new DatagramPacket(b,b.length);
???????? ?????????? ????//接收
???????? ?????????? ????ds.receive(receiveDp);
???????? ?????????? ????//讀取反饋內(nèi)容,并輸出
???????? ?????????? ????byte[] response = receiveDp.getData();
???????? ?????????? ????int len = receiveDp.getLength();
???????? ?????????? ????String s = new String(response,0,len);
???????? ?????????? ????System.out.println("服務(wù)器端反饋為:" + s);
??????????? ????}catch(Exception e){
???????? ?????????????? e.printStackTrace();
??????????????? }finally{
???????? ?????????????? try{
???????? ????????????????? //關(guān)閉連接
???????? ????????????????? ds.close();
???????? ?????????????? }catch(Exception e){}
??????????????? }
??????????? }
?????? ?}
在該示例代碼中,首先建立UDP方式的網(wǎng)絡(luò)連接,然后獲得當(dāng)前系統(tǒng)時(shí)間,這里獲得的系統(tǒng)時(shí)間是客戶端程序運(yùn)行的本地計(jì)算機(jī)的時(shí)間,然后將時(shí)間字符串以及服務(wù)器端的IP和端口,構(gòu)造成發(fā)送數(shù)據(jù)包對象,調(diào)用連接對象ds的send方法發(fā)送出去。在數(shù)據(jù)發(fā)送出去以后,構(gòu)造接收數(shù)據(jù)的數(shù)據(jù)包對象,調(diào)用連接對象ds的receive方法接收服務(wù)器端的反饋,并輸出在控制臺。最后在finally語句塊中關(guān)閉客戶端網(wǎng)絡(luò)連接。
和下面將要介紹的服務(wù)器端一起運(yùn)行時(shí),客戶端程序的輸出結(jié)果為:
????服務(wù)器端反饋為:OK
下面是該示例程序的服務(wù)器端代碼實(shí)現(xiàn):
???????package udp;
??????? import java.net.*;
??????? /**
???????? *?簡單UDP服務(wù)器端,實(shí)現(xiàn)功能是輸出客戶端發(fā)送數(shù)據(jù),
???????????并反饋?zhàn)址癘K"給客戶端
???????? */
??????? public class SimpleUDPServer {
??????????? public static void main(String[] args) {
???????? ??????????? DatagramSocket ds = null;?//連接對象
???????? ??????????? DatagramPacket sendDp; //發(fā)送數(shù)據(jù)包對象
???????? ??????????? DatagramPacket receiveDp; //接收數(shù)據(jù)包對象
???????? ??????????? final int PORT = 10010; //端口
?????????????????????????????????????????????? try{
???????? ?????????????? //建立連接,監(jiān)聽端口
???????? ?????????????? ds = new DatagramSocket(PORT);
???????? ??????????????System.out.println("服務(wù)器端已啟動(dòng):");
???????? ?????????????? //初始化接收數(shù)據(jù)
???????? ?????????????? byte[] b = new byte[1024];
???????? ?????????????? receiveDp = new DatagramPacket(b,b.length);
???????? ?????????????? //接收
???????? ?????????????? ds.receive(receiveDp);
???????? ?????????????? //讀取反饋內(nèi)容,并輸出
???????? ?????????????? InetAddress clientIP = receiveDp.getAddress();
???????? ?????????????? int clientPort = receiveDp.getPort();
???????? ?????????????? byte[] data = receiveDp.getData();
???????? ?????????????? int len = receiveDp.getLength();
???????? ?????????????? System.out.println("客戶端IP:" + clientIP.getHostAddress());
???????? ?????????????? System.out.println("客戶端端口:" + clientPort);
???????? ?????????????? System.out.println("客戶端發(fā)送內(nèi)容:" + new String(data,0,len));
??????????????????????????????????????????????????????????????????????????
???????? ?????????????? //發(fā)送反饋
???????? ?????????????? String response = "OK";
???????? ????????????? ?byte[] bData = response.getBytes();
???????? ?????????????? sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort);
???????? ?????????????? //發(fā)送
???????? ?????????????? ds.send(sendDp);
?????????????????????????????????????????????? }catch(Exception e){
???????? ?????????????? e.printStackTrace();
?????????????????????????????????????????????? }finally{
???????? ?????? ????????try{
???????? ????????????????? //關(guān)閉連接
???????? ????????????????? ds.close();
???????? ?????????????? }catch(Exception e){}
?????????????????????????????????????????????? }
??????????? }
??????? }
在該服務(wù)器端實(shí)現(xiàn)中,首先監(jiān)聽10010號端口,和TCP方式的網(wǎng)絡(luò)編程類似,服務(wù)器端的receive方法是阻塞方法,如果客戶端不發(fā)送數(shù)據(jù),則程序會在該方法處阻塞。當(dāng)客戶端發(fā)送數(shù)據(jù)到達(dá)服務(wù)器端時(shí),則接收客戶端發(fā)送過來的數(shù)據(jù),然后將客戶端發(fā)送的數(shù)據(jù)內(nèi)容讀取出來,并在服務(wù)器端程序中打印客戶端的相關(guān)信息,從客戶端發(fā)送過來的數(shù)據(jù)包中可以讀取出客戶端的IP以及客戶端端口號,將反饋數(shù)據(jù)字符串“OK”發(fā)送給客戶端,最后關(guān)閉服務(wù)器端連接,釋放占用的系統(tǒng)資源,完成程序功能示例。
和前面TCP方式中的網(wǎng)絡(luò)編程類似,這個(gè)示例也僅僅是網(wǎng)絡(luò)編程的功能示例,也存在前面介紹的客戶端無法進(jìn)行多次數(shù)據(jù)交換,以及服務(wù)器端不支持多個(gè)客戶端的問題,這兩個(gè)問題也需要對于代碼進(jìn)行處理才可以很方便的進(jìn)行解決。
在解決該問題以前,需要特別指出的是UDP方式的網(wǎng)絡(luò)編程由于不建立虛擬的連接,所以在實(shí)際使用時(shí)和TCP方式存在很多的不同,最大的一個(gè)不同就是“無狀態(tài)”。該特點(diǎn)指每次服務(wù)器端都收到信息,但是這些信息和連接無關(guān),換句話說,也就是服務(wù)器端只是從信息是無法識別出是誰發(fā)送的,這樣就要求發(fā)送信息時(shí)的內(nèi)容需要多一些,這個(gè)在后續(xù)的示例中可以看到。
下面是實(shí)現(xiàn)客戶端多次發(fā)送以及服務(wù)器端支持多個(gè)數(shù)據(jù)包同時(shí)處理的程序結(jié)構(gòu),實(shí)現(xiàn)的原理和TCP方式類似,在客戶端將數(shù)據(jù)的發(fā)送和接收放入循環(huán)中,而服務(wù)器端則將接收到的每個(gè)數(shù)據(jù)包啟動(dòng)一個(gè)專門的線程進(jìn)行處理。實(shí)現(xiàn)的代碼如下:
??? package udp;
??? import java.net.*;
??? import java.util.*;
??? /**
??? ?*?簡單的UDP客戶端,實(shí)現(xiàn)向服務(wù)器端發(fā)生系統(tǒng)時(shí)間功能
??? ?*?該程序發(fā)送3次數(shù)據(jù)到服務(wù)器端
??? ?*/
??? public class MulUDPClient {
????????????????? public static void main(String[] args) {
???????? ??????? DatagramSocket ds = null;?//連接對象
???????????????????????????????????? DatagramPacket sendDp; //發(fā)送數(shù)據(jù)包對象
???????????????????????????????????? DatagramPacket receiveDp; //接收數(shù)據(jù)包對象
???????????????????????????????????? String serverHost = "127.0.0.1"; //服務(wù)器IP
???????????????????????????????????? int serverPort = 10012;?//服務(wù)器端口號
???????????????????????????????????? try{
???????? ?????????? //建立連接
???????? ?????????? ds = new DatagramSocket();
???????? ?????????? //初始化
??????????????????????????? ??InetAddress address = InetAddress.getByName(serverHost);
???????? ?????????? byte[] b = new byte[1024];
???????? ?????????? receiveDp = new DatagramPacket(b,b.length);
???????? ?????????? System.out.println("客戶端準(zhǔn)備完成");
???????? ?????????? //循環(huán)30次,每次間隔0.01秒
???????? ?????????? for(int i = 0;i < 30;i++){
??????????????????????????????????????????????????????? //初始化發(fā)送數(shù)據(jù)
??????????????????????????????????????????????????????? Date d = new Date(); //當(dāng)前時(shí)間
??????????????????????????????????????????????????????? String content = d.toString(); //轉(zhuǎn)換為字符串
??????????????????????????????????????????????????????? byte[] data = content.getBytes();
??????????????????????????????????????????????????????? //初始化發(fā)送包對象
??????????????????????????????????????????????????????? sendDp = new DatagramPacket(data,data.length,address, serverPort);
??????????????????????????????????????????????????????? //發(fā)送
??????????????????????????????????????????????????????? ds.send(sendDp);
??????????????????????????????????????????????????????? //延遲
??????????????????????????????????????????????????????? Thread.sleep(10);
??????????????????????????????????????????????????????? //接收
??????????????????????????????????????????????????????? ds.receive(receiveDp);
??????????????????????????????????????????????????????? //讀取反饋內(nèi)容,并輸出
??????????????????????????????????????????????????????? byte[] response = receiveDp.getData();
??????????????????????????????????????????????????????? int len = receiveDp.getLength();
??????????????????????????????????????????????????????? String s = new String(response,0,len);
??????????????????????????????????????????????????????? System.out.println("服務(wù)器端反饋為:" + s);
???????? ??????????? }
???????? ??????? }catch(Exception e){
???????? ??????????? e.printStackTrace();
???????? ??????? }finally{
???????? ??????????? try{
??????????????????????????????????????????????????????? //關(guān)閉連接
??????????????????????????????????????????????????????? ds.close();
???????? ??????????? }catch(Exception e){}
???????? ??????? }
???????? }
???? }
在該示例中,將和服務(wù)器端進(jìn)行數(shù)據(jù)交換的邏輯寫在一個(gè)for循環(huán)的內(nèi)部,這樣就可以實(shí)現(xiàn)和服務(wù)器端的多次交換了,考慮到服務(wù)器端的響應(yīng)速度,在每次發(fā)送之間加入0.01秒的時(shí)間間隔。最后當(dāng)數(shù)據(jù)交換完成以后關(guān)閉連接,結(jié)束程序。
實(shí)現(xiàn)該邏輯的服務(wù)器端程序代碼如下:
package udp;
import java.net.*;
/**
*?可以并發(fā)處理數(shù)據(jù)包的服務(wù)器端
*?功能為:顯示客戶端發(fā)送的內(nèi)容,并向客戶端反饋?zhàn)址癘K”
*/
public class MulUDPServer {
public static void main(String[] args) {
DatagramSocket ds = null;?//連接對象
DatagramPacket receiveDp; //接收數(shù)據(jù)包對象
final int PORT = 10012; //端口
byte[] b = new byte[1024];
receiveDp = new DatagramPacket(b,b.length);
try{
//建立連接,監(jiān)聽端口
ds = new DatagramSocket(PORT);
System.out.println("服務(wù)器端已啟動(dòng):");
while(true){
//接收
ds.receive(receiveDp);
//啟動(dòng)線程處理數(shù)據(jù)包
new LogicThread(ds,receiveDp);
}
}catch(Exception e){
???????? e.printStackTrace();
}finally{
try{
//關(guān)閉連接
ds.close();
}catch(Exception e){}
}
}
}
該代碼實(shí)現(xiàn)了服務(wù)器端的接收邏輯,使用一個(gè)循環(huán)來接收客戶端發(fā)送過來的數(shù)據(jù)包,當(dāng)接收到數(shù)據(jù)包以后啟動(dòng)一個(gè)LogicThread線程處理該數(shù)據(jù)包。這樣服務(wù)器端就可以實(shí)現(xiàn)同時(shí)處理多個(gè)數(shù)據(jù)包了。
實(shí)現(xiàn)邏輯處理的線程代碼如下:
package udp;
import java.net.*;
/**
?*?邏輯處理線程
?*/
public class LogicThread extends Thread {
/**連接對象*/
DatagramSocket ds;
/**接收到的數(shù)據(jù)包*/
DatagramPacket dp;
?
public LogicThread(DatagramSocket ds,DatagramPacket dp){
this.ds = ds;
this.dp = dp;
start(); //啟動(dòng)線程
}
?
public void run(){
try{
//獲得緩沖數(shù)組
byte[] data = dp.getData();
//獲得有效數(shù)據(jù)長度
int len = dp.getLength();
//客戶端IP
InetAddress clientAddress = dp.getAddress();
//客戶端端口
int clientPort = dp.getPort();
//輸出
System.out.println("客戶端IP:" + clientAddress.getHostAddress());
System.out.println("客戶端端口號:" + clientPort);
System.out.println("客戶端發(fā)送內(nèi)容:" + new String(data,0,len));
//反饋到客戶端
byte[] b = "OK".getBytes();
DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort);
//發(fā)送
ds.send(sendDp);
}catch(Exception e){
e.printStackTrace();
}
}
}
在該線程中,只處理一次UDP通訊,當(dāng)通訊結(jié)束以后線程死亡,在線程內(nèi)部,每次獲得客戶端發(fā)送過來的信息,將獲得的信息輸出到服務(wù)器端程序的控制臺,然后向客戶端反饋?zhàn)址癘K”。
由于UDP數(shù)據(jù)傳輸過程中可能存在丟失,所以在運(yùn)行該程序時(shí)可能會出現(xiàn)程序阻塞的情況。如果需要避免該問題,可以將客戶端的網(wǎng)絡(luò)發(fā)送部分也修改成線程實(shí)現(xiàn)。
關(guān)于基礎(chǔ)的UDP網(wǎng)絡(luò)編程就介紹這么多了,下面將介紹一下網(wǎng)絡(luò)協(xié)議的概念。
?網(wǎng)絡(luò)協(xié)議
?????????對于需要從事網(wǎng)絡(luò)編程的程序員來說,網(wǎng)絡(luò)協(xié)議是一個(gè)需要深刻理解的概念。那么什么是網(wǎng)絡(luò)協(xié)議呢?
?????????網(wǎng)絡(luò)協(xié)議是指對于網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)格式的規(guī)定。對于網(wǎng)絡(luò)編程初學(xué)者來說,沒有必要深入了解TCP/IP協(xié)議簇,所以對于初學(xué)者來說去讀大部頭的《TCP/IP協(xié)議》也不是一件很合適的事情,因?yàn)樯钊肓私釺CP/IP協(xié)議是網(wǎng)絡(luò)編程提高階段,也是深入網(wǎng)絡(luò)編程底層時(shí)才需要做的事情。
?????????對于一般的網(wǎng)絡(luò)編程來說,更多的是關(guān)心網(wǎng)絡(luò)上傳輸?shù)倪壿嫈?shù)據(jù)內(nèi)容,也就是更多的是應(yīng)用層上的網(wǎng)絡(luò)協(xié)議,所以后續(xù)的內(nèi)容均以實(shí)際應(yīng)用的數(shù)據(jù)為基礎(chǔ)來介紹網(wǎng)絡(luò)協(xié)議的概念。
?????????那么什么是網(wǎng)絡(luò)協(xié)議呢,下面看一個(gè)簡單的例子。春節(jié)晚會上“小沈陽”和趙本山合作的小品《不差錢》中,小沈陽和趙本山之間就設(shè)計(jì)了一個(gè)協(xié)議,協(xié)議的內(nèi)容為:
???????????????????如果點(diǎn)的菜價(jià)錢比較貴是,就說沒有。
?????????按照該協(xié)議的規(guī)定,就有了下面的對話:
???????????????????趙本山:4斤的龍蝦
???????????????????小沈陽:(經(jīng)過判斷,得出價(jià)格比較高),沒有
???????????????????趙本山:鮑魚
???????????????????小沈陽:(經(jīng)過判斷,得出價(jià)格比較高),沒有
?????????這就是一種雙方達(dá)成的一種協(xié)議約定,其實(shí)這種約定的實(shí)質(zhì)和網(wǎng)絡(luò)協(xié)議的實(shí)質(zhì)是一樣的。網(wǎng)絡(luò)協(xié)議的實(shí)質(zhì)也是客戶端程序和服務(wù)器端程序?qū)τ跀?shù)據(jù)的一種約定,只是由于以計(jì)算機(jī)為基礎(chǔ),所以更多的是使用數(shù)字來代表內(nèi)容,這樣就顯得比較抽象一些。
?????????下 面再舉一個(gè)簡單的例子,介紹一些基礎(chǔ)的網(wǎng)絡(luò)協(xié)議設(shè)計(jì)的知識。例如需要設(shè)計(jì)一個(gè)簡單的網(wǎng)絡(luò)程序:網(wǎng)絡(luò)計(jì)算器。也就是在客戶端輸入需要計(jì)算的數(shù)字和運(yùn)算符,在 服務(wù)器端實(shí)現(xiàn)計(jì)算,并將計(jì)算的結(jié)果反饋給客戶端。在這個(gè)例子中,就需要約定兩個(gè)數(shù)據(jù)格式:客戶端發(fā)送給服務(wù)器端的數(shù)據(jù)格式,以及服務(wù)器端反饋給客戶端的數(shù) 據(jù)格式。
?????????可能你覺得這個(gè)比較簡單,例如客戶端輸入的數(shù)字依次是12和432,輸入的運(yùn)算符是加號,可能最容易想到的數(shù)據(jù)格式是形成字符串“12+432”,這樣格式的確比較容易閱讀,但是服務(wù)器端在進(jìn)行計(jì)算時(shí),邏輯就比較麻煩,因?yàn)樾枰紫炔鸱衷撟址?#xff0c;然后才能進(jìn)行計(jì)算,所以可用的數(shù)據(jù)格式就有了一下幾種:
?????????????????? “12,432,+”?? ??格式為:第一個(gè)數(shù)字,第二個(gè)數(shù)字,運(yùn)算符
?????????????????? “12,+,432”?????格式為:第一個(gè)數(shù)字,運(yùn)算符,第二個(gè)數(shù)字
?????????其實(shí)以上兩種數(shù)據(jù)格式很接近,比較容易閱讀,在服務(wù)器端收到該數(shù)據(jù)格式以后,使用“,”為分隔符分割字符串即可。
?????????假設(shè)對于運(yùn)算符再進(jìn)行一次約定,例如約定數(shù)字0代表+,1代表減,2代表乘,3代表除,整體格式遵循以上第一種格式,則上面的數(shù)字生產(chǎn)的協(xié)議數(shù)據(jù)為:
???????????????????“12,432,0”
?????????這就是一種基本的發(fā)送的協(xié)議約定了。
?????????另 外一個(gè)需要設(shè)計(jì)的協(xié)議格式就是服務(wù)器端反饋的數(shù)據(jù)格式,其實(shí)服務(wù)器端主要反饋計(jì)算結(jié)果,但是在實(shí)際接受數(shù)據(jù)時(shí),有可能存在格式錯(cuò)誤的情況,這樣就需要簡單 的設(shè)計(jì)一下服務(wù)器端反饋的數(shù)據(jù)格式了。例如規(guī)定,如果發(fā)送的數(shù)據(jù)格式正確,則反饋結(jié)果,否則反饋?zhàn)址板e(cuò)誤”。這樣就有了以下的數(shù)據(jù)格式:
???????????????????客戶端:“1,111,1”????? ???服務(wù)器端:”-110”
???????????????????客戶端:“123,23,0”?? ?服務(wù)器端:“146”
????????客戶端:“1,2,5”???????服務(wù)器端:“錯(cuò)誤”
?????????這樣就設(shè)計(jì)出了一種最最基本的網(wǎng)絡(luò)協(xié)議格式,從該示例中可以看出,網(wǎng)絡(luò)協(xié)議就是一種格式上的約定,可以根據(jù)邏輯的需要約定出各種數(shù)據(jù)格式,在進(jìn)行設(shè)計(jì)時(shí)一般遵循“簡單、通用、容易解析”的原則進(jìn)行。
?????????而對于復(fù)雜的網(wǎng)絡(luò)程序來說,需要傳輸?shù)臄?shù)據(jù)種類和數(shù)據(jù)量都比較大,這樣只需要依次設(shè)計(jì)出每種情況下的數(shù)據(jù)格式即可,例如QQ程序,在該程序中需要進(jìn)行傳輸?shù)木W(wǎng)絡(luò)數(shù)據(jù)種類很多,那么在設(shè)計(jì)時(shí)就可以遵循:登錄格式、注冊格式、發(fā)送消息格式等等,一一進(jìn)行設(shè)計(jì)即可。所以對于復(fù)雜的網(wǎng)絡(luò)程序來說,只是增加了更多的命令格式,在實(shí)際設(shè)計(jì)時(shí)的工作量增加不是太大。
?????????不管怎么說,在網(wǎng)絡(luò)編程中,對于同一個(gè)網(wǎng)絡(luò)程序來說,一般都會涉及到兩個(gè)網(wǎng)絡(luò)協(xié)議格式:客戶端發(fā)送數(shù)據(jù)格式和服務(wù)器端反饋數(shù)據(jù)格式,在實(shí)際設(shè)計(jì)時(shí),需要一一對應(yīng)。這就是最基本的網(wǎng)絡(luò)協(xié)議的知識。
?????????網(wǎng)絡(luò)協(xié)議設(shè)計(jì)完成以后,在進(jìn)行網(wǎng)絡(luò)編程時(shí),就需要根據(jù)設(shè)計(jì)好的協(xié)議格式,在程序中進(jìn)行對應(yīng)的編碼了,客戶端程序和服務(wù)器端程序需要進(jìn)行協(xié)議處理的代碼分別如下。
客戶端程序需要完成的處理為:
1、?客戶端發(fā)送協(xié)議格式的生成
2、?服務(wù)器端反饋數(shù)據(jù)格式的解析
服務(wù)器端程序需要完成的處理為:
1、?服務(wù)器端反饋協(xié)議格式的生成
2、?客戶端發(fā)送協(xié)議格式的解析
這里的生成是指將計(jì)算好的數(shù)據(jù),轉(zhuǎn)換成規(guī)定的數(shù)據(jù)格式,這里的解析指,從反饋的數(shù)據(jù)格式中拆分出需要的數(shù)據(jù)。在進(jìn)行對應(yīng)的代碼編寫時(shí),嚴(yán)格遵循協(xié)議約定即可。
所以,對于程序員來說,在進(jìn)行網(wǎng)絡(luò)程序編寫時(shí),需要首先根據(jù)邏輯的需要設(shè)計(jì)網(wǎng)絡(luò)協(xié)議格式,然后遵循協(xié)議格式約定進(jìn)行協(xié)議生成和解析代碼的編寫,最后使用網(wǎng)絡(luò)編程技術(shù)實(shí)現(xiàn)整個(gè)網(wǎng)絡(luò)編程的功能。
由于各種網(wǎng)絡(luò)程序使用不同的協(xié)議格式,所以不同網(wǎng)絡(luò)程序的客戶端之間無法通用。
而對于常見協(xié)議的格式,例如HTTP(Hyper Text Transfer Protocol,超文本傳輸協(xié)議)、FTP(File Transfer Protocol,文件傳輸協(xié)議),SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協(xié)議)等等,都有通用的規(guī)定,具體可以查閱相關(guān)的RFC文檔。
最后,對于一種網(wǎng)絡(luò)程序來說,網(wǎng)絡(luò)協(xié)議格式是該程序最核心的技術(shù)秘密,因?yàn)橐坏﹨f(xié)議格式泄漏,則任何一個(gè)人都可以根據(jù)該格式進(jìn)行客戶端的編寫,這樣將影響服務(wù)器端的實(shí)現(xiàn),也容易出現(xiàn)一些其它的影響。
13.2.6小結(jié)
?????????關(guān)于網(wǎng)絡(luò)編程基本的技術(shù)就介紹這么多,該部分介紹了網(wǎng)絡(luò)編程的基礎(chǔ)知識,以及Java語言對于網(wǎng)絡(luò)編程的支持,網(wǎng)絡(luò)編程的步驟等,并詳細(xì)介紹了TCP方式網(wǎng)絡(luò)編程和UDP方式網(wǎng)絡(luò)編程在Java語言中的實(shí)現(xiàn)。
?????????網(wǎng)絡(luò)協(xié)議也是網(wǎng)絡(luò)程序的核心,所以在實(shí)際開始進(jìn)行網(wǎng)絡(luò)編程時(shí),設(shè)計(jì)一個(gè)良好的協(xié)議格式也是必須進(jìn)行的工作。
網(wǎng)絡(luò)編程示例
?????????“實(shí)踐出真知”,所以在進(jìn)行技術(shù)學(xué)習(xí)時(shí),還是需要進(jìn)行很多的練習(xí),才可以體會技術(shù)的奧妙,下面通過兩個(gè)簡單的示例,演示網(wǎng)絡(luò)編程的實(shí)際使用。
13.3.1質(zhì)數(shù)判別示例
?????????該示例實(shí)現(xiàn)的功能是質(zhì)數(shù)判斷,程序?qū)崿F(xiàn)的功能為客戶端程序接收用戶輸入的數(shù)字,然后將用戶輸入的內(nèi)容發(fā)送給服務(wù)器端,服務(wù)器端判斷客戶端發(fā)送的數(shù)字是否是質(zhì)數(shù),并將判斷的結(jié)果反饋給客戶端,客戶端根據(jù)服務(wù)器端的反饋顯示判斷結(jié)果。
?????????質(zhì)數(shù)的規(guī)則是:最小的質(zhì)數(shù)是2,只能被1和自身整除的自然數(shù)。當(dāng)用戶輸入小于2的數(shù)字,以及輸入的內(nèi)容不是自然數(shù)時(shí),都屬于非法輸入。
?????????網(wǎng)絡(luò)程序的功能都分為客戶端程序和服務(wù)器端程序?qū)崿F(xiàn),下面先描述一下每個(gè)程序分別實(shí)現(xiàn)的功能:
1、?客戶端程序功能:
a)?????????接收用戶控制臺輸入
b)?????????判斷輸入內(nèi)容是否合法
c)?????????按照協(xié)議格式生成發(fā)送數(shù)據(jù)
d)?????????發(fā)送數(shù)據(jù)
e)?????????接收服務(wù)器端反饋
f)??????????解析服務(wù)器端反饋信息,并輸出
2、?服務(wù)器端程序功能:
a)?????????接收客戶端發(fā)送數(shù)據(jù)
b)?????????按照協(xié)議格式解析數(shù)據(jù)
c)?????????判斷數(shù)字是否是質(zhì)數(shù)
d)?????????根據(jù)判斷結(jié)果,生成協(xié)議數(shù)據(jù)
e)?????????將數(shù)據(jù)反饋給客戶端
分解好了網(wǎng)絡(luò)程序的功能以后,就可以設(shè)計(jì)網(wǎng)絡(luò)協(xié)議格式了,如果該程序的功能比較簡單,所以設(shè)計(jì)出的協(xié)議格式也不復(fù)雜。
?????????客戶端發(fā)送協(xié)議格式:
???????????????????將用戶輸入的數(shù)字轉(zhuǎn)換為字符串,再將字符串轉(zhuǎn)換為byte數(shù)組即可。
???????????????????例如用戶輸入16,則轉(zhuǎn)換為字符串“16”,使用getBytes轉(zhuǎn)換為byte數(shù)組。
???????????????????客戶端發(fā)送“quit”字符串代表結(jié)束連接
?????????服務(wù)器端發(fā)送協(xié)議格式:
反饋數(shù)據(jù)長度為1個(gè)字節(jié)。數(shù)字0代表是質(zhì)數(shù),1代表不是質(zhì)數(shù),2代表協(xié)議格式錯(cuò)誤。
例如客戶端發(fā)送數(shù)字12,則反饋1,發(fā)送13則反饋0,發(fā)送0則反饋2。
?????????功能設(shè)計(jì)完成以后,就可以分別進(jìn)行客戶端和服務(wù)器端程序的編寫了,在編寫完成以后聯(lián)合起來進(jìn)行調(diào)試即可。
?????????下面分別以TCP方式和UDP方式實(shí)現(xiàn)該程序,注意其實(shí)現(xiàn)上的差異。不管使用哪種方式實(shí)現(xiàn),客戶端都可以多次輸入數(shù)據(jù)進(jìn)行判斷。對于UDP方式來說,不需要向服務(wù)器端發(fā)送quit字符串。
?????????以TCP方式實(shí)現(xiàn)的客戶端程序代碼如下:
?????????????????? package example1;
import java.io.*;
import java.net.*;
/**
?*?以TCP方式實(shí)現(xiàn)的質(zhì)數(shù)判斷客戶端程序
?*/
public class TCPPrimeClient {
???????? static BufferedReader br;
???????? static Socket socket;
???????? static InputStream is;
???????? static OutputStream os;
???????? /**服務(wù)器IP*/
???????? final static String HOST = "127.0.0.1";
???????? /**服務(wù)器端端口*/
???????? final static int PORT = 10005;
????????
???????? public static void main(String[] args) {
?????????????????? init(); //初始化
?????????????????? while(true){
??????????????????????????? System.out.println("請輸入數(shù)字:");
??????????????????????????? String input = readInput(); //讀取輸入
??????????????????????????? if(isQuit(input)){ //判讀是否結(jié)束
???????????????????????????????????? byte[] b = "quit".getBytes();
???????????????????????????????????? send(b);
???????????????????????????????????? break; //結(jié)束程序
??????????????????????????? }
??????????????????????????? if(checkInput(input)){ //校驗(yàn)合法
???????????????????????????????????? //發(fā)送數(shù)據(jù)
???????????????????????????????????? send(input.getBytes());
???????????????????????????????????? //接收數(shù)據(jù)
???????????????????????????????????? byte[] data = receive();
???????????????????????????????????? //解析反饋數(shù)據(jù)
???????????????????????????????????? parse(data);
??????????????????????????? }else{
???????????????????????????????????? System.out.println("輸入不合法,請重新輸入!");
??????????????????????????? }
?????????????????? }
?????????????????? close();?//關(guān)閉流和連接
???????? }
????????
???????? /**
???????? ?*?初始化
???????? ?*/
???????? private static void init(){
?????????????????? try {
??????????????????????????? br = new BufferedReader(
?????????????????????????????????????????????? new InputStreamReader(System.in));
??????????????????????????? socket = new Socket(HOST,PORT);
??????????????????????????? is = socket.getInputStream();
??????????????????????????? os = socket.getOutputStream();
?????????????????? } catch (Exception e) {}
???????? }
????????
???????? /**
???????? ?*?讀取客戶端輸入
???????? ?*/
???????? private static String readInput(){
?????????????????? try {
??????????????????????????? return br.readLine();
?????????????????? } catch (Exception e) {
??????????????????????????? return null;
?????????????????? }
???????? }
????????
???????? /**
???????? ?*?判斷是否輸入quit
???????? ?* @param input?輸入內(nèi)容
???????? ?* @return true代表結(jié)束,false代表不結(jié)束
???????? ?*/
???????? private static boolean isQuit(String input){
?????????????????? if(input == null){
??????????????????????????? return false;
?????????????????? }else{
??????????????????????????? if("quit".equalsIgnoreCase(input)){
???????????????????????????????????? return true;
??????????????????????????? }else{
???????????????????????????????????? return false;
??????????????????????????? }
?????????????????? }
???????? }
????????
???????? /**
???????? ?*?校驗(yàn)輸入
???????? ?* @param input?用戶輸入內(nèi)容
???????? ?* @return true代表輸入符合要求,false代表不符合
???????? ?*/
???????? private static boolean checkInput(String input){
?????????????????? if(input == null){
??????????????????????????? return false;
?????????????????? }
?????????????????? try{
??????????????????????????? int n = Integer.parseInt(input);
??????????????????????????? if(n >= 2){
???????????????????????????????????? return true;
??????????????????????????? }else{
???????????????????????????????????? return false;
??????????????????????????? }
?????????????????? }catch(Exception e){
??????????????????????????? return false;?//輸入不是整數(shù)
?????????????????? }
???????? }
????????
???????? /**
???????? ?*?向服務(wù)器端發(fā)送數(shù)據(jù)
???????? ?* @param data?數(shù)據(jù)內(nèi)容
???????? ?*/
???????? private static void send(byte[] data){
?????????????????? try{
??????????????????????????? os.write(data);
?????????????????? }catch(Exception e){}
???????? }
????????
???????? /**
???????? ?*?接收服務(wù)器端反饋
???????? ?* @return?反饋數(shù)據(jù)
???????? ?*/
???????? private static byte[] receive(){
?????????????????? byte[] b = new byte[1024];
?????????????????? try {
??????????????????????????? int n = is.read(b);
??????????????????????????? byte[] data = new byte[n];
??????????????????????????? //復(fù)制有效數(shù)據(jù)
??????????????????????????? System.arraycopy(b, 0, data, 0, n);
??????????????????????????? return data;
?????????????????? } catch (Exception e){}
?????????????????? return null;
???????? }
????????
???????? /**
???????? ?*?解析協(xié)議數(shù)據(jù)
???????? ?* @param data?協(xié)議數(shù)據(jù)
???????? ?*/
???????? private static void parse(byte[] data){
?????????????????? if(data == null){
??????????????????????????? System.out.println("服務(wù)器端反饋數(shù)據(jù)不正確!");
??????????????????????????? return;
?????????????????? }
?????????????????? byte value = data[0]; //取第一個(gè)byte
?????????????????? //按照協(xié)議格式解析
?????????????????? switch(value){
?????????????????? case 0:
??????????????????????????? System.out.println("質(zhì)數(shù)");
??????????????????????????? break;
?????????????????? case 1:
??????????????????????????? System.out.println("不是質(zhì)數(shù)");
??????????????????????????? break;
?????????????????? case 2:
??????????????????????????? System.out.println("協(xié)議格式錯(cuò)誤");
??????????????????????????? break;
?????????????????? }
???????? }
????????
???????? /**
???????? ?*?關(guān)閉流和連接
???????? ?*/
???????? private static void close(){
?????????????????? try{
??????????????????????????? br.close();
??????????????????????????? is.close();
??????????????????????????? os.close();
??????????????????????????? socket.close();
?????????????????? }catch(Exception e){
??????????????????????????? e.printStackTrace();
?????????????????? }
???????? }
}
?????????在該代碼中,將程序的功能使用方法進(jìn)行組織,使得結(jié)構(gòu)比較清晰,核心的邏輯流程在main方法中實(shí)現(xiàn)。
?????????以TCP方式實(shí)現(xiàn)的服務(wù)器端的代碼如下:
?????????????????? package example1;
import java.net.*;
/**
?*?以TCP方式實(shí)現(xiàn)的質(zhì)數(shù)判別服務(wù)器端
?*/
public class TCPPrimeServer {
???????? public static void main(String[] args) {
?????????????????? final int PORT = 10005;
?????????????????? ServerSocket ss?= null;
?????????????????? try {
??????????????????????????? ss = new ServerSocket(PORT);
??????????????????????????? System.out.println("服務(wù)器端已啟動(dòng):");
??????????????????????????? while(true){
???????????????????????????????????? Socket s = ss.accept();
???????????????????????????????????? new PrimeLogicThread(s);
??????????????????????????? }
?????????????????? } catch (Exception e) {}
?????????????????? finally{
??????????????????????????? try {
???????????????????????????????????? ss.close();
??????????????????????????? } catch (Exception e2) {}
?????????????????? }
??????????????????
???????? }
}
package example1;
import java.io.*;
import java.net.*;
/**
?*?實(shí)現(xiàn)質(zhì)數(shù)判別邏輯的線程
?*/
public class PrimeLogicThread extends Thread {
???????? Socket socket;
???????? InputStream is;
???????? OutputStream os;
????????
???????? public PrimeLogicThread(Socket socket){
?????????????????? this.socket = socket;
?????????????????? init();
?????????????????? start();
???????? }
???????? /**
???????? ?*?初始化
???????? ?*/
???????? private void init(){
?????????????????? try{
??????????????????????????? is = socket.getInputStream();
??????????????????????????? os = socket.getOutputStream();
?????????????????? }catch(Exception e){}
???????? }
????????
???????? public void run(){
?????????????????? while(true){
??????????????????????????? //接收客戶端反饋
??????????????????????????? byte[] data = receive();
??????????????????????????? //判斷是否是退出
??????????????????????????? if(isQuit(data)){
???????????????????????????????????? break;?//結(jié)束循環(huán)
??????????????????????????? }
??????????????????????????? //邏輯處理
?????????????????? ???????? byte[] b = logic(data);
??????????????????????????? //反饋數(shù)據(jù)
??????????????????????????? send(b);
?????????????????? }
?????????????????? close();
???????? }
????????
???????? /**
???????? ?*?接收客戶端數(shù)據(jù)
???????? ?* @return?客戶端發(fā)送的數(shù)據(jù)
???????? ?*/
???????? private?byte[] receive(){
?????????????????? byte[] b = new byte[1024];
?????????????????? try {
??????????????????????????? int n = is.read(b);
??????????????????????????? byte[] data = new byte[n];
??????????????????????????? //復(fù)制有效數(shù)據(jù)
??????????????????????????? System.arraycopy(b, 0, data, 0, n);
??????????????????????????? return data;
?????????????????? } catch (Exception e){}
?????????????????? return null;
???????? }
????????
???????? /**
???????? ?*?向客戶端發(fā)送數(shù)據(jù)
???????? ?* @param data?數(shù)據(jù)內(nèi)容
???????? ?*/
???????? private void send(byte[] data){
?????????????????? try{
??????????????????????????? os.write(data);
?????????????????? }catch(Exception e){}
???????? }
????????
???????? /**
???????? ?*?判斷是否是quit
???????? ?* @return?是返回true,否則返回false
???????? ?*/
???????? private boolean isQuit(byte[] data){
?????????????????? if(data == null){
??????????????????????????? return false;
?????????????????? }else{
??????????????????????????? String s = new String(data);
??????????????????????????? if(s.equalsIgnoreCase("quit")){
???????????????????????????????????? return true;
??????????????????????????? }else{
???????????????????????????????????? return false;
??????????????????????????? }
?????????????????? }
???????? }
????????
???????? private byte[] logic(byte[] data){
?????????????????? //反饋數(shù)組
?????????????????? byte[] b = new byte[1];
?????????????????? //校驗(yàn)參數(shù)
?????????????????? if(data == null){
??????????????????????????? b[0] = 2;
??????????????????????????? return b;
?????????????????? }
?????????????????? try{
??????????????????????????? //轉(zhuǎn)換為數(shù)字
??????????????????????????? String s = new String(data);
??????????????????????????? int n = Integer.parseInt(s);
??????????????????????????? //判斷是否是質(zhì)數(shù)
??????????????????????????? if(n >= 2){
???????????????????????????????????? boolean flag = isPrime(n);
???????????????????????????????????? if(flag){
?????????????????? ??????????????????????????? b[0] = 0;
???????????????????????????????????? }else{
?????????????????????????????????????????????? b[0] = 1;
???????????????????????????????????? }
??????????????????????????? }else{
???????????????????????????????????? b[0] = 2;?//格式錯(cuò)誤
???????????????????????????????????? System.out.println(n);
??????????????????????????? }
?????????????????? }catch(Exception e){
??????????????????????????? e.printStackTrace();
??????????????????????????? b[0] = 2;
?????????????????? }
?????????????????? return b;
???????? }
????????
???????? /**
???????? ?*
???????? ?* @param n
???????? ?* @return
???????? ?*/
???????? private boolean isPrime(int n){
?????????????????? boolean b = true;
?????????????????? for(int i = 2;i <= Math.sqrt(n);i++){
??????????????????????????? if(n % i == 0){
???????????????????????????????????? b = false;
???????????????????????????????????? break;
??????????????????????????? }
?????????????????? }
?????????????????? return b;
???????? }
????????
???????? /**
???????? ?*?關(guān)閉連接
???????? ?*/
???????? private void close(){
?????????????????? try {
??????????????????????????? is.close();
??????????????????????????? os.close();
??????????????????????????? socket.close();
?????????????????? } catch (Exception e){}
???????? }
}
?????????本示例使用的服務(wù)器端的結(jié)構(gòu)和前面示例中的結(jié)構(gòu)一致,只是邏輯線程的實(shí)現(xiàn)相對來說要復(fù)雜一些,在線程類中的logic方法中實(shí)現(xiàn)了服務(wù)器端邏輯,根據(jù)客戶端發(fā)送過來的數(shù)據(jù),判斷是否是質(zhì)數(shù),然后根據(jù)判斷結(jié)果按照協(xié)議格式要求,生成客戶端反饋數(shù)據(jù),實(shí)現(xiàn)服務(wù)器端要求的功能。
?
?
猜數(shù)字小游戲
?????????下面這個(gè)示例是一個(gè)猜數(shù)字的控制臺小游戲。該游戲的規(guī)則是:當(dāng)客戶端第一次連接到服務(wù)器端時(shí),服務(wù)器端生產(chǎn)一個(gè)【0,50】之間的隨機(jī)數(shù)字,然后客戶端輸入數(shù)字來猜該數(shù)字,每次客戶端輸入數(shù)字以后,發(fā)送給服務(wù)器端,服務(wù)器端判斷該客戶端發(fā)送的數(shù)字和隨機(jī)數(shù)字的關(guān)系,并反饋比較結(jié)果,客戶端總共有5次猜的機(jī)會,猜中時(shí)提示猜中,當(dāng)輸入”quit”時(shí)結(jié)束程序。
?????????和 前面的示例類似,在進(jìn)行網(wǎng)絡(luò)程序開發(fā)時(shí),首先需要分解一下功能的實(shí)現(xiàn),覺得功能是在客戶端程序中實(shí)現(xiàn)還是在服務(wù)器端程序中實(shí)現(xiàn)。區(qū)分的規(guī)則一般是:客戶端 程序?qū)崿F(xiàn)接收用戶輸入等界面功能,并實(shí)現(xiàn)一些基礎(chǔ)的校驗(yàn)降低服務(wù)器端的壓力,而將程序核心的邏輯以及數(shù)據(jù)存儲等功能放在服務(wù)器端進(jìn)行實(shí)現(xiàn)。遵循該原則劃分 的客戶端和服務(wù)器端功能如下所示。
?????????客戶端程序功能列表:
1、?接收用戶控制臺輸入
2、?判斷輸入內(nèi)容是否合法
3、?按照協(xié)議格式發(fā)送數(shù)據(jù)
4、?根據(jù)服務(wù)器端的反饋給出相應(yīng)提示
?????????服務(wù)器端程序功能列表:
1、?接收客戶端發(fā)送數(shù)據(jù)
2、?按照協(xié)議格式解析數(shù)據(jù)
3、?判斷發(fā)送過來的數(shù)字和隨機(jī)數(shù)字的關(guān)系
4、?根據(jù)判斷結(jié)果生產(chǎn)協(xié)議數(shù)據(jù)
5、?將生產(chǎn)的數(shù)據(jù)反饋給客戶端
?????????在該示例中,實(shí)際使用的網(wǎng)絡(luò)命令也只有兩條,所以顯得協(xié)議的格式比較簡單。
?????????其中客戶端程序協(xié)議格式如下:
1、?將用戶輸入的數(shù)字轉(zhuǎn)換為字符串,然后轉(zhuǎn)換為byte數(shù)組
2、?發(fā)送“quit”字符串代表退出
?????????其中服務(wù)器端程序協(xié)議格式如下:
1、?反饋長度為1個(gè)字節(jié),數(shù)字0代表相等(猜中),1代表大了,2代表小了,其它數(shù)字代表錯(cuò)誤。
?????????實(shí)現(xiàn)該程序的代碼比較多,下面分為客戶端程序?qū)崿F(xiàn)和服務(wù)器端程序?qū)崿F(xiàn)分別進(jìn)行列舉。
?????????客戶端程序?qū)崿F(xiàn)代碼如下:
??????????????? ?package guess;
import java.net.*;
import java.io.*;
/**
?*?猜數(shù)字客戶端
?*/
public class TCPClient {
?public static void main(String[] args) {
???????? Socket socket = null;
???????? OutputStream os = null;
???????? InputStream is = null;
???????? BufferedReader br = null;
???????? byte[] data = new byte[2];
???????? try{
?????????????????? //建立連接
?????????????????? socket = new Socket(
???????????????????????????????????? "127.0.0.1",10001);
??????????????????
?????????????????? //發(fā)送數(shù)據(jù)
?????????????????? os= socket.getOutputStream();
??????????????????
?????????????????? //讀取反饋數(shù)據(jù)
?????????????????? is = socket.getInputStream();
??????????????????
?????????????????? //鍵盤輸入流
?????????????????? br = new BufferedReader(
???????????????????????????????????? new InputStreamReader(System.in));
??????????????????
?????????????????? //多次輸入
?????????????????? while(true){
??????????????????????????? System.out.println("請輸入數(shù)字:");
??????????????????????????? //接收輸入
??????????????????????????? String s = br.readLine();
??????????????????????????? //結(jié)束條件
??????????????????????????? if(s.equals("quit")){
???????????????????????????????????? os.write("quit".getBytes());
???????????????????????????????????? break;
??????????????????????????? }
??????????????????????????? //校驗(yàn)輸入是否合法
??????????????????????????? boolean b = true;
??????????????????????????? try{
???????????????????????????????????? Integer.parseInt(s);
??????????????????????????? }catch(Exception e){
???????????????????????????????????? b = false;
??????????????????????????? }
??????????????????????????? if(b){ //輸入合法
???????????????????????????????????? //發(fā)送數(shù)據(jù)
???????????????????????????????????? os.write(s.getBytes());
???????????????????????????????????? //接收反饋
???????????????????????????????????? is.read(data);
???????????????????????????????????? //判斷
???????????????????????????????????? switch(data[0]){
???????????????????????????????????? case 0:
?????????????????????????????????????????????? System.out.println("相等!祝賀你!");
?????????????????????????????????????????????? break;
???????????????????????????????????? case 1:
?????????????????????????????????????????????? System.out.println("大了!");
?????????????????????????????????????????????? break;
???????????????????????????????????? case 2:
?????????????????????????????????????????????? System.out.println("小了!");
?????????????????????????????????????????????? break;
???????????????????????????????????? default:
?????????????????????????????????????????????? System.out.println("其它錯(cuò)誤!");
???????????????????????????????????? }
???????????????????????????????????? //提示猜的次數(shù)
???????????????????????????????????? System.out.println("你已經(jīng)猜了" + data[1] + "次!");
???????????????????????????????????? //判斷次數(shù)是否達(dá)到5次
???????????????????????????????????? if(data[1] >= 5){
?????????????????????????????????????????????? System.out.println("你掛了!");
?????????????????????????????????????????????? //給服務(wù)器端線程關(guān)閉的機(jī)會
?????????????????????????????????????????????? os.write("quit".getBytes());
?????????????????????????????????????????????? //結(jié)束客戶端程序
?????????????????????????????????????????????? break;
???????????????????????????????????? }
??????????????????????????? }else{?//輸入錯(cuò)誤
???????????????????????????????????? System.out.println("輸入錯(cuò)誤!");
??????????????????????????? }
?????????????????? }
???????? }catch(Exception e){
?????????????????? e.printStackTrace();
???????? }finally{
?????????????????? try{
??????????????????????????? //關(guān)閉連接
??????????????????????????? br.close();
??????????????????????????? is.close();
??????????????????????????? os.close();
??????????????????????????? socket.close();
?????????????????? }catch(Exception e){
??????????????????????????? e.printStackTrace();
?????????????????? }
???????? }
?}
????? }
???在該示例中,首先建立一個(gè)到IP地址為127.0.0.1的端口為10001的連接,然后進(jìn)行各個(gè)流的初始化工作,將邏輯控制的代碼放入在一個(gè)while循環(huán)中,這樣可以在客戶端多次進(jìn)行輸入。在循環(huán)內(nèi)部,首先判斷用戶輸入的是否為quit字符串,如果是則結(jié)束程序,如果輸入不是quit,則首先校驗(yàn)輸入的是否是數(shù)字,如果不是數(shù)字則直接輸出“輸入錯(cuò)誤!”并繼續(xù)接收用戶輸入,如果是數(shù)字則發(fā)送給服務(wù)器端,并根據(jù)服務(wù)器端的反饋顯示相應(yīng)的提示信息。最后關(guān)閉流和連接,結(jié)束客戶端程序。
?????????服務(wù)器端程序的實(shí)現(xiàn)還是分為服務(wù)器控制程序和邏輯線程,實(shí)現(xiàn)的代碼分別如下:
??????????????? ?package guess;
import java.net.*;
/**
?* TCP連接方式的服務(wù)器端
?*?實(shí)現(xiàn)功能:接收客戶端的數(shù)據(jù),判斷數(shù)字關(guān)系
?*/
public class TCPServer {
?public static void main(String[] args) {
???????? try{
?????????????????? //監(jiān)聽端口
?????????????????? ServerSocket ss = new ServerSocket(10001);
?????????????????? System.out.println("服務(wù)器已啟動(dòng):");
?????????????????? //邏輯處理
?????????????????? while(true){
??????????????????????????? //獲得連接
??????????????????????????? Socket s = ss.accept();
??????????????????????????? //啟動(dòng)線程處理
??????????????????????????? new LogicThread(s);
?????????????????? }
??????????????????
???????? }catch(Exception e){
?????????????????? e.printStackTrace();
???????? }
?}
?????}
????? package guess;
import java.net.*;
import java.io.*;
import java.util.*;
/**
?*?邏輯處理線程
?*/
public class LogicThread extends Thread {
????? Socket s;
?????
????? static Random r = new Random();
?????
????? public LogicThread(Socket s){
????????????? this.s = s;
????????????? start();?//啟動(dòng)線程
????? }
?????
????? public void run(){
????????????? //生成一個(gè)[0,50]的隨機(jī)數(shù)
????????????? int randomNumber = Math.abs(r.nextInt() % 51);
????????????? //用戶猜的次數(shù)
????????????? int guessNumber = 0;
????????????? InputStream is = null;
????????????? OutputStream os = null;
????????????? byte[] data = new byte[2];
????????????? try{
?????????????????????? //獲得輸入流
?????????????????????? is = s.getInputStream();
?????????????????????? //獲得輸出流
?????????????????????? os = s.getOutputStream();
?????????????????????? while(true){?//多次處理
???????????????????????????????? //讀取客戶端發(fā)送的數(shù)據(jù)
???????????????????????????????? byte[] b = new byte[1024];
???????????????????????????????? int n = is.read(b);
???????????????????????????????? String send = new String(b,0,n);
???????????????????????????????? //結(jié)束判別
???????????????????????????????? if(send.equals("quit")){
????????????????????????????????????????? break;
???????????????????????????????? }
???????????????????????????????? //解析、判斷
???????????????????????????????? try{
????????????????????????????????????????? int num = Integer.parseInt(send);
????????????????????????????????????????? //處理
????????????????????????????????????????? guessNumber++; //猜的次數(shù)增加1
????????????????????????????????????????? data[1] = (byte)guessNumber;
????????????????????????????????????????? //判斷
????????????????????????????????????????? if(num > randomNumber){
?????????????????????????????????????????????????? data[0] = 1;
????????????????????????????????????????? }else if(num < randomNumber){
?????????????????????????????????????????????????? data[0] = 2;
????????????????????????????????????????? }else{
?????????????????????????????????????????????????? data[0] = 0;
?????????????????????????????????????????????????? //如果猜對
?????????????????????????????????????????????????? guessNumber = 0; //清零
?????????????????????????????????????????????????? randomNumber = Math.abs(r.nextInt() % 51);
????????????????????????????????????????? }
????????????????????????????????????????? //反饋給客戶端
????????????????????????????????????????? os.write(data);????????????????????????????????????
?????????????????????????????????????????
???????????????????????????????? }catch(Exception e){ //數(shù)據(jù)格式錯(cuò)誤
????????????????????????????????????????? data[0] = 3;
????????????????????????????????????????? data[1] = (byte)guessNumber;
????????????????????????????????????????? os.write(data);?//發(fā)送錯(cuò)誤標(biāo)識
????????????????????????????????????????? break;
???????????????????????????????? }
???????????????????????????????? os.flush();?? //強(qiáng)制發(fā)送
?????????????????????? }
??????????????????????
????????????? }catch(Exception e){
?????????????????????? e.printStackTrace();
????????????? }finally{
?????????????????????? try{
???????????????????????????????? is.close();
???????????????????????????????? os.close();
???????????????????????????????? s.close();
?????????????????????? }catch(Exception e){}
????????????? }
????? }
}
在 該示例中,服務(wù)器端控制部分和前面的示例中一樣。也是等待客戶端連接,如果有客戶端連接到達(dá)時(shí),則啟動(dòng)新的線程去處理客戶端連接。在邏輯線程中實(shí)現(xiàn)程序的 核心邏輯,首先當(dāng)線程執(zhí)行時(shí)生產(chǎn)一個(gè)隨機(jī)數(shù)字,然后根據(jù)客戶端發(fā)送過來的數(shù)據(jù),判斷客戶端發(fā)送數(shù)字和隨機(jī)數(shù)字的關(guān)系,然后反饋相應(yīng)的數(shù)字的值,并記憶客戶 端已經(jīng)猜過的次數(shù),當(dāng)客戶端猜中以后清零猜過的次數(shù),使得客戶端程序可以繼續(xù)進(jìn)行游戲。
總體來說,該程序示例的結(jié)構(gòu)以及功能都與上一個(gè)程序比較類似,希望通過比較這兩個(gè)程序,加深對于網(wǎng)絡(luò)編程的認(rèn)識,早日步入網(wǎng)絡(luò)編程的大門。
轉(zhuǎn)載于:https://www.cnblogs.com/wxy325/archive/2013/01/05/2845541.html
總結(jié)
以上是生活随笔為你收集整理的JAVA的网络编程【转】的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 30美丽的矢量建筑艺术为灵感
- 下一篇: 六种异常处理的陋习