目錄
一、網絡初識
1.0 網絡初識
(1) 局域網廣域網
(2) 路由器和交換機
(3) IP地址和端口號
(4)協議
(5)五元組
(6) 協議分層
(6)OSI七層網絡模型
(7) 網絡設備所對應的分層
(8)傳輸層的兩個核心協議
2.0 網絡數據通信的基本流程
(1)理解網絡基本原理
(2)逆向
二、網絡編程
1.0 DatagramSocket
(1)概述
(2)方法
2.0 Udp服務器代碼示例
(1) 代碼示例
(2)代碼解析
(3)一些細節問題
3.0 Tcp服務器代碼示例
(1)代碼示例
(2)代碼解析
(3)一些細節問題
一、網絡初識
1.0 網絡初識
(1) 局域網廣域網
穿插網絡發展歷程介紹局域網和廣域網
國內 2000年前後 才真正開始進入到 網絡時代~
2000年前 網絡稀罕物 有的家庭已經有電腦了 但是沒有網~
2000年之後 網絡就開始逐漸多了 網吧~
當時有同學就把家裏的一些遊戲拷貝到學校機房 就可以聯機對戰~
局域網(LAN Local Area NetWork )
是指在有效的地理範圍內。由計算機、服務器、交換機、路由器等設備組成的私有高速通信網路
(只能在一個機房裏聯機 兩個設備連接到同一個路由器)
廣域網(WAN Wide Area NetWork)
是指跨越較大的地理範圍 通過公共或專用通信鏈路連接多個局域網、城域網或其他網路的大規模通信網絡。
(把很多局域網連到一起就是 全世界最大的廣域網 因特網 The Internet)
2007年 喬布斯發佈蘋果手機 標誌着 移動互聯網的時代 正式拉開序幕~~
差不多2011 2012年左右 真神Iphone4出現 使得諾基亞等傳統手機廠商一夜之間就G了
2017年左右 有一波風口 VR/AR 但是當時的條件不足以把這個事情做好 客觀的硬件設備
總結:單機時代 局域網時代 廣域網時代 移動互聯網時代
(2) 路由器和交換機
路由器 交換機(組建網絡重要的核心設備)
路由器:用於連接不同網絡(如LAN或WAN)並根據IP地址轉發數據包
路由器有五個口 能接入的設備有限 進行組建局域網
交換機:用於同一網絡內的設備互聯 通過MAC地址轉發數據幀
交換機上面有很多接口 可以擴展路由器
(3) IP地址和端口號
ip地址:標識網路上一台設備所在的地址 標識主機的地址
端口號:用來區分一台主機上多個應用程序的
cmd裏面輸入ipconfig也能看到看到ip地址
一台主機上可能有多個程序同時使用網絡
(4)協議
網路通信中約定的規則
就是約定 雙方都按照協議運行
主機設備 多個主機都能認同並遵守同一套協議 此時的通信才有意義的~
例如我們都是中國人 用的普通話協議 所以都聽懂對方説話哈哈
(5)五元組
進行一次網絡通信 涉及到的5個非常關鍵的信息
源IP 源端口 目的IP 目的端口 協議類型
理解: 貧僧東土大唐而來,到西方拜佛求經而去
貧僧 源端口 拜佛 目的端口 東土大唐 源IP 西方 目的IP
(6) 協議分層
網絡通信 非常複雜
如果我們設計一個協議 完成網絡通信中方方面面的問題
勢必會使這個協議非常複雜 非常龐大 所以需要拆分
把一個大的協議 拆分成若干個小的 功能單一的協議
只有相鄰的兩層協議之間可以進行交互 不能跳級交互
淘寶買東西到你手上
用户關心的是牀刷子買到之後如何使用-->應用層 賣家只要關心,收件人信息-->傳輸層
物流公司則關心,包裹怎樣路徑傳輸的-->網絡層 快遞小哥/貨車司機,考慮的是相鄰節點-->數據鏈路層
真實的互聯網世界 具體是怎麼分層的呢?
(6)OSI七層網絡模型
這種是教科書上的分層 實際上真實的網絡分層方式更簡化
TCP/IP 五層(四層) 協議模型
隨着學習過程的展開 會慢慢細化學習這部分內容
這部分內容是需要大家背下來的
七層模型和五層模型的對應:
(7) 網絡設備所對應的分層
但是真實的路由器交換機等 功能更豐富 更強大
又的路由器甚至開了什麼模式 能達到交換機的效果
你通過wx發一個數據 (數據屬於 應用層) 數據經過某個運營商的路由器
有的能給你解析出來你發的內容 (此時 路由器相當於工作在應用層)
(8)傳輸層的兩個核心協議
TCP協議和UDP協議
差別非常大,編寫代碼的時候,也是不同的風格~(因此socket api提供了兩套)
TCP 有連接 可靠傳輸 面向字節流 全雙工
UDP 無連接 不可靠傳輸 面向數據報 全雙工
連接:抽象的連接 對於TCP協議來説,就保存了對端的信息
傳輸:網絡上的數據是非常容易出現丟失的情況(丟包) (光電信號可能受到外界干擾)
可靠傳輸的意思是 不是保證數包100%到達,而是儘可能的提高傳輸成功的概率
如果出現了丟包,能感知到
不可靠傳輸 只是把數據發了,就不管了~
面向字節流:
讀寫數據時候,是以字節為單位 支持任意長度 容易出現粘包問題
面向數據報:
讀寫數據的時候,以一個數據報為單位(不是字符) 一次必須讀寫一個UDP數據報 不能是 半個~~ 不存在粘包問題
全雙工:一個通信鏈路 支持 雙向通信 (能讀也能寫)
半雙工:只支持單向通信(要麼讀 要麼寫)
2.0 網絡數據通信的基本流程
(1)理解網絡基本原理
例子:用協議的角度看 我通過qq 發送hello給對方
1應用程序 獲取用户輸入 構造一個應用層的數據包
(網絡傳輸的數據 本質上都是“字符串” 或者 “二進制的bit流”)
2應用程序調用傳輸層 提供的接口(API) 把數據交給傳輸層,傳輸層拿到數據構造傳輸層數據包
3傳輸層調用網絡層API 把傳輸層的數據包交給網絡層 網絡層繼續進行處理
網絡層最主要的協議是IP協議 IP協議繼續對上述的數據進行加工(也就是拼接上IP報頭)
4 IP協議繼續調用 數據鏈路層, 把IP數據包交給數據鏈路包
數據鏈路層,核心協議“以太網” 以太網這個協議,也會在網絡層數據包的基礎上進一步加工
5以太網繼續將這樣的數據交給硬件設備(網卡)
網卡會把上述二進制數據,最終以光信號/電信號/電磁波信號 傳播出去了
(數據終於出門了嗚嗚嗚)
整個過程五元組的信息都會出現 從上層到下層 數據都要進一步加工
(添加報頭)和 封裝(和麪向對象的封裝不是一個封裝)
接收方是從下到上依次解析,分用~ (封裝的逆向過程)
(2)逆向
為了更好的理解 簡單描述一下哈
1.數據到達接收方的網卡 光電信號 網卡把光電信號還原成二進制0101
把二進制數據交給上層數據鏈路層
2.數據鏈路層按照以太網協議進行解析
把報頭和報尾取出來,剩下的載荷,往上傳遞給網絡層
3.網絡層拿到這個數據之後,按照IP協議的格式解析,再把載荷數據交給傳輸層
4.傳輸層拿到數據之後,也是類似,按照TCP協議來解析,取出載荷,交給應用層
5.QQ應用程序,解析應用層數據,拿到關鍵信息,展示到界面上,給出提示
(雖然上述這些過程 聽起來是複雜的 但是對於計算機來説 是極快的過程)
二、網絡編程
1.0 DatagramSocket
(1)概述
操作系統提供的一組api--->socket api(傳輸層給應用層提供)
api:理解為軟件和硬件間的一座橋樑就行了
socket api進行網絡編程
前面我們講到 計算機的文件通常是一個廣義的概念 文件也可以代指一些硬件設備
例如 我們可以把網卡抽象成為socket文件 操作網卡的時候 流程和操作普通文件差不多
操作網卡轉換成操作socket文件 socket文件就相當於"網卡的遙控器"
(2)方法
DatagramSocket:UDP通信的核心類
構造方法:打開文件
//套接字 是網絡通信的基石 可以理解為網絡數據傳輸的"端點" 它應用程序通過網路協議於其他程序交換數據的 編程接口
DatagramSocket():創建一個UDP數據報套接字的Socket 綁定到本機任意一個隨機端口
DatagramSocket(int port) :創建一個UDP數據報套接字的Socket 綁定到本機指定的端口
receive (DatagramPacket p)從此套接字接收數據報
(如果沒有接收數據報 該方法會阻塞等待)
這裏也是把參數作為輸出的結果 是輸出型參數
(調用之前 先構造空的(不是null) 把對象傳遞到receive裏面 receiver就會把數據從網卡讀出來
send(DatagramPacket p)從此套接字發送數據報(不會阻塞等待 直接發送)
close() 關閉此數據報套接字
DatagramPacket() 數據報:
2.0 Udp服務器代碼示例
UDP服務器 UdpEchoClinet回顯服務器
//回顯服務器 客户端給服務器發一個數據(請求) 服務器給客户端返回一個數據(響應)
真實的服務器,請求和響應式不一樣的~~
(1) 代碼示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//這個代碼是自己搭建一個回顯服務器
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//指定了一個端口號 讓服務器來使用
socket = new DatagramSocket(port);
}
public void start() throws IOException {
//啓動服務器
System.out.println("服務器啓動");
while(true){
//循環一次 就相當於處理一次請求
//處理請求的過程 典型的服務器都是分成三個步驟
//1.讀取請求並解析
//DatagramPacket表示一個UDP數據報 此處傳入的字節數組,就保存UDP載荷部分
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//把讀取到的二進制數據,轉換成字符串 只是構造有效的部分
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根據請求 計算響應(回顯服務器最關鍵的邏輯)
//但是此處寫的是回顯服務器,這個環節相當於省略了
String response = process(request); //請求等於迴應
//3.把響應返回給客户端
//根據response構造DatagramPacket,發送客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//此處還不能直接發送 UDP協議自身沒有保存對方信息(不知道發給誰 需要指定目的ip和目的端口)
socket.send(responsePacket);
//4.打印一個日誌
System.out.printf("[%s:%d] req: %s,rep: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//後續要寫別的服務器 只修改這個地方就好了
private String process(String request){
return request;
}
}
下面是客户端代碼
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
//Udp本身不保存對端信息,就自己的代碼中保存一下
private String serverIP;
private int serverPort;
//和服務器不同 此處的構造方法是要指定訪問的服務器的地址
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
this.serverIP=serverIP;
this.serverPort=serverPort;
socket = new DatagramSocket();//為什麼這裏不能指定端口號呢 那個肉湯的例子
}
public void start() throws IOException{
Scanner scanner = new Scanner(System.in);
while(true){
//1.從控制枱讀取用户輸入的內容
System.out.println("請輸入要發送的內容:");
if(!scanner.hasNext()){
break; //這個if用來終止客户端
}
String request = scanner.next();
//2.把請求發送給服務器 需要構造DatagramPacket對象
//構造過程中 不光要構造載荷 還要設置服務器的IP和端口號
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);//這裏的服務器的IP和端口號要包裝一下 要不datagram方法裏面沒有解析這個的
//3.發送數據報
socket.send(requestPacket);
//4.接收服務器的響應
DatagramPacket responsePacket = new DatagramPacket(new byte[4096] , 4096);
socket.receive(responsePacket);
//5.從服務器讀取的數據進行解析 打印出來
String response = new String (responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("47.108.28.88",9090);
client.start();
}
}
效果圖片:
(2)代碼解析
1.socket對象代表網卡文件 讀這個文件相當於從網卡收集數據,寫這個文件相等於讓網卡發數據
socket對象創建的時候 為什麼要指定一個端口號?
指定端口號的核心原因是 標識應用程序的通信端點 確保數據能準確送達目標程序
2.while循環中做三件事(註釋裏面講解) 這三件事是一個服務器通常的流程
3.讀取請求並解析
*構造DatagramPacket對象 DatagramPacket就代表UDP數據包 報頭+載荷new字節數組保存
*調用receive 需要理解輸出型參數:調用時傳入一個空的(非null)DatagramPacket對象,方法 內部會將接收到的數據填充到對象中)
*把udp數據包載荷取出來 構造一個String
通過requestPacke.getData() 拿到DatagramPacket中的字節數組~
requestPacket.getLength() 拿到有效數據的長度
根據字節數組 構造出一個string
4.根據請求計算響應 把請求扔給迴應(回顯服務器嘛)
5.把響應返回到客户端
response.getByte() 拿到字符串中的字節數組~
resopnse.getByte().length 拿到字節數組的長度,而不是使用字符串長度(單位 字符)使用自己 的個數作為參數
requestPacket.getSocketAddress() 拿到客户端的IP和端口號~
new DatagramPacket是要幹啥?
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
構造響應數據報 上面的是請求數據報 這裏是響應數據報
6.socket.send(responsePacket); 把構造好的數據包發送出去 前提是數據包中包含了目的IP和目 的IP和目的端口(前面responsePacket的時候 就刻意指定了IP和端口)
(3)一些細節問題
輸出型參數的理解(又一個文件IO的例子)
socket不用colose的嘛?
文件要關閉 考慮清楚這個文件對象的生命週期是怎樣的~
此處的socket對象 伴隨着整個udp服務器 自始至終 提前關閉服務器運行的也沒有意義
服務器關閉(進程結束) 進程結束時就會自動釋放PCB的文件描述附表的所有資源
當前服務器啓動了,啓動了之後,客户端還沒有呢,當然也沒有請求發來啦
在客户端請求發過來之前 服務器裏面的邏輯都在幹啥呢?
receive會觸發阻塞行為 客户端請求發過拉il receive才會返回 客户端的請求沒來,receive就一直阻塞了
3.0 Tcp服務器代碼示例
(1)代碼示例
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;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
//這裏和UDP服務器類似 也是在構造對象的時候 綁定端口號
public TcpEchoServer(int port) throws IOException{
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("啓動服務器");
while(true) {//服務器7*24運行
//tcp來説 需要先處理客户端發來的連接
// 通過讀寫clientSocket 和客户端進行通信
//如果沒有客户端發起連接 此時accpet就會阻塞
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);//因為代碼邏輯比較複雜 單獨包裝成一個方法
}
}
//處理一個客户端的連接
//真實情況是可能要涉及到多個客户端的請求和響應
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上線!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//獲取客户端的輸入流和輸出流
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//為什麼套層? 在Java網絡編程中,i和o是字節流,它們直接操作原始字節(byte數組)
//而Scanner和PrintWriter是字符流(Character Stream) 可以更加方便的處理文本數據(String)
//如果是二進制傳輸的話(如文件 圖片) 直接使用i和o 如果是文本數據傳輸(Http 聊天消息)推薦使用Scanner PrintWriter
//針對InputStream套了一層
Scanner scanner = new Scanner(inputStream);//直接用scanner讀取請求裏面的內容
//針對OutputStream套了一層
PrintWriter writer = new PrintWriter(outputStream);
//分成三個步驟
while(true){
//1.讀取請求並解析 可以直接read 也可以藉助
if(!scanner.hasNext()){//這個表明條件如果是沒有下個數據可以了
//連接斷開了
//也打印一個日誌吧
System.out.printf("[%s:%d] 客户端上線!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
String request = scanner.next();
//2.根據請求計算響應
String response = process(request);
//3.返回響應到客户端
//outputStream.write(response.getBytes());
writer.println(response);
//這裏省略了具體實現 直接讀取請求 然後返回原樣
//打印日誌
System.out.printf("[%s:%d] req: %s,rep: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
}catch(IOException e){
throw new RuntimeException(e)
}
}
private String process(String request) {
return request;//這裏還是講解的回顯服務器 所以requst和response相同
}
}
客户端代碼:
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 {
//這裏直接就可以把字符串的IP地址和端口設置進來
//127.0.0.1 這種字符串
socket = new Socket(serverIp,serverPort);
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream= socket.getOutputStream()){
//為了使用方便 套殼操作
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
//從控制枱讀取請求,發送給服務器
while(true){
//1.從控制枱讀取用户輸入
String request = scanner.next();
//2.發送請求給服務器
writer.println(request);
//3.讀取服務器返回的響應
String response = scannerNet.next();
//4.打印到控制枱
System.out.println(response);
}
}catch (IOException e){
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
效果圖:
Socket clientSocket = serverSocket.accept();
private Socket socket = null;
這兩socket對象 絕對不是同一個對象(分別在不同進程中 甚至在不同主機上)
可以理解為兩部電話 A和B能打電話 但是A和B不是同一個電話
(2)代碼解析
連接
連接相當於打電話 建立對端信息(例如ip 端口) Tcp是面向連接的協議,客户端和服務器端建立一次連接後,可以在該連接上多次發送和接收數據(即多次請求-響應交互) ,而不需要每次都重新建立連接
accpet方法
阻塞等待客户端的連接請求 ,當有客户端連接時,返回一個代表連接的socket對象
如何理解ServerSocket和socket的關係?
銷售並不是一個人完成的
serverSocket相當於售樓處 外部的連接 accpet方法是接待客户
socket相當於客户 比較具體 具體談業務還是要在socket裏面
Scanner
scanner 構造方法 填入的其實是一個InputStream對象 也就是説它需要從某個輸入流讀取數據,而InputStream 是Java中所有字節輸入流的基類
outputStream.write(response.getBytes())這句代碼的作用是:將一個字符串(response)轉換為字節數組(byte)並通過outputStream寫入到某個輸出目標
為什麼套層? 在Java網絡編程中,i和o是字節流,它們直接操作原始字節(byte數組)
而Scanner和PrintWriter是字符流(Character Stream) 可以更加方便的處理文本數據(String)
如果是二進制傳輸的話(如文件 圖片) 直接使用i和o 如果是文本數據傳輸(Http 聊天消息)推薦使用Scanner PrintWriter
PrintWriter writer = new PrintWriter(outputStream);
這行代碼的作用是:創建一個PrinterWriter對象 並將其綁定到一個outputStream(字節輸出流) 以便以字符形式(文本) 更方便的寫入數據 PrintWriter是Java提供的字符流 用於格式化輸出文本
(3)一些細節問題
緩衝區:
wirte.println(request) 這個操作只是把數據放到”發送緩存區“中 還沒有真正寫入到網卡里面
使用flush方法來"沖刷緩衝區” write.flush();
實際開發中,廣泛使用緩衝區這樣的概念 flush這個操作是很關鍵的~
println
write.println(request) 行為是自動加上一個\n 如果不加\n只是print呢 這樣是不行的
print數據是發過去了 服務器收到了 但是服務器沒有真正判斷 服務器裏面有個hashNext()
遇到空白符 才認為是一個“完整的 next” 在遇到之前,都會阻塞
使用println就是在暗暗約定 一個請求/響應 使用\n作為結束標記
對端讀的時候,也是讀到\n就結束(認為是讀到一個完整的請求了)
serverSocket clientSocket的生命週期
clientSocket 每個客户端連接 都會創建一個新的 每個客户端斷開連接 這個對象可以補藥了
一個服務器要能夠給多個客户端提供服務
代碼Tcp服務器也能夠做到處理多個嗎?
Idea默認只允許啓動一個進程 如果你啓動第二個進程 它會把第一個進程關閉 需要我們手動設置
問題是隻能同時啓動一個客户端
啓動二個客户端 第三個客户端沒有反應
為什麼第二個客户端不能運行 運行代碼發現關掉第一個客户端的時候 第二個客户端立馬行了
關鍵的邏輯在while循環裏面
處理客户端一的請求的時候 卡在了! scanner.hasNext()裏面
如果客户端沒發請求的時候 就會阻塞在了hasNext裏面 意味着accpet執行不到
循環退出(客户端退出)第二個客户端才能登場
總結:等待用户發送請求的時候,沒法等accpet 這個時候 有新的客户端連過來了 也無法接通電話
怎麼解決這個代碼不合理的問題 怎麼樣同時調用accept 同時處理hasNext?
兵分兩路 多線程
多線程的最初誕生就是為了這個場景 最初是多進程 本質上都是處理服務器開發的問題
主線程負責進行accpet 每次accpet到一個客户端 就創建一個線程 由新線程負責處理客户端的請求 (類似那個例子 外場攬客 後場銷售)
如果單個線程 如果訪問量激增 會出現第一種那樣的報錯嗎?
這個也是線程池的誕生場景
多線程 還是線程池 都意味着 一個客户端對應一個線程 一個主機上創建的線程數目是否有上限呢
有的 一個主機創建幾千個線程 就已經很不容易了