欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

java中TCP實現(xiàn)回顯服務器及客戶端

 更新時間:2023年02月05日 09:45:31   作者:小小太空人w  
本文主要介紹了java中TCP實現(xiàn)回顯服務器及客戶端,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

前言:

上篇文章介紹了TCP的特點。由于TCP的特點是有連接,面向字節(jié)流,可靠傳輸?shù)?,我們就可以想象到TCP的代碼和UDP會有一定的差異。TCP和UDP具體使用哪種協(xié)議需要根據(jù)實際業(yè)務需求來選擇。

Socket API

Socket是客戶端Socket,或服務端中接收到客戶端建立連接(accept方法)的請求后,返回的服務端Socket。

不管是客戶端還是服務端Socket,都是雙方建立連接后,保存兩端信息,及用來與對方收發(fā)數(shù)據(jù)的。

Socket構造方法

注意:

創(chuàng)建一個客戶端流套接字Socket,并與對應IP的主機上,對應端口的進程建立連接。當服務端accept()阻塞時,客戶端一旦實例出Socket對象,就會建立連接。

Socket方法 

注意:

獲得套接字輸入流。如果建立連接,服務端調用這個方法,就是讀取客戶端請求。

注意:

獲得套接字輸出流。如果建立連接,服務端調用這個方法,就是往客戶端返回響應。

注意:

連接后獲得對方的IP地址。

SeverSocket API

ServerSocket 是創(chuàng)建TCP服務端Socket的API。

ServerSocket構造方法

創(chuàng)建服務端套接字,并綁定端口。這個對象就是用來與客戶端建立連接的。

ServerSocket方法

注意:

開始監(jiān)聽指定端口(創(chuàng)建時綁定的端口),有客戶端連接后,返回一個服務端Socket對象,并基于該Socket建立與客戶端的連接,用來收發(fā)數(shù)據(jù),否則阻塞等待。

注意:

由于在操做系統(tǒng)中Socket被當作文件處理,那么就需要釋放PCB中文件描述符表中的資源,同時斷開連接。

TCP中的長短連接

短連接:每次接收到數(shù)據(jù)并返回響應后,都關閉連接,即是短連接。也就是說,短連接只能一次收發(fā)數(shù)據(jù)。
長連接:不關閉連接,一直保持連接狀態(tài),雙方不停的收發(fā)數(shù)據(jù),即是長連接。也就是說,長連接可以多次收發(fā)數(shù)據(jù)。

注意:

    1)建立關閉連接耗時:很明顯短連接需要不斷的建立和斷開連接,而長連接只需要一次。長連接耗時要比短連接短。

    2)主動發(fā)送請求不同:短連接一般是客戶端主動向服務端發(fā)送請求。長連接客戶端可以向服務端主動發(fā)送,服務端也可以主動向客戶端發(fā)送。

    3)兩者使用場景不同:短連接一般適用于客戶端請求頻率不高的場景(瀏覽網頁)。長連接一般適用于客戶端與服務端通信頻繁的場景。(聊天室)

TCP實現(xiàn)回顯服務器

首先服務器是被動的一方,我們必須指定端口。然后通過ServerSocket對象中accept()方法建立連接,當返回Socket對象時,處理連接并且將響應寫回客戶端。

由于不知道客戶端什么時候建立連接,那么服務器就需要一直等待(隨時待命)。這里使用了死循環(huán)的方式,但是不會一直循環(huán),accept()方法當沒有連接時就會阻塞等待。

這里是本機到本機的數(shù)據(jù)發(fā)送,即使用環(huán)回ip即可。

 private ServerSocket serverSocket = null;
 public TcpEchoSever(int port) throws IOException {
     serverSocket = new ServerSocket(port);
 }

注意:

創(chuàng)建ServerSocket對象,并且指定端口號。

Socket clintSocket = serverSocket.accept();

注意:

accept()方法會阻塞等待。客戶端Socket對象一旦實例化,就會與服務端建立連接。

processConnection(clintSocket);

注意:

 這里通過一個方法來處理連接。這樣寫會有很大的好處。

 try(InputStream inputStream = clintSocket.getInputStream();
     OutputStream outputStream = clintSocket.getOutputStream())

注意:

    我們首先需要獲得讀和寫的流對象。服務器需要接收請求(讀),返回響應(寫)。這里使用的是帶有資源的try(),這樣就會自動關閉流對象。

Scanner scanner = new Scanner(inputStream);
String request = scanner.next();

注意:

這里通過Scanner去從流對象中讀取數(shù)據(jù)。注意這里的next()方法,當讀到一個換行符/空格/其他空白符結束,但最終結果不包含上述空白符。

因為我們不清楚客戶端連接后發(fā)送多少次請求,因此我們采用死循環(huán)的方式讀和向客戶端響應數(shù)據(jù)。這里不會一直循環(huán)因為scanner當讀不到數(shù)據(jù)就會阻塞。

String response = process(request);
public String process(String request) {
    return request;
}

注意:

這里通過一個函數(shù)來處理請求并且返回處理后結果。由于是回顯服務器直接返回即可。

  PrintWriter printWriter = new PrintWriter(outputStream);
  printWriter.println(response);
  printWriter.flush();

注意:

我們?yōu)榱朔奖阒苯訉懽址瑢utputStream轉換成PrintWriter。然后將響應寫入到網卡,并且換行。因為客戶端和服務端讀數(shù)據(jù)都是需要空白符結束的,所以這里必須有一個空白符。

由于數(shù)據(jù)首先會寫入緩沖區(qū),我們將緩沖區(qū)刷新一下保證數(shù)據(jù)正常寫入到文件中(網卡)

finally {
      clintSocket.close();
}

注意:

和一個客戶端建立連接后,返回Socket對象(使用文件描述表),如果并發(fā)量大(會創(chuàng)建很多對象,文件描述符表就有可能滿),就可能導致無法創(chuàng)建連接。因此需要保證資源得到釋放,包裹在finally里。

特別注意:

上述代碼只能處理一個客戶端。當代碼執(zhí)行到processConnection函數(shù)里,首先是一個死循環(huán),然后還有scanner的阻塞,當處理一個連接代碼就會一直在這個函數(shù)里。沒有辦法執(zhí)行到accept()和客戶端連接。想要處理下一個客戶端的連接,就必須斷開這個客戶端,顯然這是不合理的。

解決方案:

使用多線程。當有客戶端連接后,創(chuàng)建一個線程去處理這個連接,主線程代碼繼續(xù)執(zhí)行,就會到accept()方法。要是有多個客戶端都可以建立連接,并且有獨立的線程去處理這些連接,這些線程是并發(fā)的關系。

但是存在一個問題,如果并發(fā)量足夠大(客戶端數(shù)量非常多),就會創(chuàng)建大量的線程,也會存在大量線程的銷毀,這些就會消耗大量的系統(tǒng)資源。因此使用線程池,使用動態(tài)變化的線程數(shù)量,根據(jù)并發(fā)量來調整線程數(shù)量。而且直接使用線程池中的線程代碼上就可以實現(xiàn),這樣就會減少系統(tǒng)資源的消耗。

代碼實現(xiàn)(有詳細解釋)

public class TcpEchoSever {
    //Tcp協(xié)議服務器,使用ServerSocket類,來建立連接
    private ServerSocket serverSocket = null;
    public TcpEchoSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("啟動服務器");
        //使用線程池,防止客戶端數(shù)量過多,創(chuàng)建銷毀大量線程開銷太大
        //動態(tài)變化的線程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            //這里會阻塞,直到和客戶端建立連接,返回Socket對象,來和客戶端通信
            //客戶端構造Socket對象時,會指定IP和端口,就會建立連接(客戶端主動連接)
            Socket clintSocket = serverSocket.accept();
            threadPool.submit(() -> {
                try {
                    processConnection(clintSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            //要連接多個客戶端,需要多線程去處理連接
            //這樣才能讓主線程繼續(xù)執(zhí)行到accept阻塞,然后和其他客戶端建立連接(每個線程是獨立的執(zhí)行流,彼此之間是并發(fā)的關系)
            //如果客戶端數(shù)量非常大,這里就會創(chuàng)建很多線程,數(shù)量過多對于系統(tǒng)來說也是很大的開銷(使用線程池)
//            Thread t = new Thread(() -> {
//                try {
//                    processConnection(clintSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();
        }
    }
 
    private void processConnection(Socket clintSocket) throws IOException {
        System.out.printf("【%s : %d】客戶端上線\n", clintSocket.getInetAddress(), clintSocket.getPort());
        //讀客戶端請求
        //處理請求
        //將結果寫回客戶端(響應)
        try(InputStream inputStream = clintSocket.getInputStream();
            OutputStream outputStream = clintSocket.getOutputStream()) {
 
            //流式數(shù)據(jù),循環(huán)讀取
            while (true) {
                Scanner scanner = new Scanner(inputStream);
                //讀取完畢,客戶端下線
                if(!scanner.hasNext()) {
                    System.out.printf("【%s : %d】客戶端下線\n", clintSocket.getInetAddress(), clintSocket.getPort());
                    break;
                }
                //讀取請求
                // 注意!! 此處使用 next 是一直讀取到換行符/空格/其他空白符結束, 但是最終返回結果里不包含上述 空白符 .
                String request = scanner.next();
                //處理請求
                String response = process(request);
 
                //寫回客戶端處理請求結果(響應)
                //為了直接寫字符串,這里將字節(jié)流轉換為字符流
                //也可以將字符串轉為字節(jié)數(shù)組
                PrintWriter printWriter = new PrintWriter(outputStream);
                //寫入且換行
                printWriter.println(response);
                //寫入首先是寫入了緩沖區(qū),這里為了保險就刷新一下緩沖區(qū)
                printWriter.flush();
                System.out.printf("【%s : %d】請求:%s  響應:%s\n", clintSocket.getInetAddress(), clintSocket.getPort(),
                        request, response);
            }
 
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            //和一個客戶端建立連接后,返回Socket對象(使用文件描述表),如果并發(fā)量大(會創(chuàng)建很多對象,文件描述符表就有可能滿),就可能導致無法創(chuàng)建連接
            //因此需要保證資源得到釋放,包裹在finally里
            clintSocket.close();
        }
    }
    public String process(String request) {
        return request;
    }
 
    public static void main(String[] args) throws IOException {
        TcpEchoSever tcpEchoSever = new TcpEchoSever(8280);
        tcpEchoSever.start();
    }
}

TCP實現(xiàn)回顯客戶端

客戶端不需要指定端口號??蛻舳顺绦蛟谟脩糁鳈C上,我們如果指定就有可能和其他程序沖突,因此讓操作系統(tǒng)隨機分配一個空閑的端口號。客戶端需要明確服務端的ip和端口號,這樣才能明確哪個主機和哪個進程。

那么服務端為什么可以指定端口號呢?難道就不怕和其他進程端口號沖突嗎?(這里詳解請看上篇文章的解釋)

首先需要明確客戶端的工作流程:接收用戶輸入數(shù)據(jù) --> 發(fā)送請求 --> 接收響應

public TcpEchoClint(String severIp, int severPort) throws IOException {
    socket = new Socket(severIp, severPort);
}

注意:

創(chuàng)建Socket對象,并且指定服務端的ip和端口。當這個對象實例創(chuàng)建完成時,同時也就和服務端建立了連接,通過這個Socket對象就可以發(fā)送和接收數(shù)據(jù)。

這里不需要將字符串ip進行轉換,可以自動轉換。

try(InputStream inputStream = socket.getInputStream();
    OutputStream outputStream = socket.getOutputStream())

注意:

和服務端一樣首先獲得輸入和輸出流。用包含資源的try可以自動關閉,釋放文件描述符表中的資源。

PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();

注意:

讓用戶從控制臺輸入數(shù)據(jù),這里做了一個判斷,如果輸入“exit”就退出客戶端(break直接跳出循環(huán))。

Scanner scanner1 = new Scanner(inputStream);
String response = scanner1.next();
System.out.println(response);

注意:

為了直接發(fā)送字符串,這里將outputStream轉換成PrintWriter。這里在發(fā)送時需要換行(空白符),因為服務端讀取的next()方法需要空白符。

數(shù)據(jù)首先寫入緩沖區(qū),為了保證數(shù)據(jù)寫入到文件(網卡),這里手動刷新一下緩沖區(qū)。

Scanner scanner1 = new Scanner(inputStream);
String response = scanner1.next();
System.out.println(response);

注意:

接收響應,通過輸入流來讀取響應。將接收的響應打印出來。這里的next()方法和上面一致。

代碼實現(xiàn)(有詳細注釋)

public class TcpEchoClint {
    Socket socket = null;
    public TcpEchoClint(String severIp, int severPort) throws IOException {
        //Socket構造方法,可以識別點分十進制,不需要轉換,比DatageamPacket方便
        //實例這個對象的同時,就會進行連接
        socket = new Socket(severIp, severPort);
    }
    public void start() {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                //從控制臺讀取請求
                //空白字符結束,但不會讀空白字符
                System.out.println("請輸入請求:");
                String request = scanner.next();
                if(request.equals("exit")) {
                    System.out.println("bye bye");
                    break;
                }
                //發(fā)送請求
                PrintWriter printWriter = new PrintWriter(outputStream);
                //需要發(fā)送空白符,因為scanner需要空白符
                printWriter.println(request);
                printWriter.flush();
                //接收響應
                Scanner scanner1 = new Scanner(inputStream);
                String response = scanner1.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClint tcpEchoClint = new TcpEchoClint("127.0.0.1", 8280);
        tcpEchoClint.start();
    }
}

小結:

在寫服務端代碼時,需要考慮高并發(fā)的情況。我們需要盡可能節(jié)省系統(tǒng)資源的利用。

到此這篇關于java中TCP實現(xiàn)回顯服務器及客戶端的文章就介紹到這了,更多相關java TCP回顯服務器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論