前言
??各位讀者好, 我是小陳, 這是我的個人主頁
??小陳還在持續(xù)努力學習編程, 努力通過博客輸出所學知識
??如果本篇對你有幫助, 煩請點贊關注支持一波, 感激不盡
?? 希望我的專欄能夠幫助到你:
JavaSE基礎: 基礎語法, 類和對象, 封裝繼承多態(tài), 接口, 綜合小練習圖書管理系統(tǒng)等
Java數(shù)據(jù)結構: 順序表, 鏈表, 堆, 二叉樹, 二叉搜索樹, 哈希表等
JavaEE初階: 多線程, 網(wǎng)絡編程, TCP/IP協(xié)議, HTTP協(xié)議, Tomcat, Servlet, Linux, JVM等(正在持續(xù)更新)
上篇文章介紹了基于 UDP 協(xié)議的 Socket API, 以及簡單寫了一個回顯服務器, 實現(xiàn)了服務器和客戶端之間的網(wǎng)絡通信
本篇將介紹網(wǎng)絡編程中 : 基于 TCP 協(xié)議的 Socket 套接字的相關知識
提示:是正在努力進步的小菜鳥一只,如有大佬發(fā)現(xiàn)文章欠佳之處歡迎批評指點~ 廢話不多說,直接上干貨!
一、認識 Socket(套接字), TCP 協(xié)議和 UDP 協(xié)議
以下內容上篇介紹過了, 看過上篇文章的讀者可以跳過
上篇提到, 我們程序員進行網(wǎng)絡編程主要是在 TCP/IP 五層網(wǎng)絡模型中的應用層, 而數(shù)據(jù)在網(wǎng)絡上傳輸, 需要進行封裝和分用, 其中應用層需要調用傳輸層提供的 API , 這一組 API 就被稱作 Socket API
1, 什么是 Socket(套接字)
概念 : Socket 套接字是由系統(tǒng)提供于網(wǎng)絡通信的技術, 是基于 TCP/IP 協(xié)議的網(wǎng)絡通信的基本操作黨員, 基于 Socket 套接字的網(wǎng)絡程序開發(fā)就是網(wǎng)絡編程
要進行網(wǎng)絡通信, 需要有一個 socket 對象, 一個 socket 對象對應著一個 socket 文件, 這個文件在 網(wǎng)卡上而不是硬盤上, 所以有了 sokcet 對象才能通過操作內存來操作網(wǎng)卡
在 socket 文件中寫數(shù)據(jù)相當于通過網(wǎng)卡發(fā)送數(shù)據(jù), 在 socket 文件中讀數(shù)據(jù)相當于通過網(wǎng)卡接收數(shù)據(jù)
Socket API 分為兩類 : 基于 TCP 協(xié)議的 API , 和基于 UDP 協(xié)議的 API, 下面先認識一下 TCP 協(xié)議和 UDP 協(xié)議的區(qū)別和特點
2, 淺談 TCP 協(xié)議和 UDP 協(xié)議的區(qū)別和特點
TCP 協(xié)議 | 說明 | UDP 協(xié)議 | 說明 |
---|---|---|---|
有連接 | 通信雙方需要刻意保存對方的相關信息 | 無鏈接 | 通信雙方不需要刻意保存對方的信息 |
可靠傳輸 | 如果數(shù)據(jù)發(fā)送不成功, 發(fā)送方會知道 | 不可靠傳輸 | 發(fā)送方不關心數(shù)據(jù)是否發(fā)送成功 |
面向字節(jié)流 | 發(fā)送的數(shù)據(jù)以字節(jié)為單位 | 面向數(shù)據(jù)報 | 發(fā)送的數(shù)據(jù)以 UDP 數(shù)據(jù)報為單位 |
全雙工 | 雙向通信 | 全雙工 | 雙向通信 |
這里只做簡單介紹, 這兩個協(xié)議后續(xù)會單獨詳細介紹
二、基于 TCP 協(xié)議的 Socket API
首先要明確 TCP 協(xié)議和 UDP 協(xié)議的很重要的區(qū)別 : TCP 協(xié)議是有鏈接, 面向字節(jié)流傳輸, 主要體現(xiàn)在 : 發(fā)送方和接收方在網(wǎng)絡通信之間要先建立連接, 并且傳輸?shù)臄?shù)據(jù)的基本單位是字節(jié)
基于 TCP 協(xié)議的 Socket API 中, 要分清楚以下兩個類 :
類名 | 解釋 |
---|---|
ServerSocket | 只能服務器使用, 客戶端不能使用, 這個類是在等待客戶端發(fā)起連接之前不做任何事的"監(jiān)聽器" |
Socket | 服務器或客戶端都可以使用, 客戶端使用這個類向服務器發(fā)起連接之后, 雙端都使用這個類進行網(wǎng)絡通信 |
這兩個類的聯(lián)系就是, 服務器啟動之后, 先使用 ServerSocket 類等待客戶端發(fā)來連接請求, 連接成功后服務器和客戶端都使用 Socket 類進行通信
1, ServerSocket 類
ServerSocket 類的構造方法 :
方法簽名 | 作用 |
---|---|
ServerSocket (int port) | 創(chuàng)建一個 ServerSocket 對象, 一般用于服務器, 需要指定本機端口號 |
ServerSocket 類的成員方法 :
方法簽名 | 作用 |
---|---|
Socket accept() | 開始"監(jiān)聽", 有客戶端發(fā)來連接請求之后, 返回一個用于服務器使用的 Socket 對象, 如果客戶端沒有發(fā)起連接, 則阻塞等待 |
void close() | 關閉 ServerSocket |
2, Socket 類
再次說明, Socket 這個類用于客戶端, 也可以在服務器與客戶端連接之后使用, 無論客戶端或服務器使用, 都會保存對端的相關信息
Socket 類的構造方法 :
方法簽名 | 作用 |
---|---|
Socket(String host, int port) | 一般用于客戶端, 需要指定服務器的 IP 地址和端口號 |
void close() | 用于關閉 ServerSocket |
Socket 類的成員方法 :
由于 TCP 協(xié)議是面向字節(jié)流, 所以有兩個關于字節(jié)流輸入輸出的成員方法
方法簽名 | 作用 |
---|---|
InputStream getInputStream() | 獲取 Socket 的字節(jié)輸入流 |
OutputStream getOutputStream() | 獲取 Socket 的字節(jié)輸出流 |
InetAddress getInetAddress() | 獲取對端的 IP 地址 |
InetAddress getPort() | 獲取對端的端口號 |
調用 getInputStream() 和 getOutputStream() 這個兩個方法, 就可以通過字節(jié)流對象, 從網(wǎng)卡中讀寫數(shù)據(jù)
getInputStream()返回的對象用來輸入(讀), 從網(wǎng)卡讀數(shù)據(jù)到內存(從網(wǎng)卡接收數(shù)據(jù))
getOutputStream返回的對象用來輸出(寫), 從網(wǎng)卡寫數(shù)據(jù)到內存(從網(wǎng)卡發(fā)送數(shù)據(jù))
先對上述 API 有個印象即可, 接下來逐行解析如何從 0 到 1 地進行客戶端和服務器之間地網(wǎng)絡編程, 代碼敲完之后再消化吸收
三、逐行代碼解析網(wǎng)絡編程
下面我們還是寫一個最簡單的客戶端服務器網(wǎng)絡通信模型 : 客戶端給服務器發(fā)送什么請求, 服務器就給客戶發(fā)送什么響應(這是最簡單但是毫無意義的回顯服務器, 只是方便熟悉 TCP Socket 的 API 使用)
客戶端和服務器各自為一個進程在運行, 雙方互不干涉(當然我們現(xiàn)在要寫的客戶端服務器程序是在同一臺主機上的)
一定是服務器先啟動, 一直等待客戶端發(fā)來請求, 所以按照時間順序, 代碼邏輯應該如下所示 :
客戶端 | 服務器 |
---|---|
/ | 1, 啟動服務器, 構造 ServerSocket 對象, 調用 accept() 時刻準備和客戶端連接 |
2, 構造 Socket 對象即為發(fā)起連接 | / |
/ | 3, 連接成功, 通過 accept() 的返回值得到 Socket 對象 |
4, 把請求寫入網(wǎng)卡 | |
/ | 5, 從網(wǎng)卡讀取請求 |
/ | 6, 處理請求 |
/ | 7, 把響應寫入網(wǎng)卡 |
8, 從網(wǎng)卡讀取響應 | / |
有了這個思路, 下面正式開始使用上述 API 進行網(wǎng)絡編程
1, 逐行解析客戶端
創(chuàng)建一個類 : TCPEchoClient 作為客戶端
成員屬性 :
需要定義一個 Scoket 對象來進行和服務器的通信
public class TCPEchoClient {
// 成員屬性
private Socket socket = null;
}
構造方法 :
用于實例化客戶端的 socket 對象, 別忘了需要綁定服務器的 IP 地址和端口號
public class TCPEchoClient {
// 成員屬性
private Socket socket = null;
// 構造方法
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
}
main 方法 :
1, 構造 tcpEchoClient 對象, 由于服務器在本機, IP 地址為"127.0.0.1", 端口號隨意指定 [1024, 65535] 之間的任意一個
2, 調用 TCPEchoClient 類的核心成員方法 start(), 這個方法實現(xiàn)了客戶端的核心邏輯
public class TCPEchoClient {
// 成員屬性
private Socket socket = null;
// 構造方法
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
// main 方法
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
1.1, 核心成員方法 start()
1??構造一個 Scanner 對象, 從控制臺輸入字符串, 這個字符串當作請求的內容
2??核心邏輯在一個 while(true) 循環(huán)中, 實現(xiàn)多次發(fā)送請求
public void start() {
Scanner in = new Scanner(System.in);
// 發(fā)送多個請求
while (true) {
}
}
}
由于TCP協(xié)議是面向字節(jié)流傳輸, 所以為了方便讀寫數(shù)據(jù), 我們把字節(jié)流轉化成字符流處理
所以在進入 while 循環(huán)之前, 先構造字符流的輸入輸出對象
public void start() {
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 把字節(jié)流轉換成字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 發(fā)送多個請求
while (true) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
getInputStream()返回的對象用來輸入(讀), 從網(wǎng)卡讀數(shù)據(jù)到內存(從網(wǎng)卡接收數(shù)據(jù))
getOutputStream返回的對象用來輸出(寫), 從網(wǎng)卡寫數(shù)據(jù)到內存(從網(wǎng)卡發(fā)送數(shù)據(jù))
然后每次進入循環(huán), 主要有兩個操作 : 1, 把請求寫入網(wǎng)卡 2, 把響應從網(wǎng)卡中讀出來, 寫的使用調用 println(), 讀的時候調用 next(), 這樣能以空白符為結束標志進行讀寫數(shù)據(jù)
圖解如下 :
2, 逐行解析服務器
創(chuàng)建一個類 TCPEchoServer 作為服務器
成員屬性 :
需要定義一個 ServerSocket 對象, 用來等待客戶端發(fā)來連接的"監(jiān)聽器"
public class TCPEchoServer {
// 構造方法
private ServerSocket serverSocket = null;
}
構造方法 :
用于實例化客戶端的 ServerSocket 對象, 別忘了需要綁定本機端口號
public class TCPEchoServer {
// 構造方法
private ServerSocket serverSocket = null;
// 構造方法
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
}
main 方法 :
1, 構造 tcpEchoServer 對象, 需要綁定端口號, 必須和客戶端那邊綁定的一致
2, 調用 tcpEchoServer 類的核心成員方法 start(), 這個方法實現(xiàn)了服務器的核心邏輯
public class TCPEchoServer {
// 構造方法
private ServerSocket serverSocket = null;
// 構造方法
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// main 方法
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
2.1, 核心成員方法 start()
由于 TCP 是有連接的傳輸協(xié)議, 所以服務器在和客戶端連接之前, 要先和客戶端建立連接, 也就是調用 accept(), 連接成功之后, 服務器就可以處理這個連接了
如果有多個服務器來和客戶端連接, 服務器就需要處理多個連接, 所以把上述過程寫在 while(true) 中
public void start() throws IOException {
while (true) {
// 建立連接 返回一個 Socket 對象
Socket socket = serverSocket.accept();
// 處理連接到的這個客戶端
processConnection(socket);
}
}
處理連接的過程其實就是從網(wǎng)卡中讀取數(shù)據(jù), 處理響應, 再把響應寫回網(wǎng)卡, 我們把這個過程封裝成 processConnection(Socket socket);
注意 : 調用 accept() 的是 ServerSocket 的對象, 而這個方法的返回值是Socket, 上面已經(jīng)強調過了
接下來解析 processConnection() 的過程
和客戶端一樣, 要先把字節(jié)流轉化成字符流, 方便讀寫數(shù)據(jù)
private void processConnection(Socket socket) {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客戶端上線");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 處理多個請求
while(true) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
getInputStream()返回的對象用來輸入(讀), 從網(wǎng)卡讀數(shù)據(jù)到內存(從網(wǎng)卡接收數(shù)據(jù))
getOutputStream返回的對象用來輸出(寫), 從網(wǎng)卡寫數(shù)據(jù)到內存(從網(wǎng)卡發(fā)送數(shù)據(jù))
然后就是while循環(huán), 進入循環(huán)后主要就三個操作: 1, 從網(wǎng)卡中讀取數(shù)據(jù) 2, 處理響應 3, 再把響應寫回網(wǎng)卡
圖解如下 :
看到這里, 應該感受到了 TCP 和 UDP 的不同之處體現(xiàn)在哪了
首先是 TCP 的服務器需要先使用 ServerSocket 建立連接, 建立連接之后服務器和客戶端都是用 Socket 進行通信
通信時, TCP 進行傳輸使用的是字節(jié)流, 直接從網(wǎng)卡讀寫, 但我們可以轉化成字符流, 而 UDP 進行傳輸是把數(shù)據(jù)封裝成 DatagramPacket(數(shù)據(jù)報), 再進行發(fā)送和接收
3, bug 修改
3.1, bug1
上述代碼中, 有個隱性的嚴重的 bug, 由于服務器可能是處理多個客戶端連接, 那么處理完客戶端 A 后, 服務器這個進程不一定會結束, 很有可能還要處理客戶端 B
所以服務器和某個客戶端進行通信時打開的 Socket 文件就必須在 finally 語句塊中調用 close(), 以避免內存資源泄露, 修改后的代碼如下 :
private void processConnection(Socket socket) throws IOException {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客戶端上線");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
// 處理多個請求
while(true) {
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
客戶端那邊不需要調用 close() 是因為在當前場景下, 客戶端的 Socket 生命爭取伴隨著整個客戶端進程, 不會出現(xiàn)頻繁創(chuàng)建 Socket 但沒有 close 導致內存資源泄露
3.2, bug2
還有一個顯性的 bug, 我們首先打開兩個客戶端, 步驟如下 :
然后先運行服務器, 再運行兩個客戶端, 觀察運行效果 :
1 號客戶端 :
2 號客戶端 :
服務器 :
會發(fā)現(xiàn), 第二個開啟的客戶端并沒有和服務器成功通信, 這是因為, 我們的服務器處理多個連接時, 是在一個while循環(huán)中, 如果第一個連接的客戶端沒有下線, 就不會接收第二個客戶端的連接
public void start() throws IOException {
while (true) {
// 建立連接 返回一個 Socket 對象
Socket socket = serverSocket.accept();
// 處理連接到的這個客戶端
processConnection(socket);
}
}
正確的代碼應該是, 每連接成功一個客戶端, 就開啟一個線程來處理這個連接, 修改后的代碼如下 :
public void start() throws IOException {
while (true) {
// 建立連接 返回一個 Socket 對象
Socket socket = serverSocket.accept();
// 處理連接到的這個客戶端
Thread thread = new Thread( () -> {
try {
processConnection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
// 別忘了調用 start() 啟動線程
thread.start();
}
}
3.3, 最終運行效果
1 號客戶端 :
2 號客戶端 :
服務器 :
四、完整代碼
1, 客戶端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TCPEchoClient {
private Socket socket = null;
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
public void start() {
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 把字節(jié)流轉換成字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 發(fā)送多個請求
while (true) {
// 1,從控制臺輸入字符串
String requestString = in.next();
// 2,寫入請求
printWriter.println(requestString);
printWriter.flush();
// 3,讀取請求
String responseString = inFromSocket.next();
// 控制臺 打印請求字符串 + 響應字符串
System.out.println(requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
2, 服務器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TCPEchoServer {
private ServerSocket serverSocket = null;
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while (true) {
// 建立連接 返回一個 Socket 對象
Socket socket = serverSocket.accept();
// 處理連接到的這個客戶端
Thread thread = new Thread( () -> {
try {
processConnection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
// 別忘了調用 start() 啟動線程
thread.start();
}
}
private void processConnection(Socket socket) throws IOException {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客戶端上線");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 處理多個請求
while(true) {
if (!inFromSocket.hasNext()) {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客戶端下線");
break;
}
// 1,讀取請求
String requestString = inFromSocket.next();
// 2,處理請求
String responseString = process(requestString);
// 3,寫入響應
printWriter.println(responseString);
printWriter.flush();
// 控制臺打印 客戶端IP地址 + 客戶端端口號 + 請求字符串 + 響應字符串
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
private String process(String requestString) {
return requestString;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
總結
以上就是本篇的全部內容, 主要介紹了 : 基于 TCP協(xié)議的 Socket API , 以及利用這些 API 寫了一個最簡單但無意義的客戶端服務器網(wǎng)絡通信程序
再回顧一下, Socket 類的成員方法 :
由于TCP協(xié)議是面向字節(jié)流, 所以有兩個成員方法是關于字節(jié)流輸入輸出的
方法簽名 | 作用 |
---|---|
InputStream getInputStream() | 獲取 Socket 的字節(jié)輸入流 |
OutputStream getOutputStream() | 獲取 Socket 的字節(jié)輸出流 |
InetAddress getInetAddress() | 獲取對端的 IP 地址 |
InetAddress getPort() | 獲取對端的端口號 |
如果本篇對你有幫助,請點贊收藏支持一下,小手一抖就是對作者莫大的鼓勵啦??????~文章來源:http://www.zghlxwxcb.cn/news/detail-744896.html
上山總比下山辛苦
下篇文章見文章來源地址http://www.zghlxwxcb.cn/news/detail-744896.html
到了這里,關于Java【網(wǎng)絡編程2】使用 TCP 的 Socket API 實現(xiàn)客戶端服務器通信(保姆級教學, 附代碼)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!