個人所有文章整理在此篇,將陸續更新收錄:知無涯,行者之路莫言終(我的程式設計之路)
零、前言
不管什麼語言,什麼系統,都離不開網路,這個系列就來深入一下網路
首先是挾天子以令諸侯
的Http,它把握了整個網路的命脈。所有人必須對它言聽計從。
本文主要聚焦
1.dns的定址(域名解析)
2.tcp/ip的三次握手,建立連線
3.客戶端請求和服務端響應的詳細分析
4.騰訊雲免費ssl證書獲取,以及基於springboot2設定https支援
複製程式碼
注:本文的服務端程式碼已放在Github,如果沒有基礎,可以參見我的SpringBoot入門級系列:
如果不想接觸後端,本文也可以看,不過理解方面多少會有些欠缺,
畢竟http是客戶端
和服務端
兩個人的事,撇看一者來看,是不現實的。
一、域名解析
chrome中輸入網址敲回車之後,瀏覽器會根據域名找到對應的伺服器地址
這裡以我的網站:http://www.toly1994.com/
為例
0.url簡述:統一資源定位符(Uniform Resource Locator)
在此之前先簡單說一下url
基本URL包含
模式(或稱協議)、伺服器名稱(或IP地址)、路徑和檔名,
協議://使用者名稱:密碼@子域名.域名.頂級域名:埠號/目錄/檔名.檔案字尾?引數=值#標誌。
複製程式碼
1.解析域名--Chrome搜尋自身DNS快取
也就是根據域名找到對應的ip地址:首先看瀏覽器自身有沒有快取:
chrome://net-internals/#dns
2.解析域名--檢視本機上的DNS快取
如果1沒有的話,檢視本機上的DNS快取,
ipconfig /displaydns
3.解析域名--檢視host檔案是否有對應的網址ip
如果2沒有的話,檢視host檔案是否有對應的網址ip,
C:\Windows\System32\drivers\etc
4.解析域名--外部查詢
前三步沒有查到,這說明本地無該網站的DNS快取,由寬頻運營商的伺服器進行查詢
如果無快取,會一級一級的去找,知道找到toly1994.com對應的伺服器(即我的伺服器),
最後將查到的伺服器ip地址返回給剛才敲網址的瀏覽器
二、客戶端與服務端建立TCP/IP
連線:
為了簡單些,使用
http://www.toly1994.com:8080/swords/21
客戶端在訪問時,第一步就是查詢域名所對的ip地址(即伺服器住哪)
0.TCP報文圖
網上的圖有點醜,這裡特意畫了一幅,對於TCP/IP會有專文總結,
這裡先認識兩個控制位:SYN和ACK
SYN:同步序列編號(Synchronize Sequence Numbers),在建立連線時使用
|-- 當SYN=1,ACK=0時,表示這是一個請求建立連線的報文段;
|-- 當SYN=1,ACK=1時,表示對方同意建立連線。
|-- SYN=1,說明這是一個請求建立連線或同意建立連線的報文。只有在前兩次握手中SYN才置為1。
ACK:確認序號標誌(Acknowledgment):前面的確認號欄位是否有效。
|-- 只有當ACK=1時,前面的確認號欄位才有效。為0表示報文中不含確認資訊,忽略確認號欄位。
|-- TCP規定,連線建立後,ACK必須為1。
複製程式碼
1.第一次握手:問一下伺服器在不在
客戶端傳送SYN=1,seq=J(J為隨機數字)的報文給伺服器
服務端看到SYN=1,知道客戶端要請求連線
2.第二次握手:伺服器說在
服務端傳送SYN=1,ACK=1,seq=K(K為隨機數字),ack=J+1的報文給客戶端
客戶端看到SYN=1,ack=J+1,便知道服務端給自己回話了
3.第三次握手:客戶端說我也還在
客戶端傳送ACK=1,ack=K+1的報文給伺服器
服務端看到ack=K+1,知道客戶端收到了剛才的話
這樣就建立了一個穩固的
TCP/IP
連線
三、傳送請求與接收響應及四次揮手
上面說到服務端和客戶端建立了連線,接下來就是
請求
與響應
了
在此之前先看一下chrome試中和網路相關的工具
1.請求
2.服務端接收到請求
請求是由客戶端發出的,也就是
chrome
瀏覽器程式,關於Upgrade-Insecure-Requests詳見
客戶端將自己的情況和請求的東西用請求頭髮送給伺服器,伺服器根據請求頭找到資源
|-- 請求方式 資源路徑 http版本
GET /swords/21 HTTP/1.1
|-- 請求的伺服器域名+埠號
Host: 192.168.10.104:8080
|-- 連線引數
Connection: keep-alive
|-- 快取控制引數
Cache-Control: max-age=0
|-- 升級-不安全-的請求:
Upgrade-Insecure-Requests: 1
|-- 使用者代理:告訴伺服器自己的現狀
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
|-- 該次請求可以接收的檔案型別
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
|-- 宣告客戶端支援的編碼型別
Accept-Encoding: gzip, deflate
|-- 宣告瀏覽器支援的語言
Accept-Language: zh-CN,zh;q=0.9
複製程式碼
3.接收響應
chrome的除錯工具展現的已經處理過了,並非原樣,這裡先看一下,等會再看原生的,
伺服器傳送響應給客戶端,客戶端根據響應進行下一步動作
4.四次揮手
5. 幾個層級(暫時不深入)
四、深入請求與響應
這裡chrome除錯不夠用了,使用PostMan進行請求,使用
Fiddler
進行抓包,
基本使用很簡單,裝上就行了。以下的測試認真看,這是以後經常用到的
網上很多要不就是講的太抽象,要麼就是太片面,這裡好好把一些凌亂的點理一下
預設是所有網路請求都會顯示在左側,你可以這樣過濾:
1.GET:最簡單的請求:
請求:
GET http://192.168.10.104:8080/swords/21 HTTP/1.1
cache-control: no-cache
Postman-Token: e72160d9-48db-4933-8c10-e0157c0f86db
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Host: 192.168.10.104:8080
accept-encoding: gzip, deflate
Connection: keep-alive
複製程式碼
響應:
注意響應頭和響應體(資料)之間有一個空行
Content-Length
這個欄位很重要,它表示響應體(資料)的位元組大小(如下圖:)
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Fri, 01 Feb 2019 04:56:35 GMT
Content-Length: 406
{"id":21,"name":"Excalibur","info":"Excalibur是傳說中不列顛國王亞瑟王從湖之仙女那得到的聖劍。此劍在是精靈在阿瓦隆(Avalon)所打造,劍鍔由黃金所鑄、劍柄上鑲有寶石,並因其鋒刃削鐵如泥","imgurl":"http://localhost:8080/imgs/timg.jpg","create_time":"2018-07-17T08:29:36.000+0000","modify_time":"2018-07-17T08:29:36.000+0000","origin":"亞瑟王"}
複製程式碼
2.GET:請求中加入請求引數(params):
上面是將引數作為url的最後一級,是一種Restful的書寫規範
這裡,將請求的引數加在url後,是url自身書寫規範,和上面基本沒什麼區別:
http://192.168.10.104:8080/swords/id?id=21
請求:
GET http://192.168.10.104:8080/swords/id?id=21 HTTP/1.1
cache-control: no-cache
Postman-Token: b7d97a6f-4c97-4651-8ccf-16541e24747c
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Host: 192.168.10.104:8080
accept-encoding: gzip, deflate
Connection: keep-alive
複製程式碼
響應:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Fri, 01 Feb 2019 05:35:24 GMT
Content-Length: 406
{"id":21,"name":"Excalibur","info":"Excalibur是傳說中不列顛國王亞瑟王從湖之仙女那得到的聖劍。此劍在是精靈在阿瓦隆(Avalon)所打造,劍鍔由黃金所鑄、劍柄上鑲有寶石,並因其鋒刃削鐵如泥","imgurl":"http://localhost:8080/imgs/timg.jpg","create_time":"2018-07-17T08:29:36.000+0000","modify_time":"2018-07-17T08:29:36.000+0000","origin":"亞瑟王"}
複製程式碼
3.POST:請求中加入請求引數(params)
與GET:請求中加入請求引數(params)唯一的區別就是請求方法不同
使用POST+請求引數,引數依然在url中,但不明文顯示,注意與下面POST提交表單的區別
POST表單時請求含有請求體,而POST+請求引數並沒有請求體,引數依然通過url傳遞
POST http://192.168.10.104:8080/api/sword?name=擎天劍&imgurl=http://192.168.10.104:8080/imgs/oQttHzCOUqeOatEH.jpg&info=天地一劍,開世擎天&origin=天晴仞 HTTP/1.1
cache-control: no-cache
Postman-Token: c60f5455-8263-4928-a2b3-278af9d198fd
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.10.104:8080
cookie: JSESSIONID=8B28A94404EF579B0E246ECD7FD04056
accept-encoding: gzip, deflate
content-length: 0
Connection: keep-alive
複製程式碼
響應:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Mon, 03 Feb 2019 15:00:35 GMT
Content-Length: 227
{"code":200,"msg":"操作成功","data":{"id":70,"name":"擎天劍","info":"天地一劍,開世擎天","imgurl":"http://192.168.10.104:8080/imgs/oQttHzCOUqeOatEH.jpg","create_time":null,"modify_time":null,"origin":"天晴仞"}}
複製程式碼
4.POST:表單提交
我們都填過表單,如登陸介面,表單採用post方式提交
這時候請求體(Body)就有用了,可以將一些額外的資料傳遞給伺服器
這樣的好處就是不用將資料暴露在url裡了,注意一下表格資料傳送的格式:
請求:
POST http://192.168.10.104:8080/api/sword HTTP/1.1
cache-control: no-cache
Postman-Token: cf6cb7e3-e66d-4339-9685-54f46af7db12
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.10.104:8080
cookie: JSESSIONID=8B28A94404EF579B0E246ECD7FD04056
accept-encoding: gzip, deflate
content-type: multipart/form-data; boundary=--------------------------789466732494020503103134
content-length: 567
Connection: keep-alive
----------------------------789466732494020503103134
Content-Disposition: form-data; name="name"
擎天劍
----------------------------789466732494020503103134
Content-Disposition: form-data; name="imgurl"
http://192.168.10.104:8080/imgs/oQttHzCOUqeOatEH.jpg
----------------------------789466732494020503103134
Content-Disposition: form-data; name="info"
天地一劍,開世擎天
----------------------------789466732494020503103134
Content-Disposition: form-data; name="origin"
天晴仞
----------------------------789466732494020503103134--
複製程式碼
響應:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Mon, 04 Feb 2019 05:00:35 GMT
Content-Length: 227
{"code":200,"msg":"操作成功","data":{"id":70,"name":"擎天劍","info":"天地一劍,開世擎天","imgurl":"http://192.168.10.104:8080/imgs/oQttHzCOUqeOatEH.jpg","create_time":null,"modify_time":null,"origin":"天晴仞"}}
複製程式碼
5.POST-表單上傳檔案
注意一下這裡檔案上傳時請求的格式,可以和上面的表單對比一下
請求:
POST http://192.168.10.104:8080/upload HTTP/1.1
cache-control: no-cache
Postman-Token: c12cd0dc-fbcd-4726-9c97-ebacc6498d26
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Host: 192.168.10.104:8080
accept-encoding: gzip, deflate
content-type: multipart/form-data; boundary=--------------------------131785098353427999614106
content-length: 345
Connection: keep-alive
----------------------------131785098353427999614106
Content-Disposition: form-data; name="file"; filename="應龍.txt"
Content-Type: text/plain
《應龍》--張風捷特烈
一遊小池兩歲月,
洗卻凡世幾閒塵。
時逢雷霆風會雨,
應乘扶搖化入雲。
----------------------------131785098353427999614106--
複製程式碼
響應:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 24
Date: Fri, 01 Feb 2019 06:53:03 GMT
應龍.txt上傳成功!
複製程式碼
6.POST-傳遞原生資料
也就是在客戶端請求是攜帶請求的額外原生資料(如下),服務端可以拿到這些資料
請求:
可見請求體的資料也是和請求頭隔著一行
POST http://192.168.10.104:8080/postString HTTP/1.1
cache-control: no-cache
Postman-Token: e532e186-4a4c-4a9f-82bb-8045b1cd9403
Content-Type: text/plain
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Host: 192.168.10.104:8080
accept-encoding: gzip, deflate
content-length: 53
Connection: keep-alive
海的彼岸有我未曾見證的風采--創世神無
複製程式碼
響應:
伺服器裡我讓資料原樣返回,當然你也可以處理一下再返回
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 53
Date: Fri, 01 Feb 2019 06:19:52 GMT
海的彼岸有我未曾見證的風采--創世神無
複製程式碼
7.POST-二進位制檔案
注意一下,傳遞二進位制檔案和表單傳遞檔案、原生資料的區別
|--POST-二進位制檔案 格式上同傳遞 原生資料,由於是二進位制流,可以傳遞任意的資料
|--POST-二進位制檔案和表單上傳檔案都能上傳檔案,但請求體是完全不同的
請求:
POST http://192.168.10.104:8080/PostFile HTTP/1.1
cache-control: no-cache
Postman-Token: 607a26e6-9c84-4b6b-97e9-3f539cf9a150
Content-Type: text/plain
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Host: 192.168.10.104:8080
accept-encoding: gzip, deflate
content-length: 137
Connection: keep-alive
《應龍》--張風捷特烈
一遊小池兩歲月,
洗卻凡世幾閒塵。
時逢雷霆風會雨,
應乘扶搖化入雲。
複製程式碼
響應:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 7
Date: Fri, 01 Feb 2019 07:28:35 GMT
SUCCESS
複製程式碼
關於PUT,DELETE等於請求與POST基本一致,就不多說了
8.再認識表單
下面是一個簡單的表單,介面未優化,來看一下多個欄位是如何請求的
這樣也許你會對錶單有更深的認識,也會對多檔案上傳有思路
POST http://192.168.10.104:8080/submit_form HTTP/1.1
Host: 192.168.10.104:8080
Connection: keep-alive
Content-Length: 626
Cache-Control: max-age=0
Origin: http://192.168.10.104:8080
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7Mqt2T4cA2gNVkCa
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.10.104:8080/add_form
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
------WebKitFormBoundary7Mqt2T4cA2gNVkCa
Content-Disposition: form-data; name="name"
弒神劍
------WebKitFormBoundary7Mqt2T4cA2gNVkCa
Content-Disposition: form-data; name="info"
一劍弒神
------WebKitFormBoundary7Mqt2T4cA2gNVkCa
Content-Disposition: form-data; name="origin"
噬神者
------WebKitFormBoundary7Mqt2T4cA2gNVkCa
Content-Disposition: form-data; name="file"; filename="應龍.txt"
Content-Type: text/plain
《應龍》--張風捷特烈
一遊小池兩歲月,
洗卻凡世幾閒塵。
時逢雷霆風會雨,
應乘扶搖化入雲。
------WebKitFormBoundary7Mqt2T4cA2gNVkCa--
複製程式碼
五、如何使用請求頭
上面說了一大堆請求和響應的格式,現在說一下他們的用處
這麼想吧:瀏覽器將請求頭髮給伺服器,手機可以作為客戶端,職能上和瀏覽器並無區別
伺服器並不會區分是瀏覽器還是手機,它只認請求頭,然後做出反應
1.手機POST字串到伺服器
客戶端使用socket連線服務端,通過socket的輸出流將請求頭寫給伺服器
伺服器看到請求頭就會做出相應的反應,這裡的請求頭就是四-6
的請求頭
/**
* 通過ip和埠連線服務端核心程式碼
*
* @param ip ip地址
* @param port 埠
*/
private void connServer(String ip, int port) {
String header = "POST http://192.168.10.105:8080/postString HTTP/1.1\n" +
"cache-control: no-cache\n" +
"Postman-Token: e532e186-4a4c-4a9f-82bb-8045b1cd9403\n" +
"Content-Type: text/plain\n" +
"User-Agent: PostmanRuntime/7.4.0\n" +
"Accept: */*\n" +
"Host: 192.168.10.105:8080\n" +
"accept-encoding: gzip, deflate\n" +
"content-length: 53\n" +
"Connection: keep-alive\n" +
"\n" +
"海的彼岸有我未曾見證的風采--創世神無";
try {
//1.建立客戶端Socket物件(ip,埠)
mSocket = new Socket(ip, port);
//2.通過socket物件獲取位元組輸出流,幷包裝成PrintWriter----用於傳送給服務端資料
mPwOut = new PrintWriter(mSocket.getOutputStream(), true);
//3.通過socket物件獲取位元組輸出流
mPwOut.write(header);
} catch (IOException e) {
e.printStackTrace();
}finally {
mPwOut.close();
}
}
|-- Android中使用
new Thread(()->{
connServer("192.168.10.105", 8080);
}).start();
複製程式碼
2.服務端的處理
通過HttpServletRequest獲取輸入流,進而得到請求體
你可以根據輸入流來自定義一些操作,如儲存,轉換等
---->[SwordController#postString]----------------------------------
@PostMapping("/postString")
public String postString(HttpServletRequest request) {
ServletInputStream is = null;
try {
is = request.getInputStream();
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
System.out.println(sb.toString());
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
複製程式碼
3.上傳二進位制檔案
請求頭是
四-7
的,這裡上傳一張圖片到伺服器
/**
* 通過ip和埠連線服務端核心程式碼
*
* @param ip ip地址
* @param port 埠
*/
private void connServerPostFile(String ip, int port) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic_22);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] datas = baos.toByteArray();
String header = "POST http://192.168.10.105:8080/PostFile HTTP/1.1\n" +
"cache-control: no-cache\n" +
"Postman-Token: 607a26e6-9c84-4b6b-97e9-3f539cf9a150\n" +
"Content-Type: text/plain\n" +
"User-Agent: PostmanRuntime/7.4.0\n" +
"Accept: */*\n" +
"Host: 192.168.10.105:8080\n" +
"accept-encoding: gzip, deflate\n" +
"content-length: " + datas.length + "\n" +
"Connection: keep-alive\n" +
"\n";
try {
//1.建立客戶端Socket物件(ip,埠)
mSocket = new Socket(ip, port);
mOs = mSocket.getOutputStream();
mOs.write(header.getBytes());
mOs.write(datas);
//3.通過socket物件獲取位元組輸出流
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
mOs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製程式碼
也就是說伺服器只認請求,只有頭對了,它就提供服務。
就像員工吃食堂,管你張三李四,有工作牌就能食堂就提供打飯服務。
網路框架HttpURLConnection
,okHttp
底層都少不了對這些的封裝
4.小看一下okHttp
對錶單的拼接
okHttp也是按照Http的請求格式規範來對錶單進行拼合的
如果在用okHttp時,能意識到你的請求是什麼樣子的,會不會視野更開闊呢?
public final class MultipartBody extends RequestBody {
public static final MediaType MIXED = MediaType.get("multipart/mixed");
public static final MediaType ALTERNATIVE = MediaType.get("multipart/alternative");
public static final MediaType DIGEST = MediaType.get("multipart/digest");
public static final MediaType PARALLEL = MediaType.get("multipart/parallel");
public static final MediaType FORM = MediaType.get("multipart/form-data");
--->private static final byte[] COLONSPACE = {':', ' '};
--->private static final byte[] CRLF = {'\r', '\n'};
--->private static final byte[] DASHDASH = {'-', '-'};
--->private final ByteString boundary;//分割線
private final MediaType originalType;
private final MediaType contentType;
private final List<Part> parts;
private long contentLength = -1L;
MultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
this.boundary = boundary;
this.originalType = type;
this.contentType = MediaType.get(type + "; boundary=" + boundary.utf8());
this.parts = Util.immutableList(parts);
}
...
public static final class Part {
public static Part create(RequestBody body) {
return create(null, body);
}
public static Part create(@Nullable Headers headers, RequestBody body) {
if (body == null) {
throw new NullPointerException("body == null");
}
if (headers != null && headers.get("Content-Type") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Type");
}
if (headers != null && headers.get("Content-Length") != null) {
throw new IllegalArgumentException("Unexpected header: Content-Length");
}
return new Part(headers, body);
}
//下面是拼合表單的方法
public static Part createFormData(String name, String value) {
return createFormData(name, null, RequestBody.create(null, value));
}
public static Part createFormData(String name, @Nullable String filename, RequestBody body) {
if (name == null) {
throw new NullPointerException("name == null");
}
---> StringBuilder disposition = new StringBuilder("form-data; name=");
appendQuotedString(disposition, name);
if (filename != null) {
disposition.append("; filename=");
appendQuotedString(disposition, filename);
}
Headers headers = new Headers.Builder()
---> .addUnsafeNonAscii("Content-Disposition", disposition.toString())
.build();
return create(headers, body);
}
...
}
public static final class Builder {
private final ByteString boundary;
private MediaType type = MIXED;
private final List<Part> parts = new ArrayList<>();
public Builder() {
this(UUID.randomUUID().toString());
}
public Builder(String boundary) {
this.boundary = ByteString.encodeUtf8(boundary);
}
/**
* 設定 MIME type
*/
public Builder setType(MediaType type) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (!type.type().equals("multipart")) {
---> throw new IllegalArgumentException("multipart != " + type);
}
this.type = type;
return this;
}
...
}
}
複製程式碼
六、讓網站支援Https
1.進入控制檯
2.獲取證書
3.填表
4.下載證書
5.SpringBoot專案配置
配置好後打包上線
6.效果
OK 這就可以通過https訪問了,簡單支援一下,其他的就不深究了
這篇也挺長了,關於快取、伺服器的響應碼就放到下一篇吧。
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 附錄 |
---|---|---|
V0.1--無 | 2018-3-12 | 無 |
V0.2--無 | 2018-3-19 | 增加四次揮手 |
釋出名:
網路篇:協天子令諸侯[-Http-]
捷文連結:juejin.im/post/5c87a7…
2.更多關於我
筆名 | 微信 | |
---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 |
我的github:github.com/toly1994328
我的簡書:www.jianshu.com/u/e4e52c116…
我的簡書:www.jianshu.com/u/e4e52c116…
個人網站:www.toly1994.com
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援