網路篇:協天子令諸侯[-Http-]
手動置頂:文章整理了一下,個人所有文章整理在此篇,以後將陸續更新收錄:
知無涯,行者之路莫言終(我的程式設計之路)
零、前言
不管什麼語言,什麼系統,都離不開網路,這個系列就來深入一下網路
首先是 挾天子以令諸侯
的Http,它把握了整個網路的命脈。所有人必須對它言聽計從。
本文主要聚焦
1.dns的定址(域名解析) 2.tcp/ip的三次握手,建立連線 3.客戶端請求和服務端響應的詳細分析 4.騰訊雲免費ssl證書獲取,以及基於springboot2設定https支援
注:本文的服務端程式碼已放在 Github ,如果沒有基礎,可以參見我的SpringBoot入門級系列:
如果不想接觸後端,本文也可以看,不過理解方面多少會有些欠缺,
畢竟http是 客戶端
和 服務端
兩個人的事,撇看一者來看,是不現實的。
一、域名解析
chrome中輸入網址敲回車之後,瀏覽器會根據域名找到對應的伺服器地址
這裡以我的網站: http://www.toly1994.com/
為例

1_敲網址.png

定址簡述.png
0.url簡述:統一資源定位符(Uniform Resource Locator)
在此之前先簡單說一下url
基本URL包含 模式(或稱協議)、伺服器名稱(或IP地址)、路徑和檔名, 協議://使用者名稱:密碼@子域名.域名.頂級域名:埠號/目錄/檔名.檔案字尾?引數=值#標誌。

url簡述
1.解析域名--Chrome搜尋自身DNS快取
也就是根據域名找到對應的ip地址:首先看瀏覽器自身有沒有快取:
chrome://net-internals/#dns

DNS快取.png
2.解析域名--檢視本機上的DNS快取
如果1沒有的話,檢視本機上的DNS快取, ipconfig /displaydns

本機DNS檢視.png
3.解析域名--檢視host檔案是否有對應的網址ip
如果2沒有的話,檢視host檔案是否有對應的網址ip, C:\Windows\System32\drivers\etc

檢視host檔案.png
4.解析域名--外部查詢
前三步沒有查到,這說明本地無該網站的DNS快取,由寬頻運營商的伺服器進行查詢
如果無快取,會一級一級的去找,知道找到toly1994.com對應的伺服器(即我的伺服器),
最後將查到的伺服器ip地址返回給剛才敲網址的瀏覽器

解析.png
二、客戶端與服務端建立 TCP/IP
連線:
為了簡單些,使用 http://www.toly1994.com:8080/swords/21
客戶端在訪問時,第一步就是查詢域名所對的ip地址(即伺服器住哪)

客戶端和服務端.png

查詢ip.png
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。

TCP報文格式.png
1.第一次握手: 問一下伺服器在不在
客戶端傳送SYN=1,seq=J(J為隨機數字)的報文給伺服器
服務端看到SYN=1,知道客戶端要請求連線

第一次握手.png
2.第二次握手: 伺服器說在
服務端傳送SYN=1,ACK=1,seq=K(K為隨機數字),ack=J+1的報文給客戶端
客戶端看到SYN=1,ack=J+1,便知道服務端給自己回話了

第二次握手.png
3.第三次握手: 客戶端說我也還在
客戶端傳送ACK=1,ack=K+1的報文給伺服器
服務端看到ack=K+1,知道客戶端收到了剛才的話

第三次握手.png
這樣就建立了一個穩固的 TCP/IP
連線

建立連線.png
三、傳送請求與接收響應
上面說到服務端和客戶端建立了連線,接下來就是 請求
與 響應
了
在此之前先看一下chrome試中和網路相關的工具

chrome.png
1.請求

請球.png

請求頭.png
2.客戶端接收到請求
請求是由客戶端發出的,也就是 chrome
瀏覽器程式,關於 Upgrade-Insecure-Requests詳見
客戶端將自己的情況和請求的東西用請求頭髮送給伺服器,伺服器根據請求頭找到資源

伺服器根據客戶端請求找到資源.png
|-- 請求方式 資源路徑 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的除錯工具展現的已經處理過了,並非原樣,這裡先看一下,等會再看原生的,
伺服器傳送響應給客戶端,客戶端根據響應進行下一步動作

響應.png

伺服器通過響應傳遞資料.png
三、深入請求與響應
這裡chrome除錯不夠用了,使用PostMan進行請求,使用 Fiddler
進行抓包,
基本使用很簡單,裝上就行了。以下的測試認真看,這是以後經常用到的
網上很多要不就是講的太抽象,要麼就是太片面,這裡好好把一些凌亂的點理一下

抓包.png
預設是所有網路請求都會顯示在左側,你可以這樣過濾:

過濾操作.png
1.GET:最簡單的請求:
http://192.168.10.104:8080/swords/21
請求:
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
這個欄位很重要,它表示響應體(資料)的位元組大小(如下圖:)

Content-Length.png
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.png
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表單提交.png
請求:
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-表單上傳檔案
注意一下這裡檔案上傳時請求的格式,可以和上面的表單對比一下

表單上傳檔案.png

上傳成功.png
請求:
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-傳遞原生資料
也就是在客戶端請求是攜帶請求的額外原生資料(如下),服務端可以拿到這些資料

原生資料格式.png

post傳遞String資料.png

服務端.png
請求:
可見請求體的資料也是和請求頭隔著一行
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.再認識表單
下面是一個簡單的表單,介面未優化,來看一下多個欄位是如何請求的
這樣也許你會對錶單有更深的認識,也會對多檔案上傳有思路

表單.png
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 ipip地址 * @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獲取輸入流,進而得到請求體
你可以根據輸入流來自定義一些操作,如儲存,轉換等

服務端.png
---->[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 ipip地址 * @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.進入控制檯

控制檯.png
2.獲取證書

ssl.png
3.填表

填表.png

自動.png
4.下載證書

下載證書.png
5.SpringBoot專案配置
配置好後打包上線

專案配置.png
6.效果

支援https.png
OK 這就可以通過https訪問了,簡單支援一下,其他的就不深究了
這篇也挺長了,關於快取、伺服器的響應碼就放到下一篇吧。
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 附錄 |
---|---|---|
V0.1--無 | 2018-3-12 | 無 |
釋出名: 網路篇:協天子令諸侯[-Http-]
捷文連結: https://juejin.im/post/5c87a7fd6fb9a049bb7d2da2
2.更多關於我
筆名 | QQ|微信|
---|---|---|---|
張風捷特烈 | 1981462002|zdl1994328|
我的github: https://github.com/toly1994328
我的簡書: https://www.jianshu.com/u/e4e52c116681
我的簡書: https://www.jianshu.com/u/e4e52c116681
個人網站: http://www.toly1994.com
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png