Java多線程技術:實現多用戶服務端Socket通信


目錄

前言回顧

一、多用戶服務器

二、使用線程池實現服務端多線程

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM