菜鳥學JAVA之——網絡編程(TCP、UDP)

網絡

網卡:計算機接入到網絡的最關鍵的設備,是一個IO設備。(輸入,將從互聯網外部接受到的數據發給cpu。輸出,cpu將數據發送給網卡,網卡發送到互聯網目標地址。)

路由器撥號到公網,連接子網和公網,IP協議,分配IP地址

IP地址:當前網絡的唯一標識

通過公網來進行不同局域網的主機進行通訊,這時可以把公網看成服務器,每個主機看出客戶端

傳輸方式:

協議:TCP(傳輸控制協議)、UDP(用戶報文協議)

TCP協議

  • 客戶端和服務器之間建立一個通道,就比如修建了一個大粗管道(這個通道就是TCP),而這個大管道里面有兩個小管道,這兩個管道負責傳輸數據。

  • 服務器一定與很多個客戶端連接,所以就會建立很多管道,那麼這些管道怎麼被區分呢?不同管道傳輸的數據爲什麼不會衝突?

    端口號就是一臺電腦在建立網絡連接時來區分網絡連接裏傳輸的數據的編號

    這裏就引入了端口,建立大通道之前首先要有一對相通的IP地址,爲了保證數據不會混亂(傳給微信的就是傳給微信的,傳給虎牙的就是傳給虎牙的),就需要在建立管道的出入口處給定一個編號(端口號)

  • 在建立網絡連接時如何來區分這個連接是唯一的?通過網絡四元組,(本機IP、本機端口號、對端IP、對端端口號)

  • 每個主機上都有65535個端口

外網(服務器)不能主動向內網(客戶端)發請求,但內網可以主動向外網發請求

tcp狀態遷移圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8HheXqOq-1582506817626)(C:\Users\張澳琪\AppData\Roaming\Typora\typora-user-images\image-20200216093328280.png)]
在這裏插入圖片描述

三次握手:(建立連接的過程)

1.首先服務器端,必須先開啓某一個端口的監聽(端口不打開,誰都連不上)

2.由客戶端向服務器的監聽端口發起一個SYN請求,其中SYN等於一個數字,SYN = x,發送的時候,客戶端需要選擇一個自己的端口發送該SYN請求,一般來說,客戶端選擇的端口都是隨機的,但是當選擇完這個端口以後,就一直使用這個端口傳輸數據,在該連接沒有斷開之前,不會換端口。當發送完SYN請求以後,客戶端的該端口狀態變成了SYN_SENT狀態(同步信息發送狀態)。(右邊紅線)

3.當服務器端的監聽端口,收到客戶端發送的SYN=x這個數據的時候,自己的端口就變成了SYN_RECV,同時向發送SYN的客戶端的那個端口回覆一個數據,這個數據包含兩條信息,一個是SYN=y,另一個是ACK=x+1(回覆這個ACK的原因是:每個服務器會收到很多的客戶端發來的請求,所以服務器一定要告訴客戶端我接收到你發來的這個請求了,所以回覆一個x+1,加以確認)(左綠線)

4.當客戶端收到服務器發過來的SYN + ACK 消息的時候,我的客戶端就會將端口立刻變換成ESTABLISH狀態,同時,客戶端會發送一條數據給服務器,這個數據是ACK = y+1。

5.當服務器收到客戶端發來的ACK = y+1 的時候,服務器中的SYN_RECV狀態就變成了ESTABLISH狀態。

只有在端口在Establish狀態才能傳輸數據,否則就表示連接沒有建立好

四次揮手有

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TCqLO2pA-1582506817634)(C:\Users\張澳琪\AppData\Roaming\Typora\typora-user-images\image-20200216095250781.png)]

爲什麼ACK與FIN不一起發送呢?

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AjoublbE-1582506817634)(C:\Users\張澳琪\AppData\Roaming\Typora\typora-user-images\image-20200215221417050.png)]

首先客戶端向服務器發送FIN代表我要與你斷開了(Client不再向Service發送數據了,①要斷了,但並不代表Service不再給Client發送數據了),然後服務器等待把①號流中剩下的流接受完,再向客戶端發送ACK,我知道了,我也不再接受你發的消息了,可以斷開了(這時Client指向Service的那個流①就可以斷開了)。然後服務器知道了要斷開了,並且自己要發送的數據也發送完畢了,再向客戶端發送一個FIN,意味着服務器不會再發送數據了,客戶端收完了FIN後,再返回一個ACK代表我收完了,可以斷開了,這時Ⅱ號流就可以斷開了。

ACK代表收完了,並不代表發完了,FIN才代表收完了

什麼時候才發ACK呢?比如客戶端先給服務器發送一條數據,現在發送了八條了,服務器不會向客戶端發送ACK,只有收到十條了,才發送ACK

如果FIN和ACK一起發送,那麼客戶端向服務器發送完FIN後,①號流還不能斷開,要等到服務器發送給客戶端發送的數據發送完了,①號流才能斷開,這樣就浪費了資源。

注意:FIN和ACK分開發,至於誰先發過去無所謂,可以是ACK先到,斷開①,也可以是FIN先到,代表你把給我發數據了,剛好我也不想給你發了

爲什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?

答:雖然按道理,四個報文都發送完畢,我們可以直接進入CLOSE狀態了,但是我們必須假象網絡是不可靠的,有可以最後一個ACK丟失。所以TIME_WAIT狀態就是用來重發可能丟失的ACK報文。在Client發送出最後的ACK回覆,但該ACK可能丟失。Server如果沒有收到ACK,將不斷重複發送FIN片段。所以Client不能立即關閉,它必須確認Server接收到了該ACK。Client會在發送出ACK之後進入到TIME_WAIT狀態。Client會設置一個計時器,等待2MSL的時間。如果在該時間內再次收到FIN,那麼Client會重發ACK並再次等待2MSL。所謂的2MSL是兩倍的MSL(Maximum Segment Lifetime)。MSL指一個片段在網絡中最大的存活時間,2MSL就是一個發送和一個回覆所需的最大時間。如果直到2MSL,Client都沒有再次收到FIN,那麼Client推斷ACK已經被成功接收,則結束TCP連接。


TCP協議屬於傳輸層協議,而我們寫代碼一般是直接使用封裝好的tcp工具或者應用層協議來驅動傳輸層的工作。如果我們想在JAVA語言中,驅動TCP協議來傳輸數據,那麼我們可以使用JAVA封裝好的工具叫做Socket(套接字)編程實現。Java給出的TCP套接字就是Socket,而需要分清楚服務器和客戶之間使用的不同,服務器端使用的是ServerSocket,客戶端使用的是Socket。

Socket只是一個操作網絡傳輸數據的工具

約定:1024以內的端口都被佔中着,給應用層協議分配好了(比如80端口號是預留給了應用層的HTTP協議)

實例:

public class TCPServer {
    public static void main(String[] args) {
        ServerSocket ss = null;
        try {
            ss = new ServerSocket(18888);//監聽端口號,一般使用10000~60000之間的
            							//只要程序不斷就一直監聽這個指定的端口號
            Socket s = ss.accept();//接受客戶端發來的請求。阻塞式的,客戶端不發送他就一直等待着
            						//s是客戶端這頭,ss是服務器這頭
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
             try {
                ss.close();//TCP連接斷開,關閉流,發送FIN
            } catch (IOException e) {
                e.printStackTrace();
            }
        } 
    }
}
public class TCPClient {
    public static void main(String[] args) {
        Socket s = null;
        try {
            s = new Socket("127.0.0.1",18888);//給出要連接的服務器的IP地址和他的端口號
            								//客戶端的端口號一定是隨機的,但是要連接的服務器的端口號一定要知道(只有對端的端口號是開放的才能連接他)
           
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
             try {
                s.close();//TCP連接斷開,關閉流,發送FIN
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

ServerSock不負責傳輸數據,只負責創建一個可以傳輸數據的工具的工具

ServerSocket的setSoTimeout(int SoTime)方法:設置超時時間,連接過程中如果超過指定時間還沒連接上將會拋出異常。

設置setSoTimeout,解決DDOS攻擊(只發SYN不發ACK,佔用了全部的端口,讓服務器崩潰)

Socket的方法:

getKeepAlive():
在這裏插入圖片描述
isConnected():返回套接字的連接狀態

socket還提供兩個方法:getInputStream() 和 getOutputStream()

getInputStream() :獲得輸入流

getOutputStream():獲得輸出流

結合TCP連接和IO流,完成客戶端與服務器的交互(一問一答式)

服務器

/**
 * @author ZAQ
 * @create 2020-02-11 11:22
 */
public class TCPServer {
    public static void main(String[] args) {
        ServerSocket ss = null;
        try {
            //服務器的ServerSocket,只做接入的監聽
            ss = new ServerSocket(18888);
            //接受到一個客戶端請求,並生成新的套接字
            Socket s = ss.accept();
            //獲得輸出流並將輸出流包裝成字符流
            OutputStream os = s.getOutputStream();
            PrintWriter pw = new PrintWriter(os);//字符流,可以一次輸一行,封裝字節流
            //獲得輸入流並將輸入流包裝成字符流
            InputStream is = s.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            //使用輸出字符流,輸出一句話爲客戶端
            pw.println("歡迎來到我的服務器!");
            pw.flush();//字符流輸出都要衝刷緩衝區
            //創建掃描控制檯輸入的工具
            Scanner scanner = new Scanner(System.in);//Scanner就是對字符流的一個封裝,Scanner的作用就是從流裏掃描內容
            //獲得客戶端的反饋
            String sss = null;
            while ((sss = br.readLine()) != null) {
                System.out.println(sss);
                //接受到內容後再次向客戶端發送內容
                pw.println(scanner.nextLine());
                pw.flush();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客戶端

/**
 * @author ZAQ
 * @create 2020-02-11 11:59
 */
public class TCPClient {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1",18888);

            InputStream is = socket.getInputStream();
//            BufferedReader br = new BufferedReader(new InputStreamReader(is));//法一
//            System.out.println(br.readLine());
            Scanner scanner = new Scanner(is);  //法二  兩種方法一樣
           // System.out.println(scanner.nextLine());

            Scanner scanner1 = new Scanner(System.in);
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);
            String str = null;
            while ((str = scanner.nextLine()) != null) {
                System.out.println(str);
                pw.println(scanner1.nextLine());
                pw.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

TCP的特點:

tcp傳輸數據是需要建立一個有效連接的,也就是說,我們使用Tcp傳輸數據的第一件事是建立一個數據通道的。

TCP的優點:

  • Tcp對於有線連接是有實時感知的,也就是說,Tcp連接中斷時,另一方是有能力立刻知道的
  • Tcp傳輸的數據是安全的、有效的、有序的(這是因爲TCP連接要建立管道鏈接)

TCP的缺點:

  • Tcp佔用資源更多
  • Tcp的有序性,一定情況下會拖慢應用層程序的運行

目前市面上見多的很多IM軟件(即時通信軟件)很多哦都不去採用TCP,而是採用UDP來實現。

UDP協議

  1. UDP叫做用戶報文協議
  2. UDP是面向數據包的,或者說面向無連接的(不涉及到狀態轉換)
  3. UDP傳輸數據是不可靠的,但傳輸速度很快
  4. UDP傳播數據的過程中,沒有流的概念,只有報文概念
  • 對於UDP來說,發送數據使用的是DatagramSocket,接受數據也是使用的DatagramSocket,

所以DatagramSocket沒有所謂的明顯的服務和客戶端的區別(注:Socket有,服務端是ServerSocket,客戶端是Socket)

  • DatagramSocket是收發數據的工具,但是並不關心數據發送去哪裏,或者從哪裏來,他只做一件事情,就是要麼將數據發到網絡當中,讓數據自己去自己該去的地方(你去哪我不關心),要麼就是從網絡裏面打撈出來自己要的數據。
  • 真正的內容容器是,DatagramPacket,這個報文中在發送端,我們要設置其發送的內容,發送的地址,發送的端口號,等。設置完給DatagramSocker就可以了
  • 在接收端,我們要通過DatagramSocket來接收數據,接受的數據在byte[]裏面,拿出來就用
  • UDP傳輸數據的過程中,沒有流的概念,只有報文的概念。

UDP建立鏈接發送數據

DatagramPacket的構造器的兩個參數

  • byte[] buf:接受數據的緩衝區,接受來的數據放這裏
  • int length:接受數據長度
  • length的長度必須小於等於buf數組的長度

服務器

public class UDPServer {
    public static void main(String[] args) {
    	try {
            DatagramSocket socket = new DatagramSocket(9999);
            DatagramPacket dp = new DatagramPacket(new byte[128],128);//接受數據的緩衝區
            socket.receive(dp);//接收數據到了byte數組中了
            byte[] b = dp.getData();//獲取這些數據
            System.out.println(new String(b));//打印數據
        } catch (SocketException e) {
            e.printStackTrace();
        }
	}
}

客戶端

public class UDPClient {
    public static void main(String[] args) {
    	try {
            DatagramPacket dp = new DatagramPacket("你好UDP".getByte(),"你好UDP".getByte().length());
            packet.setAddress(InetAddress.getByName("127.0.0.1"));//設置要發送到的IP地址
            packet.setPort(9999);//設置要發送到的端口號
            DatagramSocket socket = new DatagramSocket();//這裏可以設置發送端的端口號
            socket.send(packet);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        }
	}
}

有一個問題:接收端DatagramPacket裏設置的數組的長度必須隨發送端設置的數組長度變化,但是接收端又不知道這個長度是多少,爲了解決這一問題,那就約定讓發送端每次發送定長數組的數據(比如一個包就發128個字節)

UDP建立連接並且傳輸數據代碼:

服務器

/**
 * @author ZAQ
 * @create 2020-02-12 20:37
 */
public class UDPServe {
    public static void main(String[] args) {
        try {
            DatagramSocket socket = new DatagramSocket(9999);
            DatagramPacket dp = new DatagramPacket(new byte[128],128);//接收端的數組長度要隨發送端的變化
            receive(socket,dp);
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void receive(DatagramSocket socket, DatagramPacket dp) throws IOException {
        //合併接受到的包
        byte[] result = new byte[0];
        int num = 0;
        while (true) {//收數據的時候不知道具體要收幾個包
                        //這裏可以判斷收來的數據大小不否小於128,如果比128小了,則就是最後一個包了
            socket.receive(dp);
            result = Arrays.copyOf(result,result.length + dp.getLength());
            System.arraycopy(dp.getData(),0,result,num*128,dp.getLength());
            num++;
            if(dp.getLength() < 128) {
                break;
            }
        }
        System.out.println(new String(result));
    }
}

客戶端

/**
 * @author ZAQ
 * @create 2020-02-12 11:48
 */
public class UDPClient {
    public static void main(String[] args) {

        DatagramPacket packet = null;
        DatagramSocket socket = null;
        try {
            packet = new DatagramPacket(new byte[128],128);
            socket = new DatagramSocket();
            packet.setAddress(InetAddress.getByName("127.0.0.1"));//ip地址
            packet.setPort(9999);//對應ip地址的端口號
            Scanner scanner = new Scanner(System.in);
            String str = scanner.nextLine();
            send(str,socket,packet);

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void send(String source, DatagramSocket socket, DatagramPacket packet) throws IOException {
        //用於存輸入的數據
        byte[] arrays = source.getBytes();//arrays裏就是要發送的數據
        int num = arrays.length / 128; //計算要發送多少次包
        byte[] datas = null;
        //人工拆包
        for(int i = 0; i <= num; i++) {
            //最後一包,這時數據長度不一定是128
            if(i == num) {
                datas = Arrays.copyOfRange(arrays,i*128,arrays.length);
                packet.setData(datas);
                packet.setLength(datas.length);//修改接受長度
            } else {
                //沒到頭的數據包
                datas = Arrays.copyOfRange(arrays,i*128,(i+1)*128);
                packet.setData(datas);
                //長度就是128,不用改
            }
            socket.send(packet);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章