Java簡單實現Socket非阻塞通訊
用java實現socket C/S通訊很簡單,很多教科書上都有。但是這些通訊模型大都是阻塞式的,其弊端也很明顯:一方必須要接收的到對方的訊息後,才能編輯自己的訊息發出。同樣對方也要一直等待這條訊息收到後才能傳送新的訊息。用網路通訊的知識講,大概就是半雙工通訊吧。這就好比聊天的時候,兩個人只能一人一句的聊天。不能一個人連著傳送多句話。
而要實現非阻塞通訊呢,也就是實現全雙工通訊。我不想使用java的NIO包。因為那樣有點小題大做了。其實只要使用多執行緒就能實現了。
Socket和ServerSocket
這個就不多說了,預設大家都懂了。顧名思義,ServerSocket是伺服器(S)端使用的。Socket是客戶端(C)使用的。也就是所謂的C/S。//服務端:
ServerSocket ss = new ServerSocket(5432);
//客戶端:
Socket s = new Socket("127.0.0.1",5432);
————————————————————————————————————————————注意以上的程式碼要捕獲相應的異常。這就不多說了,eclipse會幫助我們。 此時呢,服務端和客戶端的類中都持有了一個Socket物件。要完成資料交換,就要涉及IO了。//服務端: Socket s = ss.accept();
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() |
接著我對控制檯的標準輸入的包裝使用了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()方法就是建立這一程序了。————————————————————————————————————————————
實現非阻塞的通訊,我們要完成的事情只有兩個:
- 一個是從socket的輸入流中獲取對方的訊息並列印在螢幕上。
- 從標準輸入中鍵入的訊息要通過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盤傳給了旁邊的同學,最後兩臺電腦實現了通訊,呵呵。雖然是小事,還是蠻驕傲的呢。
————————————————————————————————————————————