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

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

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

前言:

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

Socket API

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

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

Socket構(gòu)造方法

注意:

創(chuàng)建一個(gè)客戶端流套接字Socket,并與對(duì)應(yīng)IP的主機(jī)上,對(duì)應(yīng)端口的進(jìn)程建立連接。當(dāng)服務(wù)端accept()阻塞時(shí),客戶端一旦實(shí)例出Socket對(duì)象,就會(huì)建立連接。

Socket方法 

注意:

獲得套接字輸入流。如果建立連接,服務(wù)端調(diào)用這個(gè)方法,就是讀取客戶端請(qǐng)求。

注意:

獲得套接字輸出流。如果建立連接,服務(wù)端調(diào)用這個(gè)方法,就是往客戶端返回響應(yīng)。

注意:

連接后獲得對(duì)方的IP地址。

SeverSocket API

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

ServerSocket構(gòu)造方法

創(chuàng)建服務(wù)端套接字,并綁定端口。這個(gè)對(duì)象就是用來(lái)與客戶端建立連接的。

ServerSocket方法

注意:

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

注意:

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

TCP中的長(zhǎng)短連接

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

注意:

    1)建立關(guān)閉連接耗時(shí):很明顯短連接需要不斷的建立和斷開(kāi)連接,而長(zhǎng)連接只需要一次。長(zhǎng)連接耗時(shí)要比短連接短。

    2)主動(dòng)發(fā)送請(qǐng)求不同:短連接一般是客戶端主動(dòng)向服務(wù)端發(fā)送請(qǐng)求。長(zhǎng)連接客戶端可以向服務(wù)端主動(dòng)發(fā)送,服務(wù)端也可以主動(dòng)向客戶端發(fā)送。

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

TCP實(shí)現(xiàn)回顯服務(wù)器

首先服務(wù)器是被動(dòng)的一方,我們必須指定端口。然后通過(guò)ServerSocket對(duì)象中accept()方法建立連接,當(dāng)返回Socket對(duì)象時(shí),處理連接并且將響應(yīng)寫(xiě)回客戶端。

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

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

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

注意:

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

Socket clintSocket = serverSocket.accept();

注意:

accept()方法會(huì)阻塞等待??蛻舳薙ocket對(duì)象一旦實(shí)例化,就會(huì)與服務(wù)端建立連接。

processConnection(clintSocket);

注意:

 這里通過(guò)一個(gè)方法來(lái)處理連接。這樣寫(xiě)會(huì)有很大的好處。

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

注意:

    我們首先需要獲得讀和寫(xiě)的流對(duì)象。服務(wù)器需要接收請(qǐng)求(讀),返回響應(yīng)(寫(xiě))。這里使用的是帶有資源的try(),這樣就會(huì)自動(dòng)關(guān)閉流對(duì)象。

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

注意:

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

因?yàn)槲覀儾磺宄蛻舳诉B接后發(fā)送多少次請(qǐng)求,因此我們采用死循環(huán)的方式讀和向客戶端響應(yīng)數(shù)據(jù)。這里不會(huì)一直循環(huán)因?yàn)閟canner當(dāng)讀不到數(shù)據(jù)就會(huì)阻塞。

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

注意:

這里通過(guò)一個(gè)函數(shù)來(lái)處理請(qǐng)求并且返回處理后結(jié)果。由于是回顯服務(wù)器直接返回即可。

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

注意:

我們?yōu)榱朔奖阒苯訉?xiě)字符串,將outputStream轉(zhuǎn)換成PrintWriter。然后將響應(yīng)寫(xiě)入到網(wǎng)卡,并且換行。因?yàn)榭蛻舳撕头?wù)端讀數(shù)據(jù)都是需要空白符結(jié)束的,所以這里必須有一個(gè)空白符。

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

finally {
      clintSocket.close();
}

注意:

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

特別注意:

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

解決方案:

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

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

代碼實(shí)現(xiàn)(有詳細(xì)解釋)

public class TcpEchoSever {
    //Tcp協(xié)議服務(wù)器,使用ServerSocket類,來(lái)建立連接
    private ServerSocket serverSocket = null;
    public TcpEchoSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("啟動(dòng)服務(wù)器");
        //使用線程池,防止客戶端數(shù)量過(guò)多,創(chuàng)建銷毀大量線程開(kāi)銷太大
        //動(dòng)態(tài)變化的線程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            //這里會(huì)阻塞,直到和客戶端建立連接,返回Socket對(duì)象,來(lái)和客戶端通信
            //客戶端構(gòu)造Socket對(duì)象時(shí),會(huì)指定IP和端口,就會(huì)建立連接(客戶端主動(dòng)連接)
            Socket clintSocket = serverSocket.accept();
            threadPool.submit(() -> {
                try {
                    processConnection(clintSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            //要連接多個(gè)客戶端,需要多線程去處理連接
            //這樣才能讓主線程繼續(xù)執(zhí)行到accept阻塞,然后和其他客戶端建立連接(每個(gè)線程是獨(dú)立的執(zhí)行流,彼此之間是并發(fā)的關(guān)系)
            //如果客戶端數(shù)量非常大,這里就會(huì)創(chuàng)建很多線程,數(shù)量過(guò)多對(duì)于系統(tǒng)來(lái)說(shuō)也是很大的開(kāi)銷(使用線程池)
//            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());
        //讀客戶端請(qǐng)求
        //處理請(qǐng)求
        //將結(jié)果寫(xiě)回客戶端(響應(yīng))
        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;
                }
                //讀取請(qǐng)求
                // 注意!! 此處使用 next 是一直讀取到換行符/空格/其他空白符結(jié)束, 但是最終返回結(jié)果里不包含上述 空白符 .
                String request = scanner.next();
                //處理請(qǐng)求
                String response = process(request);
 
                //寫(xiě)回客戶端處理請(qǐng)求結(jié)果(響應(yīng))
                //為了直接寫(xiě)字符串,這里將字節(jié)流轉(zhuǎn)換為字符流
                //也可以將字符串轉(zhuǎn)為字節(jié)數(shù)組
                PrintWriter printWriter = new PrintWriter(outputStream);
                //寫(xiě)入且換行
                printWriter.println(response);
                //寫(xiě)入首先是寫(xiě)入了緩沖區(qū),這里為了保險(xiǎn)就刷新一下緩沖區(qū)
                printWriter.flush();
                System.out.printf("【%s : %d】請(qǐng)求:%s  響應(yīng):%s\n", clintSocket.getInetAddress(), clintSocket.getPort(),
                        request, response);
            }
 
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            //和一個(gè)客戶端建立連接后,返回Socket對(duì)象(使用文件描述表),如果并發(fā)量大(會(huì)創(chuàng)建很多對(duì)象,文件描述符表就有可能滿),就可能導(dǎo)致無(wú)法創(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實(shí)現(xiàn)回顯客戶端

客戶端不需要指定端口號(hào)??蛻舳顺绦蛟谟脩糁鳈C(jī)上,我們?nèi)绻付ň陀锌赡芎推渌绦驔_突,因此讓操作系統(tǒng)隨機(jī)分配一個(gè)空閑的端口號(hào)??蛻舳诵枰鞔_服務(wù)端的ip和端口號(hào),這樣才能明確哪個(gè)主機(jī)和哪個(gè)進(jìn)程。

那么服務(wù)端為什么可以指定端口號(hào)呢?難道就不怕和其他進(jìn)程端口號(hào)沖突嗎?(這里詳解請(qǐng)看上篇文章的解釋)

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

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

注意:

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

這里不需要將字符串ip進(jìn)行轉(zhuǎn)換,可以自動(dòng)轉(zhuǎn)換。

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

注意:

和服務(wù)端一樣首先獲得輸入和輸出流。用包含資源的try可以自動(dòng)關(guān)閉,釋放文件描述符表中的資源。

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

注意:

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

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

注意:

為了直接發(fā)送字符串,這里將outputStream轉(zhuǎn)換成PrintWriter。這里在發(fā)送時(shí)需要換行(空白符),因?yàn)榉?wù)端讀取的next()方法需要空白符。

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

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

注意:

接收響應(yīng),通過(guò)輸入流來(lái)讀取響應(yīng)。將接收的響應(yīng)打印出來(lái)。這里的next()方法和上面一致。

代碼實(shí)現(xiàn)(有詳細(xì)注釋)

public class TcpEchoClint {
    Socket socket = null;
    public TcpEchoClint(String severIp, int severPort) throws IOException {
        //Socket構(gòu)造方法,可以識(shí)別點(diǎn)分十進(jìn)制,不需要轉(zhuǎn)換,比DatageamPacket方便
        //實(shí)例這個(gè)對(duì)象的同時(shí),就會(huì)進(jìn)行連接
        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) {
                //從控制臺(tái)讀取請(qǐng)求
                //空白字符結(jié)束,但不會(huì)讀空白字符
                System.out.println("請(qǐng)輸入請(qǐng)求:");
                String request = scanner.next();
                if(request.equals("exit")) {
                    System.out.println("bye bye");
                    break;
                }
                //發(fā)送請(qǐng)求
                PrintWriter printWriter = new PrintWriter(outputStream);
                //需要發(fā)送空白符,因?yàn)閟canner需要空白符
                printWriter.println(request);
                printWriter.flush();
                //接收響應(yīng)
                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();
    }
}

小結(jié):

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

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

相關(guān)文章

最新評(píng)論