Socket網絡編程(三):Socket TCP快速入門

博客主頁java

1. TCP

TCP(Transmission Control Protocol)是傳輸控制協議,一種面向鏈接的,可靠的,基於字節流的傳輸層通訊協議。算法

TCP 通訊同UDP通訊同樣,都可以實現兩臺計算機之間的通訊,通訊的兩端都須要建立Socket對象。segmentfault

區別在於:數組

  1. UDP中只有發送端和接收端,不區分客戶端與服務端,計算機之間能夠任意地發送數據
  2. TCP通訊嚴格區分客戶端與服務器端,在通訊時,必須先又客戶端去鏈接服務器端才能通訊,服務器端不能主動鏈接客戶端,而且,服務器端須要先啓動,等待客戶端的鏈接

在JDK中,提供了兩個類用於實現TCP通訊程序緩存

  1. 客戶端:java.net.Socket類表示。建立Socket對象,向服務器端發出鏈接請求,服務器端響應請求,二者創建鏈接才能開始通訊。
  2. 服務端:java.net.ServerSocket類表示。建立ServerSocket對象,至關於開啓了一個服務,等待客戶端鏈接。

2. ServerSocket

JDK中java.net包中提供ServerSocket類,該類的實例對象能夠實現一個服務器段的程序。服務器

  1. ServerSocket類提供了多種構造方法:
ServerSocket(int port)
建立綁定到指定端口的服務器套接字。

使用該構造方法在建立ServerSocket對象時,就能夠將其綁定到一個指定的端口號上(參數port就是端口號)網絡

  1. ServerSocket的經常使用方法:
Socket    accept()
偵聽要鏈接到此套接字並接受它。該方法將阻塞直到創建鏈接。

InetAddress    getInetAddress()
返回此服務器套接字的本地地址。若是套接字被綁定在closed以前,則該方法將在套接字關閉後繼續返回本地地址。

ServerSocket對象負責監聽某臺計算機的某個端口號,在建立ServerSocket對象後,須要繼續調用該對象的accept()方法,接收來自客戶端的請求。當執行了accept()方法以後,服務器端程序會發生阻塞,直到客戶端發出鏈接請求,accept()方法纔會返回一個Scoket對象用於和客戶端實現通訊,程序才能繼續向下執行.socket

3. Socket

JDK提供了一個Socket類,用於實現TCP客戶端程序。tcp

  1. Socket類一樣提供了多種構造方法
Socket(String host, int port)
建立流套接字並將其鏈接到指定主機上的指定端口號。

使用該構造方法在建立Socket對象時,會根據參數去鏈接在指定地址和端口上運行的服務器程序,其中參數host接收的是一個字符串類型的IP地址。函數

Socket(InetAddress address, int port)
建立流套接字並將其鏈接到指定IP地址的指定端口號。

若是指定的主機是null ,則至關於指定地址爲InetAddress.getByName (null) 。 換句話說,它至關於指定回送接口的地址。

該方法在使用上與第二個構造方法相似,參數address用於接收一個InetAddress類型的對象,該對象用於封裝一個IP地址。

  1. Socket的經常使用方法
int    getPort()
返回此套接字鏈接到的遠程端口號。

InetAddress    getLocalAddress()
獲取套接字所綁定的本地地址。

void    close()
關閉此套接字。任何線程當前被阻塞在這個套接字上的I / O操做將會拋出一個SocketException 。
關閉此socket也將關閉socket的InputStream和OutputStream 。

InputStream    getInputStream()
返回此套接字的輸入流。關閉返回的InputStream將關閉相關的套接字。

OutputStream    getOutputStream()
返回此套接字的輸出流。關閉返回的OutputStream將關閉相關的套接字。

void    shutdownOutput()
禁用此套接字的輸出流。
任何先前寫入的數據將被髮送,隨後是TCP的正常鏈接終止序列。
若是在套接字上調用shutdownOutput()以後寫入套接字輸出流,則流將拋出IOException。

在Socket類的經常使用方法中,getInputStream()和getOutStream()方法分別用於獲取輸入流和輸出流。當客戶端和服務端創建鏈接後,數據是以IO流的形式進行交互的,從而實現通訊。

4. 實現一個簡單的TCP網絡程序

瞭解了Socket 和 ServerSocket這兩個類的基本用法,經過下面簡單的TCP加深理解。

注意:若是先啓動客戶端,拋出java.net.ConnectException: Connection refused (Connection refused)異常

4.1 客戶端向服務端發送數據

服務端實現:

public class TcpServer {
    public static void main(String[] args) throws IOException {

        System.out.println("Server->啓動");
        // 建立ServerSocket對象,並綁定到指定端口爲20000
        ServerSocket serverSocket = new ServerSocket(20000);

        // 偵聽鏈接,獲取Socket對象
        // accept方法將阻塞直到創建鏈接。
        Socket socket = serverSocket.accept();

        // 經過socket獲取網絡輸入流
        InputStream is = socket.getInputStream();

        // 從輸入流中讀取字節數據到buffer中
        byte[] buffer = new byte[1024];
        int len= is.read(buffer);

        String msg = new String(buffer, 0, len);

        // 打印接收到的數據
        System.out.println("Server-> receive msg:" + msg);

        // 關閉資源
        socket.close();

        System.out.println("Server->關閉");
    }
}

客戶端實現:

public class TcpClient {
    public static void main(String[] args) throws IOException {
        System.out.println("Client->啓動");
        // 建立Socket對象,並鏈接到指定主機上的指定端口號。
        Socket socket = new Socket("127.0.0.1", 20000);

        // 經過Socket獲取網絡輸出流
        OutputStream os = socket.getOutputStream();

        // 經過輸出流寫入數據
        os.write("hello tcp!".getBytes());

        //關閉資源
        os.close();

        System.out.println("Client->關閉");
    }
}

4.2 服務端向客戶端回寫數據

服務端實現:

public class TcpServer {
    public static void main(String[] args) throws IOException {
         //...

        // ====================回寫數據====================
        // 經過socket獲取網絡輸出流
        OutputStream os = socket.getOutputStream();
        // 經過網絡輸出流回寫數據
        os.write("hello, 我收到了.".getBytes());

        // ...
    }
}

客戶端實現:

public class TcpClient {
    public static void main(String[] args) throws IOException {
        // ...

        // ====================接收服務端回寫數據====================
        // 經過Socket獲取網絡輸入流
        InputStream is = socket.getInputStream();

        // 從網絡輸入流中讀取數據
        byte[] buffer = new byte[1024];
        int len = is.read(buffer);

        System.out.println("Client-> receive msg: " + new String(buffer, 0, len));
        
        // ...
    }
}

5. 案例實操-TCP傳輸初始化配置,基本數據傳輸實例

5.1 TCP傳輸初始化配置

TCP客戶端Client初始化配置

1. 客戶端Socket建立方式

在實際項目實操中,建立客戶端Socket時,使用無參數的Socket構造,或者經過Socket(Proxy proxy)構造,這樣Socket對象建立成功後,是一個未鏈接Socket,就能夠經過Socket對象進行初始化配置。

private static final int PORT = 20001;
private static final int LOCAL_PORT = 30001;

private static Socket createSocket() throws IOException {
    // 建立一個未鏈接的Socket對象
    Socket socket = new Socket();
    // 或者使用無代理(忽略任何其餘代理配置)的構造函數,等效於空構造函數
    //Socket socket = new Socket(Proxy.NO_PROXY);

    // 將Socket綁定到本地IP地址和端口號
    socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));
    return socket;
}

也能夠在建立Socket時,指定應該使用什麼樣的代理轉發數據。

// 建立一個經過指定的HTTP代理服務器鏈接的Socket,數據經過指定的代理轉發
Proxy proxy = new Proxy(
        Proxy.Type.HTTP,
        new InetSocketAddress("www.baidu.com", 1080)
);
Socket socket = new Socket(proxy);

下面幾種方式建立Socket對象時,在建立時就鏈接到指定的服務器上,不能作一些初始化配置。

// 建立Socket,並將其鏈接到指定主機上和指定端口號的服務器上
Socket socket = new Socket("localhost", PORT);

//Socket(InetAddress address, int port)
//建立流套接字並將其鏈接到指定IP地址的指定端口號。
Socket socket = new Socket(Inet4Address.getLocalHost(), PORT);

//Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
//建立套接字並將其鏈接到指定的遠程端口上指定的遠程地址。
Socket socket = new Socket(
        Inet4Address.getLocalHost(), 
        PORT, 
        Inet4Address.getLocalHost(), 
        LOCAL_PORT
);

//Socket(String host, int port, InetAddress localAddr, int localPort)
//建立套接字並將其鏈接到指定遠程端口上的指定遠程主機。
Socket socket = new Socket(
        "localhost",
        PORT, 
        Inet4Address.getLocalHost(),
        LOCAL_PORT
);
2. Socket初始化配置

在設置Socket一些初始化配置時,須要注意,在Socket鏈接後配置將不起做用,必須在鏈接以前調用。

private static void configSocket(Socket socket) throws SocketException {
    // 設置讀取超時時間,單位:毫秒。timeout=0時,無限超時;timeout>0時,與此Socket相關聯的InputStream上的read()調用將僅阻止此時間.
    // 若是超時超時,則引起java.net.SocketTimeoutException
    socket.setSoTimeout(2000);

    //Nagle的算法,true啓用TCP_NODELAY, false禁用。
    socket.setTcpNoDelay(true);

    // 是否須要在長時無數據響應時發送確認數據(相似心跳包),時間大約爲2小時
    socket.setKeepAlive(true);

    // 設置逗留時間(以秒爲單位),最大超時值是平臺特定的,該設置僅影響關Socket關閉。默認爲false,0
    // false, 0: 默認狀況,關閉時當即返回,底層系統接管輸出流,將緩衝區的數據發送完成
    // true, 0: 當即關閉返回,緩存區數據拋棄,直接發送RST結束命令到對方,並沒有需通過2MSL等待
    // true, 2: 關閉時最長堵塞2秒,隨後按照第二種狀況處理
    socket.setSoLinger(true, 2);

    // 是否接收TCP緊急數據,默認爲false,禁止接收,在Socket接收的TCP緊急數據被靜默地丟棄。
    socket.setOOBInline(true);

    // 設置接收緩衝區區大小
    // 增長接收緩衝區大小能夠提升大容量鏈接的網絡I / O的性能,同時能夠幫助減小輸入數據的積壓
    // 須要注意:1.對於客戶端Socket,在將Socket鏈接到服務器以前,必須調用setReceiveBufferSize()
    // 2. 對於ServerSocket接受的Socket,必須經過在ServerSocket綁定到本地地址以前調用ServerSocket.setReceiveBufferSize(int)來完成。
    socket.setReceiveBufferSize(64 * 1024 * 1024);

    // 設置發送緩衝區大小的大小,該值必須大於0
    socket.setSendBufferSize(64 * 1024 * 1024);

    // 注意:在此連Socket鏈接後調用此方法將不起做用,必須在鏈接以前調用
    // 設置此Socket的性能參數:
    // connectionTime :一個 int表達短鏈接時間的相對重要性
    // latency :一個 int表達低延遲的相對重要性
    // bandwidth :一個 int表達高帶寬的相對重要性
    // 這三個值只是簡單的比較,哪一個參數設置的值大偏向誰
    socket.setPerformancePreferences(1, 1, 0);
}

最後在建立Socket並配置後,將此Socket鏈接到具備指定的服務器。

public static void main(String[] args) throws IOException {
    
    Socket socket = createSocket();
    
    configSocket(socket);
    
    //connect(SocketAddress endpoint, int timeout)
    //將此Socket鏈接到具備指定超時值的服務器
    socket.connect(new InetSocketAddress(Inet4Address.getLocalHost(), PORT), 3000);
}

TCP客戶端ServerSocket初始化配置

1. 客戶端ServerSocket建立方式

一般在建立ServerSocket對象時,使用空參數的構造函數,這樣後續能夠給ServerSocket設置一些配置。

private static ServerSocket createServerSocket() throws IOException {
    // 建立未綁定的服務器套接字
    ServerSocket server = new ServerSocket();

    return server;
}

下面幾種方式建立ServerSocket對象時,在建立時就bind到指定的端口,不能作一些初始化配置

// 建立綁定到指定端口的服務器套接字
// 等待鏈接的最大隊列長度設置爲50 ,若是鏈接在隊列已滿時到達,則鏈接被拒絕
ServerSocket server = new ServerSocket(PORT);

// 建立服務器套接字並將其綁定到指定的本地端口號,同時並指定了積壓
// 等待鏈接的最大隊列長度設置爲backlog ,若是鏈接在隊列已滿時到達,則鏈接被拒絕
ServerSocket server = new ServerSocket(PORT, 50);

// 建立一個具備指定端口的服務器,偵聽backlog和本地IP地址綁定
ServerSocket server = new ServerSocket(PORT, 50, Inet4Address.getLocalHost());

最後在建立ServerSocket並設置配置後,bind指定的端口

private static final int PORT = 20001;
public static void main(String[] args) throws IOException {

    ServerSocket server = createServerSocket();

    configServerSocket(server);

    //將 ServerSocket綁定到特定地址(IP地址和端口號)
    server.bind(new InetSocketAddress(Inet4Address.getLocalHost(), PORT));

}
2. ServerSocket初始化配置

在設置ServerSocket一些初始化配置時,須要在bind以前纔能有效。

private static void configServerSocket(ServerSocket server) throws SocketException {
    // 當TCP鏈接關閉時,鏈接可能會在鏈接關閉後一段時間內保持在超時狀態(一般稱爲TIME_WAIT狀態或2MSL等待狀態)
    // 若是在套接字地址或端口的超時狀態中存在鏈接,則可能沒法將套接字綁定到所需的SocketAddress
    // 設置爲true,套接字bind(SocketAddress)容許在上一個鏈接處於超時狀態時綁定套接字
    server.setReuseAddress(true);

    //設置套接字接收緩衝區的大小
    // 注意:在ServerSocket在綁定到本地地址以前調用
    // 也就是意味着必須使用無參數構造函數建立ServerSocket,而後調用setReceiveBufferSize()
    server.setReceiveBufferSize(64 * 1024 * 1024);

    // 設置讀取超時時間,單位:毫秒。timeout=0時,無限超時;timeout>0時,與此ServerSocket的accept()調用將僅阻止此時間.
    // 若是超時超時,則引起java.net.SocketTimeoutException
    //server.setSoTimeout(2000);

    //設置性能參數:短連接,延遲,帶寬的相對重要性
    server.setPerformancePreferences(1, 1, 0);
}

5.2 基本數據傳輸

在使用Socket的輸出流,傳輸基本數據類型時,如int類型。

咱們先來看使用Socket傳輸int類型數據,例如:傳輸int類型10

// 客戶端
private static void todo_client(Socket socket) throws IOException {
    OutputStream os = socket.getOutputStream();

    InputStream is = socket.getInputStream();

    os.write(10);

    // 釋放資源
    socket.close();
}

// 服務器
private void todo_server(Socket socket) throws IOException {
    // 獲取網絡輸入流
    InputStream is = socket.getInputStream();

    byte[] buffer = new byte[1024];
    int len = is.read(buffer);

    System.out.println("Server-> len: " + len + " data: " + new String(buffer, 0, len));
}

咱們發現打印輸出的log不是咱們指望的,數字10沒有輸出

> Task :TcpServer.main()
Server-> len: 1 data:

當咱們在傳輸int類型10時,調用下面方法,客戶端將int類型轉爲byte數組。

public static byte[] intToByteArray(int a) {
    return new byte[]{
            (byte) ((a >> 24) & 0xFF),
            (byte) ((a >> 16) & 0xFF),
            (byte) ((a >> 8) & 0xFF),
            (byte) (a & 0xFF)
    };
}

在服務端接收時,調用下面方法,客戶端將byte數組轉爲int類型。

public int byteArrayToInt(byte[] b) {
    return b[3] & 0xFF |
            (b[2] & 0xFF) << 8 |
            (b[1] & 0xFF) << 16 |
            (b[0] & 0xFF) << 24;
}

打印輸出的log輸出咱們指望的值

> Task :TcpServer.main()
Server-> len: 4 data: 10

在JDK java.nio包中,爲咱們提供了更方面的類ByteBuffer,一個字節緩存區。緩衝區的索引不是以字節爲單位,而是根據其值的類型特定大小,如int類型大小爲4,long類型大小爲8。更重要是緩衝區更高效。

客戶端:使用ByteBuffer的wrap方法將字節數組封裝到緩衝區中,而後put到緩存區中。如int類型,將int值的四個字節寫入當前位置的緩衝區

private static void todo_client(Socket socket) throws IOException {
    OutputStream os = socket.getOutputStream();

    InputStream is = socket.getInputStream();

    byte[]  buffer = new byte[256];
    // 將一個字節數組包裝到緩衝區中。
    ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);

    // byte
    byte b = 126;
    byteBuffer.put(b);

    // char
    char c = 'a';
    byteBuffer.putChar(c);

    // int
    int i = 1223344;
    byteBuffer.putInt(i);

    //bool
    boolean bool = true;
    byteBuffer.put(bool ? (byte) 1 : (byte) 0);

    // long
    long l = 1287655778990L;
    byteBuffer.putLong(l);

    //float
    float f = 3.1345f;
    byteBuffer.putFloat(f);

    // double
    double d = 12223.0232199761;
    byteBuffer.putDouble(d);

    // String
    String str = "hello, 你好啊!";
    byteBuffer.put(str.getBytes());

    os.write(buffer, 0, byteBuffer.position() + 1);

    // 釋放資源
    socket.close();
}

服務端:使用ByteBuffer的wrap方法將字節數組封裝到緩衝區中,而後從緩存區中讀取值,如int類型值,在該緩衝區的當前位置讀取接下來的四個字節

private void todo_server(Socket socket) throws IOException {
    // 獲取網絡輸入流
    InputStream is = socket.getInputStream();

    byte[] buffer = new byte[256];

    int len = is.read(buffer);

    ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, len);

    //  byte
    byte b = byteBuffer.get();

    // char
    char c = byteBuffer.getChar();

    // int
    int i = byteBuffer.getInt();

    // boolean
    boolean bool = byteBuffer.get() == 1;

    //long
    long l = byteBuffer.getLong();

    // float
    float f = byteBuffer.getFloat();

    // double
    double d = byteBuffer.getDouble();

    int pos = byteBuffer.position();
    String str = new String(buffer, pos, len - pos - 1);


    System.out.println("Server-> len: " + len + "\n"
            +  " b: " + b + "\n"
            +  " c: " + c + "\n"
            +  " i: " + i + "\n"
            +  " bool: " + bool + "\n"
            +  " l: " + l + "\n"
            +  " f: " + f + "\n"
            +  " d: " + d + "\n"
            +  " str: " + str + "\n"
    );
}

從輸出的log能夠看出,全部基本數據類型的輸出都正確:

Server-> len: 46
 b: 126
 c: a
 i: 1223344
 bool: true
 l: 1287655778990
 f: 3.1345
 d: 12223.0232199761
 str: hello, 你好啊!

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索