Java基於TCP協議的Socket通信

簡介

TCP簡介

TCP(Transmission Control Protocol 傳輸控制協議)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,由IETF的RFC 793定義。在簡化的計算機網絡OSI模型中,它完成第四層傳輸層所指定的功能,用戶數據報協議(UDP,下一篇博客會實現)是同一層內 另一個重要的傳輸協議。在因特網協議族(Internet protocol suite)中,TCP層是位於IP層之上,應用層之下的中間層。不同主機的應用層之間經常需要可靠的、像管道一樣的連接,但是IP層不提供這樣的流機制,而是提供不可靠的包交換。

應用層向TCP層發送用於網間傳輸的、用8位字節表示的數據流,然後TCP把數據流分區成適當長度的報文段(通常受該計算機連接的網絡的數據鏈路層的最大傳輸單元( MTU)的限制)。之後TCP把結果包傳給IP層,由它來通過網絡將包傳送給接收端實體的TCP層。TCP為瞭保證不發生丟包,就給每個包一個序號,同時序號也保證瞭傳送到接收端實體的包的按序接收。然後接收端實體對已成功收到的包發回一個相應的確認(ACK);如果發送端實體在合理的往返時延(RTT)內未收到確認,那麼對應的數據包就被假設為已丟失將會被進行重傳。TCP用一個校驗和函數來檢驗數據是否有錯誤;在發送和接收時都要計算校驗和。

JAVA Socket簡介

所謂socket 通常也稱作”套接字“,用於描述IP地址和端口,是一個通信鏈的句柄。應用程序通常通過”套接字”向網絡發出請求或者應答網絡請求

以J2SDK-1.3為例,Socket和ServerSocket類庫位於java.net包中。ServerSocket用於服務器端,Socket是建立網絡連接時使用的。在連接成功時,應用程序兩端都會產生一個Socket實例,操作這個實例,完成所需的會話。對於一個網絡連接來說,套接字是平等的,並沒有差別,不因為在服務器端或在客戶端而產生不同級別。不管是Socket還是ServerSocket它們的工作都是通過SocketImpl類及其子類完成的。

重要的Socket API:

java.net.Socket繼承於java.lang.Object,有八個構造器,其方法並不多,下面介紹使用最頻繁的三個方法,其它方法大傢可以見JDK-1.3文檔。

  • . Accept方法用於產生”阻塞”,直到接受到一個連接,並且返回一個客戶端的Socket對象實例。”阻塞”是一個術語,它使程序運行暫時”停留”在這個地方,直到一個會話產生,然後程序繼續;通常”阻塞”是由循環產生的。
  • . getInputStream方法獲得網絡連接輸入,同時返回一個InputStream對象實例。
  • . getOutputStream方法連接的另一端將得到輸入,同時返回一個OutputStream對象實例。

註意:其中getInputStream和getOutputStream方法均會產生一個IOException,它必須被捕獲,因為它們返回的流對象,通常都會被另一個流對象使用。

SocketImpl介紹

既然不管是Socket還是ServerSocket它們的工作都是通過SocketImpl類及其子類完成的,那麼當然要介紹啦。

抽象類 SocketImpl 是實際實現套接字的所有類的通用超類。創建客戶端和服務器套接字都可以使用它。

具體JDK見:

https://www.jb51.net/softs/214120.html

由於它是超類具體代碼實現還是見下面的Socket

TCP 編程

構造ServerSocket

具體API見:https://www.jb51.net/article/232094.htm

構造方法:

  • ServerSocket() ~創建非綁定服務器套接字。
  • ServerSocket(int port) ~創建綁定到特定端口的服務器套接字。
  • ServerSocket(int port, int backlog) ~利用指定的 backlog 創建服務器套接字並將其綁定到指定的本地端口號。
  • ServerSocket(int port, int backlog, InetAddress bindAddr) ~使用指定的端口、偵聽 backlog 和要綁定到的本地 IP 地址創建服務器。

1.1 綁定端口

除瞭第一個不帶參數的構造方法以外, 其他構造方法都會使服務器與特定端口綁定, 該端口有參數 port 指定. 例如, 以下代碼創建瞭一個與 80 端口綁定的服務器:

ServerSocket serverSocket = new ServerSocket(80);

如果運行時無法綁定到 80 端口, 以上代碼會拋出 IOException, 更確切地說, 是拋出 BindException, 它是 IOException 的子類. BindException 一般是由以下原因造成的:

  • 端口已經被其他服務器進程占用;
  • 在某些操作系統中, 如果沒有以超級用戶的身份來運行服務器程序, 那麼操作系統不允許服務器綁定到 1-1023 之間的端口.

如果把參數 port 設為 0, 表示由操作系統來為服務器分配一個任意可用的端口. 有操作系統分配的端口也稱為匿名端口. 對於多數服務器, 會使用明確的端口, 而不會使用匿名端口, 因為客戶程序需要事先知道服務器的端口, 才能方便地訪問服務器.

1.2 設定客戶連接請求隊列的長度

當服務器進程運行時, 可能會同時監聽到多個客戶的連接請求. 例如, 每當一個客戶進程執行以下代碼:

Socket socket = new Socket("www.javathinker.org", 80); 

就意味著在遠程 www.javathinker.org 主機的 80 端口上, 監聽到瞭一個客戶的連接請求. 管理客戶連接請求的任務是由操作系統來完成的. 操作系統把這些連接請求存儲在一個先進先出的隊列中. 許多操作系統限定瞭隊列的最大長度, 一般為 50 . 當隊列中的連接請求達到瞭隊列的最大容量時, 服務器進程所在的主機會拒絕新的連接請求. 隻有當服務器進程通過 ServerSocket 的 accept() 方法從隊列中取出連接請求, 使隊列騰出空位時, 隊列才能繼續加入新的連接請求.

對於客戶進程, 如果它發出的連接請求被加入到服務器的請求連接隊列中, 就意味著客戶與服務器的連接建立成功, 客戶進程從 Socket 構造方法中正常返回. 如果客戶進程發出的連接請求被服務器拒絕, Socket 構造方法就會拋出 ConnectionException.

Tips: 創建綁定端口的服務器進程後, 當客戶進程的 Socket構造方法返回成功, 表示客戶進程的連接請求被加入到服務器進程的請求連接隊列中. 雖然客戶端成功返回 Socket對象, 但是還沒跟服務器進程形成一條通信線路. 必須在服務器進程通過 ServerSocket 的 accept() 方法從請求連接隊列中取出連接請求, 並返回一個Socket 對象後, 服務器進程這個Socket 對象才與客戶端的 Socket 對象形成一條通信線路.

ServerSocket 構造方法的 backlog 參數用來顯式設置連接請求隊列的長度, 它將覆蓋操作系統限定的隊列的最大長度. 值得註意的是, 在以下幾種情況中, 仍然會采用操作系統限定的隊列的最大長度:

  • backlog 參數的值大於操作系統限定的隊列的最大長度;
  • backlog 參數的值小於或等於0;
  • 在ServerSocket 構造方法中沒有設置 backlog 參數.

以下的 Client.java 和 Server.java 用來演示服務器的連接請求隊列的特性.

Client.java

import java.net.Socket;
public class Client {
 public static void main(String[] args) throws Exception{
  final int length = 100;
  String host = "localhost";
  int port = 1122;
  Socket[] socket = new Socket[length];
  for(int i = 0;i<length;i++){
   socket[i] = new Socket(host,port);
   System.out.println("第"+(i+1)+"次連接成功!");
  }
  Thread.sleep(3000);
  for(int i=0;i<length;i++){
   socket[i].close();
  }
 }
}

Server.java

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
 private int port = 1122;
 private ServerSocket serverSocket;

 public Server() throws Exception{
  serverSocket = new ServerSocket(port,3);
  System.out.println("服務器啟動!");
 }
 public void service(){
  while(true){
   Socket socket = null;
   try {
    socket = serverSocket.accept();
    System.out.println("New connection accepted "+
      socket.getInetAddress()+":"+socket.getPort());
   } catch (IOException e) {
    e.printStackTrace();
   }finally{
    if(socket!=null){
     try {
      socket.close();
     } catch (IOException e) {
      e.printStackTrace();
     }
    }
   }
  }
 }

 public static void main(String[] args) throws Exception{
  Server server = new Server();
  Thread.sleep(60000*10);
  server.service();
 }
}

Client 試圖與 Server 進行 100 次連接. 在 Server 類中, 把連接請求隊列的長度設為 3. 這意味著當隊列中有瞭 3 個連接請求時, 如果Client 再請求連接, 就會被 Server 拒絕.  下面按照以下步驟運行 Server 和 Client 程序.

⑴ 在Server 中隻創建一個 ServerSocket 對象, 在構造方法中指定監聽的端口為1122 和 連接請求隊列的長度為 3 . 構造 Server 對象後, Server 程序睡眠 10 分鐘, 並且在 Server 中不執行 serverSocket.accept() 方法. 這意味著隊列中的連接請求永遠不會被取出. 運行Server 程序和 Client 程序後, Client程序的打印結果如下:
第 1 次連接成功
第 2 次連接成功
第 3 次連接成功
Exception in thread “main” java.net.ConnectException: Connection refused: connect
…………….
從以上打印的結果可以看出, Client 與 Server 在成功地建立瞭3 個連接後, 就無法再創建其餘的連接瞭, 因為服務器的隊已經滿瞭.
⑵ 在Server中構造一個跟 ⑴ 相同的 ServerSocket對象, Server程序不睡眠, 在一個 while 循環中不斷執行 serverSocket.accept()方法, 該方法從隊列中取出連接請求, 使得隊列能及時騰出空位, 以容納新的連接請求. Client 程序的打印結果如下:
第 1 次連接成功
第 2 次連接成功
第 3 次連接成功
………..
第 100 次連接成功
從以上打印結果可以看出, 此時 Client 能順利與 Server 建立 100 次連接.(每次while的循環要夠快才行, 如果太慢, 從隊列取連接請求的速度比放連接請求的速度慢的話, 不一定都能成功連接)

1.3 設定綁定的IP 地址

如果主機隻有一個IP 地址, 那麼默認情況下, 服務器程序就與該IP 地址綁定. ServerSocket 的第 4 個構造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一個 bindAddr 參數, 它顯式指定服務器要綁定的IP 地址, 該構造方法適用於具有多個IP 地址的主機. 假定一個主機有兩個網卡, 一個網卡用於連接到 Internet, IP為 222.67.5.94, 還有一個網卡用於連接到本地局域網, IP 地址為 192.168.3.4. 如果服務器僅僅被本地局域網中的客戶訪問, 那麼可以按如下方式創建 ServerSocket:

ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName(“192.168.3.4”));

1.4 默認構造方法的作用

ServerSocket 有一個不帶參數的默認構造方法. 通過該方法創建的 ServerSocket 不與任何端口綁定, 接下來還需要通過 bind() 方法與特定端口綁定.

這個默認構造方法的用途是, 允許服務器在綁定到特定端口之前, 先設置ServerSocket 的一些選項. 因為一旦服務器與特定端口綁定, 有些選項就不能再改變瞭.比如:SO_REUSEADDR 選項

在以下代碼中, 先把 ServerSocket 的 SO_REUSEADDR 選項設為 true, 然後再把它與 8000 端口綁定:

ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); //設置 ServerSocket 的選項
serverSocket.bind(new InetSocketAddress(8000));  //與8000端口綁定

如果把以上程序代碼改為:

ServerSocket serverSocket = new ServerSocket(8000);
serverSocket.setReuseAddress(true);//設置 ServerSocket 的選項

那麼 serverSocket.setReuseAddress(true) 方法就不起任何作用瞭, 因為 SO_REUSEADDR 選項必須在服務器綁定端口之前設置才有效.

多線程示例

客戶端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

/*
 * 客戶端
 */
public class Client {
    public static void main(String[] args) {
        try {
            //1.創建客戶端Socket,指定服務器地址和端口
            Socket socket=new Socket("localhost", 8888);
            //2.獲取輸出流,向服務器端發送信息
            OutputStream os=socket.getOutputStream();//字節輸出流
            PrintWriter pw=new PrintWriter(os);//將輸出流包裝為打印流
            pw.write("用戶名:whf;密碼:789");
            pw.flush();
            socket.shutdownOutput();//關閉輸出流
            //3.獲取輸入流,並讀取服務器端的響應信息
            InputStream is=socket.getInputStream();
            BufferedReader br=new BufferedReader(new InputStreamReader(is));
            String info=null;
            while((info=br.readLine())!=null){
                System.out.println("我是客戶端,服務器說:"+info);
            }
            //4.關閉資源
            br.close();
            is.close();
            pw.close();
            os.close();
            socket.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服務器:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/*
 * 基於TCP協議的Socket通信,實現用戶登陸
 * 服務器端
 */
public class Server {
    public static void main(String[] args) {
        try {
            //1.創建一個服務器端Socket,即ServerSocket,指定綁定的端口,並監聽此端口
            ServerSocket serverSocket=new ServerSocket(8888);
            Socket socket=null;
            //記錄客戶端的數量
            int count=0;
            System.out.println("***服務器即將啟動,等待客戶端的連接***");
            //循環監聽等待客戶端的連接
            while(true){
                //調用accept()方法開始監聽,等待客戶端的連接
                socket=serverSocket.accept();
                //創建一個新的線程
                ServerThread serverThread=new ServerThread(socket);
                //啟動線程
                serverThread.start();

                count++;//統計客戶端的數量
                System.out.println("客戶端的數量:"+count);
                InetAddress address=socket.getInetAddress();
                System.out.println("當前客戶端的IP:"+address.getHostAddress());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服務器處理類:

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

/*
 * 服務器線程處理類
 */
public class ServerThread extends Thread {
    // 和本線程相關的Socket
    Socket socket = null;

    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    //線程執行的操作,響應客戶端的請求
    public void run(){
        InputStream is=null;
        InputStreamReader isr=null;
        BufferedReader br=null;
        OutputStream os=null;
        PrintWriter pw=null;
        try {
            //獲取輸入流,並讀取客戶端信息
            is = socket.getInputStream();
            isr = new InputStreamReader(is);
            br = new BufferedReader(isr);
            String info=null;
            while((info=br.readLine())!=null){//循環讀取客戶端的信息
                System.out.println("我是服務器,客戶端說:"+info);
            }
            socket.shutdownInput();//關閉輸入流
            //獲取輸出流,響應客戶端的請求
            os = socket.getOutputStream();
            pw = new PrintWriter(os);
            pw.write("歡迎您!");
            pw.flush();//調用flush()方法將緩沖輸出
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //關閉資源
            try {
                if(pw!=null)
                    pw.close();
                if(os!=null)
                    os.close();
                if(br!=null)
                    br.close();
                if(isr!=null)
                    isr.close();
                if(is!=null)
                    is.close();
                if(socket!=null)
                    socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

以上所述是小編給大傢介紹的Java基於TCP協議的Socket通信,希望對大傢有所幫助。在此也非常感謝大傢對WalkonNet網站的支持!

推薦閱讀: