关于网络和Linux常见的十道面试题汇总

TCP丢失的消息会一直重传吗?说一下TCP的超时重传策略是啥?

造成消息丢失和超时重传的场景有以下两种:

  1. 发送消息时丢失
  2. ACK确认消息发送丢失

无论哪种情况,**TCP不会一直重传丢失的消息,因为这样如果对方真正的下线,会造成系统资源浪费。**所以TCP设计了两种重传策略:

  1. 动态重传时间:每次传递时间翻倍,例如第一次1s,第二次2s,第四次4s
  2. 最大重传次数:TCP如果超过一定的重试次数,那么就会强制断开连接,不会继续重传

什么是TCP粘包问题?如何解决?

TCP粘包和半包是数据传输中比较常见的问题。所谓的粘包问题就是指在数据传输的时候,在一条消息中读取到了另一条消息的部分数据 ,如下图:

半包是指接收端只收到了部分的数据,而非完整的数据的情况 ,如下图:

大部分情况下我们都把粘包问题和半包问题看成同一个问题,所以下文就用粘包问题来替代粘包和半包问题

为什么会有粘包问题

粘包问题发生在TCP/IP协议中,因为TCP是面向连接的传输协议,它是以流的形式传输数据的,而流数据是没有明确开始和结尾的边界的,所以就会出现粘包问题

粘包问题演示

接下来我们用代码来演示一下粘包和半包问题,为了演示的直观性,我会设置两个角色:

  • 服务器端用来接收消息
  • 客户端用来发送一段固定的消息

服务端代码实现:

java 复制代码
import java.io.*;
import java.net.*;

public class Server {
    private static final int BYTE_LENGTH = 20;

    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(8888);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (InputStream inputStream = clientSocket.getInputStream()) {
            while (true) {
                // 循环获取客户端发送的信息
                byte[] bytes = new byte[BYTE_LENGTH];
                // 读取客户端发送的信息
                int count = inputStream.read(bytes, 0, BYTE_LENGTH);
                if (count > 0) {
                    // 成功接收到有效消息并打印
                    System.out.println("接收到客户端的信息是:" + new String(bytes, 0, count));
                }
            }
        }
    }
}

客户端代码实现:

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class Client {
    public static void main(String[] args) throws IOException {
        String serverAddress = "127.0.0.1";
        int port = 8888;

        try (Socket socket = new Socket(serverAddress, port);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
            String message = "hello,world";
            OutputStream outputStream= socket.getOutputStream();
            for (int i = 0; i < 10; i++) {
                outputStream.write(message.getBytes());
            }
        }
    }
}

程序执行结果:

此时我们发现出现了粘包问题,正常应该是直接输出10次hello world 才对

解决方案

粘包问题的常见解决方案有以下三种:

  1. 固定数据大小:发送方和接收方固定发送消息的大小,当字符长度不够的时候用空字符弥补,有了固定大小就知道每条消息的边界了
  2. 自定义数据协议(定义数据边界):在TCP协议的基础上封装上一层自定义数据协议,在自定义的数据协议中,包含数据头(存储数据的大小)和数据的具体内容,这样服务端的得到的数据头就可以知道数据的具体长度,也就没有粘包问题
  3. 以特殊字符结尾:比如以"/n"字符结尾,这样就可以直到数据的具体边界,可以避免粘包问题(推荐使用)
解决方案一:固定数据大小

收、发固定大小的数据,服务端实现代码

java 复制代码
import java.io.*;
import java.net.*;

public class Server {
    private static final int BYTE_LENGTH = 1024;

    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器
        ServerSocket serverSocket = new ServerSocket(8888);
        // 获取客户端连接
        Socket clientSocket = serverSocket.accept();
        // 得到客户端发送的流对象
        try (InputStream inputStream = clientSocket.getInputStream()) {
            while (true) {
                // 循环获取客户端发送的信息
                byte[] bytes = new byte[BYTE_LENGTH];
                // 读取客户端发送的信息
                int count = inputStream.read(bytes);
                if (count > 0) {
                    // 成功接收到有效消息并打印
                    System.out.println("接收到客户端的信息是:" + new String(bytes, 0, count));
                }
            }
        }
    }
}

客户端实现代码:

java 复制代码
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class Client {
    private static final int BYTE_LENGTH = 1024;
    public static void main(String[] args) throws IOException, InterruptedException {
        String serverAddress = "127.0.0.1";
        int port = 8888;
        String message = "hello,world";
        try (Socket socket = new Socket(serverAddress, port)) {

            OutputStream outputStream= socket.getOutputStream();
            byte[] bytes = new byte[BYTE_LENGTH];
            int idx= 0;
            for(byte b:message.getBytes()){
                bytes[idx]= b;
                idx++;
            }
            for (int i = 0; i < 10; i++) {
                outputStream.write(bytes,0,BYTE_LENGTH);
            }
        }
    }
}

运行结果:

后面是字符编码的问题

优缺点分析

从以上代码可以看出,虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担,因此不是理想的解决方案

解决方案二:自定义请求协议

这种解决方案的实现思路是将请求的数据封装成两部分:消息头(发送的数据大小)+消息体(发送的具体数据),如下图:

此解决方案的实现为以下三部分:

  1. 编写一个消息的封装类
  2. 编写客户端
  3. 编写服务器端

消息的封装类:

java 复制代码
import java.nio.charset.StandardCharsets;

public class CustomProtocol {
    private static final int HEAD_SIZE = 8; // 假设消息头固定为8个字节

    public static byte[] toBytes(String context) {
        // 协议体 byte 数组
        byte[] bodyByte = context.getBytes(StandardCharsets.UTF_8);
        int bodyByteLength = bodyByte.length;

        // 最终封装对象
        byte[] result = new byte[HEAD_SIZE + bodyByteLength];

        // 借助 NumberFormat 将int 转换为 byte[]
        NumberFormat numberFormat = NumberFormat.getNumberInstance();
        numberFormat.setMinimumIntegerDigits(HEAD_SIZE);
        numberFormat.setGroupingUsed(false);

        // 协议头 byte 数组
        byte[] headByte = numberFormat.format(bodyByteLength).getBytes();

        // 封装协议头
        System.arraycopy(headByte, 0, result, 0, HEAD_SIZE);

        // 封装协议体
        System.arraycopy(bodyByte, 0, result, HEAD_SIZE, bodyByteLength);
        
        return result;
    }
    public int getHeader(InputStream inputStream) throws IOException {
        int result = 0;
        byte[] bytes = new byte[HEAD_SIZE];
        inputStream.read(bytes, 0, HEAD_SIZE); // 得到消息体的字节长度
        result = Integer.valueOf(new String(bytes));
        return result;
    }
}

客户端代码:

java 复制代码
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Random;

public class MySocketClient {
    public static void main(String[] args) throws IOException {
        // 启动 Socket 并尝试连接服务器
        Socket socket = new Socket("127.0.0.1", 9093);

        // 发送消息合集(随机发送一条消息)
        final String[] messages = {"hello world"};

        // 创建协议封装对象
        SocketPacket socketPacket = new SocketPacket();
        
        try (OutputStream outputStream = socket.getOutputStream()) {
            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 随机发送一条消息
                String msg = messages[new Random().nextInt(messages.length)];
                
                // 将内容封装为:协议头+协议体
                byte[] bytes = socketPacket.toBytes(msg);
                
                // 发送消息
                outputStream.write(bytes, 0, bytes.length);
                outputStream.flush();
            }
        }
    }
}

服务器端代码:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MySocketServer {
    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器端
        ServerSocket serverSocket = new ServerSocket(9093);

        // 使用线程池处理更多的客户端
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        while (true) {
            // 获取客户端连接
            Socket clientSocket = serverSocket.accept();

            // 客户端消息处理
            threadPool.submit(() -> {
                processMessage(clientSocket);
            });
        }
    }

    // 客户端消息处理
    private static void processMessage(Socket clientSocket) {
        // Socket 封装对象
        SocketPacket socketPacket = new SocketPacket();

        // 获取客户端发送的消息对象
        try (InputStream inputStream = clientSocket.getInputStream()) {
            while (true) {
                // 获取消息头(也就是消息体的长度)
                int bodyLength = socketPacket.getHeader(inputStream);

                // 消息体 byte 数组
                byte[] bodyByte = new byte[bodyLength];

                // 每次实际读取字节数
                int readCount = 0;

                // 消息体赋值下标
                int bodyIndex = 0;

                // 循环接收消息头中定义的长度
                while (bodyIndex <= (bodyLength - 1) && (readCount = inputStream.read(bodyByte, bodyIndex, bodyLength)) != -1) {
                    bodyIndex += readCount;
                }

                bodyIndex = 0;

                // 成功接收到客户端的消息并打印
                System.out.println("接收到客户端的信息:" + new String(bodyByte));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

优缺点分析:

此解决方案虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案

解决方案三:特殊字符结尾

以特殊字符结尾就可以知道流的边界了,它的具体实现是:使用Java 中自带的 BufferedReader 和Bufferedwriter ,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n 来结尾,读取的时候使用 readLine按行来读取数据,这样就知道流的边界了,从而解决了粘包的问题

服务器端代码:

java 复制代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ServerSocketV3 {
    public static void main(String[] args) throws IOException {
        // 创建 Socket 服务器端
        ServerSocket serverSocket = new ServerSocket(9092);

        // 使用线程池处理更多的客户端
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        while (true) {
            // 获取客户端连接
            Socket clientSocket = serverSocket.accept();

            // 消息处理
            threadPool.submit(() -> {
                processMessage(clientSocket);
            });
        }
    }

    // 消息处理
    private static void processMessage(Socket clientSocket) {
        // 获取客户端发送的消息流对象
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
            while (true) {
                // 按行读取客户端发送的消息
                String msg = bufferedReader.readLine();
                if (msg != null) {
                    // 成功接收到客户端的消息并打印
                    System.out.println("接收到客户端的信息:" + msg);
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

客户端代码:

java 复制代码
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class ClientSocketV3 {
    public static void main(String[] args) throws IOException {
        // 启动 Socket 并尝试连接服务器
        Socket socket = new Socket("127.0.0.1", 9092);
        final String message = "hello world";

        // 发送消息
        try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
            // 给服务器端发送 10 次消息
            for (int i = 0; i < 10; i++) {
                // 注意:结尾的\n 不能省略,它表示按行写入
                bufferedWriter.write(message + "\n");

                // 刷新缓冲区(此步骤不能省略)
                bufferedWriter.flush();
            }
        }
    }
}

运行结果:

优缺点分析:

以特殊符号作为粘包的解决方案的最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性

TCP为什么要四次挥手?说一下四次挥手的流程?

四次挥手的具体流程如下:

  1. 用户发送FIN包:客户端发送一个FIN包,其中FIN标志位为1,表示客户端希望关闭连接
  2. 服务器发送ACK包:服务器收到客户端的FIN包,向客户端发送一个ACK包,其中ACK标志位为1,表示服务器已经收到了客户端的请求,并将确认号(ACK)设置为发送的序列号+1
  3. 服务器发送FIN包:服务器在发送完ACK包之后,也会发送一个FIN包,其中FIN标志位为1,表示服务器也希望关闭连接
  4. 客户端发送ACK包:客户端收到服务器的FIN包之后,向服务器发送一个ACK包,其中ACK标志位为1,表示客户端已经收到了服务器的请求,并将确认号设置为服务器发送的序列号+1

TCP进行四次挥手的作用主要是有两点:

  1. 确保所有数据都被传输完成:在关闭连接之前,双方都有可能还有数据需要传输,因此需要通过四次挥手来确保所有数据已经传输完成
  2. 确保双方都能正常关闭连接:四次挥手的过程中,客户端和服务器都需要发送 FIN 和 ACK 包,以确保双都能正确地关闭连接,避免连接一方关闭而另一方仍然处于连接状态

TCP四次挥手为什么要等两个MSL(最大生存时间)?

四次挥手时发送者最后一次等待时间是两个MSL(最大生存时间),目的就是确保最后一个ACK的可靠传输,在四次挥手的最后一步,接收方发送一个ACK给发送方,表示接受到了关闭连接的请求。发送方需要等待一段时间,确保这个ACK报文额能够可靠的传输到接受方。如果发送方在等待期间收到接收方的重传请求,可以重发ACK

TCP和UDP有什么区别?

TCP和UDP都是传输层的重要协议,但是它们存在以下几点不同:

  1. 连接机制不同:TCP是面向连接的协议,需要在客户端和服务器之间建立一个稳定的连接,然后再进行数据传输;而UDP是无连接协议,数据包可以直接发送给目标主机,不需要事先建立连接
  2. 数据传输方式不同:TCP采用可靠的数据传输方式,即在传输的过程中使用序号、确认号和重传机制等控制手段来保证数据的可靠传输;而UDP采用不可靠的数据传输方式,数据包可能会丢失或重复,不提供数据的可靠性保障
  3. 数据传输效率不同:由于 TCP 需要进行连接、序号确认等额外的数据包传输,因此在数据传输效率方面相对于 UDP 要低一些
  4. 数据大小限制不同:UDP 对数据包的大小有限制,最大只能传输 64KB 的数据,而 TCP 的数据包大小没有限制
  5. 应用场景不同:TCP适用于要求数据传输可靠高的场景,如网页浏览、文件下载、电子邮件等;而UDP适用实时性要求高的场景,如视频会议、在线游戏等

Linux上如何查询某个端口是否被占用?

在Linux上,你可以使用以下几种方法来查询某个端口是否被占用:

  1. 使用netstat命令:

    复制代码
    netstat -tuln | grep <端口号>

    这个命令会列出当前正在运行的所有TCP和UDP端口,并过滤出指定端口是否被占用。如果端口被占用,会显示对应的PID和程序名称。

  2. 使用ps命令结合grep命令:

    复制代码
    ps -aux | grep <端口号>

    这个命令会列出所有正在运行的进程,并过滤出包含指定端口号的进程信息。通过查看对应的PID和程序名称,可以判断该端口是否被占用。

Linux常见目录有哪些?

Linux 系统中常见的目录有以下几个

  1. /bin:存放常用的命令二进制可执行文件,如 ls、grep 等
  2. /etc:系统配置文件目录,存放系统管理和配置文件
  3. /home:普通用户的主目录,在个人使用时通常将用户目录放在这里
  4. /root:超级管理员的主目录
  5. /usr:存放应用程序和文件。
    1. /usr/bin:应用程序目录,与/bin类似,但存放不常用的应用程序
    2. /usr/lib:共享库目录,存放系统和应用程序所需的共享库文件
    3. /usr/share:共享文件目录,不同系统和程序共享的数据文件存放这里
  6. /var:放系统运行过程中的各种变化文件,如日志
  7. /opt:可选应用软件包安装目录
  8. /tmp:临时文件目录

Linux中的常用命令有哪些?

Linux常用命令如下:

  1. ls: 列出当前目录下的文件和子目录信息。

    • 使用方式:ls -options file/directory
    • 示例:ls /home/user 列出 /home/user 目录下的所有文件信息。
  2. cd: 切换当前工作目录。

    • 使用方式:cd directory
    • 示例:cd Documents 切换到 Documents 目录,使用 cd ... 返回上一级目录。
  3. pwd: 显示当前工作目录的绝对路径。

    • 使用方式:pwd
  4. mkdir: 创建新的目录。

    • 使用方式:mkdir directory
    • 示例:mkdir javacn 创建 "javacn" 文件夹。
  5. touch: 创建新文件或者更新已存在文件的时间戳。

    • 使用方式:touch file
    • 示例:touch newfile.txt
  6. rm: 删除指定的文件或目录。

    • 使用方式:rm -options file/directory
    • 示例:rm file.txt(删除单个文件)、rm -r directory(递归删除目录及其内容)。
  7. cp: 复制文件或目录。

    • 使用方式:cp -options source destination
    • 示例:cp file1.txt file2.txt(复制文件)、cp -r dir1 dir2(复制目录)
  8. mv: 移动或重命名文件或目录。

    • 使用方式:mv -options source destination
    • 示例:mv file.txt newfile.txt(重命名文件)、mv file.txt /dir/(移动文件到指定目录)。
  9. find: 在指定目录下查找满足条件的文件。

    • 使用方式:find path expression
    • 示例:find . -name "*.txt" 在当前目录及其子目录下查找扩展名为 .txt 的文件。
  10. grep: 搜索文件中匹配特定模式的行。

    • 使用方式:grep options pattern file(s)
    • 示例:grep "hello" file.txt 在 file.txt 中查找包含 "hello" 的行。

你可以使用命令的 --help 参数或查看相关命令的手册页(man 页)来获取更详细的信息,比如 man ls 可以查看 ls 命令的手册页

Linux中如何查看日志信息?

在 Linux 系统中查看日志一般需要以下两个步骤:

第一步:找到存放日志的位置。 日志文件通常存放在 /var/log 目录及其子目录中,不同的服务和应用程序可能会有自己独立的日志文件。常见的系统日志文件包括 /var/log/messages/var/log/syslog/var/log/dmesg 等,而特定服务或应用程序的日志文件则位于其对应的目录下,例如 Apache Web 服务器的日志文件通常位于 /var/log/apache2/ 目录下

第二步:使用以下命令查询具体的日志信息:

  1. cat: 查看某个日志文件中的所有内容

    • 使用示例:cat file.txt 显示 file.txt 文件的所有内容
  2. head: 查看某个日志文件中开头的内容

    • 使用示例:head file.txt 显示 file.txt 文件的前 10 行内容。也可以指定查看的行数,例如,head -n 5 file.txt 显示 file.txt 文件的前 5 行内容
  3. tail: 查看某个日志文件中末尾的内容,用法和 head 类似

    • 使用示例:tail file.txt 显示 file.txt 文件的最后 10 行内容
  4. more: 分页方式显示文件内容,常用来查看大型日志文件

    • 使用示例:more file.txt 分页查看 file.txt 文件内容,一次只显示一屏内容,用户通过空格键或回车键翻看下一页,按 q 退出查看

Linux中如何查看负载情况?什么情况算负载比较高?

在Linux系统中,可以使用以下几种方式来查看系统的负载情况:

  1. 使用 uptime 命令:

    • uptime 命令可以显示系统的运行时间以及平均负载。
    • 示例:uptime
  2. 使用 top 命令:

    • top 命令可以实时显示系统的各项指标,包括负载情况、CPU利用率、内存使用情况等。
    • 示例:top

单核负载 0.7 以下是健康的情况,反之大于 0.7 表示负载比较高,对应四核健康的情况就是 0.7*4=2.8 是健康的情况,大于 2.8 就表示负载比较高

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 天前
Linux 11 动态监控指令top
linux
网络研究院4 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展