Socket - TCP快速入門

Socket - TCP 快速入門

TCP 是什麼

  • 英語:Transmission Control Protocol,縮寫爲 TCP。
  • TCP 是傳輸控制協議;是一種面向鏈接的、可靠的基於字節流傳輸層通訊協議,由 IETF 的 RFC 793 定義。
  • 與 UDP 同樣,完成第四層傳輸層所指定的功能與職責。
  • 和 UDP 最大的區別是須要鏈接的,三次握手四次揮手,校驗機制保證了數據傳輸的穩定性和可靠性。

TCP 的機制

  • 三次握手、四次揮手。
  • 具備校驗機制、可靠、穩定的數據傳輸。

TCP鏈接、傳輸過程

TCP 能作什麼

  1. 聊天消息傳輸、推送。
  2. 單人語音視頻聊天。
  3. 幾乎 UDP 能作的都能作,但要考慮複雜性、性能問題。
  4. TCP 沒法進行廣播多播的操做。
  5. 沒法搜索,搜索只能 UDP 來作。

TCP 核心 API 講解

  • socket() :建立一個客戶端 Socket。
  • bind() : 綁定一個 Socket 到一個本地地址和端口上。
  • accept():服務器端接受一個新的鏈接。
  • write() :把數據寫入到 Socket 輸出流。
  • read():從 Socket 輸入流中讀取數據。

客戶端建立流程

客戶端建立流程

服務端建立流程

服務端建立流程

三次握手、四次揮手

三次握手java

  • 第一次握手:客戶端發送帶有 SYN 標誌的鏈接請求報文段,而後進入 SYN_SEND 狀態,等待服務端確認。
  • 第二次握手:服務端接受到客戶端的 SYN 報文段後,須要發送 ACK 信息對這個 SYN 報文段進行確認。同時,還要發送本身的 SYN 請求信息。服務端會將上述信息放到一個報文段(SYN+ACK 報文段)中,一併發送給客戶端,此時服務端進入 SYN_RECV 狀態。
  • 第三次握手:客戶端接收到服務端的 SYN+ACK 報文段後,會向服務端發送 ACK 確認報文段,這個報文段發送完畢後,客戶端和服務端都進入 ESTABLEISHED 狀態,完成 TCP 三次握手。

TCP 三次握手

四次揮手算法

當被動方收到主動方的 FIN 報文通知時,它僅僅表示主動方沒有數據再發送給被動方了。但未必被動方全部的數據都完整的發送給了主動方,因此被動方不會立刻關閉 SOCKET,它可能還須要發送一些數據給主動方後,再發送 FIN 報文給主動方,告訴主動方贊成關閉鏈接,因此這裏的 ACK 報文和 FIN 報文多數狀況下都是分開發送的。服務器

原理:網絡

  1. 第一次揮手:Client 發送一個 FIN,用來關閉 Client 到 Server 的數據傳送,Client 進入 FIN_WAIT_1 狀態。
  2. 第二次揮手:Server 收到 FIN 後,發送一個 ACK 給 Client,確認序號爲收到序號+1(與 SYN 相同,一個 FIN 佔用一個序號),Server 進入 CLOSE_WAIT 狀態。
  3. 第三次揮手:Server 發送一個 FIN,用來關閉 Server 到 Client 的數據傳送,Server 進入 LAST_ACK 狀態。
  4. 第四次揮手:Client 收到 FIN 後,Client 進入 TIME_WAIT 狀態,接着發送一個 ACK 給 Server,確認序號爲收到序號+1,Server 進入 CLOSED 狀態,完成四次揮手。

TCP 傳輸可靠性

  • 校驗和

    發送的數據包的二進制相加而後取反,目的是檢測數據在傳輸過程當中的任何變化。若是收到報文段的檢驗和有差錯,TCP 將丟棄這個報文段和不確認收到此報文段。併發

  • 確認應答與序列號

    TCP 給發送的每個包進行編號,接收方對數據包進行排序,把有序數據傳送給應用層。異步

  • 超時重傳

    TCP 發出一個段後,它啓動一個定時器,等待目的端確認收到這個報文段。若是不能及時收到一個確認,將重發這個報文段。socket

  • 流量控制

    TCP 鏈接的每一方都有固定大小的緩衝空間,TCP 的接收端只容許發送端發送接收端緩衝區能接納的數據。當接收方來不及處理髮送方的數據,能提示發送方下降發送的速率,防止包丟失。TCP 使用的流量控制協議是可變大小的滑動窗口協議。ide

  • 擁塞控制

    當網絡擁塞時,減小數據的發送。函數

應用數據被分割成 TCP 認爲最適合發送的數據塊。性能

TCP 的接收端會丟棄重複的數據

TCP 數據發送流程

TCP數據發送流程

排序、組裝流程

因爲數據傳輸的順序多是不必定的,因此在此中間會進行排序。

排序、組裝流程

數據包丟失,進行重傳

數據包丟失,進行重傳

鏈接中斷

當鏈接中斷後,須要進行重連、而後從新進行重傳。

鏈接中斷

一對多傳輸流程

一對多傳輸流程

TCP 基礎數據傳輸案例代碼

public class Server {
    private static final int PORT = 20000;

    public static void main(String[] args) throws IOException {
        ServerSocket server = createServerSocket();
        initServerSocket(server);
        // 綁定到本地端口上
        server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 50);
        System.out.println("服務器準備就緒~");
        System.out.println("服務器信息:" + server.getInetAddress() + " P:" + server.getLocalPort());
        // 等待客戶端鏈接
        for (; ; ) {
            // 獲得客戶端
            Socket client = server.accept();
            // 客戶端構建異步線程
            ClientHandler clientHandler = new ClientHandler(client);
            // 啓動線程
            clientHandler.start();
        }

    }

    private static ServerSocket createServerSocket() throws IOException {
        // 建立基礎的ServerSocket
        ServerSocket serverSocket = new ServerSocket();
        // 綁定到本地端口20000上,而且設置當前可容許等待連接的隊列爲50個
        //serverSocket = new ServerSocket(PORT);
        // 等效於上面的方案,隊列設置爲50個
        //serverSocket = new ServerSocket(PORT, 50);
        // 與上面等同
        // serverSocket = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());
        return serverSocket;
    }

    private static void initServerSocket(ServerSocket serverSocket) throws IOException {
        // 是否複用未徹底關閉的地址端口
        serverSocket.setReuseAddress(true);
        // 等效Socket#setReceiveBufferSize
        serverSocket.setReceiveBufferSize(64 * 1024 * 1024);
        // 設置serverSocket#accept超時時間
        // serverSocket.setSoTimeout(2000);
        // 設置性能參數:短連接,延遲,帶寬的相對重要性
        serverSocket.setPerformancePreferences(1, 1, 1);
    }

    /**
     * 客戶端消息處理
     */
    private static class ClientHandler extends Thread {
        private Socket socket;

        ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            super.run();
            System.out.println("新客戶端鏈接:" + socket.getInetAddress() + " P:" + socket.getPort());
            try {
                // 獲得套接字流
                OutputStream outputStream = socket.getOutputStream();
                InputStream inputStream = socket.getInputStream();
                byte[] buffer = new byte[256];
                int readCount = inputStream.read(buffer);
                ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, readCount);
                // byte
                byte be = byteBuffer.get();
                // char
                char c = byteBuffer.getChar();
                // int
                int i = byteBuffer.getInt();
                // bool
                boolean b = byteBuffer.get() == 1;
                // Long
                long l = byteBuffer.getLong();
                // float
                float f = byteBuffer.getFloat();
                // double
                double d = byteBuffer.getDouble();
                // String
                int pos = byteBuffer.position();
                String str = new String(buffer, pos, readCount - pos - 1);
                System.out.println("收到數量:" + readCount + " 數據:" + be + "\n" + c + "\n" + i + "\n" + b + "\n" + l + "\n" + f + "\n" + d + "\n" + str + "\n");
                outputStream.write(buffer, 0, readCount);
                outputStream.close();
                inputStream.close();

            } catch (Exception e) {
                System.out.println("鏈接異常斷開");
            } finally {
                // 鏈接關閉
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("客戶端已退出:" + socket.getInetAddress() + " P:" + socket.getPort());
        }
    }
}
public class Client {
    private static final int PORT = 20000;
    private static final int LOCAL_PORT = 20001;

    public static void main(String[] args) throws IOException {
        Socket socket = createSocket();
        initSocket(socket);
        // 連接到本地20000端口,超時時間3秒,超過則拋出超時異常
        socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
        System.out.println("已發起服務器鏈接,並進入後續流程~");
        System.out.println("客戶端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
        System.out.println("服務器信息:" + socket.getInetAddress() + " P:" + socket.getPort());
        try {
            // 發送接收數據
            todo(socket);
        } catch (Exception e) {
            System.out.println("異常關閉");
        }
        // 釋放資源
        socket.close();
        System.out.println("客戶端已退出~");

    }

    private static Socket createSocket() throws IOException {
        /*
        // 無代理模式,等效於空構造函數
        Socket socket = new Socket(Proxy.NO_PROXY);
        // 新建一份具備HTTP代理的套接字,傳輸數據將經過www.baidu.com:8080端口轉發
        Proxy proxy = new Proxy(Proxy.Type.HTTP,
                new InetSocketAddress(Inet4Address.getByName("www.baidu.com"), 8800));
        socket = new Socket(proxy);
        // 新建一個套接字,而且直接連接到本地20000的服務器上
        socket = new Socket("localhost", PORT);
        // 新建一個套接字,而且直接連接到本地20000的服務器上
        socket = new Socket(Inet4Address.getLocalHost(), PORT);
        // 新建一個套接字,而且直接連接到本地20000的服務器上,而且綁定到本地20001端口上
        socket = new Socket("localhost", PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        socket = new Socket(Inet4Address.getLocalHost(), PORT, Inet4Address.getLocalHost(), LOCAL_PORT);
        */
        Socket socket = new Socket();
        // 綁定到本地20001端口
        socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));

        return socket;
    }

    private static void initSocket(Socket socket) throws SocketException {
        // 設置讀取超時時間爲2秒
        socket.setSoTimeout(2000);
        // 是否複用未徹底關閉的Socket地址,對於指定bind操做後的套接字有效
        socket.setReuseAddress(true);
        // 是否開啓Nagle算法
        socket.setTcpNoDelay(true);
        // 是否須要在長時無數據響應時發送確認數據(相似心跳包),時間大約爲2小時
        socket.setKeepAlive(true);
        // 對於close關閉操做行爲進行怎樣的處理;默認爲false,0
        // false、0:默認狀況,關閉時當即返回,底層系統接管輸出流,將緩衝區內的數據發送完成
        // true、0:關閉時當即返回,緩衝區數據拋棄,直接發送RST結束命令到對方,並沒有需通過2MSL等待
        // true、200:關閉時最長阻塞200毫秒,隨後按第二狀況處理
        socket.setSoLinger(true, 20);
        // 是否讓緊急數據內斂,默認false;緊急數據經過 socket.sendUrgentData(1);發送
        socket.setOOBInline(true);
        // 設置接收發送緩衝器大小
        socket.setReceiveBufferSize(64 * 1024 * 1024);
        socket.setSendBufferSize(64 * 1024 * 1024);
        // 設置性能參數:短連接,延遲,帶寬的相對重要性
        socket.setPerformancePreferences(1, 1, 0);
    }

    private static void todo(Socket client) throws IOException {
        // 獲得Socket輸出流
        OutputStream outputStream = client.getOutputStream();
        // 獲得Socket輸入流
        InputStream inputStream = client.getInputStream();
        byte[] buffer = new byte[256];
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
        // byte
        byteBuffer.put((byte) 126);
        // char
        char c = 'a';
        byteBuffer.putChar(c);
        // int
        int i = 2323123;
        byteBuffer.putInt(i);
        // bool
        boolean b = true;
        byteBuffer.put(b ? (byte) 1 : (byte) 0);
        // Long
        long l = 298789739;
        byteBuffer.putLong(l);
        // float
        float f = 12.345f;
        byteBuffer.putFloat(f);
        // double
        double d = 13.31241248782973;
        byteBuffer.putDouble(d);
        // String
        String str = "Hello你好!";
        byteBuffer.put(str.getBytes());
        // 發送到服務器
        outputStream.write(buffer, 0, byteBuffer.position() + 1);
        // 接收服務器返回
        int read = inputStream.read(buffer);
        System.out.println("收到數量:" + read);
        // 資源釋放
        outputStream.close();
        inputStream.close();
    }
}
相關文章
相關標籤/搜索