1. 程式人生 > >這可能是最容易入門的socket教程了

這可能是最容易入門的socket教程了

前言:

  如今,網路程式設計已然成為了一個後端開發工程師需要具備的核心技能之一。因此,該部落格力求提供最簡單、通俗的描述方式,來描繪網路程式設計中常見的知識點,同時附帶程式碼示例,後期會加上具體的抓包分析,實際專案、框架案例,希望可以和大家共同探索網路世界。

什麼是socket?

  在計算機通訊領域,socket被翻譯為“套接字”。   但是這種翻譯方法其實是不優雅的,因為從字面意思來看,我們並不能很直白的理解socket的具體用途。如果可以的話,我更希望它被翻譯成“通訊插頭”。

 

 

  我們可以這樣想象一下,現在有一根“網線”,一頭連線著客戶端,一頭連線著伺服器,客戶端和伺服器的資料通過這跟“網線”來進行傳輸,而socket的作用,其實就相當於連線網線的插頭。在通訊之前,客戶端和伺服器都要建立一個socket。

socket在網路程式設計中的位置

socket是基於TCP、UDP協議的,如果你熟悉TCP/IP協議。你就會知道,TCP、UDP屬於傳輸層。由於我們主要是學習Socket,所以這裡只是簡單介紹一下這兩種協議:
    1. TCP是面向連線的、UDP是面向無連線的,所謂面向連線,指的就是通訊雙方會維護一組狀態,保證連線的可靠性。
    2. TCP的資料包結構相對複雜,而UDP則相對簡單。無論是TCP還是UDP,他們的包頭上都應該有埠號和目標埠號,TCP的包頭上有序號(順序),確認序號(不丟包)、視窗大小(流量控制 & 擁塞控制)、狀態碼(FIN ACK SYN)等。TCP和UDP的包頭如下圖。
    3. TCP資料傳輸方式是基於資料流的,而UDP則是基於資料報。

 

udp包頭

 

 

 tcp包頭

(我們將在其他博文中對TCP和UDP進行具體講解)  一個包在網路中傳輸,一定不會只有上層沒下層。TCP、UDP處於傳輸層。TCP/IP協議從上到下分別為:應用層、傳輸層、網路層、鏈路層、物理層。所以傳輸層的資料包想要在網路中傳輸,除了埠號,我們還需要網路層中的ip地址(網路層)。socket的建立需要提供的資訊就是 ip+埠。

如何使用socket:

TCP是基於資料流的,所以如果是基於TCP的Socket,我們就需要建立連線,然後獲取資料流,再進行通訊,每對連線需要建立一組socket,具體步驟如下:

 

 

程式碼:

伺服器:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 伺服器.
 *
 * @author jialin.li
 * @date 2019-12-10 19:45
 */
public class TcpServer {
    public static void main(String[] args) throws IOException {
        int port = 8099;
        ServerSocket connectionSocket = new ServerSocket(port);
        // 監聽埠,accept為阻塞方法
        System.out.println("Get socket successfully, wait for request...");
        Socket communicationSocket = connectionSocket.accept();
        // 獲取輸入流,讀取資料
        InputStream inputStream = communicationSocket.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String message;
        while ((message = bufferedReader.readLine()) != null) {
            System.out.printf("get message from client : %s",message);
        }
        communicationSocket.shutdownInput();
        // 獲取輸出流,返回結果
        OutputStream outputStream = communicationSocket.getOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
        bufferedWriter.write("I got your message and the communication is over.");
        bufferedWriter.flush();
        communicationSocket.shutdownOutput();
        // 關閉資源
        bufferedWriter.close();
        bufferedReader.close();
    }
}

客戶端:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

/**
 * 客戶端.
 *
 * @author jialin.li
 * @date 2019-12-10 22:30
 */
public class TcpClient {
    public static void main(String[] args) throws IOException {
        // 指定ip 埠,建立socket
        String host = "127.0.0.1";
        int port = 8099;
        Socket communicationSocket = new Socket(host, port);
        // 獲取輸出流,寫入資料
        OutputStream outputStream = communicationSocket.getOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
        bufferedWriter.write("hello world");
        bufferedWriter.flush();
        communicationSocket.shutdownOutput();
        // 獲取輸入流,讀取伺服器返回資訊
        InputStream inputStream = communicationSocket.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String message;
        while ((message = bufferedReader.readLine()) != null) {
            System.out.printf("get message from server : %s", message);
        }
        communicationSocket.shutdownInput();
        // 關閉資源
        bufferedWriter.close();
        bufferedReader.close();
    }
}

執行結果:

server:

Get socket successfully, wait for request...
get message from client : hello world

client:

get message from server : I got your message and the communication is over.

outputStream.close與shutdownOutput的區別:

  這裡只對outputStream進行分析,inputStream與之相同。

  可以看出,上述程式碼,我們在執行完輸入/輸出動作之後,會呼叫一個shutdownInput/shutdownOutput方法,這個方法有什麼作用呢?可不可以用close方法替代?

  首先,我們閱讀jdk中關於該方法的doc註釋:

Disables the output stream for this socket.
For a TCP socket, any previously written data will be sent
followed by TCP's normal connection termination sequence.

If you write to a socket output stream after invoking
shutdownOutput() on the socket, the stream will throw
an IOException.

  大體意思是說,該方法會禁用socket的輸出流,對於基於TCP協議的Socket,任何在方法執行之前傳送資料,都可以被正常傳送,如果在這個方法執行後傳送資料,就會丟擲一個IO異常。通過該方法關閉流,Socket連線不會收到影響,但是如果我們直接使用close方法關閉流,那麼Socket連線也會隨之關閉。接下來我們來測試一下,在Server中用inputStream.close來代替socket.shutdownInput方法。

結果是由於inputStream.close提前關閉了socket,導致伺服器在輸出資料的時,socket.getOutputStream方法丟擲異常:java.net.SocketException: Socket is closed 

為什麼我們每次呼叫BufferedWrite的write方法,都要呼叫flush方法:

  這個問題不屬於網路程式設計的範疇,但卻是我們在寫socket程式時,很容易犯的一個錯誤。bufferedWrite是字元快取流,它的原理其實很簡單,在記憶體中設定一個快取區,將原本逐個傳送的字元快取起來,批量傳送(有很多框架也採用了這種思想,比如kafka的批量傳送),flush方法是手動的將我們快取區中的資料刷出。快取區中的資料,將會在流關閉之前,進行flush。但是由於我們在傳送資料之後,呼叫了shutdownOutput方法,導致最後close的時候,沒辦法將快取區中的資料flush,因此會丟擲一個寫入失敗的異常:java.net.SocketException: Broken pipe (Write failed)。

UDP是基於資料報的,因此不需要每對連線都建立一組socket,而是隻要有一個socket,就能與多個客戶端通訊,所以只需要通過建立資料報,然後通過socket傳送即可,具體步驟如下:

程式碼:

伺服器:

import java.net.DatagramPacket;
import java.io.IOException;
import java.net.DatagramSocket;

/**
 * 伺服器.
 *
 * @author jialin.li
 * @date 2019-12-10 19:45
 */
public class UdpServer {
    public static void main(String[] args) throws IOException {
        // 監聽埠,阻塞方法
        int port = 8099;
        DatagramSocket socket = new DatagramSocket(port);
        // 建立資料報,用於接收客戶端傳送的資料
        byte[] data = new byte[1024];
        DatagramPacket packet = new DatagramPacket(data, data.length);
        // 接收客戶端傳送的資料
        System.out.println("Get socket successfully, wait for request...");
        socket.receive(packet);
        String message = new String(data, 0, packet.getLength());
        System.out.printf("get message from client : %s", message);

        // 向客戶端傳送資料
        byte[] data2 = "I got your message and the communication is over.".getBytes();
        DatagramPacket packet2 = new DatagramPacket(data2, data2.length, packet.getAddress(), packet.getPort());
        socket.send(packet2);
        socket.close();
    }
}

客戶端:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 客戶端.
 *
 * @author jialin.li
 * @date 2019-12-11 11:32
 */
public class UdpClient {
    public static void main(String[] args) throws IOException {
        // 封裝資料報:ip、埠、資料
        InetAddress ip = InetAddress.getByName("127.0.0.1");
        int port = 8099;
        byte[] data = "hello world".getBytes();
        DatagramPacket packet = new DatagramPacket(data, data.length, ip, port);
        // 建立socket,傳送資料報
        DatagramSocket socket = new DatagramSocket();
        socket.send(packet);

        // 讀取伺服器返回資訊
        byte[] data2 = new byte[1024];
        DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
        socket.receive(packet2);
        // 讀取資料
        String message = new String(data2, 0, packet2.getLength());
        System.out.printf("get message from server : %s", message);
        //關閉資源
        socket.close();
    }
}