JAVA 經過 Socket 實現 TCP 編程

簡介

TCP簡介

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

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

JAVA Socket簡介

所謂socket 一般也稱做」套接字「,用於描述IP地址和端口,是一個通訊鏈的句柄。應用程序一般經過」套接字」向網絡發出請求或者應答網絡請求web

以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對象實例。socket

注意:其中getInputStream和getOutputStream方法均會產生一個IOException,它必須被捕獲,由於它們返回的流對象,一般都會被另外一個流對象使用。tcp

SocketImpl介紹

既然不論是Socket仍是ServerSocket它們的工做都是通**過SocketImpl類及其子類完成的,那麼固然要介紹啦。函數

抽象類 SocketImpl 是實際實現套接字的全部類的通用超類。建立客戶端和服務器套接字均可以使用它。

具體JDK見:
http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html

因爲它是超類具體代碼實現仍是見下面的Socket

TCP 編程

構造ServerSocket

具體API見:http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html

構造方法:

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); 
  • 1
 

若是運行時沒法綁定到 80 端口, 以上代碼會拋出 IOException, 更確切地說, 是拋出 BindException, 它是 IOException 的子類. BindException 通常是由如下緣由形成的:

  1. 端口已經被其餘服務器進程佔用;
  2. 在某些操做系統中, 若是沒有以超級用戶的身份來運行服務器程序, 那麼操做系統不容許服務器綁定到 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 參數用來顯式設置鏈接請求隊列的長度, 它將覆蓋操做系統限定的隊列的最大長度. 值得注意的是, 在如下幾種狀況中, 仍然會採用操做系統限定的隊列的最大長度:

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

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

 1 import java.net.Socket;
 2 public class Client {
 3  public static void main(String[] args) throws Exception{
 4   final int length = 100;
 5   String host = "localhost";
 6   int port = 1122;
 7   Socket[] socket = new Socket[length];
 8   for(int i = 0;i<length;i++){
 9    socket[i] = new Socket(host,port);
10    System.out.println("第"+(i+1)+"次鏈接成功!");
11   }
12   Thread.sleep(3000);
13   for(int i=0;i<length;i++){
14    socket[i].close();
15   }
16  }
17 }

  Server.java

 1 import java.io.IOException;
 2 import java.net.ServerSocket;
 3 import java.net.Socket;
 4 public class Server {
 5  private int port = 1122;
 6  private ServerSocket serverSocket;
 7 
 8  public Server() throws Exception{
 9   serverSocket = new ServerSocket(port,3);
10   System.out.println("服務器啓動!");
11  }
12  public void service(){
13   while(true){
14    Socket socket = null;
15    try {
16     socket = serverSocket.accept();
17     System.out.println("New connection accepted "+
18       socket.getInetAddress()+":"+socket.getPort());
19    } catch (IOException e) {
20     e.printStackTrace();
21    }finally{
22     if(socket!=null){
23      try {
24       socket.close();
25      } catch (IOException e) {
26       e.printStackTrace();
27      }
28     }
29    }
30   }
31  }
32 
33  public static void main(String[] args) throws Exception{
34   Server server = new Server();
35   Thread.sleep(60000*10);
36   server.service();
37  }
38 }

⑴ 在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 選項必須在服務器綁定端口以前設置纔有效.

多線程示例

客戶端:

 1 import java.io.BufferedReader;
 2 import java.io.IOException;
 3 import java.io.InputStream;
 4 import java.io.InputStreamReader;
 5 import java.io.OutputStream;
 6 import java.io.PrintWriter;
 7 import java.net.Socket;
 8 import java.net.UnknownHostException;
 9 
10 /*
11  * 客戶端
12  */
13 public class Client {
14     public static void main(String[] args) {
15         try {
16             //1.建立客戶端Socket,指定服務器地址和端口
17             Socket socket=new Socket("localhost", 8888);
18             //2.獲取輸出流,向服務器端發送信息
19             OutputStream os=socket.getOutputStream();//字節輸出流
20             PrintWriter pw=new PrintWriter(os);//將輸出流包裝爲打印流
21             pw.write("用戶名:whf;密碼:789");
22             pw.flush();
23             socket.shutdownOutput();//關閉輸出流
24             //3.獲取輸入流,並讀取服務器端的響應信息
25             InputStream is=socket.getInputStream();
26             BufferedReader br=new BufferedReader(new InputStreamReader(is));
27             String info=null;
28             while((info=br.readLine())!=null){
29                 System.out.println("我是客戶端,服務器說:"+info);
30             }
31             //4.關閉資源
32             br.close();
33             is.close();
34             pw.close();
35             os.close();
36             socket.close();
37         } catch (UnknownHostException e) {
38             e.printStackTrace();
39         } catch (IOException e) {
40             e.printStackTrace();
41         }
42     }
43 }

服務器:

 1 import java.io.BufferedReader;
 2 import java.io.IOException;
 3 import java.io.InputStream;
 4 import java.io.InputStreamReader;
 5 import java.io.OutputStream;
 6 import java.io.PrintWriter;
 7 import java.net.InetAddress;
 8 import java.net.ServerSocket;
 9 import java.net.Socket;
10 
11 /*
12  * 基於TCP協議的Socket通訊,實現用戶登錄
13  * 服務器端
14  */
15 public class Server {
16     public static void main(String[] args) {
17         try {
18             //1.建立一個服務器端Socket,即ServerSocket,指定綁定的端口,並監聽此端口
19             ServerSocket serverSocket=new ServerSocket(8888);
20             Socket socket=null;
21             //記錄客戶端的數量
22             int count=0;
23             System.out.println("***服務器即將啓動,等待客戶端的鏈接***");
24             //循環監聽等待客戶端的鏈接
25             while(true){
26                 //調用accept()方法開始監聽,等待客戶端的鏈接
27                 socket=serverSocket.accept();
28                 //建立一個新的線程
29                 ServerThread serverThread=new ServerThread(socket);
30                 //啓動線程
31                 serverThread.start();
32 
33                 count++;//統計客戶端的數量
34                 System.out.println("客戶端的數量:"+count);
35                 InetAddress address=socket.getInetAddress();
36                 System.out.println("當前客戶端的IP:"+address.getHostAddress());
37             }
38         } catch (IOException e) {
39             e.printStackTrace();
40         }
41     }
42 }

服務器處理類:

 1 import java.io.BufferedReader;
 2 import java.io.IOException;
 3 import java.io.InputStream;
 4 import java.io.InputStreamReader;
 5 import java.io.OutputStream;
 6 import java.io.PrintWriter;
 7 import java.net.Socket;
 8 
 9 /*
10  * 服務器線程處理類
11  */
12 public class ServerThread extends Thread {
13     // 和本線程相關的Socket
14     Socket socket = null;
15 
16     public ServerThread(Socket socket) {
17         this.socket = socket;
18     }
19 
20     //線程執行的操做,響應客戶端的請求
21     public void run(){
22         InputStream is=null;
23         InputStreamReader isr=null;
24         BufferedReader br=null;
25         OutputStream os=null;
26         PrintWriter pw=null;
27         try {
28             //獲取輸入流,並讀取客戶端信息
29             is = socket.getInputStream();
30             isr = new InputStreamReader(is);
31             br = new BufferedReader(isr);
32             String info=null;
33             while((info=br.readLine())!=null){//循環讀取客戶端的信息
34                 System.out.println("我是服務器,客戶端說:"+info);
35             }
36             socket.shutdownInput();//關閉輸入流
37             //獲取輸出流,響應客戶端的請求
38             os = socket.getOutputStream();
39             pw = new PrintWriter(os);
40             pw.write("歡迎您!");
41             pw.flush();//調用flush()方法將緩衝輸出
42         } catch (IOException e) {
43             // TODO Auto-generated catch block
44             e.printStackTrace();
45         }finally{
46             //關閉資源
47             try {
48                 if(pw!=null)
49                     pw.close();
50                 if(os!=null)
51                     os.close();
52                 if(br!=null)
53                     br.close();
54                 if(isr!=null)
55                     isr.close();
56                 if(is!=null)
57                     is.close();
58                 if(socket!=null)
59                     socket.close();
60             } catch (IOException e) {
61                 e.printStackTrace();
62             }
63         }
64     }
65 }
相關文章
相關標籤/搜索