2024年9月

1.简介

在此之前,宏哥已经介绍和讲解过Wireshark的启动界面。但是很多初学者还会碰到一个难题,就是感觉wireshark抓包界面上也是同样的问题很多东西不懂怎么看。其实还是挺明了的宏哥今天就单独写一篇对其抓包界面进行详细地介绍和讲解一下。

2.Wireshak抓包界面概览

通过上一篇我们知道如何使Wireshark处于抓包状态,进行抓包。其界面显示如下图所示:

Wireshark网络封包分析软件 主要分为这几个界面:


Display Filter (显示过滤器):用于过滤。


Packet List Pane (封包列表):显示捕获到的封包, 有源地址和目标地址,端口号。 颜色不同代表抓取封包的协议不同。


Packet Details Pane (封包详细信息),:显示封包中的字段。


Dissector Pane (16进制数据)


Miscellanous (地址栏,杂项)

2.1显示过滤器

Display Filter(显示过滤器),  用于设置过滤条件进行数据包列表过滤。菜单路径:Analyze --> Display Filters。显示过滤器用于查找捕捉记录中的内容。请不要将捕捉过滤器和显示过滤器的概念相混淆。如下图所示:

2.2封包列表

Packet List Pane(数据包列表), 显示全部已经捕获到的数据包,每个数据包包含编号,时间戳,源地址,目标地址,协议,长度,以及数据包信息。 不同协议的数据包使用了不同的颜色区分显示。

2.3封包详细信息

Packet Details Pane(数据包详细信息), 在数据包列表中选择指定数据包,在数据包详细信息中会显示数据包的所有详细信息内容。数据包详细信息面板是最重要的,用来查看协议中的每一个字段。各行信息分别为

(1)Frame:   物理层的数据帧概况

(2)Ethernet II: 数据链路层以太网帧头部信息

(3)Internet Protocol Version 4: 互联网层IP包头部信息

(4)Transmission Control Protocol:  传输层T的数据段头部信息,此处是TCP

(5)Hypertext Transfer Protocol:  应用层的信息,此处是HTTP协议

由此可见,Wireshark 对 HTTP 协议数据包进行解析,显示了 HTTP 协议的层次结构。

2.3.1Frame

物理层数据帧概况。如下图所示:


2.3.2Ethernet II

数据链路层以太网帧头部信息。如下图所示:

2.3.3Internet Protocol Version 4

互联网层IP包头部信息。如下图所示:

IP包头。如下图所示:

2.3.4Transmission Control Protocol

传输层数据段头部信息,此处是TCP协议。如下图所示:

2.3.5Hypertext Transfer Protocol

应用层信息,此处是HTTP协议。

2.4 十六进制数据

Dissector Pane(数据包字节区)。

2.5状态栏

MISCELLANOUS(杂项),主要显示,包文件明,配置文件名,以及打开文件有多少个分组,当前显示了多少个分组(例如执行条件过滤后,只显示被过滤规则命中的分组)。

3.网络七层协议

3.1OSI

OSI是一个开放性的通信系统互连参考模型,它是一个定义得非常好的协议规范。OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端,点到点的数据流。

3.2OSI和封包详细信息的对应

下面跟随宏哥一起来看一下,Wireshark抓包查看网络请求封包中的每一个字段所对应的OSI。 以 HTTP 协议数据包为例,了解该数据包的层次结构。在 Packet List 面板中找到一个 HTTP 协议数据包。如下图所示:

用户对数据包分析就是为了查看包的信息,展开每一层,可以查看对应的信息。例如,查看数据链路层信息,展开 Ethernet II 层,显示信息如下:

Ethernet II, Src: Tp-LinkT_46:70:ba (ec:17:2f:46:70:ba), Dst: Giga-Byt_17:cf:21 (50:e5:49:17:cf:21)
Destination: Giga-Byt_17:cf:21 (50:e5:49:17:cf:21) #目标MAC地址
Source: Tp-LinkT_46:70:ba (ec:17:2f:46:70:ba) #源MAC地址
Type: IPv4 (0x0800)

显示的信息包括了该数据包的发送者和接收者的 MAC 地址(物理地址)。

可以以类似的方法分析其他数据包的层次结构。

4.颜色区分Wireshark网络封包分析软件抓取到的不同网络协议

说明:
数据包列表区中不同的网络协议使用了不同的颜色区分

协议颜色标识定位在菜单栏 View -->  Coloring Rules 。(视图-->着色规则)如下图所示:

5.小结

好了,今天主要是关于Wireshark抓包界面详解。到此宏哥就将Wireshark抓包界面讲解和分享完了,是不是很简单了。今天时间也不早了,就到这里!感谢您耐心的阅读~~

问题

给定一个二分图,左部有
\(n\)
个点,右部有
\(m\)
个点,边
\((u_i, v_j)\)
的边权为
\(A_{i,j}\)
。求该二分图的最大权完美匹配。

转化

问题可以写成线性规划的形式,设
\(f_{i, j}\)
表示匹配中是否有边
\((u_i, v_j)\)
,求

\[\begin{gather*}
\text{maximize} \quad & \sum_{i=1}^n \sum_{j=1}^m f_{i, j} \times A_{i, j} \\
\text{subject to} \quad & \sum_{i=1}^n f_{i, j} = 1 \quad \forall j \in [1, m] \\
& \sum_{j=1}^m f_{i,j}=1\quad\forall i \in [1, n] \\
& f_{i,j} \ge 0 \quad\forall i, j
\end{gather*}
\]

转为对偶问题:

\[\begin{gather*}
\text{minimize} \quad & \sum_{i=1}^n hu_i + \sum_{j=1}^m hv_i \\
\text{subject to} \quad & hu_i + hv_i \ge A_{i,j} \quad\forall i, j
\end{gather*}
\]

在这个问题中,
\(hu\)

\(hv\)
又称作“顶标”。

分析

根据互补松弛定理,如果
\(f_{i,j}=1\)
,则有
\(hu_i+hv_j=A_{i,j}\)
。这给出了判定一组顶标是否最优的方式:

一组顶标
\(hu, hv\)
最优,当且仅当子图
\(H = \left\{(i, j) \mid hu_i + hv_j = A_{i,j}\right\}\)
存在完美匹配。

做法

首先给出一个满足
\(hu_i+hv_j \ge A_{i,j}\)
的顶标(不一定最优)(例如,令
\(hu_i = hv_j = +\infty\)
),并维护对应的匹配。然后尝试在不破坏条件的情况下修改顶标,使得匹配可以被增广。

具体而言,遍历
\(i\)

\(1\)

\(n\)
,每次尝试将
\(i\)
加入到匹配中(类似于求二分图最大匹配的匈牙利算法)。我们可以求出

\(i\)
为根

的交错树,如果已经存在增广路,那么直接增广便是,否则我们需要修改顶标来使交错树生长。设交错树中的左部点集为
\(S\)
,右部点集为
\(T\)
,那么必有
\(|S| = |T| + 1\)
(交错树的性质)。令
\(\Delta = \min\{hu_i + hv_j - A_{i,j} \mid i \in S \land j \notin T\}\)
,那么将
\(S\)
中的点顶标减去
\(\Delta\)
,将
\(T\)
中的点顶标加上
\(\Delta\)
,可以验证得到的新的顶标依然满足
\(hu_i+hv_j \ge A_{i,j}\)
的限制,且原图中的匹配一定包含于对应的新图
\(H'\)
中,此外,新图中至少增加了一条边,这使得我们的交错树可以继续生长,直到找到增广路为止。

代码(
洛谷 P6577

#define DEBUG 0
#include <cstdio>
#include <cassert>
#include <vector>
template <class T> using Arr = std::vector<T>;
#define int long long
const int INF = 1e8;

signed main() {
	int n, m;
	scanf("%lld%lld", &n, &m);
	struct edge_t {
		int v, w;
	};
	Arr<Arr<edge_t>> e(2 * n);
	for (int i = 0; i < m; ++i) {
		int l, r, w;
		scanf("%lld%lld%lld", &l, &r, &w);
		--l; r += n - 1;
		e[l].push_back({r, w});
		e[r].push_back({l, w});
	}

	Arr<int> match(2 * n, -1), h(2 * n, INF);
	for (int i = 0; i < n; ++i) {
		Arr<int> vis(2 * n, false);    // 是否在当前求出的交错树中,即 $S \cup T$ 
		Arr<int> upd(2 * n, n * INF);  // 最小的 Δ 值                             
		Arr<int> from(2 * n, -1);      // 维护增广路所用,即交错树上的父亲        
		int p = i;
		vis[p] = true;
		int d, dp;  // Δ 及其对应的 j
		while (true) {
			d = n * INF, dp = -1;

			// 求出 Δ
			for (auto [to, w] : e[p])
				if (!vis[to]) {
					int delta = h[p] + h[to] - w;
					if (delta < upd[to])
						upd[to] = delta, from[to] = p;
				}
			for (int j = n; j < 2 * n; ++j)
				if (!vis[j] && upd[j] < d && from[j] != -1)
					d = upd[j], dp = j;
			assert(~dp);


			// 修改顶标
			h[i] -= d;
			for (int j = n; j < 2 * n; ++j)
				if (vis[j])
					h[j] += d, h[match[j]] -= d;
				else
					upd[j] -= d;

			// 找到增广路
			if (match[dp] == -1)
				break;

			// 生长交错树
			vis[dp] = true;
			vis[match[dp]] = true;
			p = match[dp];
		}

		// 增广
		while (~dp) {
			match[dp] = from[dp];
			int tmp = match[from[dp]];
			match[from[dp]] = dp;
			dp = tmp;
		}
	}

	long long ans = 0;
	for (int i = 0; i < 2 * n; ++i)
		ans += h[i];
	printf("%lld\n", ans);
	for (int i = n; i < 2 * n; ++i)
		printf("%lld ", match[i] + 1);
	puts("");
	return 0;
}

参考资料

首先,这个问题考察的是你对线程池 execute 方法和 submit 方法的理解,在 Java 线程池的使用中,我们可以通过 execute 方法或 submit 方法给线程池添加任务,但如果线程池中的程序在执行时,遇到了未处理的异常会怎么呢?接下来我们一起来看。

1.execute方法

execute 方法用于提交一个不需要返回值的任务给线程池执行,它接收一个 Runnable 类型的参数,并且不返回任何结果。

它的使用示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecuteDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        // 使用 execute 方法提交任务
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("Task running in " + Thread.currentThread().getName());
                try {
                    // 模拟任务执行
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.err.println("Task was interrupted");
                }
                System.out.println("Task finished");
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

2.submit方法

submit 方法用于提交一个需要返回值的任务(Callable 对象),或者不需要返回值但希望获取任务状态的任务(Runnable 对象,但会返回一个 Future 对象)。

它接收一个 Callable 或 Runnable 类型的参数,并返回一个 Future 对象,通过该对象可以获取任务的执行结果或检查任务的状态。

2.1 提交Callable任务

示例代码如下:

import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
public class SubmitCallableDemo {  
    public static void main(String[] args) {  
        // 创建一个固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(2);  
  
        // 提交一个 Callable 任务给线程池执行  
        Future<String> future = executorService.submit(new Callable<String>() {  
            @Override  
            public String call() throws Exception {  
                Thread.sleep(2000); // 模拟任务执行时间  
                return "Task's execution result";  
            }  
        });  
  
        try {  
            // 获取任务的执行结果  
            String result = future.get();  
            System.out.println("Task result: " + result);  
        } catch (InterruptedException | ExecutionException e) {  
            e.printStackTrace();  
        }  
  
        // 关闭线程池  
        executorService.shutdown();  
    }  
}

2.2 提交Runnable任务

提交 Runnable 任务并获取 Future 对象,示例代码如下:

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
public class SubmitRunnableDemo {  
    public static void main(String[] args) {  
        // 创建一个固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(2);  
  
        // 提交一个 Runnable 任务给线程池执行,并获取一个 Future 对象  
        Future<?> future = executorService.submit(new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("Task is running in thread: " + Thread.currentThread().getName());  
            }  
        });  
  
        // 检查任务是否完成(这里只是为了示例,实际使用中可能不需要这样做)  
        if (future.isDone()) {  
            System.out.println("Task is done");  
        } else {  
            System.out.println("Task is not done yet");  
        }  
  
        // 关闭线程池  
        executorService.shutdown();  
    }  
}

3.遇到未处理异常

线程池遇到未处理的异常执行行为和添加任务的方法有关,
也就是说 execute 方法和 submit 方法在遇到未处理的异常时执行行为是不一样的

3.1 execute方法遇到未处理异常

示例代码如下:

import java.util.concurrent.*;

public class ThreadPoolExecutorExceptionTest {
    public static void main(String[] args)  {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                1000,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(100));
        // 添加任务一
        executor.execute(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        // 添加任务二
        executor.execute(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
    }
}

以上程序的执行结果如下:

从上述结果可以看出,线程池中的核心和最大线程数都为 1 的情况下,到遇到未处理的异常时,执行任务的线程却不一样,这说明了:
当使用 execute 方法时,如果遇到未处理的异常,会抛出未捕获的异常,并将当前线程进行销毁

3.2 submit方法遇到未处理异常

然而,当我们将线程池的添加任务方法换成 submit() 之后,执行结果又完全不同了,以下是示例代码:

import java.util.concurrent.*;

public class ThreadPoolExecutorExceptionTest {
    public static void main(String[] args)  {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                1000,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(100));
        // 添加任务一
       Future<?> future = executor.submit(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        // 添加任务二
        Future<?> future2 =executor.submit(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        try {
            future.get();
        } catch (Exception e) {
            System.out.println("遇到异常:"+e.getMessage());
        }
        try {
            future2.get();
        } catch (Exception e) {
            System.out.println("遇到异常:"+e.getMessage());
        }
    }
}

以上程序的执行结果如下:

从上述结果可以看出,submit 方法遇到未处理的异常时,并
将该异常封装在 Future 的 get 方法中,而不会直接影响执行任务的线程,这样线程就可以继续复用了

小结

线程池在遇到未处理的异常时,不同添加任务的方法的执行行为是不同的:

  • execute 方法
    :遇到未处理的异常,线程会崩溃,并打印异常信息。
  • submit 方法
    :遇到未处理的异常,线程本身不会受到影响(线程可以复用),只是将异常信息封装到返回的对象 Future 中。

课后思考

为什么遇到未处理的异常时,execute 方法中的线程会崩溃,而 submit 方法中的线程却可以复用?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

前端部分(Vue 3 + Element Plus)

1. 修改
MPS002HList.vue
(主生产计划列表)

a. 添加查询表单

在模板中添加查询表单,包含产品料号、品名、规格和年月的输入项。

<template>
  <div>
    <!-- 查询表单 -->
    <el-form :inline="true" :model="filters" class="demo-form-inline">
      <el-form-item label="产品料号">
        <el-input v-model="filters.bo_no" placeholder="请输入产品料号"></el-input>
      </el-form-item>
      <el-form-item label="品名">
        <el-input v-model="filters.item_name" placeholder="请输入品名"></el-input>
      </el-form-item>
      <el-form-item label="规格">
        <el-input v-model="filters.item_spec" placeholder="请输入规格"></el-input>
      </el-form-item>
      <el-form-item label="年月">
        <el-date-picker
v
-model="filters.mps_ym"type="month"placeholder="选择年月"format="yyyy-MM"value-format="yyyy-MM" /> </el-form-item> <el-form-item> <el-button type="primary" @click="fetchMpsList">查询</el-button> <el-button @click="resetFilters">重置</el-button> </el-form-item> </el-form> <!-- 生产计划列表 --> <el-table :data="mpsList" style="width: 100%" v-loading="loading"> <el-table-column prop="mps_no" label="单号" width="180"> <template #default="{ row }"> <el-button type="text" @click="showMpsDetails(row.mps_no)">{{ row.mps_no }}</el-button> </template> </el-table-column> <el-table-column prop="mps_date" label="单据时间" width="180" /> <el-table-column prop="fa_no_name" label="厂别" width="180" /> <el-table-column prop="bo_no" label="产品料号" width="180" /> <el-table-column prop="bo_no_name" label="品名" width="180" /> <el-table-column prop="bo_no_spec" label="规格" width="180" /> <el-table-column prop="mps_ym" label="年月" width="100" /> <el-table-column prop="mps_qty" label="数量" width="100" /> </el-table> <!-- 分页 --> <el-pagination
v
-if="mpsList.length"background
:current
-page="page":page-size="pageSize"layout="total, prev, pager, next":total="total"@current-change="handlePageChange" /> <!-- 详情对话框 --> <el-dialog :visible.sync="showDetails" width="80%"> <template #header> <h3>主生产计划详情</h3> </template> <MPS002HDetail :mps_no="selectedMpsNo" /> </el-dialog> </div> </template>

b. 修改脚本部分


setup
函数中,添加
filters
数据,并修改
fetchMpsList
函数以包含查询参数。

<script>import { ref, onMounted } from'vue';
import { getMPS002 } from
'@/api/mpsApp/MPS002HModel';
import MPS002HDetail from
'./MPS002HDetail.vue';

export
default{
components: { MPS002HDetail },
setup() {
const mpsList
=ref([]);
const page
= ref(1);
const pageSize
= ref(10);
const total
= ref(0);
const loading
= ref(false);
const showDetails
= ref(false);
const selectedMpsNo
= ref(null);

const filters
=ref({
bo_no:
'',
item_name:
'',
item_spec:
'',
mps_ym:
'',
});

const fetchMpsList
= async () =>{
loading.value
= true;try{
const params
={
page: page.value,
page_size: pageSize.value,
bo_no: filters.value.bo_no,
item_name: filters.value.item_name,
item_spec: filters.value.item_spec,
mps_ym: filters.value.mps_ym,
};
const response
=await getMPS002(params);
mpsList.value
=response.data.results;
total.value
=response.data.count;
}
catch(error) {
console.error(
'Error fetching MPS002 list:', error);
}
finally{
loading.value
= false;
}
};

const resetFilters
= () =>{
filters.value
={
bo_no:
'',
item_name:
'',
item_spec:
'',
mps_ym:
'',
};
fetchMpsList();
};

const showMpsDetails
= (mps_no) =>{
selectedMpsNo.value
=mps_no;
showDetails.value
= true;
};

const handlePageChange
= (newPage) =>{
page.value
=newPage;
fetchMpsList();
};

onMounted(fetchMpsList);
return{
mpsList,
page,
pageSize,
total,
loading,
showDetails,
selectedMpsNo,
filters,
fetchMpsList,
resetFilters,
showMpsDetails,
handlePageChange,
};
},
};
</script>

2. 修改
MPS002D1List.vue
(物料需求明细列表)

a. 添加查询表单

<template>
  <div>
    <!-- 查询表单 -->
    <el-form :inline="true" :model="filters" class="demo-form-inline">
      <el-form-item label="料号">
        <el-input v-model="filters.item_no" placeholder="请输入料号"></el-input>
      </el-form-item>
      <el-form-item label="品名">
        <el-input v-model="filters.item_name" placeholder="请输入品名"></el-input>
      </el-form-item>
      <el-form-item label="规格">
        <el-input v-model="filters.item_spec" placeholder="请输入规格"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="fetchMpsD1List">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 物料需求明细列表 -->
    <el-table :data="mpsD1List" style="width: 100%" v-loading="loading">
      <el-table-column prop="item_no" label="料号" width="180" />
      <el-table-column prop="item_name" label="品名" width="180" />
      <el-table-column prop="item_spec" label="规格" width="180" />
      <el-table-column prop="item_qty" label="需求数量" width="180" />
      <!-- 添加更多列 -->
    </el-table>

    <!-- 分页 -->
    <el-pagination
v
-if="mpsD1List.length"background
:current
-page="page":page-size="pageSize"layout="total, prev, pager, next":total="total"@current-change="handlePageChange" /> </div> </template>

b. 修改脚本部分

<script>import { ref, onMounted } from'vue';
import { getMPS002D1 } from
'@/api/mpsApp/MPS002D1Model';

export
default{
setup() {
const mpsD1List
=ref([]);
const page
= ref(1);
const pageSize
= ref(10);
const total
= ref(0);
const loading
= ref(false);

const filters
=ref({
item_no:
'',
item_name:
'',
item_spec:
'',
});

const fetchMpsD1List
= async () =>{
loading.value
= true;try{
const params
={
page: page.value,
page_size: pageSize.value,
item_no: filters.value.item_no,
item_name: filters.value.item_name,
item_spec: filters.value.item_spec,
};
const response
=await getMPS002D1(params);
mpsD1List.value
=response.data.results;
total.value
=response.data.count;
}
catch(error) {
console.error(
'Error fetching MPS002D1 list:', error);
}
finally{
loading.value
= false;
}
};

const resetFilters
= () =>{
filters.value
={
item_no:
'',
item_name:
'',
item_spec:
'',
};
fetchMpsD1List();
};

const handlePageChange
= (newPage) =>{
page.value
=newPage;
fetchMpsD1List();
};

onMounted(fetchMpsD1List);
return{
mpsD1List,
page,
pageSize,
total,
loading,
filters,
fetchMpsD1List,
resetFilters,
handlePageChange,
};
},
};
</script>

后端部分(Django REST Framework)

为了支持前端的查询功能,需要在后端的视图中添加筛选功能。

1. 修改
MPS002HModel
的视图

from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import MPS002HModel
from .serializers import MPS002HSerializer

class MPS002HViewSet(viewsets.ModelViewSet):
queryset
= MPS002HModel.objects.all().order_by('-mps_date')
serializer_class
=MPS002HSerializer
filter_backends
=[DjangoFilterBackend, filters.SearchFilter]
filterset_fields
= ['mps_ym']
search_fields
= ['bo_no__item_no', 'bo_no__item_name', 'bo_no__item_spec']

说明

  • filter_backends
    :使用
    DjangoFilterBackend

    SearchFilter
    ,可以实现精确过滤和模糊搜索。
  • filterset_fields
    :精确过滤的字段,这里包括
    mps_ym
  • search_fields
    :模糊搜索的字段,包括关联的
    bo_no
    (产品料号)的
    item_no

    item_name

    item_spec

2. 修改
MPS002D1Model
的视图

from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import MPS002D1Model
from .serializers import MPS002D1Serializer

class MPS002D1ViewSet(viewsets.ModelViewSet):
queryset
=MPS002D1Model.objects.all()
serializer_class
=MPS002D1Serializer
filter_backends
=[DjangoFilterBackend, filters.SearchFilter]
search_fields
= ['item_no__item_no', 'item_no__item_name', 'item_no__item_spec']

说明

  • search_fields
    :对于物料需求明细,可以根据
    item_no
    (料号)、
    item_name
    (品名)、
    item_spec
    (规格)进行模糊搜索。

3. 安装和配置
django-filter

如果还没有安装
django-filter
,需要先安装:

pip install django-filter

并在
settings.py
中添加:

REST_FRAMEWORK ={'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

更新序列化器

确保您的序列化器包含必要的字段,以便前端能够正确接收数据。

MPS002HSerializer

from rest_framework import serializers
from .models import MPS002HModel

class MPS002HSerializer(serializers.ModelSerializer):
fa_no_name
= serializers.CharField(source='fa_no.fa_name', read_only=True)
bo_no_name
= serializers.CharField(source='bo_no.item_name', read_only=True)
bo_no_spec
= serializers.CharField(source='bo_no.item_spec', read_only=True)
bo_no
= serializers.CharField(source='bo_no.item_no', read_only=True)

class Meta:
model
=MPS002HModel
fields
= ['id', 'mps_no', 'mps_date', 'fa_no', 'fa_no_name', 'bo_no', 'bo_no_name', 'bo_no_spec', 'mps_ym', 'mps_qty']

MPS002D1Serializer

from rest_framework import serializers
from .models import MPS002D1Model

class MPS002D1Serializer(serializers.ModelSerializer):
item_name
= serializers.CharField(source='item_no.item_name', read_only=True)
item_spec
= serializers.CharField(source='item_no.item_spec', read_only=True)
item_no
= serializers.CharField(source='item_no.item_no', read_only=True)

class Meta:
model
=MPS002D1Model
fields
= ['id', 'mps_no', 'item_no', 'item_name', 'item_spec', 'item_qty', 'rmk']

更新 API 请求

1. 更新前端 API 调用

在您的 API 请求文件中,确保查询参数能够正确传递。

MPS002HModel
API

import request from '@/utils/request';
const baseUrl
= '/mpsApp/MPS002HModel/';

export
functiongetMPS002(params) {returnrequest({
url: baseUrl,
method:
'get',
params,
});
}

MPS002D1Model
API

import request from '@/utils/request';
const baseUrl
= '/mpsApp/MPS002D1Model/';

export
functiongetMPS002D1(params) {returnrequest({
url: baseUrl,
method:
'get',
params,
});
}


总结

通过上述步骤,我们实现了:

  • 前端
    :在
    MPS002HModel

    MPS002D1Model
    的列表页面中添加了查询表单,可以根据指定的字段进行筛选,并将查询条件传递给后端。

  • 后端
    :在 Django REST Framework 的视图中,使用
    django-filter

    SearchFilter
    实现了对指定字段的精确过滤和模糊搜索。

  • 序列化器
    :更新了序列化器,以便在返回数据时包含关联字段的信息,如品名和规格。

  • API 请求
    :确保查询参数能够正确地通过前端 API 请求发送到后端。

这样,用户就可以在前端界面上根据产品料号、品名、规格和年月对主生产计划列表进行查询,也可以在物料需求明细列表中根据料号、品名、规格进行筛选。

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

回顾下
上一讲
的内容。主线程 m0 蓄势待发,准备干活。g0 为 m0 提供了执行环境,P 和 m0 绑定,为 m0 提供活,也就是 goroutine。那么问题来了,活呢?哪里有活给 m0 干?

这一讲我们将介绍 m0 执行的第一个活,也就是 main goroutine。main gouroutine 就是执行 main 函数的 goroutine,有别于用
go
关键字创建的 goroutine,它们在执行过程中有一些区别(后续会讲)。

1. main goroutine 创建

接着上一讲的内容,调度器初始化之后,执行到
asm_amd64.s/rt0_go:352

TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
		...
	    // create a new goroutine to start program
352 	MOVQ	$runtime·mainPC(SB), AX		// entry
353 	PUSHQ	AX
354 	CALL	runtime·newproc(SB)
355 	POPQ	AX

// dlv 进入到指令执行处
dlv exec ./hello
Type 'help' for list of commands.
(dlv) b /usr/local/go/src/runtime/asm_amd64.s:352
Breakpoint 1 set at 0x45433c for runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:352
(dlv) c
(dlv) si
> runtime.rt0_go() /usr/local/go/src/runtime/asm_amd64.s:353 (PC: 0x454343)
Warning: debugging optimized function
		asm_amd64.s:349 0x454337        e8e4290000      call $runtime.schedinit
        asm_amd64.s:352 0x45433c*       488d05659d0200  lea rax, ptr [rip+0x29d65]
=>      asm_amd64.s:353 0x454343        50              push rax

结合 CPU 执行指令和 Go plan9 汇编代码一起分析。

首先,将
$runtime·mainPC(SB)
地址传给 AX 寄存器,CPU 执行的指令是
mov qword ptr [rsp+0x8], rax
。使用
regs
可以看到 rax 的值,也就是
$runtime·mainPC(SB)
的地址:

(dlv) regs
    Rip = 0x0000000000454343
    Rsp = 0x00007ffd58324080
    Rax = 0x000000000047e0a8        // rax = $runtime.mainPC(SB) = [rsp+0x8]

那么
$runtime.mainPC(SB)
的地址指的是什么呢?我们看
$runtime.mainPC(SB)
的定义:

// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA	runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL	runtime·mainPC(SB),RODATA,$8

$runtime.mainPC(SB)
是一个为了执行
runtime.main
的函数值。

继续执行
PUSH AX

runtime.mainPC(SB)
放到栈上。注意,这里的栈是 g0 栈,也就是主线程 m0 运行的栈。

接着往下走:

=>      asm_amd64.s:354 0x45ca64        e8f72a0000              call $runtime.newproc
        asm_amd64.s:355 0x45ca69        58                      pop rax

调用
$runtime.newproc
函数,
newproc
就是创建 goroutine 的函数。我们使用
go
关键字创建的 goroutine 都经编译器转换最终调用到
newproc
创建 goroutine。可想而知,这个函数是非常重要的。

进入这个函数我们的操作还是在 g0 栈。

// Create a new g running fn.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
func newproc(fn *funcval) {
	gp := getg()                            // gp = g0
	pc := getcallerpc()                     // 获取调用者的指令地址,也就是调用 newproc 时由 call 指令压栈的函数返回地址
	systemstack(func() {
		newg := newproc1(fn, gp, pc)        // 创建 g
		...
	})
}

newproc
调用
newproc1
创建 goroutine,分别介绍传入
newproc1
的参数
fn

gp

pc

首先
fn
是包含
runtime.main
的函数值,打印
fn
如下:

(dlv) print fn
(*runtime.funcval)(0x47e0a8)
*runtime.funcval {fn: 4386432}

可以看到,
fn
是一个指向结构体
funcval
的地址(也就是前面介绍的
$runtime.mainPC(SB)
,地址
0x47e0a8
),该结构体内装的
fn
才是实际执行的
runtime.main
函数的地址:

type funcval struct {
	fn uintptr
	// variable-size, fn-specific data here
}

第二个参数 gp 等于 g0,g0 为主线程 m0 提供运行时环境,pc 是调用 newproc 时由 call 指令压栈的函数返回地址。

参数讲完了,在看下
systemstack
函数。
systemstack
会将
goroutine
运行的
fn
调用到系统栈(g0 栈)运行,这里 m0 已经在 g0 栈上运行了,不用调用。如果不是 g0 栈的 goroutine,比如 m0 运行 g1 栈,则
systemstack
会先将 g1 栈切到 g0 栈,接着运行完 fn 在返回到 g1 栈。详细内容可以参考
这里

现在进入
newproc1(fn, gp, pc)
查看
newproc1
是如何创建新 goroutine 的。

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
	mp := acquirem()						// acquirem 获取当前 goroutine 绑定的线程,这里是 m0
	pp := mp.p.ptr()						// 获取该线程绑定的 P,这里 pp = allp[0]

	// 从 P 的本地队列 gFree 或者全局 gFree 队列中获取空闲的 goroutine,如果拿不到则返回 nil
	// 这里是创建 main goroutine 阶段,无空闲的 goroutine
	newg := gfget(pp)		
	if newg == nil {
		newg = malg(stackMin)				// malg 创建新的 goroutine
		casgstatus(newg, _Gidle, _Gdead)	// 创建的 goroutine 初始状态是 _Gidle,这里更新 goroutine 状态为 _Gdead
		allgadd(newg) 						// 增加新 goroutine 到全局变量 allgs
	}
	...
}

首先调用
gfget
获取当前线程 P 或全局空闲队列中空闲的 goroutine,如果没有则调用
malg(stackMin)
创建新 goroutine。
malg(stackMin)
中的
stackMin
等于 2048,也就是 2K。查看
malg
做了什么:

func malg(stacksize int32) *g {
	newg := new(g)													// new 创建 g
	if stacksize >= 0 {												// stacksize = 2048
		stacksize = round2(stackSystem + stacksize)					// stackSystem = 0, stacksize = 2048
		systemstack(func() {
			newg.stack = stackalloc(uint32(stacksize))				// 调用 stackalloc 获得新 goroutine 的栈,新 goroutine 的栈大小为 2K
		})
		newg.stackguard0 = newg.stack.lo + stackGuard
		newg.stackguard1 = ^uintptr(0)
		// Clear the bottom word of the stack. We record g
		// there on gsignal stack during VDSO on ARM and ARM64.
		*(*uintptr)(unsafe.Pointer(newg.stack.lo)) = 0
	}
	return newg
}

malg
创建一个新的 goroutine,并且 goroutine 的栈大小为 2KB。

接着调用
casgstatus
更新 goroutine 的状态为
_Gdead
。然后调用
allgadd
函数将创建的 goroutine 和全局变量
allgs
关联:

func allgadd(gp *g) {
	lock(&allglock)																// allgs 是全局变量,给全局变量加锁
	allgs = append(allgs, gp)													// 将 newg:gp 添加到 allgs
	if &allgs[0] != allgptr {													// allgptr 是一个指向 allgs[0] 的指针,这里是 nil
		atomicstorep(unsafe.Pointer(&allgptr), unsafe.Pointer(&allgs[0]))		// allgptr = &allgs[0]
	}
	atomic.Storeuintptr(&allglen, uintptr(len(allgs)))							// 更新全局变量 allglen = len(allgs)
	unlock(&allglock)															// 解锁
}

继续往下看
newproc1
的执行过程:

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
	...
	totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) 	// extra space in case of reads slightly beyond frame
	totalSize = alignUp(totalSize, sys.StackAlign)
	sp := newg.stack.hi - totalSize								// sp 是栈顶指针

	// 设置 newg.sched 的所有成员为 0,后续要对它们重新赋值
	memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
	newg.sched.sp = sp											
	newg.stktopsp = sp

	// newg.sched.pc 表示当 newg 被调度起来运行时从这个地址开始执行指令
	newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
	newg.sched.g = guintptr(unsafe.Pointer(newg))
	gostartcallfn(&newg.sched, fn)

这段代码主要是给
newg.sched
赋值,
newg.sched
的结构体如下:

type gobuf struct {
	sp   uintptr				// goroutine 的 栈顶指针
	pc   uintptr				// 执行 goroutine 的指令地址
	g    guintptr				// goroutine 地址
	ctxt unsafe.Pointer			// 包装 goroutine 执行函数的结构体 funcval 的地址
	ret  uintptr				// 返回地址
	lr   uintptr
	bp   uintptr
}

newg.sched
主要的成员如注释所示,线程通过该结构体就能知道要从哪里运行代码。

在赋值
newg.sched
时,这段代码很有意思:

newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum

它是将
goexit
函数的地址 + 1 在传给
newg.sched.pc
,查看此时
newg.sched.pc
的值:

  4530:         newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
=>4531:         newg.sched.g = guintptr(unsafe.Pointer(newg))
(dlv) print newg.sched
runtime.gobuf {sp: 824633976800, pc: 4540513, g: 0, ctxt: unsafe.Pointer(0x0), ret: 0, lr: 0, bp: 0}
(dlv) print unsafe.Pointer(4540513)
unsafe.Pointer(0x454861)

实际是将
0x454861
传给了
newg.sched.pc
,我们先不管这个
0x454861
,接着往下看。调用
gostartcallfn(&newg.sched, fn)
函数:

func gostartcallfn(gobuf *gobuf, fv *funcval) {
	var fn unsafe.Pointer
	if fv != nil {
		fn = unsafe.Pointer(fv.fn)								// 将 funcval.fn 赋给 fn,实际是 runtime.main 的地址值
	} else {
		fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc))
	}
	gostartcall(gobuf, fn, unsafe.Pointer(fv))
}

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
	sp := buf.sp												// 取 g1 的栈顶指针
	sp -= goarch.PtrSize										// 栈顶指针向下减 1 个字节
	*(*uintptr)(unsafe.Pointer(sp)) = buf.pc					// 减的 1 个字节空间用来放 abi.FuncPCABI0(goexit) + sys.PCQuantum
	buf.sp = sp													// 将减了 1 个字节的 sp 作为新栈顶
	buf.pc = uintptr(fn)										// 重新将 pc 指向 fn
	buf.ctxt = ctxt												// 将 buf.ctxt 指向 funcval
}

看到这里我们明白了,为什么要加一层
goexit
并且将栈顶指针往下减 1 作为新栈顶了。因为新栈顶在返回时会执行到
goexit
,这也是调度器希望每个 goroutine 都要做的,在执行完执行
goexit
才能真正退出。

好了我们回到
newproc1
继续往下看:

func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
	...
	newg.parentGoid = callergp.goid					// newg 的 父 id,newg.parentGoid = 0
	newg.gopc = callerpc							// 调用者的 pc
	
	newg.startpc = fn.fn							// newg.startpc = funcval.fn = &runtime.main

	...
	casgstatus(newg, _Gdead, _Grunnable)			// 更新 newg 的状态为 _Grunnable
	newg.goid = pp.goidcache						// 通过 goidcache 获得新的 newg.goid,这里 main goroutine 的 goid 是 1

	...
	releasem(mp)
	return newg
}

至此我们的新的 goroutine 就创建出来了。回顾下,首先给新 goroutine 申请 2KB 的栈空间,接着在新 goroutine 中创建执行 goroutine 的环境
newg.sched
,线程根据
newg.sched
就可以运行 goroutine。最后,设置 goroutine 的状态为 _Grunnable,表示 goroutine 状态就绪可以运行了。

我们根据上述分析画出内存分布如下图:

image

2. 小结

到这里创建 main goroutine 的逻辑基本介绍完了。下一讲,将继续介绍 main gouroutine 是怎么运行起来的。