目錄
前言
學習網絡編程前,需要有IO流使用基礎,以及多線程編程基礎。
OSI概述
層 | 模型 | 功能 | TCP/IP四層模型 | 網絡協議 |
7 | 應用層 | 程序,軟件的運行操作 | 應用層 | HTTP、TFTP, FTP, NFS, WAIS、SMTP |
6 | 表示層 |
對數據的接收進行解釋,加密與解密,壓縮與解壓縮等,也就是把計算機能夠識別的東西轉換成人能識別的東西,如圖片,音頻 |
Telnet, Rlogin, SNMP, Gopher | |
5 | 會話層 |
通過傳輸層(端口號:傳輸端口與接收端口)建立連接通道。 主要用於接收回話與發送會話請求,設備之間互相認識可以是IP、MAC、主機名。 |
SMTP, DNS | |
4 | 傳輸層 |
定義了一些傳輸協議和端口號(如 www 80端口等)。 TCP(傳輸控制協議)傳輸效率低,可靠性強,傳輸數據量大的數據。 UDP(用戶數據報協議)與TCP恰恰相反,用於傳輸可靠性不高,數據量低的數據。如QQ就是UDP協議傳輸信息。 此層主要是將下層接收數據進行分段和傳輸,傳輸數據數據到大目的後再進行重組,長長把這一層叫做段。 |
傳輸層 | TCP, UDP |
3 | 網絡層 |
主要是將下層接收的數據進行IP地址(如192.168.x.x)的封裝與解封裝。 這一層的工作設備是路由器,常把這一層叫做數據包。 |
網際層 | IP, ICMP, ARP, RARP, AKP, UUCP |
2 | 數據鏈路層 |
主要是從將物理層接收到的數據進行MAC地址(網卡地址)的封裝與解封裝。 常把這一層數據叫做幀,這一層的工作設備是交換機 |
數據鏈路層 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,幀中繼 |
1 | 物理層 |
主要定義物理設備標準,如網卡接口類型,光線接口類型,各種傳輸介質的傳輸速率等。 主要作用是傳輸Bit比特流,(也就是由1、0轉換爲電流強弱進行傳輸,到達目的後再轉換爲1、0,這一層的數據稱爲bit比特流) |
IEEE 802.1A, IEEE 802.2到IEEE 802. |
傳輸層
主要任務就是負責向兩臺主機進程之間的通信提供通用的數據傳輸服務。應用進程利用該服務傳送應用層報文。
傳輸層主要使用一下兩種協議
- 傳輸控制協議-TCP:提供面向連接的,可靠的數據傳輸服務。
- 用戶數據協議-UDP:提供無連接的,盡最大努力的數據傳輸服務(不保證數據傳輸的可靠性)。
UDP | TCP | |
是否連接 | 無連接 | 面向連接 |
是否可靠 | 不可靠傳輸,不使用流量控制和擁塞控制 | 可靠傳輸,使用流量控制和擁塞控制 |
連接對象個數 | 支持一對一,一對多,多對一和多對多交互通信 | 只能是一對一通信 |
傳輸方式 | 面向報文 | 面向字節流 |
首部開銷 | 首部開銷小,僅8字節 | 首部最小20字節,最大60字節 |
場景 | 適用於實時應用(IP電話、視頻會議、直播、文字信息等) | 適用於要求可靠傳輸的應用,例如文件傳輸 |
1. 運行在TCP協議上的協議:
HTTP(Hypertext Transfer Protocol,超文本傳輸協議),主要用於普通瀏覽。
HTTPS(HTTP over SSL,安全超文本傳輸協議),HTTP協議的安全版本。
FTP(File Transfer Protocol,文件傳輸協議),用於文件傳輸。
POP3(Post Office Protocol, version 3,郵局協議),收郵件用。
SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議),用來發送電子郵件。
TELNET(Teletype over the Network,網絡電傳),通過一個終端(terminal)登陸到網絡。
SSH(Secure Shell,用於替代安全性差的TELNET),用於加密安全登陸用。
2. 運行在UDP協議上的協議:
BOOTP(Boot Protocol,啓動協議),應用於無盤設備。
NTP(Network Time Protocol,網絡時間協議),用於網絡同步。
DHCP(Dynamic Host Configuration Protocol,動態主機配置協議),動態配置IP地址。
3. 運行在TCP和UDP協議上:
DNS(Domain Name Service,域名服務),用於完成地址查找,郵件轉發等工作。
IP地址分類及DNS解析
簡單瞭解IP地址分類與DNS解析鏈接:
端口
- 用於標識不同進程(程序)的地址(程序標識)
- 有效端口:0 ~ 65535
- 保留端口:0 ~ 1024。這是系統系統使用的端口。
- 作用:IP地址僅僅代表一臺主機,主機與主機之間的進程要想進行通信,進程必須獨立監聽一個端口。
Socket
Socket就是爲網絡服務提供的一種機制
- 網絡通信其實就是Socket之間的通信
- 通信兩段都有Socket
- 數據在兩個Socket之間通過IO傳輸
IP類
InetAddress類
此類用來描述IP地址的,並沒有構造IP的公共構造方法,因爲計算機底層(網絡層)本身已經幫我們封裝好了,我們要想拿到本機IP與外網IP只需要調用其靜態方法獲得InetAddress對象。
Inet4Address(IPv4)和Inet6Address(IPv6)對象都繼承自InetAddress類
Java網絡操作框架,在java.net包
基礎常見方法:
- InetAddress InetAddress.getLocalHost(),獲得本主機IP地址對象。
- String getHostAddress(),獲得IP地址。
- String getHostName(),獲得主機名。
- InetAddress getByName(String host),直接傳遞域名,或IP地址構造InetAddress。
package com.bin.demo;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
//獲取本機器的IP及主機名
InetAddress my_inetAddress = InetAddress.getLocalHost();
String my_ip = my_inetAddress.getHostAddress();
String my_name = my_inetAddress.getHostName();
System.out.println(my_ip + " ———— " + my_name);
//獲取指定域名IP地址及主機名
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
String str_ip = inetAddress.getHostAddress();
String str_name = inetAddress.getHostName();
System.out.println(str_ip + " ———— " + str_name);
//如果不是直接傳字符串IP地址,而是直接傳域名,其實就是通過設置的DNS服務器對域名解析成IP
my_inetAddress = InetAddress.getByName("localHost"); //DNS解析會去hosts文件查找是否已有解析緩存
my_ip = my_inetAddress.getHostAddress();
my_name = my_inetAddress.getHostName();
System.out.println(my_ip + " ———— " + my_name);
}
}
輸出:
192.168.0.108 ———— bin
183.232.231.172 ———— www.baidu.com
127.0.0.1 ———— localHost
UDP協議間的通信
UDP沒有客戶端和服務端之說,因爲UDP不關心是否消息是否被安全送達
UDP發送每個數據報包最大限制64k。
UDP協議相關類
DatagramSocket類
- 此類表示用來發送和接收數據報包的套接字,使用UDP協議。
- send( DatagramPacket p ):發送數據。
- receive( DatagramPacket p ):接收數據。阻塞性方法。
- close():涉及到使用系統IO資源,使用完時關閉資源。
- 不同步的,在多線程中 能用一個DatagramSocket來發送和接收。
- 接收端:必需要爲程序構造或設置一個 監聽端口
- 發送端 和 接收端:都需要指定一個自身運行程序的 監聽端口,不指定則默認使用系統未分配的可用端口,一般只需要設置接收端的 監聽端口。
- 一個機器裏每個DatagramSocket對象只能監聽一個獨立的端口,如果一個端口已被其它程序佔用,再次綁定時會報java.net.BindException: Address already in use: Cannot bind 異常
常用的構造方法:
DatagramPacket類
- 此類表示數據報包。
- 發送端:必需要構造一個 目的IP + 目的端口
- 數據報包:發送端和接收端都需要一個byte[]數組進行存儲數據,這個byte[]將成爲緩衝區,也就是UDP不能發超過64K的數據。因爲封裝的UDP類有緩衝區會自動提取 下一段 數據併發送,所以不必擔心。
接收端接收數據時,解包獲取數據的常見方法:
- InetAddress getAddress():獲取源IP對象。
- int getPort():獲取源端口。
- byte[] getData():獲取接收到的數據,但數據字節的長度可能 不是 數組的長度,需要 getLength()方法獲取準確的數據字節長度。
- int getLength():獲取已接收到的數據字節長度。
紅框圈起來的是接收端的數據報包構造,其餘有設置 IP + port 的都是發送端的數據報包構造。
UDP發送端
package com.bin.demo;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
// TODO
}
public static void send() throws IOException {
// UDP發送端
DatagramSocket ds = new DatagramSocket();
// 明確數據源
byte[] data = "user = bin; passworld = 7758258;".getBytes("UTF-8"); // 使用UTF-8編碼
// 明確目的地IP + port
InetAddress ip = InetAddress.getByName("192.168.0.108"); // 我的本機IP,發給我自己
int port = 21024; // 目地程序監聽端口
// 構造數據報包
DatagramPacket dp = new DatagramPacket(data, data.length, ip, port);
// 發送數據報包
ds.send(dp);
// 關閉IO資源
ds.close();
}
}
UDP接收端 + 發送端
package com.bin.demo;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
receive(); //先啓動接收端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(100); //讓main線程暫停100ms,防止接收端線程未啓動
send();
}
public static void send() throws IOException {
// UDP發送端
DatagramSocket ds = new DatagramSocket();
// 明確數據源
byte[] buf = "user = bin; passworld = 7758258;".getBytes("UTF-8"); // 使用UTF-8編碼
// 明確目的地IP + port
InetAddress ip = InetAddress.getByName("192.168.0.108"); // 我的本機IP,發給我自己
int port = 21024; // 目地程序監聽端口
// 構造數據報包:封包
DatagramPacket dp = new DatagramPacket(buf, buf.length, ip, port);
// 發送數據報包
ds.send(dp);
// 關閉IO資源
ds.close();
}
public static void receive() throws IOException {
// UDP接收端
DatagramSocket ds = new DatagramSocket(21024); //必須明確監聽端口
// 構造接收數據報包容器:解包(對象)
byte[] buf = new byte[1024]; //緩衝區大小
DatagramPacket dp = new DatagramPacket(buf, buf.length);
//接收數據報包
ds.receive(dp); //等待數據:阻塞性方法
//提取數據
String str_ip = dp.getAddress().getHostAddress();
int port = dp.getPort();
// 通過數據解包對象獲取byte[]數據,並通過getLength()方法獲取已接收的字節長度
String data = new String(dp.getData(), 0, dp.getLength(), "UTF-8"); // 指定編碼解碼
System.out.println(str_ip + " : " + port + " data--> " + data); //打印輸出
//關閉IO資源
ds.close();
}
}
輸出:
192.168.0.108 : 59254 data--> user = bin; passworld = 7758258;
UDP聊天 + 全局廣播
這裏要實現一個類似對講機的功能,就是即能發送又能接收的功能。涉及到多線程,一條線程負責發送,一條線程負責接收
Phone.java類:用於發送和接收的描述類
package com.bin.demo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class Phone {
private InetAddress sendIP; //發送目的地
private int receivePort; //接收目的地
private class Send implements Runnable {
private DatagramSocket ds;
Send(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //從鍵盤中讀取數據
String line;
try {
while ((line = br.readLine()) != null) { //循環讀取,readLine()阻塞性方法,讀取一行數據
byte[] buf = line.getBytes("UTF-8"); //指定編碼
DatagramPacket dp = new DatagramPacket(buf, buf.length, sendIP, receivePort); //byte[]數據 + 目地IP + 目地port
ds.send(dp); //發送數據
if ("over".equals(line)) { //定義標記,退出聊天
break;
}
}
} catch (IOException e){
// ...
} finally {
if (ds != null && !ds.isClosed()) { //關閉資源
ds.close();
}
}
}
}
private class Receive implements Runnable {
private DatagramSocket ds;
Receive(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
while (true) { //無限循環接收消息
byte[] buf = new byte[1024]; //緩衝區容器大小
DatagramPacket dp = new DatagramPacket(buf, buf.length); //解包對象
ds.receive(dp); //等待消息,阻塞性方法
String ip = dp.getAddress().getHostAddress(); // 獲取源IP
int port = dp.getPort(); // 獲取源端口
String data = new String(dp.getData(), 0, dp.getLength(), "UTF-8"); // 獲取數據
if ("over".equals(data)) { // 讀取到有人退出聊天
System.out.println(ip + " : " + port + " [已離線]");
if (InetAddress.getLocalHost().getHostAddress().equals(ip)) { //如果是本機發出的over,接收線程也退出
break;
}
} else {
System.out.println(ip + " : " + port + " ---> " + data); //打印消息
}
}
} catch (IOException e) {
// ...
} finally {
if (ds != null && !ds.isClosed()) { //關閉資源
ds.close();
}
}
}
}
public Phone(String sendIP, int receivePort) throws UnknownHostException { // 構造方法
this.sendIP = InetAddress.getByName(sendIP); // 解析成InetAddress對象
this.receivePort = receivePort; // 接收監聽的端口
}
public void start() throws SocketException {
// 構造一個DatagramSocket服務
DatagramSocket ds = new DatagramSocket(receivePort); // 發送和接收的線程都使用一個UDP服務端口
new Thread(new Receive(ds)).start();
new Thread(new Send(ds)).start();
}
}
main.java
package com.bin.demo;
public class Main {
public static void main(String[] args) throws Exception {
Phone phone = new Phone("255.255.255.255", 21024);
phone.start();
}
}
輸出:
雷姆
192.168.0.108 : 21024 ---> 雷姆
愛密莉亞
192.168.0.108 : 21024 ---> 愛密莉亞
狂三
192.168.0.108 : 21024 ---> 狂三
over
192.168.0.108 : 21024 [已離線]
255.255.255.255是局域網全局廣播,這樣每臺機器都能接收到消息。
也可以指定網段 192.168.x.255。
UDP發送大文件
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
receive();
System.out.println("複製完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //mian線程等待200ms,防止接收端未開啓
System.out.println("正在發送... ...");
send();
}
public static void send() throws Exception {
//創建UDP發送端對象
DatagramSocket ds = new DatagramSocket();
//目的地
InetAddress sendIP = InetAddress.getByName("192.168.0.108");
int sendPort = 10248;
FileInputStream fis = new FileInputStream("F:\\avi.avi"); //讀取要發送的文件
byte[] buf = new byte[1024]; //每次讀取的字節緩衝大小
int len = 0; //讀取到的字節數
while ((len = fis.read(buf)) != -1) {
// 封裝UDP包
DatagramPacket dp = new DatagramPacket(buf, 0, len, sendIP, sendPort);
//發送
ds.send(dp);
}
byte[] flag = "over".getBytes(); //結束標記
ds.send(new DatagramPacket(flag, flag.length, sendIP, sendPort));
//關閉資源
fis.close();
ds.close();
}
public static void receive() throws Exception {
//創建UDP接收端對象,並指定監聽一個端口
DatagramSocket ds = new DatagramSocket(10248);
//要存放的目地
FileOutputStream fos = new FileOutputStream("F:\\avi_back.avi");
byte[] buf = new byte[1024]; //接收的容器大小
DatagramPacket dp = new DatagramPacket(buf, buf.length);
while (true) {
ds.receive(dp); //等待消息
byte[] data = dp.getData();
if ("over".equals(new String(data, 0, dp.getLength()))) {
break;
}
fos.write(data, 0, dp.getLength()); //寫入
}
//關閉資源
fos.close();
ds.close();
}
}
輸出:
正在發送... ...
複製完成
圖:
TCP協議間的通信
TCP面向連接也就是3此握手4次揮手,分客戶端和服務端。
客戶端和服務端建立連接後,都能進行數據的發送和接收
TCP協議相關類
客戶端:
Socket類
此類實現客戶端套接字(也可以就叫“套接字”)。套接字是兩臺機器間通信的端點。
基礎構造方法:
基礎使用方法:
- getInputStream():返回此套接字的輸入流。
- getOutputStream():返回此套接字的輸出流。
- shutdownOutputStream():禁用此套接字的輸出流。也就是通知服務器此套接字已經輸出完成。(因爲輸出如文件類型的大數據,服務端會一直等待讀取,客戶端輸出完畢時,服務端並不知道),其實就是發送了一個輸出結束標記給服務器。
- InetAddress getInetAddress():獲取此套接字綁定的IP地址對象。
- boolean isConnected():判斷此套接字是否成功連接服務器。
- close():關閉IO流。
服務端:
ServerSocket類
此類實現服務器套接字。服務器套接字等待請求通過網絡傳入。它基於該請求執行某些操作,然後可能向請求者返回結果。
構造方法:
基礎使用方法:
- Socket accept():此方法是阻塞性方法,等待客戶端連接,返回Socket套接字對象。
- close():服務端一般都是不用關閉的。
Socket訪問ServerSocket原理
TCP客戶端
package com.bin.demo;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// TODO
}
public static void socket() throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 21024); // 明確連接地址
// 明確數據源,這裏發送一條文本數據
byte[] buf = "雷姆".getBytes("UTF-8"); // 指定編碼
// 獲取輸出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 發送數據
// 關閉資源,out字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
s.close();
}
}
TCP服務端 + 客戶端
package com.bin.demo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服務端啓動...");
serverSocket(); //先啓動服務端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main線程暫停200ms,防止服務端未啓動
System.out.println("TCP客戶端啓動...");
socket("雷姆"); //啓動客戶端
}
public static void socket(String info) throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 21024); // 明確連接地址
// 明確數據源,這裏發送一條文本數據
byte[] buf = info.getBytes("UTF-8"); // 指定編碼
// 獲取輸出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 發送數據
// 關閉資源,out字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
s.close();
}
public static void serverSocket() throws IOException {
// TCP服務端
ServerSocket ss = new ServerSocket(21024); // 監聽一個端口
// 獲取Socket套接字對象
Socket s = ss.accept(); // 阻塞性方法
// 獲取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印輸出看看是誰連接了服務端
System.out.println(ip + " : " + port + " connection");
// 獲取此套接字的讀取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024]; // 數據容器
int length = in.read(buf); // 讀取數據到容器,並返回讀取到數據的字節長度
String data = new String(buf, 0, length, "UTF-8");
//打印輸出數據
System.out.println(ip + " : " + port + " data————> " + data);
//關閉資源,in字節讀取流本身是Socket套接字所持有,只需關閉套接字
s.close(); //這個套接字是在本服務器內存生成的,所以需要關閉套接字
ss.close(); //關閉服務端套接字
}
}
輸出:
TCP服務端啓動...
TCP客戶端啓動...
192.168.0.108 : 51381 connection
192.168.0.108 : 51381 data————> 雷姆
注意:
這裏服務端獲取連接的Socket套接字的讀取流時,沒有用到循環讀取。
如果用了循環讀取,服務端根本不知道是否已經讀取完畢,所以客戶端套接字對象需要調用 shutdownOutputStream() 方法發送輸出結束標記給服務端,Socket告訴ServerSocket我已輸出完成服務端停止讀取。
TCP上傳文件例子將運用 shutdownOutputStream() 方法。
TCP客戶端與服務端互訪
小改一下上面的代碼,客戶端和服務端,都獲取 輸出流 和 輸入流 通道 並操作。
package com.bin.demo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服務端啓動...");
serverSocket(); //先啓動服務端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main線程暫停200ms,防止服務端未啓動
System.out.println("TCP客戶端啓動...");
socket("保護雷姆"); //啓動客戶端
}
public static void socket(String info) throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 21024); // 明確連接地址
// 明確數據源,這裏發送一條文本數據
byte[] buf = info.getBytes("UTF-8"); // 指定編碼
// 獲取輸出通道
OutputStream out = s.getOutputStream();
out.write(buf); // 發送數據
// 等待服務端回話
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 關閉資源,out和in字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
s.close();
}
public static void serverSocket() throws IOException {
// TCP服務端
ServerSocket ss = new ServerSocket(21024); // 監聽一個端口
// 獲取Socket套接字對象
Socket s = ss.accept(); // 阻塞性方法
// 獲取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印輸出看看是誰連接了服務端
System.out.println(ip + " : " + port + " connection");
// 獲取此套接字的讀取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024]; // 數據容器
int length = in.read(buf); // 讀取數據到容器,並返回讀取到數據的字節長度
String data = new String(buf, 0, length, "UTF-8");
//打印輸出數據
System.out.println(ip + " : " + port + " data————> " + data);
// 回覆客戶端
OutputStream out = s.getOutputStream();
out.write("已接收到請求".getBytes("UTF-8"));
//關閉資源,in和out字節讀取流本身是Socket套接字所持有,只需關閉套接字
s.close(); //這個套接字是在本服務器內存生成的,所以需要關閉套接字
ss.close(); //關閉服務端套接字
}
}
輸出:
TCP服務端啓動...
TCP客戶端啓動...
192.168.0.108 : 53471 connection
192.168.0.108 : 53471 data————> 保護雷姆
已接收到請求
TCP上傳文件
其實就是文件的Copy過程,客戶端讀取文件併發送給服務端。
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服務端啓動...");
serverSocket(); //先啓動服務端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main線程暫停200ms,防止服務端未啓動
System.out.println("TCP客戶端啓動...");
socket(); //啓動客戶端
}
public static void socket() throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 21024); // 明確連接地址
// 明確數據源,上傳文件
FileInputStream file_in = new FileInputStream("F:\\test.txt");
// 獲取輸出通道
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024]; //每次讀取的字節容器
int file_length; //讀取到的字節數
while ((file_length = file_in.read(buf)) != -1) {
out.write(buf, 0, file_length); //寫入讀取到的字節長度
}
// 發送輸出結束標記
s.shutdownOutput();
// 等待服務端回話
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 關閉資源,out和in字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
file_in.close(); //關閉Copy文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服務端
ServerSocket ss = new ServerSocket(21024); // 監聽一個端口
// 獲取Socket套接字對象
Socket s = ss.accept(); // 阻塞性方法
// 獲取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印輸出看看是誰連接了服務端
System.out.println(ip + " : " + port + " connection");
// 獲取此套接字的讀取通道 + 明確源文件存放目地
InputStream in = s.getInputStream();
long time = System.currentTimeMillis(); // 獲取當前時間戳設置文件名,避免文件重名或被覆蓋問題
FileOutputStream file_out = new FileOutputStream("F:\\test_" + ++time +".txt"); //存放目的地
byte[] buf = new byte[1024]; // 每次讀取的數據容器
int length; // 讀取到數據的字節長度
while ((length = in.read(buf)) != -1) {
file_out.write(buf, 0, length);
}
// 回覆客戶端
OutputStream out = s.getOutputStream();
out.write("上傳成功".getBytes("UTF-8"));
//關閉資源,in和out字節讀取流本身是Socket套接字所持有,只需關閉套接字
file_out.close(); //關閉寫出文件流
s.close(); //這個套接字是在本服務器內存生成的,所以需要關閉套接字
ss.close(); //關閉服務端套接字
}
}
輸出:
TCP服務端啓動...
TCP客戶端啓動...
192.168.0.108 : 54457 connection
上傳成功
TCP下載文件
其實就是文件的Copy過程,客戶端請求服務端文件並下載。
package com.bin.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服務端啓動...");
serverSocket(); //先啓動服務端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main線程暫停200ms,防止服務端未啓動
System.out.println("TCP客戶端啓動...");
socket(); //啓動客戶端
}
public static void socket() throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 10248); // 明確連接地址
// 請求的文件路徑
String file = new String("F:\\test.txt");
// 獲取輸出通道
OutputStream out = s.getOutputStream();
out.write(file.getBytes("UTF-8")); //發送請求
// 等待服務端返回數據
InputStream in = s.getInputStream();
FileOutputStream file_out = new FileOutputStream("F:\\下載_test.txt");
byte[] inBuf = new byte[1024]; //每次讀取的字節大小
int length;
while ((length = in.read(inBuf)) != -1) {
file_out.write(inBuf, 0, length);
}
System.out.println("下載完成");
// 關閉資源,out和in字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
file_out.close(); //關閉寫出文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服務端
ServerSocket ss = new ServerSocket(10248); // 監聽一個端口
// 獲取Socket套接字對象
Socket s = ss.accept(); // 阻塞性方法
// 獲取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印輸出看看是誰連接了服務端
System.out.println(ip + " : " + port + " connection");
// 獲取此套接字的讀取通道
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int length = in.read(buf);
File file = new File(new String(buf, 0, length));
if (file.exists()) { //檢查服務器是否存在該文件
// 回覆客戶端
OutputStream out = s.getOutputStream(); //獲取輸出通道
FileInputStream file_in = new FileInputStream(file);
byte[] file_buf = new byte[1024];
int file_len;
while ((file_len = file_in.read(file_buf)) != -1) {
out.write(file_buf, 0, file_len);
}
// 發送一個輸出結束標記
file_in.close(); //關閉讀取文件流
}
//關閉資源,in和out字節讀取流本身是Socket套接字所持有,只需關閉套接字
s.close(); //這個套接字是在本服務器內存生成的,所以需要關閉套接字
ss.close(); //關閉服務端套接字
}
}
輸出:
TCP服務端啓動...
TCP客戶端啓動...
192.168.0.108 : 55706 connection
下載完成
TCP多客戶端併發訪問服務器
服務器一般都是不需要直接關閉的,而是持久的運行着。
這裏服務端只需要爲每個已連接Socket套接字開啓新線程,就OK了。+ 服務器無限循環。
這裏用了TCP上傳文件的例子示範
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
new Thread() {
@Override public void run() {
try {
System.out.println("TCP服務端啓動...");
serverSocket(); //先啓動服務端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(200); //main線程暫停200ms,防止服務端未啓動
for (int i = 0; i < 10; ++i) { //創建10條線程併發訪問服務器,上傳文件
new Thread() {
@Override public void run() {
try {
socket(); //啓動客戶端
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
public static void socket() throws IOException {
// TCP客戶端
Socket s = new Socket("192.168.0.108", 21024); // 明確連接地址
// 明確數據源,上傳文件
FileInputStream file_in = new FileInputStream("F:\\test.txt");
// 獲取輸出通道
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024]; //每次讀取的字節容器
int file_length; //讀取到的字節數
while ((file_length = file_in.read(buf)) != -1) {
out.write(buf, 0, file_length); //寫入讀取到的字節長度
}
// 發送輸出結束標記
s.shutdownOutput();
// 等待服務端回話
InputStream in = s.getInputStream();
byte[] inBuf = new byte[1024];
int length = in.read(inBuf);
String data = new String(inBuf, 0, length, "UTF-8");
System.out.println(data);
// 關閉資源,out和in字節輸出流對象本身是Socket套接字所持有,所以只需關閉套接字
file_in.close(); //關閉Copy文件流
s.close();
}
public static void serverSocket() throws IOException {
// TCP服務端
ServerSocket ss = new ServerSocket(21024); // 監聽一個端口
// 無限循環
while (true) {
// 獲取Socket套接字對象
Socket s = ss.accept(); // 阻塞性方法
// 服務端只需要爲每個已連接Socket套接字開啓新線程,就OK了
new Thread(new Task(s)).start();
}
}
static class Task implements Runnable {
private Socket s;
private static int count; //文件計數,防止文件被覆蓋
Task(Socket s) {
this.s = s;
}
@Override public void run() {
try {
// 獲取此套接字的IP地址
String ip = s.getInetAddress().getHostAddress();
int port = s.getPort();
// 打印輸出看看是誰連接了服務端
System.out.println(ip + " : " + port + " connection");
// 獲取此套接字的讀取通道 + 明確源文件存放目地
InputStream in = s.getInputStream();
long time = System.currentTimeMillis(); // 獲取當前時間戳設置文件名,避免文件重名或被覆蓋問題
FileOutputStream file_out = new FileOutputStream("F:\\test_" + time + "_" + ++count + ".txt"); //存放目的地
byte[] buf = new byte[1024]; // 每次讀取的數據容器
int length; // 讀取到數據的字節長度
while ((length = in.read(buf)) != -1) {
file_out.write(buf, 0, length);
}
// 回覆客戶端
OutputStream out = s.getOutputStream();
out.write("上傳成功".getBytes("UTF-8"));
//關閉資源,in和out字節讀取流本身是Socket套接字所持有,只需關閉套接字
file_out.close(); //關閉寫出文件流
s.close(); //這個套接字是在本服務器內存生成的,所以需要關閉套接字
} catch (Exception e) {
// ...
}
}
}
}
輸出:
TCP服務端啓動...
192.168.0.108 : 56845 connection
192.168.0.108 : 56848 connection
192.168.0.108 : 56846 connection
192.168.0.108 : 56847 connection
192.168.0.108 : 56849 connection
上傳成功
上傳成功
上傳成功
192.168.0.108 : 56850 connection
192.168.0.108 : 56852 connection
上傳成功
上傳成功
192.168.0.108 : 56851 connection
192.168.0.108 : 56853 connection
192.168.0.108 : 56854 connection
上傳成功
上傳成功
上傳成功
上傳成功
上傳成功
TCP幾個小問題
- 避免上傳或下載重名問題,文件會被覆蓋
- Socket套接字上傳文件已到末尾結束時,必須調用 Socket套接字的 shutdownOutput() 方法發送輸出結束標記給服務端
- Socket下載服務端文件時,服務端輸出完成無需調用 Socket 套接字的 shutdownOutput() 方法。
- 多客戶端併發訪問服務器時,爲每個Socket套接字單獨用創建一條線程運行。
- 如果一個客戶端已連接服務端,但某個時刻客戶端掛掉了,服務端不知道,這時候服務端會有計時器,每隔一段時間發送消息到已掛掉的客戶端判斷是否還在,達到一定次數客戶端未迴應後,服務端就會關閉這個已掛掉的套接字資源。
瀏覽器與Tomcat
瀏覽器訪問服務器基本原理
- 封裝了Socket的程序都是客戶端(如瀏覽器,不同廠商的瀏覽器)
- 封裝了ServerSocket的程序都爲服務器(如Tomcat)。
- html文件包含需要的資源信息會繼續發送請求(也就是並不是我們表面上看到請求一個html頁面時只發送一個請求)
- 不指定資源則會默認返回默認資源(index.html)
- 指定資源請求時可能還會附加請求參數(用戶名、密碼)
- 瀏覽器其實就是封裝了Socket,服務器封裝了ServerSocket
模擬服務器接收瀏覽器發送的HTTP協議請求消息
package com.bin.demo;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// 模擬服務器
ServerSocket ss = new ServerSocket(9090);
System.out.println("服務器啓動 ...");
// 獲取連接對象
Socket s = ss.accept();
System.out.println(s.getInetAddress().getHostAddress() + " : " + s.getPort() + " connection"); // 打印誰連接了服務器
// 讀取請求HTTP協議信息
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int length = in.read(buf);
String str = new String(buf);
System.out.println(str); // 打印請求的HTTP協議信息
// 關閉資源
s.close();
ss.close();
}
}
輸出:
服務器啓動 ...
192.168.0.108 : 51853 connection
GET / HTTP/1.1
Host: 192.168.0.108:9090
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
瀏覽器發送的HTTP協議請求信息圖解:
模擬瀏覽器接收服務器返回的HTTP協議響應消息
package com.bin.demo;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws Exception {
// 模擬瀏覽器
Socket s = new Socket("www.baidu.com", 80); // web資源的域名,一般默認爲80端口
// 發送請求
PrintWriter out = new PrintWriter(s.getOutputStream(), true); //使用打印流,第二個參數的自動刷新
out.println("GET / HTTP/1.1");
out.println("Host: " + s.getInetAddress().getHostAddress() +":80");
out.println("Connection: close");
out.println("Accept: */*"); //告訴服務器我支持的文件查看類型,這裏使用通配符告訴服務器我支持全部類型的文件
out.println("Accept-Language: zh");
out.println();
// 接收返回的HTTP響應協議信息
InputStream in = s.getInputStream();
byte[] buf = new byte[3024]; //這裏只讀取3024,自己調調也可以
int length = in.read(buf);
System.out.println(new String(buf, 0, length, "UTF-8")); //國際標準的UTF-8編碼解碼方式
// 關閉資源
s.close();
}
}
這裏只提取一部分HTTP響應內容:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
Content-Type: text/html
Date: Sun, 05 Apr 2020 17:07:16 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Pragma: no-cache
Server: BWS/1.1
Set-Cookie: BAIDUID=F42521B806B3D4A92243E42336518FED:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=F42521B806B3D4A92243E42336518FED; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: PSTM=1586106436; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BAIDUID=F42521B806B3D4A9493E1FA4E62E6E4D:FG=1; max-age=31536000; expires=Mon, 05-Apr-21 17:07:16 GMT; domain=.baidu.com; path=/; version=1; comment=bd
Traceid: 1586106436030523265010215562608745898825
Vary: Accept-Encoding
X-Ua-Compatible: IE=Edge,chrome=1
Connection: close
<!DOCTYPE html><!--STATUS OK-->
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<link rel="dns-prefetch" href="//s1.bdstatic.com"/>
<link rel="dns-prefetch" href="//t1.baidu.com"/>
<link rel="dns-prefetch" href="//t2.baidu.com"/>
<link rel="dns-prefetch" href="//t3.baidu.com"/>
<link rel="dns-prefetch" href="//t10.baidu.com"/>
<link rel="dns-prefetch" href="//t11.baidu.com"/>
<link rel="dns-prefetch" href="//t12.baidu.com"/>
<link rel="dns-prefetch" href="//b1.bdstatic.com"/>
<title>百度一下,你就知道</title>
<link href="http://s1.bdstatic.com/r/www/cache/static/home/css/index.css" rel="stylesheet" type="text/css" />
<!--[if lte IE 8]><style index="index" >#content{height:480px\9}#m{top:260px\9}</style><![endif]-->
<!--[if IE 8]><style index="index" >#u1 a.mnav,#u1 a.mnav:visited{font-family:simsun}</style><![endif]-->
<script>var hashMatch = document.location.href.match(/#+(.*wd=[^&].+)/);if (hashMatch && hashMatch[0] && hashMatch[1]) {document.location.replace("http://"+location.host+"/s?"+hashMatch[1]);}var ns_c = function(){};</script>
<script>function h(obj){obj.style.behavior='url(#default#homepage)';var a = obj.setHomePage('//www.baidu.com/');}</script>
<noscript><meta http-equiv="refresh" content="0; url=/baidu.html?from=noscript"/></noscript>
<script>window._ASYNC_START=new Date().getTime();</script>
</head>
簡單的圖解:
資源定位類
URL類
URL鏈接地址字符串的解析對象
基本的方法:
- getProtocol():獲取協議
- getHost():獲取IP地址
- getPort():獲取端口
- getFile():獲取資源路徑及請求參數
- getQuery():只獲取參數
- URLConnection openConnection():創建返回一個URLConnection連接對象,調用此方法將會進行連接,它代表應用程序和 URL 之間的通信鏈接。此類的實例可用於讀取和寫入此 URL 引用的資源(重要方法)
package com.bin.demo;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception {
//創建URL對象
URL url = new URL("http://192.168.0.108:9090/myapp/1.html?user=bin&password=520"); //請求這個頁面時攜帶數據參數
//基本方法,解析URL中的數據
System.out.println("getProtocol : " + url.getProtocol()); //獲取協議
System.out.println("getHost : " + url.getHost());
System.out.println("getPort : " + url.getPort());
System.out.println("getFile : " + url.getFile()); //獲取資源路徑及請求參數
System.out.println("getQuery : " + url.getQuery()); //獲取參數
}
}
輸出:
getProtocol : http
getHost : 192.168.0.108
getPort : 9090
getFile : /myapp/1.html?user=bin&password=520
getQuery : user=bin&password=520
URLConnection類
此類封裝了Socket,也就是能和服務器進行讀寫操作的類。
它代表應用程序和 URL 之間的通信鏈接。此類的實例可用於讀取和寫入此 URL 引用的資源。
此類是抽象類,通過URL對象創建URLConnection連接對象
基礎常用方法:
- OutputStream getOutputStream():寫出數據到此資源定位路徑。
- InputStream getInputStream():讀取此定位資源。
- URL getURL():返回URL對象。
package com.bin.demo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
public class Main {
public static void main(String[] args) throws Exception {
//創建URL對象
URL url = new URL("http://www.baidu.com");
//獲得URL連接對象
URLConnection conn = url.openConnection();
//讀取此定位資源
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String str = null;
while ((str = in.readLine()) != null) {
System.out.println(str);
}
}
}
輸出(這裏是被解析後的應答體數據):
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新聞</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地圖</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>視頻</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>貼吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登錄</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登錄</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多產品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>關於百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必讀</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意見反饋</a> 京ICP證030173號 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
注意:
返回的數據已經幫我們解析好了,讀取服務器返回的HTTP協議消息應答頭已被解析,甚至連應答體中包含的資源信息也被解析了,所以給程序員返回的只有數據內容。要想拿到應答頭調用相應方法即可。
URLConnection 下載圖片實戰
基礎常用方法:
- setRequestProperty(String key, String value):設置一般請求屬性。如果已存在具有該關鍵字的屬性,則用新值改寫其值。設置的一般請求屬性取決於協議。
常用方法(連接成功(URL調用openConnection()方法)後可調用的方法):
- OutputStream getOutputStream():寫出數據到此資源定位路徑。
- InputStream getInputStream():讀取此定位資源。
- String getContentEncoding():獲取數據的壓縮方式,如gzip、deflate、compress
- String getContentType():獲取內容數據類型,如文件的類型。
- int getContentLength():獲取響應體內容數據的長度,爲int類型,單位字節byte。
- long getContentLengthLong():同getContentLength()方法,這裏返回long類型。
- long getLastModifed():獲取此資源的上一次修改時間,單位爲毫秒值。
- String getHeaderField(String key):獲取返回的響應頭信息。
package com.bin.demo;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) throws Exception {
//創建URL對象
URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1586181338511&di=0ef74d62555781d08e5a0968b397c75e&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2F83025aafa40f4bfb1209aa1d0b4f78f0f6361899.jpg");
//獲得URL連接對象
URLConnection conn = url.openConnection();
//獲取參數
String http = conn.getHeaderField(null); // 獲取第一行應答頭信息,也就是協議頭
String encoding = conn.getContentEncoding();
String type = conn.getContentType();
int length = conn.getContentLength();
long lengthLong = conn.getContentLengthLong();
long motifyTime = conn.getLastModified();
System.out.println("協議頭 :" + http);
System.out.println("數據壓縮方式 :" + encoding);
System.out.println("類型 :" + type);
System.out.println("int字節長度 :" + length);
System.out.println("long自己長度 :" + lengthLong);
System.out.println("上一次修改時間 :" + new SimpleDateFormat("yyyy年MM/dd日 k:m:s:S").format(new Date(motifyTime)));
System.out.println("——————————————————");
//確定存放路徑
FileOutputStream out = new FileOutputStream("F:\\LeiMu.jpg");
//讀取此定位資源
BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
byte[] buf = new byte[1024]; //每次讀取的字節數
int len = 0;
long count = 0; //判斷文件是否下載成功的統計字節數(根據真實內容的字節大小 對比 已下載的內容字節大小)
while ((len = in.read(buf)) != -1) {
count += len;
out.write(buf, 0, len);
}
//關閉下載Copy資源
out.close();
// 判斷是否下載完成,(數據是否完整)
if (count == lengthLong) {
System.out.println("下載成功");
}
}
}
輸出:
協議頭 :HTTP/1.1 200 OK
數據壓縮方式 :null
類型 :image/jpeg
int字節長度 :44160
long自己長度 :44160
上一次修改時間 :2016年08/14日 19:51:29:0
——————————————————
下載成功
下面是一些常見的Content-Type
字段的值:
- text/plain
- text/html
- text/css
- image/jpeg
- image/png
- image/svg+xml
- audio/mp4
- video/mp4
- application/javascript
- application/pdf
- application/zip
- application/atom+xml
HttpURLConnection類
此類是抽象類並繼承自URLConnection類,通養通過URL的openConnection()獲取 URLConnection對象並強轉爲HttpURLConnection對象。
此類支持 HTTP 特定功能的 URLConnection。
常見基礎方法:
setRequestMethod(String method):設置 URL 請求的方法,以上方法之一是合法的,具體取決於協議的限制。默認方法爲 GET。
- GET
- POST
- HEAD
- OPTIONS
- PUT
- DELETE
- TRACE
int getResponseCode():獲取返回的狀態碼。
String getRequestMethod():獲取當前請求的方法。
String getRequestMessage():獲取返回的狀態碼描述信息。
disconnect() : 關閉連接。
靜態常量狀態碼,還有很多沒截圖完:
package com.bin.demo;
import java.net.HttpURLConnection;
import java.net.URL;
public class Main {
public static void main(String[] args) throws Exception {
//創建URL對象
URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1586181338511&di=0ef74d62555781d08e5a0968b397c75e&imgtype=0&src=http%3A%2F%2Fb.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2F83025aafa40f4bfb1209aa1d0b4f78f0f6361899.jpg");
//獲得URL連接對象
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
String method = conn.getRequestMethod(); //請求方法
int code = conn.getResponseCode(); //響應狀態碼
String codeInfo = conn.getResponseMessage(); //狀態碼描述信息
System.out.println(method); //GET
System.out.println(code); //200
System.out.println(codeInfo); //OK
conn.disconnect(); //關閉連接
}
}
輸出:
GET
200
OK
作用
Socket和ServerSocket:
- 能拿到請求與應答(頭與體)消息的全部消息,但並沒有進一步的資源信息解析
- Socket接收ServerSocket返回的應答體也就是源碼
URLConnection對象(返回的數據已經幫我們解析好了):
- 讀取服務器返回的HTTP協議消息應答頭已被解析,甚至連應答體中包含的資源信息也被解析了,所以給程序員返回的只有數據內容。
- 想要得到應答頭內容,只需要調用相應API方法。
HttpURLConnection:
- 看名字就知道是基於HTTP的,加入了響應碼,已經自帶的方法。
網絡架構
C/S
Client Server (客戶端服務器)
- 客戶端和服務端都需要編寫
- 客戶端需要維護
- 客戶端可以分擔運算
如大型運算的網絡遊戲,3D建模,技能特效。
B/S
Browser Server (瀏覽器服務器)
- 只需要編寫服務端,(客戶端就是瀏覽器)
- 客戶端是不必關心的,不需要維護的
- 運算全在服務器端
HTTP
HTTP 是一個在計算機世界裏專門在兩點之間傳輸文字、圖片、音頻、視頻等超文本數據的約定和規範
HTTP響應模型
模型 | 簡介作用 |
單進程I/O模型 | 服務端開啓一個進程,一個進程僅能處理一個請求,並且對請求順序處理 |
多進程I/O模型 | 服務端並行開啓多個進程,同樣的一個進程只能處理一個請求,這樣服務端就可以同時處理多個請求 |
複用I/O模型 | 服務端開啓一個進程,但是呢,同時開啓多個線程,一個線程響應一個請求,同樣可以達到同時處理多個請求,線程間併發執行 |
複用多線程I/O模型 | 服務端並行開啓多個進程,同時每個進程開啓多個線程,這樣服務端可以同時處理進程數M*每個進程的線程數N個請求。 |
協議版本
協議版本 | 簡介作用 |
HTTP/0.9 | HTTP協議的最初版本,功能簡陋,僅支持請求方式GET,並且僅能請求訪問HTML格式的資源。 |
HTTP/1.0 |
在0.9版本上做了進步,增加了請求方式POST和HEAD;不再侷限於0.9版本的HTML格式,根據Content-Type可以支持多種數據格式。 但是1.0版本的工作方式是每次TCP連接只能發送一個請求,當服務器響應後就會關閉這次連接,下一個請求需要再次建立TCP連接,就是不支持Connection: keep-alive |
HTTP/1.1 |
1.1 版的最大變化,就是引入了持久連接(persistent connection),即TCP連接默認不關閉,可以被多個請求複用,不用聲明 客戶端和服務器發現對方一段時間沒有活動,就可以主動關閉連接。不過,規範的做法是,客戶端在最後一個請求時,發送 |
HTTP/2.0 |
爲了解決1.1版本利用率不高的問題,提出了HTTP/2.0版本。增加雙工模式,即不僅客戶端能夠同時發送多個請求,服務端也能同時處理多個請求,解決了隊頭堵塞的問題(HTTP2.0使用了多路複用的技術,做到同一個連接併發處理多個請求,而且併發請求的數量比HTTP1.1大了好幾個數量級)並以壓縮的方式傳輸,提高利用率。 當前主流的協議版本還是HTTP/1.1版本。 |
HTTP/1.0與HTTP.1.1比較圖:
通用協議標頭
可以出現在請求標頭和響應標頭中。
字段 | 作用 |
Date | 返回的值爲距離格林威治標準時間 1970 年 1 月 1 日的毫秒數。 |
Cache-Control |
有四個參數:
|
Connection |
Connection 決定當前事務(一次三次握手和四次揮手)完成後,是否會關閉網絡連接。Connection 有兩種,一種是
|
HTTP1.1 其他通用標頭如下:
實體協議標頭
實體標頭是描述消息正文內容的 HTTP 標頭。實體標頭用於 HTTP 請求和響應中。
字段 | 作用 |
Content-Length | 體報頭指示實體主體的大小,以字節爲單位,發送到接收方。 |
Content-Language | 實體報頭描述了客戶端或者服務端能夠接受的語言 |
Content-Encoding |
這個實體報頭用來壓縮媒體類型。Content-Encoding 指示對實體應用了何種編碼。
|
實體標頭字段圖:
請求協議標頭
字段 | 作用 | ||||||||||
Host | Host 請求頭指明瞭服務器的域名(對於虛擬主機來說),以及(可選的)服務器監聽的TCP端口號。如果沒有給定端口號,會自動使用被請求服務的默認端口(比如請求一個 HTTP 的 URL 會自動使用80作爲端口)。 | ||||||||||
Referer | HTTP Referer 屬性是請求標頭的一部分,當瀏覽器向 web 服務器發送請求的時候,一般會帶上 Referer,告訴服務器該網頁是從哪個頁面鏈接過來的,服務器因此可以獲得一些信息用於處理。 | ||||||||||
Upgrade-Insecure-Requests | Upgrade-Insecure-Requests 是一個請求標頭,用來向服務器端發送信號,表示客戶端優先選擇加密及帶有身份驗證的響應。如:Upgrade-Insecure-Requests: 1 | ||||||||||
If-Modified-Since |
HTTP 的 If-Modified-Since 使其成爲條件請求:
If-Modified-Since 通常會與 If-None-Match 搭配使用,If-Modified-Since 用於確認代理或客戶端擁有的本地資源的有效性。獲取資源的更新日期時間,可通過確認首部字段 Last-Modified 來確定。
大白話說就是如果在 Last-Modified 之後更新了服務器資源,那麼服務器會響應200,如果在 Last-Modified 之後沒有更新過資源,則返回 304。 |
||||||||||
If-None-Match | If-None-Match HTTP請求標頭使請求成爲條件請求。 對於 GET 和 HEAD 方法,僅當服務器沒有與給定資源匹配的 ETag 時,服務器纔會以200狀態發送回請求的資源。 對於其他方法,僅當最終現有資源的ETag 與列出的任何值都不匹配時,纔會處理請求。 |
||||||||||
Accept |
告知服務端,客戶端都安裝了那些媒體軟件,可接收那種數據類型 文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml 圖片文件: image/jpeg、image/gif、image/png 視頻文件: video/mpeg、video/quicktime 應用程序二進制文件: application/octet-stream、application/zip 如:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 q 表示的是權重,媒體類型增加優先級,沒有顯示權重的時候默認值是1.0 :
這是一個放置順序,權重高的在前,低的在後 |
||||||||||
Accept-Charset |
規定服務器處理表單數據所使用的字符集。 常用的字符集有: UTF-8 - Unicode 字符編碼 ;
|
||||||||||
Accept-Language | 告知服務器用戶代理能夠處理的自然語言 |
基於 HTTP 1.1:
響應協議標頭
字段 | 作用 |
Access-Control-Allow-Origin | 一個返回的 HTTP 標頭可能會具有 Access-Control-Allow-Origin ,Access-Control-Allow-Origin 指定一個來源,它告訴瀏覽器允許該來源進行資源訪問。 否則-對於沒有憑據的請求 *通配符,告訴瀏覽器允許任何源訪問資源。例如,要允許源 https://mozilla.org 的代碼訪問資源 |
Keep-Alive |
Keep-Alive 表示的是 Connection 非持續連接的存活時間,如下: Connection: Keep-Alive Keep-Alive: timeout=5, max=997 有兩個參數:
|
Server | 服務器使用的軟件的信息。 |
Set-Cookie | 服務器返回的Cookie認證標識 |
Transfer-Encoding |
首部字段 Transfer-Encoding 規定了傳輸報文主體時採用的編碼方式。 |
X-Frame-Options | 首部字段 X-Frame-Options 屬於 HTTP 響應首部,用於控制網站內容在其他 Web 網站的 Frame 標籤內的顯示問題。其主要目的是爲了防止點擊劫持(clickjacking)攻擊。 |
基於 HTTP 1.1:
狀態碼
狀態碼類別:
類別 | 原因短語 |
1XX | Informational(信息性狀態碼) 接受的請求正在處理 |
2XX | Success(成功狀態碼) 請求正常處理完畢 |
3XX | Redirection(重定向狀態碼) 需要進行附加操作以完成請求 |
4XX | Client Error(客戶端錯誤狀態碼) 服務器無法處理請求 |
5XX | Server Error(服務器錯誤狀態碼) 服務器處理請求出錯 |
常用HTTP狀態碼:
2XX | 成功(這系列表明請求被正常處理了) |
200 | OK,表示從客戶端發來的請求在服務器端被正確處理 |
204 | No content,表示請求成功,但響應報文不含實體的主體部分 |
206 | Partial Content,進行範圍請求成功 |
3XX | 重定向(表明瀏覽器要執行特殊處理) |
301 | moved permanently,永久性重定向,表示資源已被分配了新的 URL |
302 | found,臨時性重定向,表示資源臨時被分配了新的 URL |
303 | see other,表示資源存在着另一個 URL,應使用 GET 方法獲取資源(對於301/302/303響應,幾乎所有瀏覽器都會刪除報文主體並自動用GET重新請求) |
304 | not modified,表示服務器允許訪問資源,但請求未滿足條件的情況(與重定向無關) |
307 | temporary redirect,臨時重定向,和302含義類似,但是期望客戶端保持請求方法不變向新的地址發出請求 |
4XX | 客戶端錯誤 |
400 | bad request,請求報文存在語法錯誤 |
401 | unauthorized,表示發送的請求需要有通過 HTTP 認證的認證信息 |
403 | forbidden,表示對請求資源的訪問被服務器拒絕,可在實體主體部分返回原因描述 |
404 | not found,表示在服務器上沒有找到請求的資源 |
5XX | 服務器錯誤 |
500 | internal sever error,表示服務器端在執行請求時發生了錯誤 |
501 | Not Implemented,表示服務器不支持當前請求所需要的某個功能 |
503 | service unavailable,表明服務器暫時處於超負載或正在停機維護,無法處理請求 |
常用請求方法
HTTP協議中定義了瀏覽器和服務器進行交互的不同方法,基本方法有4種,分別是GET,POST,PUT,DELETE。這四種方法可以理解爲,對服務器資源的查,改,增,刪。
- GET:從服務器上獲取數據,也就是所謂的查,僅僅是獲取服務器資源,不進行修改。
- POST:向服務器提交數據,這就涉及到了數據的更新,也就是更改服務器的數據。
- PUT:英文含義是放置,也就是向服務器新添加數據,就是所謂的增。
- DELETE:從字面意思也能看出,這種方式就是刪除服務器數據的過程。
HTTP與HTTPS的區別
區別 | HTTP | HTTPS |
協議 | 運行在 TCP 之上,明文傳輸,客戶端與服務器端都無法驗證對方的身份 | 身披 SSL( Secure Socket Layer )外殼的 HTTP,運行於 SSL 上,SSL 運行於 TCP 之上, 是添加了加密和認證機制的 HTTP。 |
端口 | 80 | 443 |
資源消耗 | 較少 | 由於加解密處理,會消耗更多的 CPU 和內存資源 |
開銷 | 無需證書 | 需要證書,而證書一般需要向認證機構購買 |
加密機制 | 無 | 共享密鑰加密和公開密鑰加密並用的混合加密機制 |
安全性 | 弱 | 由於加密機制,安全性強 |
Session、Cookie和Token
HTTP協議本身是無狀態的。什麼是無狀態呢,即服務器無法判斷用戶身份。
什麼是cookie:
cookie是由Web服務器保存在用戶瀏覽器上的小文件(key-value格式),包含用戶相關的信息。客戶端向服務器發起請求,如果服務器需要記錄該用戶狀態,就使用response向客戶端瀏覽器頒發一個Cookie。客戶端瀏覽器會把Cookie保存起來。當瀏覽器再請求該網站時,瀏覽器把請求的網址連同該Cookie一同提交給服務器。服務器檢查該Cookie,以此來辨認用戶身份。
什麼是session:
session是依賴Cookie實現的。session是服務器端對象
session 是瀏覽器和服務器會話過程中,服務器分配的一塊儲存空間。服務器默認爲瀏覽器在cookie中設置 sessionid,瀏覽器在向服務器請求過程中傳輸 cookie 包含 sessionid ,服務器根據 sessionid 獲取出會話中存儲的信息,然後確定會話的身份信息。
cookie與session區別:
- 存儲位置與安全性:cookie數據存放在客戶端上,安全性較差,session數據放在服務器上,安全性相對更高;
- 存儲空間:單個cookie保存的數據不能超過4K,很多瀏覽器都限制一個站點最多保存20個cookie,session無此限制
- 佔用服務器資源:session一定時間內保存在服務器上,當訪問增多,佔用服務器性能,考慮到服務器性能方面,應當使用cookie。
什麼是Token:
Token的引入:Token是在客戶端頻繁向服務端請求數據,服務端頻繁的去數據庫查詢用戶名和密碼並進行對比,判斷用戶名和密碼正確與否,並作出相應提示,在這樣的背景下,Token便應運而生。
Token的定義:Token是服務端生成的一串字符串,以作客戶端進行請求的一個令牌,當第一次登錄後,服務器生成一個Token便將此Token返回給客戶端,以後客戶端只需帶上這個Token前來請求數據即可,無需再次帶上用戶名和密碼。
使用Token的目的:Token的目的是爲了減輕服務器的壓力,減少頻繁的查詢數據庫,使服務器更加健壯。
Token 是在服務端產生的。如果前端使用用戶名/密碼向服務端請求認證,服務端認證成功,那麼在服務端會返回 Token 給前端。前端可以在每次請求的時候帶上 Token 證明自己的合法地位。
session與token區別:
- session機制存在服務器壓力增大,CSRF跨站僞造請求攻擊,擴展性不強等問題;
- session存儲在服務器端,token存儲在客戶端
- token提供認證和授權功能,作爲身份認證,token安全性比session好;
- session這種會話存儲方式方式只適用於客戶端代碼和服務端代碼運行在同一臺服務器上,token適用於項目級的前後端分離(前後端代碼運行在不同的服務器下)
深入學習參考:重學TCP/IP協議和三次握手四次揮手