Java基礎系列:Socket編程

俗世遊子:專一技術研究的程序猿html

說到前面的話

沒有實戰案例的理論基礎都是在耍流氓,因此今天主要是想經過這裏的案例可以讓你們加深對以前的理解java

本節咱們會一步步實現一個點對點聊天小程序編程

Java中的Socket實現

InetAddress

InetAddress是Java對IP地址的封裝,這個類是一個基礎類,下面的ServerSocketDatagramSocket都離不開這個類小程序

InetAddress沒法經過new的方式來初始化,只能提供過其提供的靜態方法來調用:api

// 獲取本地地址
InetAddress localHost = InetAddress.getLocalHost();

InetAddress的方法

這裏是InetAddress的一些方法:瀏覽器

// 主機名:DESKTOP-ATG4KKE
System.out.println("主機名:" + localHost.getHostName());

// IP地址:192.168.87.1
System.out.println("IP地址:" + localHost.getHostAddress());

// 是否正常:true
System.out.println("是否正常:" + localHost.isReachable(5000));

這裏是我測試時的輸出,服務器

關於isReachable()的方法,用來檢測該地址是否能夠訪問,由此咱們能夠作一些健康檢查操做,好比:網絡

// 經過主機IP或者域名來獲得InetAddress對象
InetAddress inetAddress = InetAddress.getByName("192.168.87.139");
System.out.println("是否正常:" + inetAddress.isReachable(5000));

在5s以內盡最大可能嘗試鏈接到主機,若是沒有就認爲主機不可用,這裏受限於防火牆服務器配置併發

固然,作健康檢查這種方法仍是low了點,生產環境中確定不會這麼幹oracle

PS: 生產環境的網絡操做不會使用到這節裏的東西,大部分狀況下采用的都是Netty

ServerSocket

ServerSocket是服務端套接字,是基於TCP/IP協議下的實現

初始化

一般咱們這樣來構建:

ServerSocket serverSocket = new ServerSocket(9999);

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(9999));

這樣就完成了服務端的初始化,而且將端口9999綁定起來

等待鏈接

若是客戶端想要和ServerSocket創建鏈接,咱們須要這麼作

for(;;) {
    Socket socket = serverSocket.accpet();
    // Socket[addr=/0:0:0:0:0:0:0:1,port=62445,localport=9999]
    System.out.println(socket);
}

accpet()是偵聽與ServerSocket創建的鏈接,這個方法是一個阻塞方法,會一直等待鏈接接入進來

若是有鏈接接入進來,咱們能夠經過返回值來獲得當前接入進來的Socket

通訊

在網絡中傳遞數據其實也是按照IO流的方式進行傳遞的,可是咱們只能獲取到字節流:

InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();

InputStream讀取數據,OutputStream寫出數據,這些基本操做咱們在以前的IO流中都介紹過,這裏就再也不多說

這裏咱們爲了可以提升效率,能夠採用包裝流或者處理流來處理,這前面也介紹過了

完整小例子

其實到這裏,ServerSocket的關鍵介紹也就完了,下面咱們來作一個小例子:

  • 當有客戶端鏈接進來以後,給客戶端返回:Hello World
public class _ServerSocket {
    // 用來存儲請求客戶端和Socket之間的對應關係
    static Map<String, Socket> MAP = new HashMap<>(); 

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(9999));

            for (; ; ) {
                String token = UUID.randomUUID().toString().replace("-", "").toLowerCase();

                Socket socket = serverSocket.accept();
                // 對應
                MAP.put(token, socket);

                outHtml(socket);
            }

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

    public static void outHtml(Socket socket) {
        OutputStream outputStream = null;
        try {
            outputStream = socket.getOutputStream();
            outputStream.write(("HTTP/1.1 200 OK\n\nHello World").getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != outputStream) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

HTTP/1.1 200 OK\n\nHello World\n

這是HTTP協議下返回類型,前面是Response固定格式,Hello World是真正返回的內容,這樣咱們的ServerSocket就可以經過瀏覽器來訪問了

ServerSocket

Socket

Socket屬於客戶端套接字,只有先和服務端套接字創建鏈接才能作其餘的操做,Socket的使用方式很是簡單

創建鏈接

Socket socket = new Socket("127.0.0.1", 9999);

// 驗證是否鏈接成功
if (socket.isConnected()) {
    System.out.println("到服務端鏈接成功");
}

這是其中一種構造方法,更多狀況下是採用這種方式

和服務端的鏈接創建成功以後,後續的操做就和ServerSocket通訊步驟同樣了,這裏就再也不多廢話了

下面用一個完整的例子來鞏固一下

案例:TCP點對點聊天

服務端

public class Server {
    /**
     * 將客戶端標識和socket關聯起來
     */
    private static final Map<String, Socket> SOCKET_MAP = new HashMap<>();
    /**
     * 反向關聯,用來獲取標識
     */
    private static final Map<Socket, String> SOCKET_TOKEN_MAP = new HashMap<>();

    public static void main(String[] args) throws IOException {
        /**
         * 開啓ServerSocket並監聽9999端口
         */
        ServerSocket serverSocket = new ServerSocket(9999);

        for (;;) {
            /**
             * 等待客戶端鏈接
             */
            Socket socket = serverSocket.accept();

            /**
             * IO讀取是阻塞式方法,因此須要開啓新線程,這裏能夠優化成線程池
             */
            new Thread(() -> {
                try {
                    saveToMap(socket);
                    getClientMsg(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    /**
     * 綁定SOCKET
     */
    private static void saveToMap(Socket socket) throws IOException {
        String token = StringUtil.uuid();
        SOCKET_MAP.put(token, socket);
        SOCKET_TOKEN_MAP.put(socket, token);

        System.out.println("---客戶端鏈接成功,編號:" + token);
        System.out.println("當前用戶:" + SOCKET_MAP.size());

        /**
         * 由於沒有登陸,因此這裏要告知客戶端本身的標識
         */
        send(token, token, token);
    }

    /**
     * 獲取客戶端發送過來的消息,併發送出指定指定的客戶端
     */
    private static void getClientMsg(Socket socket) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = "";
        while ((line = reader.readLine()) != null) {
            // 讀取到一行之後,從這裏發送出去
            send(socket, line);
        }
    }

    /**
     * 發送消息
     */
    private static void send(Socket socket, String line) throws IOException {
        String[] s = line.split("#");
        final String from = SOCKET_TOKEN_MAP.get(socket);
        send(s[0], s[1], from);
    }

    /**
     * 發送消息
     * @param token
     * @param msg
     * @param from 這裏在目標客戶端展現
     * @throws IOException
     */
    private static void send(String token, String msg, String from) throws IOException {
        Socket sk = SOCKET_MAP.get(token);

        if (null == sk)
            return;

        String s = from + ":" + msg;
        System.out.println("---發送給客戶端:" + s );
        // 字符流輸出
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(sk.getOutputStream()));
        writer.write(s);
        writer.newLine();
        writer.flush();

    }
}

客戶端

public class Client {

    public static void main(String[] args) throws IOException {
        /**
         * 鏈接到服務端
         */
        Socket socket = new Socket("127.0.0.1", 9999);

        /**
         * 開新線程讀取消息,能夠優化
         */
        new Thread(() -> {
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String line = "";
                while (StringUtil.isNotBlank(line = reader.readLine())) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        /**
         * 從控制檯寫入消息併發送出去
         */
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String next = scanner.next();
            send(next, socket);
        }
    }

    private static void send(String msg, Socket socket) throws IOException {
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        writer.write(msg);
        writer.newLine();
        writer.flush();
    }
}

代碼已經經過測試,註釋寫的也很是清楚,你們能夠嘗試下,按照標識#消息的格式就能夠點對點聊天了。

若是想要羣聊

  • 將Socket保存到集合中,而後循環集合就能夠了,很是簡單

很久沒有用Socket寫聊天程序了,差點就放棄了

下次改用Netty來寫,NettySocket方便多了

DatagramSocket

DatagramSocket是用於發送和接收數據報包的套接字,是基於UDP協議下的實現。根據類中官方介紹:

數據報套接字是數據包傳遞服務的發送或接收點。 在數據報套接字上發送或接收的每一個數據包都通過單獨尋址和路由。 從一臺機器發送到另外一臺機器的多個數據包可能會以不一樣的方式路由,而且可能以任何順序到達

咱們也能明白UDP協議的特性。

DatagramPacket

該類表示數據報包,在DatagramSocket中傳遞和接收數據都是靠這個類來完成的,好比:

  • 接收數據
byte[] buffer = new byte[1024];
DatagramPacket p = new DatagramPacket(buffer, buffer.length);
  • 發送數據
DatagramPacket p = new DatagramPacket("123".getBytes(), "123".getBytes().length, InetAddress.getByName("localhost"), 9999);

發送數據出去,DatagramPacket須要指定接收端的IP和端口,這樣纔可以發送出去

下面咱們來看看具體如何用

初始化

DatagramSocket socket = new DatagramSocket(9999);

DatagramSocket s = new DatagramSocket(null);
s.bind(new InetSocketAddress(9999));

兩種方式均可以完成初始化,沒有什麼區別

接收消息

byte[] buffer = new byte[1024];
DatagramPacket p = new DatagramPacket(buffer, buffer.length);

socket.receive(p);

System.out.println(new String(p.getData(), 0, p.getLength()));

根據DatagramPacket的接收參數,構造出來一個byte[],而後調用receive(),這樣消息就接收到了

receive()是一個阻塞方法,只有等有消息的時候纔會繼續執行

發送消息

DatagramPacket p = new DatagramPacket("123".getBytes(), "123".getBytes().length, InetAddress.getByName("localhost"), 9999);

socket.send(p);

構造發送數據包,而後調用send()方法就能夠完成數據包的發送

UDP不須要鏈接,直接經過IP+PORT的方式就能夠發送數據

案例:UDP聊天

public class _DatagramPacket {

    public static void main(String[] args) throws IOException {
        // 從命令行獲得須要綁定的端口和發送數據的端口
        DatagramSocket datagramSocket = new DatagramSocket(Integer.parseInt(args[0]));

        System.out.println("已啓動");

        new Thread(() -> {

            byte[] buffer = new byte[1024];
            DatagramPacket p = new DatagramPacket(buffer, buffer.length);
            try {
                for (;;) {
                    // 構建接收數據
                    datagramSocket.receive(p);
                    System.out.println(p.getPort() + ":" + new String(buffer, 0, p.getLength()));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        DatagramPacket p = new DatagramPacket(new byte[0], 0, new InetSocketAddress("127.0.0.1", Integer.parseInt(args[1])));
        while (scanner.hasNext()) {
            String next = scanner.next();
            // 構建發送數據包
            p.setData(next.getBytes());
            datagramSocket.send(p);
        }
    }

有瑕疵,空格會換行,這裏交給你們去修改了

最後的話

到這裏,關於Socket編程方面的東西就聊完了,沒有介紹不少的API方法,這些在用到的時候再看也是同樣的。

如下是java.net所在的目錄文檔:

點擊這裏查看

相關文章
相關標籤/搜索