1. 網絡編程概述
1.1 C/S和B/S
C/S
客戶端 服務器軟件結構
服務提供商給予用戶服務需要準備的內容
1. 各大平臺的客戶端
Android iOS PC Windows Linux macOS
QQ 微信 淘寶 JD 劍與遠征
2. 服務器提供服務
軟件更新:
LOL服務器版本更新,同時本地軟件也要進行更新操作。這個操作非常耗時。
熱更新
B/S
瀏覽器 服務器軟件結構
服務提供商只要提供數據服務就OK,以及前端數據展示方式
1. 瀏覽器提供商非常非常多
谷歌,火狐,歐朋,Safari,Edge
2. 服務器提供服務
軟件更新:
服務器更新數據,瀏覽器刷新就ok了
1.2 網絡通信協議
協議:
protocol協議
網絡通信協議是要求雙方傳遞數據的計算機必須遵守的,按照對應的網絡傳輸協議,纔可以進入數據的交互和傳遞。
目前網絡段數據傳輸比較常見的協議:
UDP TCP/IP
1.3 UDP和TCP/IP區別
UDP
1. 面向無連接,數據傳遞不算特別安全
2. 因爲面向無連接,傳輸速度快
3. 因爲面向無連接,數據傳遞存在丟包問題
4. UDP沒有客戶端和服務器區別,都可以作爲發送端和接收端,相互的
UDP協議使用場景
直播,網絡遊戲
實時的大部分都是UDP
TCP/IP
1. 面向連接,數據傳遞較爲安全
2. 因爲面向連接,所有傳遞速度較慢
3. 面向連接,數據傳遞有保障
4. TCP/IP協議是有明確的服務器和客戶端概念
TCP/IP協議使用場景
客戶端登陸,數據下載,文件傳輸
一個軟件肯定是混合協議的,不是單獨的。
1.4 網絡編程的三要素
1. 協議
兩個在於網絡情況下的計算機數據傳遞,都需要對應的協議來完成。
2. IP地址
Internet Protocol Address
當前計算機在網絡中的一個地址編號,類似於手機號號碼
IP地址有IPv4協議和IPv6協議
IPv4是一個32位的二進制數,通常展示效果是a.b.c.d 例如 192.168.1.1
a.b.c.d 各代表0 ~ 255的數字,目前已經消耗殆盡 42億個
IPv6
IPv6是能夠保證地球上的每一粒沙子都有一個IP地址。
128位地址長度,16字節一組
8組 0x0 ~ 0xFFFF
3. 端口號
端口號是當前應用程序在計算機中的一個編號。可以讓計算機明確知道,當前的數據是給予哪一個程序使用,或者數據從哪一個程序出現的。
端口號是一個short類型 0 ~ 65535
0~1024不能用於自定義端口號使用,特定的系統端口號
2.IP類
SUN公司提供給開發使用的IP地址類
InetAddress
常用方法:
InetAddress getLocalhost();
獲取本機IP地址類對象
InetAddress getByName(String str);
根據指定的主機名獲取對應的IP地址對象
InetAddress[] getAllByName(String str);
獲取指定主機名,或者域名對應的所有IP地址類對象
代碼演示:
package com.qfedu.a_ip;
import java.net.InetAddress;
import java.net.UnknownHostException;
/*
* IP類演示
*/
public class Demo1 {
public static void main(String[] args) throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
InetAddress byName = InetAddress.getByName("DESKTOP-M89SDP7");
System.out.println(byName);
InetAddress byName2 = InetAddress.getByName("www.4399.com");
System.out.println(byName2);
System.out.println("----------------------------------");
InetAddress[] allByName = InetAddress.getAllByName("www.baidu.com");
for (InetAddress inetAddress : allByName) {
System.out.println(inetAddress);
}
System.out.println("----------------------------------");
InetAddress[] allByName1 = InetAddress.getAllByName("www.taobao.com");
for (InetAddress inetAddress : allByName1) {
System.out.println(inetAddress);
}
System.out.println("----------------------------------");
InetAddress[] allByName2 = InetAddress.getAllByName("www.jd.com");
for (InetAddress inetAddress : allByName2) {
System.out.println(inetAddress);
}
}
}
3.UDP協議數據傳輸
3.1 UDP數據傳輸方式
User Datagram Protocol
數據傳遞採用數據包方式傳遞,所有的數據要進行打包操作,並且沒有對應的客戶端服務器概念,有且只有發送端和接收端
Socket 套接字
數據需要進行傳遞操作,在數據傳遞的兩臺計算機當中必須有對應的Socket。這裏採用UDP協議,那麼必須有一個UDP協議的Socket
DatagramSocket();
創建一個發送端UDP協議Socket對象
DatagramSocket(int port);
創建一個接收端UDP協議的Socket對象,這裏需要【監聽】指定端口
發送端數據包的打包方法:
DatagramPacket DatagramPacket(byte[] buf, int length, InetAddress address, int port);
buf: 需要傳遞數據的字節數組
length:是當前字節數組中數據容量字節數
address:接收端IP地址對象
port: 接收端對應的端口號
接收端數據包接收方式
這裏需要準備一個空的數據包
DatagramPacket DatagramPacket(byte[] buf, int length);
buf: 字節緩衝數組,通常是1024整數倍
length: 當前字節緩衝數組的容量
3.2 發送端
流程:
1. 創建UDP服務器對應的發送端Socket
2. 準備對應數據包,需要帶有指定數據
3. 發送數據 send
4. 關閉UDP發送端
代碼如下:
package udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/*
流程:
1. 創建UDP服務器對應的發送端Socket
2. 準備對應數據包,需要帶有指定數據
DatagramPacket DatagramPacket(byte[] buf, int length, InetAddress address, int port);
buf: 需要傳遞數據的字節數組
length:是當前字節數組中數據容量字節數
address:接收端IP地址對象
port: 接收端對應的端口號
3. 發送數據 send
4. 關閉UDP發送端
*/
public class SenderDemo1 {
public static void main(String[] args) throws IOException {
//標記一下
System.out.println("發送端啓動");
//1. 創建UDP服務器對應的發送端Socket
DatagramSocket datagramSocket = new DatagramSocket();
//準備對應數據包,需要帶有指定數據
byte[] bytes= "發送端:我來朵蜜你了!!!".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8848);
datagramSocket.send(packet);
datagramSocket.close();
}
}
3.3 接收端
流程:
1. 打開UDP服務,並且監聽指定端口
2. 創建新的空數據包
3. 通過Socket接收數據
4. 關閉UDP服務接收端
package udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/*
流程:
1. 打開UDP服務,並且監聽指定端口
2. 創建新的空數據包
3. 通過Socket接收數據
4. 關閉UDP服務接收端
*/
public class ReciveDemo1 {
public static void main(String[] args) throws IOException {
//標記
System.out.println("接收端打開");
//1. 打開UDP服務,並且監聽指定端口
DatagramSocket datagramSocket = new DatagramSocket(8848);
//2. 創建新的空數據包
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
//3. 通過Socket接收數據
datagramSocket.receive(packet);
//4.確定接收到的字節長度
int length = packet.getLength();
System.out.println(new String(bytes, 0, length));
//5. 關閉UDP服務接收端
datagramSocket.close();
}
}
3.4 UDP數據傳遞丟失問題
這是udp的缺點之一,因爲面向無連接,所以可能會丟數據,類似於玩電腦遊戲丟包,瞬移,卡頓。
- 網絡不夠好,穩定性不行,帶寬不夠
- 電腦性能不好
3.5 FeiQ
網絡傳輸都有自己的傳輸規格,如果軟件接受到的數據是自己的規格,那麼可以讀取數據
如果不是,丟棄!!!
FeiQ
version:time:sender:ip:flag:content
版本:時間:發送者名字:發送人IP:標記:內容
數據是一個String類型
而且使用的協議是UDP協議
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class FeiQ {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket();
String data = getData("Welcome to Java World!!!");
DatagramPacket datagramPacket = new DatagramPacket(data.getBytes(), data.getBytes().length
, InetAddress.getByName("192.168.31.255"), 2425);
socket.send(datagramPacket);
socket.close();
}
/**
* 傳入數據,轉換成FeiQ可以識別的數據
* version:time:sender:ip:flag:content
*
* @param message 字符串類型內容
* @return 符合FeiQ格式要求的字符串
*/
public static String getData(String message) {
StringBuilder stb = new StringBuilder();
stb.append("1.0:");
stb.append(System.currentTimeMillis() + ":");
stb.append("Anonymous:");
stb.append("10.1.1.1:");
stb.append("32:");
stb.append(message);
return stb.toString();
}
}
4. TCP
4.1 TCP概述
TCP相對於UDP比較穩定的傳輸協議,這裏存在三次握手,保證連接狀態,同時有明確的客戶端和服務端之分
TCP服務中需要服務器端先啓動,需要監聽指定端口,等待客戶端連接。
客戶端主動連接服務器,和服務器連接之後,纔可以進行數據交互,服務器不能主動連接客戶端的。
TCP操作而言,Java中提供了兩個Socket
-
服務端Socket
java.net.ServerSocket;
創建對應的ServerScoket開啓服務器,等待客戶端連接 -
客戶端Socket(這個比較重要)
java.net.Socket
創建客戶端Scoket,並且連接服務器,同時將Socket發送給服務器綁定註冊。
4.2 Socket 客戶端Socket
給客戶端提供數據傳輸的符合TCP/IP要求的Socket對象
構造方法 Constructor
Socket(String host, int port);
host是服務器IP地址,port對應服務器程序的端口號
通過指定的服務器IP地址和端口號,獲取TCP連接對象
成員方法 Method
InputStream getInputStream();
獲取Socket對象輸入字節流,可以從服務器獲取對應的數據
InputStream是一個資源,需要在程序退出是關閉
Read
OutputStream getOutputStream();
獲取Sokcet對象輸出字節流,可以發送數據到服務器
OutputStream是一個資源,需要在程序退出是關閉
Write
void close();
關閉客戶端Socket
void shutdownOutput();
禁止當前Socket發送數據
TCP/IP協議對應的Socket是給予IO流實現的。
4.3 ServerSocket服務端Socket
在服務端開啓Socket服務器
構造方法 Constructor:
ServerSocket(int port);
開啓ServerSocket服務器,並且明確當前服務端口是誰
成員方法 Method:
Socket accept();
監聽並且連接,得到一個Socket對象,同時該方法是一個阻塞方法,會處於一個始終的監聽狀態
返回的是Socket,也就是客戶端Socket對象,獲取到當前Socket對象,相對於獲取到客戶端連接,同時使用的Socket和客戶端一致。
4.6 TCP協議代碼演示
4.6.1 服務器代碼
流程:
1. 創建ServerSocket服務器,同時監聽指定端口
2. 通過accept方法獲取Socket連接,得到客戶端Socket對象
3. 通過Socket對象,獲取InputStream,讀取客戶端發送數據
4. 通過Socket對象,獲取OutputStream,發送數據給客戶端
5. 關閉服務
代碼如下:
package com.qfedu.c_tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
流程:
1. 創建ServerSocket服務器,同時監聽指定端口
2. 通過accept方法獲取Socket連接,得到客戶端Socket對象
3. 通過Socket對象,獲取InputStream,讀取客戶端發送數據
4. 通過Socket對象,獲取OutputStream,發送數據給客戶端
5. 關閉服務
*/
public class TcpServer1 {
public static void main(String[] args) throws IOException {
System.out.println("服務器啓動");
System.out.println("-----------------------");
// 1. 創建ServerSocket服務器,同時監聽指定端口
ServerSocket serverSocket = new ServerSocket(8848);
// 2. 通過accept方法獲取Socket連接,得到客戶端Socket對象
Socket socket = serverSocket.accept();
// 3. 通過Socket對象,獲取InputStream,讀取客戶端發送數據
InputStream inputStream = socket.getInputStream();
// IO流操作
byte[] buf = new byte[1024];
int length = inputStream.read(buf);
System.out.println(new String(buf, 0, length));
// 4. 通過Socket對象,獲取OutputStream,發送數據給客戶端
OutputStream outputStream = socket.getOutputStream();
String str = "歡迎來到德萊聯盟";
outputStream.write(str.getBytes());
// 5. 關閉Socket服務 同時關閉當前Socket使用的輸入字節流和輸出字節流
// Closing this socket will also close the socket's InputStream and OutputStream.
socket.close();
}
}
4.6.2 客戶端代碼
流程:
1. 創建Socket服務,同時明確連接服務器的IP地址和對應端口號
2. 通過Socket對象,獲取對應的OutputStream對象,發送數據給服務器
3. 通過Socket對象,獲取對應的InputStream對象,接收服務器發送數據
4. 關閉服務
代碼如下:
package com.qfedu.c_tcp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
/*
流程:
1. 創建Socket服務,同時明確連接服務器的IP地址和對應端口號
2. 通過Socket對象,獲取對應的OutputStream對象,發送數據給服務器
3. 通過Socket對象,獲取對應的InputStream對象,接收服務器發送數據
4. 關閉服務
*/
public class TcpClient1 {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("客戶端啓動");
System.out.println("------------------------");
// 1. 創建Socket服務,同時明確連接服務器的IP地址和對應端口號
Socket socket = new Socket("192.168.31.154", 8848);
// 2. 通過Socket對象,獲取對應的OutputStream對象,發送數據給服務器
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好服務器!!!".getBytes());
// 3. 通過Socket對象,獲取對應的InputStream對象,接收服務器發送數據
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int length = inputStream.read(buf);
System.out.println(new String(buf, 0, length));
// 4. 關閉服務
socket.close();
}
}
4.6.3 代碼總結
在這裏只是一個小的演示,傳遞的只有一句話,比較小,所以用一個1024的字節數組就可以接收信息了。
下邊來傳輸比較大的文件,會用到之前的IO流操作。
4.7 文件上傳操作
4.7.1 分析過程
4.7.2 客戶端程序
流程:
1. 創建對應文件的輸入字節流操作,這裏可以使用緩衝
2. 啓動Socket
3. 獲取Socket輸出OutputStream對象,發送數據給服務器
4. 邊讀邊發
5. 當文件讀取結束,發送完畢,關閉客戶端
代碼如下:
package fileupload;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
public class tcpClient {
public static void main(String[] args) throws IOException {
// 1. 創建對應文件的輸入字節流操作,這裏可以使用緩衝
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("d:/aaa/1.mp4")));
//2. 啓動Socket,
Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(),8848);
//3. 獲取Socket輸出OutputStream對象,發送數據給服務器
OutputStream outputStream = socket.getOutputStream();
int length = -1;
byte buf[] = new byte[1024 * 8];
// 4. 讀取數據,發送數據
while ((length = bis.read(buf)) != -1) {
outputStream.write(buf,0,length);
}
// 5.關閉資源
socket.close();
bis.close();
}
}
4.7.3 服務端程序
流程:
1. 開啓服務端服務,創建ServerSocket對象
2. 明確保存文件的位置,創建對應文件夾的輸出緩衝字節流
3. 讀取數據,寫入文件
4. 關閉服務器
代碼如下:
package fileupload;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
流程:
1. 開啓服務端服務,創建ServerSocket對象
2. 明確保存文件的位置,創建對應文件夾的輸出緩衝字節流
3. 讀取數據,寫入文件
4. 關閉服務器
*/
public class tcpServer {
public static void main(String[] args) throws IOException {
//1. 開啓服務端服務,創建ServerSocket對象
ServerSocket serverSocket = new ServerSocket(8848);
//獲取客戶端socket,合體
Socket accept = serverSocket.accept();
// 2. 明確保存文件的位置,創建對應文件夾的輸出緩衝字節流
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(new File("d:/aaa/temp.mp4")));
// 3. 獲取Socket對應的輸入流
InputStream inputStream = accept.getInputStream();
int length = -1;
byte[] bytes = new byte[1024*8];
// 4. 邊讀邊寫
while((length = inputStream.read(bytes)) != -1) {
bufferedOutputStream.write(bytes,0,length);
}
//關閉資源
bufferedOutputStream.close();
serverSocket.close();
accept.close();
}
}
這裏用到了IO流操作,用於寫入讀取文件。如果有看不懂的可以回顧一下IO博客。IO流
4.7.4 目前服務端代碼問題
在上邊的代碼中,我們存在一些邏輯問題
-
保存的文件名都是一致的,無法保存多個文件。
這裏可以考慮使用UUID作爲文件名 -
服務端沒有這麼low,代碼肯定不能執行完一個上傳功能就結束
-
同理,服務端代碼不可能只有一個上傳文件功能
在這裏多線程可以很好地解決問題
解決問題如下:
服務端代碼優化如下:
package fileupload;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BetterTcpServer {
public static void main(String[] args) throws IOException {
System.out.println("服務端代碼啓動");
// 1. 啓動TCP服務端服務
ServerSocket serverSocket = new ServerSocket(8848);
// 2. 使用線程池
ExecutorService pool = Executors.newFixedThreadPool(5);
while (true) {
Socket accept = serverSocket.accept();
pool.submit(() -> {
System.out.println(Thread.currentThread().getName());
try {
//設置文件名,用UUID來設置隨機不重複文件名並且把下劃線取消掉
String filename = UUID.randomUUID().toString().replaceAll("-", "");
//新建緩衝輸出字節流,並且確定文件存放位置
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
new FileOutputStream(new File("d:/aaa/temp/",filename + ".mp4")));
//獲取客戶端傳來的inputstream
InputStream inputStream = accept.getInputStream();
//邊讀邊寫
int length = -1;
byte[] bus = new byte[1024*8];
while((length = inputStream.read(bus)) != -1) {
bufferedOutputStream.write(bus);
}
// 5. 關閉資源
bufferedOutputStream.close();
accept.close();
//serverSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
}
}
手速還可以,截了張圖
可以看到,實現了多線程操作,而且可以儲存多分一樣的文件,文件名使用UUID隨機。也可以看到,代碼運行的時候,我的網速跑到了十幾M每秒,代碼優化成功。
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐
鏈接