1. 程式人生 > >7.4 (java學習筆記)網路程式設計之TCP

7.4 (java學習筆記)網路程式設計之TCP

一、TCP

  1.1 TCP(Transmission Control Protocol 傳輸控制協議),是一種面向連線的,安全的傳輸協議,但效率相比於UDP而言比較低。

  TCP傳輸時需要確保先建立連線之後,再進行傳輸這樣就保證了傳輸的可靠性。

  java中將TCP封裝成了對應的類。

    ServerSocket:服務端

    Socket:客戶端

 

  1.2TCP連線的建立與取消(三次握手與四次揮手)

    連線(三次握手):

      1.初始狀態,伺服器處於監聽狀態,主機的傳輸控制模組(TCB)像伺服器傳送連線請求,客戶端進入同步已傳送狀態。

      2.伺服器受到客服端傳送的連線請求,如果同同意連線則向客戶端傳送確認,伺服器進入同步收到狀態。

      3.客戶端受到確認後,繼續給伺服器傳送確認報文,客戶端進入已連線狀態。

      後續伺服器收到客服端的確認後也進入已建立連線狀態。

 

     建立連線後,客戶端和伺服器就可以愉快的傳送資訊了,資訊傳送完畢後,就要斷開連線。

     斷開(四次揮手):

      1.客戶端傳送釋放報文,同時停止傳送資料主動關閉TCP連線,進入終止等待狀態1。

      2.伺服器收到釋放報文後傳送確認,此時伺服器進入關閉等待狀態。此時客戶端到伺服器方向的連線就釋放了。

      此時TCP進入半連線狀態,伺服器到客戶端的連線未釋放,此時伺服器還可以將未傳送完的資料向客戶端傳送。

      3.伺服器沒有資料向客戶端傳送之後,就會發出連線釋放報文等待客戶端確認,伺服器進入最終確認狀態。

      4.客戶端收到伺服器傳送的釋放報文後,向伺服器傳送一個確認報文,伺服器進入連線關閉狀態。客戶端同時進入時間等待(TIME-WAIT)狀態。

        此時連線還沒有被釋放掉。客戶端會等待2MSL的時間,然後進入連線關閉狀態。至此連線斷開完成。

       

      每一條TCP的連線唯一的被兩個通訊兩端的兩個端點表示,也就是是四元組(源IP,源埠,目的IP,目的埠),

      而不是單純的用一個IP地址和埠區別。

      也就意味著一個TCP可以建立多個連線,比如伺服器IP是127.0.0.1,埠是8888;

      例如客戶端一:127.0.0.1:3389

        客戶端二:127.0.0.1:3390

        客戶端三:127.0.0.1:3390

      三個連線對應的四元組

      TCP 127.0.0.1:3889 127.0.0.1:8888 ESTABLISHED

      TCP 127.0.0.1:3890 127.0.0.1:8888 ESTABLISHED

      TCP 127.0.0.1:3891 127.0.0.1:8888 ESTABLISHED

      我們可以發現即使目的地IP和埠相同,但本地的埠不同導致整個四元組不同。

      伺服器可以建立多個連線,前提是四元組不同。連線中無法出現兩個四元組相同的連線。

       TCP可以連線多個客戶端,為其每一個客戶端建立一個Socket,Socket不同代表不同連線。

      客戶端伺服器之間通過Socket通訊,伺服器加上多執行緒為每一個Socket分配一個執行緒就可實現併發處理。

      

      參考:1、計算機網路(第四版) 謝希仁編著。

         2、https://www.cnblogs.com/Andya/p/7272462.html

           3、https://blog.csdn.net/sssnmnmjmf/article/details/68486261

 

二、ServerSocket

    ServerSocket(int port)//建立繫結到指定埠的伺服器套位元組。

    預設繫結的IP地址是本地的IP地址。

    例如我這裡是在個人電腦上面執行,繫結的地址就是當前主機的IP地址。

    當前IP地址可按win鍵+r輸入cmd,然後輸入ipconfig -all檢視乙太網介面卡的IPv4地址,後面帶有首選的。

    

    也可以認為是繫結到127.0.0.1上,因為當前C/S都是在一臺電腦上執行都屬於本機訪問,

    所以本地測試使用的迴環地址(127.0.0.1和本機IP(192.168.190.1)都可以。

    

     2.主要方法

    Socket accept()//監聽要對當前物件IP上指定埠的連線,如果發現有連線請求則連線它。

    例如客戶端傳送一個連線請求到當前服務端的對應埠,則建立客戶端與服務端的連線。

    這個監聽是一個阻塞式的監聽,意思就是說如果沒有建立連線的話當前程序就不會繼續向下執行。

    成功建立連線後,會返回一個Socket物件,而Socket物件中有獲取輸入輸出流的方法,這時就在

    客戶端,服務端之間建立輸入輸出流管道,兩者就可以通過這個管道通訊。

 

三、Socket

     1.構造方法:

    Socket(InetAddress address, int port)

    Socket(String host, int port)
    //建立套位元組,並將其繫結到指定的(IP|域名)上的指定埠。

 

    2.主要方法:

    InputStream getInputStream()//返回當前Socket物件的輸入流

    OutputStream getOutputStream()//返回當前Socket物件的輸出流

 

四、例子

  Server:

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class Server {
    public static void main(String[] args) throws UnknownHostException, IOException {
        String msg = "歡迎連線到Server!";
        ServerSocket server = new ServerSocket(8880);//繫結到本地IP的8880埠
        Socket socket = server.accept();//阻塞式接收,接收成功建立連線管道
        //連線管道的輸出流,即對連線物件(客戶端)進行輸出。
        BufferedWriter bos = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8"));
        bos.write(msg);//伺服器將指定內容發給客戶端
        bos.newLine();
        bos.flush();
    }
}

 

Client:

import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    public static void main(String[] args) throws UnknownHostException, IOException {
        Socket client = new Socket("192.168.190.1",8880);//向指定IP地址的指定埠進行連線
    //    Socket client = new Socket("127.0.0.1",8880);//使用127.0.0.1和使用192.168.190.1都可以完成通訊
        //連線成功後,獲取連線管道的輸入流,即對伺服器寫入內容進行讀取
        BufferedReader isr = new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
        String re = isr.readLine();//讀取內容
        System.out.println(re);
    }
}
執行結果:
歡迎連線到Server!

先執行Server會進行阻塞式接收,沒有建立連線前後面的語句都不會執行。

然後執行Client建立連線後,Server向連線管道中寫入資料,Client向連線管道中讀取資料。

最後將內容顯示到控制檯。 

 

 

五、簡易聊天室

下面結合多執行緒,和網路程式設計實現一個簡易聊天室。

客戶端先將訊息傳送到伺服器,服務接收訊息後轉發給其他客戶端。

每個客戶端是一個執行緒。

 

基本流程:

1.A客戶端讀取鍵盤輸入資料,並將其傳送到伺服器。

2.伺服器與A客戶端建立連線後,將A客戶端放入一個容器,同時將A客戶端傳送的訊息,轉發給容器中除A客戶端之外的所有客戶端。

 伺服器中為每一個Socket分配一個執行緒,就可以實現併發轉發所有聊天訊息。

3.傳送給其他客戶端後,其他客戶端會讀取伺服器傳送的內容並顯示到自己控制檯。

 

Send:讀取鍵盤輸入內容並將其傳送給伺服器

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Socket;

public class Send implements Runnable {
    private boolean Running = true;
    private DataInputStream dis;//用於讀取service返回的訊息
    private DataOutputStream dos;//用於向server傳送訊息
    private BufferedReader br;
    public Send(){
        
    }
    
    public Send(Socket client){
        try {
            dis = new DataInputStream(client.getInputStream());
            dos = new DataOutputStream(client.getOutputStream());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("初始化連線失敗!");
            Running = false;
            try {
                dis.close();
                dos.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("關閉異常!");
            }
            
        }
        
    }
    //讀取鍵盤輸入資訊並返回
    private String reciver(){
        String msg=null;
        br = new BufferedReader(new InputStreamReader(System.in));
        try {
            msg = br.readLine();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.out.println("讀取使用者輸入異常!");
            Running = false;
            try {
                br.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
        return msg;
    }
    //將鍵盤輸入資訊傳送至伺服器
    private void send(){
        String msg = reciver();
        try {
            if(msg != null && !msg.equals("")){
                dos.writeUTF(msg);
                dos.flush();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.err.println("使用者傳送資訊異常!");
            Running = false;
            try {
                dos.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
    }
    
    public void run(){
        while(Running){
            send();
        }
    }
}

 

reciver:讀取伺服器傳送的資料

package ChatRoom;

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

public class Reciver implements Runnable{
    private boolean Running = true;
    private DataInputStream dis;
    
    public Reciver(){
        
    }
    //初始化,獲取連線
    public Reciver(Socket client){
        try {
            dis = new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("Client-->Server連線失敗!");
            Running = false;
            try {
                dis.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("關閉異常!");
            }
        }
    }
    //讀取客戶端傳送的資料
    private String reciver(){
        String msg=null;
        try {
            msg = dis.readUTF();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.err.println("接受客戶端訊息異常!");
            Running = false;
            try {
                dis.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                System.err.println("關閉異常!");
            }
        }
        return msg;
    }
    
    public void run(){
        while(Running){
            System.out.println(reciver());
        }
    }
}

 

Client:(192.168.1.1~253) 255.255.255.0

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

public class Client {

    public static void main(String[] args) throws UnknownHostException, IOException {
        // TODO Auto-generated method stub
        Socket client = new Socket("192.168.1.254",8888);//連線伺服器
            new Thread(new Send(client)).start();//讀取鍵盤資料併發送給伺服器
            new Thread(new Reciver(client)).start();//讀取伺服器傳送回來的訊息
    }

}

 

Server: 192.168.1.254   255.255.255.0

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;

public class Server {
    static List<Server.Connect> allUser;
    public static void main(String[] args) throws IOException{
        allUser = new LinkedList<Server.Connect>();//儲存客戶端的容器
        ServerSocket serverSocket = new ServerSocket(8888);//設定監聽埠
        while(true){//不斷接受客戶端的連線請求
            Socket con = serverSocket.accept();//獲取伺服器與客戶端的Socket
       //   System.out.println(con.getPort());
            Server server = new Server();//例項化一個伺服器
            Server.Connect connect = server.new Connect(con);//建立一個客戶端到伺服器的連線(socket)
            allUser.add(connect);//將已經連線的客戶端放入容器,也可以看做將socket放入伺服器
            new Thread(connect).start();//每連線一個客戶端(socket)就為其開闢一條執行緒,一個伺服器對應多個客戶端。
        }
    }
    
    class Connect implements Runnable{//
        private boolean Running = true;//執行標誌位
        DataInputStream dis;
        DataOutputStream dos;
        
        public Connect(){
            
        }
        
        public Connect(Socket client){//客戶端連線上伺服器後的socket
            try {//初始化獲取對客戶的讀寫流
                dis = new DataInputStream(client.getInputStream());
                dos = new DataOutputStream(client.getOutputStream());
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.err.println("Server-->Client連線失敗!");
                try {
                    dos.close();
                    dis.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("關閉異常");
                }
            }
        }
        
        public String reciver(){//讀取客戶端傳送的訊息
            String msg = null; 
            try {
                msg = dis.readUTF();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                System.err.println("獲取客戶端資訊異常!");
                Running = false;
                try {
                    dis.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("關閉異常");
                }
            }
            return msg;//返回讀取的訊息
        }
        
        public void send(String msg){//將訊息傳送到輸出流dos對應的客戶端
            try {
                dos.writeUTF(msg);
                dos.flush();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                System.err.println("傳送客戶端資訊異常!");
                Running = false;
                try {
                    dos.close();
                } catch (IOException e1) {
                    // TODO Auto-generated catch block
                    System.err.println("關閉異常");
                }
            }
            
        }
        
        public void sendOther(){//將訊息傳送到其它客戶端,例如A客戶端傳送過來的訊息,就傳送給除A之外的客戶端
            String msg = this.reciver();
            System.out.println(msg);
            for(Connect temp : allUser){//遍歷存放客戶端的容器
                if(temp == this)//如果容器中當前物件時是A,就跳過這次迴圈,不是則將訊息傳送到對應的客戶端。
                    continue;
                temp.send(msg);//哪一個客戶端呼叫就將訊息發給誰,假如這裡的temp是B就將呼叫B中的send,此時傳送的輸出流是向客戶端B寫入的。
            }
        }

        @Override
        public void run() {//開啟多執行緒後伺服器不斷接收客戶端訊息,然後轉發
            // TODO Auto-generated method stub
            while(Running){//執行標誌位,如果中途出現讀寫異常則終止。
                sendOther();
            }
        }    
    }
}

如果是一臺電腦上測試,則將客戶端中連線伺服器的地址修改為127.0.0.1或localhost埠任選(大於1024即可)。

如果是多臺電腦測試,例如兩臺電腦(將兩臺電腦的網線介面用一根網線連線)。

將其中一臺電腦的IP地址修改為192.169.1.245:255.255.255.0,

另外一臺IP地址只需和其保持同一網段即可例如(192.168.1.1:255.255.255.0)。(最好禁用其餘網絡卡)

在192.168.1.254上先執行伺服器然後執行客戶端,在另外一個電腦上執行客戶端。

通過控制檯輸入可以實現聊天。

如果想實現同一網段多個電腦間通訊需要用到交換機連線多個電腦。