个人所有文章整理在此篇,将陆续更新收录:知无涯,行者之路莫言终(我的编程之路)
零、前言
不管什么语言,什么系统,都离不开网络,这个系列就来深入一下网络
首先是挟天子以令诸侯
的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-]
捷文链接: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----看到这里,我在此感谢你的喜欢与支持