目錄
前言:
1.網(wǎng)絡(luò)編程的基礎(chǔ)
1.1為什么需要網(wǎng)絡(luò)編程
1.2什么是網(wǎng)絡(luò)編程
1.3網(wǎng)絡(luò)編程中的基本概念
1.3.1發(fā)送端和接收端
1.3.2請(qǐng)求和響應(yīng)
1.3.3客戶端和服務(wù)端
2.Socket套接字
2.1概念
2.2分類(lèi)
3.UDP數(shù)據(jù)報(bào)套接字編程
3.1DataGramSocket?API
3.2DatagramPacket?API
3.3基于UDP的回顯服務(wù)器(echo?server)
3.4簡(jiǎn)單的翻譯服務(wù)器
4.TCP流套接字
4.1ServerSocket?API
4.2Socket?API
4.3基于TCP的回顯程序
5.再談協(xié)議
結(jié)束語(yǔ):
前言:
在上一節(jié)中小編主要是與大家分享了一些有關(guān)于網(wǎng)絡(luò)的基礎(chǔ)知識(shí),但是里面的細(xì)節(jié)和基礎(chǔ)的編程還沒(méi)有給大家來(lái)交代,這節(jié)中小編就給大家倆交代一下有關(guān)于網(wǎng)絡(luò)基礎(chǔ)編程方面的一些基礎(chǔ)的編程吧,大家趕快跟上小編的步伐一起來(lái)往下看吧。如果還沒(méi)有看小編網(wǎng)絡(luò)基礎(chǔ)知識(shí)的部分的同學(xué)建議先去看看這篇博文吧:?http://t.csdn.cn/aj9ov
1.網(wǎng)絡(luò)編程的基礎(chǔ)
1.1為什么需要網(wǎng)絡(luò)編程
用戶在瀏覽器中,打開(kāi)在線視頻網(wǎng)站,比如抖音短視頻其實(shí)質(zhì)是通過(guò)網(wǎng)絡(luò),獲取到網(wǎng)絡(luò)上的一個(gè)視頻資源,與本地打開(kāi)視頻文件類(lèi)似,只是視頻文件這個(gè)資源的來(lái)源是網(wǎng)絡(luò),相比本地資源來(lái)說(shuō),網(wǎng)絡(luò)提供了更為豐富的網(wǎng)絡(luò)資源,所謂的網(wǎng)絡(luò)資源,其實(shí)就是在網(wǎng)絡(luò)中可以獲取的各種數(shù)據(jù)資源,而所有的網(wǎng)絡(luò)資源,都是通過(guò)網(wǎng)絡(luò)編程來(lái)進(jìn)行數(shù)據(jù)傳輸?shù)摹?/strong>
1.2什么是網(wǎng)絡(luò)編程
網(wǎng)絡(luò)編程:指網(wǎng)絡(luò)上的主機(jī),通過(guò)不同的進(jìn)程,以編程的方式實(shí)現(xiàn)網(wǎng)絡(luò)通信(網(wǎng)絡(luò)數(shù)據(jù)傳輸)。
當(dāng)然,我們只要滿足進(jìn)程不同就行,所以即便是同一個(gè)主機(jī),只要是不同進(jìn)程,基于網(wǎng)絡(luò)來(lái)傳輸數(shù)據(jù),也屬于網(wǎng)絡(luò)編程。
1.3網(wǎng)絡(luò)編程中的基本概念
1.3.1發(fā)送端和接收端
在一次網(wǎng)絡(luò)數(shù)據(jù)傳輸時(shí):
- 發(fā)送端:數(shù)據(jù)的發(fā)送方進(jìn)程,稱(chēng)為發(fā)送端。發(fā)送端主機(jī)即網(wǎng)絡(luò)通信中的源主機(jī)。
- 接收端:數(shù)據(jù)的接收方進(jìn)程,稱(chēng)為接收端。接收端主機(jī)即網(wǎng)絡(luò)通信中的目的主機(jī)。
- 收發(fā)端:發(fā)送端和接收端兩端,也簡(jiǎn)稱(chēng)Wie收發(fā)端。
注意:發(fā)送端和接收端只是相對(duì)的概念,只是一次網(wǎng)絡(luò)數(shù)據(jù)傳輸產(chǎn)生數(shù)據(jù)流向后的概念。
1.3.2請(qǐng)求和響應(yīng)
一般來(lái)說(shuō),獲取一個(gè)網(wǎng)絡(luò)資源,涉及到兩次網(wǎng)絡(luò)數(shù)據(jù)傳輸。
- 第一次:請(qǐng)求數(shù)據(jù)的發(fā)送。
- 第二次:響應(yīng)數(shù)據(jù)的發(fā)送。
就像是在餐廳點(diǎn)飯一樣,先發(fā)起請(qǐng)求:點(diǎn)一份蛋炒飯。餐廳在給一個(gè)響應(yīng):提供一份蛋炒飯。
1.3.3客戶端和服務(wù)端
- 服務(wù)端:在常見(jiàn)的網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)膱?chǎng)景下,把提供服務(wù)的這一方進(jìn)程,稱(chēng)為服務(wù)端,可以提供對(duì)外服務(wù)。
- 客戶端:獲取服務(wù)的一方進(jìn)程,稱(chēng)為客戶端。
對(duì)于服務(wù)來(lái)說(shuō),一般是提供:
- 客戶端獲取服務(wù)資源。
- 客戶端保存資源在服務(wù)端。
就像是我們?cè)阢y行辦事:
- 銀行提供存款服務(wù):用戶(客戶端)保存現(xiàn)金(資源)在銀行(服務(wù)端)。
- 銀行提供取款服務(wù):用戶(客戶端)獲取服務(wù)端資源(銀行替用戶保管現(xiàn)金)。
2.Socket套接字
2.1概念
Socket套接字是操作系統(tǒng)提供給應(yīng)用程序的一組用于網(wǎng)絡(luò)編程的API。他是基于TCP/IP協(xié)議的通信的的基本操作單元。
注意:操作系統(tǒng)原生的Socket?API是C語(yǔ)言的但是這里我們學(xué)習(xí)的是Java封裝之后的版本。
2.2分類(lèi)
Socket套接字主要針對(duì)傳輸層協(xié)議劃分為如下三類(lèi):
- 數(shù)據(jù)報(bào)套接字:使用傳輸層UDP協(xié)議。
UDP,即User?Datagram?Protocol(用戶數(shù)據(jù)報(bào)協(xié)議),傳輸層協(xié)議。它的特點(diǎn)是:無(wú)連接、不可靠傳輸、面向數(shù)據(jù)報(bào)、全雙工。
- 流套接字:使用傳輸層TCP協(xié)議。
TCP,即Transmission?Control?Protocol(傳輸控制協(xié)議),傳輸層協(xié)議。它的特點(diǎn)是:有連接、可靠傳輸、面向字節(jié)流、全雙工。
對(duì)于字節(jié)流來(lái)說(shuō),可以簡(jiǎn)單理解為傳輸?shù)臄?shù)據(jù)是基于IO流的,流式數(shù)據(jù)的特征就是在IO流沒(méi)喲關(guān)閉的情況下,是無(wú)邊界的數(shù)據(jù),可以多次發(fā)送,也可以分開(kāi)多次接收。
- 原始套接字:
原始套接字用于自定義傳出層協(xié)議,用于讀寫(xiě)內(nèi)核沒(méi)有處理的IP協(xié)議數(shù)據(jù),這里我們對(duì)此不做過(guò)多討論,我們重點(diǎn)是理解和應(yīng)用前兩個(gè)。
TCP特點(diǎn)vsUDP特點(diǎn):
UDP特點(diǎn) | TCP特點(diǎn) |
無(wú)連接:使用UDP通信雙方不需要刻意保存對(duì)方的相關(guān)信息 | 有連接:使用TCP通信雙方則需要刻意保存對(duì)方的相關(guān)信息 |
不可靠傳輸:消息發(fā)了就發(fā)了不關(guān)注結(jié)果 | 可靠傳輸:不是說(shuō)發(fā)送之后對(duì)方就可以100%能夠達(dá)到對(duì)方,這要求就太高了,只是說(shuō)盡可能的傳輸過(guò)去。 |
面向數(shù)據(jù)報(bào):以UDP數(shù)據(jù)報(bào)為基本單位。 | 面向字節(jié)流:以字節(jié)為傳輸?shù)幕締挝?,讀寫(xiě)方式非常靈活 |
全雙工:一條路徑,雙向通信 | 全雙工:一條路徑,全向通信。 |
解釋?zhuān)喝p工vs半雙工。
- 全雙工:是一條路徑,全向通信,你可以理解為,一個(gè)雙向通道的馬路。
- 半雙工:是一條路徑,只能由一側(cè)向另一側(cè)通信,你可理解為單向通道的馬路。
針對(duì)上述的TCP協(xié)議和UDP協(xié)議也給我們提供了兩組不同的API。下面我們來(lái)一步一步的了解一下。
3.UDP數(shù)據(jù)報(bào)套接字編程
3.1DataGramSocket?API
DataGramSocket?是UDP?Socket,用于發(fā)送和接收UDP數(shù)據(jù)報(bào),所謂Socket,是一個(gè)特殊的文件,是網(wǎng)卡這個(gè)硬件設(shè)備的抽象表示,你也可以理解為是一個(gè)遙控器,想要進(jìn)行網(wǎng)絡(luò)通信就需要有socket文件這樣的對(duì)象,借助這個(gè)socket文件對(duì)象,才能夠間接的操作網(wǎng)卡。
- 往這個(gè)socket對(duì)象里寫(xiě)數(shù)據(jù),相當(dāng)于通過(guò)網(wǎng)卡發(fā)送消息。
- 從這個(gè)socket對(duì)象中讀數(shù)據(jù),相當(dāng)于通過(guò)網(wǎng)卡接收消息。
DatagramSocket的構(gòu)造方法,可以綁定一個(gè)端口號(hào)(服務(wù)器),也可以不顯示指定客戶端。
方法簽名 | 方法說(shuō)明 |
DatagramSocket() | 創(chuàng)建一個(gè)UDP數(shù)據(jù)報(bào)套接字的Socket,綁定到本機(jī)任意一個(gè)隨機(jī)端口(一般用于客戶端) |
DatagramSocket(int port) | 創(chuàng)建一個(gè)UDP數(shù)據(jù)報(bào)套接字的Socket,綁定到本機(jī)指定的端口(一般用于服務(wù)端) |
- 服務(wù)器這邊的socket往往要關(guān)聯(lián)一個(gè)具體的端口號(hào)。
- 客戶端這邊則不需要手動(dòng)指定,系統(tǒng)會(huì)自動(dòng)分配一個(gè)閑置的端口號(hào)。
舉個(gè)例子:
比如現(xiàn)在我開(kāi)了一家餐廳,要發(fā)傳單,那么在傳單上面我這邊可定是要標(biāo)清楚我的餐廳的具體位置在哪,窗口號(hào)是多少,都得事先分配好,此時(shí)我開(kāi)的這家餐館就相當(dāng)于是服務(wù)器,確定的地址和窗口號(hào)就是服務(wù)器事先分配好的端口號(hào)。那么如果此時(shí)客人看到我發(fā)的傳單就來(lái)到我的餐館吃飯了,那么它點(diǎn)完餐之后,就會(huì)隨便找一個(gè)空著的位置坐下,等飯。此時(shí)客人就相當(dāng)于是客戶端,隨便找的位置就是系統(tǒng)給隨機(jī)分配的一個(gè)空閑的端口號(hào)。
?DatagramSocket方法:
方法簽名 | 方法說(shuō)明 |
void?receive(DatagramPacket p) | 從此套接字接收數(shù)據(jù)報(bào)(如果沒(méi)有接收到數(shù)據(jù)報(bào),該方法會(huì)阻塞等待) |
void send(DatagramPacket p) | 從此套接字發(fā)送數(shù)據(jù)報(bào)(不會(huì)阻塞等待,直接發(fā)送) |
void close() | 關(guān)閉數(shù)據(jù)報(bào)套接字(釋放資源) |
注意:
- DatagramPacket表示一個(gè)UDP數(shù)據(jù)報(bào)。
- 在close的時(shí)候,到底啥時(shí)候調(diào)用close,一定是要socket/文件,確定一定以及肯定不再使用,此時(shí)才能調(diào)用close。
3.2DatagramPacket?API
DatagramPacket是UDPSocket發(fā)送和接收的數(shù)據(jù)報(bào)。
DatagramPacket構(gòu)造方法:
方法簽名 | 方法說(shuō)明 |
DatagramPacket(byte[] buf, int length) | 構(gòu)造一個(gè)DatagramPacket以用來(lái)接收數(shù)據(jù)報(bào),接收的數(shù)據(jù)保存在字節(jié)數(shù)組(第一個(gè)參數(shù)buf)中,接收指定長(zhǎng)度(第二個(gè)參數(shù)length) |
DatagramPacket(byte[] buf, int offset, int lenght, SocketAddress address) | 構(gòu)造一個(gè)DatagramPacket以用來(lái)發(fā)送數(shù)據(jù)報(bào),發(fā)送的數(shù)據(jù)為字節(jié)數(shù)組(第一個(gè)參數(shù)buf)中,從0到指定長(zhǎng)度(第二個(gè)參數(shù)length),address指定目的的主機(jī)的IP和端口號(hào)。 |
?DatagramPacket方法:
方法簽名 | 方法說(shuō)明 |
InetAddress?getAddress() | 從接收的數(shù)據(jù)報(bào)中,獲取發(fā)送端主機(jī)IP地址,或從發(fā)送的數(shù)據(jù)報(bào)中,獲取接收端主機(jī)IP地址。 |
int?getPort() | 從接收的數(shù)據(jù)報(bào)中,獲取發(fā)送端主機(jī)的端口號(hào),或從發(fā)送的數(shù)據(jù)報(bào)中,獲取接收端主機(jī)端口號(hào)。 |
byte[] getData() | 獲取數(shù)據(jù)報(bào)中的數(shù)據(jù)。 |
3.3基于UDP的回顯服務(wù)器(echo?server)
介紹了DatagramSocket?和?DatagramPacket?API之后,我們基于UDP?socket寫(xiě)一個(gè)簡(jiǎn)單的客戶端服務(wù)器程序。?也就是讓客戶端發(fā)一個(gè)請(qǐng)求,在服務(wù)器上返回一個(gè)一模一樣的響應(yīng)。
首先來(lái)明確一點(diǎn),一個(gè)服務(wù)器主要做的三個(gè)核心的工作:
- 讀取請(qǐng)求并解析。
- 根據(jù)請(qǐng)求并計(jì)算響應(yīng)。(代碼中省略了)
- 把響應(yīng)返回給客戶端。
服務(wù)端代碼:
package network;
//服務(wù)端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer{
//需要先定義一個(gè)Socket對(duì)象
//通過(guò)網(wǎng)絡(luò)通信,必須要使用socket對(duì)象
private DatagramSocket socket = null;
//綁定一個(gè)端口,不一定能成功
//如果某個(gè)端口已經(jīng)被別的進(jìn)程占用了,此時(shí)這里的綁定操作就會(huì)出錯(cuò)。
//同一個(gè)主機(jī)上,一個(gè)端口,同一個(gè)時(shí)刻,只能被一個(gè)進(jìn)程綁定
public UdpEchoServer(int port) throws SocketException {
//構(gòu)造socket的同時(shí),指定要關(guān)聯(lián)/綁定的端口。
socket = new DatagramSocket(port);
}
//啟動(dòng)服務(wù)器的主邏輯
public void start() throws IOException {
System.out.println("服務(wù)器啟動(dòng)成功!");
while (true) {
//每次循環(huán),要做三件事
//1.讀取請(qǐng)求并解析
// 構(gòu)造空飯盒
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
// 食堂大媽給飯盒里打菜(飯從網(wǎng)卡上來(lái))
//這里的receive會(huì)阻塞等待,等到客戶端那邊發(fā)送數(shù)據(jù)過(guò)來(lái)
socket.receive(requestPacket);
//為了方便處理這個(gè)請(qǐng)求,需要把數(shù)據(jù)報(bào)轉(zhuǎn)換成String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//2.根據(jù)請(qǐng)求計(jì)算響應(yīng)(此處省略這個(gè)步驟)
String response = process(request);
//3.把響應(yīng)結(jié)果寫(xiě)回到客戶端
// 根據(jù)response 字符串,構(gòu)造一個(gè)DatagramPacket
// 和請(qǐng)求packet 不同,此處構(gòu)造響應(yīng)的時(shí)候,需要指定這個(gè)包要發(fā)給誰(shuí),這里調(diào)用requestPacket.getSocketAddress()就可以得知了
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//打印一下請(qǐng)求的地址和請(qǐng)求的端口號(hào),以及請(qǐng)求的內(nèi)容和響應(yīng)的內(nèi)容
System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
//這個(gè)方法是希望我們根據(jù)請(qǐng)求計(jì)算響應(yīng)。
//由于咱們寫(xiě)的是個(gè)回顯程序,請(qǐng)求是啥,響應(yīng)就是啥
//如果后續(xù)寫(xiě)一個(gè)別的服務(wù)器,不再回顯了,而是具有具體的業(yè)務(wù)了,就可以修改process方法
//根據(jù)需求來(lái)重新構(gòu)造響應(yīng)
//之所以單獨(dú)列成一個(gè)方法,就是想讓大家知道這個(gè)是一個(gè)關(guān)鍵的環(huán)節(jié)。
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
客戶端代碼:
package network;
//客戶端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;//服務(wù)器的地址
private int serverPort;//服務(wù)器的端口
//客戶端啟動(dòng),需要知道服務(wù)器在哪里
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//對(duì)于客戶端來(lái)說(shuō),不需要顯示關(guān)聯(lián)空閑的端口
//不代表沒(méi)有端口,而是系統(tǒng)自動(dòng)分配了一個(gè)空閑的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
//通過(guò)這個(gè)客戶端可以多次和服務(wù)器進(jìn)行交互
Scanner scanner = new Scanner(System.in);
while (true) {
//1.先從控制臺(tái),讀取一個(gè)字符串過(guò)來(lái)
//先打印一個(gè)提示符,提示用戶要輸入內(nèi)容
System.out.println("->");
String request = scanner.next();
//2.把字符串構(gòu)造成UDP packet,并進(jìn)行發(fā)送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
//3.客戶端嘗試讀取服務(wù)器返回的響應(yīng)
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//4.把響應(yīng)數(shù)據(jù)轉(zhuǎn)換成String顯示出來(lái)
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
udpEchoClient.start();
}
}
啟動(dòng)服務(wù)器和客戶端結(jié)果展示:
?
注意:這里一定是先啟動(dòng)服務(wù)器,再啟動(dòng)客戶端?。?!
執(zhí)行流程如下所示:
?
注意:在上述過(guò)程中我們是客戶端和服務(wù)器在同一個(gè)主機(jī)上,使用的是127這個(gè)IP,不同主機(jī)則就寫(xiě)實(shí)際的IP即可。
在上述通信過(guò)程中,站在客戶端發(fā)送數(shù)據(jù)的角度:
- 源IP是:127.0.0.1
- 源端口是:64982,他是系統(tǒng)自動(dòng)分配的空閑端口。
- 目的IP是:127.0.0.1
- 目的端口是:9090
在上述過(guò)程中就有同學(xué)好奇了不是說(shuō)是要使用close來(lái)關(guān)閉資源的嗎?為什么在代碼中好像沒(méi)有看到釋放資源這一步,其實(shí)對(duì)于UdpEchoServer來(lái)說(shuō),這個(gè)socket對(duì)象是出了循環(huán)就不用了,但是循環(huán)結(jié)束,意味著start結(jié)束,意味著main方法結(jié)束,同時(shí)意味著進(jìn)程結(jié)束,那么此時(shí)進(jìn)程都結(jié)束了所以的資源也就自然釋放了,所以就不必顯示釋放資源了。
3.4簡(jiǎn)單的翻譯服務(wù)器
在上述中我們編寫(xiě)的是一個(gè)回顯服務(wù)器,它是沒(méi)有實(shí)際意義的。那么如何寫(xiě)一個(gè)提供實(shí)在價(jià)值的服務(wù)器呢?當(dāng)響應(yīng)和請(qǐng)求不一樣了,響應(yīng)是根據(jù)不同的請(qǐng)求計(jì)算得到的,這里就需要我們對(duì)上述過(guò)程沒(méi)有寫(xiě)的process方法來(lái)進(jìn)行編寫(xiě),那么下來(lái)我們就具體來(lái)實(shí)現(xiàn)一下。我們就來(lái)編寫(xiě)一個(gè)簡(jiǎn)單的英文單詞翻譯服務(wù)器,請(qǐng)求是一個(gè)英文單詞,響應(yīng)是這個(gè)單詞的中文翻譯。
服務(wù)端代碼展示:
package network;
//詞典查詢服務(wù)端
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//使用繼承,是為了復(fù)用之前的代碼
public class UdpDicServer extends UdpEchoServer{
private Map<String, String> dict = new HashMap<>();
public UdpDicServer(int port) throws SocketException {
super(port);
dict.put("dog", "小狗");
dict.put("cat", "小貓");
dict.put("tiger", "老虎");
//注意:這里可以無(wú)限添加很多個(gè)數(shù)據(jù)
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"該單詞沒(méi)有查到!");
}
public static void main(String[] args) throws IOException {
UdpDicServer udpDicServer = new UdpDicServer(9090);
udpDicServer.start();
}
}
客戶端代碼展示:
package network;
//客戶端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;//服務(wù)器的地址
private int serverPort;//服務(wù)器的端口
//客戶端啟動(dòng),需要知道服務(wù)器在哪里
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//對(duì)于客戶端來(lái)說(shuō),不需要顯示關(guān)聯(lián)空閑的端口
//不代表沒(méi)有端口,而是系統(tǒng)自動(dòng)分配了一個(gè)空閑的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
//通過(guò)這個(gè)客戶端可以多次和服務(wù)器進(jìn)行交互
Scanner scanner = new Scanner(System.in);
while (true) {
//1.先從控制臺(tái),讀取一個(gè)字符串過(guò)來(lái)
//先打印一個(gè)提示符,提示用戶要輸入內(nèi)容
System.out.println("->");
String request = scanner.next();
//2.把字符串構(gòu)造成UDP packet,并進(jìn)行發(fā)送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
//3.客戶端嘗試讀取服務(wù)器返回的響應(yīng)
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//4.把響應(yīng)數(shù)據(jù)轉(zhuǎn)換成String顯示出來(lái)
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
udpEchoClient.start();
}
}
運(yùn)行結(jié)果展示:
?
注意:在上述編寫(xiě)服務(wù)端代碼時(shí)我們是直接使用了繼承,重寫(xiě)了父類(lèi)的process方法。這樣就減少了我們的工作。
4.TCP流套接字
在TCP中有兩個(gè)核心的類(lèi):
- ServerSocket:是給服務(wù)器使用的。
- Socket:即會(huì)給客戶端使用,又會(huì)給服務(wù)器端使用。
下面我們就來(lái)分別看看ServerSocket和Socket的具體使用方法。
4.1ServerSocket?API
他是創(chuàng)建服務(wù)端使用的API。
SocketSocket構(gòu)造方法:
方法簽名 | 方法說(shuō)明 |
ServerSocket(int port) | 創(chuàng)建一個(gè)服務(wù)流套接字Socket,并綁定到指定端口 |
SocketSocket方法:?
方法簽名 | 方法說(shuō)明 |
Socket?accept() | 開(kāi)始監(jiān)聽(tīng)指定端口(創(chuàng)建時(shí)綁定的端口),有客戶端連接后,返回一個(gè)服務(wù)端Socket對(duì)象,并基于該Socket建立與客戶端的連接,否則阻塞等待。 |
void?close() | 關(guān)閉此套接字 |
這里的accept意思就是接收,在客戶端主動(dòng)向服務(wù)器發(fā)起連接請(qǐng)求,服務(wù)器就得同意一下,但是實(shí)際上的這個(gè)accept又和我們上述給大家解釋的意思不太一樣,這里的accept只是在應(yīng)用層面的接收,實(shí)際的TCP連接的接受是在該內(nèi)核里已經(jīng)完成了。這個(gè)后面在將TCP的時(shí)候會(huì)給大家交代的。?
4.2Socket?API
Socket是客戶端Socket,或服務(wù)端中接收到客戶端建立連接(accept方法)的請(qǐng)求后,返回的服務(wù)端Socket。
不管是客戶端還是服務(wù)端Socket,都是雙方建立連接以后,保存的對(duì)端的信息,及用來(lái)與對(duì)方收發(fā)數(shù)據(jù)的。
Socket的構(gòu)造方法:
方法簽名 | 方法說(shuō)明 |
Socket(String?host,?int?port) | 創(chuàng)建一個(gè)客戶端流套接字Socket,并與對(duì)應(yīng)IP的主機(jī)上,對(duì)應(yīng)端口的進(jìn)程建立連接。 |
這里的host和port指的是服務(wù)器的IP和端口,TCP是有連接的,在客戶端new?Socket對(duì)象的時(shí)候就會(huì)嘗試和指定IP端口的目標(biāo)建立連接了。?
Socket的方法:
方法簽名 | 方法說(shuō)明 |
InetAddress?getInetAddress() | 返回套接字所連接的地址 |
InputStream?getInputStream() | 返回此套接字的輸入流 |
OutPutStream?getOutStream() | 返回此套接字的輸出流 |
?InputStream?getInputStream()和OutPutStream?getOutStream()是字節(jié)流,就可以通過(guò)上述字節(jié)流對(duì)象,進(jìn)行數(shù)據(jù)傳輸了。
- 從?InputStream?這里讀數(shù)據(jù),就相當(dāng)于是從網(wǎng)卡接收。
- 往?OutPutStream?這里寫(xiě)數(shù)據(jù),就相當(dāng)于從網(wǎng)卡發(fā)送。
注意:
這個(gè)Socket和DatagramSocket定位類(lèi)似,都是構(gòu)造的時(shí)候指定一個(gè)具體的端口,讓服務(wù)器綁定該端口,但是ServerSocket一定要綁定具體的端口。
4.3基于TCP的回顯程序
服務(wù)端代碼展示?:
package network;
//服務(wù)端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
//serverSocket只有一個(gè),clientSocket會(huì)給每一個(gè)客戶都分配一個(gè)
private ServerSocket severSocket = null;
public TcpEchoServer(int port) throws IOException {
severSocket = new ServerSocket(port);
}
public void start() throws IOException {
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("服務(wù)器啟動(dòng)成功!");
while (true) {
Socket clientSocket = severSocket.accept();
//如果直接調(diào)用,該方法會(huì)影響這個(gè)循環(huán)的二次執(zhí)行,導(dǎo)致accept不及時(shí)了。
//創(chuàng)建新的線程,用新線程來(lái)調(diào)用processConnection
//每次來(lái)一個(gè)新的客戶端都搞一個(gè)新的線程即可!
// Thread t = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
//讀取這個(gè)方法來(lái)處理一個(gè)連接
//讀取請(qǐng)求
//根據(jù)請(qǐng)求計(jì)算響應(yīng)
//把響應(yīng)返回給客戶端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客戶端上線!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
//使用try()這種寫(xiě)法,()中允許寫(xiě)多個(gè)流對(duì)象,使用;來(lái)分隔
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//沒(méi)有這個(gè)scanner和printWriter,完全可以,但是代價(jià)就是得一個(gè)字節(jié)一個(gè)字節(jié)扣,找到哪個(gè)是請(qǐng)求結(jié)束的標(biāo)記\n
//不是不能做,而是代替代碼比較麻煩
//為了簡(jiǎn)單,把字節(jié)流包裝成了更方便的字符流
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
//1.讀取請(qǐng)求
//采用hasNext判定接下來(lái)還有沒(méi)有數(shù)據(jù)了,如果對(duì)端關(guān)閉了連接(客戶端關(guān)閉連接),此時(shí)hasNext就會(huì)返回false,循環(huán)就結(jié)束
if (!scanner.hasNext()) {
//讀取的流到了結(jié)尾了(對(duì)端關(guān)閉了)
System.out.printf("[%s:%d] 客戶端下線了!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//直接使用scanner讀取一段字符串
//next會(huì)一直往后讀,讀到空白符結(jié)束(空格、換行、制表符、翻頁(yè)符...都算空白符)
//nextLine只是讀到換行符結(jié)束,所以這里沒(méi)有使用它
String request = scanner.next();
//2.根據(jù)請(qǐng)求計(jì)算響應(yīng)
String response = process(request);
//3.把響應(yīng)寫(xiě)回給客戶端,不要忘記了,響應(yīng)也是要帶上換行的
//返回響應(yīng)的時(shí)候要把換行符加回來(lái),方便客戶端那邊來(lái)區(qū)分從哪里到哪里是一個(gè)完整的響應(yīng)。
printWriter.println(response);
//flush當(dāng)數(shù)據(jù)不夠大的時(shí)候直接進(jìn)行強(qiáng)制刷新,將緩沖區(qū)中的數(shù)據(jù)發(fā)給客戶端
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只是一個(gè)連接提供服務(wù)的,這個(gè)還是要進(jìn)行關(guān)閉的
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
客戶端代碼展示:
package network;
//客戶端
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 port) throws IOException {
//這個(gè)操作相當(dāng)于讓客戶端和服務(wù)器建立TCP連接
//這里的連接連上了,服務(wù)器的accept就會(huì)返回
socket = new Socket(serverIP, port);
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while (true) {
//1.從鍵盤(pán)上讀取用戶輸入的內(nèi)容
System.out.println("->");
String request = scanner.next();
//2.把讀取到的內(nèi)容構(gòu)造成請(qǐng)求,發(fā)送給服務(wù)器
//注意,這里的發(fā)送,是帶有換行的。
printWriter.println(request);
printWriter.flush();
//3.從服務(wù)器讀取響應(yīng)的內(nèi)容
String response = scannerFromSocket.next();
//4.把響應(yīng)結(jié)果顯示到控制臺(tái)上
System.out.printf("req: %s; resp: %s\n", request, response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
結(jié)果展示:
執(zhí)行流程如下所示:
?
那么這里我們只是啟動(dòng)了一個(gè)客戶端,在實(shí)際中不可能是一個(gè)服務(wù)器只給一個(gè)客戶端進(jìn)行服務(wù),那么如何啟動(dòng)多個(gè)客戶端呢?這里在idea中是默認(rèn)下只能啟動(dòng)一個(gè)的,那么這里我們需要打開(kāi)idea配置一下。配置過(guò)程如下所示:
?
?
此時(shí)當(dāng)我們?cè)俅吸c(diǎn)擊上述的三角形就可以再次啟動(dòng)另一個(gè)客戶端了。
?
?
5.再談協(xié)議
回顧并理解我們?yōu)槭残枰獏f(xié)議
以上我們實(shí)現(xiàn)的UDP和TCP數(shù)據(jù)傳輸,除了UDP和TCP之外,程序還存在應(yīng)用層定義協(xié)議,可以想想分別都是什么樣的協(xié)議格式。
對(duì)于客戶端及服務(wù)端應(yīng)用程序來(lái)說(shuō),請(qǐng)求和響應(yīng),需要約定一致的數(shù)據(jù)格式:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-646514.html
- 客戶端發(fā)送請(qǐng)求和服務(wù)端解析請(qǐng)求和要使用相同的數(shù)據(jù)格式。
- 服務(wù)端返回響應(yīng)和客戶端解析響應(yīng)也要使用相同的數(shù)據(jù)格式。
- 請(qǐng)求格式和響應(yīng)格式可以相同,也可以不同。
- 約定相同的數(shù)據(jù)格式,主要目的是為了讓接收端在解析的時(shí)候明確如何解析數(shù)據(jù)中的各個(gè)字段。
- 可以使用知名協(xié)議(廣泛使用的協(xié)議格式),如果想自己約定數(shù)據(jù)格式,就屬于自定義協(xié)議。
結(jié)束語(yǔ):
這節(jié)中小編主要是和大家分享了網(wǎng)絡(luò)編程中的兩個(gè)重要的編程UDP和TCP,后期小編還會(huì)繼續(xù)出有關(guān)于網(wǎng)絡(luò)方面的知識(shí)的,希望這節(jié)對(duì)大家了解網(wǎng)絡(luò)有一定幫助,想要學(xué)習(xí)的同學(xué)記得關(guān)注小編和小編一起學(xué)習(xí)吧!如果文章中有任何錯(cuò)誤也歡迎各位大佬及時(shí)為小編指點(diǎn)迷津(在此小編先謝過(guò)各位大佬啦?。?span toymoban-style="hidden">文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-646514.html
到了這里,關(guān)于網(wǎng)絡(luò)編程(JavaEE初階系列10)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!