Web的工作原理可以分為以下幾個步驟:
建立連線:Web瀏覽器與Web伺服器之間建立TCP/IP連線,以便傳輸資料。
傳送HTTP請求:Web瀏覽器向Web伺服器傳送HTTP請求,請求所需的Web資源。
接收HTTP響應:Web伺服器接收HTTP請求,並通過HTTP響應將Web資源傳送給Web瀏覽器。
渲染Web頁面:Web瀏覽器接收HTTP響應,並根據所收到的資料渲染Web頁面,以便呈現給使用者。
關閉連線:Web瀏覽器和Web伺服器之間的TCP/IP連線被關閉。
HTTP(超文字傳輸協定)是一種用於 Web 伺服器和使用者端(瀏覽器)之間通訊以通過 Internet 傳輸資料的協定。 HTTP 請求是使用者端向伺服器傳送的請求特定資源(如網頁、影象或視訊)的訊息。 HTTP 請求方法決定了請求的型別,例如 GET、POST、PUT、DELETE、HEAD、OPTIONS、CONNECT、TRACE 等。
HTTP 請求由請求行、檔頭和可選的訊息正文組成。
請求行包括 HTTP 方法、所請求資源的 URL(統一資源定位符)以及所使用的 HTTP 版本。
檔頭提供有關請求的其他資訊,例如使用者代理、請求的內容型別和可接受的編碼格式。
訊息正文是可選的,包含資料,例如表單資料或 JSON 。
HTTP 定義了各種請求方法,指示要對 URL 標識的資源執行的操作。最常見的 HTTP 請求方法是:
GET:從伺服器獲取資源,例如網頁或影象。
POST:將資料提交給伺服器進行處理,例如表單提交或檔案上傳。
PUT:用新資料更新伺服器上的現有資源。
DELETE:從伺服器中刪除資源。
HEAD:檢索資源的檔頭,不帶訊息正文。
OPTIONS:用於獲取當前URL所支援的方法。若請求成功,則它會在HTTP頭中包含一個名為「Allow」的頭,值是所支援的方法,如「GET, POST」。
CONNECT:建立到資源的網路連線,例如代理伺服器。
TRACE:回顯接收到的請求訊息,用於偵錯目的。 HTTP 檔頭:
HTTP 檔頭用於提供有關 HTTP 請求或響應的附加資訊。它們是由冒號分隔的鍵值對,包含在請求或響應訊息中。 Header有多種型別,例如通用Header、請求Header、響應Header和實體Header。一些常見的檔頭包括:
User-Agent:向存取網站提供你所使用的瀏覽器型別、作業系統及版本、CPU 型別、瀏覽器渲染引擎、瀏覽器語言、瀏覽器外掛等資訊的標識
Accept:指定使用者端接受的內容型別。
Content-Type:指定請求或響應訊息中內容的型別。
Content-Length:指定訊息體的長度,以位元組為單位。
Cache-Control:指定響應的快取指令,例如 max-age 和 must-revalidate。
Authorization:指定請求的身份驗證憑據。 HTTP 訊息體body:
HTTP 訊息正文是 HTTP 請求或響應的可選部分,包含各種格式的資料,例如 HTML、JSON、XML 或二進位制資料。在請求中,訊息正文包含要傳送到伺服器的資料,例如表單資料或檔案上傳。在響應中,訊息正文包含所請求資源的實際內容,例如 HTML 頁面或影象。
總之,瞭解 http 請求和響應的基礎知識對於 Web 開發以及 Web 伺服器和使用者端之間的通訊至關重要。通過了解如何構建 HTTP 請求、各種請求方法、檔頭的使用以及可選的訊息正文,開發人員可以有效地與伺服器通訊並建立健壯的 Web 應用程式。
HTTP響應是由伺服器向客戶傳送的對請求的響應。 HTTP 響應包含以下元件:
HTTP版本
狀態碼
狀態訊息
響應頭
響應體
響應中使用的 HTTP 版本在響應的第一行中指定。例如:
HTTP/1.1 200 OK
這指定使用 HTTP 版本 1.1 傳送響應。
狀態程式碼錶示所請求操作的結果。 HTTP 定義了五類狀態碼:
資訊提示 (100-199)
成功 (200-299)
重定向 (300-399)
使用者端錯誤 (400-499)
伺服器錯誤 (500-599) 每個狀態碼都是一個三位數位,它包含在HTTP版本之後的響應的第一行中。例如:
HTTP/1.1 200 OK
這表明操作成功,請求的資源包含在響應正文中。
狀態訊息是對狀態程式碼的描述。它包含在狀態程式碼之後的響應的第一行中。例如:
HTTP/1.1 404 Not Found
這表示未找到請求的資源,狀態訊息提供了問題的簡短描述。
響應檔頭包含有關響應的後設資料。它們類似於請求檔頭,但提供有關響應而不是請求的資訊。響應檔頭可以包括有關伺服器、快取策略、cookie 等的資訊。
響應檔頭包含在第一行之後的響應中。每個檔頭都是一個由冒號分隔的鍵值對。例如:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1274
Server: Apache
在此範例中,響應包含三個檔頭:Content-Type、Content-Length 和 Server。
響應正文包含請求的資源或錯誤訊息,如果找不到請求的資源。響應正文的格式取決於響應中包含的 Content-Type 檔頭。例如,如果 Content-Type 檔頭設定為「text/html」,則響應正文應包含 HTML 程式碼。
響應主體包含在響應檔頭之後的響應中。如果響應主體很大,它可能會被拆分成多個封包。
HTTP 請求和響應有幾個開發人員需要注意的技術點。這些技術點決定了請求和響應如何在使用者端和伺服器之間構建和傳輸。
HTTP 請求和響應包含提供有關請求或響應的附加資訊的檔頭。檔頭是在訊息檔頭中傳送的鍵值對,它們用於傳達有關請求或響應的後設資料。 Headers 有多種型別,包括通用 Headers、請求 Headers、響應 Headers 和實體 Headers。
通用檔頭適用於請求和響應,並提供有關整個訊息的資訊,例如訊息格式、傳送日期和時間以及訊息是否可以快取。
請求檔頭用於提供有關發出請求的使用者端的資訊,例如使用者代理(用於存取伺服器的軟體)、接受的內容型別以及用於壓縮訊息正文的編碼。
響應頭提供了伺服器對使用者端請求的響應資訊,如響應的內容型別、訊息體的長度、響應是否可以快取等。
實體檔頭用於提供有關訊息正文的資訊,例如內容長度和內容編碼。
HTTP 請求使用一組方法來指定要對 URL 中標識的資源執行的所需操作。最常見的 HTTP 方法是 GET、POST、PUT、DELETE 和 HEAD。
GET:用於從伺服器檢索資料。
POST:用於向伺服器傳送資料以建立或更新資源。
PUT:用於更新伺服器上的現有資源。
DELETE:用於從伺服器中刪除資源。
HEAD:用於檢索資源的檔頭,而不是訊息正文。 HTTP 狀態碼 HTTP 響應包括指示請求狀態的狀態程式碼。有五類狀態程式碼,每類都有自己的一組程式碼:
• 1xx: 資訊 - 表示伺服器已收到請求並正在繼續處理它。 • 2xx: 成功——表示請求被成功接收、理解和接受。 • 3xx: 重定向——表示使用者端需要採取進一步的行動來完成請求。 • 4xx: 使用者端錯誤 - 表示請求包含錯誤的語法或無法實現。 • 5xx: 伺服器錯誤 - 表示伺服器未能滿足有效請求。 最常見的狀態程式碼是 200 OK(請求成功)、404 Not Found(未找到請求的資源)和 500 Internal Server Error(伺服器在處理請求時遇到錯誤)。
HTTP 請求和響應還可以包括 cookie,它們是儲存在使用者端計算機上的小文字檔案。 Cookie 用於儲存有關使用者端偏好或之前與伺服器互動的資訊。伺服器可以使用 cookie 來識別使用者端並提供客製化的內容。
HTTP 請求和響應還可以被快取,這意味著使用者端或中間伺服器可以儲存響應的副本以備將來使用。快取有助於減少網路流量並提高效能,但如果快取的內容過時或陳舊,也會導致問題。
作者部落格:
yhttps://blog.51cto.com/sdwml/6104070
請求(HttpServletRequest):獲取請求資料
在瀏覽器地址輸入地址,點選回車請求伺服器,這個過程就是一個請求過程。
在原始的Web程式當中,需要通過Servlet中提供的API:HttpServletRequest(請求物件),獲取請求的相關資訊。比如獲取請求引數:
Tomcat接受到Http請求時:把請求的相關資訊封裝到HttpServletRequest物件中。
在Controller中,我們要想獲取Request物件,可以直接在方法的形參中宣告HttpServletRequest物件。然後就可以通過該物件來獲取請求資訊:
//根據指定的引數名獲取請求引數的資料值
String request.getParameter("引數名")
@RestController
public class RequestController {
//原始方式
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// http://localhost:8080/simpleParam?name=Tom&age=10
// 請求引數: name=Tom&age=10 (有2個請求引數)
// 第1個請求引數: name=Tom 引數名:name,引數值:Tom
// 第2個請求引數: age=10 引數名:age , 引數值:10
String name = request.getParameter("name");//name就是請求引數名
String ageStr = request.getParameter("age");//age就是請求引數名
int age = Integer.parseInt(ageStr);//需要手動進行型別轉換
System.out.println(name+" : "+age);
return "OK";
}
}
在Springboot的環境中,對原始的API進行了封裝,接收引數的形式更加簡單。 如果是簡單引數,引數名與形參變數名相同,定義同名的形參即可接收引數。
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=10
// 第1個請求引數: name=Tom 引數名:name,引數值:Tom
// 第2個請求引數: age=10 引數名:age , 引數值:10
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){//形參名和請求引數名保持一致
System.out.println(name+" : "+age);
return "OK";
}
}
結論:不論是GET請求還是POST請求,對於簡單引數來講,只要保證==請求引數名和Controller方法中的形參名保持一致==,就可以獲取到請求引數中的資料值。
如果方法形參名稱與請求引數名稱不一致,controller方法中的形參還能接收到請求引數值嗎?
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 請求引數名:name
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String username , Integer age ){//請求引數名和形參名不相同
System.out.println(username+" : "+age);
return "OK";
}
}
答案:執行沒有報錯。 controller方法中的username值為:null,age值為20
結論:對於簡單引數來講,請求引數名和controller方法中的形參名不一致時,無法接收到請求資料
那麼如果我們開發中,遇到了這種請求引數名和controller方法中的形參名不相同,怎麼辦?
解決方案:可以使用Spring提供的@RequestParam註解完成對映
在方法形參前面加上 @RequestParam 然後通過value屬性執行請求引數名,從而完成對映。程式碼如下:
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 請求引數名:name
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam("name") String username , Integer age ){
System.out.println(username+" : "+age);
return "OK";
}
}
注意事項:
@RequestParam中的required屬性預設為true(預設值也是true),代表該請求引數必須傳遞,如果不傳遞將報錯
如果該引數是可選的,可以將required屬性設定為false
@RequestMapping("/simpleParam") public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){ System.out.println(username+ ":" + age); return "OK"; }
在使用簡單引數做為資料傳遞方式時,前端傳遞了多少個請求引數,後端controller方法中的形參就要書寫多少個。如果請求引數比較多,通過上述的方式一個引數一個引數的接收,會比較繁瑣。
此時,我們可以考慮將請求引數封裝到一個實體類物件中。 要想完成資料封裝,需要遵守如下規則:請求引數名與實體類的屬性名相同
定義POJO實體類:
public class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Controller方法
@RestController
public class RequestController {
//實體引數:簡單實體物件
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
}
上面我們講的呢是簡單的實體物件,下面我們在來學習下複雜的實體物件。
複雜實體物件指的是,在實體類中有一個或多個屬性,也是實體物件型別的。如下:
User類中有一個Address型別的屬性(Address是一個實體類)
複雜實體物件的封裝,需要遵守如下規則:
請求引數名與形參物件屬性名相同,按照物件層次結構關係即可接收巢狀實體類屬性引數。
定義POJO實體類:
Address實體類
public class Address {
private String province;
private String city;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
User實體類
public class User {
private String name;
private Integer age;
private Address address; //地址物件
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
Controller方法:
@RestController
public class RequestController {
//實體引數:複雜實體物件
@RequestMapping("/complexPojo")
public String complexPojo(User user){
System.out.println(user);
return "OK";
}
}
Postman測試:
陣列集合引數的使用場景:在HTML的表單中,有一個表單項是支援多選的(核取方塊),可以提交選擇的多個值。
後端程式接收上述多個值的方式有兩種:
陣列
集合
陣列引數:請求引數名與形引陣列名稱相同且請求引數為多個,定義陣列型別形參即可接收引數
Controller方法:
@RestController
public class RequestController {
//陣列集合引數
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby));
return "OK";
}
}
Postman測試:
在前端請求時,有兩種傳遞形式:
方式一:http://localhost:8080/arrayParam?hobby=game&hobby=java
方式二:http://localhost:8080/arrayParam?hobby=game,java
集合引數:請求引數名與形參集合物件名相同且請求引數為多個,@RequestParam 繫結引數關係
預設情況下,請求中引數名相同的多個值,是封裝到陣列。如果要封裝到集合,要使用@RequestParam繫結引數關係
Controller方法:
@RestController
public class RequestController {
//陣列集合引數
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby){
System.out.println(hobby);
return "OK";
}
}
Postman測試:
方式一:http://localhost:8080/listParam?hobby=game&hobby=java
方式二:http://localhost:8080/listParam?hobby=game,java
上述演示的都是一些普通的引數,在一些特殊的需求中,可能會涉及到日期型別資料的封裝。比如,如下需求:
因為日期的格式多種多樣(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那麼對於日期型別的引數在進行封裝的時候,需要通過@DateTimeFormat註解,以及其pattern屬性來設定日期的格式。
@DateTimeFormat註解的pattern屬性中指定了哪種日期格式,前端的日期引數就必須按照指定的格式傳遞。
後端controller方法中,需要使用Date型別或LocalDateTime型別,來封裝傳遞的引數。
Controller方法:
@RestController
public class RequestController {
//日期時間引數
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime);
return "OK";
}
}
如果是比較複雜的引數,前後端通過會使用JSON格式的資料進行傳輸。 (JSON是開發中最常用的前後端資料互動方式)
我們學習JSON格式引數,主要從以下兩個方面著手:
Postman在傳送請求時,如何傳遞json格式的請求引數
在伺服器端的controller方法中,如何接收json格式的請求引數
Postman傳送JSON格式資料:
伺服器端Controller方法接收JSON格式資料:
傳遞json格式的引數,在Controller中會使用實體類進行封裝。
封裝規則:JSON資料鍵名與形參物件屬性名相同,定義POJO型別形參即可接收引數。需要使用 @RequestBody標識。
@RequestBody註解:將JSON資料對映到形參的實體類物件中(JSON中的key和實體類中的屬性名保持一致)
實體類:Address
public class Address {
private String province;
private String city;
//省略GET , SET 方法
}
實體類:User
public class User {
private String name;
private Integer age;
private Address address;
//省略GET , SET 方法
}
Controller方法:
@RestController
public class RequestController {
//JSON引數
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}
}
Postman測試:
傳統的開發中請求引數是放在請求體(POST請求)傳遞或跟在URL後面通過?key=value的形式傳遞(GET請求)。
在現在的開發中,經常還會直接在請求的URL中傳遞引數。例如:
http://localhost:8080/user/1
http://localhost:880/user/1/0
上述的這種傳遞請求引數的形式呢,我們稱之為:路徑引數。
學習路徑引數呢,主要掌握在後端的controller方法中,如何接收路徑引數。
路徑引數:
前端:通過請求URL直接傳遞引數
後端:使用{…}來標識該路徑引數,需要使用@PathVariable獲取路徑引數
Controller方法:
@RestController
public class RequestController {
//路徑引數
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
}
}
傳遞多個路徑引數:
Controller方法:
@RestController
public class RequestController {
//路徑引數
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){
System.out.println(id+ " : " +name);
return "OK";
}
}
響應(HttpServletReponse):設定響應資料
伺服器根據瀏覽器傳送的請求,放回資料到瀏覽器在網頁上進行顯示的過程就叫響應。
controller方法中的return的結果,怎麼就可以響應給瀏覽器呢?
答案:使用@ResponseBody註解
@ResponseBody註解:
型別:方法註解、類註解
位置:書寫在Controller方法上或類上
作用:將方法返回值直接響應給瀏覽器
如果返回值型別是實體物件/集合,將會轉換為JSON格式後在響應給瀏覽器
但是在我們所書寫的Controller中,只在類上新增了@RestController註解、方法新增了@RequestMapping註解,並沒有使用@ResponseBody註解,怎麼給瀏覽器響應呢?
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
}
原因:在類上新增的@RestController註解,是一個組合註解。
@RestController = @Controller + @ResponseBody
@RestController原始碼:
@Target({ElementType.TYPE}) //元註解(修飾註解的註解)
@Retention(RetentionPolicy.RUNTIME) //元註解
@Documented //元註解
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
結論:在類上新增@RestController就相當於新增了@ResponseBody註解。
類上有@RestController註解或@ResponseBody註解時:表示當前類下所有的方法返回值做為響應資料
方法的返回值,如果是一個POJO物件或集合時,會先轉換為JSON格式,在響應給瀏覽器
下面我們來測試下響應資料:
@RestController
public class ResponseController {
//響應字串
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
//響應實體物件
@RequestMapping("/getAddr")
public Address getAddr(){
Address addr = new Address();//建立實體類物件
addr.setProvince("廣東");
addr.setCity("深圳");
return addr;
}
//響應集合資料
@RequestMapping("/listAddr")
public List<Address> listAddr(){
List<Address> list = new ArrayList<>();//集合物件
Address addr = new Address();
addr.setProvince("廣東");
addr.setCity("深圳");
Address addr2 = new Address();
addr2.setProvince("陝西");
addr2.setCity("西安");
list.add(addr);
list.add(addr2);
return list;
}
}
在伺服器端響應了一個物件或者集合,那私前端獲取到的資料是什麼樣子的呢?我們使用postman傳送請求來測試下。測試效果如下:
大家有沒有發現一個問題,我們在前面所編寫的這些Controller方法中,返回值各種各樣,沒有任何的規範。
如果我們開發一個大型專案,專案中controller方法將成千上萬,使用上述方式將造成整個專案難以維護。那在真實的專案開發中是什麼樣子的呢?
在真實的專案開發中,無論是哪種方法,我們都會定義一個統一的返回結果。方案如下:
前端:只需要按照統一格式的返回結果進行解析(僅一種解析方案),就可以拿到資料。
統一的返回結果使用類來描述,在這個結果中包含:
響應狀態碼:當前請求是成功,還是失敗
狀態碼資訊:給頁面的提示資訊
返回的資料:給前端響應的資料(字串、物件、集合)
定義在一個實體類Result來包含以上資訊。程式碼如下:
public class Result {
private Integer code;//響應碼,1 代表成功; 0 代表失敗
private String msg; //響應碼 描述字串
private Object data; //返回的資料
public Result() { }
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
//增刪改 成功響應(不需要給前端返回資料)
public static Result success(){
return new Result(1,"success",null);
}
//查詢 成功響應(把查詢結果做為返回資料響應給前端)
public static Result success(Object data){
return new Result(1,"success",data);
}
//失敗響應
public static Result error(String msg){
return new Result(0,msg,null);
}
}
改造Controller:
@RestController
public class ResponseController {
//響應統一格式的結果
@RequestMapping("/hello")
public Result hello(){
System.out.println("Hello World ~");
//return new Result(1,"success","Hello World ~");
return Result.success("Hello World ~");
}
//響應統一格式的結果
@RequestMapping("/getAddr")
public Result getAddr(){
Address addr = new Address();
addr.setProvince("廣東");
addr.setCity("深圳");
return Result.success(addr);
}
//響應統一格式的結果
@RequestMapping("/listAddr")
public Result listAddr(){
List<Address> list = new ArrayList<>();
Address addr = new Address();
addr.setProvince("廣東");
addr.setCity("深圳");
Address addr2 = new Address();
addr2.setProvince("陝西");
addr2.setCity("西安");
list.add(addr);
list.add(addr2);
return Result.success(list);
}
}
使用Postman測試: