Socket編程--使用Tcp實現簡單的聊天程序

Socket編程--使用Tcp實現簡單的聊天程序

Socket

在Android聊天程序的實現中,通常是通過http請求拉取最新聊天信息,由於http請求是無狀態(Stateless)的,無法隨時獲知新消息的到達,通常採用推送的方式告知客戶端有新的消息。除了這種http+推送的方式外,也可以通過Socket編程實現聊天程序。本文將對網絡層次進行簡單的講解,並結合一個簡單的demo講解Socket編程的實現。

本文Github Demo地址

計算機網絡基礎知識

計算機網絡用於連接不同的計算機,並實現計算機間的數據傳輸。整個數據傳輸是個很複雜的過程,需要很多步驟進行,這些步驟被劃分成了不同的層級,所以計算機網絡有標準的層級結構,從底層到高層依次是物理層,數據鏈路層,網絡層,傳輸層,應用層,如下圖:

計算機網絡五層協議

分層的優點在於各層級之間是獨立的,靈活性好,在結構上可分隔開,易於實現和維護,同時能促進每層的標準化工作。每個層次都有運行在該層的一系列網絡協議,如網絡層的IP協議,傳輸層的TCP/UDP協議,應用層的Http/SMTP/DNS協議,如下圖:

每層包含的協議

其中Http協議運行在應用層,典型的應用是網絡瀏覽器。Http協議是無狀態的,同一個客戶多次訪問同一個服務器上的頁面時,服務器的響應時間跟第一次訪問時是相同的,因爲服務器並不記得曾經訪問過的這個客戶,也不記得上次訪問的內容。Http協議的無狀態特性簡化了服務器的設計,使得服務器更容易支持大量併發的Http請求。

因爲Http協議完成一次數據交互後就斷開連接,所以服務器無法通過Http請求主動告知客戶端某條新消息的到達。而Tcp/Udp協議可以做到了這一點。Tcp/Udp協議是傳輸層協議,可以進行全雙工通信。這樣就使得服務器端可以在有新消息到達的時候主動通知客戶端,讓客戶端拉取最新消息。

Tcp和Udp兩個協議的區別在於,Tcp是面向連接的,在傳輸數據區必須先建立連接,數據傳輸結束後要釋放連接。能保證數據的可靠傳輸,建立連接的過程較複雜,佔用較多的處理機資源。而Udp是無連接的,不需要建立連接,不提供可靠交付,但是處理機開銷小,效率高。基於這些特點,Udp協議通常用於對傳輸數據容錯性高、時效性強的場景,如語音電話數據的傳輸;而Tcp協議通常用於對傳輸數據完整性和順序要求高的場景,如文件傳輸。

在我們的聊天程序中,我們將利用Tcp協議進行聊天內容的傳輸。

使用Tcp協議的Socket編程

使用Tcp協議的Socket編程主要用到兩個類,Socket和ServerSocket。ServerSocket本身也是一個Socket,只是它同時包含了一些額外的服務器終端的功能,比如監聽端口,等待客戶端Socket前來建立連接等。通過accept方法一旦和客戶端建立起連接,就會返回一個普通的Socket和客戶端Socket進行對等的通信。

本文的Demo實現的功能很簡單,用戶輸入消息,發送給服務器,服務器原封不動返回給客戶端。原理是:啓動一個服務器線程和兩個客戶端線程。服務器線程監聽2000端口,等待客戶端進程的連接。客戶端線程包括一個發送線程和一個接收線程,發送線程連接服務器的2000端口,將用戶輸入的消息發送給服務器線程,接收線程監聽2001端口,等待服務器返回的數據。服務器通過2000端口收到客戶端發來的數據後,將數據原封不動發送到客戶端的2001端口,完成通信。

實現原理圖

由於通過Socket進行收發消息是阻塞式的,也就是說在等待接收消息的時候不能同時發送消息,所以客戶端啓用了兩個線程分別進行收消息和發消息的操作。代碼結構如下圖:

代碼結構

服務器收發線程關鍵代碼:

@Override    
public void run() {        
    try {            
        // 服務器線程通過2000端口監聽客戶端發來的消息            
        serverSocket = new ServerSocket(2000);            
        serverReceiveSocket = serverSocket.accept();            
        // 和客戶端2001端口進行通信,向客戶端發送消息            
        serverSendSocket = new Socket("127.0.0.1", 2001);            
        dataInputStream = new DataInputStream(serverReceiveSocket.getInputStream());            
        printStream = new PrintStream(serverSendSocket.getOutputStream());            
        bufferedReader = new BufferedReader(new InputStreamReader(dataInputStream, "UTF-8"));            
        while (!Thread.currentThread().isInterrupted()) {                
            try {      
                String line = bufferedReader.readLine();                    
                printStream.println(line);                
            } catch (IOException e) {                    
                e.printStackTrace();                
            }            
        }        
    } catch (IOException e) {            
        e.printStackTrace();            
        return;        
    }        
    cleanUpJob();    
}

客戶端發送線程關鍵代碼:

@Override    
public void run() {        
    try {            
        // 客戶端連接服務器端2000端口,通過該端口向服務器發消息            
        clientSocket = new Socket("127.0.0.1", 2000);            
        outputStream=new PrintStream(clientSocket.getOutputStream());            
        while (!Thread.currentThread().isInterrupted()) {                
            if (toSendList != null && toSendList.size() > 0) {                    
                outputStream.println(toSendList.get(0));                    
                toSendList.clear();                
            }            
        }        
    } catch (UnknownHostException e) {            
        e.printStackTrace();        
    } catch (IOException e) {            
        e.printStackTrace();        
    }    
    
    cleanUpJob();    
}

客戶端收消息關鍵代碼:

@Override    
public void run() {       
     try {            
        // 客戶端監聽2001端口,等待服務器的連接,接收服務器發來的消息            
        server = new ServerSocket(2001);            
        socket = server.accept();            
        inputStream = new DataInputStream(socket.getInputStream());                  
        bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));            
        while (!Thread.currentThread().isInterrupted()) {
            String line = bufferedReader.readLine();                
            if (!TextUtils.isEmpty(line)) {                    
                strList.add(line);                    
                handler.sendEmptyMessage(-1);                
            }           
         }        
    } catch (IOException e) {            
    e.printStackTrace();            
    return;      
    }        
    cleanUpJob();   
 }

本文Github Demo地址

參考:
http://stackoverflow.com/questions/2004531/what-is-the-difference-between-socket-and-serversocket
http://stackoverflow.com/questions/5764065/the-difference-between-inputstream-and-inputstreamreader-when-reading-multi-byte

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