精华内容
下载资源
问答
  • 此时,您应该本地计算机上拥有一个真正的Git存储库,并您面前拥有其所有文件的签出或工作副本。通常,每当项目达到要记录​​的状态时,您就需要开始进行更改并将这些更改的快照提交到存储库中。 请记住,工作...

    记录对存储库的更改

     此时,您应该在本地计算机上拥有一个真正的 Git存储库,并在您面前拥有其所有文件的签出或工作副本。通常,每当项目达到要记录​​的状态时,您就需要开始进行更改并将这些更改的快照提交到存储库中。

    请记住,工作目录中的每个文件都可以处于以下两种状态之一:已跟踪或未跟踪。跟踪的文件是上一个快照中的文件;它们可以不修改,修改或上演。简而言之,跟踪文件是Git知道的文件。

    未跟踪的文件就是所有其他内容-工作目录中不在上一个快照中且不在暂存区中的所有文件。第一次克隆存储库时,所有文件都会被跟踪和未修改,因为Git只是将它们签出,并且您还没有进行任何编辑。

    在编辑文件时,Git会将其视为已修改,因为自上次提交以来已对其进行了更改。在工作时,您有选择地暂存这些已修改的文件,然后提交所有已暂存的更改,然后重复该循环。

    文件状态的生命周期。

     图8.文件状态的生命周期。

     检查文件状态

     用于确定git status命令处于哪个状态的主要工具。如果在克隆后直接运行此命令,则应看到类似以下内容的内容:

    <span style="color:#333333"><code class="language-console">$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    nothing to commit, working directory clean</code></span>

     这意味着您有一个干净的工作目录。换句话说,没有任何跟踪文件被修改。Git也看不到任何未跟踪的文件,否则它们将在此处列出。最后,该命令会告诉您您所在的分支,并告知您该分支尚未与服务器上的同一分支分开。现在,该分支始终是“ master”,这是默认设置。您在这里不必担心。 Git分支将详细介绍分支和参考。

    假设您向项目中添加了一个新文件,即一个简单README文件。如果该文件以前不存在,然后运行git status,则会看到未跟踪的文件,如下所示:

    <span style="color:#333333"><code class="language-console">$ echo 'My Project' > README
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
    
        README
    
    nothing added to commit but untracked files present (use "git add" to track)</code></span>

     您可以看到新README文件处于未跟踪状态,因为它位于状态输出中“未跟踪的文件”标题下。基本上,未跟踪意味着Git会看到您在上一个快照中没有的文件(提交);在您明确要求Git之前,Git不会开始将其包含在提交快照中。这样做是为了确保您不会意外地包括生成的二进制文件或其他您不想包含的文件。您确实想开始包含README,因此让我们开始跟踪文件。

     跟踪新文件

     为了开始跟踪新文件,请使用命令git add。要开始跟踪README文件,可以运行以下命令:

    $ git add README

    如果再次运行status命令,则可以看到README现在已跟踪文件并准备将其提交:

    <span style="color:#333333"><code class="language-console">$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   README</code></span>

    您可以说它是分阶段的,因为它在“要更改的更改”标题下。如果此时提交,则在运行时文件的版本git add就是后续历史快照中的版本。您可能还记得,当您git init早先运行时,然后又运行了git add <files> —那是为了开始跟踪目录中的文件。该git add命令采用文件或目录的路径名;如果是目录,则该命令以递归方式添加该目录中的所有文件。

    暂存修改的文件

    让我们更改一个已经被跟踪的文件。如果更改了以前跟踪的文件CONTRIBUTING.md,然后git status再次运行命令,则会得到如下所示的内容:

    <span style="color:#333333"><code class="language-console">$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   README
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   CONTRIBUTING.md</code></span>

    CONTRIBUTING.md文件将出现在名为“未暂存的更改未提交”的部分下-这意味着已跟踪的文件已在工作目录中被修改但尚未暂存。要登台,请运行git add命令。 git add是一个多用途命令-您可以使用它来开始跟踪新文件,暂存文件以及执行其他操作,例如将合并冲突的文件标记为已解决。将其更多地视为“将此内容精确地添加到下一个提交”而不是“将该文件添加到项目中”可能会有所帮助。git add现在运行以暂存CONTRIBUTING.md文件,然后git status再次运行:

    <span style="color:#333333"><code class="language-console">$ git add CONTRIBUTING.md
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   README
        modified:   CONTRIBUTING.md</code></span>

    这两个文件都已暂存,将进入下一个提交。此时,假设您记得在CONTRIBUTING.md提交之前要进行的一项小更改。您再次打开它并进行更改,就可以提交了。但是,让我们再运行git status一次:

    <span style="color:#333333"><code class="language-console">$ vim CONTRIBUTING.md
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   README
        modified:   CONTRIBUTING.md
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   CONTRIBUTING.md</code></span>

    有没有搞错?现在CONTRIBUTING.md被列为已暂存和未暂存。那怎么可能?事实证明,Git与运行git add命令时的阶段完全相同。如果现在提交,则CONTRIBUTING.md上次运行git add命令时的版本是该命令如何进入提交的方式,而不是运行时在工作目录中显示的文件的版本git commit。如果在运行后修改文件git add,则必须git add再次运行以暂存文件的最新版本:

    <span style="color:#333333"><code class="language-console">$ git add CONTRIBUTING.md
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   README
        modified:   CONTRIBUTING.md</code></span>

    空头状态

    虽然git status输出非常全面,但也很罗word。Git还具有简短的状态标志,因此您可以以更紧凑的方式查看更改。如果运行,git status -s或者git status --short从命令中获得了更为简化的输出:

    <span style="color:#333333"><code class="language-console">$ git status -s
     M README
    MM Rakefile
    A  lib/git.rb
    M  lib/simplegit.rb
    ?? LICENSE.txt</code></span>

    未跟踪的??新文件旁边有一个,已添加到暂存区域的新文件有一个A,已修改的文件有一个M,依此类推。输出有两列-左列指示临时区域的状态,右列指示工作树的状态。因此,例如在该输出中,README文件已在工作目录中修改但尚未暂存,而lib/simplegit.rb文件已修改并暂存。已对其Rakefile进行了修改,分段和再次修改,因此对它所做的更改既可以分段也可以分段。

    忽略文件

    通常,您会拥有一类文件,这些文件是您不希望Git自动添加甚至不显示给您的文件。这些通常是自动生成的文件,例如日志文件或构建系统生成的文件。在这种情况下,您可以创建文件列表模式以匹配名为的文件.gitignore。这是一个示例.gitignore文件:

    <span style="color:#333333"><code class="language-console">$ cat .gitignore
    *.[oa]
    *~</code></span>

    第一行告诉Git忽略任何以“ .o”或“ .a”结尾的文件-对象和归档文件,它们可能是构建代码的产物。第二行告诉Git忽略名称以波浪号(~)结尾的所有文件,许多文本编辑器(例如Emacs)使用该文件来标记临时文件。您可能还包括一个log,tmp或pid目录。自动生成的文档;等等。.gitignore在开始之前为新存储库设置文件通常是一个好主意,因此您不会意外提交您确实不希望在Git存储库中使用的文件。

    可以放入.gitignore文件中的模式的规则如下:

    • 空行或以开头的行将#被忽略。

    • 标准的glob模式起作用,并将在整个工作树中递归地应用。

    • 您可以使用正斜杠(/)开头的样式,以避免递归。

    • 您可以使用正斜杠(/)结束模式以指定目录。

    • 您可以通过以感叹号(!)开头来否定模式。

    球形模式类似于shell使用的简化正则表达式。星号(*)匹配零个或多个字符;[abc]匹配括号内的任何字符(在本例中为a,b或c);问号(?)与单个字符匹配;和用连字符([0-9])分隔的括号匹配它们之间的任何字符(在本例中为0到9)。您也可以使用两个星号来匹配嵌套目录。a/**/z将匹配a/za/b/za/b/c/z,等等。

    这是另一个示例.gitignore文件:

    <span style="color:#333333"><code># ignore all .a files
    *.a
    
    # but do track lib.a, even though you're ignoring .a files above
    !lib.a
    
    # only ignore the TODO file in the current directory, not subdir/TODO
    /TODO
    
    # ignore all files in any directory named build
    build/
    
    # ignore doc/notes.txt, but not doc/server/arch.txt
    doc/*.txt
    
    # ignore all .pdf files in the doc/ directory and any of its subdirectories
    doc/**/*.pdf</code></span>

    小费

    如果您想要项目的起点,GitHub会.gitignorehttps://github.com/github/gitignore上为数十个项目和语言维护一个相当全面的优秀文件示例列表。

    注意

    在简单的情况下,存储库.gitignore的根目录中可能只有一个文件,该文件递归地应用于整个存储库。但是,也可能.gitignore在子目录中有其他文件。这些嵌套.gitignore文件中的规则仅适用于它们所在目录下的文件。(Linux内核源存储库有206个.gitignore文件。)

    深入探讨多个.gitignore文件的细节超出了本书的范围。man gitignore有关详细信息,请参见。

    查看分阶段和非分阶段的更改

    如果该git status命令对您来说太含糊-您想确切地知道您所做的更改,而不仅仅是更改了哪些文件,则可以使用该git diff命令。稍后我们将git diff详细介绍,但您可能最常使用它来回答以下两个问题:您做了哪些更改但尚未上演?您打算上演什么?尽管git status通常通过列出文件名来回答这些问题,但仍会git diff向您显示添加和删除的确切行(即补丁程序)。

    假设您README再次编辑并CONTRIBUTING.md暂存文件,然后在不暂存的情况下编辑文件。如果您运行git status命令,您将再次看到以下内容:

    <span style="color:#333333"><code class="language-console">$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        modified:   README
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   CONTRIBUTING.md</code></span>

    要查看已更改但尚未上演的内容,请键入git diff其他参数:

    <span style="color:#333333"><code class="language-console">$ git diff
    diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
    index 8ebb991..643e24f 100644
    --- a/CONTRIBUTING.md
    +++ b/CONTRIBUTING.md
    @@ -65,7 +65,8 @@ branch directly, things can get messy.
     Please include a nice description of your changes when you submit your PR;
     if we have to read the whole diff to figure out why you're contributing
     in the first place, you're less likely to get feedback and have your change
    -merged in.
    +merged in. Also, split your changes into comprehensive chunks if your patch is
    +longer than a dozen lines.
    
     If you are starting to work on a particular area, feel free to submit a PR
     that highlights your work in progress (and note in the PR title that it's</code></span>

    该命令将工作目录中的内容与暂存区中的内容进行比较。结果告诉您尚未上演的更改。

    如果您想查看下一次提交的阶段,可以使用git diff --staged。此命令将您分阶段的更改与上一次提交进行比较:

    <span style="color:#333333"><code class="language-console">$ git diff --staged
    diff --git a/README b/README
    new file mode 100644
    index 0000000..03902a1
    --- /dev/null
    +++ b/README
    @@ -0,0 +1 @@
    +My Project</code></span>

    重要的是要注意,git diff它本身不会显示自上次提交以来所做的所有更改-仅显示仍未暂存的更改。如果您上演了所有更改,git diff则将不输出任何信息。

    再举一个例子,如果您暂存CONTRIBUTING.md文件然后对其进行编辑,则可以git diff用来查看文件中暂存的更改和未暂存的更改。如果我们的环境如下所示:

    <span style="color:#333333"><code class="language-console">$ git add CONTRIBUTING.md
    $ echo '# test line' >> CONTRIBUTING.md
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        modified:   CONTRIBUTING.md
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   CONTRIBUTING.md</code></span>

    现在,您可以使用git diff来查看尚未暂存的内容:

    <span style="color:#333333"><code class="language-console">$ git diff
    diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
    index 643e24f..87f08c8 100644
    --- a/CONTRIBUTING.md
    +++ b/CONTRIBUTING.md
    @@ -119,3 +119,4 @@ at the
     ## Starter Projects
    
     See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
    +# test line</code></span>

    git diff --cached查看您到目前为止已经上演了什么(--staged并且--cached是同义词):

    <span style="color:#333333"><code class="language-console">$ git diff --cached
    diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
    index 8ebb991..643e24f 100644
    --- a/CONTRIBUTING.md
    +++ b/CONTRIBUTING.md
    @@ -65,7 +65,8 @@ branch directly, things can get messy.
     Please include a nice description of your changes when you submit your PR;
     if we have to read the whole diff to figure out why you're contributing
     in the first place, you're less likely to get feedback and have your change
    -merged in.
    +merged in. Also, split your changes into comprehensive chunks if your patch is
    +longer than a dozen lines.
    
     If you are starting to work on a particular area, feel free to submit a PR
     that highlights your work in progress (and note in the PR title that it's</code></span>

    注意

    外部工具中的Git Diff

    git diff在本书的其余部分中,我们将继续以各种方式使用该命令。如果您更喜欢图形或外部差异查看程序,则有另一种查看这些差异的方法。如果您运行git difftool而不是git diff,则可以在软件中查看任何这些差异,例如emerge,vimdiff以及更多(包括商业产品)。运行git difftool --tool-help以查看系统上可用的功能。

    做出改变

    现在您已经按照需要的方式设置了暂存区,您可以提交更改。请记住,所有尚未暂存的内容(自创建git add以来从未运行过的所有已创建或修改的文件)都不会进入此提交。它们将作为修改后的文件保留在您的磁盘上。在这种情况下,假设您上次运行时git status,您看到一切都已上演,那么您就可以提交更改了。提交的最简单方法是键入git commit

    $ git commit

    这样做将启动您选择的编辑器。(这是由shell的EDITOR环境变量设置的,通常是vim或emacs,尽管您可以使用入门中git config --global core.editor看到的命令使用所需的任何内容对其进行配置)。

    编辑器显示以下文本(此示例是Vim屏幕):

    <span style="color:#333333"><code># Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    # On branch master
    # Your branch is up-to-date with 'origin/master'.
    #
    # Changes to be committed:
    #	new file:   README
    #	modified:   CONTRIBUTING.md
    #
    ~
    ~
    ~
    ".git/COMMIT_EDITMSG" 9L, 283C</code></span>

    您可以看到默认的提交消息包含已git status注释掉的命令的最新输出,并在顶部有一个空行。您可以删除这些注释并输入提交消息,也可以将它们留在此处以帮助您记住要提交的内容。(为更明确地提醒您已修改的内容,您可以将-v选项传递给git commit。这样做还可以将更改的差异放入编辑器中,以便您可以确切地看到要提交的更改。)退出时编辑器,Git使用该提交消息(删除了注释和差异)创建您的提交。

    另外,您可以commit通过在-m标志后指定它来在命令中内联键入提交消息,如下所示:

    <span style="color:#333333"><code class="language-console">$ git commit -m "Story 182: Fix benchmarks for speed"
    [master 463dc4f] Story 182: Fix benchmarks for speed
     2 files changed, 2 insertions(+)
     create mode 100644 README</code></span>

    现在,您已经创建了第一次提交!您可以看到提交为您提供了一些有关其自身的输出:提交给(master)的分支,提交具有的SHA-1校验和(463dc4f),更改了多少文件以及有关提交中添加和删除的行的统计信息。

    请记住,提交记录了您在临时区域中设置的快照。您没有上演的所有内容仍坐在那里进行修改;您可以再次提交以将其添加到您的历史记录中。每次执行提交时,都会记录项目的快照,以后可以还原或比较该快照。

    跳过暂存区

    尽管它对于完全按照所需的方式制作提交很有用,但临时区域有时比工作流程中所需的要复杂一些。如果要跳过暂存区域,Git提供了一个简单的快捷方式。-agit commit命令中添加选项后,Git会在提交之前自动暂存已跟踪的每个文件,从而使您可以跳过该git add部分:

    <span style="color:#333333"><code class="language-console">$ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   CONTRIBUTING.md
    
    no changes added to commit (use "git add" and/or "git commit -a")
    $ git commit -a -m 'added new benchmarks'
    [master 83e38c7] added new benchmarks
     1 file changed, 5 insertions(+), 0 deletions(-)</code></span>

    注意,在这种情况下,提交前不必git addCONTRIBUTING.md文件上运行。这是因为该-a标志包括所有更改的文件。这很方便,但是要小心;有时,此标志将导致您包括不需要的更改。

    删除文件

    要从Git中删除文件,您必须将其从跟踪文件中删除(更准确地说,将其从暂存区中删除),然后提交。该git rm命令会执行此操作,并且还会从工作目录中删除该文件,因此下次您不会将其视为未跟踪的文件。

    如果你只是删除您的工作目录中的文件,它显示了在“不改变上演承诺”(即未分级)您的区域git status输出:

    <span style="color:#333333"><code class="language-console">$ rm PROJECTS.md
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes not staged for commit:
      (use "git add/rm <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
            deleted:    PROJECTS.md
    
    no changes added to commit (use "git add" and/or "git commit -a")</code></span>

    然后,如果运行git rm,它将分阶段删除文件:

    <span style="color:#333333"><code class="language-console">$ git rm PROJECTS.md
    rm 'PROJECTS.md'
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        deleted:    PROJECTS.md</code></span>

    下次提交时,文件将消失并且不再被跟踪。如果您修改了文件或已经将其添加到临时区域,则必须使用该-f选项强制删除。这是一项安全功能,可防止意外删除尚未记录在快照中且无法从Git恢复的数据。

    您可能想要做的另一件事是将文件保留在工作树中,但将其从暂存区中删除。换句话说,您可能希望将文件保留在硬盘上,但不再需要Git对其进行跟踪。如果您忘记向.gitignore文件中添加某些内容并意外地将其暂存,例如大型日志文件或一堆已.a编译的文件,这将特别有用。为此,请使用以下--cached选项:

    $ git rm --cached README

    您可以将文件,目录和文件全局模​​式传递给git rm命令。这意味着您可以执行以下操作:

    $ git rm log/\*.log

    注意\。前面的反斜杠()*。这是必要的,因为Git会执行自己的文件名扩展,除了扩展Shell的文件名。此命令将删除目录中所有具有.log扩展名的log/文件。或者,您可以执行以下操作:

    $ git rm \*~

    此命令将删除所有名称以~。结尾的文件。

    移动文件

    与许多其他VCS系统不同,Git不会明确跟踪文件移动。如果您在Git中重命名文件,则Git中不会存储任何元数据来告诉您您已重命名该文件。但是,Git在事后弄清楚这一点很聪明–我们稍后将处理检测文件移动的问题。

    因此,Git有一个mv命令有点令人困惑。如果要在Git中重命名文件,可以运行以下命令:

    $ git mv file_from file_to

    而且效果很好。实际上,如果您运行类似的命令并查看状态,您会发现Git认为它是重命名的文件:

    <span style="color:#333333"><code class="language-console">$ git mv README.md README
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        renamed:    README.md -> README</code></span>

    但是,这等效于运行如下代码:

    <span style="color:#333333"><code class="language-console">$ mv README.md README
    $ git rm README.md
    $ git add README</code></span>

    Git指出这是隐式的重命名,因此,以这种方式或使用mv命令重命名文件都没有关系。唯一真正的区别是git mv一个命令而不是三个命令—这是一个便捷功能。更重要的是,您可以使用任何喜欢的工具来重命名文件,并在提交前稍后解决添加/ rm问题。

    以上就是小编所了解的记录对Git存储库的更改,尽情学习吧!

    展开全文
  • 5、大势至共享文件审计系统可以重要共享文件的修改、删除操作进行实时备份和实时还原,从而有效地防止了共享文件被误操作、恶意修改而丢失、损坏的情况。 6、大势至共享文件审计系统通过基于用户、IP地址、MAC...
  • 设置显示文件的扩展名.reg 设置我的电脑中显示打印机.reg 鼠标自动激活当前窗口.reg 缩短关闭无响应程序时的等待时间.reg 提高光驱的读写能力.reg 提高历史缓冲区的个数.reg 提高软驱的性能.reg 提高显示器...
  • 如果您还没有设置保存聊天记录的话,请点击主界面上的设置按钮,出现一设置对话框, 选中其它设置栏中“保存聊天记录和日志”选项。然后设置下聊天记录要保存的文件位置。聊天窗口中聊天记录按钮可以查看您跟该...
  • 在计算机上用作虚拟驱动器。 您可以使用常规软件访问和修改存储在Parsec中所有数据,就像在本地硬盘驱动器上一样。 永不丢失任何数据。 与远程服务器同步永远不会破坏任何数据,因此您可以浏览数据历史记录并从...
  • 可以员工计算机的所有硬件设备进行禁用,以避免员工使用与工作不相关硬件设备,错误修改网络属性,方便统一管理。 4、远程管理员工计算机: 好易网管为管理者提供了强大远程计算机管理功能,从远程文件系统...
  • 计算机实习日记

    2011-11-26 12:22:41
    原来这里网线布置比较合理,这里一律是交换机上插几个下一级交换机,再把所有机器都接在下一级的计算机上。这样,大家速度都比较平均,而且单位里没有人利用电驴之类BT软件不停下载。而我们学校宿舍里...
  • 甚至无法读取数据文件的情况下,通 过分析文件大小或文件目录中的有关信息推测出数据的特点。 • 网络通信的威胁:可分为被动威胁和主动威胁。被动威胁并不改变数据流,而是采用各种手段窃 取通信链路传输的...
  • 这些条款或文档中,必须规定用于标识软件产品、控制和实现软件的修改记录和报告修改实现的状态以及评审和检查配置管理工作等四方面的活动。还必须规定用以维护和存储软件受控版本的方法和设施;必须规定所发现...
  • 它允许不使用活动调试器情况下,本地计算机上或通过 Internet 查看和记录调试会话输出。 Desktops 使用这一新实用工具可以创建最多四个虚拟桌面,使用任务栏界面或热键预览每个桌面上内容并这些桌面...
  • Wiki还包含有关如何本地计算机上运行Siero。 贡献 为项目做贡献之前,请阅读并理解》。 执照 Siero已获得GNU通用公共许可证3.0许可。 简而言之,这意味着您可以自由地您自己项目中使用,修改或分发此...
  • 大势至共享文件审计系统

    热门讨论 2012-12-24 14:56:52
    5、大势至共享文件审计系统可以重要共享文件的修改、删除操作进行实时备份和实时还原,从而有效地防止了共享文件被误操作、恶意修改而丢失、损坏的情况。 6、大势至共享文件审计系统通过基于用户、IP地址、MAC...
  • ② 在数据进行处理时,各数据元素在计算机存储关系,即数据存储结构; ③ 各种数据结构进行运算。 2. 逻辑结构 数据逻辑结构是数据元素之间逻辑关系描述,它可以用一个数据元素集合和定义在此...
  •  1、记录服务器共享文件夹或共享文件的访问情况,包括读取、拷贝、修改、删除、重命名、打印等情况,可以记录访问者采用的登录账户、IP地址、MAC地址、时间、访问时长、具体操作情况等。  2、根据共享文件夹或...
  • 学习了MOOC计算机网络课程(华南理工)中第六章附录中套接字编程入门后(linux环境下TCP套接字),将一些基础知识和学习笔记记录在这里,并课程中代码做了修改,实现一个服务端可以同时连接多个客户端。...

    在学习了MOOC计算机网络课程(华南理工)中第六章附录中的套接字编程入门后(linux环境下的TCP套接字),将一些基础知识和学习笔记记录在这里,并对课程中的代码做了修改,实现一个服务端可以同时连接多个客户端。(原课程地址可以参考这里,这是上一次开课的视频,因为这学期最新的课程中还没有讲到这里。2020.5.11)

    Linux中有着“一切皆文件”的思想:

    即系统中的所有事物都可以当成文件(windows中不是文件的东西,比如进程、硬盘、显示器等,在linux中都被抽象成了文件)。这么做虽然对于普通用户来说不太友好,但是对开发者很方便,因为这样做屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户,你可以根据它们的目录,使用同一套API(open、close,write,read函数)来访问它们。

    套接字(socket)是什么:

    我们知道传输层实现端到端的通信,而套接字(socket)就作为传输层连接的这个端点,它有一个自己的主机IP地址和一个主机端口号。TCP/IP的核心内容被封装在操作系统中,我们如果要进行网络编程,就需要用到操作系统提供的套接字(socket)接口。

    对于Unix/Linux而言,套接字(socket)可以理解成是Unix/Linux中设定的一种特殊的文件,我们可以用open()、close()、write()、read()这种对于文件而言统一的API来访问它们,操作系统还为我们提供了一些对于socket而言常用的接口函数,如socket()、bind()、listen()、connect()等。

    总结:套接字(socket)可以理解成是网络编程中的API。具体到Linux系统中,它被封装成一种特殊的文件来使用,并提供了一系列操作这种文件的函数
    (说一句题外话,socket一词在计算机硬件领域中泛指插座,在软件领域中就指我们上文所讲的内容了,有一定的比喻的意思,表达建立网络连接就跟插插座一样。只是我觉得把socket翻译成“套接字”过于僵硬了。。具体关于socket名字的来源,可以参考这个回答

    文件描述符是什么:

    对于 Linux 的每一个进程,在内核空间都有一个与之对应的 PCB 进程控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。除了文件描述符表,系统还需要维护另外两张表:打开文件表(Open file table)和 i-node 表(i-node table)。
    文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示(左边是文件描述符表)。
    在这里插入图片描述
    如图可以发现这三种表都是结构体数组,0,1,2,3等是数组的下标。表头只是作者自己添加的注释,数组本身是没有的。实线箭头表示指针的指向。

    通过上图我们可以发现,文件描述符就是一个数组的下标,是文件描述符表的索引。通过文件描述符这个下标,可以找到对应的文件指针,通过该指针可以进入打开文件表。该表存储了以下信息:
    1.文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
    2.状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
    3.i-node 表指针。

    然而,要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表,该表包含了诸如以下的信息:
    1.文件类型,例如常规文件、套接字或 FIFO。
    2.文件大小。
    3.时间戳,比如创建时间、更新时间。
    4.文件锁。
    (这部分参考来自于https://blog.csdn.net/wan13141/article/details/89433379)

    课件视频中的内容:

    课件中简单介绍了socket的几种类型、客户端和服务端要完成的工作、函数调用的顺序等,在这里粘贴过来了一些关键部分的截图,具体可参考课件视频的讲解。

    socket的类型:
    在这里插入图片描述
    服务器和客户端要做的工作如下:
    在这里插入图片描述
    对应的函数调用顺序如下:
    在这里插入图片描述

    要用到的函数:

    这里简单介绍一下C语言中操作socket文件的一些函数,以及select函数。
    socket函数
    int socket (int domain, int type, int protocol)
    该函数类似普通文件的open操作,该函数返回打开的socket的文件描述符,后续的各种操作几乎都要用到这个描述符。
    第一个参数domain,取值AF_INET时代表IPv4协议,取值AF_INET6时代表IPv6协议。
    第二个参数type,表示套接字的类型。取值SOCK_STREAM时表示字节流套接字,取值SOCK_DGRAM时表示数据报套接字,取值SOCK_RAW时表示原始套接字。
    第三个参数protocol,取值IPPROTO_TCP时表示TCP传输协议,取值IPPROTO_UDP时表示UDP传输协议。
    返回值:成功时返回一个非负整数值,表示socket的文件描述符。

    bind函数
    int bind(int fd, const struct sockaddr *addr, socklen_t addrlen);
    将协议地址(32位的IPv4地址/或128位的IPv6地址 和 16位的端口号)与套接字绑定在一起,即把地址赋予给套接字。该函数一般在服务器端使用。
    第一个参数fd,是套接字socket的文件描述符。
    第二个参数addr,是一个指向sockaddr结构类型的地址变量的指针
    第三个参数addrlen,是这个地址变量的长度。
    返回值:返回0表示成功,返回-1表示不成功。
    注意:这里要说一下第二个参数里的地址结构类型。对于IPv4而言,我们用的是struct sockaddr_in类型的变量,该结构定义在头文件<netinet/in.h>中,具体如下:

    struct sockaddr_in {
        uint8_t sin_len;
        sa_family_t sin_family;
        in_port_t   sin_port; 
        struct  in_addr sin_addr;
        char    sin_zero[8];
    };
    
    struct in_addr {
        in_addr_t s_addr;           
    };    
    

    下面解释下ipv4套接字地址结构中每个字段的意义:
    sin_len:无符号短整数,用来指明套接字地址结构的长度。
    sin_family:无符号整数,用来指明套接字地址结构的地址族。 需要主动设置。AF_INET时表示采用IPv4协议。
    sin_port:无符号的16位整数,用来指明TCP或UDP的端口号. 需要主动设置。
    sin_addr:下面声明的结构体,用来存放IPV4的32-bit地址. 需要主动设置。
    sin_zero[8]:char类型。填充位,用来保证struct sockaddr_in和struct sockaddr size一致。编程时不需要填写。

    我们首先需要定义这个sockaddr_in类型的地址变量,如下图代码。
    由于输入32位ip地址太复杂了,直接输入点分十进制的ip地址较为简便,所以我们可以将我们输入的点分十进制地址(argv[1])通过inet_addr函数转化为32位地址。
    还有一个问题就是字节序的问题。字节序就是我们平常说的大端和小端模式,小端模式就是低位字节排放在内存的低地址端,大端模式反之。由于TCP/IP首部中所有的二进制整数在网络中传输时都是以大端模式的字节序(因此网络字节序是大端模式),所以我们要将主机字节序的端口号转换成为网络字节序的大端模式(如果你不确定你的主机字节序与网络字节序相同的话)。转换的方法是:首先argv[2]是我们输入的第二个参数即端口号,这是一个字符数组的类型,首先通过atoi函数将其转化成int类型,然后再通过htons函数将它从主机字节序转变成网络字节序(如下图)。

    struct sockaddr_in ser;
    
    ser.sin_family=AF_INET;//采用ipv4协议
    ser.sin_port=htons(atoi(argv[2]));
    ser.sin_addr.s_addr=inet_addr(argv[1]);
    

    在我们的ipv4套接字地址结构中,用到的是struct sockaddr_in类型,而其它的协议会有不同的地址结构类型,也就是说存在多种地址结构类型,而bind函数中的第二个参数指定的只有一种结构类型,是sockaddr结构类型(这是一种通用的结构类型),因此我们用到bind函数时需要对第二个参数进行强制类型转换。这样设计的意义就在于可以处理各种协议类型的地址结构,从而做到“协议无关性”。

    listen函数:
    int listen (int fd, int n)
    该函数只在服务器调用,该函数可以把套接字从CLOSED状态变成LISTEN状态,指示内核应接受指向该套接字的连接请求,此时该套接字就准备好监听来自客户端的请求了。
    第一个参数fd,是要进行监听的socket的文件描述符。
    第二个参数n,规定了内核应该为相应套接字socket排队的最大连接个数。具体来讲,内核为每个监听套接字维护两个队列:未完成连接队列(每个正处于三次握手中的TCP连接,套接字处于SYN_RCVD状态)和已完成连接队列(每个已经完成三次握手的TCP连接,套接字处于ESTABLISHED状态)。这两个队列之和不能超过n,否则无法建立TCP连接。SYN Flood攻击就是利用的这一点。
    返回值:成功返回0,失败返回-1

    connect函数:
    int connect (int fd, const struct sockaddr * addr, socklen_t addrlen)
    该函数在客户端使用,客户端通过该函数建立和TCP服务器的连接。一个注意的点是该函数参数中的地址要用服务器的地址。那么客户端本机地址什么时候输入呢?这个地址一般是由内核自动确定的,而不需要再用bind函数。
    第一个参数fd,是套接字的文件描述符
    第二个参数addr,是指向结构地址的指针,这里跟bind函数那里一样,需要做强制类型转换。
    第三个参数addrlen,是这个地址变量的长度。
    返回值:成功返回0,失败返回-1

    accept函数
    int accept (in fd, struct sockaddr * addr, socklen_t *addrlen)
    该函数的功能是返回已完成的连接。在客户端依次调用socket()、connect()之后就向服务器发送了一个连接请求。TCP服务器监听到这个请求并完成连接之后,accept()函数就可以返回这个已完成的连接。如果此时的已完成连接队列为空,即如果没有已完成的连接,那么会进入阻塞状态,直到有连接已完成。
    第一个参数fd,是服务器套接字的文件描述符
    第二个参数addr,是指向struct sockaddr结构类型的一个指针,这个结构会用来装等会返回的客户端的协议地址(该结构需要提前声明定义一下,内容可以不用填。注意声明的时候用IPv4地址结构的类型,即sockaddr_in类型的地址变量,然后在accept函数中强制转换成sockaddr类型)。
    第三个参数addrlen,这是一个地址长度的变量的指针,该指针会用来指向等会返回的客户端的地址的长度。如果对返回的客户端的协议地址不感兴趣,那么可以把第二个和第三个参数设成NULL。在课件视频中其实并没有用到返回的客户端的地址,所以视频中代码处可以改成NULL。
    返回值:如果成功则返回一个非负整数,是一个新的已连接套接字的文件描述符,这个新的套接字代表服务端与客户端之间的连接。失败时返回-1。
    注意:服务器一开始通过socket函数建立的那个套接字,是监听套接字,它只负责监听有无客户端的连接请求,该套接字只有一个。如果accept函数成功返回,则新创建一个已连接套接字,它表示服务端与客户端的连接,这种套接字可以有很多个,取决于有多少个客户端连接到服务器。

    close函数
    int close (int fd)
    参数fd是文件描述符。
    该函数可以用来关闭套接字,终止TCP连接。在多进程并发的服务器中,该函数只是将套接字的描述符引用计数减1。
    那么引用计数是什么呢?如果在父进程里创建一个新的描述符,那么它的引用计数是1,如果有一个子进程继承了父进程的这个描述符的话,那么引用计数就会增加1。
    如果只在父进程中创建了这个描述符而没有子进程继承的话,那么它的引用计数是1,此时调用一次close函数就会把引用计数减1变成0,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
    所以要尤其注意子进程会继承父进程的描述符,所以在多进程编程中可能需要多次使count–,或者使用shutdown()函数直接执行TCP终止。
    返回值:成功返回0,失败返回-1。

    通过前面的那几个函数,我们已经可以建立服务器和客户端之间的连接了,那么就可以进行网络I/O读写操作了,进行读写操作主要用到的是read()、write()等函数,在这里作简要介绍。
    read函数
    ssize_t read(int fd, void *buf, size_t count);
    其中fd是要读取文件的文件描述符,buf是把文件内容要读到的缓冲区,是一个指针,count是请求读取的字节数。正常情况下会返回读到的字节数,是ssize_t类型,即有符号整型(在32位机器上等同与int,在64位机器上等同与long int);如果返回0则说明读取时,读写指针已经达到文件末尾;如果返回-1则读取失败。
    注意:对于这类函数,读写时,文件读写指针( 记录文件中读到的位置 )会随读到的字节移动,如果读完一个文件之后,读写指针就会移动到文件末尾,如果要再次重头读的话,需要用fseek函数来把读写指针移动到文件的开头处。

    write函数
    ssize_t write(int fd, const void *buf, size_t count);
    其中fd是要进行写操作的文件的文件描述符,buf是需要输出的缓冲区,是一个指针,count是写入的字节数。正常情况下返回写入的字节数,出错则返回-1。

    recv和send函数
    它们提供了和read和write差不多的功能,不过它们提供了第四个参数来控制读写操作,更适用于套接字的通信。(课件视频中用到的就是recv和send函数,如果把它们替换成read和write函数也是完全可以的)
    int recv(int sockfd,void *buf,int len,int flags)
    int send(int sockfd,void *buf,int len,int flags)
    前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合:
    MSG_DONTROUTE 不查找表
    MSG_OOB 接受或者发送带外数据
    MSG_PEEK 查看数据,并不从系统缓冲区移走数据
    MSG_WAITALL 等待所有数据
    如果flags为0,则和read,write一样的操作(read对应于recv,write对应于send)。
    注意一下返回值,recv函数正常情况下跟read函数一样返回读取的字节数,但如果recv返回0说明在等待协议接收数据时网络中断了,这是与read函数所不同的地方。对于send函数,正常情况下返回值是跟write函数一样的,如果返回0说明写入了0个字节,即写入的内容为空。

    最后说一下select函数(较为复杂)
    select函数
    该函数主要用于我们的socket通信当中,这个函数可以监控指定文件的读写情况。比如当服务器给客户端发了一条消息,客户端的select函数就监控到这个套接字文件可以读了(因为服务器要想发消息给客户端,就要调用send函数对套接字文件进行写操作,套接字文件被写入了东西就可以读了,于是被select监控到)。
    int select (int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout)
    该函数一共有五个参数。
    第一个参数是一个int类型,它要比你要监控的最大的描述符至少大1(等会会讲为什么)。
    第二、三、四个参数,代表集合,即三个集合,这些集合用来装你要监控的文件描述符。如果要监控一个文件是否可读,就把它的描述符放到第一个集合readfds中;如果要监控一个文件是否可写,就把它的描述符放到第二个集合writefds中;如果要监控一个文件是否错误异常,就把它的描述符放到第三个集合errorfds中。比如在这次的代码中我们只需要监控文件描述符是否可读,而不需要关注是否可写是否异常,所以第三个和第四个参数不需要,设置成NULL即可。
    第五个参数timeout,表示监控时间。如果监控时间设置成0s 0ms,那么调用这个函数时就会立即返回(非阻塞函数),告诉你是否有文件可读或可写;如果设置成某一个具体的时间,那么如果有文件可读或可写就会返回,如果没有的话就一直等待直到超时再返回;如果把这个参数设置成NULL,那么就相当于把时间设置成无穷大(阻塞状态),如果没有文件可读可写的话,程序就会一直卡在这里等待,直到有文件可读或可写了再返回。

    第一个参数为什么要比监控的最大的描述符至少大1呢?第一个参数其实是指select能感知到的文件描述符的个数,如果它是10,那么select就能感知到0~ 9这十个文件描述符;如果它是15,那么select就能感知到0~14这十五个文件描述符。因此你应该明白了,如果你要监控的描述符最大是9,那么第一个参数就要至少比9大1,才能让select能感知到的范围包含你所有要监控的文件描述符。(讲道理我觉得linux中设计成这样挺鸡肋的,事实上windows中并不需要这么做)

    第二、三、四这三个参数,是一个定义的结构体struct fd_set,该结构体可以用来装文件描述符,可以理解成是一个集合。下面来简单介绍一下这个集合的数据结构,这个集合通常是用一个整数数组来实现,假设数组中的每一个整数都能转换成32位二进制数,那么每个整数都可以对应32个描述符,即如果这个描述符存在,那么二进制对应的位就是1,如果不存在就是0。那么数组的第一个元素对应于描述符0~ 31,第二个元素对应于描述符32~63,以此类推。

    在头文件中定义了一些宏函数,用来操作这个集合。结合刚才讲到的集合的数据结构,我们可以理解到操作集合的本质其实就是把数组中文件描述符对应的二进制位设置成0或1。
    FD_ZERO(fd_set *fdset):清空fdset指向的集合(所有位设置成0)。
    FD_SET(int fd, fd_set *fdset):把文件描述符fd加入fdset指向的集合(对应位设置成1)。
    FD_CLR(int fd, fd_set *fdset):把文件描述符fd从fdset指向的集合中删去(对应位设置成0)。
    FD_ISSET(int fd, fd_set *fdset):查看fd描述符是否在fdset指向的集合中,若在集合中则返回真。(查看对应位是0还是1)

    第五个参数,是struct timeval类型的结构体,用来代表时间值,有两个成员,一个是秒数一个是毫秒数。它的定义如下:

    struct timeval {
         long     tv_sec;     /* 秒数 */
         long    tv_usec;    /* 毫秒数 */
    };
    

    该函数的返回值:如果监测到了文件描述符集中有文件可读或可写,返回一个正整数,表示已就绪的总位数;如果定时器时间到了则返回0;如果出错则返回-1。

    注意:该函数返回后,会把描述符集合中那些不能读或不能写的描述符去掉,只留下可读或可写的描述符。比如我调用 select (sfd+1, &rdset, NULL, NULL, NULL) 成功返回后,说明rdset指向的集合中有文件可以读了,同时这个集合中那些不可读的文件描述符都会被删去(即对应二进制位置成0)。所以此时如果我想判断之前监视的那个文件是不是可读了,我只需要用FD_ISSET宏函数来判断这个文件描述符是不是在集合中(因为集合中留下的都是可读的文件)。接下来如果通过read、recv函数读完这个文件后,这个文件就变成不可读了(因为读写指针会随着读的过程而移动到末尾)。

    课程代码:

    知道了以上的基本原理以及函数API的作用,应该就可以很好地理解课程中的代码了。课程中的服务端是单进程的,所以服务端只能同一个客户建立连接。为了实现一个服务端可以和多个客户端建立连接,我对代码做了一些修改,把服务端改成了多进程。同时我为客户端和服务器加入了退出命令,即输入exit时可以退出(我觉得课程中通过Ctrl+C退出有些暴力…)。先贴出我修改后的代码(代码中的注释一些是原课件的,一些是我加的。注释较长,在网页中需要拉横滚轮,可能影响观看,建议先拷贝到自己的编辑器中再看)。

    服务端代码
    注意由于用到了信号量相关头文件和函数,所以在编译时记得加上-pthread。
    我新添加的内容如下:
    1.通过多进程实现一个服务端可以同时和多个客户端建立连接。实现方法是当监听到客户端的连接请求后,第一个进程负责通过fork创建子进程,子进程中接受连接请求并建立连接。也就是说只有第一个进程可以创建子进程,而每一个子进程,都代表着一个客户端和服务端的连接。
    2.申请了一块共享内存,目的是当有一个进程接收到exit命令后退出之后就会修改共享内存,然后其它进程每次while循环的开头都会检查这个共享内存区中的内容(因此select函数改成了非阻塞函数,不然就一直阻塞在这里而无法查看共享内存),可以检测到是否有其他进程退出,如果检测到的话自己也会退出,这样就避免了服务端只有一个进程接收到了exit指令退出,而其它进程不退出的问题,只要输一次exit指令,服务端的所有进程都会退出。其中涉及到共享缓冲区读写的地方用互斥信号量进行保护。(对于创建进程的fork函数、申请共享内存的shmget函数和shmat函数、创建信号量的sem_open函数等,如果不了解的话可以自行查阅相关资料)

    #include <signal.h>
    #include <sys/select.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <semaphore.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    int main(int argc,char* argv[])
    {
    	if(argc!=3)
    	{
    		printf("error args!请输入命令行参数ip和port:\n");
    		return -1;
    	}
    	int sfd;
    	sfd=socket(AF_INET,SOCK_STREAM,0);//服务器端生成一个socket描述符。AF_INET是IPv4协议;SOCK_STREAM是字节流套接字;0应该是TCP协议
    	if(-1==sfd)
    	{
    		perror("socket");
    		return -1;
    	}
    	struct sockaddr_in ser;//该结构体变量存储ip、port等socket信息数据
    	memset(&ser,0,sizeof(ser));//清空结构体
    	ser.sin_family=AF_INET;//采用ipv4协议
    	ser.sin_port=htons(atoi(argv[2]));//端口号,将主机字节序的port转换为网络字节序的port
    	ser.sin_addr.s_addr=inet_addr(argv[1]);//ip,将点分十进制ip转换为网络字节序的32位二进制数值
    	int ret;
    	ret=bind(sfd,(struct sockaddr*)&ser,sizeof(ser));//将socket描述符与服务器的ip、port绑定
    
    	if(-1==ret)
    	{
    		perror("bind");
    		return -1;
    	}
    	
        listen(sfd,10);
    	//使服务器的这个端口和ip处于监听状态,等待客户机的连接请求;10:同时能处理的最大连接请求数
    	
        int new_fd=-1; //new_fd是一个新的描述符,表示客户端和服务器之间的连接,由之后的accept函数返回。
    	struct sockaddr_in cli; //再创建一个结构体cli,用于之后的接收的客户端的信息。
    	int len=sizeof(cli);
    	char buf[128]={0};
        
    	fd_set rdset;//定义一个描述符集合类型的变量rdset
    	fd_set tmpset;//定义另一个描述符集合类型的变量tmpset,用来记录我们要监控的描述符
    	FD_ZERO(&tmpset);//先清空描述符集
    	FD_SET(0,&tmpset);//将标准输入注册为要监控的描述符
    	FD_SET(sfd,&tmpset);//将sfd注册为要监控的描述符
    	
        pid_t process_id = getpid(); //获得本进程id
        
        struct timeval t; //t是select函数中的最后一个变量,在这里相当于把时间t设成0
        t.tv_sec=0;
        t.tv_usec=0;
    
        int shmid; //接下来申请一块共享内存,如果服务器某一个进程退出的话,其他进程就会发现,于是让所有进程都退出。
        int *ShareAddress;
        shmid = shmget( 123, sizeof(int), IPC_CREAT|0777); 
        if (shmid == -1){
            printf("Error!\n");
        }
        ShareAddress = shmat(shmid, NULL, 0);
        ShareAddress[0]=0; //共享内存中的内容初始化为0
        
        sem_t *mutex; //定义一个互斥信号量
        mutex=sem_open("mutex",O_CREAT,0777,1);
        
        while(1)
    	{
            ShareAddress = shmat(shmid, NULL, 0);
            sem_wait(mutex); //使信号量的值减1,进行互斥保护
            int flag = ShareAddress[0];
            sem_post(mutex); //释放信号量
            if(flag) //查看该值,如果是1则说明其它进程已经检测到要退出了,于是本进程也退出。
            {
                break;
            }
            FD_ZERO(&rdset);//memcpy函数是从存储区str1复制n个字符到存储区str2。之后会覆盖原来的内容,所以这句话其实可以去掉。
    		memcpy(&rdset,&tmpset,sizeof(fd_set));//将需要监控的描述符集拷贝到rdset
    		ret=select(11,&rdset,NULL,NULL,&t);//select函数监控rdset描述符集中相关文件的读变化
    		if(ret>0)
    		{
    			if(FD_ISSET(sfd,&rdset) && process_id == getpid())//检查sfd是否发生读变化,即是否侦听到客户端的链接请求
    			{
                    if (!fork())
                    {
                        new_fd=accept(sfd,(struct sockaddr*)&cli,&len);
                        //接收客户端的连接请求,并返回一个新的socket描述符,用于标识服务器与这个特定客户端的连接
                        if(-1==new_fd)
                        {
                            perror("accept");
                            return -1;
                        }
                        FD_SET(new_fd,&tmpset);//将该新的描述符加入监控集
                        printf("Server:已成功连接到客户端%s:%d,您可以向客户端发送信息了!\n",inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));                
                    }
    
    			}
    			
                if(FD_ISSET(new_fd,&rdset) && process_id != getpid())//检查new_fd是否发生读变化,即客户端是否发送信息过来
    			{
    				memset(buf,0,sizeof(buf));
    				ret=recv(new_fd,buf,sizeof(buf),0);//接收客户端发来的信息,传到buf里面
    				if(ret>0)
    				{
    					printf("Client:%s\n",buf); //打印出来buf中存入的客户端发来的信息。
    				}
                    else if(ret==0)
    				{
    					printf("byebye,客户端已下线!\n");
    					//close(new_fd);
    					FD_CLR(new_fd,&tmpset);//从集合当中移除该new_fd
    				}
    			}
    			
                if(FD_ISSET(0,&rdset) && process_id != getpid())//检查服务器的标准输入是否发生读变化,即服务器是否有信息要发送给客户端
    			{
    				memset(buf,0,sizeof(buf)); //先清空buf
    				ret=read(0,buf,sizeof(buf));//读取标准输入的输入信息放到buf
    				if(ret>0)
    				{
                        int c = strcmp(buf, "exit\n");
                        if(c == 0)
                        {
                            printf("byebye,客户端准备退出,聊天即将关闭!\n");
                            ShareAddress = shmat(shmid, NULL, 0); //修改共享内存中的值,这样其它进程就可以看到有进程退出了
                            sem_wait(mutex);  //使信号量的值减1,进行互斥保护
                            ShareAddress[0]=1;
                            sem_post(mutex); //释放信号量
                            break;
                        }
    					ret=send(new_fd,buf,strlen(buf)-1,0);//将标准输入的信息发送给客户端
    					if(ret< 0)
    					{
    						printf("Server:发送失败!\n");
    					}
                        else if(ret==0)
                        {
                            printf("Server:您未输入任何信息,请重新输入!\n");
                        }
    				}
                    else
                    {
    					printf("Server:输入失败,请重新输入\n");
    				}
    			}
    		}
    	}
    	close(new_fd);
    	close(sfd);
    	return 0;
    }
    

    客户端代码
    客户端代码部分新添的东西不多,就是添加了一个exit退出命令,跟服务端的实现方式一样,就是输入字符串后先判断是不是“exit”,如果是的话就直接退出。

    #include <signal.h>
    #include <sys/select.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <string.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main(int argc,char* argv[])
    {
    	if(argc!=3)
    	{
    		printf("error args!请输入命令行参数ip和port:\n");
    		return -1;
    	}
    	int sfd;
    	
        sfd=socket(AF_INET,SOCK_STREAM,0);//客户端生成一个socket描述符
    	if(-1==sfd)
    	{
    		perror("socket");
    		return -1;
    	}
        
    	struct sockaddr_in ser;//该结构体变量存储ip、port等服务器端的socket信息数据
    	memset(&ser,0,sizeof(ser));//清空结构体
    	ser.sin_family=AF_INET;//采用ipv4协议
    	ser.sin_port=htons(atoi(argv[2]));//端口号,将主机字节序的port转换为网络字节序的port
    	ser.sin_addr.s_addr=inet_addr(argv[1]);//ip,将点分十进制ip转换为网络字节序的32位二进制数值
    	int ret;
    	//向服务器发出连接请求,将客户端生成的sfd描述符信息连接到ip、port指定的服务器上去
    	ret=connect(sfd,(struct sockaddr*)&ser,sizeof(ser));
    	if(-1==ret)
    	{                                                
    		perror("connect");
    		return -1;
    	}
    	printf("Client:已成功连接到服务器,您可以向服务器发送消息了!\n");
    	char buf[128]={0};
    	fd_set rdset;//定义一个监控描述符集
    	
        while(1)
    	{
    		FD_ZERO(&rdset);//清空监控描述符集
    		FD_SET(0,&rdset);//将标准输入加入监控
    		FD_SET(sfd,&rdset);//将与服务器通信的sfd加入监控,以监控服务器是否有消息传送过来
    		ret=select(sfd+1,&rdset,NULL,NULL,NULL);//用select函数将监控描述符集rdset的读变化
    		if(ret>0)
    		{
    			if(FD_ISSET(sfd,&rdset))//检查sfd是否有读变化,即判断服务器是否有信息发送过来
    			{
    				memset(buf,0,sizeof(buf));
    				ret=recv(sfd,buf,sizeof(buf),0);//接收服务器发送的信息,存入缓冲区buf
    				if(ret>0)
    				{
    					printf("Server:%s\n",buf);
    				}
                    else if(ret==0)
    				{
    					printf("byebye,服务器已下线,聊天终止!\n");
    					break;
    				}
    			}
    			
                if(FD_ISSET(0,&rdset))//检查标准输入是否有读变化,即检查客户端标准输入是否有用户输入
    			{
    				memset(buf,0,sizeof(buf));
    				ret=read(0,buf,sizeof(buf));//读取标准输入
    				if(ret>0)
    				{
                        int c = strcmp(buf, "exit\n");
                        if(c == 0)
                        {
                            printf("byebye,客户端准备退出,聊天即将关闭!\n");
                            break;
                        }
    					ret=send(sfd,buf,strlen(buf)-1,0);//将用户输入信息发送给服务器
    					if(ret<0)
    					{
    						printf("Client:发送失败!\n");
    					}
                        else if(ret ==0)
                        {
                            printf("Client:您未输入任何信息,请重新输入!\n");
                        }
    				}
                    else
                    {
    					printf("Client:输入失败,请重新输入\n");
    					break;
    				}
    			}
    		}
    	}
    	close(sfd);
    	return 0;
    }
    

    一些注意的点:

    1.父子进程之间变量是否共享。
    对于所有变量来说,如果不修改(不重新定义它、不进行写操作)而是只读取它的值的话,那么这个变量是父子进程共享的(子进程用的还是父进程的,没有创建副本);一旦修改了它的值,那么子进程就会自己创建一个副本,此时父子进程就不能共享这个变量了。

    但是对于指针等类型的变量,如果要保证父子进程共享,那么不仅不能修改指针变量本身,而且指针指向的内容也不能修改。下面将介绍一下文件描述符和普通指针的共享关系。

    对于父进程打开的文件描述符,子进程如果不修改描述符,而是通过描述符来修改描述符所指向的文件,那么父子进程是共享这个描述符的(因为调用fork之后,只拷贝了PCB本身,拷贝的只是指针,没有拷贝指针所指向的内容,这种情况叫做浅拷贝)

    对于普通指针,跟文件描述符有所不同,即使子进程不修改指针而是修改指针指向的内容,那么父子进程就不能共享这个指针了(比如int a = 1 ; int p = &a 那么此时在子进程中用p=2的话,那么p指向的内容就又不能共享了,因为此时子进程拷贝指针的同时也拷贝了指针所指向的内容,与之前打开文件描述符的情况有所不同)

    2.程序测试情况:
    注意由于服务端代码用到了信号量相关头文件和函数,所以在编译时记得加上-pthread。

    在一台电脑上测试的话,是打开多个ubuntu窗口,IP地址用的是环回地址,这里再简单介绍一下环回地址:环回地址是主机用于向自身发送通信的一个特殊地址(也就是一个特殊的目的地址)。同一台主机上的两项服务若使用环回地址而非分配的主机地址,就可以绕开TCP/IP协议栈的下层。(也就是说:不用再通过什么链路层,物理层,以太网传出去了,而是可以直接在自己的网络层,传输层进行处理了)。因此,ping 127.0.0.1一般也作为测试本机TCP/IP协议栈正常与否的判断之一。

    我修改版的实现了一个服务端连接多个客户端,但目前有一个问题是当服务端向客户端发送消息时(如果有多个客户端的话),那么消息就不一定发送给哪个客户端了。解决方法可以是每次服务端发送消息时都先指明发给哪一个客户端,然后让各个进程先读入字符串判断要发给哪一个客户端后,再进行相应的操作。只是感觉这样并不是一个好方法,对于用户来讲有些繁琐,如果要完美地实现一对多的话,我觉得需要重头修改了。

    3.我在写代码的过程中犯的一些错误:
    忘记了C语言比较两个字符串是否相等不能直接用= = ,因为字符串变量代表的是一个地址,如果用==比较的话,比较的就是地址是否相等而不是地址中的内容是否相等。因此要用int c = strcmp(a, b)这样比较两个字符串的大小,返回值c是0则说明a和b相等。

    还有一个犯的错误就是悬浮指针。int * p; * p=7;这样不行。因为这里的p还没有指向,p没有对应的地址值的,是一个悬浮指针,不能直接赋值的。应该先进行形如int a = 0;int * p = &a 这种,此时指针p就不是悬浮指针了,就可以用*p = 这样来赋值了。

    本文参考的文章和资料
    【1】慕课计算机网络(华南理工)https://www.icourse163.org/learn/SCUT-1002700002?tid=1206622278#/learn/content?type=detail&id=1211470387
    【2】socket为什么要翻译成套接字https://www.zhihu.com/question/21383903/answer/64103663
    【3】linux文件描述符是什么https://blog.csdn.net/wan13141/article/details/89433379
    【4】套接字函数入门1 https://zhuanlan.zhihu.com/p/54706273
    【5】chapter3 套接字编程简介https://zhuanlan.zhihu.com/p/95496500
    【6】什么是select函数https://zhuanlan.zhihu.com/p/57439225
    【7】linux socket编程https://zhuanlan.zhihu.com/p/73840903
    【8】socket编程中write、read和send、recv之间的区别https://www.cnblogs.com/kex1n/p/6501970.html
    【9】select函数详解https://www.cnblogs.com/lvchaoshun/p/9478175.html
    【10】LINUX下的FD_SET宏介绍https://www.cnblogs.com/xianzhedeyu/archive/2013/09/02/3296675.html
    【11】为什么linux select函数的第一个参数总应该是fdmax+1 https://blog.csdn.net/stpeace/article/details/73612532
    【12】父子进程间文件共享https://blog.csdn.net/qq_38623623/article/details/78416216

    展开全文
  • 桃源文件系统v3.3

    2014-08-04 12:12:37
    软件介绍:《桃源企业文件管理系统》适用于任何机构内部或内外之间的电子文档存储管理、网络服务、传阅签收、公文审批等业务流程,便于机构全体、部门、个人的电子文档共享,有关...,实现对文件的严格管理与可控共享...
  • 一般只修改头指针,但是删除结点若为最后一个时,则要重新尾指针赋值。 一个递归算法必须包括() A.递归部分 B.终止条件和递归部分 C.迭代部分 D.终止条件和迭代部分 D 略 串是一种特殊线性表,其...
  • 《桃源企业文件管理系统》适用于任何机构内部或内外之间的电子文档存储管理、网络服务、传阅签收、公文审批等业务流程,便于机构全体、部门、个人的电子文档共享,有关文档按机构、...,实现对文件的严格管理与可控共享...
  • 2. 顺序文件采用顺序结构实现文件的存储,大型的顺序文件的少量修改,要求重新复制整个文件,代价很高,采用( )的方法可降低所需的代价。【北京邮电大学 2000 二、8 (20/8分)】 A. 附加文件 B. 按关键字大小...
  • 每个Win98特殊东东都有一个提示,比如把鼠标停留电脑,就会有个提示你我电脑是什么,改变这个提示并不难,到HKEY_CLASSES_ROOT\CLSID\{20D04FE0-3AEA-1069-A2D8-08002B30309D},修改右边InfoTip...
  • 20、文件加解密:提供上传文件自动加密,使管理员直接服务器无法查看文件内容,只有有权限人通过本系统下载才能查看。 21、文件全文检索:可搜索到TXT、word、excel、PowerPoint等格式详细文件内容文字,...
  • 机器上可这些源代码进行修改并运行它们。 用于测试实例系统 不幸是所有操作系统都不断变更,Unix也不例外。下图示出了系统V和4.XBSD最近进展情况。 4.XBSD是由加州大学贝克莱分校计算机系统研究...
  • 记录windows登陆密码

    2009-09-09 12:29:18
    程序不同系统做了处理,2000,2003,xp,vista都可以截取, 2000,2003,xp中,通过UNICODE_STRING.Length 高8位取xor key,如果密码是编码过,则通过ntdll.RtlRunDecodeUnicodeString解码, vista则通过...
  • 系统硬盘中MSDOS.SYS内容比较完整,包括必要启动配置命令,文件长度必须大于1024 bytes,即占用两个以上磁盘扇区,这一要求文件中说明为保证兼容性,但从未见更深入介绍,实际上文件小于1024 bytes系统...
  • 我们都知道磁盘工作时是转动,它所存储信息是按一系列同心圆记录在其表面上的,每一个同心圆称为一个磁道,一个整圆环为一个磁道,一个磁道有若干个扇区,所以我们不难看出,越靠外磁道上的单个扇区其体积...
  • 版本控制、Git和Domino

    千次阅读 2014-09-06 12:20:42
    而版本的想法和其的管理更是在计算机出现之前的印刷以至手写时代就一直存在。版本是作品生长衍变的徵象。手写和印刷时代的文字作品之还仅是记录的修改生长。及至计算机和电子文件出现,文件大小、内容复杂程度、...

    版本控制

    一提到版本控制,程序员们就会想到他们工作中用到的各种源代码控制工具,Subversion、Git、Mercurial……实际上源代码之外的许多其他文件也会有版本控制的需要。而版本的想法和对其的管理更是在计算机出现之前的印刷以至手写时代就一直存在。版本是作品生长衍变的徵象。手写和印刷时代的文字作品之还仅是记录它的修改生长。及至计算机和电子文件出现,文件大小、内容复杂程度、版本更新速度都空前增加。除了单纯记录不同版本,版本控制又被提出许多新的功能需求:恢复到任一历史版本、比较两个版本之间的差异、创建多个分支版本、合并不同版本。尤其是对程序员而言,这些功能对于调试错误、了解代码演变、多人合作以及为多个用户和目的开发等任务都大有裨益。

    Git

    与其他软件工具一样,版本控制系统(VCS即Version Control System,或RCS即Revision Control System)也伴随着实际需要不断演化生长。起初所有被管理文件的数据都保存在本机的存储库(repository)里,后来进化成客户端服务器模式,存储库保存在服务器上,版本控制系统从而由单机版变成了可多人合作的网络版。这样的架构唯一的缺点是只有服务器上保有文件的完整历史数据,每个客户端仅有当前工作的版本。这样在多个版本之间的切换、比较、合并等操作都需要连接服务器查询和传输数据。此外一旦服务器上的存储库被损坏,所有的历史积累将无法挽回。为了更加适应多人自由开发的环境,分布式版本控制系统(DVCS即Distributed Version Control System)诞生了。就像分布式数据库系统在不同节点上有单个数据库的多个副本一样,每个DVCS用户机器上都保存了完整的存储库。在无联网的单机状态下,程序员也能使用版本控制的所有功能,联网时则可以同服务器上的存储库同步。

    Git就是一款优秀的分布式版本控制工具。它最初是为了Linux的程序员开发系统而编写的,现在已经成为,特别是开源领域,最流行的版本控制工具。GitHub即依讬此系统成为最大的源代码托管网站。与前驱相比,Git有一些特别的理念,例如每个版本保存的不是文件的差异,而是完整的文件(如果某个文件没有改变,则包含对上个版本文件的引用)。网上有很多优秀的介绍和教材,例如Git官方网站上的文档http://www.git-scm.com/doc还有这篇通俗易懂、图文并茂的git concepts simplified

    Domino

    在Notes杂谈系列文章里,我反复提到和讨论了Domino作为专有的(proprietary)开发平台的独特性,就像与世隔绝的科莫多岛上孕育出奇异的科莫多龙一样。在版本控制领域,Domino也有独特的“国情”。

    困难

    表单视图等设计元素都是以二进制格式保存在数据库里。与文本格式的文件相比,二进制文件在比较版本差别和合并方面都有困难。DXL的发明使得在设计元素和XML文件之间相互转换成为可能。但是长时间来DXL有很多bug没解决,不能保证相互转换的精度。直到R9面世,IBM才对DXL有了信心,在Designer里加入基于它的版本控制特性。

    在主流的采用某种编程语言开发的环境里,文件都是文本格式,版本控制系统只需处理文件和文件夹。Domino平台下要达到版本控制系统的这个初始环境就大费周章。Designer先将数据库里的设计元素映射到一个虚拟的文件系统(virtual file system)里,例如一个表单Person就会被映射到数据库路径下的文件Forms\Person.form。Desinger迁移到Eclipse平台上后,一个Domino应用程序作为一个Java项目来校验、编译,利用Eclipse的种种视图和编辑器来显示和修改设计元素,Eclipse读写的就是这个虚拟的文件系统。在设计元素到文件的转换过程中,表单、大纲这些以用户界面为主的元素都经过DXL转换成XML文件;脚本库(Java脚本库是个例外)、Java设计元素、XPages这类基本为了容纳代码的设计元素被解析成两个甚至更多文件,一类是容纳代码和配置本身的文本文件,另一个是记录设计元素元数据的metadata文件。例如一个名为task的XPage会被转换成task.xsp和task.xsp.metadata,前者的内容就与在XPages编辑器的源代码视图里看到的一模一样。

    为了让Designer外部的版本控制工具能够访问到这个虚拟文件系统,R9增加了一组features,能够设定一个本机的文件夹,使其中的内容与虚拟文件系统同步。如此,版本控制系统面对的就是与其他项目无异的文件系统,它对此文件系统的内容做的任何改变,如切换版本、合并又会被Designer检测到并自动转换回设计元素。

    经过这种对用户透明的设计元素与文本文件之间的自动双向转换,就能对Notes应用程序运用任何版本控制工具。不过二进制格式带来的问题并没有完全解决。版本控制工具可以比较某个表单两个版本之间的差异,但其结果是Notes程序员不习惯和不易阅读的XML。原因就是Domino并没有被改造成以XML的格式来记录设计元素,DXL只是勉强将它们输出为XML,许多内容仅仅是以<rawitemdata>元件将二进制的内容以字符编码显示。例如表单的布局信息记录在$Body域里,转换成XML后看不到HTML那样的DIV、TABLE,而是一堆字符编码。

    <item name='$Body'sign='true'><rawitemdata type='1'>gQKC/1oAAQAAAAAAAAAAAKAFAACgBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAJSHAAAAAAAAAAAAAAAAAQAAAAQAAAC7/zIAAQAYANLVpGPEnCQAAAAWAAEABwBbQWRtaW5dAOcAdQIDAAcADAAFAAkwUzBFAIMEAQCF/xUAAQAACkFkbWluV3JpdGVyczoA3QYAAIr//v9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACK/1QAjwABBQYgAgAAAAAAAAIBAAAKJAAAAAAAAAAMAAAAAAAkAAAAFgD0AAEABwBbQWRtaW5dAB8CAwAHAAwABQAJMFMwRQBBZG1pbldyaXRlcnPeBgAAiv+F/wgAAQAACoECgwQBAIX/EAABAAAKV3JpdGVyczrdBgAAiv/+/1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA……</rawitemdata>

    这样的XML是有名无实的,与二进制格式差别不大,不是供人阅读的,只有转化回设计元素才能由Notes客户端或服务器解读。版本控制工具比较出的差异若是位于这样的字符编码实体内,程序员根本无法判断其含义。同理,同一设计元素的合并遇到冲突时,程序员也无法直观地了解内容以化解。只有对上举代码类设计元素,比较与合并功能才能发挥威力。

    综上可见,Domino应用程序的版本控制,最多只在单人开发时好用,团队开发场景下常见的比较和合并开发人员各自的分支版本都会时时受限。

    需求

    Domino应用程序的源代码管理从未被很好地解决过,但是Notes程序员似乎不以为意,甚至有很多人从来没意识到Git之类的版本控制系统之需求。原因同样来自Domino平台的特殊性。作为一个快速开发环境,Domino提供了现成的用户管理、权限管理、与邮件的集成以及视图等等基础功能(infrastructure),省去了大量的用户代码。另一方面,数据库与体系的限制使得Domino只适于做某些领域的开发,做出的系统也不会像通用语言如Java的大型项目那样庞大。所以一个Domino应用程序的代码量较少,往往都是一个程序员包办。最后,Domino应用程序的代码高度耦合,XPages引入之前,很多系统的数据设计、用户界面、业务逻辑全都混在一起,即使采用XPages,还是要有良好的设计才不至把视图层和业务层混合。代码模块化程度不高,也给多人同时开发造成障碍。所以即使在高度依赖Domino的大公司,需要团队开发的项目也罕见(当然一个项目团队有多人分别负责需求、测试和开发是很常见的),即使是这样的项目,也是多个数据库分人负责。

    总之,一方面Domino应用程序规模小,可视化程度高,多人合作开发的需求不大;另一方面在Domino中应用版本控制系统过去困难重重,现在效果不充分。这并不是说,Domino应用程序就一直没有过版本控制,许多公司也开发了内部使用的系统管理Domino数据库的版本。但这类系统是囫囵吞枣,将整个数据库作为附件或文件保存。既无法高效地切换版本,更不能自动比较差异和合并代码。Domino平台与版本控制系统绝缘了几十年,在可见的将来也会继续独行下去。

    Domino+Git

    上面分析了,Domino Designer自R9起,对一个应用程序应用外部的版本控制系统成为可能。国外有一些热心人还开发了Designer与Subversion等工具集成的插件。不过我在上面已经泼了冷水,这样的版本控制,单人开发时用来管理历史设计可以,要发挥版本控制的更多功用就会遇到掣肘。这一节就介绍一下Domino+Git的使用方法。

    在导航视图内选中要启用版本控制的应用程序,在右键菜单中选择Team Development,子菜单中的第一项Set Up Source Control for this Application,在随后的对话框里选定一个文件夹用来容纳该应用程序被转换输出的文件,如D:\Lotus\Notes\Data\workspace\task。Domino一侧的设置就完成了。该子菜单中的其他几项顾名思义都很好理解。

    接下来就可以用Git做上面所说的能做的事。运行Git控制台(Git shell),转入D:\Lotus\Notes\Data\workspace\task(在其中手工创建一个过滤文件.gitignore)。

    git init:创建了一个新的存储库。

    git add .:将所有文件编入index。

    git commit –m initial:第一次提交。你的Domino应用程序已经开始被Git管理了。

    git branch:可以看到默认创建的master分支。

    在进行版本管理的过程中,有意思的是可以从一组文件的视角来看待Notes应用程序的组成,以及每次修改带来哪些文件的变化。有兴趣的朋友还可以将自己的存储库推送到GitHub上。

    展开全文
  • JAVA百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
    2、对文件的加密,解密输出到文件 利用随机函数抽取幸运数字 简单 EJB的真实世界模型(源代码) 15个目标文件 摘要:Java源码,初学实例,基于EJB的真实世界模型  基于EJB的真实世界模型,附源代码,部分功能需JSP...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 606
精华内容 242
关键字:

在计算机上对文件的修改记录