Java網絡編程(二)Socket用法淺學

自己總結的,難免有錯,歡迎指出,在下感謝不盡!

第二章 Socket用法淺學

前言

Java網絡程序致力於實現應用層,傳輸層嚮應用層提供了Socket(套接字)類,Socket封裝了下層的數據傳輸細節,應用層的程序通過Socket來建立和遠程主機的連接,以及進行數據的傳輸。

Socket類的種類和應用:

使用的協議 Socket類 源主機 遠程主機 誰先啓動
UDP協議 DatagramSocket 發送端 接收端 java.net 都行
TCP協議 Socket、ServerSocket 客戶端 服務器端 java.net 服務器

基於以上,Java網絡編程又叫Socket編程

2.1 IP地址的封裝對象InetAddress

2.1.1、InetAddress

類名 父類 子類 使用的協議 實現接口(都爲序列化)
InetAddress Object Inet4Address、Inet6Address 這個類表示一個互聯網協議(IP)地址。 Serializable
public final class Inet4Address InetAddress 這個類表示一個互聯網協議版本4 (IPv4)地址 Serializable
public final class Inet6Address InetAddress 這個類表示一個互聯網協議版本6 (IPv6)地址 Serializable

InetAddress類表示服務器的IP地址,InetAddress提供了一系列的靜態工廠方法用於返回實例

一個InetAddress實例由一個IP地址和可能的對應的主機名構成(取決於它是否是一個主機名或是否已經做了反向主機名解析構造)。

靜態工廠方法

1、常用靜態工廠方法

  • 1.1、static InetAddress getByName(String host)
    如 String host=“www.baidu.com”,或者host=“192.168.1.1” 確定主機的IP地址,給定主機名。
  • 1.2、static InetAddress[] getAllByName(String host)
    如新浪的是集羣服務器 ,“www.sina.com"或者"111.111.111.1"什麼的
    給定一個主機的名稱,返回其IP地址的數組,基於系統上的配置的名稱服務。
    有一些主機集羣了,使得其ip有很多。其中的addr爲一個字節數組 ,
    參數按網絡字節順序:地址的高位字節位於 getAddress()[0] 中,
    IPv4 地址 byte 數組的長度必須爲 4 個字節,IPv6 byte 數組的長度必須爲 16 個字節。
  • 1.3、static InetAddress getByAddress(String host, byte[] addr)
    它創造了一個基於所提供的主機名和IP地址。
  • 1.4、static InetAddress getByAddress(byte[] addr)
    返回給定的原始IP地址 InetAddress對象。
  • 1.5、static InetAddress getLocalHost()
    返回本地主機的地址。 是IP地址對象

常用方法:

2、常用的獲取方法

  • 2.1、String getHostAddress()
    返回文本表示中的IP地址字符串。 主機IP地址的字符串形式
  • 2.2、String getHostName()
    獲取此IP地址的主機名。 主機名字符串
  • 2.3、public String getCanonicalHostName()
    獲取此IP地址的完全限定的域名。最好的工作方法,這意味着我們可能無法返回FQDN取決於底層的系統配置。
  • 2.4、boolean isReachable(int timeout) 方法
    測試是否可以達到該地址
  • 2.5、toString() 方法
    將此 IP 地址轉換爲 String

Demo

public class Practice1_InetAddress {
    public static void main(String[] args) throws IOException {
        getInstance1(getInstance3());
        getInstance2();
    }

    /**
     * 使用靜態工廠方法1構造一個IP對象,
     * static InetAddress getByName(String host)
     * @return byte[] 返回ip地址的byte數組。
     */
    private static byte[] getInstance3() throws IOException {
        System.out.println("getInstance1_getByName");
        InetAddress ip = InetAddress.getByName("www.qq.com");//可以是域名形式www.qq.com 主機IP地址:112.53.27.11

        //注意,這裏使用的什麼形式構造ip對象,則getHostName就返回什麼。

        //獲取其IP地址和對應的主機名
        String str_ip = ip.getHostAddress();//對應Ip網址的主機IP地址,主機IP地址:112.53.27.11
        String str_host = ip.getHostName();//對應的主機名www.qq.com
        System.out.println("主機名:" + str_host + " 主機IP地址:" + str_ip);
        System.out.println(ip.getCanonicalHostName());//獲取此 IP地址的完全限定域名 112.53.27.11


        boolean reachable=ip.isReachable(2000);//獲取布爾類型,看是否能到達此IP地址
        System.out.println("是否能達到該IP地址:"+reachable);
        byte[] b;
        b = ip.getAddress();
        for(byte f:b){
            System.out.print(f+" ");//[B@735b478 [B@735b478 [B@735b478 [B@735b478
        }
        System.out.println();
        return b;

    }

    /**
     * 演示靜態方法static InetAddress getLocalHost()
     * 返回本地主機的地址。  是IP地址對象
     */
    private static void getInstance2() throws UnknownHostException {
        System.out.println("getInstance2_getLocalHost");
        InetAddress host = InetAddress.getLocalHost();
        String ip = host.getHostAddress();
        String name = host.getHostName();
        System.out.println("name:"+name+" ip:"+ip);//name:滾被單fhg ip:192.158.13.1
    }

    /**
     * 演示靜態工廠方法static InetAddress getByAddress(byte[] addr)
     *返回給定的原始IP地址 InetAddress對象。
     * @param bytes 傳入的IP地址的byte數組。
     * @throws IOException 可能拋出的異常
     */
    private static void getInstance1(byte[]bytes) throws IOException {
        System.out.println("getInstance3_getByAddress");
        InetAddress ip = InetAddress.getByAddress(bytes);
        //注意,這裏使用的什麼形式構造ip對象,則getHostName就返回什麼。

        //獲取其IP地址和對應的主機名
        String str_ip = ip.getHostAddress();//對應Ip網址的主機IP地址,主機IP地址:112.53.27.11
        String str_host = ip.getHostName();//對應的主機名www.qq.com
        System.out.println("主機名:" + str_host + " 主機IP地址:" + str_ip);
        System.out.println(ip.getCanonicalHostName());//獲取此 IP地址的完全限定域名 112.53.27.11


        boolean reachable=ip.isReachable(2000);//獲取布爾類型,看是否能到達此IP地址
        System.out.println("是否能達到該IP地址:"+reachable);

    }
}

2.1.2、抽象類SocketAddress

這個類表示沒有協議附件的套接字地址。作爲一個抽象類,它意味着要用特定的、依賴協議的實現子類化。它提供了套接字用於綁定、連接或作爲返回值的不可變對象。

父類 子類 協議 實現接口
public abstract class SocketAddress Object InetSocketAddress 這個類表示沒有協議附件的套接字地址。 Serializable
InetSocketAddress SocketAddress IP協議 Serializable

InetSocketAddress類

這個類實現了一個IP套接字地址(IP地址+端口號),它也可以是一對(主機名+端口號),在這種情況下,將嘗試解析主機名。如果解析失敗,那麼該地址被認爲是無法解析的,但是在某些情況下仍然可以使用,比如通過代理進行連接。
它提供了套接字用於綁定、連接或作爲返回值的不可變對象。
通配符是一個特殊的本地IP地址。它通常表示“any”,只能用於綁定操作。

構造器:

類別 描述
InetSocketAddress​(int port) 創建一個套接字地址,其中IP地址爲通配符地址,端口號爲指定值。
InetSocketAddress​(String hostname, int port) 從主機名和端口號創建套接字地址。
InetSocketAddress​(InetAddress addr, int port) 根據IP地址和端口號創建套接字地址。

常用方法:

  • 1.1、static InetSocketAddress createUnresolved​(String host, int port)
    從主機名和端口號創建一個未解析的套接字地址。
  • 1.2、InetAddress getAddress()
    獲取地址
    1.3、String getHostName()
    獲取主機名。
    1.4、String getHostString()
    返回主機名,如果沒有主機名,則返回地址的字符串形式(它是使用文字創建的)。
  • 1.5、int getPort()
    獲取端口號
    1.6 int hashCode()
    Returns a hashcode for this socket address.
  • 1.7、boolean isUnresolved()
    檢查地址是否已被解析。
  • 1.8、String toString()
    構造這個InetSocketAddress的字符串表示形式。

2.2 UDP協議及DatagramSocket類

2.2.1、UDP協議

在TCP/IP參考模型中,傳輸層的協議可使用TCP協議或者UDP協議

UDP協議
UDP是一個無連接協議,傳輸數據之前源端和終端不建立連接,當它想傳送時就簡單地去抓取來自應用程序的數據,並儘可能快地把它扔到網絡上。在發送端,UDP傳送數據的速度僅僅是受應用程序生成數據的速度、計算機的能力和傳輸帶寬的限制;在接收端,UDP把每個消息段放在隊列中,應用程序每次從隊列中讀一個消息段。
UDP 是一個簡單的傳輸層協議。和 TCP 相比,UDP 有下面幾個顯著特性:

  • UDP 缺乏可靠性。UDP 本身不提供確認,序列號,超時重傳等機制。UDP 數據報可能在網絡中被複制,被重新排序。即 UDP 不保證數據報會到達其最終目的地,也不保證各個數據報的先後順序,也不保證每個數據報只到達一次
  • UDP 數據報是有長度的。每個 UDP 數據報都有長度,如果一個數據報正確地到達目的地,那麼該數據報的長度將隨數據一起傳遞給接收方。而 TCP 是一個字節流協議,沒有任何(協議上的)記錄邊界。
  • UDP 是無連接的。UDP 客戶和服務器之前不必存在長期的關係。UDP 發送數據報之前也不需要經過握手創建連接的過程。
  • UDP 支持多播和廣播。
  • UDP信息包的標題很短,只有8個字節,相對於TCP的20個字節信息包而言UDP的額外開銷很小。

應用場景
在選擇UDP作爲傳輸協議時必須要謹慎。在網絡質量令人十分不滿意的環境下,UDP協議數據包丟失會比較嚴重。但是由於UDP的特性:它不屬於連接型協議,因而具有資源消耗小,處理速度快的優點,所以通常音頻、視頻和普通數據在傳送時使用UDP較多,因爲它們即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。比如我們聊天用的ICQ和QQ就是使用的UDP協議。如果使用TCP傳輸實時音頻,由於速度慢 ,會導致接收方時常出現停頓,聲音斷斷續續,這是無法容忍的。由此,UDP更加比TCP適於傳輸實時音頻。
在這裏插入圖片描述

2.2.2、DatagramSocket類用法

public class DatagramSocket extends Object implements Closeable

描述
這類代表一個發送和接收數據包的插座。 數據報套接字發送或者接收點的分組傳送服務。每個發送的數據包或數據報套接字上接收單獨尋址和路由。從一臺機器發送到另一臺機器的多個數據包可能會被不同的路由,並可以以任何順序到達。
在可能的情況下,一個新建的DatagramSocket有SO_BROADCAST套接字選項已啓用,以便允許廣播數據報傳輸。爲了收到廣播包應該將DatagramSocket綁定到通配符地址。在一些實施方案中,廣播包,也可以接受當一個DatagramSocket綁定到一個更具體的地址。
構造器

類型 描述
DatagramSocket() 構建一個數據報套接字綁定到本地主機的任何可用的端口。
protected DatagramSocket(DatagramSocketImpl impl) 創建一個綁定的數據報套接字與指定的datagramsocketimpl。
DatagramSocket(int port)《常用》 構建一個數據報套接字綁定到本地主機的指定端口。
DatagramSocket(int port, InetAddress laddr) 《常用》 創建一個數據報套接字,綁定到指定的本地地址。
DatagramSocket(SocketAddress bindaddr) 創建一個數據報套接字,綁定到指定的本地套接字地址。

常用方法

1、 判斷

  • 1.1 boolean isBound()
    返回套接字的綁定狀態。
  • 1.2 boolean isClosed()
    返回套接字是否關閉或不關閉的。
  • 1.3 boolean isConnected()
    返回套接字的連接狀態。

2、發送接受數據報

  • 2.1 void receive(DatagramPacket p)
    接收數據報包從這個插座。
  • 2.2 void send(DatagramPacket p)
    從這個套接字發送數據報包。

3、 獲取端口和IP地址

  • 3.1 InetAddress getInetAddress()
    返回此套接字連接的地址。
  • 3.2 InetAddress getLocalAddress()
    獲取綁定的套接字的本地地址。
  • 3.3 int getLocalPort()
    返回此套接字綁定的本地主機上的端口號。
  • 3.4 int getPort()
    返回此套接字連接的端口號。
  • 3.5 SocketAddress getLocalSocketAddress()
    返回此套接字綁定到的端點的地址。

4 連接和關閉

  • 4.1 void close()
    關閉該數據報套接字。
  • 4.2 void connect(InetAddress address, int port)
    將套接字連接到這個套接字的遠程地址。
  • 4.3 void connect(SocketAddress addr)
    將此套接字連接到遠程套接字地址(IP地址+端口號)。
  • 4.4 void disconnect()
    斷開插座。

2.2.3、數據報DatagramPacket類

描述
UDP的數據都是以數據報包來發送的,大小限制在64K以內。

  • IPv4的數據報包理論上最大長度65507字節(約64K)
  • IPv6的數據報包理論上最大長度65536字節(64K)
  • 許多基於UDP的協議都規定數據報的長度,如DNS:不超過512字節;TFTP:不超過512字節;NFS:最大爲8KB;多於數據報的最大長度,網絡會將數據截斷、分片、或丟棄部分數據。而這種情況java程序是收不到任何通知的!
    在這裏插入圖片描述
    數據報包封裝爲DatagramPacket類:
 public final class DatagramPacket extends Object
  • 這類表示一個數據報包。一個完整的UDP數據報包包括IP頭、UDP頭和數據3部分。UDP頭部分包括了源端口、目標端口、UDP長度和檢驗和,數據包的長度就是指數據這部分的長度。
  • 數據包是用來實現一個無連接的分組傳送服務。每個消息都是從一臺機器路由到另一個完全基於包含在該數據包內的信息。
  • 從一臺機器發送到另一臺機器的多個數據包可能會被不同的路由,並可能以任何順序到達。包交付沒有保證。

構造器
構造用於接收數據的對象

  • public DatagramPacket(byte[]data,int length);
    data是用來存放數據的數組,length指定接受的字節長度。
  • public DatagramPacket(byte[]data,int offset,int length);
    同上,offset指定數據從哪個角標開始存放,data[offset]。

構造用於發送數據的對象

  • public DatagramPacket(byte[]data,int offset,int length,InetAddress address,int port);
    要指明要發送的主機IP地址和端口
  • public DatagramPacket(byte[]data,int offset,int length,SocketAddress address);
    要指明要發送的address,SocketAddress類型

常用方法:

InetAddress getAddress()
返回的IP地址的機器,這個數據包被髮送或從收到的數據報。

  • byte[] getData()
    返回數據緩衝區。
  • int getLength()
    返回要發送的數據的長度或收到的數據的長度。
  • int getOffset()
    返回所要發送的數據的偏移量或所接收的數據的偏移量。
  • int getPort()
    返回遠端端口號,這個數據包被髮送或從收到的數據報。
  • SocketAddress getSocketAddress()
    獲取SocketAddress(通常是IP地址+端口號)的遠程主機,數據包被髮送到或來自。
  • void setAddress(InetAddress iaddr)
    設置本機的IP地址,這個數據包被髮送。
  • void setData(byte[] buf)
    爲這個數據包設置數據緩衝區。
  • void setData(byte[] buf, int offset, int length)
    爲這個數據包設置數據緩衝區。
  • void setLength(int length)
    爲這個數據包設置長度。
  • void setPort(int iport)
    設置遠端端口號,這個數據包被髮送。
  • void setSocketAddress(SocketAddress address)
    集SocketAddress(通常是IP地址+端口號)的遠程主機的數據報發送。

2.2.4、使用DatagramSocket實現簡單信息交換

需求:使用UDP,發送端發送信息(鍵盤錄入)給接收端,接收端將信息打出來。

public class Practice2_DatagramSocket_UDPSend {
    private DatagramSocket UdpClient;//服務端套接字

    /**
     * 有參構造器,UDP發送端初始化
     * @param port 發送端綁定的本地端口
     */
    public Practice2_DatagramSocket_UDPSend(int port) throws IOException {
        System.out.println("UDP發送端初始化,端口爲"+port);
        this.UdpClient = new DatagramSocket();//發送端不用指定端口
        System.out.println("端口綁定完畢!發送端啓動....");
        System.out.println("請輸入你要發送的信息:");
        //sendDate(port);
    }
    public void sendDate(int port) throws IOException {
        //讀取鍵盤錄入信息,發送到接收端
        //1.需要鍵盤錄入信息並存儲到一個緩衝區
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String line;//=null
        //2.將信息封裝爲數據包——DatagramPacket
        //發送數據封裝爲數據包
        DatagramPacket dataPacket;//=null;
        while((line=reader.readLine())!=null){
            byte[]bytes = line.getBytes();
            dataPacket = new DatagramPacket(bytes,bytes.length,
                    InetAddress.getByName("192.168.137.1"),port);//從主機的port端口發送數據包
            //x.x.x.255是廣播ip,此時x.x.x.2發信息到廣播IP,則x.x.x.0~x.x.x.255的IP都能收到,這就是廣播。
            //3.發送數據包
            UdpClient.send(dataPacket);
            System.out.println("send successfully:"+line);
            if(line.equals("bye")){
                break;
            }
        }
        //5.關閉DatagramSocket流
        UdpClient.close();
        System.out.println("發送端退出!");
    }
    public static void main(String[]args) throws IOException {
        int port = 11010;
        new Practice2_DatagramSocket_UDPSend(port).sendDate(port);
    }
    
}

發送端

 /**
 * 接收端,接受UDP發送過過來的信息,並打印出來
 *
 */

public class Practice2_DatagramSocket_UDPReceive {
    private DatagramSocket UdpServer;
    Practice2_DatagramSocket_UDPReceive(int port) throws SocketException {
        System.out.println("接收端啓動!接收端的端口號爲:"+port);
        UdpServer = new DatagramSocket(port);

        //receiveData();

    }

    public void receiveData() {
        while(true){
            try{
                DatagramPacket data;
                byte[]bytes = new byte[1024];
                if(UdpServer!=null){
                    data = new DatagramPacket(bytes,bytes.length);
                    UdpServer.receive(data);//這個是阻塞式方法,與read()類似。
                    int port = data.getPort();
                    String hostName = data.getAddress().getHostName();//獲取發送端的主機名
                    System.out.println("收到發送端信息!");

                    System.out.println("端口"+port+" 主機名:"+hostName+"\n消息爲:"
                            + new String(data.getData(),0,data.getLength()));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[]args) throws IOException {
        new Practice2_DatagramSocket_UDPReceive(11010).receiveData();//10010被佔用則報錯!

    }
}

運行結果
發送端
在這裏插入圖片描述
接收端:
在這裏插入圖片描述

2.3 TCP協議及Socket、ServerSocket

2.3.1、TCP協議

  • TCP 提供一種面向連接的、可靠的字節流服務
  • 在一個 TCP 連接中,僅有兩方進行彼此通信。廣播和多播不能用於 TCP
  • TCP 使用校驗和,確認和重傳機制來保證可靠傳輸
  • TCP 給數據分節進行排序,並使用累積確認保證數據的順序不變和非重複
  • TCP 使用滑動窗口機制來實現流量控制,通過動態改變窗口的大小進行擁塞控制

注意:TCP 並不能保證數據一定會被對方接收到,因爲這是不可能的。TCP 能夠做到的是,如果有可能,就把數據遞送到接收方,否則就(通過放棄重傳並且中斷連接這一手段)通知用戶。因此準確說 TCP 也不是 100% 可靠的協議,它所能提供的是數據的可靠遞送或故障的可靠通知。

由於暫時還不能深入瞭解TCP協議,這裏就插一個鏈接TCP (傳輸控制協議)

2.3.2、Socket類創建客戶端

描述

 public class Socket
  extends Object implements Closeable

這個類實現了客戶端套接字(也被稱爲“套接字”)。套接字是兩臺機器之間的通信的一個端點。
套接字的實際工作是由該類的一個實例進行SocketImpl。一個應用程序,通過改變創建套接字實現的套接字工廠,可以配置自己創建適合本地防火牆的套接字。
1、Socket類的構造器

  • 1、構造器:
  • 1.1、Socket(String host, int port)
  • 創建一個流套接字,並將其與指定的主機上的指定端口號連接起來。
  • 1.2、Socket()
  • 創建一個連接的套接字,與socketimpl系統默認的類型。
  • 1.3、 Socket(InetAddress address, int port)
  • 創建一個流套接字,並將其與指定的IP地址中的指定端口號連接起來。
  • 1.4、Socket(Proxy proxy)
  • 創建一個連接的套接字類型,指定代理,如果有,應該使用無論任何其他設置。
  • 1.5、protected Socket(SocketImpl impl)
  • 創建一個用戶指定的socketimpl連接插座。
  • 1.6、Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
    創建一個指定IP和端口的套接字,並將其與指定的遠程端口上的指定的遠程地址連接起來。
  • 1.7、Socket(String host, int port, InetAddress localAddr, int localPort)
    創建一個指定IP和端口套接字,並將其連接到指定的遠程端口上的指定的遠程主機上。

2、如何發送數據?
Socket類和ServerSocket類它們的數據是可以說是在一個IO流裏邊傳輸。

  • 2.1、InputStream getInputStream()
    返回此套接字的輸入流。
  • 2.2、OutputStream getOutputStream()
    返回此套接字的輸出流

此時我們就將發送接受數據迴歸到了操作IO流裏邊了。
3、Socket如何獲取IP、端口等信息

獲取本地IP、遠程IP、本地輸入流、輸出流、關閉Socket流

  • 3.1、InetAddress getInetAddress()
    返回套接字連接的地址。
  • 3.2、InetAddress getLocalAddress()
    獲取綁定的套接字的本地地址。
  • 3.3、void close()
    關閉這個套接字。

獲取本地端口、連接的遠程端口

  • 3.4、int getPort()
    返回此套接字連接的遠程端口號。
  • 3.5、int getLocalPort()
    返回此套接字綁定的本地端口號。
  • 3.6、void connect(SocketAddress endpoint)
    將此套接字連接到服務器。
  • 3.7 void connect(SocketAddress endpoint, int timeout)
    將此套接字與指定的超時值連接到服務器。
  • 3.8 void bind(SocketAddress bindpoint)
    將套接字綁定到本地地址。

4、、客戶端的IP地址如何設置
在一個Socket對象中,既包含遠程服務器的IP地址和端口信息,又包含本地客戶的IP地址和端口信息。默認情況客戶端的ip地址來自客戶端的主機,端口隨機分配。

我們顯示設置客戶端的IP地址和端口可使用Socket構造方法1.6、1.7。

如果一個主機同屬於兩個以上的網絡,它就可能擁有多個IP地址。如,一個主機在Internet網絡的IP地址是222.43.1.34,在一個局域網的IP地址是“112.5.4.5”,假定該主機上的客戶程序希望和同一個局域網的一個服務器程序(112.5.4.45:8000)通信,則可如下構建客戶端Socket對象:

InetAddress remoteAddr = InetAddress.getByName("112.5.4.45");
InetAddress localAddr = InetAddress.getByName("112.5.4.5");
Socket socket = new Socket(remoteAddr,8000,localAddr,2345);//2345表示客戶端的端口。

5、設置等待建立連接的超時時間
因爲TCP協議的原因,客戶端的Socket構造方法請求和服務器連接的時候,可能要等待一段時間,默認情況下,阻塞狀態,一直等,直到連接成功或者拋出異常。Socket連接時毫無疑問受網速的影響,可能會長時間處於等待狀態,這個時候我們可設置客戶端嘗試連接的時間。

Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);//服務器是本地主機,端口8000
socket.connect(remoteAddr,60000);//等到連接的超時時間設置爲1分鐘,60000ms

一分鐘內連接到了服務器,則connect成功返回;若1分鐘內出現了某種異常則拋出該異常,若1分鐘後都沒連接上去,則拋出SocketTimeoutException。時間如果設置爲0,則永遠不會超時。

6、實例之客戶端創建
需求:向服務器發送請求,服務器響應併發送反饋信息。

public class Practice1_Socket_Client {
    private Socket client;

    /**
     * 1、Socket(String host, int port)
     * 創建一個流套接字,並將其與指定的主機上的指定端口號連接起來。
     * 輸入bye退出連接,輸入send發送請求。
     *
     * @param port 傳入的端口號——服務器端口
     * @param host 指定主機——服務器主機
     */
    public Practice1_Socket_Client(String host, int port) throws IOException {
        System.out.println("客戶端啓動...");
        client = new Socket(host, port);
    }

    public void talk() throws IOException {//向服務端發送請求。得到的響應封裝爲流返回。
        OutputStream out = client.getOutputStream();
        BufferedWriter bufr = new BufferedWriter(new OutputStreamWriter(out));
        String mess = "我來了";
        //out.write(mess.getBytes());//非阻塞方法,因爲直接寫字節。如改爲BufferedWriter,不加刷新發送不出去。
        bufr.write(mess);
        //################
        // 不加刷新,這個方法只是把mess寫到了out裏邊,沒有發送出去。進而阻塞了服務器那邊的讀取,
        //服務器一直在讀 in.read(bytes);這個方法會阻塞。
        bufr.flush();

        //接受服務器的應答,實現客戶和服務器的交互

        InputStream in = client.getInputStream();

        byte[]bytes = new byte[1024];
        int len = in.read(bytes);//同理,如果服務器沒有發送數據,這個方法也會阻塞。
        System.out.println(new String(bytes,0,len));

        client.close();

    }

    public static void main(String[] args) throws IOException {
        Practice1_Socket_Client client = new Practice1_Socket_Client("localhost", 9000);
        client.talk();
    }
}

2.3.3、ServerSocket類創建服務器端

描述

public class ServerSocket
extends Object implements Closeable

這個類實現了服務器套接字。服務器套接字等待來自網絡的請求。它基於該請求執行某些操作,然後可能向請求者返回結果。 服務器套接字的實際工作由SocketImpl類的一個實例進行。一個應用程序可以更改創建套接字實現的套接字工廠,以配置自己創建適合本地防火牆的套接字。

1、ServerSocket的構造器

  • ServerSocket()
    創建一個綁定服務器套接字。
  • ServerSocket(int port)
    創建一個服務器套接字,綁定到指定的端口。
  • ServerSocket(int port, int backlog)
    創建一個服務器套接字,並將其綁定到指定的本地端口號,並使用指定的積壓。
  • ServerSocket(int port, int backlog, InetAddress bindAddr)
    用指定的端口創建一個服務器,聽積壓,和本地IP地址綁定到。

port是服務器要綁定的端口號,backlog是客戶連接請求隊列的長度、bindAddr指定服務器所綁定的ip地址。

2、如何監聽客戶端的請求
方法:
ServerSocket.accept();從連接請求的隊列裏邊取出一個客戶的請求連接,然後創建與該客戶連接的Socket對象,並將它返回。如果沒有客戶連接請求,則該方法會一直等待(阻塞式方法)。直到收到連接請求才能返回。
3、收發數據
同客戶端。使用Socket的方法:

  • Socket.getInputStream()
  • Socket.getOutputStream()
    這樣,通過流建立了傳輸通道,就能與客戶進行數據的交互!注意!當服務器正在進行發送數據的操作,如果客戶端斷開連接,服務器會拋出一個IOException的子類SocketException異常:
java.net.SocketException:Connection reset by peer

這是服務器與單個客戶通信發送的異常,應該要捕抓,使得服務器繼續和其他的客戶端通信。

3、實例之服務器端的創建
需求:向服務器發送請求,服務器響應併發送反饋信息。

/**
 * TCP之服務器端
 * 使用ServerSocket類
 */

public class Practice1_ServerSocket_Server {
    private ServerSocket serverSocket;

    public Practice1_ServerSocket_Server(int port) throws IOException {
        System.out.println("服務器啓動!");
        serverSocket = new ServerSocket(port);
    }
    public void receiveServer(){
        while (true) {//服務器一般是不斷進行監聽的,有客戶請求,就做出響應。
            Socket socket = null;
            try{
                socket = serverSocket.accept();//等待客戶端連接。一旦返回socket對象意味着與一個客戶連接。
                System.out.println("New connection accepted" + socket.getInetAddress() +
                        ":" + socket.getPort());
                InputStream in = socket.getInputStream();
                byte[]bytes = new byte[1024];
                in.read(bytes);
                System.out.println(new String(bytes));
                //服務器做出應答:
                OutputStream out = socket.getOutputStream();
                out.write("收到".getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
   }
    public static void main(String[]args) throws IOException {
        Practice1_ServerSocket_Server server = new Practice1_ServerSocket_Server(9000);//必須服務器先啓動。
        server.receiveServer();
    }
}

不同於UDP,TCP傳輸中必須先啓動服務器,而且服務器一般晝夜運行。

運行結果:
1、啓動服務器:
在這裏插入圖片描述
2、啓動客戶端:
立馬收到了服務器的響應:“收到"
在這裏插入圖片描述
服務器收到客戶端的“我來了”請求:
在這裏插入圖片描述

2.3.4、實戰上傳文件(圖片、視頻)服務

需求:創建一個服務器,滿足用戶上傳文件的需求。上傳的文件可以是圖片,文本等。服務器將數據保存到本地。同時服務器告知客戶端上傳成功。
1、創建客戶端

/**
 * 客戶端上傳文件,然後服務器進行上傳成功的響應!
 * 文件類型不限於文本、圖片、音樂。
 *
 */

public class Practice3_TCP_FileUpload {
    public static void main(String[]args) throws IOException {
        System.out.println("客戶端啓動!");
        //1、創建客戶端,要指定服務器的端口,主機地址
        Socket client = new Socket("localhost",10111);//主機爲本機。端口10111

        //2、獲取要上傳的文件,開始上傳
        OutputStream out = client.getOutputStream();

        //用於讀取文件的流
        FileInputStream fileinput = new FileInputStream(new File("E:\\socket1.JPG"));

        //3、開始發送數據
        byte[]bytes = new byte[1024*1024];//分配1MB大小的內存
        int len=0;
        while((len=fileinput.read(bytes))!=-1){
            out.write(bytes,0,len);//使勁的發送到客戶端。字節流不需要刷新?
        }

        //寫完以後要告訴服務器
        client.shutdownOutput();

        InputStream in = client.getInputStream();
        //開始接受服務器的響應
        byte[]re_bytes = new byte[1024];
        while((len=in.read(re_bytes))!=-1){
            System.out.println(new String(re_bytes,0,len));
        }
        //4、注意本地的讀取流需要關閉
        fileinput.close();
        //斷開客戶端和服務器的連接。
        client.close();


    }
}

2、服務器的創建

/**
 * 上傳文件到服務器,服務器告知客戶上傳成功!
 *
 */
public class Practice3_TCP_FileServer {
    public static void main(String[]args) throws IOException {
        System.out.println("服務器啓動!");
        //1、服務器必須明確自己是哪一個端口。
        ServerSocket server = new ServerSocket(10111);

        //2、判斷是否有客戶連接
        Socket client = server.accept();//成功返回socket對象說明有客戶連接。
        System.out.println(client.getInetAddress()+"客戶連接。");
        //3、獲取socket流,進行接受數據並給予反饋
        InputStream in = client.getInputStream();
        OutputStream out = client.getOutputStream();

        //本地寫入流
        FileOutputStream fileOut = new FileOutputStream(new File("E:\\socket_upload.JPG"));

        //4、接受數據、使勁往裏邊寫
        byte[]bytes = new byte[1024*1024];//1M大小的內存
        int len=0;
        while((len=in.read(bytes))!=-1){
            fileOut.write(bytes,0,len);
        }

        //5、接受完畢需要給客戶端發送上傳完畢的反饋。
        out.write("UpLoad successfully!".getBytes());

        fileOut.close();//本地流關
        client.close();//客戶端關
        //服務器一般不關
    }
}

選擇的文件:
在這裏插入圖片描述

3、開始上傳
先啓動服務器:
在這裏插入圖片描述
啓動客戶端開始上傳文件:
由於上傳太快,一運行就收到上傳成功。
在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

2.3.5、客戶連接服務器容易出現的細節問題——線程阻塞狀態

  • read()和readLine()等阻塞式方法,在接受客戶端或者服務器等的發送的數據,一定要注意是否能終止,而不出現服務器和客戶端都在等待的現象
  • 使用字符流來發送和接受數據時,是否添加了刷新?有些字節流不需要刷新。如果要刷新的不加刷新,會導致阻塞,進而出現服務器和客戶端都在等待的現象,此時數據發送接受異常
  • 注意什麼時候使用socket.shutdownOutput(),socket.shutDownInput()

阻塞通信(我們必須避免
1、客戶端
在這裏插入圖片描述
2、服務器端
在這裏插入圖片描述
在這裏插入圖片描述
我們要非常注意這個阻塞狀態的發送,它可能導致程序無法達到我們預期的功能。

我們要做到非阻塞通信!

2.4、深入剖析Socket用法♥

2.4.1 客戶端連接服務器可能拋出的異常

在這裏插入圖片描述

2.4.2 關閉Socket類

毫無疑問,Socket調用到了底層資源,如網卡,端口等。我們不用的時候就該關閉。

socket.close();//儘量將其放在fianlly快裏邊,保證一定被執行。

狀態測試:
在這裏插入圖片描述

2.4.3 半關閉Socket

在這裏插入圖片描述
就是在發送數據或者讀取發過來的數據的時候,告知對方發送完畢,或者接受完畢。進而需要使用一個標識,做法可以是:

  • 使用時間戳標識
  • 使用某個字符串(不建議、因爲文件或者發送消息可能包含該字符串)、
  • 使用內置的方法:shutdownInput()或者shutdownOutput()方法。

2.4.4 設置Socket選項

在這裏插入圖片描述

  • 1、void setTcpNoDelay(boolean on)
    啓用/禁用 TCP_NODELAY(禁用/啓用Nagle的算法)。 在這裏插入圖片描述
  • 2、void setReuseAddress(boolean on)
    啓用/禁用 SO_REUSEADDR套接字選項。
    使用Socket.close()方法關閉Socket,並不會立即釋放該端口,而是等待一段時間,如果此時有數據發送過來,Socket會接受而不作任何處理。防止因立即釋放端口,而導致數據被其他佔用該端口的程序接受在這裏插入圖片描述
  • 3、void setSoTimeout(int timeout)
    啓用/禁用 SO_TIMEOUT以指定的超時時間,以毫秒爲單位。
    我們知道Socket的輸入流讀取數據是阻塞式的(read,readLine),當沒有數據過來,會一直等待。我們可以設置限制的等待時間。(可以和Thread.sleep()搭配觀察效果)在這裏插入圖片描述
  • 4、void setSoLinger(boolean on, int linger)
    啓用/禁用 SO_LINGER與指定的逗留的時間秒。 在這裏插入圖片描述
  • 5、void setSendBufferSize(int size)
    設置這個 Socket指定值的 SO_SNDBUF選項。在這裏插入圖片描述
  • 6、void setReceiveBufferSize(int size)
    集 SO_RCVBUF選項,這 Socket指定值。 在這裏插入圖片描述
  • 7、void setKeepAlive(boolean on)
    啓用/禁用 SO_KEEPALIVE。 在這裏插入圖片描述
  • 8、void setOOBInline(boolean on)
    啓用/禁用 SO_OOBINLINE(TCP緊急數據收據)默認情況下,此選項是禁用TCP套接字上接收緊急數據是默默丟棄。在這裏插入圖片描述
    9、public void setTrafficClass(int TrafficClass)
    設置服務類型 在這裏插入圖片描述
    10、public void setPerformances(int connectionTime,int latency,int bandwidth)
    設置帶寬、連接時間和延遲
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章