Java知識點——網絡編程(如何網絡編程和多線程實現tcp基礎多文件交換功能)

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的缺點之一,因爲面向無連接,所以可能會丟數據,類似於玩電腦遊戲丟包,瞬移,卡頓。

  1. 網絡不夠好,穩定性不行,帶寬不夠
  2. 電腦性能不好

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

  1. 服務端Socket
    java.net.ServerSocket;
    創建對應的ServerScoket開啓服務器,等待客戶端連接

  2. 客戶端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 目前服務端代碼問題

在上邊的代碼中,我們存在一些邏輯問題

  1. 保存的文件名都是一致的,無法保存多個文件。
    這裏可以考慮使用UUID作爲文件名

  2. 服務端沒有這麼low,代碼肯定不能執行完一個上傳功能就結束

  3. 同理,服務端代碼不可能只有一個上傳文件功能

    在這裏多線程可以很好地解決問題
    解決問題如下:
    服務端代碼優化如下:

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每秒,代碼優化成功。
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐
鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章