java中TCP實現(xiàn)回顯服務器及客戶端
前言:
上篇文章介紹了TCP的特點。由于TCP的特點是有連接,面向字節(jié)流,可靠傳輸?shù)龋覀兙涂梢韵胂蟮絋CP的代碼和UDP會有一定的差異。TCP和UDP具體使用哪種協(xié)議需要根據(jù)實際業(yè)務需求來選擇。
Socket API
Socket是客戶端Socket,或服務端中接收到客戶端建立連接(accept方法)的請求后,返回的服務端Socket。
不管是客戶端還是服務端Socket,都是雙方建立連接后,保存兩端信息,及用來與對方收發(fā)數(shù)據(jù)的。
Socket構造方法
![]()
注意:
創(chuàng)建一個客戶端流套接字Socket,并與對應IP的主機上,對應端口的進程建立連接。當服務端accept()阻塞時,客戶端一旦實例出Socket對象,就會建立連接。
Socket方法
![]()
注意:
獲得套接字輸入流。如果建立連接,服務端調(diào)用這個方法,就是讀取客戶端請求。
![]()
注意:
獲得套接字輸出流。如果建立連接,服務端調(diào)用這個方法,就是往客戶端返回響應。
![]()
注意:
連接后獲得對方的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)兩者使用場景不同:短連接一般適用于客戶端請求頻率不高的場景(瀏覽網(wǎng)頁)。長連接一般適用于客戶端與服務端通信頻繁的場景。(聊天室)
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()方法會阻塞等待??蛻舳薙ocket對象一旦實例化,就會與服務端建立連接。
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)榱朔奖阒苯訉懽址?,將outputStream轉(zhuǎn)換成PrintWriter。然后將響應寫入到網(wǎng)卡,并且換行。因為客戶端和服務端讀數(shù)據(jù)都是需要空白符結束的,所以這里必須有一個空白符。
由于數(shù)據(jù)首先會寫入緩沖區(qū),我們將緩沖區(qū)刷新一下保證數(shù)據(jù)正常寫入到文件中(網(wǎng)卡)
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ā)量來調(diào)整線程數(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é)流轉(zhuǎn)換為字符流
//也可以將字符串轉(zhuǎn)為字節(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上,我們?nèi)绻付ň陀锌赡芎推渌绦驔_突,因此讓操作系統(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進行轉(zhuǎn)換,可以自動轉(zhuǎn)換。
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轉(zhuǎn)換成PrintWriter。這里在發(fā)送時需要換行(空白符),因為服務端讀取的next()方法需要空白符。
數(shù)據(jù)首先寫入緩沖區(qū),為了保證數(shù)據(jù)寫入到文件(網(wǎng)卡),這里手動刷新一下緩沖區(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構造方法,可以識別點分十進制,不需要轉(zhuǎn)換,比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回顯服務器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springMVC?@RestControllerAdvice注解使用方式
這篇文章主要介紹了springMVC?@RestControllerAdvice注解使用方式,下面通過一個簡單的示例,演示如何使用?@RestControllerAdvice,感興趣的朋友跟隨小編一起看看吧2024-08-08
Java中的線程池ThreadPoolExecutor深入解析
這篇文章主要介紹了Java中的線程池ThreadPoolExecutor深入解析,線程池,thread pool,是一種線程使用模式,線程池維護著多個線程,等待著監(jiān)督管理者分配可并發(fā)執(zhí)行的任務,需要的朋友可以參考下2023-11-11
springboot多模塊多環(huán)境配置文件問題(動態(tài)配置生產(chǎn)和開發(fā)環(huán)境)
這篇文章主要介紹了springboot多模塊多環(huán)境配置文件問題(動態(tài)配置生產(chǎn)和開發(fā)環(huán)境),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04

