java中TCP實(shí)現(xiàn)回顯服務(wù)器及客戶端
前言:
上篇文章介紹了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)文章
Mybatis使用collection標(biāo)簽進(jìn)行樹(shù)形結(jié)構(gòu)數(shù)據(jù)查詢時(shí)攜帶外部參數(shù)查詢
這篇文章主要介紹了Mybatis使用collection標(biāo)簽進(jìn)行樹(shù)形結(jié)構(gòu)數(shù)據(jù)查詢時(shí)攜帶外部參數(shù)查詢,需要的朋友可以參考下2023-10-10Java實(shí)現(xiàn)系統(tǒng)限流的示例代碼
限流是保障系統(tǒng)高可用的方式之一,也是大廠高頻面試題,它在微服務(wù)系統(tǒng)中,緩存、限流、熔斷是保證系統(tǒng)高可用的三板斧,所以本文我們就來(lái)聊聊如何實(shí)現(xiàn)系統(tǒng)限流吧2023-09-09spring 整合 mybatis 中數(shù)據(jù)源的幾種配置方式(總結(jié)篇)
因?yàn)閟pring 整合mybatis的過(guò)程中, 有好幾種整合方式,尤其是數(shù)據(jù)源那塊,經(jīng)??吹讲灰粯拥呐渲梅绞?,總感覺(jué)有點(diǎn)亂,所以今天有空總結(jié)下,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05Java Dubbo協(xié)議下的服務(wù)端線程使用詳解
Dubbo是阿里開(kāi)源項(xiàng)目,國(guó)內(nèi)很多互聯(lián)網(wǎng)公司都在用,已經(jīng)經(jīng)過(guò)很多線上考驗(yàn)。Dubbo內(nèi)部使用了Netty、Zookeeper,保證了高性能高可用性,使用Dubbo可以將核心業(yè)務(wù)抽取出來(lái),作為獨(dú)立的服務(wù),逐漸形成穩(wěn)定的服務(wù)中心2023-03-03SpringBoot單元測(cè)試使用@Test沒(méi)有run方法的解決方案
這篇文章主要介紹了SpringBoot單元測(cè)試使用@Test沒(méi)有run方法的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01java逐行讀取文件(讀取文件每一行、按行讀取文件)附帶詳細(xì)代碼
這篇文章主要給大家介紹了關(guān)于java逐行讀取文件(讀取文件每一行、按行讀取文件)的相關(guān)資料,讀取文件是我們?cè)谌粘9ぷ髦薪?jīng)常遇到的一個(gè)需求,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09