关键词:OSI七层参考模型,TCP/IP四层模型,HTTP/HTTPS,三次握手/四次挥手,TCP/UDP
如何实现网络通信
- 在一定的规则(网络通信协议)之下实现具体的通信,根据通信协议分层思想划分各层,实现各层之间的数据传输:
- OSI参考模型(7层结构):模型过于理想化;”物联网叔会使用“😂
- TCP/IP参考模型(或TCP/IP协议):实际上的国际标准。
- 通过通信双方(某个进程和某个进程之间)的地址进行双方的定位:IP地址+端口号
网络通信协议
?
微观网络通信流程
?
数据在OSI模型各层中的格式
? ? 参考:
?? ?
? ? 传输层:上层传来的数据在TCP/UDP协议下,得到TCP/UDP报文段,简称:TCP/UDP数据报
? ? 网络层:上层传来的数据在IP协议下,得到IP数据报(数据包)
? ? 数据链路层:将IP数据报封装成帧
? ? 物理层:比特流
?? ?
宏观网络通信路程(【面试常问】一次完整的HTTP请求所经历的步骤)
? ? 参考:面试-网络篇-一次完整的HTTP请求所经历的7个步骤_张罗丰的博客-CSDN博客
?? ? Web浏览器与Web服务器之间将完成下列7个步骤:
- 建立TCP连接
- 在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能进行更高层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。
- Web浏览器向Web服务器发送请求命令
- 一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET/sample/hello.jsp HTTP/1.1。<== 通过HTTP协议开始工作
- Web浏览器发送请求头信息
- 浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行(POST请求)来通知服务器,它已经结束了该头信息的发送。
- Web服务器应答
- 客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
- Web服务器发送应答头信息
- 正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。
- Web服务器向浏览器发送数据
- Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
- Web服务器关闭TCP连接
- 一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive,TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
细节补充
==> 涉及到了SpringMVC处理静态资源的解决方式??
<!--映射到Tomcat默认的servlet处理-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-必须同时添加上mvc驱动-->
<mvc:annotation-driven>
- ?POST请求格式
- 响应的HTTP协议格式
HTTP协议 与 HTTPS协议
先参考下面链接,有空整理:
TCP
三次握手
?? ?A:我要连接了
?? ?B:好的,我知道了,可以建立连接
?? ?A:嗯,我知道可以连了。
?? ?为什么?三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
? ? ?提前告知
- 刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
- 确认值(Acknowledgement),为1便是确认连接
- 确认编号(Acknowledgement Number),即接收到的上一次远端主机传来的seq然后+1,再发送给远端主机。提示远端主机已经成功接收上一次所有数据。
- SYN 报文段不能携带数据,但要消耗掉一个序号,ACK报文段可以携带数据,不携带数据则不消耗序号。
- 在socket编程中,客户端执行connect()时,将触发三次握手。
?? ? 三次握手详细描述
- 客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号。此时客户端处于 SYN_SEND 状态。
- 初始序号seq=x,首部的同步位SYN=1,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
- 服务器收到客户端的SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号。同时会把客户端的(序列号+ 1)作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
- 在确认报文段中初始序号seq=y,SYN=1,ACK=1,确认号ack=x+1。
- 客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的(序列号+ 1)作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
- 确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
? ? 三次握手的目的
- 服务器端得出:客户端的发送能力、服务端的接收能力是正常的。
- 客户端得出:服务端的接收、发送能力,客户端的接收、发送能力是正常的。但是服务器端仅能得到:服务端的接收、发送能力、客户端的发送能力是正常的。
- 服务端得出:客户端的接收、发送能力正常,服务器的发送、接收能力正常。
? ? 如果两次握手
?? ??? ?? ? 在第二次服务器发送报文结束后,可能客户端没有收到服务器的应答报文,但是服务器以为客户端收到了,服务器就会一直保存这个通信过程,造成了一定的资源浪费。所以一定要让服务器端知道客户端的接收能力正常。
四次挥手
?? ?? ? 提前告知
- ACK和ack的使用可能有些混乱,但是看编号和值还是可以分的清楚的。
- 客户端或服务器均可主动发起挥手动作。
- 单工数据传输: 只支持数据在一个方向上传输;例如:电视机
- 半双工数据传输:允许数据在两个方向上传输,但是在某一时刻,只允许数据在一个方向上传输,它实际上是一种可以切换方向的单工通信;例如:对讲机
- 全双工数据通信:允许数据同时在两个方向上传输。
- TCP在两个应用程序之间建立的是全双工的通信传输。
- 收到一个FIN只意味着在这一方向上没有数据流动。
- 客户端或服务器均可主动发起挥手动作。
- 在socket编程中,任何一方执行close()操作即可产生挥手操作。
A:我要断开连接了
B:好的,我知道你要断开了
B:我现在也要断开了
A:好的,我知道你也要断开了。
B断开,A等2MSL后,也断开了。
????????四次挥手详细描述
?? ??? ? 刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。
- 客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
- 发出连接释放报文段(FIN=1,序号seq=X),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
- 服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的(序列号值 +1)作为 ACK 报文的确认编号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
- 服务端收到连接释放报文段后即发出确认报文段(确认值ack=1,确认编号ACK=X+1,序号Seq=Z),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放(客户端结束了它的发送)。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
- 服务端也要断开连接,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
- 服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,确认值ack=1,序号Seq=Y,确认编号ACK=X),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
- 客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的(序列号值 +1)作为自己 ACK 报文的确认编号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
- 客户端收到服务端的连接释放报文段后,对此发出确认报文段(确认值ack=1,Seq=X,确认编号ACK=Y),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
?? ??? ?为什么挥手要有四次?
- 这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力(只关闭了发送的能力,却还可以接收另一端发动的数据,所以另一端的发送能力也要关闭掉)。
- 为什么只需要三次握手客户端和服务器端就可以建立连接呢?因为当第一次客户端告诉服务器端它想建立连接的时候,后面服务器端是确认和同步一起发送的,最后一次客户端发确认让服务端知道自己可以正常接收,所以只需要三次。
- 当客户端想要关闭连接时,发送FIN报文,但是服务器端不会立即关闭SOCKET,只回复了确认,表示:只有等服务器端所有的报文都发送完了,服务器端才会关闭服务器自己的连接。第三次表示服务器端要关闭了,所以才发了FIN,第四次,客户端发确认,知道服务器端关闭了。服务器收到来自客户端的确认之后就会直接关闭了,但是客户端为了以防服务器没有收到自己的确认信号,会等待2MSL,来确保服务器到客户端的连接正常关闭。
?? ?? ? 2MSL等待状态
?? ??? ??? ? TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。
?? ?? ? 【面试题】
- TIME_WAIT是在服务器端还是客户端?
- 因为客户端和服务器端都可以发起挥手动作,从第一次发起动作的来源看,第一次发送FIN报文段的一方会在第四次挥手后进入TIME_WAIT状态。
- 四次挥手释放连接时,等待2MSL的意义?
- 保证服务器端能够收到最后一个应答报文段,进入CLOSED状态。==>? 如何服务器端没有收到,会超时重传,所以客户端可以重发应答。
- 防止“已失效的连接请求报文段”出现在本连接中。
- 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
????????示例代码理解
先开服务器端,再开客户端发送,才能正常测试。
/**
* 实现TCP的网络编程
* 例子1:客户端发送信息给服务端,服务端将数据显示在控制台上
* @author wkn
* @create 2021-06-22 21:01
*/
public class TCPTest1 {
//客户端
@Test
public void client(){
Socket socket = null;
OutputStream os = null;
try {
//1. 创建客户端的Socket对象,指明服务器端的ip和端口号,为了输出到fw端
InetAddress inet = InetAddress.getByName("172.28.160.1");
socket = new Socket(inet, 8899);
//2.获取一个输出流,用于从客户端输出数据
os = socket.getOutputStream();
//3.客户端写出数据的操作
os.write("这是从客服端传来的数据".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务端
@Test
public void server() throws IOException {
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
//1.创建服务器端的ServerSocket,指明自己的端口号
try {
ss = new ServerSocket(8899);
} catch (IOException e) {
e.printStackTrace();
}
//2.服务器端的ServerSocket调用accept()表示服务端可以接收到来自于客户端的socket,服务器监听本地8899端口,监听到后,就三次握手连接
try {
socket = ss.accept();
} catch (IOException e) {
e.printStackTrace();
}
//3.获取客户端的输入流
try {
is = socket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
//方式一:直接将byte类型转换成String类型,输出到控制台
//不建议这样写,可能会有乱码
// byte[] buffer = new byte[1024];
// int len;
// while((len = is.read(buffer)) != -1){
// String str = new String(buffer,0,len);
// System.out.print(str);
// }
//方式二:通过ByteArrayOutputStream(),将输入流中的数据完全存到内部的数组中之后,通过toString()重写
baos = new ByteArrayOutputStream();//不用传文件等目的参数
byte[] buffer = new byte[5];
int len;
while ((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress()+"的数据");
}
}
注:方式一为什么会出现乱码?因为对于utf-8中文占3个字节,如果用于每次从输入流中保存数据的数组太小可能会使得保存了截开的字节。
/**
* 客户端上传图像,服务器端保存到本地
* @author wkn
* @create 2021-06-23 9:39
*/
public class TCPTest2 {
/*
这里涉及到的异常,应该使用try-catch-finally处理
*/
@Test
public void client() throws IOException {
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);
OutputStream os = socket.getOutputStream();
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
fis.close();
os.close();
socket.close();
}
@Test
public void server() throws IOException {
ServerSocket ss = new ServerSocket(9090);
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream(new File("beauty1.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1){
fos.write(buffer, 0 , len);
}
fos.close();
is.close();
socket.close();
ss.close();
}
}
/**
* 实现TCP的网络编程
* 例题3:从客户端发送文件给服务端,服务端保存到本地,并返回“发送成功”给客户端并关闭相应的连接
* 服务器端有响应
* @author wkn
* @create 2021-06-23 19:53
*/
public class TCPTest3 {
@Test
public void client() throws IOException {
System.out.println(InetAddress.getLocalHost());
Socket socket = new Socket(InetAddress.getLocalHost(),9090);
OutputStream os = socket.getOutputStream();
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//关闭数据输出
socket.shutdownOutput();
//5.接收来自于服务器端的数据,并显示到控制台上
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer1 = new byte[1024];
int len1;
while ((len1 = is.read(buffer1)) != -1){
baos.write(buffer1,0,len1);
}
System.out.println(baos.toString());
baos.close();
is.close();
}
@Test
public void server() throws IOException {
ServerSocket ss = new ServerSocket(9090);
Socket socket = ss.accept();//监听本地的9090端口,监听到后,就三次握手连接
InputStream is = socket.getInputStream();//往本地写
OutputStream os = socket.getOutputStream();//向管道发出去
FileOutputStream fos = new FileOutputStream(new File("beauty2.jpg"));
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("图像传输完成!");
os.write("客户端,你好,照片我已收到!".getBytes());
fos.close();
is.close();
socket.close();
ss.close();
os.close();
}
}
【练习题】
来源:关于 Socket 通信编程,以下描述正确的是:( )__牛客网
?对于【D】选项:服务器端通过new ServerSocket()创建ServerSocket对象,ServerSocket对象的accept方法产生阻塞,当监听到一个来自客户端的请求时,会创建一个Socket对象,使用此Socket对象与客户端进行通信。
UDP
未完待续......
|