網絡基礎
計算機網絡
計算機網絡是指將地理位置不同的具有獨立功能的多臺計算機及其外部設備,通過通信線路連接起來,在網絡操作系統,網絡管理軟件及網絡通信協議的管理和協調下,實現資源共享和信息傳遞的計算機系統。
網絡編程就是用來實現網絡互連的不同計算機上運行的程序間可以進行數據交換。
網絡通信
兩臺計算機通過網絡進行通信,需要具備以下條件
IP地址:計算機唯一標識
協議:通訊語言/規則
端口:程序入口,不同進程的標識
TCP/IP協議
TCP/IP是目前世界上應用最為廣泛的協議
是以TCP和IP為基礎的不同層次上多個協議的集合
也稱:TCP/IP協議族或TCP/IP協議棧
TCP:Transmission Control Protocol
傳輸控制協議
IP:Internet Protocol
互聯網協議
TCP/IP模型
相關概念
IP地址:實現網絡中不同計算機之間的通信,每臺機器都必須有一個唯一的標識
端口:用于區分不同應用程序,端口號范圍0-65535,其中0-1023是系統保留的端口號
IP地址和端口號組成了所謂的Socket
,Socket
是網絡上運行的程序之間雙向通信鏈路的終結點,是TCP和UDP的基礎
java中的網絡支持
針對網絡通信的不同層次,Java提供的網絡功能有四大類:在 java.net包下
InetAddress
: 用于標識網絡上的硬件資源
URL
: 統一資源定位符通過URL可以直接讀取或寫入網絡上的數據
Socket
: 使用TCP協議實現網絡通信的Socket相關的類
Datagram
: 使用UDP協議,將數據保存在數據報中,通過網絡進行通信
InetAddress類
此類表示互聯網協議(IP)地址
常用方法
示例
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
public class InetAddressDemo {
public static void main(String[] args) throws UnknownHostException {
// 獲取本機的IntetAddress實例
InetAddress address = InetAddress.getLocalHost();
System.out.println("計算機名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();// 獲取字節數組形式的IP地址
System.out.println("字節數組:" + Arrays.toString(bytes));
System.out.println(address);// 直接輸出InetAddress對象
// InetAddress address2 = InetAddress.getByName("DESKTOP-JQI6O11");
InetAddress address2 = InetAddress.getByName("192.168.84.1");
System.out.println("計算機名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
/*
* 輸出結果
* 計算機名:DESKTOP-JQI6O11
* IP地址:192.168.84.1
* 字節數組:[-64, -88, 84, 1]
* DESKTOP-JQI6O11/192.168.84.1
* 計算機名:DESKTOP-JQI6O11
* IP地址:192.168.84.1
*/
}
}
URL類
URL(Uniform Resource Locator) 統一資源定位符,表示Internet上某一資源的地址
URL主要由兩部分組成:協議名稱和資源名稱,中間用冒號隔開
構造方法
常用方法
示例
import java.net.MalformedURLException;
import java.net.URL;
public class URLDemo {
public static void main(String[] args) {
try {
URL silly = new URL("http://www.silly.com");
//?后面代表參數,#后面表示錨點
URL url = new URL(silly, "/index.html?username=tom#test");
System.out.println("協議:"+url.getProtocol());
System.out.println("主機:"+url.getHost());
//如果未指定端口號,則使用默認http協議的端口號80進行訪問
//對于缺省端口號getPort()方法統一返回-1
System.out.println("端口:"+url.getPort());
System.out.println("文件路徑:"+url.getPath());
System.out.println("文件名:"+url.getFile());
System.out.println("相對路徑:"+url.getRef());
System.out.println("查詢字符串:"+url.getQuery());
} catch (MalformedURLException e) {
e.printStackTrace();
}
/*
協議:http
主機:www.silly.com
端口:-1
文件路徑:/index.html
文件名:/index.html?username=tom
相對路徑:test
查詢字符串:username=tom
*/
}
}
使用URL讀取網頁內容
通過URL對象的openStream()
方法可以得到指定資源的輸入流
通過輸入流可以讀取、訪問網絡上的數據
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
public class URLDemo2 {
public static void main(String[] args) {
//創建url實例
try {
URL url = new URL("http://www.baidu.com");
//通過url的openStream方法獲取url對象所表示的資源的字節輸入流
InputStream is = url.openStream();
//將字節輸入流轉化為字符輸入流
InputStreamReader isr = new InputStreamReader(is,"utf-8");
//為字符輸入流添加緩沖
BufferedReader br = new BufferedReader(isr);
String data = br.readLine(); //讀取數據
while(data != null){ //循環讀取數據
System.out.println(data);//輸出數據
data = br.readLine();
}
br.close();
is.close();
isr.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/*
打印的內容就是百度首頁的html文檔
*/
}
Socket通信簡介
TCP協議是面向連接、可靠的、有序的,以字節流的方式發送數據
基于TCP協議實現網絡通信的類
— 客戶端的Socket
類
— 服務器端的ServerSocket
類
通訊模型
Socket通信實現步驟
- 創建ServerSocket和Socket
- 打開連接到Socket的輸入/輸出流
- 按照協議對Socket進行讀/寫操作
- 關閉輸入輸出流、關閉Socket
ServerSocket類
此類實現服務器套接字。服務器套接字等待請求通過網絡傳入并進行相關操作
構造方法
ServerSocket(int port)
:創建綁定到特定端口的服務器套接字
常用方法
Socket類
此類實現客戶端套接字,套接字是兩臺機器間通信的端點
構造方法
Socket(String host, int port)
:創建一個套接字并將其連接到指定主機上的指定端口號
常用方法
DatagramPacket類
此類表示數據報包,數據報包用來實現無連接包投遞服務
構造方法
常用方法
DatagramSocket類
此類表示用來發送和接收數據報包的套接字,數據報套接字是包投遞服務的發送或接收點
構造方法
常用方法
基于TCP的socket通信
服務端
- 創建
ServerSocket
對象,綁定監聽端口 - 通過
accept()
方法監聽客戶端請求 - 連接建立后,通過輸入流讀取客戶端發送的請求信息
- 通過輸出流向客戶端發送響應信息
- 關閉相關資源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 基于tcp協議的socket通信,實現用戶登錄
* 服務器端
*/
public class Server {
public static void main(String[] args) {
try {
//1.創建服務器端Socket,指定綁定的端口
ServerSocket server = new ServerSocket(8888);
//2.調用accept()方法開始監聽,等待客戶端的連接
System.out.println("***服務器啟動,等待客戶端連接***");
Socket socket = server.accept();
//3.獲取輸入流,并讀取客戶端信息
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));//轉換為緩沖字符流
String info = null;
while((info = br.readLine()) != null){//循環讀取客戶端信息
System.out.println("我是服務器,客戶端說:"+info);
}
//此處必須關閉輸入流
socket.shutdownInput();
//4.獲取輸出流,響應客戶端的請求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);//包裝為打印流
pw.write("歡迎您!");
pw.flush(); //將緩沖輸出
socket.shutdownOutput();
//5.關閉資源
socket.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客戶端
- 創建
Socket
對象,指明需要連接的服務器的地址和端口號 - 連接建立后,通過輸出流向服務器端發送請求信息
- 通過輸入流獲取服務器響應的信息
- 關閉相關資源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 基于TCP的socket通信,實現用戶登錄
* 客戶端
*/
public class Client {
public static void main(String[] args) {
try {
// 1.創建客戶端Socket,指定服務器地址和端口
Socket socket = new Socket("127.0.0.1", 8888);
// 2.獲取輸出流,向服務器端發送信息
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);// 將輸出流包裝為打印流
pw.write("用戶名:alice;密碼:123456");
pw.flush();
// 此處必須關閉輸出流
//注意不能使用os.close()或者pw.close()方法關閉流,會導致socket也被關閉
socket.shutdownOutput();
// 3.獲取輸入流,并讀取服務器端的響應信息
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String info = null;
while ((info = br.readLine()) != null) {
System.out.println("我是客戶端,服務器說:" + info);
}
socket.shutdownInput();
// 4.關閉資源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運行結果
首先啟動服務端,然后啟動客戶端
# 服務端打印內容
***服務器啟動,等待客戶端連接***
我是服務器,客戶端說:用戶名:alice;密碼:123456
# 客戶端打印內容
我是客戶端,服務器說:歡迎您!
多線程服務器
應用多線程來實現服務器與多客戶端之間的通信基本步驟
- 服務器端創建
ServerSocket
,循環調用accept()
等待客戶端連接 - 客戶端創建一個
socket
并請求和服務器端連接 - 服務器端接受客戶端請求,創建
socket
與該客戶建立專線連接 - 建立連接的兩個
socket
在一個單獨的線程上對話 - 服務器端繼續等待新的連接
創建服務端線程處理類
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 服務器線程處理類
*/
public class ServerThread implements Runnable{
// 和本線程相關的Socket
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
//線程執行的操作,響應客戶端的請求
public void run() {
try {
//獲取輸入流,并讀取客戶端信息
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));//轉換為緩沖字符流
String info = null;
while((info = br.readLine()) != null){//循環讀取客戶端信息
System.out.println("我是服務器,客戶端說:"+info);
}
//此處必須關閉輸入流
socket.shutdownInput();
//獲取輸出流,響應客戶端的請求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);//包裝為打印流
pw.write("歡迎您!");
pw.flush();//將緩沖輸出
socket.shutdownOutput();
//關閉資源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服務端改寫
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
*
* 基于tcp協議的socket通信,實現用戶登錄
* 服務器端多線程實現
*/
public class Server {
public static void main(String[] args) {
try {
//創建服務器端Socket,指定綁定的端口
ServerSocket server = new ServerSocket(8888);
Socket socket = null;
//記錄計數連接過服務器的客戶端數量
int count = 0;
System.out.println("***服務器啟動,等待客戶端連接***");
//循環監聽等待客戶端的連接
while(true){
//調用accept()方法開始監聽,等待客戶端的連接
socket = server.accept();
//創建一個新的線程
ServerThread serverThread = new ServerThread(socket);
Thread thread = new Thread(serverThread);
thread.setPriority(4); //設置線程優先級,范圍為[1-10],默認值為5
//啟動線程
thread.start();
count++;//統計數量
System.out.println("連接過服務器端的客戶端的數量:"+count);
InetAddress address = socket.getInetAddress();
System.out.println("當前客戶端的IP:"+address.getHostAddress());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
運行結果
首先啟動服務端,然后啟動一個客戶端,修改客戶端寫入的內容,再次啟動一個客戶端
# 服務端打印內容
***服務器啟動,等待客戶端連接***
連接過服務器端的客戶端的數量:1
當前客戶端的IP:127.0.0.1
我是服務器,客戶端說:用戶名:alice;密碼:123456
連接過服務器端的客戶端的數量:2
當前客戶端的IP:127.0.0.1
我是服務器,客戶端說:用戶名:tom;密碼:555666
基于UDP的socket通信
UDP協議(用戶數據報協議)是無連接、不可靠的、無序的,UDP協議以數據報作為數據傳輸的載體
進行數據傳輸時,首先需要將要傳輸的數據定義成數據報(Datagram)
,在數據報中指明數據所要到達的Socket
(主機地址和端口號),然后再將數據報發送出去
服務器端
- 創建
DatagramSocket
,指定端口號 - 創建
DatagramPacket
- 接收客戶端發送的數據信息
- 讀取數據
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* 服務器端,實現基于UDP的用戶登陸
*/
public class UDPServer {
public static void main(String[] args) throws IOException {
/*********接收客戶端發送的數據*********/
// 1.創建服務器端DatagramSocket,指定端口
DatagramSocket socket = new DatagramSocket(8800);
// 2.創建數據報,用于接收客戶端發送的數據
byte[] data = new byte[1024];// 創建字節數組,指定接收的數據包的大小
DatagramPacket packet = new DatagramPacket(data, data.length);
// 3.接收客戶端發送的數據
System.out.println("***服務器端已經啟動,等待客戶端發送數據***");
socket.receive(packet);// 此方法在接收到數據報之前會一直阻塞
// 4.讀取數據
String info = new String(packet.getData(), 0, packet.getLength());
System.out.println("我是服務器,客戶端說:" + info);
/*********向客戶端響應數據*********/
// 1.定義客戶端的地址、端口號、數據
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 = "歡迎您!".getBytes();
// 2.創建數據報,包含響應的數據信息
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
// 3.響應客戶端
socket.send(packet2);
// 4.關閉資源
socket.close();
}
}
客戶端
- 定義發送信息
- 創建
DatagramPacket
,包含將要發送的信息 - 創建
DatagramSocket
- 發送數據
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* 客戶端,實現基于UDP的用戶登陸
*/
public class UDPClient {
public static void main(String[] args) throws IOException {
/*********向服務器端發送數據*********/
// 1.定義服務器的地址、端口號、數據
InetAddress address = InetAddress.getByName("localhost");
int port = 8800;
byte[] data = "用戶名:admin;密碼:123".getBytes();
// 2.創建數據報,包含發送的數據信息
DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
// 3.創建DatagramSocket對象
DatagramSocket socket = new DatagramSocket();
// 4.向服務器端發送數據報
socket.send(packet);
/*********接收服務器端響應的數據*********/
// 1.創建數據報,用于接收服務器端響應的數據
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
// 2.接收服務器響應的數據
socket.receive(packet2);
// 3.讀取數據
String reply = new String(data2, 0, packet2.getLength());
System.out.println("我是客戶端,服務器說:" + reply);
// 4.關閉資源
socket.close();
}
}
運行結果
首先啟動服務端,然后啟動客戶端
# 服務端打印內容
***服務器端已經啟動,等待客戶端發送數據***
我是服務器,客戶端說:用戶名:admin;密碼:123
# 客戶端打印內容
我是客戶端,服務器說:歡迎您!
Socket總結
多線程優先級
在使用多線程處理多客戶端通信時,未設置優先級時可能會導致運行時速度非常慢,可以降低優先級
輸入和輸出流的關閉
- 在同時使用輸入和輸出流時,
socket
只能接受一個流,要么輸入要么輸出。完成輸入或輸出流之后必須關閉(調用socket.shutdownInput()
或socket.shutdownInput()
)讓下一個流進來,此時socket
仍然是連接狀態。 - 對于同一個
socket
,如果直接關閉輸入或者輸出流(調用close()
方法),則與該輸入或者輸出流關聯的socket
也會被關閉,所以一般不用關閉流,直接關閉socket
即可。
使用TCP通信傳輸對象
真實的開發中都是以對象作為傳輸數據,可以使用ObjectOutputStream
和 ObjectInputStream
完成對象傳輸
1.定義User類,實現序列化接口
public class User implements Serializable{
private String username;
private String password;
//省略getter和setter
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
2.修改Client發送邏輯
3.修改ServerThread處理邏輯
Socket編程傳遞文件
這里簡單演示服務器向客戶端發送文件
1.修改ServerThread處理邏輯
2.修改Client邏輯