Java多執行緒技術:實現多使用者服務端Socket通訊

Charzueus發表於2020-11-14

目錄

前言回顧

一、多使用者伺服器

二、使用執行緒池實現服務端多執行緒

1、單執行緒版本

2、多執行緒版本

三、多使用者與服務端通訊演示

四、多使用者伺服器完整程式碼

最後


前言回顧

在上一篇《Java多執行緒實現TCP網路Socket程式設計(C/S通訊)》,我們解決了伺服器端在建立連線後,連續傳送多條資訊給客戶端接收的問題,解決辦法容易理解,將客戶端接收資訊的功能集中給執行緒處理,實現多執行緒同步進行。

同理,上一篇結束語留下來一個問題,簡而言之,相當於多使用者訪問伺服器資源,伺服器應該與各個客戶端建立連線,並進行通訊對話,就像我們日常使用QQ、微信、視訊等客戶端,就是多使用者與伺服器通訊的例子。

而上一篇中服務端只實現了單使用者的功能,本篇將解決這個問題,詳細記錄服務端多執行緒的實現,目標是多使用者(客戶端)能夠同時與伺服器建立連線並通訊,避免阻塞,進一步完善TCP的Socket網路通訊,運用Java多執行緒技術,實現多使用者與服務端Socket通訊!

Java實現socket通訊網路程式設計系列文章:

    1. UDP協議網路Socket程式設計(java實現C/S通訊案例) 
    2. Java:基於TCP協議網路socket程式設計(實現C/S通訊)
    3. Java多執行緒實現TCP網路Socket程式設計(C/S通訊)

一、多使用者伺服器

多使用者伺服器是指伺服器能同時支援多個使用者併發訪問伺服器所提供的服務資源,如聊天服務、檔案傳輸等。

上一篇的TCPServer是單使用者版本,每次只能和一個使用者對話。我們可以嘗試多使用者連線,開啟多個客戶端,具體操作如下:

這樣就允許同時並行執行多個客戶端,測試發現,單使用者版本的TCPServer.java程式能同時支援多個使用者併發連線(TCP三次握手),但不能同時服務多使用者對話,只有前一個使用者退出後,後面的使用者才能完成伺服器連線。

多執行緒技術,執行緒呼叫的並行執行。

上一篇提到在java中有兩種實現多執行緒的方法,一是使用Thread類,二是使用Runnable類並實現run()方法。下面將使用Runnable類對服務端相關操作功能進行封裝,結合上一篇,就學到了兩種多執行緒實現方法。

//使用Runnable類,作為匿名內部類
class Handler implements Runnable {
    public void run() {
   //實現run方法
    }
}

伺服器面臨很多客戶的併發連線,這種情況的多執行緒方案一般是:

  1. 主執行緒只負責監聽客戶請求和接受連線請求,用一個執行緒專門負責和一個客戶對話,即一個客戶請求成功後,建立一個新執行緒來專門負責該客戶。對於這種方案,可以用上一篇方式new Thread建立執行緒,但是頻繁建立執行緒需要消耗大量系統資源。所以不採用這種方法。
  2. 對於伺服器,一般使用執行緒池來管理和複用執行緒。執行緒池內部維護了若干個執行緒,沒有任務的時候,這些執行緒都處於等待狀態。如果有新任務,就分配一個空閒執行緒執行。如果所有執行緒都處於忙碌狀態,新任務要麼放入佇列等待,要麼增加一個新執行緒進行處理。

顯然,我們採用第2種執行緒池的方法。 常見建立方法如下:

ExecutorService executorService = Executors.newFixedThreadPool(n);//指定執行緒數量
ExecutorService executorService = Executors.newCachedThreadPool();//動態執行緒池

接下來就是選擇執行緒池的型別了。 使用第一個固定執行緒數的執行緒池,顯然不夠靈活,第二種方式的執行緒池會根據任務數量動態調整執行緒池的大小,作為小併發使用問題不大,但其在實際生產環境使用並不合適,如果併發量過大,常常會引發超出記憶體錯誤(OutOfMemoryError),根據我們的應用場景,可以用這個動態調整執行緒池。

二、使用執行緒池實現服務端多執行緒

1、單執行緒版本

首先,與之前的單執行緒通訊對比一下,下面程式碼只能實現單使用者與服務端通訊,如果多使用者與伺服器通訊,則出現阻塞。

    //單客戶版本,每次只能與一個使用者建立通訊連線
    public void Service(){
        while (true){
            Socket socket=null;
            try {
                //此處程式阻塞,監聽並等待使用者發起連線,有連線請求就生成一個套接字
                socket=serverSocket.accept();
 
                //本地伺服器控制檯顯示客戶連線的使用者資訊
                System.out.println("New connection accepted:"+socket.getInetAddress());
                BufferedReader br=getReader(socket);//字串輸入流
                PrintWriter pw=getWriter(socket);//字串輸出流
                pw.println("來自伺服器訊息:歡迎使用本服務!");
 
                String msg=null;
                //此處程式阻塞,每次從輸入流中讀入一行字串
                while ((msg=br.readLine())!=null){
                    //如果使用者傳送資訊為”bye“,就結束通訊
                    if(msg.equals("bye")){
                        pw.println("來自伺服器訊息:伺服器斷開連線,結束服務!");
                        System.out.println("客戶端離開。");
                        break;
                    }
                    msg=msg.replace("?","!").replace("?","!")
                            .replace("嗎","").replace("嗎?","").replace("在","沒");
                    pw.println("來自伺服器訊息:"+msg);
                    pw.println("來自伺服器,重複訊息:"+msg);
                }
            }catch (IOException e){
                e.printStackTrace();
            }finally {
                try {
                    if (socket!=null)
                        socket.close();//關閉socket連線以及相關的輸入輸出流
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }

所以,根據上面的分析,將該單執行緒版本服務端與客戶端通訊對話的功能獨立處理,由一個執行緒來處理。這樣就不會阻塞主程式的執行。具體實現如下面。

2、多執行緒版本

1、建立匿名內部類Handler,實現Runnable類的run方法,將通訊對話放到run()裡面:

    class Handler implements Runnable {
        private Socket socket;
 
        public Handler(Socket socket) {
            this.socket = socket;
        }
 
        public void run() {
            //本地伺服器控制檯顯示客戶端連線的使用者資訊
            System.out.println("New connection accept:" + socket.getInetAddress());
            try {
                BufferedReader br = getReader(socket);
                PrintWriter pw = getWriter(socket);
 
                pw.println("From 伺服器:歡迎使用服務!");
 
                String msg = null;
                while ((msg = br.readLine()) != null) {
                    if (msg.trim().equalsIgnoreCase("bye")) {
                        pw.println("From 伺服器:伺服器已斷開連線,結束服務!");
 
                        System.out.println("客戶端離開。");
                        break;
                    }
                    pw.println("From 伺服器:" + msg);
                    pw.println("來自伺服器,重複訊息:"+msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (socket != null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

 2、使用newCachedThreadPool( )動態建立執行緒池

執行緒池作為成員變數:

    //建立動態執行緒池,適合小併發量,容易出現OutOfMemoryError
    private ExecutorService executorService=Executors.newCachedThreadPool();

 服務端的Service方法中建立新執行緒,交給執行緒池處理。

    //多客戶版本,可以同時與多使用者建立通訊連線
    public void Service() throws IOException {
        while (true){
            Socket socket=null;
                socket=serverSocket.accept();
                //將伺服器和客戶端的通訊交給執行緒池處理
                Handler handler=new Handler(socket);
                executorService.execute(handler);
            }
    }

三、多使用者與服務端通訊演示

之前服務端只支援單使用者通訊對話時候,新使用者傳送的資訊阻塞,伺服器無法返回。

很有趣發現一點,另外一端結束通訊,與此同時,另一端則立即收到伺服器的回覆資訊。

從顯示的時間上初步觀察,可以判斷之前傳送的資訊是阻塞在服務端程式,斷開一方連線後,服務端才將阻塞佇列的資訊傳送到客戶端。那使用多執行緒之後,結果是怎麼樣呢?

動圖演示進一步體會:

 

四、多使用者伺服器完整程式碼

/*
 * TCPThreadServer.java
 * Copyright (c) 2020-11-14
 * author : Charzous
 * All right reserved.
 */
 
package chapter05;
 
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class TCPThreadServer {
    private int port =8008;//伺服器監聽視窗
    private ServerSocket serverSocket;//定義伺服器套接字
    //建立動態執行緒池,適合小併發量,容易出現OutOfMemoryError
    private ExecutorService executorService=Executors.newCachedThreadPool();
 
    public TCPThreadServer() throws IOException{
        serverSocket =new ServerSocket(8008);
        System.out.println("伺服器啟動監聽在"+port+"埠...");
 
    }
 
    private PrintWriter getWriter(Socket socket) throws IOException{
        //獲得輸出流緩衝區的地址
        OutputStream socketOut=socket.getOutputStream();
        //網路流寫出需要使用flush,這裡在printWriter構造方法直接設定為自動flush
        return new PrintWriter(new OutputStreamWriter(socketOut,"utf-8"),true);
    }
 
    private BufferedReader getReader(Socket socket) throws IOException{
        //獲得輸入流緩衝區的地址
        InputStream socketIn=socket.getInputStream();
        return new BufferedReader(new InputStreamReader(socketIn,"utf-8"));
    }
 
    //多客戶版本,可以同時與多使用者建立通訊連線
    public void Service() throws IOException {
        while (true){
            Socket socket=null;
                socket=serverSocket.accept();
                //將伺服器和客戶端的通訊交給執行緒池處理
                Handler handler=new Handler(socket);
                executorService.execute(handler);
            }
    }
 
 
    class Handler implements Runnable {
        private Socket socket;
 
        public Handler(Socket socket) {
            this.socket = socket;
        }
 
        public void run() {
            //本地伺服器控制檯顯示客戶端連線的使用者資訊
            System.out.println("New connection accept:" + socket.getInetAddress());
            try {
                BufferedReader br = getReader(socket);
                PrintWriter pw = getWriter(socket);
 
                pw.println("From 伺服器:歡迎使用服務!");
 
                String msg = null;
                while ((msg = br.readLine()) != null) {
                    if (msg.trim().equalsIgnoreCase("bye")) {
                        pw.println("From 伺服器:伺服器已斷開連線,結束服務!");
 
                        System.out.println("客戶端離開。");
                        break;
                    }
 
                    pw.println("From 伺服器:" + msg);
                    pw.println("來自伺服器,重複訊息:"+msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (socket != null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws IOException{
        new TCPThreadServer().Service();
    }
 
}
 
 

最後

本篇將解決了服務端多使用者通訊的問題,詳細記錄服務端多執行緒的實現,目標是多使用者(客戶端)能夠同時與伺服器建立連線並通訊,避免阻塞,進一步完善TCP的Socket網路通訊,運用Java多執行緒技術,實現多使用者與服務端Socket通訊!簡而言之,相當於多使用者訪問伺服器資源,伺服器應該與各個客戶端建立連線,就像我們日常使用QQ、微信、視訊等客戶端,就是多使用者與伺服器通訊的例子。

老問題了,๑乛◡乛๑,好像完成這個之後,可以來實現一個什麼有趣的呢?這裡停留思考3秒!

……

……

……

就是:實現一個群組聊天房間,類似QQ、微信的群聊,可以多個使用者之間的對話交流,是不是感覺挺有趣的。

基於本篇多執行緒技術實現多使用者伺服器端的功能,是否能夠解決群組聊天房間的功能呢?實現這個功能,等待更新下一篇!

Java實現socket通訊網路程式設計系列文章:

  1. UDP協議網路Socket程式設計(java實現C/S通訊案例) 
  2. Java:基於TCP協議網路socket程式設計(實現C/S通訊) 
  3. Java多執行緒實現TCP網路Socket程式設計(C/S通訊)

如果覺得不錯歡迎“一鍵三連”哦,點贊收藏關注,有問題直接評論,交流學習!

我的部落格園:https://www.cnblogs.com/chenzhenhong/p/13972517.html

我的CSDN部落格:https://blog.csdn.net/Charzous/article/details/109440277


 

版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。
本文連結:https://blog.csdn.net/Charzous/article/details/109440277

 

相關文章