微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)
轉自:https://mp.weixin.qq.com/s/XX...html
咱們是幸運的,由於咱們擁有網絡。網絡是一個神奇的東西,它改變了你和個人生活方式,改變了整個世界。 然而,網絡的無標度和小世界特性使得它又是複雜的,無所不在,無所不能,以至於咱們沒法區分甚至沒法描述。java
對於一個碼農而言,瞭解網絡的基礎知識可能仍是從瞭解定義開始,認識OSI的七層協議模型,深刻Socket內部,進而熟練地進行網絡編程。node
關於網絡python
關於網絡,在詞典中的定義是這樣的:程序員
在電的系統中,由若干元件組成的用來使電信號按必定要求傳輸的電路或這種電路的部分,叫網絡。web
做爲一名從事過TMN開發的通訊專業畢業生,執拗地認爲網絡是從通訊系統中誕生的。通訊是人與人之間經過某種媒介進行的信息交流與傳遞。傳統的通訊網絡(即電話網絡)是由傳輸、交換和終端三大部分組成,通訊網絡是指將各個孤立的設備進行物理鏈接,實現信息交換的鏈路,從而達到資源共享和通訊的目的。通訊網絡能夠從覆蓋範圍,拓撲結構,交換方式等諸多視角進行分類...... 滿滿的回憶,仍是留在書架上吧。面試
網絡的概念外延被不斷的放大着,抽象的思惟能力是人們創新乃至創造的根源。網絡用來表示諸多對象及其相互聯繫,數學上的圖,物理學上的模型,交通網絡,人際網絡,城市網絡等等,總之,網絡被總結成從同類問題中抽象出來用數學中的圖論科學來表達並研究的一種模型。算法
不少夥伴認爲,瞭解這些以後呢,然並卵。咱們關心的只是計算機網絡,算機網絡是用通訊線路和設備將分佈在不一樣地點的多臺計算機系統互相鏈接起來,按照網絡協議,分享軟硬件功能,最終實現資源共享的系統。特別的,咱們談到的網絡只是互聯網——Internet,或者移動互聯網,須要的是寫互連網應用程序。可是,一位工做了五六年的編程高手曾對我說,如今終於瞭解到基礎知識有多重要,技術在不斷演進,而相對不變的就是那些原理和編程模型了。數據庫
老碼農深覺得然,編程實踐就是從具體到抽象,再到具體,循環往復,螺旋式上升的過程。瞭解前世此生,只是爲了可能觸摸到「勢」。基礎越紮實,建築就會越有想象的空間。 對於網絡編程的基礎,大概要從OSI的七層協議模型開始了。編程
七層模型(OSI,Open System Interconnection參考模型),是參考是國際標準化組織制定的一個用於計算機或通訊系統間互聯的標準體系。它是一個七層抽象的模型,不只包括一系列抽象的術語和概念,也包括具體的協議。 經典的描述以下:
簡述每一層的含義:
每一層利用下一層提供的服務與對等層通訊,每一層使用本身的協議。瞭解了這些,然並卵。可是,這一模型確實是絕大多數網絡編程的基礎,做爲抽象類存在的,而TCP/IP協議棧只是這一模型的一個具體實現。
TCP/IP是Internet的基礎,是一組協議的代名詞,包括許多協議,組成了TCP/IP協議棧。TCP/IP 有四層模型和五層模型之說,區別在於數據鏈路層是否做爲獨立的一層存在。我的傾向於5層模型,這樣2層和3層的交換設備更容易弄明白。當談到網絡的2層或3層交換機的時候,就知道指的是那些協議。
數據是如何傳遞呢?這就要了解網絡層和傳輸層的協議,咱們熟知的IP包結構是這樣的:
IP協議和IP地址是兩個不一樣的概念,這裏沒有涉及IPV6的。不關注網絡安全的話,對這些結構沒必要耳熟能詳的。傳輸層使用這樣的數據包進行傳輸,傳輸層又分爲面向鏈接的可靠傳輸TCP和數據報UDP。TCP的包結構:
TCP 鏈接創建的三次握手確定是必知必會,在系統調優的時候,內核中關於網絡的相關參數與這個圖息息相關。UDP是一種無鏈接的傳輸層協議,提供的是簡單不可靠的信息傳輸。協議結構相對簡單,包括源和目標的端口號,長度以及校驗和。基於TCP和UDP的數據封裝及解析示例以下:
仍是然並卵麼?一個數據包的大小了解了,會發現什麼呢?PayLoad究竟是多少?在設計協議通訊的時候,這些都爲咱們提供了粒度定義的依據。進一步,經過一個例子看看吧。
FTP是一個比較好的例子。爲了方便起見,假設兩條計算機分別是A 和 B,將使用FTP 將A上的一個文件X傳輸到B上。
首先,計算機A和B之間要有物理層的鏈接,能夠是有線好比同軸電纜或者雙絞線經過RJ-45的電路接口鏈接,也能夠是無線鏈接例如WIFI。先簡化一下,考慮局域網,暫不討論路由器和交換機以及WIFI熱點。這些物理層的鏈接創建了比特流的原始傳輸通路。
接下來,數據鏈路層登場,創建兩臺計算機的數據鏈路。若是A和B所在的網絡上同時鏈接着計算機C,D,E等等,A和B之間如何創建的數據鏈路呢?這一過程就是物理尋址,A要在衆多的物理鏈接中找到B,依賴的是計算機的物理地址即MAC地址,對就是網卡上的MAC地址。以太網採用CSMA/CD方式來傳輸數據,數據在以太網的局域網中都是以廣播方式傳輸的,整個局域網中的全部節點都會收到該幀,只有目標MAC地址與本身的MAC地址相同的幀纔會被接收。A經過差錯控制和接入控制找到了B的網卡,創建可靠的數據通路。
那IP地址呢? 數據鏈路創建起來了,還須要IP地址麼?咱們FTP 命令中制定的是IP地址而不是MAC地址呀?IP地址是邏輯地址,包括網絡地址和主機地址。若是A和B在不一樣的局域網中,中間有着多個路由器,A須要對B進行邏輯尋址才能夠的。物理地址用於底層的硬件的通訊,邏輯地址用於上層的協議間的通訊。在以太網中:邏輯地址就是IP地址,物理地址就是MAC 地址。在使用中,兩種地址是用必定的算法將他們兩個聯繫起來的。因此,IP是用來在網絡上選擇路由的,在FTP的命令中,IP中的原地址就是A的IP地址,目標地址就是B的IP地址。這應該就是網絡層,負責將分組數據從源端傳輸到目的端。
A向B傳輸一個文件時,若是文件中有部分數據丟失,就可能會形成在B上沒法正常閱讀或使用。因此須要一個可靠的鏈接,可以確保傳輸過程的完整性,這就是傳輸層的TCP協議,FTP就是創建在TCP之上的。TCP的三次握手肯定了雙方數據包的序號、最大接受數據的大小(window)以及MSS(Maximum Segment Size)。TCP利用IP完成尋址,TCP中的提供了端口號,FTP中目的端口號通常是21。傳輸層的端口號對應主機進程,指本地主機與遠程主機正在進行的會話。
會話層用來創建、維護、管理應用程序之間的會話,主要功能是對話控制和同步,編程中所涉及的session是會話層的具體體現。表示層完成數據的解編碼,加解密,壓縮解壓縮等,例如FTP中bin命令,表明了二進制傳輸,即所傳輸層數據的格式。 HTTP協議裏body中的Json,XML等均可以認爲是表示層。應用層就是具體應用的自己了,FTP中的PUT,GET等命令都是應用的具體功能特性。
簡單地,物理層到電纜鏈接,數據鏈路層到網卡,網絡層路由到主機,傳輸層到端口,會話層維持會話,表示層表達數據格式,應用層就是具體FTP中的各類命令功能了。
瞭解了7層模型就能夠編程了麼,拿起編程語言就能夠耍了麼?剛開始上手嘗試仍是能夠的,若是要進一步,老碼農以爲仍是看看底層實現的好,由於一切歸根到底都會歸結爲系統調用。到了操做系統層面如何看網絡呢?Socket登場了。
在Linux世界,「一切皆文件」,操做系統把網絡讀寫做爲IO操做,就像讀寫文件那樣,對外提供出來的編程接口就是Socket。因此,socket(套接字)是通訊的基石,是支持TCP/IP協議網絡通訊的基本操做單元。socket實質上提供了進程通訊的端點。進程通訊以前,雙方首先必須各自建立一個端點,不然是沒有辦法創建聯繫並相互通訊的。一個完整的socket有一個本地惟一的socket號,這是由操做系統分配的。
從設計模式的角度看, Socket實際上是一個外觀模式,它把複雜的TCP/IP協議棧隱藏在Socket接口後面,對用戶來講,一組簡單的Socket接口就是所有。當應用程序建立一個socket時,操做系統就返回一個整數做爲描述符(descriptor)來標識這個套接字。而後,應用程序以該描述符爲傳遞參數,經過調用函數來完成某種操做(例如經過網絡傳送數據或接收輸入的數據)。
在許多操做系統中,Socket描述符和其餘I/O描述符是集成在一塊兒的,操做系統把socket描述符實現爲一個指針數組,這些指針指向內部數據結構。進一步看,操做系統爲每一個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統把一個指向此文件內部數據結構的指針寫入文件描述符表,並把該表的索引值返回給調用者 。
既然Socket和操做系統的IO操做相關,那麼各操做系統IO實現上的差別會致使Socket編程上的些許不一樣。看看我Mac上的Socket.so 會發現和CentOS上的仍是些不一樣的。
進程進行Socket操做時,也有着多種處理方式,如阻塞式IO,非阻塞式IO,多路複用(select/poll/epoll),AIO等等。
多路複用每每在提高性能方面有着重要的做用。select系統調用的功能是對多個文件描述符進行監視,當有文件描述符的文件讀寫操做完成以及發生異常或者超時,該調用會返回這些文件描述符。select 須要遍歷全部的文件描述符,就遍歷操做而言,複雜度是 O(N)。
epoll相關係統調用是在Linux 2.5 後的某個版本開始引入的。該系統調用針對傳統的select/poll不足,設計上做了很大的改動。select/poll 的缺點在於:
epoll 是把 select/poll 單個的操做拆分爲 1 個 epollcreate,多個 epollctrl和一個 wait。此外,操做系統內核針對 epoll 操做添加了一個文件系統,每個或者多個要監視的文件描述符都有一個對應的inode 節點,主要信息保存在 eventpoll 結構中。而被監視的文件的重要信息則保存在 epitem 結構中,是一對多的關係。因爲在執行 epollcreate 和 epollctrl 時,已經把用戶模式的信息保存到內核了, 因此以後即使反覆地調用 epoll_wait,也不會重複地拷貝參數,不會重複掃描文件描述符,也不反覆地把當前進程放入/拿出等待隊列。
因此,當前主流的Server側Socket實現大都採用了epoll的方式,例如Nginx, 在配置文件能夠顯式地看到 use epoll
。
瞭解了7層協議模型和操做系統層面的Socket實現,能夠方便咱們理解網絡編程。
在系統架構的時候,有重要的一環就是拓撲架構,這裏涉及了網絡等基礎設施,那麼7層協議下四層就會有助於咱們對業務系統網絡結構的觀察和判斷。在系統設計的時候,每每採用面向接口的設計,而接口也每每是基於HTTP協議的Restful API。 那接口的粒度就能夠將data segment做爲一個約束了,同時能夠關注到移動互聯網中的弱網環境。
不一樣的編程語言,有着不一樣的框架和庫,真正的編寫網絡程序代碼並不複雜,例如,用Erlang 中 gen_tcp 用於編寫一個簡單的Echo服務器:
Start_echo_server()-> {ok,Listen}= gen_tcp:listen(1234,[binary,{packet,4},{reuseaddr,true},{active,true}]), {ok,socket}=get_tcp:accept(Listen), gen_tcp:close(Listen), loop(Socket). loop(Socket) -> receive {tcp,Socket,Bin} -> io:format(「serverreceived binary = ~p~n」,[Bin]) Str= binary_to_term(Bin), io:format(「server (unpacked) ~p~n」,[Str]), Reply= lib_misc:string2value(Str), io:format(「serverreplying = ~p~n」,[Reply]), gen_tcp:send(Socket,term_to_binary(Reply)), loop(Socket); {tcp_closed,Socket} -> Io:format(「ServerSocket closed ~n」) end.
然而,寫出漂亮的服務器程序仍然是一件很是吃功夫的事情,例如,我的很是喜歡的python Tornado 代碼, 在ioloop.py 中有對多路複用的選擇:
@classmethod def configurable_default(cls): if hasattr(select, "epoll"): from tornado.platform.epoll import EPollIOLoop return EPollIOLoop if hasattr(select, "kqueue"): # Python 2.6+ on BSD or Mac from tornado.platform.kqueue import KQueueIOLoop return KQueueIOLoop from tornado.platform.select import SelectIOLoop return SelectIOLoop
在HTTPServer.py 中一樣繼承了TCPServer,進而實現了HTTP協議,代碼片斷以下:
class HTTPServer(TCPServer, Configurable, httputil.HTTPServerConnectionDelegate): ... def initialize(self, request_callback, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, protocol=None, decompress_request=False, chunk_size=None, max_header_size=None, idle_connection_timeout=None, body_timeout=None, max_body_size=None, max_buffer_size=None): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol self.conn_params = HTTP1ConnectionParameters( decompress=decompress_request, chunk_size=chunk_size, max_header_size=max_header_size, header_timeout=idle_connection_timeout or 3600, max_body_size=max_body_size, body_timeout=body_timeout) TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, max_buffer_size=max_buffer_size, read_chunk_size=chunk_size) self._connections = set() ...
Java提供了很是易用的網絡API,調用這些API咱們能夠很方便的經過創建TCP/IP或UDP套接字,在網絡之間進行相互通訊,其中TCP要比UDP更加經常使用,但在本教程中咱們對這兩種方式都有說明。
在網站上還有其餘三個與Java網絡相關的教程,以下:
3.Java服務器多線程教程 (參與翻譯能夠聯繫咱們)
儘管Java網絡API容許咱們經過套接字(Socket)打開或關閉網絡鏈接,但全部的網絡通訊均是基於Java IO類 InputStream和OutputStream實現的。
此外,咱們還可使用Java NIO API中相關的網絡類,用法與Java網絡API基本相似,Java NIO API能夠以非阻塞模式工做,在某些特定的場景中使用非阻塞模式能夠得到較大的性能提高。
Java TCP網絡基礎
一般狀況下,客戶端打開一個鏈接到服務器端的TCP/IP鏈接,而後客戶端開始與服務器之間通訊,當通訊結束後客戶端關閉鏈接,過程以下圖所示:
ClientServerOpen ConnectionSend RequestReceive ResponseClose Connection
客戶端經過一個已打開的鏈接能夠發送不止一個請求。事實上在服務器處於接收狀態下,客戶端能夠發送儘量多的數據,服務器也能夠主動關閉鏈接。
Java中Socket類和ServerSocket類
當客戶端想要打開一個鏈接到服務器的TCP/IP鏈接時,就要使用到Java Socket類。socket類只須要被告知鏈接的IP地址和TCP端口,其他的都有Java實現。
假如咱們想要打開一個監聽服務,來監聽客戶端鏈接某些指定TCP端口的鏈接,那就須要使用Java ServerSocket類。當客戶端經過Socket鏈接服務器端的ServerSocket監聽時,服務器端會指定這個鏈接的一個Socket,此時客戶端與服務器端間的通訊就變成Socket與Socket之間的通訊。
關於Socket類和ServerSocket類會在後面的文章中有詳細的介紹。
Java UDP網絡基礎
UDP的工做方式與TCP相比略有不一樣。使用UDP通訊時,在客戶端與服務器之間並無創建鏈接的概念,客戶端發送到服務器的數據,服務器可能(也可能並無)收到這些數據,並且客戶端也並不知道這些數據是否被服務器成功接收。當服務器向客戶端發送數據時也是如此。
正由於是不可靠的數據傳輸,UDP相比與TCP來講少了不少的協議開銷。
在某些場景中,使用無鏈接的UDP要優於TCP,這些在文章Java UDP DatagramSocket類介紹中會有更多介紹。
當咱們想要在Java中使用TCP/IP經過網絡鏈接到服務器時,就須要建立java.net.Socket對象並鏈接到服務器。假如但願使用Java NIO,也能夠建立Java NIO中的SocketChannel對象。
建立Socket
下面的示例代碼是鏈接到IP地址爲78.64.84.171服務器上的80端口,這臺服務器就是咱們的Web服務器(www.jenkov.com),而80端口就是Web服務端口。
Socket socket = new Socket("78.46.84.171", 80);
咱們也能夠像以下示例中使用域名代替IP地址:
Socket socket = new Socket("jenkov.com", 80);
Socket發送數據
要經過Socket發送數據,咱們須要獲取Socket的輸出流(OutputStream),示例代碼以下:
Socket socket = new Socket("jenkov.com", 80); OutputStream out = socket.getOutputStream(); out.write("some data".getBytes()); out.flush(); out.close(); socket.close();
代碼很是簡單,可是想要經過網絡將數據發送到服務器端,必定不要忘記調用flush()方法。操做系統底層的TCP/IP實現會先將數據放入一個更大的數據緩存塊中,而緩存塊的大小是與TCP/IP的數據包大小相適應的。(譯者注:調用flush()方法只是將數據寫入操做系統緩存中,並不保證數據會當即發送)
Socket讀取數據
從Socket中讀取數據,咱們就須要獲取Socket的輸入流(InputStream),代碼以下:
Socket socket = new Socket("jenkov.com", 80); InputStream in = socket.getInputStream(); int data = in.read(); //... read more data... in.close(); socket.close();
代碼也並不複雜,但須要注意的是,從Socket的輸入流中讀取數據並不能讀取文件那樣,一直調用read()方法直到返回-1爲止,由於對Socket而言,只有當服務端關閉鏈接時,Socket的輸入流纔會返回-1,而是事實上服務器並不會不停地關閉鏈接。假設咱們想要經過一個鏈接發送多個請求,那麼在這種狀況下關閉鏈接就顯得很是愚蠢。
所以,從Socket的輸入流中讀取數據時咱們必需要知道須要讀取的字節數,這能夠經過讓服務器在數據中告知發送了多少字節來實現,也能夠採用在數據末尾設置特殊字符標記的方式連實現。
關閉Socket
當使用完Socket後咱們必須將Socket關閉,斷開與服務器之間的鏈接。關閉Socket只須要調用Socket.close()方法便可,代碼以下:
Socket socket = new Socket("jenkov.com", 80); socket.close();
Java 網絡教程: ServerSocket
用java.net.ServerSocket實現java服務經過TCP/IP監聽客戶端鏈接,你也能夠用Java NIO 來代替java網絡標準API,這時候須要用到 ServerSocketChannel。
如下是一個建立ServerSocket類來監聽9000端口的一個簡單的代碼
ServerSocket serverSocket = new ServerSocket(9000);
要獲取請求的鏈接須要用ServerSocket.accept()方法。該方法返回一個Socket類,該類具備普通java Socket類的全部特性。代碼以下:
ServerSocket serverSocket = new ServerSocket(9000); boolean isStopped = false;while(!isStopped){ Socket clientSocket = serverSocket.accept(); //do something with clientSocket}
對每一個調用了accept()方法的類都只得到一個請求的鏈接。
另外,請求的鏈接也只能在線程運行的server中調用了accept()方法以後纔可以接受請求。線程運行在server中其它全部的方法上的時候都不能接受客戶端的鏈接請求。因此」接受」請求的線程一般都會把Socket的請求鏈接放入一個工做線程池中,而後再和客戶端鏈接。更多關於多線程服務端設計的文檔請參考 java多線程服務
客戶端請求執行完畢,而且不會再有該客戶端的其它請求發送過來的時候,就須要關閉Socket鏈接,這和關閉一個普通的客戶端Socket鏈接同樣。以下代碼來執行關閉:
socket.close();
要關閉服務的時候須要關掉 ServerSocket鏈接。經過執行以下代碼:
serverSocket.close();
DatagramSocket類是java經過UDP通訊的途徑。UDP仍位於IP層的上面。 你能夠用DatagramSocket類發送和接收UDP數據包。
UDP 和TCP
UDP工做方式和TCP有點不一樣。當你經過TCP發送數據時,你先要建立鏈接。一旦TCP鏈接創建了,TCP會保證你的數據傳遞到對端,不然它將告訴你已發生的錯誤。
僅僅用UDP來發送數據包(datagrams)到網絡間的某個IP地址。你不能保證數據會不會到達。你也不能保證UDP數據包到達接收方的指令。這意味着UDP比TCP有更少的協議開銷(無完整檢查流)。
當數據傳輸過程當中不在意數據包是否丟失時,UDP就比較適合這樣的數據傳輸。好比,網上的電視信號的傳輸。你但願信號到達客戶端時儘量地接近直播。所以,若是丟失一兩個畫面,你一點都不在意。你不但願直播延遲,值想確保全部的畫面顯示在客戶端。你寧肯跳過丟失的畫面,但願一直看到最新的畫面。
這種狀況也會發生在網上攝像機直播節目中。誰會關心過去發生的什麼,你只想顯示當前的畫面。你不但願比實際狀況慢30s結束,只由於你想看到攝像機顯示給觀衆的全部畫面。這跟攝像機錄像有點不一樣。從攝像機錄製畫面到磁盤,你不但願丟失一個畫面。你可能還但願有點延遲,若是有重大的狀況發生,就不須要倒回去檢查畫面。
經過Java的DatagramSocket類發送數據,首先須要建立DatagramPacket。以下:
1 |
buffer = `new byte[ 65508`]; |
---|
2 |
---|
3 |
InetAddress address = `new DatagramPacket(buffer, buffer.length, address,9000 );` |
---|
字節緩衝塊(字節數組)就是UDP數據包中用來發送的數據。緩衝塊上限長度爲65508字節,是單一UDP數據包發送的最大的數據量。
數據包構造函數的長度就是緩存塊中用於發送的數據的長度。全部多於最大容量的數據都會被忽略。
包含節點(例如服務器)地址的InetAddress實例攜帶節點(如服務器)的地址發送的UDP數據包。InetAddress類表示一個ip地址(網絡地址)。getByName()方法返回帶有一個InetAddress實例,該實例帶有匹配主機名的ip地址。
端口參數是UDP端口服務器用來接收正在監聽的數據。UDP端口和TCP端口是不同的。一臺電腦同時有不一樣的進程監聽UDP和TCP 80端口。
爲了發送數據包,你須要建立DatagramSocket來發送數據。以下:
1 |
DatagramSocketdatagramSocket = `new DatagramSocket();` |
---|
調用send()方法發送數據,像這樣:
1 |
datagramSocket.send(packet); |
---|
完整示例:
1 |
DatagramSocket datagramSocket = `new DatagramSocket();` |
---|
2 |
---|
3 |
byte `[] buffer = "0123456789" .getBytes();` |
---|
4 |
---|
5 |
InetAddress receiverAddress = InetAddress.getLocalHost(); |
---|
6 |
---|
7 |
DataframPacket packet = `new DatagramPacket( buffer, buffer.length, receiverAddress,80 );` |
---|
8 |
datagramSocket.send(packet); |
---|
從DataframSocket獲取數據時,首先建立DataframPacket ,而後經過DatagramSocket類的receive()方法接收數據。例如:
1 |
DatagramSocket datagramSocket = `new DatagramSocket(80 );` |
---|
2 |
---|
3 |
byte `[] buffer = new` `byte [10 ];` |
---|
4 |
---|
5 |
DatagramPacket packet = `new DatagramPacket(buffer, buffer.length);` |
---|
6 |
---|
7 |
datagramSocket.receive(packet); |
---|
注意DatagramSocket是如何經過傳遞參數80到它的構造器初始化的。這個參數是UDP端口的DatagramSocket用來接收UDP數據包的。像以前提到的,TCP和UDP端口是不同的,也不重疊。你能夠有倆個不一樣的進程同時在端口80監聽TCP和UDP,沒有任何衝突。
第二,字節緩存塊和DatagramPacket建立了。注意DatagramPacket是沒有關於節點如何發送數據的信息的,當建立一個方數據的DatagramPacket時,它會直到這個信息。這就是爲何咱們會用DatagramPacket接收數據而不是發送數據。所以沒有目標地址是必須的。
最後,調用DatagramSocket的receive()方法。直到數據包接收到爲止,這個方法都是阻塞的。
接收的數據位於DatagramPacket的字節緩衝塊。緩衝塊能夠經過調用getData()得到:
1 |
byte `[] buffer = packet.getData();` |
---|
緩衝塊接收了多少的數據須要你去找出來。你用的協議應該定義每一個UDP包發多少數據,活着定義一個你能找到的數據結束標記。
一個真正的服務端程序可能會在一個loop中調用receive()方法,傳送全部接收到的DatagramPacket到工做的線程池中,就像TCP服務器處理請求鏈接同樣(查看Java Multithreaded Servers獲取更多詳情)
在java.net包中包含兩個有趣的類:URL類和URLConnection類。這兩個類能夠用來建立客戶端到web服務器(HTTP服務器)的鏈接。下面是一個簡單的代碼例子:
1 |
URL url = `new URL("[http://jenkov.com](https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fjenkov.com%2F)" );` |
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
InputStream input = urlConnection.getInputStream(); |
---|
4 |
int data = input.read(); |
---|
5 |
while `(data != -1 ){` |
---|
6 |
System.out.print(( `char`) data); |
---|
7 |
data = input.read(); |
---|
8 |
} |
---|
9 |
input.close(); |
---|
默認狀況下URLConnection發送一個HTTP GET請求到web服務器。若是你想發送一個HTTP POST請求,要調用URLConnection.setDoOutput(true)方法,以下:
1 |
URL url = `new URL("[http://jenkov.com](https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fjenkov.com%2F)" );` |
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
urlConnection.setDoOutput( `true`); |
---|
一旦你調用了setDoOutput(true),你就能夠打開URLConnection的OutputStream,以下:
1 |
OutputStream output = urlConnection.getOutputStream(); |
---|
你可使用這個OutputStream向相應的HTTP請求中寫任何數據,但你要記得將其轉換成URL編碼(關於URL編碼的解釋,自行Google)(譯者注:具體名字是:application/x-www-form-urlencoded MIME 格式編碼)。
當你寫完數據的時候要記得關閉OutputStream。
URL也被叫作統一資源定位符。若是你的代碼不關心文件是來自網絡仍是來自本地文件系統,URL類是另一種打開文件的方式。
下面是一個如何使用URL類打開一個本地文件系統文件的例子:
1 |
URL url = `new URL("file:/c:/data/test.txt" );` |
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
InputStream input = urlConnection.getInputStream(); |
---|
4 |
int data = input.read(); |
---|
5 |
while `(data != -1 ){` |
---|
6 |
System.out.print(( `char`) data); |
---|
7 |
data = input.read(); |
---|
8 |
} |
---|
9 |
input.close(); |
---|
注意:這和經過HTTP訪問一個web服務器上的文件的惟一不一樣處就是URL:」file:/c:/data/test.txt」。
Java網絡教程:JarURLConnection
Java的JarURLConnection類用來鏈接Java Jar文件。一旦鏈接上,你能夠獲取Jar文件的信息。一個簡單的例子以下:
01 |
String urlString = `"http://butterfly.jenkov.com/"` |
---|
02 |
+ `"container/download/"` |
---|
03 |
+ `"jenkov-butterfly-container-2.9.9-beta.jar"`; |
---|
04 |
---|
05 |
URL jarUrl = `new URL(urlString);` |
---|
06 |
JarURLConnection connection = `new JarURLConnection(jarUrl);` |
---|
07 |
---|
08 |
Manifest manifest = connection.getManifest(); |
---|
09 |
---|
10 |
JarFile jarFile = connection.getJarFile(); |
---|
11 |
//do something with Jar file... |
---|
_InetAddress_ 是 Java 對 IP 地址的封裝。這個類的實例常常和 UDP DatagramSockets 和 Socket,ServerSocket 類一塊兒使用。
_InetAddress_ 沒有公開的構造方法,所以你必須經過一系列靜態方法中的某一個來獲取它的實例。
<!–more–>
下面是爲一個域名實例化 InetAddres 類的例子:
InetAddress address = InetAddress.getByName("jenkov.com");
固然也會有爲匹配某個 IP 地址來實例化一個 InetAddress:
InetAddress address = InetAddress.getByName("78.46.84.171");
另外,它還有經過獲取本地 IP 地址的來獲取 _InetAddress_ 的方法(正在運行程序的那臺機器)
InetAddress address = InetAddress.getLocalHost();
_InetAddress_ 類還擁有大量你能夠調用的其它方法。例如:你能夠經過調用_getAddress()_方法來獲取 IP 地址的 byte 數組。若是要了解更多的方法,最簡單的方式就是讀 JavaDoc 文檔中關於 _InetAddress_ 類的部分。
若是設計一個客戶端到服務器的系統,那麼同時也須要設計客戶端和服務器之間的通訊協議。固然,有時候協議已經爲你決定好了,好比HTTP、XML_RPC(http response 的 body 使用xml)、或者SOAP(也是http response 的 body 使用xml)。設計客戶端到服務端協議的時候,一旦協議決定開啓一下子,來看一些你必須考慮的地方:
1. 客戶端到服務端的往返通信
2.區分請求結束和響應結束。
3.防火牆穿透
客戶端-服務端往返
當客戶端和服務端通訊,執行操做時,他們在交換信息。好比,客戶端執行一個服務請求,服務端嘗試完成這個請求,發回響應告訴客戶端結果。這種客戶端和服務端的信息交換就叫作往返。示意圖以下:
當一個計算機(客戶端或者服務端)在網絡中發送數據到另外一個計算機時,從數據發送到另外一端接收數據完會花費必定時間。這就是數據在網絡間的傳送的時間花費。這個時間叫作延遲。
協議中含有越多的往返,協議變得越慢,延遲特別高。HTTP協議只包含一個單獨的響應來執行服務。換句話說就是一個單獨的往返。另外一方面,在一封郵件發送前,SMTP協議包含了幾個客戶端和服務端的往返。
在協議中有多個往返的緣由是:有大量的數據從客戶端發送到服務端。這種狀況下你有2個選擇:
1.在分開往返中發送頭信息;
2.將消息分紅更小的數據塊。
若是服務端能完成頭信息的一些初始驗證 ,那麼分開發送頭信息是很明智的。若是頭信息是空白的,發送大量數據自己就是浪費資源。
在傳輸大量數據時,若是網絡鏈接失敗了,得從頭開始從新發送數據。數據分割發送時,只須要在網絡鏈接失敗處從新發送數據塊。已經發送成功的數據塊不須要從新發送。
區分請求結束和響應結束
若是協議允許在同一個鏈接中發送多個請求,須要一個讓服務端知道當前請求什麼時候結束、下一個請求什麼時候開始。客戶端也須要知道一個響應什麼時候結束了,下一個響應什麼時候開始。
對於請求有2個方法區分結束:
1.在請求的開始處發送請求的字長
2.在請求數據的最後發送一個結束標記。
HTTP用第一個機制。在請求頭中 發送了「Content-Length」。請求頭會告訴服務端在頭文件後有多少字節是屬於請求的。
這個模型的優點在於沒有請求結束標誌的開銷。爲了不數據看上去像請求結束標誌,也不須要對數據體進行編碼。
第一個方法的劣勢:在數據傳輸前,發送者必須知道多少字節數將被傳輸。若是數據時動態生成的,在發送前,首先你得緩存全部的數據,這樣才能計算出數據的字節數。
運用請求結束標誌時,不須要知道發送了多少字節數。只須要知道請求結束標誌在數據的末尾。固然,必須確認已發送的數據中不包含會致使請求結束標誌錯誤的數據。能夠這樣作:
能夠說請求結束標誌是字節值255。固然數據可能包含值255。所以,對數據中包含值255的每個字節添加一個額外的字節,還有值255。結束請求標誌被從字節值255到255以後的值爲0。以下編碼:
255 in data –>255, 255
end-of-request –> 255, 0
這種255,0的序列永遠不會出如今數據中,由於你把全部的值255變成了255,255。同時,255,255,0也不會被錯認爲255,0。255,255被理解成在一塊兒的,0是單獨的。
防火牆穿透
比起HTTP協議,大多數防火牆會攔截全部的其餘通訊。所以把協議放在HTTP的上層是個好方法,像XML-RPC,SOAP和REST也能夠這樣作。
協議置於HTTP的上層,在客戶端和服務端的HTTP請求和響應中能夠來回發送數據。記住,HTTP請求和響應不止包含text或者HTML。也能夠在裏面發送二進制數據。
將請求放置在HTTP協議上,惟一有點奇怪的是:HTTP請求必須包含一個「主機」頭字段。若是你在HTTP協議上設計P2P協議,一樣的人最可能不會運行多個「主機」。在這種狀況下須要頭字段是沒必要要的開銷(可是個小開銷)。