Java網絡編程(二):Socket編程詳解(雷驚風)

一.基本概念。

      在UDP/TCP文章中已經說過,在TCP/IP網絡模型中,分爲了四層,分別是應用層,傳輸層,網際層,數據鏈路層,Http是位於應用層的協議,它是基於TCP實現的,TCP是傳輸層協議,網絡層有IP協議,那麼我們的Socket在哪呢?它是位於應用層之下,傳輸層之上的一個接口層,也就是操作系統提供給用戶訪問網絡的系統接口,我們可以藉助於Socket接口層,對傳輸層,網際層以及物理鏈路層進行操作,來實現不同的應用層協議。

      我們知道系統爲我們實現網絡編程提供了很多已經寫好的Socket類,比如上篇文章講到的在TCP中用Socket與ServerSocket,在UDP中有DatagramSocket等。我們只要知道在什麼時候用哪種Socket就行,不用自己單獨去實現,因爲Java爲我們提供的已經足夠我們進行日常開發了。

二.應用方式。

     下面我們基於TCP由淺入深的來了解一下具體Socket是如何應用的。

1.   簡單應用,建立連接,接收從客戶端的數據

     服務端代碼如下:

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * @author liuyonglei
 *簡單應用,建立連接,接收從客戶端的數據
 */
public class Server {
	 public static void main(String[] args) throws Exception {
		    int port = 12347;
		    ServerSocket server = new ServerSocket(port);
		    System.out.println("LYL_等待連接");
		    Socket socket = server.accept();
		    InputStream inputStream = socket.getInputStream();
		    byte[] bytes = new byte[1024];
		    int len;
		    StringBuilder sb = new StringBuilder();
		    while ((len = inputStream.read(bytes)) != -1) {
		      sb.append(new String(bytes, 0, len,"UTF-8"));
		    }
		    System.out.println("LYL_客戶端數據: " + sb);
		    inputStream.close();
		    socket.close();
		    server.close();
		  }
}

客戶端代碼如下:

import java.io.OutputStream;
import java.net.Socket;


public class Client {
	public static void main(String args[]) throws Exception {
	    String host = "127.0.0.1"; // localhost
	    int port = 12347;
	    Socket socket = new Socket(host, port);
	    OutputStream outputStream = socket.getOutputStream();
	    String message="你好,我是客戶端";
	    socket.getOutputStream().write(message.getBytes("UTF-8"));
	    outputStream.close();
	    socket.close();
	  }
}

       以上Socket應用爲最基本的應用方式,單項數據傳遞,服務端監聽端口,客戶端建立連接,通過輸出流寫入數據,服務端獲取輸入流,並通過輸入流接收客戶端傳遞的數據。在此過程中,需要注意編碼,要保證服務端的編碼與客戶端一致,防止出現亂碼的情況。

2. 雙向單線程通信實現

 服務端實現代碼如下:
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * @author liuyonglei
 *Socket雙向通信,客戶端發送消息,服務端接收到消息後,回覆信息,客戶端接收回覆信息。
 */

public class Server {
	public static void main(String[] args) throws Exception {
	    int port = 12348;
	    ServerSocket server = new ServerSocket(port);
	    
	    System.out.println("LYL_等待連接");
	    Socket socket = server.accept();
	    InputStream inputStream = socket.getInputStream();
	    byte[] bytes = new byte[1024];
	    int len;
	    StringBuilder sb = new StringBuilder();
	    while ((len = inputStream.read(bytes)) != -1) {
	      sb.append(new String(bytes, 0, len, "UTF-8"));
	    }
	    System.out.println("LYL_接收到的客戶端信息: " + sb);

	    OutputStream outputStream = socket.getOutputStream();
	    outputStream.write("我是服務端,您的消息我收到了.".getBytes("UTF-8"));

	    inputStream.close();
	    outputStream.close();
	    socket.close();
	    server.close();
	  }
}

客戶端代碼如下:

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;


public class Client {
	public static void main(String args[]) throws Exception {
	    String host = "127.0.0.1";
	    int port = 12348;
	    Socket socket = new Socket(host, port);
	    OutputStream outputStream = socket.getOutputStream();
	    String message = "我是客戶端發送的數據";
	    socket.getOutputStream().write(message.getBytes("UTF-8"));
	    //告訴服務器數據已經發送完,後續只能接受數據
	    socket.shutdownOutput();
	    
	    InputStream inputStream = socket.getInputStream();
	    byte[] bytes = new byte[1024];
	    int len;
	    StringBuilder sb = new StringBuilder();
	    while ((len = inputStream.read(bytes)) != -1) {
	      sb.append(new String(bytes, 0, len,"UTF-8"));
	    }
	    System.out.println("LYL_服務器返回數據: " + sb);
	    
	    inputStream.close();
	    outputStream.close();
	    socket.close();
	  }
}

       以上實例實現了單線程客戶端與服務端信息相互發送的邏輯,服務端創建ServerSocket指定端口號,並等待客戶端連接,客戶端創建Socket綁定到服務端端口,通過輸出流向服務端寫入數據,服務端獲取到客戶端發送的數據後,通過輸入流讀取,讀完後通過輸出流告訴客戶端數據已經處理完成,客戶端接收到服務端的回覆信息後,關閉流數據關閉socket數據完成最終操作。

      這裏有必要提一下如何告訴對方,我已經發送完成的信息,這個其實還是挺重要的,一般來說,客戶端打開一個輸出流,如果不做約定,也不關閉它,那麼服務端永遠不知道客戶端是否發送完消息,這樣的話服務端會一直等待,直到讀取超時。所以怎麼通知服務端我已經發送完消息就顯得尤爲重要。我們可以通過以下幾種方式做到:

(1).關閉Socket,當客戶端Socket關閉的時候,服務端會收相關關閉信號,這樣一來服務端也就知道流已經關閉了,這個時候讀取操作完成,就可以繼續進行後面的工作了。但是這樣會有一些瑕疵,客戶端Socket關閉後,將不能接受服務端發送的消息,也不能再次發送消息,如果客戶端再想發送消息,那麼需要重新建立連接。

(2).調用Socket的shutdownOutput()方法。調用Socket的shutdownOutput()方法,底層會告訴服務端我這邊已經寫完了,服務端接收到消息後,便知道已經讀取完消息,如果服務端有要返回給客戶的消息那麼就可以通過服務端的輸出流發送給客戶端,如果沒有,直接關閉Socket。缺點跟第一種方式一樣,如果客戶端在想發送信息,那麼需要重新建立連接。

(3).服務端與客戶端達成一致,統一約定,比如當客戶端發送“writeEnd”的時候就表示寫入完成了,那麼在服務端讀取到相關文字的時候就表示知道客戶端已經操作完成了,可以進行後續操作了。這種方式的優點就是再次發送數據的時候不需要重新建立連接;缺點就是如果客戶端發送的數據中如果有約定字段,那麼就會出現問題。

(4).在發送的時候指定數據的長度。我們在發送數據以前獲取本次需要發送的數據的總長度,寫入輸出流,當服務端接收到相關字段後便知道本次需要接收的數據的長度。那麼只需要接收制定長度的數據就行。

3. 多線程異步處理網絡數據

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class Server {
	public static void main(String args[]) throws Exception {
	    
	    int port = 123459;
	    ServerSocket server = new ServerSocket(port);
	    System.out.println("LYL_等待連接");
	    //防止併發過高時創建過多線程耗盡資源
	    ExecutorService threadPool = Executors.newFixedThreadPool(50);
	    while (true) {
	      Socket socket = server.accept();
	      Receive receiveRunnable = new Receive(socket);
	      threadPool.submit(receiveRunnable);
	    }

	  }
	 
}

Receive實現:

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;


public class Receive implements Runnable {
    private Socket socket;
    public Receive(Socket socket2) {
    	this.socket=socket;
	}
	public void run() {
        try {
	          InputStream inputStream = socket.getInputStream();
	          byte[] bytes = new byte[1024];
	          int len;
	          StringBuilder sb = new StringBuilder();
	          while ((len = inputStream.read(bytes)) != -1) {
	            
	            sb.append(new String(bytes, 0, len, "UTF-8"));
	          }
	          System.out.println("LYL_接收到的信息: " + sb);
	          inputStream.close();
	          socket.close();
            }catch (IOException e) {
            e.printStackTrace();
        }
    }
    }

      大多數服務器的設計都是這個樣子的,因爲同一個時間段多臺客戶端與服務端通信是最常見的了,這裏我只修改了服務端代碼,客戶端就不寫了,跟上邊的一樣。這裏通過一個無限While循環來實現監聽所有客戶端發起的連接請求,用一個實現了Runnable接口的類Receive來處理每一個Socket。最終將Runnable實現類放入一個長度爲50的線程池中統一管理,這樣的好處是不僅防止了每次連接創建Socket的時間問題,同時也避免了短時間內創建太多線程引起的資源消耗。

三.其他

這裏說一下Socket粘包、拆包的小知識點。粘包就是多個單獨的數據包連接在了一起。客戶端與服務端都有可能發生粘包的情況,客戶端需要等到緩衝區滿才發送數據會造成粘包;服務端沒有及時處理緩衝區接收到的數據,一次接收到多個數據包就會造成服務端粘包現象。可以通過setTcpNoDelay方法設置true,防止出現粘包問題。解決粘包: 對於客戶端,我們可以每次發送數據時,將數據長度寫入輸出流,服務端就可以根據長度定於緩衝區大小了。拆包:一次發送(Socket)的數據量過大,而底層(TCP/IP)不支持一次發送那麼大的數據量,則會發生拆包現象。這個問題我們必須重視,在TCP/IP中:最大報文段長度(MSS)表示TCP傳往另一端的最大塊數據的長度。當一個連接建立時,連接的雙方都要通告各自的 MSS。客戶端會盡量滿足服務端的要求且不能大於服務端的MSS值,當沒有協商時,會使用值536字節。雖然看起來MSS值越大越好,但是考慮到一些其他情況,這個值還是不太好確定,具體詳見《TCP/IP詳解卷1:協議》。對於拆包發送完一條消息,對於已知數據長度的模式,可以構造相同大小的數組,循環讀取。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章