面试直通车之网络部分

在进入大三阶段,马上面临大四即将到来的实习,在此时期为了能找到一个好的工作,对大学所学的知识,依据慕课网《剑指Java面试-Offer直通车》的讲解体系,进行了一个系统化的总结

接下来进入第一章网络部分的讲解

TCP


OSI开放式互联参考模型

  • 物理层:机械、电子、定时接口通信商的原始比特流传输,这里就是发送比特流最基本的数据,就是0101二进制数据,最根本的就是电流的强弱来进行传输,比如高电平是1低电平是0。

  • 数据链路层:物理寻址、同时将原始比特流转变为逻辑传输线路。这里主要是对数据进行格式化,对数据进行一层封装,加了一层的校验。

  • 网络层:控制子网的运行,如逻辑地址、分组传输、路由的选择。把网络地址翻译成对应的物理地址,我的理解就是可以寻址。给这段代码在网络上给了一个表示,目的地址和本地地址应该在这里面。

  • 传输层:接收上一层的数据,在必要的时候吧数据进行分割,并将这些数据交给网络层,且保证这些数据段有效到达对端。这一层主要考虑的是传输质量的问题,对于接收方能接受的数据的程度来对数据进行处理。对数据包可以进行强制分割,以至于在传输的过程中不会造成大量丢失,使网络传输的接受范围之内。

  • 会话层:不同机器上的据用之间建立和管理回话。

  • 表示层:信息的语法语义以及它们的关联,如加密解密、转换翻译、压缩解压缩。主要一个功能就是解决不同系统之间的通信,比如我是windows和一个linux系统的之间通信就是靠表示层来进行转换翻译和解压缩的。

  • 应用层:这里比较关注的就是HTTP协议

OSI并不是一个标准,而是一个在制定标准时所使用的概念性框架。

先自上而下,后自下而上处理数据头部

TCP/IP协议

OSI的“实现”:TCP/IP

传输控制协议TCP简介

  1. 面向连接的、可靠的、基于字节流的传输层通信协议

  2. 将应用层的数据流分割成报文段并发送给目标节点的TCP层

  3. 数据包都有序号,对方收到则发送ACK确认,未收到则重新传

  4. 使用校验和来检验数据在传输过程中是否有误 TCP Flags

TCP Flags

  • URG:紧急指针标志

  • ACK:确认序号标志(1表示确认号有效,0报文中不含确认信息)

  • PSH:push标志(相当于优先处理,不在缓冲区排队)

  • RST:重置连接标志

  • **SYN:同步序号,用于建立连接过程 **

  • FIN:finish标志用于释放连接(1没有数据发送了,0还有数据)

三次握手

TCP是全双工的通信,三次握手是为了建立连接

 流程图

Seq(Sequence Number)是初始信号正整数值

在第一次握手接收端返回的ack为x+1,因为在之前seq消耗了一个x就要+1,seq是为缓存初始一个序列号y

同理在第三次握手的时候发送端的seq为x+1,ack为y+1

在TCP/IP协议中,TCP协议提供可靠的链接服务,采用三次握手建立一个链接

第一次握手:建立连接时,客户端发送SYN包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到SYN包,必须确认客户的SYN(ackj+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,进行数据的传输。

TCP报文头

TCP报文头

这里我对TCP的报文头不做过多的说明,在网上有好多博客写了这方面的知识比较全面

如果对这里特别感兴趣可以下载Wireshark的抓包工具。

Wireshark

我打开这个软件,随便浏览网上一个网址,在上面地址栏输入正则表达式,然后对一个固定IP36.104.142.35进行抓包,就可以查看TCP的链接状态,以及报文内容格式

抓包截图

SYN Flood的防护

在第一次握手的时候会有超时的隐患

问题起因的分析

  • Server(服务器端)收到Clinent(客户端)的SYN,恢复SYN-ACK的时候,我们的Clinent就掉线了,Server未收到ACK的确认,那么这个连接就会处于一个中间的状态,即没有成功也没有失败

  • Server(服务器端)不断重连直至超时,Linux默认重试次数为5次,每次重连的时间从1秒开始,每次都翻倍,一共的等待1+2+4+6+16+32=63秒(这里可以理解为一个加号为一次重连机会)才断开连接

在这个时候有可能会遭受到SYN Flood攻击的风险,攻击程序会给服务器发送一个SYN的报文,发了之后它就下线了需要服务器默认等63秒才会断开这个连接,这样攻击者就会把服务器的SYN连接的队列耗尽,将正常的连接请求不能处理。

针对SYN Flood的防护措施

  • 在Linux下呢,SYN队列满了后,通过源地址端口和目标地址端口和时间戳打造出特别的seq就是tcp_syncookies参数回发SYN Cookie,如果是攻击者是不会有响应的

  • 若为正常连接则Clinet会回发SYN Cookie,利用Cookie来直接建立连接。

建立连接后,Client出现了故障怎么办

TCP还设有保活机制

  • 在一段时间,连接处于非活动状态,开启保护功能的一段向对方发送保活探测报文,如果发送端没有收到响应报文,那么经过体检经过设置好的保活时间内则继续发送知道连接为止

  • 尝试次数达到保活探测数仍未收到响应则中断连接

为什么要进行三次握手

为了初始化Sequence Number的初始值,通信的双方通知自己的初始化的SN的值,作为以后通信的信号,不会因为网络上的传输问题而乱序,TCP用这个序号来拼接这个序号。

四次挥手

流程图

TCP采用四次挥手来释放连接

第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态;

第二次挥手:server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态;

第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态;

第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

TIME_WAIT

为什么会有TIME_WAIT的状态?

  • 确保有足够的时间让对端收到ACK的包,如果被动关闭的一方没有收到ACK,就会触发被动端重发Finish。

  • 有足够的时间,让连接不会跟后面的链接混在一起,因为有些路由器会缓存IP数据包,如果连接被重用了那么这些延迟收到的包呢,就有可能会跟新连接的包混在一起。

为什么要进行四次挥手

因为是全双工的,发送方和接收方都需要FIN报文和ACK报文。其实只需要两次挥手即可,只不过有一方是被动的所以看上去就成了所谓的四次挥手

CLOSE_WAIT

服务器出现大量CLOSE_WAIT状态的原因

对方关闭socket连接,我方忙于读或者写,没有及时的关闭连接,遇到这种情况多数是程序有BUG,还有可能是线程的配置不合理。

解决方法

  • 检查代码

  • 检查配置

滑动窗口

首先了解RTT和RTO是什么

  • RTT:发送一个数据包到收到对应的ADC,所花费的时间

  • RTO:重传时间间隔

前面我们了解到TCP会将数据拆分成段进行发送,出于效率和传输速度的考虑,我们不可能等一段数据去发送,等到上一段数据被确认之后再发送下一段数据,这个效率是非常低的,我们是要实现对数据的批量发送,TCP必须要解决可靠传输以及包乱序的问题,所以TCP需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

TCP使用滑动窗口做流量控制与乱序重排,两个作用

  • 保证TCP的可靠性

  • 保证TCP的流控特性

前面在TCP的报文头中有一个字段是WINDOWS,就是第四行第二的字段就是滑动窗口的字段。用于接收方通知发送方自己,还有多少缓冲区可以接收数据,发送方根据接收方的处理能力来发送数据。不会导致接收方处理不过来,这就是流量控制。

滑动窗口

根据上面的图可以清晰的理解到底什么是滑动窗口

TCP和UDP的区别

UDP报文

上图是UDP报文的结构,相对于TCP少了很多。

UDP的特点:

  • 面向非连接,传送速度是根据程序生成的速度所影响。

  • 不维护连接状态,服务端支持同时向多个客户端传输相同的消息

  • 数据包报头只有8个字节,额外开销小

  • 吞吐量只受限于数据生成速率、传输速率以及机器性能

  • 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表。

  • 面向报文,不对应用程序提交的报文信息进行拆分或者合并

TCP和UDP的区别结论

  • 面向连接 VS 无连接

  • TCP是可靠的

  • TCP是有序的

  • TCP的速度比UDP的速度慢

  • UDP是轻量级的

HTTP

超文本传输协议HTTP主要特点:

  • 支持客户/服务器模式

  • 简单快速(只需要传送请求方法和路径,get和post)

  • 灵活(传输任意类型的数据对象)

  • 无连接

  • 无状态(对于事务的处理没有记忆能力)

请求结构

HTTP的请求结构

响应结构

HTTP的响应结构

请求/响应步骤

  1. 客户端连接到Web服务器

  2. 发送HTTP请求

  3. 服务器接收请求并返回HTTP响应

  4. 释放连接TCP连接

  5. 客户端浏览器解析HTML内容

浏览器输入URL经历的流程

  1. DNS解析,先接续本地,然后路由器,然后上级域名,然后顶级域名这样如果查到了不再继续向上查询。

  2. TCP连接

  3. 发送HTTP请求

  4. 服务器处理请求并返回HTTP报文

  5. 浏览器解析渲染页面,就是所谓的加载界面

  6. 连接结束

状态码

这里的状态码由于太多了,我进行了一个归类根据第一个数字进行归类如下

  • 1XX:指示信息–表示请求已经接受,继续处理

  • 2XX:成功–表示请求已被成功接受、理解、接受

  • 3XX:重定向–要完成请求必须进行跟进一步的操作

  • 4XX:客户端错误–请求有语法错误或请求无法实现

  • 5XX:服务器端错误–服务器未能实现合法的请求

根据我所遇到的有如下经常看到的进行了总结

  • 200 OK:正常返回信息

  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解

  • 401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

  • 403 Forbidden:服务器收到请求,但是拒绝提供服务

  • 404 Not Found:请求资源不存在或者输入了错误的URL

  • 500 Internal Server Error:服务器发生不可预期的错误

  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

GET和POST的区别

从三个层面来解答

  • 从Http报文的层面上来说,get请求的信息可以放在URL中,有些浏览器对于URL对于长度有限制,那么get的请求可能会丢失信息。对于大量的来说只能用POST

  • 从数据库的层面上来说,get请求是对数据库进行了操作,操作和结果是一致的,具有幂等性。对数据库的操作,但是没有改变数据库中的数据,get请求是做查询操作,所以不会改变数据库,所以也具有安全性 。

  • 其他层面:GET可以被缓存,可以存在浏览器的缓存中

Cookie和Session的区别

Cookie

是由服务器发给客户端的特殊信息,以文本的形式存放在客户端

客户端再次请求的时候,会把Cookie回发

服务器接收到后,会解析Cookie生成与客户端相对应的内容

Cookie的设置以及发送过程

Cookie的设置以及发送过程

Session

服务器的机制,在服务器上保存的信息

解析客户端请求并操作session id,按需要保存状态信息

Session实现方式有两种:

  • 用Cookie来实现

  • 使用URL回写来实现

它们之间的区别

  • Cookie数据存放在客户的浏览器上,Session数据放在服务器上

  • Session相对于Cookie更安全

  • 若考虑减轻服务器的负担,应当使用Cooke

HTTP和HTTPS的区别

HTTP和HTTPS的区别

图中的SLL(Security Sockets Layer,安全套接层)

  • 为网络通信提供安全及数据完整性的一种安全协议

  • 是操作系统对外的API,SSL3.0后更名为TLS

  • 采用身份验证和数据加密保证网络通信的安全和数据的完整性

HTTPS数据传输流程

  • 浏览器将支持的加密算法信息发送给服务器

  • 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器

  • 浏览器验证证书合法性,并结合证书公钥加密信息发送给服务器

  • 服务器使用私钥解密信息,验证哈希,加密响应消息回发浏览器

  • 浏览器解密响应消息,并对消息进行验真,之后进行加密交互数据

它们根本的区别

  • HTTPS需要到CA申请证书,HTTP不需要

  • HTTPS密文传输,HTTP明文传输

  • 连接方式不同,HTTPS默认使用443端口,HTTP使用80端口

  • HTTPS=HTTP+加密

Socket

Socket是对TCP/IP协议的抽象,是操作系统对外开放的接口

Socket是基于一种从打开到读和写再到关闭的这种模式去实现的,服务器和客户端各自维护一个文件再建立连接打开后,可以向自己文件写入内容供对方读取,或者读取对方内容,再通信结束时,就会关闭文件。

Socket通信流程

##关于SOCKET的一道面试题

编写一个网络应用程序,有客户端与服务器端,客户端向服务器发送一个字符串,服务器收到该字符串后将其打印到命令行上,然后向客户端返回该字符串的长度,最后,客户端输出服务器端返回的该字符串的长度,分别用TCP和UDP两种方法去实现

TCP实现具体代码如下

TCP Server(服务器)

import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception{
        // 创建socket,并将socket绑定到65000端口
        ServerSocket ss = new ServerSocket(65000);
        // 死循环,使得socket一直等待并处理客户端发送过来的请求
        while(true){
            // 监听65000端口,知道客户端返回连接信息才返回
            Socket socket = ss.accept();
            // 获取客户端的请求信息后,执行相关业务逻辑
            new LengthCalculator(socket).start();
        }
    }
}

LengthCalculator

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class LengthCalculator extends Thread{
    // 以socket为成员变量
    private final Socket socket;

    public LengthCalculator(Socket socket) {

        this.socket = socket;
    }
    @Override
    public void run(){
        try {
            // 获取socket的输出流
            OutputStream os = socket.getOutputStream();
            // 获取socket的输入流
            InputStream is = socket.getInputStream();
            int ch = 0 ;
            byte[] buff = new byte[1024];
            // buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度
            ch = is.read(buff);
            // 将接收流的byte数组转换成字符串,这里获取的内容是客户端发过来的字符串参数
            String content = new String(buff,0,ch);
            System.err.println(content);
            // 往输出流里写入获得的字符串的长度,并回发给客户端
            os.write(String.valueOf(content.length()).getBytes());
            // 这里不要忘了关闭输入输出流的socket
            os.close();
            is.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCP Client(客户端)

import jdk.internal.util.xml.impl.Input;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPClient
{
    public static void main(String[] args)throws Exception{
        // 创建socket,并制定连接的是本机的端口号为65000 的服务器socket
        Socket socket = new Socket("10.1.149.66",65000);
        // 获取输出流
        OutputStream os = socket.getOutputStream();
        // 获取输入流
        InputStream is = socket.getInputStream();
        // 将要传递给server的字符串参数转换成byte数组,并将数组写入到输出流当中
        os.write(new String("Hello world").getBytes());
        int ch = 0;
        byte[] buff = new byte[1024];
        //buff主要用来读取输入的内容,存成byte数组,ch主要用来获取读取数组的长度
        ch = is.read(buff);
        // 将接收流的byte数组转换成字符串,这里是从服务端回发回来的字符串参数的长度
        String content= new String(buff,0,ch);
        System.out.println(content);
        // 这里不要忘了关闭输入输出流的socket
        is.close();
        os.close();
        socket.close();
    }
}

UDP

UDP Server

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
    public static void main(String[] args)throws Exception{
            // 服务端接收客户端发送的数据包
            DatagramSocket socket = new DatagramSocket(65001);//监听端口号
            // 存储从客户端接收到的内容
            byte [] buff= new byte[100];
            DatagramPacket packet = new DatagramPacket(buff,buff.length);
            // 接收客户端发过来的内容,并将内容封装进DatagramPacket对象中
            socket.receive(packet);
            // 从DatagramPacket对象中获取到真正存储的数据
            byte[] data = packet.getData();
            // 将数据从二进制转换成字符串形式
            String content = new String(data,0,packet.getLength());
            System.err.println(content);
            // 将要发送给客户端的数据转换成二进制
            byte[] sendedContent = String.valueOf(content.length()).getBytes();
            // 服务端给客户端发送数据报
            // 从DatagramPacket对象中获取到数据的来源地址与端口号
            DatagramPacket packetToclient = new DatagramPacket(sendedContent,
                    sendedContent.length,packet.getAddress(),packet.getPort());
            socket.send(packetToclient);  // 发送数据给客户端
    }
}

UDP Client

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPClient {
    public static void main(String[] args) throws Exception{
        // 客户端发数据报给服务端
        DatagramSocket socket =new DatagramSocket();
        // 要发给服务端的数据
        byte[] buf = "Hello World".getBytes();
        // 将IP地址封装成InetAddress对象
        InetAddress address = InetAddress.getByName("10.1.149.66");
        // 将要发送给服务端的数据封装成DatagramPacket对象,需要填写上IP地址与端口号
        DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 65001);
        //发送数据给服务端
        socket.send(packet);

        //客户端接收服务端发过来的数据报
        byte[] data = new byte[100];
        // 创建DatagramPacket对象用来存储服务端发过来的数据
        DatagramPacket receivedPacket = new DatagramPacket(data,data.length);
        // 将接收到的数据存储到DatagramPacket对象中
        socket.receive(receivedPacket);
        // 将服务器端发送过来的数据取出来并打印到控制台
        String content = new String(receivedPacket.getData(),0,receivedPacket.getLength());
        System.err.println(content);

    }
}


小伙伴们想知道最后的结果自己亲身动手打一下代码就知道其中的原理喽。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值