前言
本人是一個剛剛上路的IT新兵,菜鳥!分享一點(diǎn)自己的見解,如果有錯誤的地方歡迎各位大佬蒞臨指導(dǎo),如果這篇文章可以幫助到你,勞請大家點(diǎn)贊轉(zhuǎn)發(fā)支持一下!
今天分享的內(nèi)容是TCP流套接字實(shí)現(xiàn)的客戶端與服務(wù)器的通信,一定要理解 DatagramSocket,DatagramPacket 這兩個類的作用以及方法,十分有助于你理解服務(wù)器,客戶端代碼。
一、理論準(zhǔn)備
Socket套接字是什么
Socket套接字,是由系統(tǒng)提供用于網(wǎng)絡(luò)通信的技術(shù),是基于TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元。基于Socket套接字的網(wǎng)絡(luò)程序開發(fā)就是網(wǎng)絡(luò)編程。
程序猿?????編寫網(wǎng)絡(luò)程序,主要編寫的是 應(yīng)用層的程序代碼 ,但是真正想要發(fā)送或接收數(shù)據(jù),都是要 通過應(yīng)用層調(diào)用傳輸層 。
因此傳輸層就為應(yīng)用層(為我們編寫代碼)提供了一組api統(tǒng)稱為
Socket api。
簡單來說,這一組api是提供給咱們 編寫網(wǎng)絡(luò)程序使用的接口 , 用來發(fā)送 / 接收網(wǎng)絡(luò)數(shù)據(jù)使用的接口 。
Socket套接字主要針對傳輸層協(xié)議劃分為如下三類:
1?? 數(shù)據(jù)報套接字:使用傳輸層UDP協(xié)議 (本文重點(diǎn)講解)
2?? 流套接字:使用傳輸層TCP協(xié)議 (下篇文章重點(diǎn)講解)
3??原始套接字(不做介紹)
TCP協(xié)議的特點(diǎn)
特點(diǎn) | 說明 |
---|---|
有連接 | 刻意保存對端的相關(guān)信息 |
可靠傳輸 | 盡全力將數(shù)據(jù)傳輸過去不是百分百成功,自己會知道數(shù)據(jù)傳輸是否成功 |
面向字節(jié)流 | 以一個字節(jié)為基本單位(一個數(shù)據(jù)可以分成幾份 多次發(fā)多次收) |
有接收緩沖區(qū),也有發(fā)送緩沖區(qū) | 后續(xù)文章介紹 |
大小不受限 | 對于要傳輸?shù)臄?shù)據(jù)大小沒有要求 |
全雙工 | 一條通信路徑,雙向通信。(可以同時發(fā)送和接收數(shù)據(jù)) |
二、TCP 流套接字提供的API
ServerSocket API
ServerSocket 是 創(chuàng)建TCP服務(wù)端Socket的API 。
Server Socket對象可以理解為一個管家,每當(dāng)有客戶端想要連接服務(wù)器時,他就會為每個連接進(jìn)來的服務(wù)器提供一個專門伺候他的Socket對象(保姆)
ServerSocket構(gòu)造方法 | 方法說明 |
---|---|
ServerSocket(int port) | 創(chuàng)建一個服務(wù)端 流套接字Socket,并綁定到指定端口 |
ServerSocket方法 | 方法說明 |
---|---|
Socket accept() | 開始監(jiān)聽指定端口(創(chuàng)建時綁定的端口),有客戶端連接后,返回一個服務(wù)端Socket對象,并基于該Socket建立與客戶端的連接,否則阻塞等待 |
void close() | 關(guān)閉此套接字 |
Socket API
Socket是客戶端的Socket,或服務(wù)端中接收到客戶端建立連接的請求后,accept方法 返回的服務(wù)端Socket。 是 創(chuàng)建TCP服務(wù)端Socket的API。
Socket對象就是ServerSocket API這個管家分配給每個服務(wù)器的保姆
Socket 構(gòu)造方法 | 方法說明 |
---|---|
Socket(String host, int port) | 創(chuàng)建一個客戶端流套接字Socket,并與對應(yīng)IP的主機(jī)上,對應(yīng)端口的進(jìn)程建立連接 |
Socket 方法 | 方法說明 |
---|---|
InetAddress getInetAddress() | 返回套接字所連接的地址 |
InputStream getInputStream() | 返回此套接字的輸入流,可以直接使用這個輸入流讀取對端發(fā)送的數(shù)據(jù) |
OutputStream getOutputStream() | 返回此套接字的輸出流,可以直接使用這個輸出流向?qū)Χ税l(fā)送數(shù)據(jù) |
三、代碼實(shí)現(xiàn)請求響應(yīng)式 客戶端服務(wù)器
服務(wù)器
TCP 流套接字是字節(jié)流讀取,因此要給每個數(shù)據(jù)規(guī)定一個結(jié)束標(biāo)志,即我們要自定義一個協(xié)議,下面就以換行為結(jié)束標(biāo)志當(dāng)作協(xié)議
服務(wù)器大致就分為三個功能。
1?? 讀取解析客戶端發(fā)來的請求
2?? 根據(jù)請求計(jì)算影響
3?? 把響應(yīng)結(jié)果寫回客戶端
下面代碼中一步一步實(shí)現(xiàn)了這三個功能,并配有詳細(xì)的注釋幫你快速理解
核心思路:
1??服務(wù)器的核心成員屬性 ServerSocket serverSocket (管家且只有一個)
2??構(gòu)造方法要給serverSocket指定端口號便于客戶端連接,
每有一個客戶端連接服務(wù)器,serverSocket就會通過accept方法專門指定一個Socket clientSocket(保姆),并生成一個獨(dú)立線程來服務(wù)這個客戶端。
3??processConnection 方法(服務(wù)器處理客戶端的主邏輯的方法),里面負(fù)責(zé)讀取客戶端的請求,然后調(diào)用process方法(根據(jù)請求計(jì)算響應(yīng)的主邏輯),計(jì)算出響應(yīng)后再返回給客戶端。
processConnection 方法通過clientSocket的getInputStream(),getOutputStream()這兩個方法得到能與對端直接通信的輸入輸出流,實(shí)現(xiàn)發(fā)送與讀取功能。
// 服務(wù)器
public class TcpEchoServer {
// serverSocket 就是管家
// clientSocket 就是伺候每個客戶端的保姆
// serverSocket 只有一個. clientSocket 會給每個客戶端都分配一個~
private ServerSocket serverSocket = null;
// 指定一個端口號綁定,便于客戶端連接
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務(wù)器啟動");
while (true) {
// 如果沒有客戶端連接,accept方法會阻塞等待
Socket clientSocket = serverSocket.accept();
// 如果直接調(diào)用 processConnection(clientSocket)方法
// 那么此時就會進(jìn)入該方法,無法及時處理其他連接進(jìn)來的客戶端的請求
// 解決方案:創(chuàng)建新的線程, 用新線程來調(diào)用 processConnection
// 每次來一個新的客戶端都搞一個新的線程即可!!
// 方法1. 每次都手動創(chuàng)建新線程
/*
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();*/
// 方法2. 創(chuàng)建線程池來解決
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 服務(wù)器處理客戶端請求的主邏輯
private void processConnection(Socket clientSocket) throws IOException {
// 因?yàn)閷Χ耸峭ㄟ^字節(jié)流來發(fā)送的數(shù)據(jù),因此如果對方發(fā)送多條數(shù)據(jù),就無法區(qū)分?jǐn)?shù)據(jù)
// 所以要雙方約定好,數(shù)據(jù)的結(jié)束標(biāo)記,遇到結(jié)束標(biāo)記就代表收到了一個完整的數(shù)據(jù)
// 此次客戶端服務(wù)器使用的結(jié)束標(biāo)記為換行 \n
System.out.printf("[%s,%d 客戶端上線!\n]",clientSocket.getInetAddress().toString(),clientSocket.getPort());
// try () 這種寫法, ( ) 中允許寫多個流對象,
// 并且會在try結(jié)束后,自動調(diào)用對應(yīng)流的close方法
// 使用 ; 來分割
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);// 讀
PrintWriter printWriter = new PrintWriter(outputStream);// 寫
// 沒有這個 scanner 和 printWriter, 完全可以!! 但是代價就是得一個字節(jié)一個字節(jié)扣, 找到哪個是請求的結(jié)束標(biāo)記 \n
// 不是不能做, 而是代碼比較麻煩.
// 為了簡單, 把字節(jié)流包裝成了更方便的字符流~~
while (true) {
// 如果對端關(guān)閉連接,hasNext就會返回false
// 如果對端有數(shù)據(jù),hasNext就會返回true
if (!scanner.hasNext()) {
// 讀取的流到了結(jié)尾了 (對端關(guān)閉了)
System.out.printf("[%s:%d] 客戶端下線!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 1. 讀取請求
// 直接使用 scanner 讀取一段字符串.
// next遇到換行自動停止讀取
String request = scanner.next();
// 2. 根據(jù)請求計(jì)算響應(yīng)
String response = process(request);
// 3. 把響應(yīng)寫回給客戶端. 不要忘了, 響應(yīng)里也是要帶上換行 \n
printWriter.println(response);// 該方法會在放送的同時添加換行添加了換行 \n
//手動刷新緩沖區(qū)
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
// 根據(jù)請求計(jì)算響應(yīng)的邏輯
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
// 實(shí)例化服務(wù)器對象
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
// 啟動主邏輯
tcpEchoServer.start();
}
}
客戶端
客戶端大致就分為三個功能。
1?? 讀取用戶輸入的請求
2?? 將請求發(fā)送至服務(wù)器
3?? 讀取服務(wù)器的響應(yīng)
4?? 將響應(yīng)轉(zhuǎn)換為字符串并打印
下面代碼中一步一步實(shí)現(xiàn)了這四個功能,并配有詳細(xì)的注釋幫你快速理解
核心思路:
1??服務(wù)器的核心成員屬性 Socket socket (保姆)
2??構(gòu)造方法要給socket指定IP地址與端口號與服務(wù)器進(jìn)行連接。
3??start方法(客戶端主邏輯),用來向服務(wù)器發(fā)送請求與讀取服務(wù)器的響應(yīng)
start方法通過socket的getInputStream(),getOutputStream()這兩個方法得到能與對端直接通信的輸入輸出流,實(shí)現(xiàn)發(fā)送與讀取功能。
// 客戶端
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int port) throws IOException {
// 這個操作相當(dāng)于讓客戶端和服務(wù)器建立 tcp 連接.
// 這里的連接連上了, 服務(wù)器的 accept 就會返回.
this.socket = new Socket(serverIp,port);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Scanner scannerFromSocket = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream)){
while (true) {
// 1. 從鍵盤上讀取用戶輸入的內(nèi)容.
System.out.print("-> ");
String request = scanner.next();
// 2. 把讀取的內(nèi)容構(gòu)造成請求, 發(fā)送給服務(wù)器.
// 注意, 這里的發(fā)送, 是帶有換行的!!
printWriter.println(request);
printWriter.flush();// 手動刷新緩沖區(qū)
// 3. 從服務(wù)器讀取響應(yīng)內(nèi)容
String response = scannerFromSocket.next();
// 4. 把響應(yīng)結(jié)果顯示到控制臺上.
System.out.printf("req: %s; resp: %s\n", request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
// 實(shí)例化客戶端對象
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
// 啟動客戶端主邏輯
tcpEchoClient.start();
}
}
通信結(jié)果:
如何同時多次運(yùn)行同一個代碼
選中第一個即可。
疑惑解答
為什么服務(wù)器進(jìn)程需要手動指定端口號而客戶端進(jìn)程不需要
服務(wù)器的功能是用來處理其他客戶端發(fā)來的請求,因此需要為客戶端提供自己的端口號,方便客戶端進(jìn)行訪問。
雖然服務(wù)器要給客戶端一個響應(yīng),但是客戶端的IP地址與端口號都可以在客戶端發(fā)來請求的數(shù)據(jù)報中獲得,因此客戶端不需要手動指定端口號
為什么客戶端中的服務(wù)器IP與端口號是"127.0.0.1" 與 9090
127.0.0.1 是主機(jī)環(huán)回地址。主機(jī)環(huán)回是指地址為 127.0.0.1 的任何數(shù)據(jù)包都不應(yīng)該離開計(jì)算機(jī)(主機(jī)),發(fā)送它——而不是被發(fā)送到本地網(wǎng)絡(luò)或互聯(lián)網(wǎng),它只是被自己“環(huán)回”,并且發(fā)送數(shù)據(jù)包的計(jì)算機(jī)成為接收者。
端口號是9090是因?yàn)槭请S意指定的,當(dāng)然也有一些特殊端口號被指定分配給了一些牛逼的程序。
為什么服務(wù)器Socket對象要關(guān)閉,ServerSocket對象卻不用,客戶端的Socket對象也不用關(guān)閉
Socket對象與ServerSocket對象都會產(chǎn)生文件描述符,如果如果文件描述符表滿了會產(chǎn)生文件資源泄露的嚴(yán)重bug,那么為什么有的調(diào)用,有的沒有調(diào)用close方法???
服務(wù)器的Socket對象為什么要關(guān)閉?
因?yàn)槊坑幸粋€客戶端連接服務(wù)器,服務(wù)器當(dāng)中的就會產(chǎn)生一個Socket對象(保姆),如果有n個客戶端連接服務(wù)器,那么服務(wù)器就很有可能會產(chǎn)生文件資源泄露,因此服務(wù)器的Socket對象在完成業(yè)務(wù)后,要調(diào)用close方法
服務(wù)器的ServerSocket對象為什么不用關(guān)閉?
服務(wù)器就只有唯一一個ServerSocket對象(管家),他會伴隨服務(wù)器整個生命周期,調(diào)用close的時候,也就是服務(wù)器這個進(jìn)程結(jié)束的時候,因此沒必要調(diào)用,進(jìn)程結(jié)束時會自動將文件關(guān)閉。
每個客戶端都只有唯一一個Socket對象,他也會伴隨整個客戶端的生命周期,調(diào)用close的時候,也就是客戶端這個進(jìn)程結(jié)束的時候,因此沒必要調(diào)用。
緩沖區(qū)是什么?為什么要手動刷新緩沖區(qū)???
讀寫硬盤,讀寫網(wǎng)卡都視為IO操作
網(wǎng)卡的IO操作很慢,為了提高效率就引入了緩沖區(qū)。
假如要往網(wǎng)卡中寫入10次,那么就先把這些數(shù)據(jù)都寫進(jìn)緩沖區(qū),等緩沖區(qū)滿了,就集中寫入網(wǎng)卡1次,這樣就盡量減少了IO的操作次數(shù),就提高了效率。
因此只有緩沖區(qū)滿了,才會真正寫入網(wǎng)卡。
因此代碼中要手動刷新緩沖區(qū),才能保證無論數(shù)據(jù)大小都可以及時發(fā)送。
總結(jié)
以上就是今天要分享的內(nèi)容,本文介紹了Socket套接字,以及使用TCP協(xié)議的特點(diǎn)以及TCP流套接字實(shí)現(xiàn)的客戶端與服務(wù)器的通信。網(wǎng)絡(luò)編程讓我愈發(fā)感覺到了編程的魅力,也讓我領(lǐng)略到了科技的神奇。各位加油!文章來源:http://www.zghlxwxcb.cn/news/detail-660981.html
路漫漫不止修身,也養(yǎng)性。文章來源地址http://www.zghlxwxcb.cn/news/detail-660981.html
到了這里,關(guān)于網(wǎng)絡(luò)編程3——TCP Socket實(shí)現(xiàn)的客戶端服務(wù)器通信完整代碼(詳細(xì)注釋幫你快速理解)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!