Java網路程式設計快速上手(SE基礎)

GaoYuan206 發表於 2021-04-17
Java

參考資料:百度百科TCP協議

本文涉及Java IO流、異常的知識,可參考我的另外的部落格

一文簡述Java IO

一文簡述JAVA內部類和異常

1.概述

計算機網路相關知識:

OSI七層模型

Java網路程式設計快速上手(SE基礎)

一個報文可以類似於一封信,就像下圖(引自狂神說Java)非常生動。

Java網路程式設計快速上手(SE基礎)

網路程式設計的目的:資料交換、通訊

網路通訊的要素:

如何實現網路通訊?

通訊雙方地址:

  • ip
  • 埠號

網路協議:

HTTP, FTP, TCP, UDP 等等

1.1 IP

IP地址:InetAddress(無構造器)

  • 唯一定位一臺網路上計算機
  • 127.0.0.1 :本機,localhost
  • ip地址分類:ipv4(4個位元組)/ipv6(128位,8個無符號整陣列成),公網(ABCD類地址)/私網(區域網)
  • 域名:記憶ip問題

主機名解析

主機名稱到IP地址解析是通過使用本地機器配置資訊和網路命名服務(如域名系統(DNS)和網路資訊服務(NIS))的組合來實現的。所使用的特定命名服務是預設配置的本地機器。對於任何主機名,返回其對應的IP地址。反向名稱解析意味著對於任何IP地址,返回與IP地址關聯的主機。

InetAddress類提供了將主機名解析為其IP地址的方法,反之亦然。

InetAddress常用方法:

Java網路程式設計快速上手(SE基礎)

舉例:

public static void main(String[] args) throws UnknownHostException {
    //查詢本機地址
    InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
    System.out.println(inetAddress);

    InetAddress i1 = InetAddress.getByName("localhost");
    System.out.println(i1);

    InetAddress i2 = InetAddress.getLocalHost();
    System.out.println(i2);

    //查詢網站ip
    InetAddress i3 = InetAddress.getByName("www.baidu.com");
    System.out.println(i3);
    //常用方法
    System.out.println(i3.getAddress());   //返回的是byte[],所以輸出了亂碼
    System.out.println(i3.getCanonicalHostName());//規範的名字
    System.out.println(i3.getHostAddress());//ip
    System.out.println(i3.getHostName());//主機名
}

InetAddress沒有構造器,所以需要呼叫靜態方法進行構造。上述程式碼結果為:

Java網路程式設計快速上手(SE基礎)

1.2 埠

埠表示計算機上的一個程式的程式

  • 不同的程式有不同的埠號,用來區分軟體。
  • 一般被規定為0~65535
  • TCP埠和UDP埠,均有65536個,兩個互不衝突。單個協議下埠是不能衝突的,例:TCP佔用8080後,不能再次佔用此TCP埠了
  • 埠分類:公有埠01023,HTTP:80,HTTPS:443,FTP:21,Telent:23。程式註冊的埠102449151,用來分配給使用者或者程式,Tomcat:8080,MySQL:3306,Oracle:1521。動態、私有:49152~65535,儘量不要用這裡的埠。
netstat -ano  #這條命令用於檢視所有埠
netstat -ano|findstr "8080"   #檢視指定的埠
tasklist|findstr "8696" #檢視指定埠的程式

以上均為Linux命令

InetSocketAddress

該類實現IP套接字地址(IP地址+埠號)它也可以是一對(主機名+埠號),在這種情況下將嘗試解析主機名。如果解決方案失敗,那麼該地址被認為是未解決的,但在某些情況下仍可以使用,例如通過代理連線。

它提供了用於繫結,連線或返回值的套接字所使用的不可變物件。

萬用字元是一個特殊的本地IP地址。 通常意味著“任何”,只能用於bind操作。

InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress inetSocketAddress1 = new InetSocketAddress("localhost",8080);
System.out.println(inetSocketAddress);
System.out.println(inetSocketAddress1);
System.out.println(inetSocketAddress.getAddress());
System.out.println(inetSocketAddress.getHostName());
System.out.println(inetSocketAddress.getPort());

以上為相關程式碼。

1.3 通訊協議

網路通訊協議可能涉及到:速率,傳輸位元速率,程式碼結構,傳輸控制等等

主要涉及的是以下兩個:

TCP:使用者傳輸協議(3次握手,確定返回資訊,以後網路相關知識具體說,不在本篇贅述)

UDP:使用者資料包協議(不確定返回資訊)

TCP和UDP對比

TCP就像打電話,需要連線,穩定

UDP就像發簡訊,不需要連線,發完即結束,不穩定

  • 基於連線與無連線;

  • 對系統資源的要求(TCP較多,UDP少);

  • UDP程式結構較簡單;

  • 流模式與資料包模式 ;(從下面demo中即可看出)

  • TCP保證資料正確性,UDP可能丟包;

  • TCP保證資料順序,UDP不保證。

2. TCP協議

Java網路程式設計快速上手(SE基礎)

TCP三次握手的過程如下:

  1. 客戶端傳送SYN(SEQ=x)報文給伺服器端,進入SYN_SEND狀態。
  2. 伺服器端收到SYN報文,回應一個SYN (SEQ=y)ACK(ACK=x+1)報文,進入SYN_RECV狀態。
  3. 客戶端收到伺服器端的SYN報文,回應一個ACK(ACK=y+1)報文,進入Established狀態。

三次握手完成,TCP客戶端和伺服器端成功地建立連線,可以開始傳輸資料了。

Java網路程式設計快速上手(SE基礎)

TCP連線終止過程:

建立一個連線需要三次握手,而終止一個連線要經過四次握手,這是由TCP的半關閉(half-close)造成的。具體過程如上圖所示。

(1) 某個應用程式首先呼叫close,稱該端執行“主動關閉”(active close)。該端的TCP於是傳送一個FIN分節,表示資料傳送完畢。

(2) 接收到這個FIN的對端執行 “被動關閉”(passive close),這個FIN由TCP確認。

注意:FIN的接收也作為一個檔案結束符(end-of-file)傳遞給接收端應用程式,放在已排隊等候該應用程式接收的任何其他資料之後,因為,FIN的接收意味著接收端應用程式在相應連線上再無額外資料可接收。

(3) 一段時間後,接收到這個檔案結束符的應用程式將呼叫close關閉它的套接字。這導致它的TCP也傳送一個FIN。

(4) 接收這個最終FIN的原傳送端TCP(即執行主動關閉的那一端)確認這個FIN。 [3]

既然每個方向都需要一個FIN和一個ACK,因此通常需要4個分節。

TCP協議相關資料參考自百度百科,計算機網路相關知識不再詳細描述。

2.1 TCP連線的實現

服務端:

public class TestTCPserver {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        Socket accept = serverSocket.accept();//等待client連線
        InputStream is = accept.getInputStream();
        //管道流,將一個輸入流通過管道轉化為一個合適的輸出流,不用管道流直接String可能會輸出亂碼
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while((len=is.read(buffer))!=-1){
            baos.write(buffer,0,len);
        }
        System.out.println(baos.toString());
        baos.close();
        is.close();
        accept.close();
        serverSocket.close();
        //正式寫程式碼的過程中,一定要用try,catch,finally,為了程式碼的安全,出了事故容易判斷
    }
}

用到了socket類,其中涉及了IO流部分的知識。

客戶端:

public class TestTCPclient {
    public static void main(String[] args) throws IOException {
        InetAddress serverIp = InetAddress.getByName("127.0.0.1");
        int port = 9999;
        //建立一個socket連線,連線的是本機
        Socket socket = new Socket(serverIp,port);
        OutputStream os = socket.getOutputStream();
        os.write("hello".getBytes());
        os.close();
        socket.close();
    }
}

注意:socket是需要關閉的

Java網路程式設計快速上手(SE基礎)

  • socket該類實現客戶端套接字(也稱為“套接字”)。套接字是兩臺機器之間通訊的端點。套接字的實際工作由SocketImpl類的例項執行。 應用程式通過更改建立套接字實現的套接字工廠,可以配置自己建立適合本地防火牆的套接字。

  • ServerSocket該類實現了伺服器套接字。 伺服器套接字等待通過網路進入的請求。 它根據該請求執行一些操作,然後可能將結果返回給請求者。
    伺服器套接字的實際工作由SocketImpl類的例項執行。 應用程式可以更改建立套接字實現的套接字工廠,以配置自己建立適合本地防火牆的套接字。

客戶端:連線伺服器socket,傳送訊息

伺服器:建立服務的埠 ServerSocket,等待使用者連線,接受使用者訊息

2.1 TCP實現檔案上傳

與訊息傳遞類似,只是IO操作稍微變了一下

服務端:

public class TCPserverdemo1 {
    public static void main(String[] args) throws IOException {
        //建立服務
        ServerSocket serverSocket = new ServerSocket(9000);
        //監聽客戶端的連線
        Socket accept = serverSocket.accept();  //阻塞式監聽,會一定等待客戶端連線
        InputStream is = accept.getInputStream();
        FileOutputStream fos = new FileOutputStream(new File("copide1.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        fos.close();
        is.close();
        accept.close();
        serverSocket.close();
    }
}

客戶端:

public class TCPclientdemo1 {
    public static void main(String[] args) throws IOException {
        //建立一個socket連線
        Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9000);
        //建立一個輸出流
        OutputStream os = socket.getOutputStream();  //.getOutputStream獲得了一個SocketOutputStream例項
        //讀取檔案
        FileInputStream fis = new FileInputStream(new File("image.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while((len=fis.read(buffer))!=-1){
            os.write(buffer,0,len);    //涉及到BIO
        }
        fis.close();
        os.close();
        socket.close();
    }
}

上面用的是位元組流,位元組緩衝流也可以使用。字元流不可以,因為可能會讀其他檔案的型別,位元組流比較穩妥。

伺服器接收完資訊後其實是可以返回訊息到客戶端的,socket通訊:

服務端可增加:

accept.shutdownInput();
//通知客戶端已經接收完畢
OutputStream os = accept.getOutputStream();
os.write("ending".getBytes());    //傳送給客戶端

客戶端可增加:

socket.shutdownOutput();//通知伺服器傳輸完畢
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while((len2 = is.read(buffer))!=-1){
    baos.write(buffer,0,len2);
}
System.out.println(baos);

客戶端讀入了服務端返回的ending字串。

實現兩端通訊之後再決定是否進行其他操作,例如close()等等。

3. UDP

無需連線,但是需要知道對方地址

3.1 UDP訊息傳送

主要依賴的是DatagramSocketDatagramPacket

  • DatagramSocket此類表示用於傳送和接收資料包資料包的套接字。 資料包套接字是分組傳送服務的傳送或接收點。 在資料包套接字上傳送或接收的每個資料包都被單獨定址和路由。 從一個機器傳送到另一個機器的多個分組可以不同地路由,並且可以以任何順序到達。 在可能的情況下,新構建的DatagramSocket啟用了SO_BROADCAST套接字選項,以允許廣播資料包的傳輸。 為了接收廣播資料包,DatagramSocket應該繫結到萬用字元地址。 在一些實現中,當DatagramSocket繫結到更具體的地址時,也可以接收廣播分組。
  • DatagramPacket該類表示資料包包。 資料包包用於實現無連線分組傳送服務。 僅基於該資料包中包含的資訊,每個訊息從一臺機器路由到另一臺機器。 從一臺機器傳送到另一臺機器的多個分組可能會有不同的路由,並且可能以任何順序到達。 包傳送不能保證。

以下實現UDP訊息傳送的一個簡單例子:

public class TestUDP1 {
    //不需要連線伺服器
    public static void main(String[] args) throws IOException {
        //建立Socket
        DatagramSocket datagramSocket = new DatagramSocket(); //為空將預設繫結一個可用埠

        //建個資料包
        String message = "hello,server";
        InetAddress inetAddress = InetAddress.getByName("localhost");
        int port = 9000;
        int len = message.getBytes().length;
        //資料,資料的長度起始位置,要傳送給誰
        DatagramPacket datagramPacket = new DatagramPacket(message.getBytes(), 0,len, inetAddress, port);

        datagramSocket.send(datagramPacket);   //進行傳送
        datagramSocket.close();
    }
}

UDP傳送完就不需要其他操作了,為了驗證我們傳送的訊息,建立了一個接收端:

public class UDPaccept {
    public static void main(String[] args) throws IOException {
        //開放埠
        DatagramSocket socket = new DatagramSocket(9000);
        //接收資料包
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); //接收並不需要對方地址和埠
        socket.receive(packet);   //阻塞式接收
        System.out.println(packet.getAddress());
        System.out.println(new String(packet.getData(),0,packet.getData().length));
        socket.close();
    }
}

其中收發訊息用到了兩個方法DatagramSocket.send()DatagramSocket.receive()

3.2 UDP聊天的實現(單向)

通過UDP協議進行一個傳送端和接收端的demo。當出現"bye"時,結束對話。

傳送端:

public class UDPsender {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(8080);//傳送埠
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        //BufferedInputStream bis = new BufferedInputStream(System.in);
        int len;
        while(true){
            String data = reader.readLine();      //接收鍵盤輸入的資訊
            DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress("localhost",6000));
            socket.send(packet);
            if(data.equals("bye"))
                break;
        }
        reader.close();
        socket.close();
    }
}

接收端:

public class UDPreceiver {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(6000);//開啟接收埠

        while(true){
            byte[] container = new byte[1024]; //用來裝資料包內容
            DatagramPacket packet = new DatagramPacket(container,0,container.length);   //接受時候只需要一個空byte[]
            socket.receive(packet);
            byte[]data = packet.getData();
            String datas = new String(data,0,data.length);  //仍帶有byte[]的其他資訊,若轉為真正字串需trim()
            System.out.println(datas);
            if(datas.trim().equals("bye"))
                break;
        }
        socket.close();
    }
}

這個demo只實現了單向發訊息。後續將實現雙向傳送。

3.3 雙向聊天(多執行緒)

雙向聊天和以上內容相似,只需要每個端開啟兩個執行緒(接收執行緒和傳送執行緒),以下為程式碼演示:

public class TalkSend implements Runnable{    //傳送執行緒
    DatagramSocket socket = null;
    BufferedReader reader = null;

    private String ToIP;
    private int ToPort;

    public TalkSend(String toString, int toPort) {
        ToIP = toString;
        ToPort = toPort;
    }

    @Override
    public void run() {
        try {
            socket = new DatagramSocket();//傳送埠
            reader = new BufferedReader(new InputStreamReader(System.in));
        } catch (SocketException e) {
            e.printStackTrace();
        }
        //BufferedInputStream bis = new BufferedInputStream(System.in);
        while(true){
            String data = null;
            try {
                data = reader.readLine();
                DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress(this.ToIP,this.ToPort));
                socket.send(packet);
                if(data.equals("bye"))
                    break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        socket.close();
    }
}
public class TalkReceive implements Runnable{   //接收執行緒
    DatagramSocket socket = null;

    private int FromPort;
    private String Person;

    public TalkReceive(int fromPort,String person) {
        FromPort = fromPort;
        Person = person;
    }

    @Override
    public void run() {
        try {
            socket = new DatagramSocket(this.FromPort);//開啟接收埠
        } catch (SocketException e) {
            e.printStackTrace();
        }

        while(true){
            byte[] container = new byte[1024]; //用來裝資料包內容
            DatagramPacket packet = new DatagramPacket(container,0,container.length);
            try {
                socket.receive(packet);
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[]data = packet.getData();
            String datas = new String(data,0,data.length);
            System.out.println(Person+":"+datas);
            if(datas.trim().equals("bye"))
                break;
        }
        socket.close();
    }
}

然後需要設定兩個端進行聊天:

new Thread(new TalkSend("localhost",8080)).start();
new Thread(new TalkReceive(6000,"老師")).start();

這裡設定為學生端,然後開啟兩個執行緒,設定傳送埠和接收埠

new Thread(new TalkSend("localhost",6000)).start();
new Thread(new TalkReceive(8080,"學生")).start();

這裡設定為老師端,然後開啟執行緒,設定埠,其實開啟了4個埠,因為學生端和老師端傳送執行緒中,DatagramSocket預設繫結的還有兩個埠。

Java網路程式設計快速上手(SE基礎)

Java網路程式設計快速上手(SE基礎)

4. URL

https://www.baidu.com/

統一資源定位符:定位網際網路上的某個資源

DNS域名解析: www.baidu.com ——》xxx.xxx.xxx.xxx

協議://ip地址:埠/專案名/資源

URL類的基本使用

URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=yuan&password=123");
System.out.println(url.getProtocol());//得到協議名
System.out.println(url.getHost());//主機
System.out.println(url.getPort());//埠
System.out.println(url.getPath());//檔案
System.out.println(url.getFile());//檔案全路徑
System.out.println(url.getQuery());   //得到url查詢的部分(引數)

Java萬物皆物件

下載一個URL資源

public class UrlDown {
    public static void main(String[] args) throws IOException {
        URL url = new URL("http://localhost:8080/gaoyuan/SecurityFile.txt");
        //連線到這個資源
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); //需要轉換型別,因為返回的是URPConnection
        InputStream is = urlConnection.getInputStream();
        FileOutputStream fos = new FileOutputStream("SecurityFile.txt");
        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        fos.close();
        is.close();
        urlConnection.disconnect();//斷開連線
    }
}

這是開啟Tomcat伺服器後,把一個檔案新增到相應目錄之後下載的。

網路程式設計基礎部分結束,例如:計算機網路,BIO等知識將會在後續部落格中釋出。