1. 程式人生 > >Java簡單實現Socket非阻塞通訊

Java簡單實現Socket非阻塞通訊

        用java實現socket C/S通訊很簡單,很多教科書上都有。但是這些通訊模型大都是阻塞式的,其弊端也很明顯:一方必須要接收的到對方的訊息後,才能編輯自己的訊息發出。同樣對方也要一直等待這條訊息收到後才能傳送新的訊息。用網路通訊的知識講,大概就是半雙工通訊吧。這就好比聊天的時候,兩個人只能一人一句的聊天。不能一個人連著傳送多句話。

        而要實現非阻塞通訊呢,也就是實現全雙工通訊。我不想使用java的NIO包。因為那樣有點小題大做了。其實只要使用多執行緒就能實現了。

Socket和ServerSocket

這個就不多說了,預設大家都懂了。顧名思義,ServerSocket是伺服器(S)端使用的。Socket是客戶端(C)使用的。也就是所謂的C/S。
    ServerSocket的構造方法要指定程式使用的埠(Port)。從網路通訊的角度來看,要通訊的話,必須要指定一個埠,因為在應用層中的程式太多了。這樣才能把資料傳送給對應的應用程式。埠號的值要小於65535,並且大於1023。而客戶端除了要指定埠以外還要指定伺服器的IP地址。在本機上測試的話可以使用“迴環地址(localhost)”127.0.0.1。
//服務端:
ServerSocket ss = new ServerSocket(5432);
//客戶端:
Socket s = new Socket("127.0.0.1",5432);
————————————————————————————————————————————
  埠號要小於65535的原因是,通訊過程都是通過IP資料報完成的。IP資料報的報頭中包含一個16位欄位用來指定埠號。而2的16次方就是65536。所以其範圍應該是0~65535   埠號之所以要大於1023是因為小於1023的都是知名埠號,也就是已經確定了某些特殊用途的埠號。比如,FTP伺服器的TCP埠號是21,Telnet是23,Http的埠是80。 ————————————————————————————————————————————     但是要實現通訊雙方都必須通過Socket來通訊。服務端可以通過ServerSocket的accept方法來獲得一個Socket物件。
//服務端:
Socket s = ss.accept();
   注意以上的程式碼要捕獲相應的異常。這就不多說了,eclipse會幫助我們。     此時呢,服務端和客戶端的類中都持有了一個Socket物件。要完成資料交換,就要涉及IO了。

IO操作

socket的IO流

Socket有相應的get方法來獲得輸入輸出流。
	InputStream in = s.getInputStream();
	OutputStream out = s.getOutputStream();
    由此獲得的兩個位元組IO流就是接下來所有資料互動的基礎了。然後為了提高效率要進行一下包裝。我是用的是DataInputStream/DataOutputStream。你也可以轉換為字元流。
	DataInputStream din = new DataInputStream(in);
	DataOutputStream dout = new DataOutputStream(out);

很多人可能會把輸入流和輸出流搞混淆掉哦。


標準輸入的包裝

我們要實現從控制檯中輸入,然後傳送到網路的輸入流(上文中的din),必須要對標準輸入(System.in)進行封裝。 誤區:     起初我覺得既然從標準輸入獲得的資料,要傳遞給DataOutputStream型別的物件dout傳送出去,那麼就用DataInputStream來包裝System.in吧。然後呼叫它的readUTF方法獲得字串,在通過dout的writeUTF方法傳送。
	DataInputStream dis = new DataInputStream(System.in));
	String msg = dis.readUTF();
	dout.writeUTF(msg);
    但是當程式執行的時候,就會發現在一方的控制檯視窗無論輸入多少行,對方都無法收到。也就是這裡也陷入了阻塞,百度下,究其原因呢,是因為在控制檯輸入的字元並不是UTF格式的。所以readUTF根本無法返回字串。 ————————————————————————————————————————————

DataInputStream/DataOutputStream提供了對於基本資料型別的寫入與讀取方法如:

 DataInputStream  DataOutputStream
readInt() writeInt()
readChar() writeChar()
readDouble() writeDouble()
    所以輸入流輸出流中的資料型別必須相同才能讀取。但是其中並沒有readString/wirteString型別。只有一個readUTF/writeUTF(原來有readLine現在不鼓勵了,但是BufferedReader類中使用的是readLine)。 ————————————————————————————————————————————
    接著我對控制檯的標準輸入的包裝使用了BufferedReader(也可以使用Scanner包裝哦)。
	BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
	String msg = bf.readLine();
	dout.writeUTF(msg);
【下面進入本片部落格的重要環節(上面的內容你都可以認為是湊字數的鋪墊)】

多執行緒的使用

————————————————————————————————————————————  一般我們執行的程式只有一個主執行緒,也就是main函式建立的那個執行緒。一個執行緒中所有的操作呢都是線性的,但如果你想同時做多件事的話,就需要建立多個執行緒。記得《射鵰》裡面老頑童有個絕技“左右互博術”,他在傳授給郭靖的時候,就是讓郭靖不停的左手畫圓,右手畫方。這就相當於郭靖要同時開兩個執行緒,一個執行緒負責不停地左手畫圓,另一執行緒負責不停的右手畫方。當然不論是電腦還是人腦實際上都是無法同時做兩件事的,只不過CPU在兩個執行緒之間快速切換,給人的感覺像是同時一樣。     Java本身提供了對於多執行緒的支援。通過繼承Thread類,或者實現Runnable介面來建立新的執行緒。兩者都要重寫run()方法。不過採用Runnable,只是實現了Runnable介面的run方法還不夠,還要用此實現的介面來建立Thread物件才可以。最後呼叫Thread物件的start()方法就是建立這一程序了。
————————————————————————————————————————————
      實現非阻塞的通訊,我們要完成的事情只有兩個:
  1. 一個是從socket的輸入流中獲取對方的訊息並列印在螢幕上。
  2. 從標準輸入中鍵入的訊息要通過socket的輸出流傳送給對方。

    以上行為要開闢新的執行緒來完成來避免阻塞。為此我定義兩個類,取名為SendThread和PrintThread

為了便於操作,我將socket的包裝後的輸出流作為引數傳遞給SendThread。同樣PrintThread中也會傳如socket的包裝後的輸入流引數

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class SendThread implements Runnable {

	private DataOutputStream dout;
	public SendThread(DataOutputStream dout){
		super();
		this.dout= dout;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true){
			String msg;
			try {
				BufferedReader bf = new BufferedReader(
						new InputStreamReader(System.in));
				//注意不能直接用DataInputStream來封裝標準輸入,原因前文已提到
				msg = bf.readLine();
				dout.writeUTF(msg);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				
				break;
			}
		}
	}

}

    為了增強可讀性,我也把IP地址傳入了PrintThread中。大家也可無視

import java.io.DataInputStream;
import java.io.IOException;

class PrintThread implements Runnable{

	private DataInputStream din;
	private String ip;
	public PrintThread(DataInputStream din,String ip) {
		super();
		this.din = din;
		this.ip = ip;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true){
			try {
				String msg = din.readUTF();
				System.out.println("["+ip+"]"+":"+msg);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				
			break;
			} 
			
		}
	}
	
}

    接著在主類Server和Client中要做的事就很簡單了。

//Server
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

	private static String ip;

	public static void main(String[] args) throws IOException {
		// TODO Auto-generated method stub
		@SuppressWarnings("resource")
		ServerSocket ss = new ServerSocket(5432);

		Socket s = ss.accept();
		ip = s.getInetAddress().toString();
		ip = ip.substring(ip.indexOf("/")+1);
		System.out.println(ip+"上線了");
		
		InputStream in = s.getInputStream();
		OutputStream out = s.getOutputStream();
		
		DataInputStream din = new DataInputStream(in);
		DataOutputStream dout = new DataOutputStream(out);
		
		SendThread it = new SendThread(dout);
		PrintThread ot = new PrintThread(din,ip);
		
		new Thread(ot).start();
		new Thread(it).start();
	}
}
//Client
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Client {

	private static String ip = "127.0.0.1";
	
	public static void main(String[] args) throws IOException{
		@SuppressWarnings("resource")
		Socket s = new Socket(ip,5432);
		OutputStream out = s.getOutputStream();
		InputStream in = s.getInputStream();
		
		DataOutputStream dout = new DataOutputStream(out);
		DataInputStream din = new DataInputStream(in);
		
		SendThread it = new SendThread(dout);
		PrintThread ot = new PrintThread(din,ip);
		
		new Thread(ot).start();
		new Thread(it).start();

	}
}


     基本的程式碼就是這些,當然這是簡單實現,還可以有很多繼續完善的地方。比如使Server可以監聽多個Client的連線請求,或者你可以加個Swing的介面(個人比較不在乎介面)。細節我也有很多未仔細處理的地方,比如裡面的Socket和各種IO流,都沒有寫關閉,(囧,沒有找到個合適的地方)所以程式碼中會有一句

@SuppressWarnings("resource")

來忽略流未關閉的警告。。哈,有點水啊。


————————————————————後記的瑣事———————————————————

這段程式,我成功在電腦和虛擬機器上實現了通訊。虛擬機器的原理就是把你的一臺電腦當成兩臺電腦來用了。只需要修改上述程式碼中的IP地址就行了,虛擬機器相當於和你處在一個局域網裡,你可以使用ipconfig命令檢視在這個區域網之下的虛擬機器的ip地址,通常host-only的就是虛擬機器的ip了。比如在這個虛擬的區域網中我本身的電腦IP地址是192.168.56.101,而虛擬機器是192.168.56.1。

    還有就是那天資料結構上機課的時候,我發現機房電腦裡裝了java(但是沒eclipse)。。於是我就用記事本手寫了上面兩個程式,果然用慣了IDE,記事本會很不習慣啊。但最後還是完成了,我把Client的程式通過U盤傳給了旁邊的同學,最後兩臺電腦實現了通訊,呵呵。雖然是小事,還是蠻驕傲的呢。
————————————————————————————————————————————