【java筆試系列八】Java網絡編程

一. 前言

網絡編程的目的就是指直接或間接地通過網絡協議與其他計算機進行通訊。
網絡編程中有兩個主要的問題

  • 一個是如何準確的定位網絡上一臺或多臺主機[【TCP/IP】
  • 一個就是找到主機後如何可靠高效的進行數據傳輸。【TCP/IP VS UDP】

在TCP/IP協議中IP層主要負責網絡主機的定位,數據傳輸的路由,由IP地址可以唯一地確定Internet上的一臺主機。而TCP層則提供面向應用的可靠的或非可靠的數據傳輸機制,這是網絡編程的主要對象,一般不需要關心IP層是如何處理數據的。

目前較爲流行的網絡編程模型是客戶機/服務器(C/S)結構。即通信雙方一方作爲服務器等待客戶提出請求並予以響應。客戶則在需要服務時向服務器提出申請。服務器一般作爲守護進程始終運行,監聽網絡端口,一旦有客戶請求,就會啓動一個服務進程來響應該客戶,同時自己繼續監聽服務端口,使後來的客戶也能及時得到服務。
學習網絡編程前還得先把計算機網絡的基礎知識複習一下,先能回答下面七個問題:

1. 什麼是TCP/IP協議?

TCP是Tranfer Control Protocol的簡稱,是一種面向連接保證可靠傳輸的協議。通過TCP協議傳輸,得到的是一個順序的無差錯的數據流。發送方和接收方的成對的兩個socket之間必須建立連接,以便在TCP協議的基礎上進行通信,當一個socket(通常都是server socket)等待建立連接時,另一個socket可以要求進行連接,一旦這兩個socket連接起來,它們就可以進行雙向數據傳輸,雙方都可以進行發送或接收操作。

2. TCP/IP有哪兩種傳輸協議,各有什麼特點?

儘管TCP/IP協議的名稱中只有TCP這個協議名,但是在TCP/IP的傳輸層同時存在TCP和UDP兩個協議。

UDP是User Datagram Protocol的簡稱,是一種無連接的協議,每個數據報都是一個獨立的信息,包括完整的源地址或目的地址,它在網絡上以任何可能的路徑傳往目的地,因此能否到達目的地,到達目的地的時間以及內容的正確性都是不能被保證的。
  下面我們對這兩種協議做簡單比較:
  使用UDP時,每個數據報中都給出了完整的地址信息,因此無需要建立發送方和接收方的連接。對於TCP協議,由於它是一個面向連接的協議,在socket之間進行數據傳輸之前必然要建立連接,所以在TCP中多了一個連接建立的時間。
  使用UDP傳輸數據時是有大小限制的,每個被傳輸的數據報必須限定在64KB之內。而TCP沒有這方面的限制,一旦連接建立起來,雙方的socket就可以按統一的格式傳輸大量的數據。UDP是一個不可靠的協議,發送方所發送的數據報並不一定以相同的次序到達接收方。而TCP是一個可靠的協議,它確保接收方完全正確地獲取發送方所發送的全部數據。
  總之,TCP在網絡通信上有極強的生命力,例如遠程連接(Telnet)和文件傳輸(FTP)都需要不定長度的數據被可靠地傳輸。相比之下UDP操作簡單,而且僅需要較少的監護,因此通常用於局域網高可靠性的分散系統中client/server應用程序。
  既然有了保證可靠傳輸的TCP協議,爲什麼還要非可靠傳輸的UDP協議呢?主要的原因有兩個。一是可靠的傳輸是要付出代價的,對數據內容正確性的檢驗必然佔用計算機的處理時間和網絡的帶寬,因此TCP傳輸的效率不如UDP高。二是在許多應用中並不需要保證嚴格的傳輸可靠性,比如視頻會議系統,並不要求音頻視頻數據絕對的正確,只要保證連貫性就可以了,這種情況下顯然使用UDP會更合理一些。

3. 什麼是URL?

統一資源定位器URL
URL(Uniform Resource Locator)是統一資源定位器的簡稱,它表示Internet上某一資源的地址。通過URL我們可以訪問Internet上的各種網絡資源,比如最常見的WWW,FTP站點。瀏覽器通過解析給定的URL可以在網絡上查找相應的文件或其他資源。

4. URL和IP地址有什麼樣的關係?

形象點說:
IP就是一個全球唯一的門牌號,每個上網的設備一旦接入網絡,即會由網絡分配給一個唯一的號碼;
URL是一個全球唯一的企業或個人名稱,相當於域名
URL必須通過DNS服務器把它轉換成門牌號(即IP),才能找到放在哪架網上的服務器上。

5. 什麼叫套接字(Socket)?

Socket通常也稱作”套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。在Internet上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不同的端口對應於不同的服務。

6. 套接字(Socket)和TCP/IP協議的關係?

套接字的作用是網絡編程的一個接口,底層使用的是TCP/IP協議

7. URL和套接字(Socket)的關係?

利用socket進行通信時,服務器端的程序可以打開多個線程與多個客戶進行通信,還可以通過服務器使各個客戶之間進行通信。這種方式比較靈活,適用於一些較複雜的通信,但是服務器端的程序必須始終處於運行狀態以監聽端口。
利用 URL進行通信時,服務器端的程序只能與一個客戶進行通信,形式比較單一。但是它不需要服務器端的CGI程序一直處於運行狀態,只是在有客戶申請時才被激活。所以,這種方式比較適用於客戶機的瀏覽器與服務器之間的通信。

二. 基於URL的Java網絡編程

1.創建一個URL

爲了表示URL, java.NET中實現了類URL。我們可以通過下面的構造方法來初始化一個URL對象:
  

1) public URL (String spec);//通過一個表示URL地址的字符串可以構造一個URL對象
     URL urlBase=new URL("http://www.data520.cn/")
(2) public URL(URL context, String spec);//通過基URL和相對URL構造一個URL對象。
     URL net263=new URL ("http://www.data520.cn/");
     URL index263=new URL(data520, "index.html")
(3) public URL(String protocol, String host, String file);
     new URL("http", "www.gamelan.com", "/pages/Gamelan.net. html");
(4) public URL(String protocol, String host, int port, String file);
     URL gamelan=new URL("http", "www.gamelan.com", 80, "Pages/Gamelan.network.html");

  注意:類URL的構造方法都聲明拋棄非運行時例外(MalformedURLException),因此生成URL對象時,我們必須要對這一例外進行處理,通常是用try-catch語句進行捕獲。格式如下:

try{
     URL myURL= new URL(…)
  }
catch (MalformedURLException e){
 
   }

2. 解析一個URL

一個URL對象生成後,其屬性是不能被改變的,但是我們可以通過類URL所提供的方法來獲取這些屬性:
   

     public String getProtocol() 獲取該URL的協議名。
   public String getHost() 獲取該URL的主機名。
   public int getPort() 獲取該URL的端口號,如果沒有設置端口,返回-1。
   public String getFile() 獲取該URL的文件名。
   public String getRef() 獲取該URL在文件中的相對位置。
   public String getQuery() 獲取該URL的查詢信息。
   public String getPath() 獲取該URL的路徑
    public String getAuthority() 獲取該URL的權限信息
   public String getUserInfo() 獲得使用者的信息
   public String getRef()獲得該URL的錨

3.從URL讀取WWW網絡資源

當我們得到一個URL對象後,就可以通過它讀取指定的WWW資源。這時我們將使用URL的方法openStream(),其定義爲:InputStream openStream();方法openSteam()與指定的URL建立連接並返回InputStream類的對象以從這一連接中讀取數據。

package com.data520.test;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;

public class URLReader {

    public static void main(String[] args) throws Exception {
        URL url = new URL("http://www.data520.cn");
        BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
            System.out.println(inputLine);
        }
        in.close();
    }
}

4.通過URLConnetction連接WWW

通過URL的方法openStream(),我們只能從網絡上讀取數據,如果我們同時還想輸出數據,例如向服務器端的CGI程序發送一些數據,我們必須先與URL建立連接,然後才能對其進行讀寫,這時就要用到類URLConnection了。CGI是公共網關接口(Common Gateway Interface)的簡稱,它是用戶瀏覽器和服務器端的應用程序進行連接的接口,有關CGI程序設計,請讀者參考有關書籍。

類URLConnection也在包java.net中定義,它表示Java程序和URL在網絡上的通信連接。當與一個URL建立連接時,首先要在一個URL對象上通過方法openConnection()生成對應的URLConnection對象。例如下面的程序段首先生成一個指向地址http://blog.csdn.net/shengmingqijiquan的對象,然後用openConnection()打開該URL對象上的一個連接,返回一個URLConnection對象。如果連接過程失敗,將產生IOException

Try{
   URL netchinaren = new URL ("http://blog.csdn.net/shengmingqijiquan");
   URLConnectonn tc = netchinaren.openConnection();
}catch(MalformedURLException e){ //創建URL()對象失敗
  …
}catch (IOException e){ //openConnection()失敗
  …
}

類URLConnection提供了很多方法來設置或獲取連接參數,程序設計時最常使用的是getInputStream()和getOurputStream(),其定義爲:
InputSteram getInputSteram();
OutputSteram getOutputStream();
通過返回的輸入/輸出流我們可以與遠程對象進行通信。看下面的例子:

package com.data520.test;

import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.URL;
import java.net.URLConnection;

public class URLReader {

    public static void main(String[] args) throws Exception {
        //創建URL對象
        URL url=new URL("http://blog.csdn.net/shengmingqijiquan");
        //由URL對象獲取URLConnection對象
        URLConnection conn=url.openConnection();
        //由URLConnection獲取輸入流,並構造DataInputStream對象
        DataInputStream dis=new DataInputStream(conn.getInputStream()); 
        //由URLConnection獲取輸出流,並構造PrintStream對象
        PrintStream ps=new PrintStream(conn.getOutputStream());
        String line=dis.readLine(); 
        ps.println("client…"); //向服務器寫出字符串 "client…"
    }
}

其中backwards爲服務器端的CGI程序。實際上,類URL的方法openSteam()是通過URLConnection來實現的。它等價於openConnection().getInputStream();
基於URL的網絡編程在底層其實還是基於下面要講的Socket接口的。WWW,FTP等標準化的網絡服務都是基於TCP協議的,所以本質上講URL編程也是基於TCP的一種應用.

三.基於Socket的Java網絡編程

網絡上的兩個程序通過一個雙向的通訊連接實現數據的交換,這個雙向鏈路的一端稱爲一個Socket。Socket通常用來實現客戶方和服務方的連接。一個Socket由一個IP地址和一個端口號唯一確定。在傳統的UNIX環境下可以操作TCP/IP協議的接口不止Socket一個,Socket所支持的協議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯繫的。在Java環境下,Socket編程主要是指基於TCP/IP協議的網絡編程。

1.Socket通訊的一般過程

使用Socket進行Client/Server程序設計的一般連接過程是這樣的:

  • Server端Listen(監聽)某個端口是否有連接請求
  • Client端向Server端發出Connect(連接)請求
  • Server端向Client端發回Accept(接受)消息。

一個連接就建立起來了。Server端和Client端都可以通過Send,Write等方法與對方通信。
對於一個功能齊全的Socket,都要包含以下基本結構,其工作過程包含以下四個基本的步驟:

1. 創建ServerSocket和Socket
2. 打開連接Socket的輸入/輸出流
3. 按照協議(通常是TCP/UDP)對Socket進行讀寫操作
4. 關閉輸入/輸出流,關閉Socket

Socket通信模型如下圖所示
這裏寫圖片描述

2.Server服務器端:

a、創建ServerSocket對象,同時綁定監聽端口
b、通過accept()方法監聽客戶端的請求
c、建立連接後,通過輸入流讀取客戶端發送的請求信息
d、通過輸出流向客戶端發送響應信息
e、關閉相應資源
package com.data520;  

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.io.OutputStream;  
import java.io.PrintWriter;  
import java.net.ServerSocket;  
import java.net.Socket;  
import java.util.Date;  

/** 
 * 基於TCP協議的服務端Socket通信 
 * 服務器端必須早於客戶端啓動 
 * 
 */  
public class ServerSocketDemo {  

    public static void main(String[] args) {  
        try {  
            //1、創建一個服務端Socket,即ServerSocket對象,指定綁定的端口,並偵聽該端口  
            ServerSocket serverSocket = new ServerSocket(5555);  

            //2、調用accept()方法開始偵聽客戶端請求,創建Socket,等待客戶端的連接  
            System.out.println("===================服務器即將啓動,等待客戶端的連接===============");  
            Socket socket = serverSocket.accept();  

            //3、獲取輸入字節流,讀取客戶端請求信息  
            InputStream is = socket.getInputStream();  

            //將字節流包裝成字符流  
            InputStreamReader isr = new InputStreamReader(is);  

            //爲字符輸入流添加緩衝  
            BufferedReader br = new BufferedReader(isr);  

            //讀取字符輸入流中的數據信息  
            String data = null;  
            while(null != (data = br.readLine())){  
                System.out.println(new Date());  
                System.out.println("我是服務器端,客戶端說:"+data);  
            }  
            //調用shutdown方法關閉輸入流  
            socket.shutdownInput();  

            //4、獲取輸出字節流,響應客戶端的信息  
            OutputStream os = socket.getOutputStream();  

            //將字節流包裝成爲字符打印流  
            PrintWriter pw = new PrintWriter(os);  

            //向客戶端回覆響應消息  
            pw.write("用戶名和密碼輸入正確");  
            //刷新緩存  
            pw.flush();  

            //關閉socket輸出流  
            socket.shutdownOutput();  

            //5、關閉資源  
            pw.close();  
            os.close();  
            br.close();  
            isr.close();  
            is.close();  
            socket.close();  
            serverSocket.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  

}  

運行結果:
服務器端:
這裏寫圖片描述

3.Client客戶端:

a、創建Socket對象,指明需要連接的服務器的地址和端口號
b、建立連接後,通過輸出流向服務器端發送請求信息
c、通過輸入流獲取服務器的響應信息
d、關閉相應資源
package com.data520;  

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.io.OutputStream;  
import java.io.PrintWriter;  
import java.net.Socket;  
import java.net.UnknownHostException;  
import java.util.Date;  

/** 
 * 基於TCP協議的客戶端Socket通信 
 *  
 */  
public class ClientSocketDemo {  

    public static void main(String[] args) {  
        try {  
            //1、創建客戶端Socket,指定服務器地址和端口號,向服務端發送請求信息  
            Socket socket = new Socket("localhost", 5555);  

            //2、獲取輸出字節流,向服務器端發送消息  
            OutputStream os = socket.getOutputStream();  

            //3、將字節輸出流包裝爲字符打印流  
            PrintWriter pw = new PrintWriter(os);  

            //向服務器端發送請求信息  
            StringBuffer bf = new StringBuffer();  
            bf.append("用戶名:").append("admin");  
            bf.append("密碼:").append("123");  
            pw.write(bf.toString());  

            //刷新緩存  
            pw.flush();  
            //關閉Socket的輸出流  
            socket.shutdownOutput();  

            //3、獲取輸入字節流,讀取服務器端的響應信息  
            InputStream is = socket.getInputStream();  

            //將輸入字節流包裝成字符字符流  
            InputStreamReader isr = new InputStreamReader(is);  

            //爲字符流添加緩衝區  
            BufferedReader br = new BufferedReader(isr);  

            //通過服務器端的響應信息  
            String data = null;  
            while(null != (data=br.readLine())){  
                System.out.println(new Date());  
                System.out.println("我是客戶端,服務器端說:"+data);  
            }  
            //關閉Socket輸入流  
            socket.shutdownInput();  

            //4、關閉資源  
            br.close();  
            isr.close();  
            is.close();  
            pw.close();  
            os.close();  
            socket.close();  
        } catch (UnknownHostException e) {  
            e.printStackTrace();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  

}  

運行結果:
客戶端
這裏寫圖片描述

四.Socket多線程編程

之前在前面已經介紹了Socket通信的一些基本原理,以及如何讓客戶端與服務器端建立通信,和實現通信的一些基本步驟(包括首先使得服務器端與客戶端建立連接,建立連接之後,服務器端開始偵聽客戶端的請求,偵聽到客戶端的請求之後,通過輸入輸出流處理相關信息實現通信,最後通信完畢結束通信等一系列流程)。
但是之前只是單個客戶端與服務器進行通信,而我們實際應用中單個客戶端的情況幾乎不存在,都是多個客戶端同時與服務器進行交互(這裏同時交互就會出現併發性的問題,對於併發性的問題暫時還不是很懂,只知道有這個概念),那就需要服務器端不停的偵聽客戶端的請求(寫在while循環中,並且條件始終爲true,死循環),每當偵聽到一個客戶端的請求時,都需要創建一個Socket與之建立通信(通過線程實現,每當偵聽到一個客戶端的請求,服務端都要單獨開闢一條線程與之進行通信)。

1、服務端Socket,(這裏面通過死循環讓服務器端一直循環偵聽來自客戶端的請求)

需要注意的是,服務端必須要先於客戶端啓動,因爲要啓動之後才能偵聽客戶端的請求

package com.socket;  

import java.io.IOException;  
import java.net.ServerSocket;  
import java.net.Socket;  

/** 
 * 服務器端Socket 
 * 
 */  
public class ServerSocketMany {  

    /** 
     * main測試方法 
     * @param args 
     * @throws IOException 
     */  
    public static void main(String[] args) throws IOException{  

        System.out.println("服務器已經啓動,等待客戶端的連接....");  

        //創建服務器端Socket,即ServerSOcket,等待客戶端的請求  
        ServerSocket server = new ServerSocket(5555);  

        int count = 0;//偵聽到的客戶端的數量  

        Socket socket = null;//服務器端Socket  

        //死循環,讓服務端循環偵聽  
        while(true){  

            //服務端開始偵聽客戶端的連接  
            socket = server.accept();  

            //啓動線程,與客戶端進行通信  
            Thread serverThread = new ServerThread(socket);  
            serverThread.start();  

            //計數,統計客戶端連接數  
            count++;  

            System.out.println("當前鏈接的客戶端的數量爲:"+count+"個....");  
        }  
    }  
}  

2.服務器端的線程實現類,線程中主要是實現與客戶端的通信(通過輸入輸出流接收並響應數據信息)

package com.socket;  

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.io.OutputStream;  
import java.io.PrintWriter;  
import java.net.Socket;  

/** 
 * 創建一個服務端線程,循環偵聽客戶端的請求,多個客戶端 
 * 
 * /  
public class ServerThread extends Thread{  

    //每當偵聽到一個新的客戶端的時,服務端這邊都要有一個Socket與之進行通信  
    public Socket socket = null;  

    //默認的構造方法,保留  
    public ServerThread(){}  

    //帶參構造方法  
    public ServerThread(Socket socket){  
        this.socket = socket;  
    }  

    //覆寫run方法  
    public void run(){  
        //獲取輸入字節流  
        InputStream in = null;  

        //將輸入字節流包裝成輸入字符流  
        InputStreamReader isr = null;  

        //爲字符輸入流添加緩衝  
        BufferedReader br = null;  

        //收到信息之後,向客戶端響應信息,獲取輸出字節流  
        OutputStream out = null;  

        //將字節輸出流包裝成字符打印輸出流  
        PrintWriter pw = null;  

        try {  

            in = socket.getInputStream();  
            isr = new InputStreamReader(in);  
            br = new BufferedReader(isr);  

            //讀取字符輸入流中的數據  
            String data = null;  
            while((data = br.readLine()) != null){  
                System.out.println("我是服務器,客戶端說:"+data);  
            }  

            //調用shutDown方法關閉Socket輸入流  
            socket.shutdownInput();  

            out = socket.getOutputStream();  
            pw = new PrintWriter(out);  
            pw.write("用戶名和密碼正確");  
            pw.flush();  

            //調用shutDown方法關閉Socket輸出流  
            socket.shutdownOutput();  

        } catch (IOException e) {  
            e.printStackTrace();  
        }finally{  

            //關閉資源  
            try {  
                if(null != pw)  
                    pw.close();  
                if(null != out)  
                    out.close();  
                if(null != br)  
                    br.close();  
                if(null != isr)  
                    isr.close();  
                if(null != in)  
                    in.close();  
                if(null != socket)  
                    socket.close();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  

3.客戶端Socket,這個和單客戶端的情況一樣

package com.socket;  

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.io.OutputStream;  
import java.io.PrintWriter;  
import java.net.Socket;  
import java.net.UnknownHostException;  

/** 
 * 客戶端Socket 
 * 
 */  
public class ClientSocketDemo {  

    /** 
     * main測試方法 
     * @param args 
     * @throws UnknownHostException 
     * @throws IOException 
     */  
    public static void main(String[] args) throws UnknownHostException, IOException{  

        //創建客戶端Socket,向服務器發起請求  
//      String address = "127.0.0.1";  
        String address = "localhost";//服務器地址  

        int port = 5555;//端口號  
        Socket socket = new Socket(address, port);  

        //獲取Socket的字節輸出流  
        OutputStream out = socket.getOutputStream();  

        //將字節輸出流包裝成字符打印輸出流  
        PrintWriter pw = new PrintWriter(out);  

//      pw.print("用戶名:admin,密碼:123");  
//      pw.print("用戶名:tom,密碼:456");  
//      pw.print("用戶名:dark,密碼:789");  
//      pw.print("用戶名:white,密碼:111");  
        pw.print("用戶名:green,密碼:222");  

        //刷新輸出流  
        pw.flush();  

        //調用shutDown方法關閉Socket的輸出流  
        socket.shutdownOutput();  

        //接收服務器端發來的響應信息,獲取字節輸入流  
        InputStream in = socket.getInputStream();  

        //將字節輸入流包裝成字符輸入流  
        InputStreamReader isr = new InputStreamReader(in);  

        //爲字符輸入流添加緩衝  
        BufferedReader br = new BufferedReader(isr);  

        //讀取字符輸入流中的數據信息  
        String data = null;  
        while((data = br.readLine()) != null){  
            System.out.println("我是客戶端,服務器說:"+data);  
        }  

        //調用shutDown方法關閉Socket的輸入流  
        socket.shutdownInput();  

        br.close();  
        isr.close();  
        in.close();  
        pw.close();  
        out.close();  
        socket.close();  
    }  
}  

運行結果:
服務器端:
這裏寫圖片描述

客戶端:
這裏寫圖片描述

參考:
http://blog.csdn.net/hu1991die/article/details/41210957
http://blog.csdn.net/csh624366188/article/details/7331716

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章