Android網絡編程(二)Socket編程

前言

上篇文章我們描述了傳輸層協議TCP、UDP,但它們畢竟只是協議,看不見摸不著,那我們怎們通過TCP、和UDP進行實際傳輸呢?不用著急,等看完這篇文章你一定會明白的。

1 Socket概述

Socket中文意思為插座的意思,專業術語稱之為套接字,它把TCP/IP封裝成了調用接口供開發者調用,也就是說開發者可以通過調用Socket相關API來實現網絡通訊。在Java中也存在Socket相關API,主要分為兩個,分別是基于UDP傳輸協議的Socket和基于TCP傳輸協議的Socket,本篇文章會對基于這兩種傳輸協議的Socket進行詳細描述。

2 UDP Socket

2.1 基本使用

通過上節的內容我們知道UDP是無連接的,只要提供對方的IP地址和端口號就能進行數據的傳輸,其中IP負責定位主機端口負責定位應用。知道了目標IP和目標端口號通過Java中的UDP Socket就能進行IO傳輸,我們來看一下具體的代碼體現

**
 * 發送方UDP
 */
public class UDPSocketSend {
    public static void main(String[] args) throws IOException {

        System.out.println("Sender Start...");

        //1.創建socket服務
        DatagramSocket ds = new DatagramSocket();

        //2.封裝數據
        String str = "Did you recite words today";
        byte[] bytes = str.getBytes();
        //地址
        InetAddress address =InetAddress.getByName("192.168.31.137");
        //參數:數據、長度、地址、端口
        DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);

        //3.發送數據包
        ds.send(dp);

        //4.關閉socket服務
        ds.close();
    }

/**
 * 接收方UDP
 */
public class UDPSocketReceive{
    public static void main(String[] args) throws IOException {

        System.out.println("Receiver Start...");

        //1.創建udp的socket服務,并聲明端口號
        DatagramSocket ds = new DatagramSocket(6666);

        //2.創建接收數據的數據包
        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes,bytes.length);

        //3.將數據接收到數據包中,為阻塞式方法
        ds.receive(dp);

        //4.解析數據
        InetAddress address = dp.getAddress();//發送方IP
        int port = dp.getPort();//發送方端口
        String content = new String(dp.getData(),0,dp.getLength());
        System.out.println("address:"+address+"---port:"+port+"---content:"+content);

        //關閉服務
        ds.close();
    }
}

由于筆者旁邊只有一臺電腦,所以我就通過端口號來區分發送方和接收方。
分別啟動發送方和接收方,我們來看一下打印結果
發送方:

Sender Start...

接收方

Receiver Start...
address:/192.168.31.137---port:65037---content:Did you recite words today

成功接收到消息,并打印出發送方IP和端口,下面我來個大家擼一遍步驟
發送方:

  • 首先創建udp的socket服務
  • 將需要發送的數據放在數據包DatagramSocket中,DatagramSocket會根據UDP協議對數據包、IP、端口號進行封裝
  • 通過udp的socket服務將數據包發送
  • 最后將udp服務關閉

接收方:

  • 創建udp的socket服務,并且明確自己的端口號
  • 創建DatagramSocket用來解析數據接收到的數據包
  • 將數據接收到數據包DatagramSocket中
  • 通過DatagramSocket解析數據
  • 關閉服務

整個UDP發送數據的流程就是這樣

注意點:

  • 因為UDP是無連接的不可靠傳輸,所以接收方需要在發送方發送數據之前就啟動,否則會接收不到數據,也就是說必須先運行UDPSocketReceive再運行UDPSocketSend。

2.2 聊天實例

把上面的例子進行一些小改動就可以實現聊天功能

public class UDPSocket1 {
    public static void main(String[] args) {
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        receive();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            send();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //接收消息方法
    private static void receive() throws IOException {
        System.out.println("UDOSocket1 Receiver Start...");

        //1.創建udp的socket服務,并聲明端口號
        DatagramSocket ds = new DatagramSocket(6666);
        //無限循環,一直處于接收狀態
        while (true) {
            //2.創建接收數據的數據包
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

            //3.將數據接收到數據包中
            ds.receive(dp);

            //4.解析數據
            String content = new String(dp.getData(), 0, dp.getLength());
            System.out.println("UDPSocket1 Receive:" + content);
        }
    }

    private static void send() throws IOException {
        //1.創建socket服務
        DatagramSocket ds = new DatagramSocket();

        //將鍵盤輸入的信息轉換成輸入流再放入到緩沖區
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        while ((line=br.readLine())!=null){
            //2.封裝數據
            byte[] bytes = line.getBytes();
            //地址
            InetAddress address =InetAddress.getByName("192.168.31.137");
            //參數:數據、長度、地址、端口
            DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,7777);

            //3.發送數據包
            ds.send(dp);
        }

        //4.關閉socket服務
        ds.close();
    }
}

public class UDPSocket2 {
    public static void main(String[] args){
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        receive();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            send();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //接收消息方法
    private static void receive() throws IOException {
        System.out.println("UDOSocket2 Receiver Start...");

        //1.創建udp的socket服務,并聲明端口號
        DatagramSocket ds = new DatagramSocket(7777);
        //無限循環,一直處于接收狀態
        while (true) {
            //2.創建接收數據的數據包
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

            //3.將數據接收到數據包中
            ds.receive(dp);

            //4.解析數據
            String content = new String(dp.getData(), 0, dp.getLength());
            System.out.println("UDPSocket2 Receive:" + content);
        }
        //關閉服務
//        ds.close();
    }

    private static void send() throws IOException {
        //1.創建socket服務
        DatagramSocket ds = new DatagramSocket();

        //將鍵盤輸入的信息轉換成輸入流再放入到緩沖區
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        while ((line=br.readLine())!=null){
            //2.封裝數據
            byte[] bytes = line.getBytes();
            //地址
            InetAddress address =InetAddress.getByName("192.168.31.137");
            //參數:數據、長度、地址、端口
            DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);

            //3.發送數據包
            ds.send(dp);
        }

        //4.關閉socket服務
        ds.close();
    }
}

在接收方方法寫一個無限循環讓其處于持續接收狀態,發送發通過對鍵盤錄入加回車進行消息的發送,并且兩個端點都具備發送和接收功能。需要注意的是receive()會執行一個無限循環,所以receive()和send()必須位于不同線程,否則會導致無法發送消息從而也接收不到消息。
來看打印結果:
UDPSocket1

UDPSocket Receiver Start...
UDPSocket Receive:hello udp1
heelo udp2

UDPSocket2

UDPSocket2 Receiver Start...
hello udp1
UDPSocket2 Receive:hello udp2

我在UDPSocket1 的控制臺中輸入了:“hello udp2”、在UDPSocket2 的控制臺中輸入了:“hello udp1”,接收內容和發送內容完全一致,一個簡單的聊天功能就實現了,希望不熟悉的朋友也可以敲一遍代碼練一遍。

3 TCP Socket

3.1 基本使用

TCP基于client-server是面向連接的可靠傳輸,上篇文章我們也講解了TCP安全傳輸機制,可謂是相當復雜,如果需要個人對TCP協議進行封裝顯然大多數開發者是很難實現的,所以Java也為開發者提供了基于TCP的Socket,不同于UDP,TCP Socket分為Socket和ServerSocket對應著client和server,下面我來用代碼實現一個簡單的TCP通訊功能:
客戶端:

//客戶端
public class TCPClient {
    public static void main(String[] args) throws IOException {

        //1.創建TCP客戶端Socket服務
        Socket client = new Socket();
        //2.與服務端進行連接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10000);
        client.connect(address);
        //3.連接成功后獲取客戶端Socket輸出流
        OutputStream outputStream = client.getOutputStream();
        //4.通過輸出流往服務端寫入數據
        outputStream.write("hello server".getBytes());
        //5.關閉流
        client.close();
    }
}

首先創建一個Socket和InetSocketAddress ,然后通過Socket的connect()方法進行連接,連接成功后可以獲取到輸出流,通過該輸出流就可以向服務端傳輸數據。

服務端:

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1.創建服務端Socket并明確端口號
        ServerSocket serverSocket = new ServerSocket(10000);
        //2.獲取到客戶端的Socket
        Socket socket = serverSocket.accept();
        //3.通過客戶端的Socket獲取到輸入流
        InputStream inputStream = socket.getInputStream();
        //4.通過輸入流獲取到客戶端傳遞的數據
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String line = null;
        while ((line = bufferedReader.readLine())!=null){
            System.out.println(line);
        }

        //5.關閉流
        socket.close();
        serverSocket.close();
    }
}

首先創建一個服務端Socket并明確端口號,通過accept()方法獲取到鏈接過來的客戶端Socket,從客戶端Socket中獲取輸入流,最后由輸入流讀取客戶端傳輸來的數據。
我們來看看服務端的打印結果:

hello server

成功接收到客戶端發來的數據

注意點:

一個服務端是可以同時和多個客戶端進行通信的,那么它是如何區分不同客戶端呢?從上面代碼我們可以看到,服務端首先通過accept()獲取到客戶端Socket,然后通過客戶端的Socket獲取的流進行通訊,這也讓服務端得以區分每個客戶端。

3.2 文件傳輸實例

流程:客戶端上傳一個文件到服務端,服務端收到文件后進行保存,保存成功后給客戶端一個響應。
客戶端代碼

public class TCPUploadClient {

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

        //1.創建TCP客戶端Socket服務
        Socket client = new Socket();

        //2.與服務端進行連接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10001);
        client.connect(address);

        //3.讀取客戶端文件
        FileInputStream fis = new FileInputStream("E://girl.jpg");

        //4.獲取輸出流
        OutputStream outputStream = client.getOutputStream();

        //5.將文件寫入到服務端
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes))!=-1){
            outputStream.write(bytes,0,len);
        }

        //6.通知服務器數據寫入完畢
        client.shutdownOutput();

        //7.讀取服務端響應的數據
        InputStream inputStream = client.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String line = br.readLine();
        System.out.println(line);

        //8.關閉流
        inputStream.close();
        fis.close();
        client.close();
    }
}

創建Socket并連接,讀取本地文件輸入流,將文件寫入到服務端,寫入成功后讀取服務端返回的數據。

服務端代碼


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

        //1.創建客戶端Socket
        ServerSocket serverSocket = new ServerSocket(10001);
        
        //2.獲取到客戶端Socket
        Socket socket = serverSocket.accept();
        
        //3.通過客戶端Socket獲取到輸入流
        InputStream is = socket.getInputStream();
        
        //4.將流以文件的形式寫入到磁盤
        File dir = new File("F://tcp");
        //如果文件夾不存在就創建文件夾
        if(!dir.exists()){
            dir.mkdirs();
        }
        File file = new File(dir,"girl.jpg");
        FileOutputStream fos = new FileOutputStream(file);
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = is.read(bytes))!=-1){
            fos.write(bytes,0,len);
        }
        
        //5.通知客戶端文件保存完畢
        OutputStream os = socket.getOutputStream();
        os.write("success".getBytes());
        
        //6.關閉流
        fos.close();
        os.close();
        socket.close();
    }
}

創建Socket并獲取到客戶端Socket和輸入流,在F盤的tcp目錄下創建一個文件,將從客戶端讀取到的數據輸出到文件中,保存成功后給客戶端返回''success''
效果


gil

客戶端控制臺

success

這樣我們就實現了一哥客戶端上傳文件的功能,還是比較簡單的,希望大家也能夠跟著代碼敲一遍。

3.3 TCP Socket多線程應用

細心的同學可能已經發現,上面我們的實例中一個服務端只能接收一個客戶端的一次請求,這顯然是不符合實際的,那么如何使一個服務端同時服務多個客戶端呢?接著擼代碼

//客戶端1
public class TCPClient1 {
    public static void main(String[] args) throws IOException {
        System.out.println("TCPClient1 Start...");
        //1.創建TCP客戶端Socket服務
        Socket client = new Socket();

        //2.與服務端進行連接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
        client.connect(address);

        //3.連接成功后獲取客戶端Socket輸出流
        OutputStream outputStream = client.getOutputStream();

        //4.通過輸出流往服務端寫入數據
        outputStream.write("Hello my name is Client1".getBytes());

        //5.告訴服務端發送完畢
        client.shutdownOutput();

        //6.讀取服務端返回數據
        InputStream is = client.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));

        //7.關閉流
        client.close();
    }
}

//客戶端2
public class TCPClient2 {

    public static void main(String[] args) throws IOException {
        System.out.println("TCPClient2 Start...");

        //1.創建TCP客戶端Socket服務
        Socket client = new Socket();

        //2.與服務端進行連接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
        client.connect(address);

        //3.連接成功后獲取客戶端Socket輸出流
        OutputStream outputStream = client.getOutputStream();

        //4.通過輸出流往服務端寫入數據
        outputStream.write("Hello my name is Client2".getBytes());

        //5.告訴服務端發送完畢
        client.shutdownOutput();

        //6.讀取服務端返回數據
        InputStream is = client.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));

        //7.關閉流
        client.close();
    }
}
//服務端
public class TCPServer {
    public static void main(String[] args) throws IOException {
        receive();
    }

    private static void receive() throws IOException {
        System.out.println("Server Start...");
        //創建服務端Socket并明確端口號
        ServerSocket serverSocket = new ServerSocket(10004);

        while (true){
            //獲取到客戶端的Socket
            Socket socket = serverSocket.accept();
            //通過客戶端的Socket獲取到輸入流
            InputStream is = socket.getInputStream();

            //通過輸入流獲取到客戶端傳遞的數據
            byte[] bytes = new byte[1024];
            int len = is.read(bytes);
            System.out.println(new String(bytes,0,len));

            //將客戶端發來的數據原封不動返回
            OutputStream os = socket.getOutputStream();
            os.write(new String(bytes,0,len).getBytes());
            //關閉連接
            socket.close();
        }
    }
}

客戶端1控制臺

TCPClient1 Start...
Hello my name is Client1

客戶端2控制臺

TCPClient2 Start...
Hello my name is Client2

這樣就可以實現一個服務端服務多個客戶端

細心的同學可能又發現了,上面的寫法是存在問題的,由于服務端始終都在主線程中處理請求,所以客戶端的請求需要被服務端排隊處理,舉個例子:Client1對服務端進行了一次請求,服務端在響應Client1之前是不會接受其他請求的,顯然這是不符合邏輯的,真正的服務器是要具備并發處理的。而多線程恰好能解決這個問題,我們來看修改之后的服務端代碼

public class TCPServer {
    public static void main(String[] args) throws IOException {
        receive();
    }

    private static void receive() throws IOException {
        System.out.println("Server Start...");

        //創建服務端Socket并明確端口號
        ServerSocket serverSocket = new ServerSocket(10004);
        while (true){
            //獲取到客戶端的Socket
            final Socket socket = serverSocket.accept();
            //通過線程執行客戶端請求
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //通過客戶端的Socket獲取到輸入流
                        InputStream is = socket.getInputStream();

                        //通過輸入流獲取到客戶端傳遞的數據
                        byte[] bytes = new byte[1024];
                        int len = is.read(bytes);
                        System.out.println(new String(bytes,0,len));

                        //將客戶端發來的數據原封不動返回
                        OutputStream os = socket.getOutputStream();
                        os.write(new String(bytes,0,len).getBytes());
                        //關閉連接
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }
}

運行效果適合加線程之前是一樣的,但這種方式效率更高。

總結

本篇文章敘述了基于UDP和TCP的Socket,UDP是無連接的,所以UDP Socket在發送數據的時候只需要目標IP和端口即可發送。TCP是面向連接的并且是基于client-server模式,在傳輸數據前需要進行連接,可以通過多線程技術實現并發處理客戶端請求。本篇文章內容還是比較簡單的,希望大家能把文章中代碼自己敲一遍,掌握Socket的同時也能讓你自己UDP和TCP的理解更加深刻。本篇文章對Socket的描述到此為止,下篇文章《Android網絡編程(三)HTTP、HTTPS》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 計算機網絡概述 網絡編程的實質就是兩個(或多個)設備(例如計算機)之間的數據傳輸。 按照計算機網絡的定義,通過一定...
    蛋炒飯_By閱讀 1,243評論 0 10
  • 網絡編程 一.楔子 你現在已經學會了寫python代碼,假如你寫了兩個python文件a.py和b.py,分別去運...
    go以恒閱讀 2,071評論 0 6
  • 網絡編程 網絡編程對于很多的初學者來說,都是很向往的一種編程技能,但是很多的初學者卻因為很長一段時間無法進入網絡編...
    程序員歐陽閱讀 2,041評論 1 37
  • 1、TCP為什么需要3次握手,4次斷開? “三次握手”的目的是“為了防止已失效的連接請求報文段突然又傳送到了服務端...
    杰倫哎呦哎呦閱讀 3,528評論 0 6
  • 其一 天暮寒云低,簾動北風凄。 幽林殘樹色,梧蒼霜葉稀。 亂山鳥空啼,愁腸入澗溪。 獨憐秋花落,入土化春泥。 其二...
    張慶軍看世界閱讀 889評論 0 15