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ù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mybatis使用collection標簽進行樹形結構數(shù)據(jù)查詢時攜帶外部參數(shù)查詢
這篇文章主要介紹了Mybatis使用collection標簽進行樹形結構數(shù)據(jù)查詢時攜帶外部參數(shù)查詢,需要的朋友可以參考下2023-10-10spring 整合 mybatis 中數(shù)據(jù)源的幾種配置方式(總結篇)
因為spring 整合mybatis的過程中, 有好幾種整合方式,尤其是數(shù)據(jù)源那塊,經??吹讲灰粯拥呐渲梅绞?,總感覺有點亂,所以今天有空總結下,感興趣的朋友跟隨腳本之家小編一起學習吧2018-05-05SpringBoot單元測試使用@Test沒有run方法的解決方案
這篇文章主要介紹了SpringBoot單元測試使用@Test沒有run方法的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01java逐行讀取文件(讀取文件每一行、按行讀取文件)附帶詳細代碼
這篇文章主要給大家介紹了關于java逐行讀取文件(讀取文件每一行、按行讀取文件)的相關資料,讀取文件是我們在日常工作中經常遇到的一個需求,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2023-09-09