Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發之簡單的聊天室(客戶端與服務器進行通信)

Android開發之簡單的聊天室(客戶端與服務器進行通信)

編輯:關於Android編程

1.使用ServerSocket創建TCP服務器端


Java中能接收其他通信實體連接請求的類是ServerSocket, ServerSocket對象用於監聽來 自客戶端的Socket連接,如果沒有連接,它將一直處於等待狀態。ServerSocket包含一個監聽來自客戶端連接請求的方法。

1) Socket accept():如果接收到一個客戶端Socket的連接請求,該方法將返回一個與連接客戶端Socket對應的Socket;否則該方法將一直處於等待狀態,線程也被阻塞。

創建ServerSocket對象,ServerSocket類提供了如下幾個構造器:

2) ServerSocket(int port):用指定的端口 port 來創建一個ServerSocket。該端口應該是有一個有效的端口整數值:0?65 535。

3) ServerSocket(int port,int backlog):增加一個用來改變連接隊列長度的參數backlog。

4) ServerSocket(int port.int backlog,lnetAddress localAdd():在機器存在多個 IP 地 址的情況下,允許通過localAddr這個參數來指定將ServerSocket綁定到指定的IP地址。

注:當ServerSocket使用完畢後,應使用ServerSocket的close()方法來關閉該ServerSocket。通常情況下,服務器不應該只接收一個客戶端請求,而應該不斷地接收來自客戶端的所有請求。如下面代碼所示:

//創建一個ServerSocket,用於監聽客戶端的連接請求

ServerSocket ss=new ServerSocket(1566);

//不停地從接收來自客戶端的請求

while (true) {

//每當接受一個來自客戶端的Socket的請求,服務器端也對應產生一個Socket

Socket s=ss.accept();

//下面就可以使用Socket進行通信了

//..........

}

2.使用Socket進行通信


客戶端通常可使用Socket的構造器來連接到指定服務器,Socket通常可使用如下兩個構造器。

1) Socket(lnetAddress/String remoteAddress, int port):創建連接到指定遠程主機、遠程端口的Socket,該構造器沒有指定本地地址、本地端口,默認使用本地主機的默認IP地址,默認使用系統動態指定的IP地址。

2) Socket(lnetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort):創建連接到指定遠程主機、遠程端口的Socket,並指定本地IP地址 和本地端口號,適用於本地主機有多個IP地址的情形。

上面兩個構造器中指定遠程主機時既可使用InetAddress來指定,也可直接使用String對象來指定,但程序通常使用String對象(如211.158.6.26)來指定遠程IP。當本地主機只有—個IP地址時,使用第一個方法更為簡單。如:

Socket socket=new Socket("169.254.77.36", 8888);

//下面就可以和服務器進行通信了

當程序執行上面代碼中的粗體字代碼時,該代碼將會連接到指定服務器,讓服務器端的ServerSocket的accept()方法向下執行,於是服務器端和客戶端就產生一對互相連接的Socket。

當客戶端、服務器端產生了對應的Socket之後,程序無須再區分服務器、客戶端,而是通過各自的Socket進行通信。Socket提供如下兩個方法來獲取輸入流和輸出流:

1) InputStream getlnputStream():返回該Socket對象對應的輸入流,讓程序通過該輸入流從Socket中取出數據。

2) OutputStream getOutputStream():返回該Socket對象對應的輸出流,讓程序通過該輸出流向Socket中輸出數據。


3.實例:和服務器進行簡單通信:


服務器端:

public static void main(String[] args) {

// TODO Auto-generated method stub

try {

//創建一個ServerSocket,用於監聽客戶端的連接請求

ServerSocket ss=new ServerSocket(8888);

//不停地從接收來自客戶端的請求

while (true) {

//每當接受一個來自客戶端的Socket的請求,服務器端也對應產生一個Socket

Socket s=ss.accept();

//下面就可以使用Socket進行通信了

OutputStream os=s.getOutputStream();

os.write("來自服務器端的消息:你好,今天天氣不錯,騷年外出散散心吧!".getBytes("utf-8"));

//關閉輸出流

os.close();

//關閉Socket

s.close();

}

} catch (Exception e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

注:上面的程序並未把OutputStream流包裝成PrintStream ,然後使用 PrintStream直接輸出整個字符串,這是因為該服務器端程序運行於Windows 主機上,當直接使用PrintStream輸出字符串時默認使用系統平台的字符串(即 GBK )進行編碼;但該程序的客戶端是Android應用,運行於Linux平台(Android 是Linux內核的),因此當客戶端讀取網絡數據時默認使用UTF-8字符集進行解碼,這樣勢必引起亂碼。為了保證客戶端能正常解析到數據,此處手動控制字符串的編碼,強行指定使用UTF-8字符集進行編碼,這樣就可以避免亂碼問

客戶端:

edtMsg=(EditText)findViewById(R.id.edtMsg);

//創建並啟動一個新線程,向服務器發送TCP請求

new Thread(){

@Override

public void run() {

// TODO Auto-generated method stub

super.run();

//創建一個Socket用於向IP為169.254.77.36的服務器的8888端口發送請求

Socket s;

try {

s = new Socket();

//如果超過10s還沒連接到服務器則視為超時

s.connect(new InetSocketAddress("169.254.77.36", 8888),10000);

//設置客戶端與服務器建立連接的超時時長為30秒

s.setSoTimeout(30000);

//將Socket對應的輸入流封裝成BufferedReader對象

BufferedReader br=new BufferedReader(new

InputStreamReader(s.getInputStream()));

String msg=br.readLine();

edtMsg.setText(msg);

br.close();

s.close();

//捕捉SocketTimeoutException異常

}catch (SocketTimeoutException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}catch (Exception e) {

// TODO: handle exception

e.printStackTrace();

}

}

}.start();

最後別忘記為程序添加訪問網絡的權限:

"android.permission.INTERNET"/>

程序運行效果圖:

客戶端與服務器進行通信


4.異常和捕捉


上面的程序為了突出通過ServerSocket和Socket建立連接並通過底層 IO流進行通信的主題,程序沒有進行異常處理,也沒有使用finally塊來關閉資源。

實際應用中,程序可能不想讓執行網絡連接、讀取服務器數據的進程一直阻塞,而是希 望當網絡連接、讀取操作超過合理時間之後,系統自動認為該操作失敗,這個合理時間就是 超時時長。Socket對象提供了一個setSoTimeout(int timeout)來設置超時時長,如下面的代碼 片段所示:

//設置客戶端與服務器建立連接的超時時長為30秒

s.setSoTimeout(30000);

為Socket對象指定了超時時長之後,如果使用Socket進行讀、寫操作完成之前已經超出了該時間限制,那麼這些方法就會拋出SocketTimeoutException異常,程序可以對該異常進行捕捉,並進行適當處理,如以下代碼所示:

Socket s;

try {

s = new Socket();

//如果超過10s還沒連接到服務器則視為超時

s.connect(new InetSocketAddress("169.254.77.36", 8888),10000);

//設置客戶端與服務器建立連接的超時時長為30秒

s.setSoTimeout(30000);

//將Socket對應的輸入流封裝成BufferedReader對象

BufferedReader br=new BufferedReader(new

InputStreamReader(s.getInputStream()));

String msg=br.readLine();

edtMsg.setText(msg);

br.close();

s.close();

//捕捉SocketTimeoutException異常

}catch (SocketTimeoutException e) {

//進行異常處理

}

假設程序需要為Socket連接服務器時指定超時時長:即經過指定時間後,如果該Socket 還未連接到遠程服務器,則系統認為該Socket連接超時。但Socket的所有構造器裡都沒有提供指定超時時長的參數,所以程序應該先創建一個無連接Socket,再調用Socket的connect() 方法來連接遠程服務器,connect()方法就可以接受一個超時時長參數。如以下代碼所示:

//創建一個無連接的Socket

Socket s= new Socket();

//如果超過10s還沒連接到服務器則視為超時

s.connect(new InetSocketAddress("169.254.77.36", 8888),10000);


5.加入多線程



前面服務器端和客戶端只是進行了簡單的通信操作:服務器接收到客戶端連接之後,服 務器向客戶端輸出一個字符串,而客戶端也只是讀取服務器的字符串後就退出了。實際應用 中的客戶端則可能需要和服務器端保持長時間通信,即服務器需要不斷地讀取客戶端數據, 並向客戶端寫入數據;客戶端也需要不斷地讀取服務器數據,並向服務器寫入數據。

當使用傳統BufferedReader的readLine()方法讀取數據時,當該方法成功返回之前,線程被阻塞,程序無法繼續執行。考慮到這個原因,服務器應該為每個Socket單獨啟動一條線程,每條線程負責與一個客戶端進行通信。

客戶端讀取服務器數據的線程同樣會被阻塞,所以系統應該單獨啟動一條線程,該線程 專門負責讀取服務器數據。

下面考慮實現一個簡單的C/S聊天室應用,服務器端則應該包含多條線程,每個Socket 對應一條線程,該線程負責讀取Socket對應輸入流的數據(從客戶端發送過來的數據),並 將讀到的數據向每個Socket輸出流發送一遍(將一個客戶端發送的數據“廣播”給其他客戶 端),因此需要在服務器端使用List來保存所有的Socket。

下面是服務器端的實現代碼,程序為服務器提供了兩個類,一個是創建ServerSocket監 聽的主類,另一個是負責處理每個Socket通信的線程類。

代碼清單:

服務器端:

ServerSocket監聽的主類:

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.ArrayList;

/**

* Description:

* 創建ServerSocket監聽的主類

* @author jph

*/

public class MyServer

{

// 定義保存所有Socket的ArrayList

public static ArrayList socketList

= new ArrayList();

public static void main(String[] args)

throws IOException

{

ServerSocket ss = new ServerSocket(30000);

while(true)

{

// 此行代碼會阻塞,將一直等待別人的連接

Socket s = ss.accept();

socketList.add(s);

// 每當客戶端連接後啟動一條ServerThread線程為該客戶端服務

new Thread(new ServerThread(s)).start();

}

}

}

負責處理每一個Socket通信的線程類:

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStream;

import java.net.Socket;

/**

* Description:

* 負責處理每一個Socket通信的線程類

* @author jph

*/

// 負責處理每個線程通信的線程類

public class ServerThread implements Runnable

{

// 定義當前線程所處理的Socket

Socket s = null;

// 該線程所處理的Socket所對應的輸入流

BufferedReader br = null;

public ServerThread(Socket s)

throws IOException

{

this.s = s;

// 初始化該Socket對應的輸入流

br = new BufferedReader(new InputStreamReader(

s.getInputStream() , "utf-8")); //②

}

public void run()

{

try

{

String content = null;

// 采用循環不斷從Socket中讀取客戶端發送過來的數據

while ((content = readFromClient()) != null)

{

// 遍歷socketList中的每個Socket,

// 將讀到的內容向每個Socket發送一次

for (Socket s : MyServer.socketList)

{

OutputStream os = s.getOutputStream();

os.write((content + "\n").getBytes("utf-8"));

}

}

}

catch (IOException e)

{

e.printStackTrace();

}

}

// 定義讀取客戶端數據的方法

private String readFromClient()

{

try

{

return br.readLine();

}

// 如果捕捉到異常,表明該Socket對應的客戶端已經關閉

catch (IOException e)

{

// 刪除該Socket。

MyServer.socketList.remove(s); //①

}

return null;

}

}

上面的服務器端線程類不斷讀取客戶端數據,程序使用readFromCHent()方法來讀取客戶端數據,如果讀取數據過程中捕獲到IOException異常,則表明該Socket對應的客戶端Socket 出現了問題(到底什麼問題我們不管,反正不正常),程序就將該Socket從socketList中刪除, 如readFromClient()方法中①號代碼所示。

當服務器線程讀到客戶端數據之後,程序遍歷socketList集合,並將該數據向socketList 集合中的每個Socket發送一次一該服務器線程將把從Socket中讀到的數據向socketList中 的每個Socket轉發一次,如run()線程執行體中的粗體字代碼所示。

注:

上面的程序中②號粗體字代碼將網絡的字節榆入流轉換為字符輸入流時,指定了轉換所用的字符串:UTF-8,這也是由於客戶端寫過來的數據是采用UTF-8 字符集進行編碼的,所以此處的服務器端也要使用UTF-8字符集進行解碼。當需 要編寫跨平台的網絡通信程序時,使用UTF-8字符集進行編碼、解碼是一種較好的解決方案。

每個客戶端應該包含兩條線程:一條負責生成主界面,並響應用戶動作,並將用戶輸入 的數據寫入Socket對應的輸出流中:另一條負責讀取Socket對應輸入流中的數據(從服務器 發送過來的數據),並負責將這些數據在程序界面上顯示出來。

客戶端:

客戶端程序同樣是一個Android應用,因此需要創建一個Android項目,這個Android 應用的界面中包含兩個文本框:一個用於接收用戶輸入,另一個用於顯示聊天信息:界面中 還有一個按鈕,當用戶單擊該按鈕時,程序向服務器發送聊天信息。該程序的界面布局代碼 如下。

/**

* 客戶端:

* */

public class MultiThreadClient extends Activity

{

// 定義界面上的兩個文本框

EditText input;

TextView show;

// 定義界面上的一個按鈕

Button send;

Handler handler;

// 定義與服務器通信的子線程

ClientThread clientThread;

@Override

public void onCreate(Bundle savedInstanceState)

{

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

input = (EditText) findViewById(R.id.input);

send = (Button) findViewById(R.id.send);

show = (TextView) findViewById(R.id.show);

handler = new Handler() //①

{

@Override

public void handleMessage(Message msg)

{

// 如果消息來自於子線程

if (msg.what == 0x123)

{

// 將讀取的內容追加顯示在文本框中

show.append("\n" + msg.obj.toString());

}

}

};

clientThread = new ClientThread(handler);

// 客戶端啟動ClientThread線程創建網絡連接、讀取來自服務器的數據

new Thread(clientThread).start(); //①

send.setOnClickListener(new OnClickListener()

{

@Override

public void onClick(View v)

{

try

{

// 當用戶按下發送按鈕後,將用戶輸入的數據封裝成Message,

// 然後發送給子線程的Handler

Message msg = new Message();

msg.what = 0x345;

msg.obj = input.getText().toString();

clientThread.revHandler.sendMessage(msg);

// 清空input文本框

input.setText("");

}

catch (Exception e)

{

e.printStackTrace();

}

}

});

}

}

代碼分析:

當用戶單擊該程序界而中的“發送”按鈕之後,程序將會把input輸入框中的的內容發 送該clientThread的revHandler對象,clientThread將負責將用戶輸入的內容發送給服務器。

為了避免UI線程被阻塞,該程序將建立網絡連接、與網絡服務器通信等工作都交給 ClientThread線程完成。因此該程序在①號代碼處啟動ClientThread線程。

由於Android不允許子線程訪問界面組件,因此上面的程序定義了一個Handler來處理 來自子線程的消息,如程序中②號粗體字代碼所示。

ClientThread子線程負責建立與遠程服務器的連接,並負責與遠程服務器通信,讀到數 據之後便通過Handler對象發送一條消息:當ClientThread子線程收到UI線程發送過來的消 息(消息攜帶了用戶輸入的內容)之後,還負責將用戶輸入的內容發送給遠程服務器。該子 線程代碼如下:

public class ClientThread implements Runnable

{

private Socket s;

// 定義向UI線程發送消息的Handler對象

private Handler handler;

// 定義接收UI線程的消息的Handler對象

public Handler revHandler;

// 該線程所處理的Socket所對應的輸入流

BufferedReader br = null;

OutputStream os = null;

public ClientThread(Handler handler)

{

this.handler = handler;

}

public void run()

{

try

{

//192.168.191.2為本機的ip地址,30000為與MultiThreadServer服務器通信的端口

s = new Socket("192.168.191.2", 30000);

br = new BufferedReader(new InputStreamReader(

s.getInputStream()));

os = s.getOutputStream();

// 啟動一條子線程來讀取服務器響應的數據

new Thread()

{

@Override

public void run()

{

String content = null;

// 不斷讀取Socket輸入流中的內容。

try

{

while ((content = br.readLine()) != null)

{

// 每當讀到來自服務器的數據之後,發送消息通知程序界面顯示該數據

Message msg = new Message();

msg.what = 0x123;

msg.obj = content;

handler.sendMessage(msg);

}

}

catch (IOException e)

{

e.printStackTrace();

}

}

}.start();

// 為當前線程初始化Looper

Looper.prepare();

// 創建revHandler對象

revHandler = new Handler()

{

@Override

public void handleMessage(Message msg)

{

// 接收到UI線程中用戶輸入的數據

if (msg.what == 0x345)

{

// 將用戶在文本框內輸入的內容寫入網絡

try

{

os.write((msg.obj.toString() + "\r\n")

.getBytes("utf-8"));

}

catch (Exception e)

{

e.printStackTrace();

}

}

}

};

// 啟動Looper

Looper.loop();

}

catch (SocketTimeoutException e1)

{

System.out.println("網絡連接超時!!");

}

catch (Exception e)

{

e.printStackTrace();

}

}

}

Android簡單聊天室

實例分析:

上面線程的功能也非常簡單,它只是不斷獲取Socket輸入流中的內容,當讀到Socket 輸入流中的內容後,便通過Handler對象發送一條消息,消息負責攜帶讀到數據,除此之外,該子線程還負責讀取UI線程發送的消到消息之後,該子線程負責將消息中攜帶的數據發送給遠程服務器。

先運行上面程序中的MyServer類,該類運行後只是作為服務器,看不到任何輸出。接 著可以運行Android客戶端一相當於啟動聊天室客戶端登錄該服務器,接著可以看到在任 何一個Android客戶端輸入一些內容後單擊“發送”按鈕,將可看到所有客戶端(包括自己) 都會收到他剛剛輸入的內容,如上圖所示,這就粗略實現了一個C/S結構聊天室的功能。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved