P13:第12讲 互斥量、条件变量和信号量的复习 - main - BV1ED4y1R7RJ
好的,我们可以开始了。欢迎大家。那么期中考试已经结束了,希望大家都做得还不错。接下来我会有几点关于考试的评论。然后是斯坦福 Shell,希望大家进展顺利。如我所说,如果你还没有开始,赶紧开始吧。对于已经开始的同学,你们应该意识到,这个作业有很多部分。
还有很多要思考的东西。并且有很多关于 Piazza 的问题,很多关于作业的问题。所以希望大家都没问题。我们这周会有很多助教。考试的截止时间是星期三晚上,还是星期四晚上?你说是星期四晚上对吧?那我们就说,稍后再讨论。
几天前。我们看看。关于期中考试,我有几点评论。你们应该已经收到了期中成绩。如果没有,请告诉我,这样我们可以确保分数能够发放。总体来说,我对结果还是挺满意的。我认为,里面有几个问题我知道会比较难。还有几个问题。
我认为应该每个人都能做好的问题,结果大部分同学确实做得不错。关于文件系统的题目,我觉得应该算是比较开放的,但也挺直接的。大多数人做得还不错。问题1D,实际上问题1是最具挑战性的问题,这也是它的设计目的之一。1D题。
让我们倒着顺序讲。第一题是关于“如何将这些输出去交织?”的问题。真的能去交织这些输出吗?
答案其实并不是很明显。我们设置输入输出的方式使得它们必须直接关联在一起,这样无论你想出的解决方案是什么,都几乎不可能实现,除非允许你先收集所有数据,再传输到一个输入,接着传到另一个输出,依此类推。我的思路是这个问题的关键在于你不知道有多少数据。
数据正在进来,但你不允许在开始传输之前把它全部存储好,这样做是不可行的。很少有人在D题部分做对了,但没关系,考试中总会有这样的情况。一定要去看看,如果你对答案有疑问,别担心,我会帮助你理解。
在评分时,一些助教开始头痛,想“等等,这是什么?”他们在黑板上做了演示,最后自己也明白了。所以这是一个很有挑战性的问题。至于实际代码,涉及两个问题。首先,你知道我们怎么用管道去做,然后你有像 FDS 和 O,Clo,ex 这样的东西。
EC 或者其他东西。你知道怎么做,然后你就不必关闭一些,但你可能需要关闭其他的或者什么的。我们当时根本没有担心这些。好了,考试时。这里有太多像是边缘案例,你可能做其中之一或者另一个,我们只是装作没事。大家都用了 O 管道,我们假装每个人都关掉了所有东西。
在子进程内部正确处理。你必须正确关闭两个管道,或者说,正确关闭两个脚本,这些是我们在乎的。但是在子进程内部,如果你去了,假设你说,哦,我不确定自己做对了,那也没关系。我们不为此担心。还有其他的事。
大家经常忽略的一点是,我们没有说你应该等待 PID,直到子进程结束。我们还是来看看程序,实际问题是什么。好的,或者我应该说,我们来看看实际代码。
你可能已经或者没有输入的部分。稍后提醒我。好了,然后接着。
然后两个输出。cc。好的,你为实际的管道等写了所有这些。
你做了所有这些,而你写的地方在哪里?你是写在主程序中,对吗?
你正在编写这个程序的主要部分,叫做双回声。
在这里,好的,这里叫做双回声,这是你所做的。
然后主程序结束。如果你忘记了加权 PID,就会发生这种情况。
它应该是这样的。让我们看看。两个输出。做同样的例子来排序,然后我会管道一个小的测试输入。好了,这其实相对不错,因为它确实排序了,并且 WC(单词计数)实际上先执行了,但也不一定。它可以在中间任何地方执行。
那是交错部分。但是你会注意到,提示并没有在两个子进程完成之前返回。这才是你在程序中想要的。你希望程序能够正确结束,让提示能够返回。好了,如果我们去掉那些。
两个加权 PID,好的,来看看到底会发生什么。好吧,这并不能保证一定会发生,但是。
让我们看看这里会发生什么。我会放,哎呀,放,这样就好了。两个输出,做同样的事情。
在之前。我们把它放在屏幕顶部。好了,看这里。发生了什么。上面我们得到了提示,然后程序就开始执行,然后就停在这里了,像这样。那是一个行为良好的程序吗?不,挺差劲的。所以我们大概是希望你能意识到,哦,如果我在写主程序并且做这两个输出的事情来处理子进程。
你应该等待子进程。所以,我对没有理解这一点的人表示抱歉。
你应该理解这就是你想做的事情。好的。如果你有其他问题,仍然有一些重新评分请求。我们对重新评分请求相对严格,你不能仅仅因为有正反两面,就说“我其实是这个意思”,并试图解释你真正的意思。如果没有出现在页面上,那就算了。还有,另一个问题。
有些人说,啊,我写了125个单词,你扣了分,什么的。我有点提醒过你们,答案应该控制在100个单词左右。所以如果你写了多一点,我们并不介意。如果你确实想要重新评分请求,我会去数一下你的单词数并做出相应处理。
这是一个决策,但在这种情况下,120可能太多了。问题。 >> [听不清] >> 好问题。问题是,嘿,等一下。我不知道这个管道填满的事儿。那是对的吗?还是管道的事?实际上是的,它是一个管道。它是一个内置的Linux问题,管道会填满。它们只有这么多空间。
在Linux中,管道的空间。它的作用是,通常不会打破东西。虽然如果你不了解的话,就像这段代码,如果我们尝试并行解交错,它会破坏我们的代码,因为我们从未真正讨论过这个。没有。我们没有从这个角度讨论过。可能在Piazza上有过一个问题。也许是上个学期的事。
但关键是,管道会填满。它们会怎么做?它们会阻塞,直到情况稍微好转。如果因为某种原因你做了一些奇怪的事情,比如那个程序,试图一次性收集所有数据,那可能会是个问题。所以我在考试中给了这个题目,是为了让大家知道,“嘿,这是个新问题,你怎么看?”
管道可能会填满。你觉得怎么样?这就是我为什么把它放在那里。是这个意思吗?不,你不知道管道会填满。不是,我是说,你可能知道。期中考试上说过。哦,是的,确实有一部分。哦,抱歉。是的,抱歉。米奇,回去看一下期中考试。它确实说了,那个就是重点。
是的,管道可能会满。抱歉,你没有读到那个问题。一个管道可能无法物理上关闭文件。没有,问题很好,当管道填满时文件会关闭吗?不,不会,管道会阻塞并说,哦,如果你想写入更多数据,我不能接受更多数据,直到。
其他人阅读后,你就可以阻塞。我会做的。今天我们会看到一个类似的例子,关于线程中的情况,这里有一个读写者的“舞蹈”,必须进行一些操作,因为你只有有限的空间,你需要弄清楚该如何处理。这个问题问得很好,非常好。
关于主函数的一个更技术性的问题。是的。
所以在我的手机上,我有一个循环。你有一个循环。它大致围绕这个展开。好的,那个。
可能是可行的。当然,它仍然是这样,我没搞明白。哦,放一个回溯问题。如果你认为那是对的。那是做循环中等待 PID 的问题,你检查所有子进程的等待是完全合法的。你绝对可以这么做。所以为此放一个回溯问题。还有其他关于这个问题的问题吗?我不想进入太多的细节。
关于考试,如果你认为我们批改错了,请告诉我们。正如我说的,我们会尽力做到公平,但我们也不会让你重新做测试并在重新评分过程中作弊。你们都经历过这个过程了。好了,明白了。正如我所说,期中考试整体来说还不错,我认为。
这次期中考试很有挑战性,但总体来说大家表现还不错。如果你发挥得不好,随时可以给我发邮件,我们可以讨论一下如何改进,稍微提升一下。而且你们还要准备期末考试和其他作业等等。好的,明白了。
我知道你们都在想斯坦福 Shell,它和线程没什么关系。但我发现上周你们都在想,天啊,我有期中考试要来了,我们复习了互斥锁、条件变量和信号量,都讲得非常快。我想做的是回过头来复习一下这些内容。让我。
你可以把你可能还有的问题或者新问题提出来。然后我们再看几个小的例子。好的,今天我不会做实际的现场操作。我只会提供一些例子,我们在讨论时一一讲解。好的。在多线程中,我们必须具备处理竞态条件的能力。我们必须有这个能力。
使得两个或多个线程能够在同一段代码中或以不会破坏数据结构的方式操作数据结构。这是你必须处理的问题。竞态条件在做这些操作时总是会发生。线程就像,多线程交叉。我们有几种不同的方法来解决这个问题。
做这些事情。我们上周大致讲过这些。主要的那个是,这是大多数这些情况的基础,就是互斥锁。好的,我们将讲解细节。我会在这里列出这些内容。互斥锁是我们学到的第一个,我们稍后会看到它的例子。条件变量,任何互斥锁都有。
没有能力让两个线程相互通知另一个线程它们已经完成。对,现在,信号可以通过其中一个线程释放锁,另一个线程获得锁的方式发生。从某种意义上说,这是一个信号,但它不是那种明确的,像“嘿,任何正在等待的人,都可以继续”的信号。所以必须有一些。
如果你不想进行忙等,或者不想仅仅使用常规的锁,这项能力就非常重要。好的,这在你有多个实体试图访问某个特定的数据或特定代码段时尤为重要,可能不止一个。你不能仅仅用互斥锁直接处理这种情况。所以这时候就需要。
条件变量,于是就出现了。接着,你可以用条件变量做各种各样的事情,其中我们常做的一件事就是等待许可,并且说,“嘿,这里有一大堆许可,很多线程可以同时工作。”所以我们说,嗯,我们可以使用一个条件变量。
任何,但实际上,让我们来一步步构建它,将其构建为另一种数据结构——信号量,它是一种不同类型的。相对容易构建,但有一些细微之处,我们稍后会讨论。它允许你,哎呀,它允许你构建更多的结构,比条件变量更容易一些。
条件变量,任何。好的,来复习一下这些内容。好的,实际上,让我们复习所有的内容。实际上,让我看看,可能不行。好了。
所以互斥锁,好。你可以把这些幻灯片当作参考,互斥锁是一种锁。好的,它有两个操作。你可以说,要么锁定,要么解锁。当一个线程获取到这个互斥锁并锁定它时,如果没有其他线程已经锁定它,它就可以拿到锁并继续执行下一行代码。好的。
就是这么简单。如果另一个线程进来并尝试使用锁,但第一个线程仍在使用它,那么发生的情况是,第二个线程会阻塞,直到第一个线程解锁。好了。这些就是你应该关注的重点。你必须通过引用或指针传递互斥锁。为什么?因为它们。
实际上需要共享相同的互斥锁。不能是互斥锁的副本,因为那样数据就无法共享了。这就是它的工作原理。顺便提一下,如果你上过并行处理课程或操作系统课程,他们可能会提到,底层是通过原子指令来创建互斥锁,以便两个线程能够共享。
无法同时获取它。所以在硬件和操作系统方面会有更多支持来执行这个操作。但基本上就是这样。好了,线程获取锁之后,会执行下一行代码。如果另一个线程无法获取锁,因为锁已经被占用了,代码就会等待,直到锁被解锁。只有线程。
只有锁定它的线程才能解锁。好了。如果另一个线程尝试解锁一个线程,但它并不是锁定它的那个线程,那就是未定义行为。是的。问题。继续,我在听。我正在尝试弄清楚这个平板。
工作正常。是的。你不记得你的问题了?哦,好的。那是什么?好的。那么,让我们再试一次,我要用这个平板试试,看看它是否。
是的。继续。你的问题是什么?是等待启动,还是可以继续?
哦,等待是可以的。好的,问题很好。问题是,“那是忙碌等待吗,还是?”
等待是可以的吗?" 是的,等待是可以的。但它并不总是能解决我们所有的问题。对吧?所以许可证部分不能真的做那种单一的互斥锁,甚至几个互斥锁。因为正如我们在旅行中的哲学家问题中看到的,我把世界上的大问题和用餐哲学家问题混在了一起,你无法,你必须。
必须有能力说,“哦,我限制了只有这么多人能做这件事。所以很多线程会做这件事。”所以,你必须,你必须等待那个。好了。其他问题?是的,Eva。
这个文本在拳击手或者朋友之间看起来是什么样的?当然。是一样的问题。[听不清]。这是一个有趣的问题。问题是,“当有锁和文件描述符时,发生了什么?”记住,锁并没有关于系统其余部分的信息。好了,如果两个线程碰巧在同一个文件描述符上工作。
如果一个线程读取某个位置,另一个线程会稍后在文件描述符中读取,但没有冲突。它们可以同时读取,程序会按需交替执行。但是,如果你想从一个线程中读取,然后从另一个线程读取,使用互斥锁是完全可以的,你可以按需要进行排序。但是,是的。
没有,没有其他原因。记住,互斥锁相对简单。你要么持有锁,要么不持有,任何其他线程尝试获取锁时,无法在你持有锁的情况下获取。互斥锁就是这么回事。好了,接下来,我们来看看,哦,关于它的其他一些事情。哦,你应该。
尽量将互斥锁持有的时间缩短。好了,为什么?因为如果其他线程正在尝试访问那个锁,而你仍在持有它做其他事情,如果这对你的逻辑没有影响,你可以提前释放它。有时候是不可避免的,你不得不稍后释放它。
因为你仍在使用那个数据结构,但你希望尽可能短地持有它们。好了,让我们看看。死锁,是指两个线程相互等待,这在使用单个互斥锁时是不可行的,因为只有一个线程可以持有它,不能同时由两个线程持有。如果你有。
多个互斥锁,这就是我们开始遇到死锁问题的时候,这也是我们必须继续研究其他方法的原因。好了。那么有一个很好的,非常好的辅助类。
世界上最简单的类就是这个叫做锁保护(lock guard)的东西。锁保护就是这么简单。它有两个方法,一个是构造函数,一个是析构函数,构造函数锁定锁,析构函数解锁锁,仅此而已。没有其他功能,没有其他变量,什么都没有。好了,就这样,锁定后解锁,就这么简单。
它将互斥锁作为参数,所以我想这是它作为引用持有的唯一变量。它的好处是,如果你知道你将使用锁,然后离开一个函数或 while 循环,整个函数,你最好围绕它放一个锁保护,这样你就不需要记得在离开之前解锁了。
它会自动为你处理。在需要条件行为的情况下,它非常有用,比如当你在 while 循环中,而你从循环中返回时,锁会为你自动解锁。你无需记得去做。如果你跳出 while 循环然后离开函数,锁也会为你解锁。
这是一个非常方便的类。如果你知道必须这么做。有时候你无法避免它,因为确实需要解锁。而且再次提醒,不要仅仅因为它好用,就在函数的开头放上锁保护。如果你知道函数中还有其他操作,可能会被其他线程打断。
可能会想要那个锁。这会是一个不好的用法。明白吗?但你会看到更多的例子。好吗?好了,这就是锁保护。现在是条件变量any。
这个稍微有点难理解。我们来回顾一下它的功能。好吗?
它与互斥锁一起工作。所以你有一个互斥锁,然后条件变量any能够基于该互斥锁等待信号。明白吗?所以,基于这个原理,明白了吗?
基本上它的工作原理是:它获取互斥锁,然后你在这个互斥锁上执行等待操作,这个条件变量any使用的是这个互斥锁。条件变量any会把你从处理器中移除,因为你在等待某些事情发生,然后它会解锁。因此,它和SIG suspend非常相似。
在这种情况下。你可以把它想象成类似的情况。但用户应该始终先锁定互斥锁。然后你通常会检查一些条件。好吗?如果条件满足,你就会继续执行下一段代码。所以,这就是条件变量any可能稍微容易使用的地方。我们将会继续讨论。
我们稍后会让它更容易使用。好吗?当你收到通知时,线程中的等待函数会尝试重新获取那个锁。这可能不可行,因为两个线程在等待,如果它们同时收到信号,通常会发生这种情况。这就是我们在使用条件变量any notify all时常见的情形。当这两个线程同时接收到信号时。
同时发出信号,它们竞相获取锁。一个线程获取到锁,另一个则继续等待。明白了吗?这就是条件变量any的工作方式,它们的能力就是这样。当等待条件满足后,你就会重新获取锁。所以当你收到信号时,情况就是这样。
一旦你获得了等待信号,你就会重新获取锁。所以你应该稍后解锁它。明白吗?这就是条件变量any的工作原理。我们常常用它来做许可控制。比如你说我有x个许可,而有y个线程,数量大于x,试图访问这个资源,我只允许这么多线程去访问。明白了吗?
总体的思路是你会等待某些东西,在这个例子中是许可。当许可数为零时,你就会一直等待。然后,另一个函数会说,增加许可数量,并通知所有人。明白吗?
就是这样,然后在那时你会检查,你会尝试获取锁。你会获取锁,然后重新检查许可数,看到它是1,跳出那个东西。然后你会递减许可数,因为现在你持有了1个。所以条件变量any,如果你理解它,你会明白,哦,我明白这里发生了什么。
这里有很多,你知道,有很多细微的差别,或者有很多递增和递减,因为你试图跟踪这个变量,它是一个许可类型变量。好吗?所以那就是,你必须做的事情。这就是条件变量any的作用。因为这个权重非常常见,或者因为这个while循环非常常见。
好吧,他们把条件变量any的权重内嵌到另一个变量中,或者他们把while循环内嵌到第二个版本的权重中,它是这样工作的,先获取互斥锁,就像常规的条件变量any等待一样,然后它接受一个谓词,即一个返回true或false的函数。好吗?这就是while条件。
它实际上是以相反的方式做的,你可以这样阅读它。好吗?这是很重要的。如果你读它,基本上是下面这样,通常我们会把那个函数转换成一个lambda函数,因为我们可以。否则,我们就必须把它作为一个单独的函数调用,这有时会有点尴尬。但在这种情况下,我们直接调用它作为lambda函数。
我们说,在这种情况下,C V在互斥锁M上等待,我们传入许可数,以便我们可以访问它。这是在捕获子句中。然后我们说,返回许可数大于零。所以你可以说,等待直到许可数大于零。基本上这就是它的意思。好吗?我喜欢在这种情况下使用until。
等待直到许可数大于零。因为实际上是当许可数不大于零时,持续等待。那基本上就是它的意思。但在这种情况下,我只是把它想象成,好吧,许可数大于零时,我就可以退出等待。好吗?那就是我们要寻找的,until部分。
好吗?所以这就是条件变量n的工作方式。你们本周在实验中会看到它们,并且会在实验中稍微练习一下。好吗?但再说一次,使用这些条件变量n时,你仍然需要像直接自己跟踪许可数这样的事情。好吗?也许是,也许不是你想要的。
你想做的。如果你不想做那个,嗯,我们有另一种方式。好吗?接下来。
我们要看的是信号量类。所以信号量也是相对低级的,尽管不知道为什么它没有被内建到 C++ 中。好吧?为什么?可能是因为它其实非常容易构建。我们马上再看看代码。但是第二个原因是,一旦我们构建了它,我们可以使用它,并且允许你在剩下的作业中使用它。
我们已经为你构建好了,我会展示给你看。但它是这样工作的。它移除了所有的递增和递减许可,并为你完成这些。非常好。好吧?设置信号量真的很容易。好吧?
基本上,你说 semaphore permits 五,这意味着有五个许可。好吧?然后,如果你执行 permits dot wait,它会查看,哦,五个许可中有多少个被使用了?哦,没用过一个?好吧。你得到一个。然后它给你一个,并且减少许可。现在剩下四个。一直减少到零。当有。
如果可用为零,它就会等待。好吧?然后当你说 permits dot signal 时,那就是你放弃许可,告诉任何其他等待许可的人,等待信号量,我已经释放了一个,去争夺我刚刚释放的那个。好吧?而且,唯一会发送信号的时刻是从零到一的时候,因为。
这就是唯一重要的地方。否则,它总是能获得许可。好吧?
所以这就是它的工作原理。你可以把 mutex 看作是信号量的一个特例,带有一个许可,差不多。好吧?你不应该,事实上,如果你去查找,比如 Stack Overflow 上,有很多人说,不要这么想。这不是一个好方法。它并不完全一样。有些人说哦,这是完全一样的。其实并不是。
但你可以把它看作是一个许可,只不过现在你实际上可以在完成后发出信号。而信号量的有趣之处在于,你可以发出信号,即使你不是获得锁的那个。就这是其中的一个大区别。mutex,唯一能够解锁的是持有锁的那个线程。
mutex 是锁定它的那个。那就是另一种区别。好吧?现在,有几件事我们没有谈到,信号量。一个初始化为零的信号量意味着什么?你们觉得呢?如果我做一个信号量,初始化为一,它意味着有一个许可。如果我做一个信号量,两个线程在争夺它。然后其中一个。
它们都试图获取它,一个会获得它,然后当那个发出信号时,另一个会获得它。一个许可为零的信号量意味着什么?你觉得呢,Cosner?[听不清],好吧。所以这是个好问题。问题是,如果它只是挂起呢?如果有两个线程在争取,它们会都等待。直到什么?你觉得呢?[听不清]。
只有父线程可以访问数据。不完全是。你在正确的轨道上。别人需要做一些事情。我们说互斥锁用来做什么?
唯一能释放互斥锁的就是那个锁定它的线程。这对于信号量也适用吗?不。任何人都可以对信号量进行信号操作,对吧?所以这样,你慢慢理解了。还有人想尝试解释一下这到底意味着什么吗?
这实际上是一个有趣的案例。是的,继续。 [听不清],是的,你在等待其他线程给你信号。无论其他线程是否必须等待那个许可都无关紧要,对吧?这只是表示,嘿,让其他信号,其他线程给我发信号。这也是你要说的吗?是的。
就是这样。好的,这就是它的样子。我们不一定有许可证。好吗?
许可证等待意味着你只需要等待一个信号,这个信号可以来自任何其他线程。我将展示一个例子,说明这是另一个更有趣的情况。是的。[听不清],是的,任何线程都可以发出许可证信号。好吗?
它是否没有任何锁定开始时并不重要。可能是你的逻辑说,嘿,某个线程,只有这个线程需要等待某些事情。事实上,我马上会给你展示一个例子,说明它实际上是有用的。但它可能会说,这个线程需要等待一大堆其他事情发生,然后它会继续。
其中一个线程会发信号。当它完成时,它会等待。它不一定有许可。它不需要锁定任何东西,直到它需要获取信号。实际上,它只需要做这个。好吧?稍后我会展示给你。我的下一个问题是,如果它说许可证为负数呢?那是什么意思?
让我们看看代码。你打算怎么说?你打算怎么说?是的。
[听不清],是的,正是这样,许多不同的线程需要发信号才能实际执行任何操作。让我们去看看信号量的实际代码。我得记住它在哪儿。
看看,slash user,slash class,CS110。我想它是本地源,我相信,然后是线程。就在那里。好的。
Semaphore.cc,你们都可以去看看这个。好的。让我们看看它是如何工作的。
顺便说一下,它里面有很多东西。它有一个条件变量(CV)。它有一个条件变量,因为我们就是这么构建的。让我们看看权重和信号。好吧。权重做了以下事情。好吧。权重有这段新文本,它实际上尝试获取,因为它确实需要更新那个值。
好的。所以在某些情况下,你必须有那个锁。好的。然后它会执行一个条件变量的等待,基本上是等到那个值大于零。好的。然后它会在实际获得锁后减少那个值。好的。这就是wait的作用。signal是做什么的呢?嗯,signal会获取锁。好的。然后递增那个值。
然后如果值变为1,它就会通知所有人。好的。是的。[听不清]。MutexM是一个类变量。对。它是一个类。如果你看一下,嗯,我看看能不能做到。实际上,我们在这里做。看看。就在那。包含。
大约是四点八。好了,就是这样。它是一个,那里它就是了。它是一个类变量。对。 所以当你创建这个,创建类时,现在你已经有了互斥量。
所以它在函数之间是共享的。好的。好吧。这就是signal和wait的工作原理。嗯。注意,如果值,它实际上只能递增或递减那个值。
所以如果你把这个值设为负数,那它就需要一段时间。但这个数字需要减去一个信号,才能增加到一,实际上,来通知等待的线程它已经完成了。好的。那么,什么时候你可能需要这个呢?嗯,如果我们做的是最后一步呢?
好的。假设我们有一个程序,里面创建了10个线程,它们每个都需要做某些事情。然后,还有一个线程需要等待,直到它继续做它的事情。现在,你可以做一些事情,比如将其他10个线程都加上join,然后做接下来的事情,如果你愿意的话。但也许你想让这些线程继续运行。也许你不想这么做。
也许你想做其他逻辑,最终,你知道的,做些其他事情或者在这些线程中做类似的事情。所以在这种情况下,你可以这样做:我们实际创建这些线程,然后每个线程执行它自己的任务。在这里,它只是会输出一个cout,然后它会在信号量上发出一个信号。
而这些线程根本不知道它们正在递增哪个数字。
就是因为,看,我已经完成了我的任务。如果有谁在乎,我会发出信号。我做完了。然后它发出了信号。对。然后这里的read after 10函数呢。嗯,它只会等待。因为我们一开始把这个信号量设置为负9。好的。等到发生了10件事情后,它就会递增到1。然后信号就会发出,事情也就发生了。
然后,实际的信号量就会允许线程继续执行。顺便提一下,最终会将信号量的值降到零。
但这就是通过线程传递信号的方式,它让你可以依次完成10件事。最后一件事会最后执行。我们试试这个。
看看我的讲座文件,应该是Red.CPP。我想它应该就是负数信号量。好的,确实是负数信号量,正是我刚才所说的。
这时,顺便说一句,我们在这里创建了一个值为负9的信号量。好的。然后将这个信号量的引用传递给所有其他线程——我们有11个线程。负数信号量,就是这样。
所以这些线程,它们都按照各自的顺序执行,依次完成。每个线程在完成后会发送信号。然后当最后一个线程收到所有这些信号后,它才会继续执行。这就是它的工作方式。好了,关于这种新方法的使用方式,大家有什么问题吗?我们稍后会看到一个例子。过一会儿会有一个更大的例子。
好的,三种不同的类型。互斥量(Mutex),条件变量,任意信号量。我们会更少使用条件变量,因为我们有信号量。你仍然会经常使用互斥量。你会使用这两个远多于单独使用条件变量,尽管你可能在某个时刻有理由使用条件变量。
好的,让我们来讨论另一个模式。这里有一个模式,它偶尔会出现。它将利用我们可以使用零(不一定是负数,但我们可以使用零)作为信号量的事实。好的,它也可以是一个方法,在绝对需要时才使用线程连接(join)。好的。
所以我们接下来要做的就是,查看一个程序,它有两个线程,两个线程都在操作一个特定的数据结构。其中一个线程在写入数据,另一个线程会从中读取数据。好的,实际上它会读取刚刚写入的数据。好的,这很像管道。好的,实际上,管道也可能在后台使用类似的机制。
但这与写入一些数据然后另一个线程从该数据结构中读取数据的想法非常相似。好的,这就像一个Web服务器和客户端的关系,客户端向Web服务器发送请求,比如输入www.google.com。这个请求会发到Google服务器,然后Google返回数据。这中间有一个握手的过程,你在请求某些数据,然后数据必须返回给你。
就是这么回事。如果今天有时间的话,我们还会做更多的操作。我们也会看到另外一个例子。就像我说的,它有点像管道。好的,这是初始程序,我将放大一部分,我们会稍微详细看一下。好的,有多少人记得,嗯,你们记得 CS106B 中的队列吗?
你有谈到过像是循环队列或循环缓冲区吗?你可能记得这些。好的,好吧,有些人记得。如果你修过170课程,你肯定不知道这些,因为我知道它是作业的一部分。这个就是循环缓冲区的概念。让我实际上缩小视图并在这里稍微画一下。我可以,嘿,看这个。所以,上次的内容。好了,清除。好了。
我的糟糕画画技能又回来了。
好的,循环缓冲区就像这样。你可以把它建模成这样,或者你想的话,你可以把它想成一个圆形。循环缓冲区的作用是,如果你从这里开始写入,下一个写入的地方就在这里,然后是这里,然后是这里,然后是这里,然后是这里,然后是这里。然后你就回绕过来继续在这里写。
所以你需要一个头和尾,差不多是这样。事实上,你可以在程序中通过一个模运算符来追踪实际循环缓冲区的位置。所以你可以让缓冲区部分填充或者其他什么情况。你接下来写的地方就是这里。如果这个位置填满了,那个位置也填满了,那个位置也填满了。
我猜你如果是接着写的,当前位置是这里,这个位置还没填满。以此类推。但重点是,给我擦掉它,像这样。假设其他的已经填充到这里。假设这是填充了的。那个也填充了。这个也填充了。我们准备写到这里。接着我们会填充这个位置。
然后我们填充这个位置,再到这个位置。接下来填充这里,然后下一个位置就填充在这里,以此类推。如果你遇到缓冲区已经填满的情况,你就不应该继续写入,因为它已经太满了。这就是循环缓冲区。
它的优点是,你不需要每次都创建节点并一个一个地遍历它们。你只需要一个数组,然后以循环的方式遍历它。好的,反正这就是循环缓冲区的概念。所以我们要做的是写一个小程序,或者查看一个有循环缓冲区的程序,其中一个线程将负责写入,另一个线程将负责读取。
它将希望能够追踪谁在什么地方写入,以及谁在读取,以确保不会相互干扰,也不会读取尚未可用的数据。
好的。那么这对程序意味着什么呢?嗯,让我们先看看目前程序是如何实现的。好的。我们有一个写入者,它基本上会写入随机字符。
它将随机添加一个字符,并且会随机地将字符一个接一个地放入缓冲区。缓冲区只有八个位置。但我们将执行这个操作320次。好的,所以它会写入然后回绕,继续写入,写入,写入,写入,写入,写入,写入。如果一切顺利,读取和写入会以相同的速度发生,这样你就可以做到这一点。
然后,写入者可能会稍微超前于读取者,他们会继续沿着那个循环模式进行。这是理想的情况。好的,但这里发生的其实是非常简单的事情。它只是把那个随机字符放到缓冲区的某个位置,然后当i变量大于七时,继续执行。
它会回绕到开始,并继续执行。它就这样做320次,把数据写入其中。好的,这就是它所做的。每次执行时,它都会说它已经把数据写入其中。好的。读取者则执行相反的操作。它遍历并逐个字符地读取数据,然后回绕继续读取数据。
好的,在一个完美的世界里,写入者可能会比读取者领先一两步,一切都会完美地工作。
好的,这些是在不同的线程中,所以你可能会想,哦,这里有一个竞态条件。嗯,确实有。
让我们实际看看这里的情况。好的,这叫做混乱的读写者。好的。
记住,它是先取随机字符,然后写入,再把那些随机字符读取出来。
所以最开始这里看起来会有点奇怪。我会实际停止它,因为它正在做那个操作。
让我们看看这里发生了什么。好的,写入者准备好写入,读取者准备好读取。然后写入者发布了S。好的,读取者接着消费了sphin。那么发生了什么?接着它说读取者消费了G,写入者写入了J,读取者消费了at。那么这里发生了什么呢?实际上是读取者在写入者还没有写入任何内容之前就开始尝试读取,而最开始缓冲区中存在的垃圾数据就是被读取出来了。
好的,最终它读取了消耗的数据,像是空的,等等,我甚至无法写下来。最终你可能会看到一些数据进去了,但它们都被弄乱了,写入者覆盖了某些部分,它的工作方式不是我们想要的。
它们是在同一个文件里读写的吗?
它们在同一个缓冲区进行读写操作,这只是一个充电的狗形缓冲区。你不需要担心任何游标。你不需要担心任何游标。我们有点儿在担心游标。让我再回到图示。
让我实际重新画一下这个图。这里是缓冲区。我们再做一点更多的事情。假设有一,二,三,四,五。假设只有六个位置。假设这里是第一次读取发生的地方,但也是第一次写入发生的地方。我们叫它读取头和写入头。
最后可能会被读取的东西在哪里?事实证明,这就是——如果你绕一圈回去的话,不过这有点微妙。但现在我们先考虑这个问题。假设写入者先尝试读取。好,它会在这里读取垃圾。所以如果它提前读取了,那就是垃圾。
假设写入者确实写了A。好,写入者写了A,然后写入者写了B,再写了C和D。假设读取者很友好,现在开始读取。那么读取者会读取A,然后到这里,读取者会读取B,然后到B这里,然后B这里,接着读取者会读取D,依此类推。假设是E,F,然后它绕到这里读取G。好。
假设由于某种原因,读取者在这里停下了读取。那么这里是G,接下来是H,然后是I,接着是J,直到读取者读取它。所以这是一个大问题。如果它们不完全同步,可能会出问题。
好的,现在完美同步其实是有点微妙的。要完美同步,多少数据可以在写入前被读取?零,对吧?
你必须先写入一些东西,才能读取它。所以,某些东西必须在读取之前被写入。最大数量的东西,对吧?
所以你可以一直写到最后,再返回去,但如果你一回去什么都没读取过,那你最好等一下。好吧,这就是我们将在这个程序中要做的事情。
好的,所以不管怎样,我们再看一遍,然后我们出去。所以我们回到了完全混乱的状态。没有任何东西是按顺序的。它应该是。它应该读取SJCNN。我们来看看,确实有SJCNN。它从那里开始读取,但最终它不一定是同步的。
一堆东西先被读取了,不管怎样。不好。好吧,我们怎么修复这个问题?
有不同的方法可以修复它。好吧,你可以,可能你可以使用几个互斥量,也许如果你想这样做并尝试设置它。这是一种做法。我要给你展示的方式是使用两个信号量。
好吧,我们将要做的是,这实际上是我们刚刚经历的过程。为什么它会坏掉。基本上没有任何东西能告诉读者某个位置可以读取了。而且没有任何东西告诉写入器所有位置都已满。这就是大问题。好吧。那么我们能做什么呢?我们可以使用两个信号量。
好吧,如果我们有一个 8 个字符的缓冲区,我们可以将空缓冲区初始化为 8。换句话说,它一开始有 8 个空缓冲区。有 8 个空的位置。目前一切正常。好的,然后我们还可以设置一个信号量,表示满缓冲区。那么最初有多少是满的?零。所以我们做一个从零开始的信号量,这是那个奇怪的情况,始终需要一个信号才能开始。
因为它一开始没有许可可用,那就是发生的事情。好吧,我们可以这么做。我们可以设置信号量满缓冲区和空缓冲区。满缓冲区,如果你说一个没有参数的信号量,它会从零开始。好吧,如果你做一个空缓冲区,如果你在这里做参数,那么这就是基本上有多少许可的意思。
而这个信号量表示有 8 个空缓冲区。然后你必须当然将满缓冲区和空缓冲区都传递给读者和写入器,因为它们都会处理读者和写入器。一个会等待并向另一个发信号,而另一个会等待并向第一个发信号。
所以我们需要在这里工作。让我们看看这个在实际功能中是如何运作的。那么我们有什么呢?
好吧,我们有了满缓冲区和空缓冲区的信号量。好的,我们仍然会做和之前一样的逻辑。我们只会通过整个缓冲区执行 for 循环。写入器做的第一件事就是它会等待空缓冲区。
但我们将这些初始化为 8,对于信号量,如果计数为 8,许可会等待吗?
它说很好,我可以开始写了。然后砰,它开始写入,然后它再次尝试等待,现在有七个可用的缓冲区,砰,它开始写入,就这样。我们会继续。当它写入一个字符时,它会向满缓冲区发信号:“嘿,我刚刚写了一个字符”。
所以它将信号量中的满缓冲区计数器递增到 1,并且它发出信号,因为它递增到 1,并且它向另一个线程发信号。嘿,有东西可用,我刚刚放入了一些东西。好吧。好了,我们来看读者。读者做的是相反的事情。它仍然会通过循环,首先等待满缓冲区。
开始时没有任何东西是满的,所以读者必须至少等一次写者插入一个东西,因为它使用的信号量从零开始。好的。但是一旦第一个东西写入,信号量就会立即被触发,读者可以立刻读取。
它可能会稍微滞后一步,因为可能写者还没有写下一个东西,然后它会再次等待,但只要有位置,它会立即执行。而且这个过程可能会有很多次,取决于写者写了多少东西。
里面可能会有很多项,最多到八个。好了,我们来看一下。然后它发出信号说“空了”,意思是我刚刚清空了一个,这时空的信号会通知写者继续。好了,顺便提一句,它最多会这样做八次,而读者什么都不做。我们可以继续测试这个,应该是有效的。
现在信号量如何帮助我们保持一切有序。然后我们来看看这个。这个只是叫做读写者的正确示例,我会让它运行一会儿。让我放大到顶部。
好的,读者或写者在写作,读者准备好读取了。太好了,写者发布了一个,猜猜读者立刻消费了a。顺便说一下,我不应该在前面的句子里说这个,这里也有随机休眠,所以这就是为什么它们是一个接一个执行的原因。
读者按照顺序获取了a,然后y进入,y出来,再到W.I.Q.W。一定是读者睡了一会儿,因为写者有机会插入四个新的东西到读者W.I.Q.W。但是读出来的是什么呢?哦,W.I.Q.W。被读取。所以我们知道这里一定是按顺序的。接下来它继续进行。
我不确定,可能会出现一种情况,八个项目会进入,但它会等待这么久,虽然我们可以这样做,但它肯定会按顺序执行。明白吗?大家为什么是这样?不明白的可以再看一次代码,记得去看看信号量的代码,看看它是如何工作的。
好的,我们来看一下代码。加入读写者的顺序有关系吗?是的,问题很好,加入哪个先并不重要,为什么呢?因为只要它们都能完成,缓冲区就会被填满,对吧?如果你尝试先等待读者那边。
可能是作家最后结束了,让我们看看试着等一下读者,可能是你必须按顺序做,因为你必须先写才能读,如果你先等读者,你可能就完全无法读取它了,因此。
我没有再想它了,嗯,我没有再想它,但我认为在这种情况下它并不是一场比赛,最终的等待可能是,我会回来告诉你。嗯,好问题,是的。所以,我的意思是我们可以试试,但我不知道这是否能证明我们的观点,证明否定,但我们还是试试看。
好的,然后再读写器,好了,让我们看看它是否能够完全通过,这种情况通常发生在接近结束时,我们会在意,因为一个会先结束。
但是不,再想一下,我觉得如果一个结束而另一个还在等待,那又怎么样呢,如果它在等待第一个而还没结束,那又怎么样呢?所以,如果数据还没有到来,另一个应该还没结束,所以我认为这无关紧要。
我很确定我会确认这一点,但它确实完成了,所以至少我们知道它是可以工作的,但这些事情通常不会。让我们这样做吧,让我们稍微再等一下,我们再看看,看看我们能不能放进去。
让我们在这里稍微加个暂停,等三秒钟才开始尝试读取。这意味着写入器会尝试放进去八个东西,然后希望它会等待。我们来看看能不能成功。好了,它放入了一二三四五六七八个东西,然后它在读者终于唤醒后继续运行,我开始读取一些内容。
就是这样工作的。另一方面,如果我们把它放在写入器之前,或者更准确地说,如果我们把它放在写入器之前,发生什么情况?你认为如果我在这里放个暂停,会发生什么?你认为它应该在做几乎任何事情之前等待三秒钟,对吧?是的,我认为这是个很好的回答。让我们创建读者写入器,然后好吧,大家都准备好了,最后写入器开始写作,三秒钟后,砰,它就开始了。
好的,什么都没有写,读者无法处理或者无法完成需要的读取任务。好问题,怎么会有九个呢?我们其实可以跑一下看看会发生什么,如果我们得到一堆数据。它是做了九个吗?可能只是一个竞争条件,谁在写谁在读,这是我的猜测。它不能在没有九个的情况下读取九个,因为没有九个位置,但很可能是最后一个写入的,读取已经打印出来了,或者还没有打印出来,或者类似的原因,所以这只是一个竞争条件。
如果是20个的话我可能会有点怀疑,但八个、九个或者十个,倒是不会怀疑,1,2,3,4,5,7,9,最后是那个,确实做了两个S的结尾,所以没有什么奇怪的。总的来说,我觉得这只是一个竞争条件的问题,可能是因为写入的速度比读取的速度快,反正读者总是赶不上,直到最后一部分输出时,才终于能读取到其余的数据。
没有问题。你有更多问题吗?你确定吗?好。还有其他人吗?好,那么这是读写者范式。让我们继续。好,我觉得我们有时间看看,可能不会看完,但我们来看看这个叫做神话粉碎者的东西。好的,这是一个杰瑞·凯恩的特别项目,叫做神话粉碎者,因为它的功能就是,当你登录一个神话时,你会发现哇,那里有一千个其他人在用,而且非常慢,挺烦人的,也许了解一下哪个神话的CS 110用户最少,感觉会更好。
好的。这就是神话粉碎者的作用。它基本上会访问所有的神话机器,可能是10台或12台,然后通过一种有点怪异的hack查询它们,看看这个班级的学生使用了多少线程。我还觉得它应该检查一下107号机,或者只检查线程,但不管怎样,它检查的是这个班级的。
好的。现在我先展示一下它的运行情况,然后再说其他的。好的。神话粉碎者,我们就让它并发运行吧,这是我们不太喜欢的那个,但也没关系。
好的,天呐,谁在使用这些进程?这是什么情况?
机器上也有一点竞争条件,没错,最少的机器是CS机器号最少加载的机器74,所以如果你要使用它,你应该登录到神话52上,但我现在真的很好奇神话65。
是神话65。神话65,查看h top。好的,有人在看,让我看看,看看因子Python因子。这个是谁?Emma,你有问题吗?好的,看起来你在程序中留下了很多因子,可能是你启动程序时的操作。
我并不是故意点名批评他,但她很高兴看起来不错,我们可以,成千上万的这些人,所以你说的并不孤单,我向你保证,还有更多。好的,还有更多。好了,让我们看看能不能再找到更多。是的,还有一大堆,所以你应该终止你所有的进程。所以实际上,当我之前运行这个时,我注意到其中的一些,我得写一个小笔记给下个季度,让每次提交时都能终止所有你仍在运行的进程,不管你在哪个 myth 环境中。
就这样,反正你们都在做这些,那些可怜的107个学生无法使用 Myth 机器,因为你们自己也不能用。所以Emma,顺便说一下,你可以说“kill all Python”,它会终止你所有的Python进程。
那台机器我相信你可以试试看,看看它是否有效,也许它能做到,如果我们都很友好,我们可以做到。顺便说一下,好消息是,所有这些进程虽然占用了内存,但它们的状态应该都是“T”(即停止状态),这意味着它们实际上并没有在不断循环,这很好,谢谢你们没有让它们一直运行。
但对,通常情况下它们并不好,比如说就那样坐着什么都不做直到机器重启,这大概是会发生的情况,不管怎么样,这就是 Myth Buster 做的事情,而且 Myth Buster 的运行有些慢,因为它必须拉取每个。
所以这是并行处理的,速度快的那种,顺序处理的那种会很慢,因为它必须按顺序处理每一个,它必须去执行这个查询,而一些 myth 机器顺便说一下,可能因为某些原因出现故障,无法使用,它对这种情况有2秒的超时设置,因此必须等到超时。
所以这意味着,嗯,并行的那种,速度快得多,是用线程处理的。好的,基本上就是说,我会开一个不同的线程,询问这个 myth 机器有多少个线程或多少个进程,如果需要一点时间也没关系,其他的一些线程还会继续运行。
你会注意到它是乱序的,我们可以找办法让它更有序一点,但你会看到56,52,58等。至少这个是有序的,虽然有些缺失,因为它们不是像 myth 57 那样挂掉,但也许应该直接说 myth 57 挂掉了。
但重点是你要并行处理,虽然慢,但一旦有了线程,你可以等待所有服务器的回复,或者处理故障。我们来看看这部分代码。好的,这是我们要使用的各种数据结构。我恰好列出了班上所有学生的列表。
这就是它所要求的,所以它读取那个文件,然后检查Smith机器与所有这些名字的匹配。它只是查找进程号。好的,它有一个所有这些的集合,这是一个集合,它读取这个文件,它有一个计算每个神话的进程数的映射,告诉你每个神话有多少个进程。
好的,编译进程计数地图将传入这两个细节,然后我们将在其中进行所有的线程操作,然后它将发布最少负载的数据,这就是程序的工作方式。好,让我们看看我已经提到的这些东西,回顾一下这些,看看有没有什么重要的东西我们需要知道。
不,我想我们已经讨论过了。细节在get num processes中。嗯,这就是它实际去获取的地方,比如神话54,它获取进程,然后根据用户名检查它们与班级中的学生是否匹配。
好的,这就是它在顺序版本中如何工作的。基本上它逐一处理每个神话。好,从51到56,然后它通过调用这个获取进程来统计每个神话中的进程数。嗯,这就是慢的部分,因为你有10个不同的产品12个进程,10个不同的神话,每个神话可能都会卡住。
好的,这就是我们需要避免的情况。这是我们已经展示给你们的工作原理。嗯,关键是每个调用大部分时间都在等待神话机器的响应。每当你登录到一个神话并等待时,嗯,你的程序也必须做这个。对,它不能继续做其他事情,如果你按顺序执行的话,所以这是关键所在。
好的,所以当你在计数这些时,你要做的不是顺序执行,而是用线程处理。好,让我们看看这如何工作。嗯,我们将再次计算CS 110的进程。我们仍然将传入num,我们仍然将传入学生集合,我们仍然将传入进程计数地图,这次我们将传入而不是仅仅得到返回值。现在我们将使用一个互斥锁来锁定进程计数地图。为什么?
因为你不希望不同的线程同时更新地图。
好的,你可能会想,为什么两个线程在两个不同的神话中同时更新地图有什么关系呢?
你还记得在幕后是如何构建地图的吗?回到CS 110或者106 B的日子。
记得一般情况下地图是如何构建的吗?如果是普通的地图,你知道它是树形结构,如果你有一棵树,而且如果是平衡树,那意味着在插入时你必须实际移动节点。
如果两个元素同时尝试插入,就会很糟糕,它们会互相移动,最终导致混乱。
无序集合使用哈希表,这也可能引发类似的问题,特别是如果发生哈希冲突等情况。
所以无论如何,你不希望两个线程同时写入那个映射,所以你会在映射周围加锁,但映射的写入非常快。
写入映射或读取它是非常非常快的。好的,所以我们可以做到这一点,不用太担心。事实上,我们只会在它周围加一个锁保护。
好的,我们要更改,我们要……我们来看看,我们将要在这里进行实际的计数过程或者获取数量的过程。一旦我们得到进程的数量,如果计数大于零,我们将更新分钟计数并将其放入映射中。
这一部分非常简短,更新映射的部分。这个部分需要花很长时间。如果你在等待,那部分确实是耗时的,但如果你在12个不同的线程、12台不同的机器上进行处理,实际上是没问题的。
好的,但你必须在更新映射时对其加锁,以确保所有人都能安全操作。然后我们在这里有许可证,这样我们就不会一次性处理所有线程。我想,处理12个不同的线程在12台不同的机器上并不是一个大问题。但在这种情况下,我们设置了多少线程?我们是不是说过要设置多少个线程?我猜我们要看看具体设置了多少。好了,所以我们说只允许同时运行八个线程。
当你在做网络操作时,限制到一个合理的范围是挺好的,虽然在这种情况下我们实际上可以处理成千上万的请求,但时间只有12分钟左右。所以就是这样。好了,然后我们在结束时发出信号,看看我们什么时候实际获得许可。
我没有看到许可的等待在那里。所以……哦,它在这里。让我们看看。哦,对了,好的,当我们实际上设置线程时,就是在这个时候。我们下周会看到一个不同的模型,这个模型是我们在这里设置线程的地方。好的,那么我们是怎么做的呢?我们说允许八个许可,然后我们会遍历,如果我们只……我们一次只会启动八个线程,当其中一个线程……
完成后它会发信号启动下一个线程。我们直到最后才会对任何线程进行 join。好吧,这些线程会结束,但我们只允许最多八个线程同时运行。到某个时候,可能会有九个线程在运行,因为信号会触发,另一个线程会启动,然后这个线程会关闭。
所以在这种情况下,我们可能会遇到“多一个”的问题,但这没关系,现在有九个线程,而不是仅仅八个。是的,哦,对不起,没告诉你这一点。所以在这种情况下,我猜在线程结束后,应该没有问题。信号量允许你在线程退出时释放信号量。
我不知道需要什么魔法才能做到这一点,可能可以在实际代码中查找。让我们快速查找一下。但没错,如果你想要在线程关闭时发信号,这就是你需要使用的方式。
很好的回答,关于这个问题。用户类 CS110 本地源线程信号量。cc。好了,接下来让我们看看线程退出时的情况。
好的,我们来看看。是的,看起来需要一些 pthread 的魔法来基本上说“嘿,线程退出时做这个”,然后你就可以这么做。所以,这里发生了一些背后的魔法,利用线程来做这一切。
你不需要专门了解这些内容,只需要知道,如果你想在线程关闭时退出或者发出信号,应该在那时执行。
很好的问题。问题是,线程在调用 join 之前不会关闭。它会关闭,但在调用 join 之前不会被清理。所以换句话说,线程停止运行了,但它不再占用处理器的任何周期。它只是等待,操作系统线程管理器会等到你调用 join 才会清理它。
是的,关于这个问题的其他问题吗?好吧,让我们看看。嗯,看看。我的意思是,竞争性执行的迷思。那对我们有什么帮助?
这让我们可以等待流网络或连接的完成。好吧,因为我们有很多连接在发生。你的电脑有能力同时向不同的机器发送很多消息。没问题。它有自己的等待和解交错机制。但就你来说,程序可以向不同的服务器发送任意多的消息。
在这种情况下,所有不同的神话同时通过多个线程运行。好的。它的信号量限制了线程的数量,是的,这是我们讨论过的线程退出的内容。它是信号的一个重载版本。在整个线程退出之后,它才会调度,这样就避免了九个线程同时运行。虽然也许在非常短的时间内会有,但总的来说不会持续很久。就这样。
你可以看到并行的和让我们这样做的区别。让我们在两个不同的窗口中做。就这样做吧。好的,所以我猜有点有趣的是,我在运行这些神话并进行检查。
所有的神话,所以有点儿。好的,我想我们可以这样做。接下来我会这样做。我们开始吧。是的,我想把这个窗口缩小一点。好的,所以我们将运行神话破解者。让我们在这一边做顺序的,然后。让我们看看。让我们开始吧。我们在这一边做神话破解者并行的。
现在试着按回车键,然后迅速按另一个键。如果我们想要的话,我们可以计时,但我说好了,准备好,砰砰。好的,顺序的神话破解者仍在运行,而另一个已经完成了。好的,它所有的任务都在线程中完成,而这个则是一个接一个地完成。
好的,你对这些内容有什么问题吗?
你看到了吗,这个线程,你得经常考虑竞态条件。我们可以做很多很酷的模型。下周我们要讨论一些其他有趣的问题,讨论我们如何建模更大的概念。
这个特别的例子是一个冰淇淋店,挺有趣的。他们将建模一个允许你使用等待其他线程的线程,并使用这种许可的想法等等。问题是,如果你没有指定某个神话,它会检查所有的神话,在这种情况下。比如说,“ umber ”是一个白天时间。哦,如果你只… 是的,是的,好的问题,如果你,这只是关于一般登录到神话的内容。
如果你只输入 SSH 神话,它会选择一个,实际上有一个负载均衡器在做我们程序刚刚做的事情。基本上,它会说谁现在忙,接着我想它会持续查看谁忙。它有持续开放的连接,一直在看谁忙,谁忙,然后尽量把你送到那个最不忙的。对,它无法判断我是不是所有的那些交叉点都停了,因此有时候它会失败。
但不,它确实在做完全相同的事情,实际上它为你做的几乎是相同的事情。是的,好问题。好的,还有其他问题吗?我们可能会提前几分钟离开。好的,我们周三见。
P14:讲座13 冰激凌店模拟 - main - BV1ED4y1R7RJ
好的,今天我们开始了。今天有一个问题。希望你们已经拿到了讲义。如果你们有问题,我准备了很多份。今天我们要看一个程序,它将会模拟。它在哪里?
这就是模拟冰激凌店的场景。实际上,我们要进行一次真人示范。我们现在开了一家冰激凌店。抱歉视频前的观众,你们看不到即将发生的混乱。接下来我需要的是11个志愿者或者征召人员。来吧,前三位来做大客户。
这意味着如果你真的买了冰激凌,你就能拿到冰激凌。好,看看。好的,一,二,三。好了。其他人,接下来的人,你们将成为服务员。有人将会是经理,还有人将会是收银员。我觉得有几个服务员。还有其他人上来吗?好的。
非常感谢你们。服务员们,请到这边来。哦,顺便说一下,每个人也拿一本,这是你们的操作手册。好的,应该每个人都有一份。好了,接下来我们要做的事情是模拟一个冰激凌店。实际上,我们要模拟的是一个问题,这个问题曾经在很久以前的 CS107 课程中提出,当时多处理和线程处理的问题让 ANSES-1 出现了麻烦。
以前是这样做的。我们从那时开始已经做了些调整。好吧,接下来我们要做的事情是这样。我们正在设置。经理在哪里?请过来,好,你是经理。你将会是……实际上,大家会为你争夺职位。好,收银员在哪里?你差不多要在最后出现。好,明白了,你有。
这里有一个小小的编号系统。所以1,2,3号,他们,顾客将按顺序取号。我这里有编号,我们有冰激凌甜筒。好的,谁是顾客?顾客在这里吗?好的。现在这是怎么运作的。好,我们现在只用三位顾客来模拟。一个顾客的操作是这样的。
客户进入我们的冰激凌店,告诉你,“我要 X 个冰激凌甜筒。”然后你会知道他们需要多少个甜筒。好的,他们会进来,说要 X 个甜筒。因为他们想尽快拿到自己的甜筒。
或者至少尽可能快,他们会去找一个服务员。出于某种原因,我们的服务员数量和当天要送出的冰激凌甜筒数量相同。每个服务员负责做一个完美的冰激凌甜筒。事实上,每个服务员只能尽力做出一个完美的冰激凌甜筒。
但是经理会保持公正,如果结果糟糕的话。我曾经有个朋友,他曾在一家冰淇淋店工作,他和他的朋友常常比赛,看看谁能做出最小的冰淇淋球,而不被人抱怨。
这真的是一件很刻薄的事。你知道那个小小的勺子,你就会想,“哦,我就是太好心,不忍心抱怨。” 不管怎样,你是不能这么做的。你实际上会有更多或更少的决定因素。这是随机的,但在冰淇淋蛋筒上是确定的。好的。
所以他们看着我,然后他们会拉一个店员。每个冰淇淋蛋筒需要的店员数量。好的,每个店员然后会实际让其他店员走到桌子那边去拿一个冰淇淋蛋筒。这是一个时间上的问题。那里会有冰淇淋蛋筒,你将要制作。当你做完一个冰淇淋蛋筒时,你要走回来并呈交给经理。
好的,顺便说一下,在这个时候,你会在四周走动,等待你的冰淇淋蛋筒。你并不会挡路,你只是等着你的冰淇淋蛋筒。当你做出了一个你认为完美的冰淇淋蛋筒,换句话说,你已经拿了一个,你要把它带回给经理。
如果有两个人同时向经理走去,你们可以争夺谁把蛋筒交给经理。好的,现在你并不需要真正参与这场争斗。基本上随便拿一个,知道吗,谁先拿就给谁,反正那是你的选择。好的,然后经理。
你要决定这个冰淇淋蛋筒是否好。不要告诉店员你不喜欢。 “e” 代表不好,“g” 代表好。哦,好极了。不要告诉店员这些。好的,反正,然后你要么告诉他们,“是的,那是一个美丽的冰淇淋蛋筒。” 在这种情况下,你要说。
“现在我已经做了今天必须做的六个冰淇淋蛋筒中的一个。” 好的,一旦你做好了,可能你会想要跟踪一下进度。你可以说,“我要做六个”,然后你可以说五个,四个。当你做完所有六个,且它们都合格,或者你已经批准过的,你就可以回家了。” 好的,好的。店员们,如果经理说,“不行。”
那不是一个好蛋筒,"你必须把那个蛋筒扔掉,随便扔在地上任何地方,然后去拿另一个蛋筒,回来继续挑战。好的,为了引起经理的注意。好的,顾客们正在等着。现在,如果经理说,“这是一个很棒的蛋筒,完全没问题。”
“你可以把它交给顾客,然后你就可以回家了。” 好的,好的。现在,一旦你拿到所有三个蛋筒,你来拿个号码,好吗?收银员在等着有人处理。好的,现在,一旦你看到有人拿了号码,你就可以说,“哦,我拿一号。”
然后,花你们想花的时间,结账时,确保顾客付对钱。无论他们想要什么。好了,但你必须帮他们结账。一旦结账完成,你就可以标记一位顾客。你必须结账三位顾客,然后你才能回家。好,好的。然后,顾客们,当她帮你结账时。
然后你可以拿着冰淇淋回家。好吧,在那之后,你应该拿到多少个冰淇淋就有多少个。现在,大家都明白了吗?那真的是一大堆东西,一下子说完了。好吧,你们手里有自己的指令。好,实际上,我会调出指令,让大家看看发生了什么。让我们看看。
我要这么做。我会把它调出来。
这里应该是这里,然后是冰淇淋的详细信息。好了。如果这能正确加载,我们就应该执行那个步骤。
然后好了,等一下,现在不行。
这里是指令。我会尽量把它放大。好了。那么,虽然这里有很多内容,但你大概能看到发生了什么,对吧?
顾客拿着冰淇淋筒,拿着冰淇淋筒去找店员,四处走动。店员做他们的事。当顾客拿回冰淇淋筒时,他们去收银台,拿一个号码,等等。大家大概明白发生了什么吗?我意思是,这可能看起来有点疯狂。让我们看看能不能在没有真正争吵的情况下完成这个。
但你可以看到会发生什么。好吧。那么,顾客们。尽情进店,做你们想做的事。好,去做你们的事情。好,店员在那边。好了,我现在要为视频中的听众做点说明。我们是冰淇淋筒的顾客正在问店员,他们在抓住店员。
一位店员走过去拿冰淇淋筒。店员走到经理那里,说:“这是一个好筒吗?”“是的,这是一个好筒。”好的。所以,我们得到了一个好筒,你把它还给那边。好了。然后现在有些人在四处游荡,但有一点,哦,坏筒。我们有一个坏筒在里面。好,这是一个好筒。好吧。所以。
你可以看到这里可能会有一点瓶颈。这里有点不太顺畅。哦,不,我们又有了坏筒。好,坏筒。好的。你有一个好筒吗?这是一个好筒。好的。好的。好了。所以,好的。那么,收银员有没有做什么事情呢?
一个人。好,看来你已经拿到了所有的冰淇淋筒。你只拿了一个筒吗?是的。只拿了一个筒。好。然后,你可以拿个号码。好的,这是一个好的冰淇淋筒。好了。让我们看看。二号。二号来了。好。一个人还在等冰淇淋筒。你拿了所有六个筒吗?我说的是六个。好了。你可以回家了。好的。
经理就是这样结束一天的工作。店员也是。如果你把到目前为止所有的冰淇淋都抓到手了,是的,你们都可以回家了。你们都很好,准备好走了。现在我们就这么做,工作完成了。好的。而且收银员是最后一个回家的。可惜。但是,就这样了。好的,你们都完成了。
然后你得到了你的冰淇淋。好的。现在,这里稍微有点混乱,对吧?没有打架。我没看到任何真正的打架。但是你们是不是看到这是我们即将尝试在一个程序中建模的那些混乱?
好的。这就是我们试图在这里建模的内容。你有程序,而且内容很多。但是我们可以一小部分一小部分地进行。好的。那么关于上面发生的事情有什么问题吗?有人看到什么奇怪的地方,或者没搞明白的地方吗?
有点弄不清楚发生了什么事情。没关系。是的。好的。所以一个顾客问一个人,然后他们都等着。就像,因为技术上只有一个“线程”可以收到信息,然后所有人都等着。是的。好的。好的。那么,第一个好问题是,看看,我对它是怎么工作的,像编程方面,如何运行,我有些困惑。
所以一切都发生得有点并行。每个顾客都可以通过告诉一个店员一次一个店员来要求制作任意数量的冰淇淋。那就是发生的事情。所以有一个顾客去找一个店员,说做一个冰淇淋。出于某种原因,我们有很多店员,每个冰淇淋一个店员,这个模型有点奇怪。
所以我承认这个冰淇淋店的模型有点奇怪,可能在现实生活中行不通。你当然可以让它更好。但是这就是模型——一个想要任何数量冰淇淋的人,去找任何数量的店员,告诉他们做一个冰淇淋。
然后他们去找经理,经理说这个是好冰淇淋,那个是坏冰淇淋,而店员必须继续制作冰淇淋,直到做出一个好的冰淇淋。Emma,你可能做了三次冰淇淋,或者其他任何的尝试,直到做出一个好的冰淇淋,对吧?所以我觉得她应该是来回走了几次,脸上带着皱眉,手里拿着几份冰淇淋。然后收银员必须处理。而注意到,店员们似乎在争抢经理的注意力。
好的。顾客们没有争抢收银员的注意力,你可能希望是这样的情况。就像在一家真正的冰淇淋店,你会希望顾客排到队后,轮流处理,接着一个接一个地被处理。至于店员和经理嘛,他们可以稍微等待,直到有一个人做出决定。好的。我们是否看到了这个基本的想法呢?好的。
现在我们将尝试使用线程技术来建模,希望我们现在已经有了一些理解,但我们将一步步地走过这些技术。
好的,我们实际上会在这个程序中讲解五种不同类型的东西。这是一个比较有分量的程序。我真不敢想象它怎么会成为一题期末考试题。也许是两题,但无论如何就是这样。我们要讨论的是一个二进制锁,到目前为止,嘿,做一个互斥锁。
就是一个二进制锁。我们要做一个广义计数器。你应该在想,也许是信号量。你应该想到这一点。一个二进制会合。二进制会合基本上是,当两个线程试图协调时。比如说,一个店员和一个经理试图做决策,比如店员必须等经理告诉他们冰淇淋是否好,而经理必须等店员过来等等。
所以这将是我们要做的事情。广义会合是指你有多个事情同时发生。这可能是,比如说,要求冰淇淋的人必须等到所有的冰淇淋都做好。它们是广义的,就像是我在等许多事情发生,然后才可以做任何事情。
然后层次化构造或多或少就像是你如何在一个事情之上做另一个事情。我们将做这个,并且看看它是如何工作的。你当然可以去下载代码。你有多线程的所有代码都在你面前。所以如果你正在看某个部分,想回去看看另一个部分是怎么运作的,尽管去做。
我知道你可能还没有机会真正看过这个内容,但我们会一项一项地讲解。
好的,那么二进制锁是什么?它本质上是一个互斥锁。好的,提醒你一下,互斥锁什么都不做,只是允许两个线程尝试进入某个临界区。而且它们不一定要进入同一个临界区,它们只是基本上在争夺某个资源。只有其中一个线程可以在某个时刻访问该资源。
许多时候,这是一个全局变量或一些共享变量,它们都想要更新,你要做我们所谓的操作。好的,然后再一次,它全是关于单线程访问。
好的,广义计数器,这是我们开始讨论信号量的地方。在这里,计数器本身,信号量本身可以原子性地递增一个变量。换句话说,只有一个线程可以在任何时候执行递增或递减操作。
如果我们有一个计数为零的信号量,比如没有许可,它基本上只是用来进行信号的往返传递。好吧,所以一个线程可以通知另一个正在等待的线程,表示这里没有许可,只有一个线程在等待。
所以我们在后面会看到这些用法。好吧,这就是你在协调一些有限资源时使用的方式,这些资源有一定数量的东西。可能是一些列数或一列,或者你知道的某种数量的东西,你可能希望将其连接起来。
好吧,好的,二进制会合就是在这里,你再次使用信号量,这是为了线程间通信。好吧,下面的这个例子很不错。假设我们有线程A需要知道线程B什么时候完成某些事情。好吧,比如当经理需要判断冰淇淋是不是好坏时,店员就得等着,必须等另一个线程完成它们的任务。好吧,我们可以做的是,让这个会合信号量初始化为零,因为我们关心的只是信号部分。
好吧,线程A实际上在等待这个信号量。好吧,在线程B完成后,它会通知线程A,线程A继续执行。好吧,线程B不关心其他线程在那时做的事情。它只是发出信号,然后继续。好吧,这就是二进制信号量工作原理,或者说在这种情况下的二进制会合。
好吧,只有一个事件需要发生,这就是我们关心的全部。好吧,这有时也用来唤醒其他线程,比如某个线程一直在等待,直到有人用这个信号将它唤醒。这是一个很好的理解方式。你可以进行双向会合。这与广义的会合不同。这基本上是一个线程等待另一个线程,而另一个线程又等待其他线程。
你必须非常小心这样做,避免它们同时等待,因为那样会导致死锁。所以你需要小心这一点。好吧,所以你得理清一些逻辑。好吧,好的,广义会合是指你有一个二进制会合和一个广义计数器。好吧,这可能就像冰淇淋顾客等待店员做某些事,或者实际上是在等待多个店员做某些事,他们必须等在那。
所以在这种情况下,线程A生成了五个线程B。听起来就像是冰淇淋顾客去问五个不同的店员。好吧,线程A生成了五个线程B,并且需要等它们都完成某个进度后再继续。这时我们会使用这种技术。好吧,再次强调,信号量初始化为零。当线程A需要和其他线程同步时,它会被阻塞,直到它们都完成。
好的,当所有人都完成后,线程a将能够继续执行。好的,这个部分的普遍性在于你有一个任务要分配。你说“我需要做三个冰激凌球”,然后将这三个任务分给三个不同的店员,你需要将这个过程普遍化,然后等待所有这些任务完成。这就是通用的会合(rendezvous)模式。好的,接下来这个层级结构基本上是将所有这些内容结合起来使用,意味着我们有一个互斥量,然后可以有一个使用该互斥量的信号量,如何将它们组合在一起。
你们有一些全局计数器,使用了这个计数器,它将与互斥量(mutex)关联,你需要等待计数器的值变化或达到某个值等等。所以这里的主要内容是,你需要能够将这些构造一起使用,以适应你需要做的事情。
好的,没问题,让我们看一下实际的代码。好的,我们已经讨论过这个了,我们已经把它建模得相当清晰了,好的,冰激凌店员、经理、顾客、收银员,很多店员,一个经理,一个收银员。顾客比较着急,所以每个冰激凌球都会配一个店员,然后一旦他们拿到冰激凌球,就去找收银员。我是说,这个模型还算不错。
然后每个店员只制作一个冰激凌球,是的,这有点奇怪,但这就是在这个世界中的运作方式,经理必须决定这个冰激凌球是否合法。好的,收银员面临的最大问题是,必须使用先进先出(FIFO)队列,否则就会一团混乱,因为顾客会说“我先到的,为什么你没有照顾我”,而布伦达是不希望那样的,因为她可以帮忙处理其他顾客。
好的,然后我们实际上需要在某个时刻决定每个人什么时候下班,这有点奇怪,因为经理必须在那之前做完六个冰激凌球,虽然有点怪,但我们本来可以用其他方法来实现,不过在这个例子中,我们直接决定从一开始就这么做。
你们将看到代码是如何体现这一点的。好的,开始之前,有没有问题?你们现在已经看过了整个大致框架,我们接下来将看一些代码,你们应该已经有个大概的了解了。好的,很好,那么接下来我们将看一些具体的内容。好的,我们将看代码的各个部分,你们已经有这些内容了,我会尽量告诉你们这些内容在哪一页,因为它们在代码中并不是按照顺序排列的。
但我们首先要看的内容是随机数生成器,只是为了了解它在哪,这个内容实际上在手册的第4页。
我把它也写在这里了。基本上我们这里有很多随机因素,为什么呢?因为我们想让它看起来像是某种实际的时间限制,并且时间并不总是一样的。好的,我们经常用这些方法,这让调试变得有些困难,因为它有点随机,但它肯定能测试很多不同的情况。
好的,这些大多是时间,我们有获取数量的功能,实际上有几个标记获取数量的甜筒,比如顾客走进来告诉我们他们想要多少个甜筒,这就不是一个时间。而有浏览时间,是顾客在周围转悠的时间,这是其中的一部分。
这里有准备时间,这是指店员制作冰淇淋甜筒所需的时间,可能好也可能不好。然后是检查时间,这个时间是经理所需要的,最后是检查结果,这是关于甜筒的二元决策——好与不好。
好的,相对简单,不需要特别关注它,只是使用了我们自己写的库函数来获取这些值。好的,我们来看一下这些结构体,所以如果你在106 B或者106 A,你可能会对看到这种全局结构体感到不适。
好的,为什么呢?因为我们并不一定喜欢全局变量,但在使用线程时,有时传递全局变量或者使用全局变量会比传递结构体更简单。出于封装的原因,我们可能会希望将它传递给一个更健壮的程序,但现在我们只是创建了一个名为inspection的结构体,它包含了这些字段。
好的,它有一个可用的互斥锁,这基本上是它与经理之间的握手,实际上是店员与经理之间的二进制交互。好的,所以有一个可用的互斥锁,这基本上意味着在同一时间只有一个店员可以和经理打交道,经理每次只能检查一个冰淇淋甜筒,这就是互斥锁。
好的,有一个信号量用于通知经理“嘿,我有一个冰淇淋甜筒给你”,还有一个信号量用于通知店员“嘿,这是你的冰淇淋甜筒,它糟糕或者很好”。还有一个布尔值,用来表示它是否通过,顺便提一下,在这里我们创建了一个名为inspection的结构体,并立即将其声明为一个变量。
似乎它过载了这个名称,但就是这样,这仍然是一个全局变量,尽管我们不一定非要做成全局变量,但这样做更简单。现在有几个有趣的点,如果只有一个值表示是否通过对吧?那在任何时候,有多少个东西可以使用这个结构体呢?
一件事,好的,一个经理或者一个职员一次只能检查或使用它,只有一个经理,这样挺好的。实际上,如果我们做了多个经理,比如看看多个经理或者助理经理之类的,那么我们就得重新考虑这个问题。我们可能需要其他东西,比如这些检查结构体的数组,或者其他什么,因为现在它的工作方式是,一次只有一个值,表示冰淇淋是否合格。
好的,好吧,这就是结构体的工作原理。到目前为止,你对这个结构体有什么问题吗?也许你没有问题。我们很快就会看到它的实际应用。
好的,还有另一个结构体,就是结账的那个,我们有点提前跳到结账阶段了,但结账有一个信号量,用于排队等待的下一个人。好的,这是一个新的整数类型,它是一个原子无符号整数,这意味着这个变量可以被多个线程同时更新,而永远不会发生重复计数。
所以是原子性的,这使得它无论 10 个线程同时操作,++ 都能正常工作,这就是我们所说的线程安全。无论 10 个线程是否更新,它都会得到 10 次递增。是的。啊,好问题,问题是,嘿,是否有时候你不想使用原子变量?大多数情况下你不会使用,因为大多数时候你并没有使用线程,对吧,而原子操作比较慢。
好的,因为它需要做其他一些操作,而且实际上需要硬件来支持它,所以它是一个不同的指令,并且有其他硬件支持,这意味着它会稍微慢一些。如果你了解 C 和 C++,底线是让用户按需快速完成任务,但也给他们提供正确完成任务的工具,如果这成为一个问题的话。
所以这是那种仅在需要时才使用原子数值的情况,否则使用更快的类型,因为大多数时候你并不需要它。非常好的问题。是的,如果你只用了一个普通的无符号整数,并且进行了 ++ 操作,它会比进行原子 ++ 操作更快,因为后者有额外的开销。
哦,对对,没错,当然你知道,最后一次我们本可以使用一个,也许我们可以用一个自主编号的信号量来实现,或者我们也可以使用条件变量。在这种情况下,我们将看到它是如何递增或被记录的,然后你会明白,如果我们有这个原子操作,我们不妨使用它。
我们会在实际操作中看到。我们会看到某人如何在“下一个排队位置”处进行操作。很好,这是我们将使用的几个结构体之一,依然是全局结构体。好的,其他的结构体也是全局的,可能有多个类型的线程同时访问它。实际上这是可以的,但收银员还是会按顺序根据数组中的顾客处理他们。
好的,也许你会明白,如果你能原子性地更新下一个排队位置,那么你就可以将其作为实际的索引来访问数组,并确保每个顾客都能在数组中得到自己的位置。你会看到这个过程的发生。好的,等待顾客的信号量通知收银员有顾客在等待,所以收银员将开始工作,然后等待顾客完成,最终顾客会发出信号通知收银员。
嘿,我准备好结账了,能结账吗?好的,有其他方式可以做这件事,但这只是其中之一。好的,我们继续下一个内容。好了,这是第一个真正的主要功能,这是顾客功能,顾客功能在页面上。
是的,第九页,接近最后,主顾客队列之前。好的,那么顾客要做什么呢?顾客一进入就知道自己想要多少个冰淇淋,我们会在主函数中初始化这一点。
然后顾客会创建一个服务员的向量。好的,它本可以创建一个服务员数组,数量无关紧要,在这种情况下我们只是创建一个向量,在C++中我们可以使用向量。然后它会调用线程来执行服务员功能,并传递它的变量i,虽然i在这里并不是顾客的ID,ID会根据实际情况传递,因为我们有两个东西要处理——冰淇淋的数量(即i)和顾客的ID。
好的,这个信息将传递给服务员,所以服务员需要知道该去找谁。顾客会触发一个请求,告诉服务员:“我的ID是某某,我是某某”。然后顾客去浏览,花点时间做其他事。
好的,接下来顾客必须做的事情是,顾客必须等待,直到所有的冰淇淋已经做好才可以结账。没问题,这意味着他们需要等到所有的服务员都做好了冰淇淋。
现在他们可能还在浏览,所有职员也已经结束了,但无论如何,当你走到第八行时,客户就知道所有的冰淇淋已经做好了。问题?是的,好问题,这里的 join 仅仅是阻塞直到线程完成,这就是 join 的全部意义,并且在线程完成后它会清理线程。
好的,到此为止,我们知道,接下来客户可以结账,因为所有职员都已经完成了冰淇淋的制作。好的,现在没有那种“这是你的冰淇淋”之类的业务,我是说,这一切本应发生在别的地方,我们没有把这些内容整合到这个环节,但你可以假设,如果我们真的在做这件事,实际上这里会有一个冰淇淋交接的过程。
但在这种情况下,职员只知道,或者客户知道,他们已经有了三只冰淇淋,可能正在等结账,谁知道呢。好了,接下来客户必须去找下一个排队的位置,看看这一行,这里可能会有你的问题。
查看下一个排队的位置,这个操作将把下一个排队的位置分配给这个客户,并原子性地递增该变量。好的,这意味着如果两个线程在同一时间到达,如果两个客户同时到来,那么其中一个将获得下一个排队的位置。
另一个客户将获得下一个排队的位置,这是有保证的。好的,这里没有竞争条件,特别是因为我们使用了原子变量,否则我们可能会在上面加一个锁,再更新排队位置,最后再解锁。
如果我们有了这个原子整数,这会让事情变得稍微简单一些,反正我们可以用它。这样一来就不再需要锁了。是的,是的,好问题,实际上你确实需要锁的情况并不多,原子操作的种类并不多,递增恰好是其中之一,我们可以在整数上执行递增和递减操作,并且能原子性地改变它。
我觉得你也可以对它做加法、减法、乘法,任何你想要的操作,你可以对它应用 map,它会原子性地执行,但如果是 map 操作,并没有原子性的 map,所以仍然需要锁和解锁,这样做虽然简单一些,但你完全可以用锁和解锁来实现这个操作。
是的。我们无法知道它会是什么吗?是不是类似的事情?
你想知道它是如何原子性地执行的吗?是的,这超出了本课程的范围,但有一些机器指令,当你调用它们时,会原子性地执行这个操作,你只需要设置它,以确保这比你所能做的稍微复杂一点。
这不仅仅是使用那个操作就像是使用那个操作一样。可能说的就是,它可能真的只是那一类操作上加了一个锁。可能就是这么简单,我没有查过,但我认为在其他情况下,会有机器指令,它会加速过程,所以你可能会想要使用这个。[听不清],是的,问题是,嘿,看看我们在进程中如何进行阻塞和解锁,如果多个进程能够更改其中一个。
它其实不是一个全局变量,因为它可能会调用一个信号处理程序,因为成员进程不共享内存。但在这种情况下,如果你使用原子变量,你就不需要锁了,只要你做的只是更新变量本身。
你不需要担心,因为它将以一种方式完成,这样你就不需要在它上面做锁。只是另外告诉你,如果是这种情况,你可以使用它。不要过度使用,因为它会稍微慢一点,但如果你能使用它,或者你也可以选择加锁,这不会有人因为你选择其中一个而扣分。好了,那么当客户排队后会发生什么呢?客户告诉收银员继续结账,所以收银员会在等待时收到信号,哦,有信号了,开始处理。
下一个客户和收银员将根据相同的变量查看下一个客户是谁。好的,你会看到当我们到达收银员时它是如何运作的。并且在收银员结账的时候,客户必须等。好了,所以客户会说“结账”,客户会等。记住,这是每个客户对应一个信号量,或者说,实际上是每个客户的信号量。
让我们再看一下信号量,它是一个信号量的客户,它只是一个零值的信号量,仅用于信号传递,不需要任何像许可证之类的东西。
你能用许可证来做吗?也许吧,不过关于许可证的问题是,仍然会有一点竞争条件,关于谁会先处理的问题。比如用许可证的方式,如果你向所有人发信号,它们就不一定按顺序处理。
这会是个问题。好的,我们需要基于我们的模型按客户到达收银员的顺序处理客户,这是最重要的部分。好了,一旦收银员传回信号,客户就结账离开了。
好的,问题来了,看看这里的等待,告诉我那是什么等待?顾客刚刚做了什么?顾客刚刚给收银员发送了信号。那么顾客接下来需要做什么?等待收银员结账,这就是发生的事情。
好的,所以这里有两个信号:一个是信号收银员说,“嘿,你能帮我结账吗?”然后等待收银员的信号回复。现在收银员必须按正确的顺序进行回复,因为这个信号没有等待的过程,你发送信号后就可以继续前进。
所以可能所有顾客都在这里等待,收银员需要发出信号给下一个正确的顾客,然后我们将看到这个是如何运作的。好的,问题来了。收银员会按顺序发出信号,我们将看到这个是如何发生的。
有没有可能排在后面的顾客会先完成他们的冰淇淋,或者是他们还没向收银员发送信号呢?对,所以第二个问题基本上是,如果排队的某个人是否可能比排在前面的顾客先完成他们的冰淇淋呢?
记住,顾客会一直在等,直到他们的冰淇淋做好,然后才会排队。好了,这不是那种大家都去拿东西然后排队的情况,不是的。你是只有在冰淇淋做好时才去排队,所以你排队的顺序就是这样。
当然,也有可能是两个顾客同时拿到冰淇淋,然后争抢谁先排队。这没问题,但无论如何排在第一位的人会先由收银员处理,我们稍后会看到面试中是怎么处理的。
还有其他问题吗?是的。有点困惑的是,这看起来有点奇怪。所以整个结账顾客的流程。这是因为你实际上有一个顾客的信号量数组,每个顾客一个信号量,或者你有一个信号量用来跟踪所有顾客的位置。
不,我们来看一下。有一个顾客数组,每个顾客有一个信号量。对,所以每个顾客都有一个自己的信号量,他们如何知道自己在排队中的位置?
他们就在这里,按顺序获得他们的位置,通过获取下一个排队位置的变量,每个顾客进来时更新这个变量,然后递增它,这样下一个顾客就能得到下一个排队位置。这就是我们在做的事情。收银员只是稍微浏览一下,按顺序处理每个人,知道下一个应该处理谁。
对,收银员知道要处理谁的唯一方式是因为收银员收到了信号。所以这就是信号传递的部分。好的。现在是不是有点开始明白这些东西是如何运作的吗?
很好。好了,如果你现在心里想:“哦,我明白了,非常棒。”那我会很高兴的。如果那是你脑海中的想法,那就太好了。
好的。顾客是如何浏览的呢?非常简单。顾客只需要获取一个浏览时间,然后按这个浏览时间休眠。好了,sleep for是线程休眠一段特定时间的方式。它们执行sleep for,然后是微秒或毫秒为单位的时间,然后它们就这样做,顾客就这么度过了多少秒。
好的,明白了,就这样。
好的。我们来看一下职员函数。记住一点。
哦,顺便说一下,发生了什么?我们有点跳过了这一部分。这是职员线程开始的地方。回到顾客函数中。这是一个有趣的点。我们会看到,到了主函数中,经理、顾客和收银员都是在主函数中创建的。然后,收银员和经理几乎立即进入睡眠状态,因为他们暂时没有事情做,但他们的线程已经启动了。
是的,他们的线程已经启动了。所以这意味着唤醒他们所需的时间其实更少。实际上,这将直接进入下一个内容。事实上,不是下一个作业,我会在今晚某个时刻发布它,或者是下一个作业。你将学习到一种叫做“线程池”的东西,它是准备好等待执行的线程。
这听起来像你见过的其他作业吗?可能是农场作业。对吧?农场作业是进程在等待启动。它们已经准备好了。它们是我们所说的“已启动准备就绪”。这是同样的情况。我们正在启动顾客、经理和收银员的线程,但直到顾客实际执行时,我们才启动职员线程。
所以就像顾客去“雇佣”一个职员。你可以这么理解,虽然那样说有点怪。
现在我们进入职员函数,看看职员会发生什么。职员需要做什么?制作一个圆锥,如果圆锥不完美,那就需要再做一个,直到经理说“你已经做出了完美的圆锥”。
所以这里有一个小的循环,表示“当没有成功时”。这个成功是一个局部变量,不需要锁定什么。它在职员本身之前,所以就是这么回事。挺不错的。每个职员都有自己的成功变量。没有必要在那个上加锁,也没有理由加锁。
职员做什么?职员制作一个圆锥。职员锁定可用的锁。可用的锁是指经理是否可用。所以职员说:“我需要锁定这个经理锁。”如果一个线程尝试锁定,而另一个人已经持有锁,会发生什么情况?
就是这样被阻塞了。所有职员都可能来到这里,这时他们在与经理争抢。经理正在看着一个锥形物,然后说经理解锁了这个锁并可以继续,或者另一个职员拿着锁并解锁它,这时经理变成了一个自由角色。
其他所有职员试图立刻抓住那位经理。然后,一旦锁定完成,职员就说:“哦,我要给经理发信号。嘿,请检查我的锥形物。”然后它会做什么?它等待。这看起来完全像之前的情况。第一次信号发出后,你会等待,因为你在告诉另一个线程。
“做点什么,我会等你完成。” 好吗?问题,怎么样?
那么,是否请求了检查部分并且已经完成,或者两者都已完成半信号?
它们都是半信号和零许可的。只是在这种情况下,它是一个信号半信号。完成的那个或请求的是请求经理的。顺便问一下,你觉得经理此时在做什么?
如果经理在等待请求的信号,它就会等待。所以这就是它正在做的事情。然后经理必须向线程发送信号:“哦,现在你可以继续了。”所以只有一个请求和一个完成。那么,这意味着一次能处理多少事物?
事实证明,只有经理和职员是相关的。但没有其他职员,因为他们都将被卡在这个锁上,直到他们能够去更改任何东西。这就是为什么这里只有一个零一的需求。>> 所以像是下面的方式。>> 总是有一个在等待,总是有一个说:“是的,然后经理发信号。”也许吧。
也许不会,取决于这些事情需要多少时间。经理可能正在睡觉,这意味着经理会一直等,直到收到信号。或者可能立刻发生。我是说,所有这些的好处是,我们设置的方式,只要事情进展顺利,它就能尽可能高效。
可能会有一些等待时间,但这种等待时间超出了我们的控制范围。我们会让它尽可能高效,所以不会有额外的等待时间。而且这里肯定没有忙碌等待。我们不是在旋转等待,也没有进行任何处理。我们只是在想,“看,我知道我在等其他线程。”
我会一直睡觉,直到事情发生。事件发生后,我继续前进。”这就是它的走向。>> 为什么要在请求和完成之后继续?它是如何等待信号请求并且然后在请求上等待的?
是的,问题很棒。为什么我们不能在请求和完成时发信号?然后再等待请求。我在考虑这个问题。我认为那里会有竞争条件。如果你先发信号,然后再等待,你可能实际上是在接收自己的信号。如果一切必须按照一定的顺序发生,你不能,必须按顺序进行。
它可能会按顺序发生,你发送一个信号,去休眠并等待。然后,当信号传播通过操作时,它会回到你这里,而不是另一个在等待。而且,不仅如此,信号量会再次下降,然后实际上需要两个信号。
所以那可能是更好的方式。实际上,两个信号会导致其中一个必须等待,除非它恰好按顺序发生。所以你应该避免那种排序的噩梦。在这种情况下,使用两个信号,因为你知道一个线程会等待请求。
另一个会等待完成,然后你就没问题了。你可以试试看。事实上,去试试用一个去构建,看看你是否能让它发生死锁。但我想你应该能做到。好问题。好了,店员方面还有别的内容吗?
店员在做什么?哦,对了,店员在做什么?这里,店员说。等待经理回来。然后,店员检查是否成功,并说,哦,要么通过,要么没有,对吧?如果没有通过,首先,它解锁。如果没有通过,那么它再做一个圆锥,对吧?否则。
如果不这样做,它会离开。所以它可能会停留在这个循环中,只要它一直在做坏的圆锥。它将一直停留在那个循环中。真正理解发生了什么。好的。好的。
做一个圆锥很简单。它将再次等待。它就像说,我马上要做一个圆锥了。然后它会花点时间,休眠一段时间。然后告诉花了多少时间。最后我将运行这个程序。它会滚动屏幕,就像,哦,天哪,发生了什么?
但你会看到它发生时的情形。
好吗?好的。让我们看一下经理函数。所以经理,记住。经理一开始就知道今天要做多少个圆锥。这有点奇怪。我们本可以做其他事情,比如有一个布尔标志,表示所有圆锥都会做完。所有其他信号都会处理,或者甚至没有信号。
你还可以有另一个信号,如果你愿意的话。或者你也可以说信号可能会很好用。再加一个总结,表示经理已经完成了。它说,回家吧。你完成了。也许你还可以将它和收银员的信号联系起来,之类的。
尽管经理实际上可以先回家,虽然这不是最好的业务流程。因为收银员可能还在照顾其他顾客。但这就是它所做的。所以它需要知道——它知道需要做多少个圆锥。经理知道需要做多少个圆锥。然后他们将尝试做一大堆。
我们这样做只是为了记录它,事实证明如此。然后他们会批准一堆圆锥。直到批准了他们需要的圆锥总数,他们才能离开。他们首先要做的事是,确定是否能离开,但他们不能立刻离开。接着他们会等待请求。因为他们在等职员来给他们一个冰激凌圆锥并说。
请在这里调查。请检查这个。然后他们检查圆锥。只是再检查一遍。它只需要一些时间。然后会花一些时间,实际上会更新结构,标明圆锥是否通过了检查。然后在检查之后,它会向等待的职员发出信号。
去检查你的圆锥。我刚检查过了。然后它会把尝试的圆锥数量加一。如果检查通过,它会显示验证的数量。现在,能否同时发生呢?这可能和职员查看检查是否通过的过程同时进行。如果他们同时查看同一个变量,这样可以吗?其实是可以的。
只要没有线程能够在另一个线程查看时更新它,许多线程可以同时查看同一个变量,因为它不会改变。所以这是完全可以的。它们可能会以某种奇怪的顺序进行,可能是在汇编语言中,但这不重要。没关系。好的,那么他们之后做什么?接下来是什么?
经理在检查一个圆锥之后做什么?他们检查圆锥编号,可能会更新圆锥的验证数量。然后他们会返回到循环中。如果他们已经达到需要的圆锥数量,就退出循环,结束这一天。这个问题就到这里。[听不清],再说一遍。[听不清],哦,真是个好问题。好的。是的。
所以你是说,看,锁在这里发生,哦,锁在这里发生。这个是这里的那个吗?
对。所以这里发生了锁定。好的。你只需要记住,锁并不关心有什么数据结构存在。它不知道。它并没有说你不能触碰这个数据结构。它只是说任何尝试获得这个锁的人都会被拒绝。对吧?它只做这件事。它并没有封装一个数据结构。它只是说没有其他人能触碰它。
它只是在说,嘿,如果你要尝试获得我的锁,你是无法得到的。所以在这种情况下,它是非常抽象的。对吧?它只是说没有人可以跨越这条线,直到锁被解开。这就是故事的结尾。好的,明白了。
好问题。这个回答能帮到你吗?它并不会影响实际操作。所以不会,经理可以去做任何事。
到那时,数据结构的操作就不需要锁了。如果是两个线程尝试更新该数据结构,那么你就需要加锁,这样只有一个线程能够基于你的锁定逻辑进行更新。所以,它可能会进行一整块操作,直到它成功修改,或者它会怎么样?不会的,你会收到一个通知。
如果尝试,嗯,非常好的问题。如果经理试图更新这里的内容,实际上它会更新,实际上它会更新检查员 Cohen,我们稍后会看到。它会更新是否通过的布尔值。完全可以做到这一点。这个不是子任务。
店员在得到信号之前不会查看它,在那种情况下会没问题。所有的内容都会更新,没问题。好的,其他关于这个的疑问吗?
听起来你们已经开始理解了。太好了。好的。那么,接下来,我们来看一下,为什么只能有一个等待中的店员?因为那个锁。这就是整个问题所在。之前的锁,如果所有店员都在那儿,他们会说:“我还不能做任何事情。”好的。那么,让我们看看,这就是检查员 Cohen。
这并不太有趣,基本上就是在检查系统时,它会睡一段时间,然后根据从 get inspection outcome 返回的随机数更新检查结果,并报告是否通过。然后结束。这就是检查 Cohen 的全部功能。但再次强调。
它确实会更新检查是否通过的结构体,但这完全没问题,因为我们知道从逻辑上讲,没有其他线程在查看它。好的。顺便提一下,它做不到的是不能访问顾客,顾客不能……如果店员去说,“哦,我没能通过这个检查。”
即使检查结果不理想,我也会把它交还给顾客。"并不是说顾客可以去检查这个,因为顾客不应该访问它。这个只是经理和店员之间的事。我可能在强调这个比喻,但这就是你不希望顾客知道的方式。
你不会希望顾客访问这个值,因为这个数字会发生变化。每次检查通过都会改变,每个通过的 Cohen 都会改变。而且它只有一个值,除了在顾客和经理,或者店员和经理关心的时候,它并没有被存储在任何地方。
然后它会再次更新,以便下一个通过的项目。这种情况下是单个结构体。好吧,为什么这里不需要锁呢?因为我们已经锁定了我们需要的部分。我们逻辑上知道唯一的更新会发生在这里,而且此时店员根本没有读取这个值。
我们知道这一点是基于我们的逻辑,而这有时是最难记住的东西。解决它,弄清楚。还有其他问题吗?
好的。现在我们终于到了收银员那儿。好了。那么收银员知道当天会有多少顾客。嗯,有点奇怪。嗯。从这个角度来看,我们本可以做一个队列来保持,或者说这个向量或队列,可能是一个队列,嗯,最终收银员会,嗯。
得到一个信号,表示没有更多的人会进入队列。当队列为空时,收银员就可以回家了。我们并不是完全按照这种方式做的。记住,这是一个期末考试问题,不像这是一个作业问题。但是,嗯。但就是这样,收银员。现在,这就是开始变得有趣的地方。
收银员确实按顺序处理。对。收银员从零开始,到顾客的总数。好的。然后,收银员做的第一件事是等待那个,嗯,等待的顾客。换句话说,如果还没有顾客在那儿,就什么都不做,只是坐在那里等。好了。等待那个信号。你能不能做到先处理第一个顾客,然后信号才到呢?
也许。这可能是另一种做法。但在这种情况下,我们只有这个。某些资源在等待多个顾客。那么它接下来做了什么呢?好了。它结账了顾客,因为每当它接收到第一个信号时,它就知道。那个信号必须是来自第一个,嗯,来自那个,第一个,嗯。
顾客。好的。那么它就调出了顾客。然后它做了什么?嗯。它知道这是刚刚结账的顾客。所以它通过那个顾客的信号灯向该顾客发出信号,表示“你已经结账了,可以去拿你的冰淇淋了”。然后它就去处理下一个顾客。那么在这期间,如果另一个顾客进来了并在排队等待,会发生什么呢?
嗯,这个权重,正是这个权重,将会直接突破。它会突破,因为另一个顾客已经发出了信号。这就是不同之处。这是信号灯和进程中信号传递的区别。记住,信号是什么?它是在一个信号灯上。它只是递减或递增一个计数器。
所以那儿有一个计数器,在信号传递的情况下已经递增。到这个权重发生时,如果另一个顾客已经在那儿,直接通过并立即处理下一个顾客。好了。这是一个重要的部分。好了。然后,嗯,它向那个顾客发出信号,去处理下一个顾客,如果有的话,可能需要等待。
顾客还没准备好。嗯,如果顾客准备好了,那么,嗯,它就会继续,接着和那个顾客一起结账。但是它是按顺序进行的。这就是这里重要的部分,因为这是一个排队的系统。好的。一旦它处理完所有需要的顾客,它就可以回家了。好的。问题。是的。
好问题。它是怎么按顾客的顺序进行的呢?不仅仅是按订单的顺序。抱歉,不是他们到达的顺序,而是他们到达结账的顺序。换句话说,顾客拿到了她的冰淇淋蛋筒,然后她走到收银员那里,或者走到队伍前面,那里可能有其他顾客,然后站在那里。
此时,让我们快速回顾一下顾客做了什么,嗯,嗯,看看,嗯,顾客做了什么。
看,这就是顾客正在做的事。
这是顾客正在做的事。记住,顾客通过检查队列中的下一个位置变量来获得他们的排队位置,然后这就是它最终会等待的信号,对吗?因为它实际上,记得,这里它是怎么做的?它会在排队的位置等待。
所以它排队的唯一方式就是去拿下一个位置,然后更新计数器,让下一个排队的人得到下一个位置。它有点像在超市里把那个小条形标放在你的商品后面,就像,你知道,下一个人会站在那个后面,对吧?这就是你正在做的事。
你就像,哦,这是我的位置,然后我会把那个条形标放在下一个人后面。这就是那个更新正在发生的事情。好的。还有其他问题吗?
好的。
让我们回顾一下,看看,之前我们做了经理尊重收银员的部分。好的,收银员。收银员在检查每个订单时完成。是的。好的,你明白了。那是什么?
这有什么有趣的地方吗?不,这正是她所说的等待的内容。好的。那它是如何知道按顺序等待的呢?是的。顾客得到了订单。因为如果我们正在更新顾客的数组并记录他们的订单,然后我们只是在查看那个数组的信号。是的。所以它就是这样,是的,这就是,嗯,我的意思是。
这是一个有趣的方法。这个函数并不真正知道有多少顾客,像是哪个顾客在哪儿,除了它正在通过这个循环。它之所以能通过这个循环,唯一的原因是它必须先等待那个信号量。就是等待信号量,意思是,是否有顾客在排队?这其实就是它的意思。
然后当顾客进来的时候,当信号发生时,这个过程就被记录下来。再来一次。但是如果有另一个顾客进来并被递增,假设有两个顾客进来,它就会足够高,表示队列中有两个人。然后程序继续进行。但因为有这个循环,它会按顺序执行。
强制程序按0、1、2顺序进行。实际上它信号的顺序是怎样的呢?它是先信号0,再信号1,然后信号2。就是这样。对了,记住,这个程序实际上非常非常简单,基本上几乎没有线程之间的实际通信。我的意思是,顾客基本上就是在等待信号。
然后那个信号突然传来,程序继续运行。你知道的,程序知道它在队列中的位置,但此时它并不在乎。它只是等待,然后收到信号,“我完成了,可以离开这个商店了。” 所以,是的。好问题。
好吧。如果我们没有使用数组,处理收银员和顾客的事务时,是否还能处理好员工经理?
不太对吧?问题在于我们需要按顺序执行这一操作。所以我们需要为每个线程准备足够的信号量,让它们在各自的位置上等待。否则,如果只有一个信号量,就会发生争抢情况,当信号传递过来时,排在最后的人可能突然就跳到最前面。
这样可就不好了,我们可不希望那样。就像超市排队的情形,排队的最后一个人突然走到另一个收银员那里。你和其他人会想,“这顺序不对。” 混乱。就是这样。好了。
关于收银员的操作有其他问题吗?好吧。所以,是的,我们必须这么做。
主函数 —— 我们终于到了主函数。好。主函数负责设置一切。让我们看看它是如何设置的。好。它设置了顾客、经理和收银员。记住,它没有做任何关于员工的事情,因为员工的操作发生在顾客那里。
顾客某种程度上神奇地创建了一个收银员线程。好吧,就是这样运作的。好。然后,顾客 —— 总订单的锥形数组 —— 其实就是用来记录日志的。它在这里设置。然后顾客 —— 我们知道会有多少顾客进来,因为我们在编写这个程序。也许这可以是一个随机数。
但这是 —— 就是 —— 我们把它做成了常量。好吧。对于每个顾客,我们该怎么做?
我们得到他们想要多少个冰淇淋。这就是我们确定每个顾客想要多少个冰淇淋的地方。然后我们为每个顾客设置一个线程,基本上是顾客ID,并且是他们想要的冰淇淋数量。就这样每个顾客知道去找多少个收银员——每个冰淇淋一个收银员——然后它将自己的ID传递给收银员和收银员。
然后我们实际上只需跟踪总的冰淇淋订单,因为我们想要报告这个。好吧,我们需要在某个时候加入这些线程,因为它们都是线程,而且它们——当它们结束时我们需要加入它们,我们会处理这个。好吧。然后我们需要一个线程给经理,一个线程给收银员。
我们不需要告诉他们任何事情。此时——好吧,我们需要告诉经理——总共订购了多少个冰淇淋。这时收银员会使用全局常量来告诉有多少个顾客。所以不需要传递进去。可能应该考虑到封装问题。
所以注意到——我注意到收银员在经理之前就被加入了。这里有什么想法吗?
这是故意的吗?收银员在经理之前被加入。我觉得这没关系。是的,我觉得这些不相关,因为它们都会结束,它们结束了并且——它们没有协调,实际上也不需要担心这个。是的。[听不清]。最大收银员数就是总共订购的冰淇淋数,没错。是的。
这是因为我们知道每个收银员只制作一个冰淇淋。不是的,这不对。每个收银员可以制作无限多个冰淇淋,但他只制作一个合格的冰淇淋。他只制作一个通过检查的冰淇淋。所以另一个问题是,是否有可能收银员在经理的任务完成之前就发出信号?
是否有可能收银员在经理的任务完成之前就发出信号?当然。那时不会发生任何事情,因为没有人——发生的只是你发出信号。记住,这和进程中的信号不一样。这只是更新信号量。所以信号量无论如何都会被更新。
谁在乎是否已经有经理线程了?除非经理线程准备好并检查该信号。否则如果它等待这个信号,它会直接继续。是的。好问题。这个信号并不是在相同的意义上进行的。很遗憾它被称为信号,因为它并不完全像我那样发信号。你发的是信号量,然后其他人可能会在等待这个信号量。
这就是抽象信号部分的作用。好问题。其他人对此有问题吗?
那么,在我们接下来做什么呢——现在这是最有趣的部分。我已经稍微提到过了。经理线程——顾客们只是都在启动并忙着他们的事务。他们在创建收银员等等。经理和收银员,他们最初可能做的事情就是等待。
但它们已经在运行并且准备好,只是在等待那个信号的发生。当通过条件变量任何时候发生时,啪,它们就开始工作,开始执行。你不需要重新创建一个线程,而创建线程会花费一些时间。这是一个很好的方法。这将为我们设置接下来讨论的线程池内容做准备,大约在两次作业后。
我们到时候会讨论这个。线程池是一个很好的方法,可以减少创建线程时带来的延迟。确实会有一些延迟。好吧,那么我们该怎么做?我们先让所有顾客都完成任务。然后我们让职员和经理完成任务。我不认为这些任务的顺序有什么关系,因为我们只是等待它们全部结束。那意味着我们不会结束程序,直到所有任务完成。好的。
有很多代码需要查看。希望我们是分段讲解的,这样你就能理解每一部分它们做了什么。这样比我现场打字要好得多。那会是一个噩梦。到目前为止,关于代码有任何问题吗?
从中你能学到什么?这里有很多内容。我们做了一个很大的模型。其实并不算很大,但足够大,有很多活动的部分需要管理所有线程、等待。你确实需要计划好这一切。这不是你可以随便拼凑一下,然后就说,“哦,现在我需要些什么”。
你必须坐在那里种植。相信我,朱莉·泽伦斯基,一旦你创建了这个,肯定花了一些时间在想,“好吧,我真正需要什么?”
我希望所有这些事情都能发生。我需要一些经理,那里将需要一个信号量。职员也会有另一个信号量,我只需要这些,其他的都可以。并不是她一开始就写代码的。她显然已经计划过这一切。虽然我想我知道朱莉足够了解她,她可能只是开始写了,但她很厉害。
足够做这些事情。我不会做这些的。无论如何,你甚至没有计划好这些。这并不是唯一的方法。你可以以无数种不同的方式修改这个模型。你可以让职员们一直被激活,然后你就能在某个时刻拿到一个。这将是一个线程池的类似实现,职员们。
一旦他们做出一个好的冰淇淋,就会回到泳池,再拿一个。也许你会想要一个信号量,或者一些许可数量来限制每次能处理的职员数之类的东西。谁知道呢?但你会这么做的。如果我们有多个经理,我们已经讨论过相关细节。那将不会涉及更多的结构。
不同的结构,信号量。然后你可能做一个主程序,再稍微启动它们。是的。所以我一直在谈论这个线程池的东西。线程池基本上是一些线程,它们都在等待执行任务。我认为它就像农场那样,所有那些Python进程。
仍然在全球范围内的机器上运行。你有所有这些已经准备好的进程,等待着做事情,然后轰的一下,它们就做了。它们不需要启动其他任何东西。你将在作业中看到这个。你实际上会构建一个线程池,并了解它是如何工作的。但其实它并不复杂。你有很多线程,然后你说开始,但只是等待一下,然后我就会。
最终信号会传达给你。这其实就是所有的内容。然后,嗯,那个就是线程池。等待中。我们想避免启动一个线程,浪费时间去做这件事。如果可以的话。但对于这个程序,我们是为店员做的,或者说我们并没有为店员做这件事。我们是为经理和收银员做的。好吧。这就是程序。
让我实际展示给你看。我们去找一个终端。哦,跳过这个版本。需要回到光标。跳过这个版本。稍等。好了。好的。所以我们需要去看看。我们在神话中吗?是的,我们在。我们需要去110和Spring。直播讲座线程。CPP。
好的。在这里,我已经创建了冰淇淋店。我们来看它的运作。现在。这个会持续下去。它会一直接着,一直接着。如果我们往上看,看看这里发生了什么,店员开始为顾客7制作冰淇淋锥形杯。经理拿到一个冰淇淋锥形杯。店员刚花了0。
做一个冰淇淋锥形杯需要287秒。对吧?等等,等等。它会一直继续,一直继续,继续下去。你可以看到,最终它会继续下去。我不知道有多少个。好吧。到最后,这里会发生什么呢?嗯。它显示经理拿到一个冰淇淋锥形杯。然后经理完成了。
经理在批准27个冰淇淋锥形杯之前,检查了总共333个冰淇淋锥形杯。真是糟糕的店员。结果是这样的。对吧?浪费了大量的冰淇淋。90%的冰淇淋被浪费了。不好,不会做生意很长时间。然后经理离开了。对吧?然后最后的顾客。最后一位顾客在柜台上排队,看看,是14号位置。为什么会这样?
我们如何进行顾客排队呢?我不知道。我得调查一下。为什么它不是第10位。我会翻转一下。我得看看。哦,我知道为什么了。因为12、11、5,等等。它们都发生在之前。结果,制作顾客10的冰淇淋花费了最多的时间。而顾客的顺序不是这样,顾客10会排队。顾客10的顺序是第14次切割。
最终顾客拿到他们的冰淇淋锥形杯,这是发生的情况。明白吗?
然后收银员已经把每个人的账都结了。结账花费的时间更少。我给收银员打个好评。是的。[听不清],好了。问题是,技术上来说,收银员的线程在经理线程之前结束。经理线程是在收银员线程之后加入的。所以,如果你想考虑这个问题。
收银员必须在离开前向门口挥手。是的。虽然线程实际上已经结束了,但线程的所有清理工作还没有完成。所以我要说,收银员。经理把咖啡杯留在了桌子上。有人必须去清理。它还在那里。但是经理早就走了。一切都完成了。好的。好了。随时可以查看这段代码,修改它。
检查一下,试着做一些其他的信号灯,做些更改,看看会发生什么。这对我们的期末考试来说并不是一个坏题目。如果你想做的话,如果这是那种——现在。设计Hall并使其工作是,最难的部分,就像设计这个问题一样。所以它是明确无误的。但这个问题还是挺好的。
所以如果你回去看幻灯片,看看代码,运行代码,我真的建议你要理解这一点,可能在期末考试中会看到类似的内容。我们会通过结果来见面。
P15:讲座 14 网络基础介绍 - main - BV1ED4y1R7RJ
欢迎,欢迎来到第七周,我们快到了。 作业五进展怎么样?
没问题,希望这是第一个线程作业,显然。 它有一些细微的差别,你肯定需要思考锁定和。 你得思考七个锁,还有新的测试方法,我们加入了。 这些内容,比如你必须确保你只会有这么多线程。
这一切都是为了测试你使用信号量的能力。 RSS。 新闻订阅是我们现在不怎么使用的东西,但是大约。 六七年前,它曾经是非常流行的,当这些东西。 发布时,它是一个大事件,大家都认为这是你获取新闻的方式。
你将会获取到所有这些订阅,它们会是这些。 小片段的新闻,你可以去阅读更大的文章之类的。 每个人都觉得很酷,接着我们都用了大约一周。 然后就觉得很无聊,像以前那样,慢慢就又回去了。
但是大多数地方仍然有这些RSS新闻订阅服务,所以你可以。 获取一个新闻订阅阅读器,这正是你正在构建的,用来实际创建这些内容。 好的,希望这不会太糟,它是周五截止的,我想我会做的是。 我们在作业上有点落后,仅仅是因为期中考试的原因。
发生了什么,我想给你们多一点时间完成上一个作业。 所以我们可能会这样做,我可能会在周四发布下一个作业。 给你一个稍微小一点的作业,比我们之前计划的要简单些。我得把细节都工作出来,然后我们会继续进行。
跟上进度,因为接下来的两个作业七和八都是相当有分量的。 不过是很有分量的,我想给你们足够的时间来理解。 这些内容,不至于让你们觉得有点疯狂。 好的,尽管我们还在处理中间的作业,我们。
正在转向新的、希望是有趣的内容,我们今天要讨论的是。
好的,我们今天要讨论的是网络,这是接下来的。 大事,也是我们接下来要讨论的最后一件大事。 我们要谈论的是网络,连接。 两台计算机之间,通过类似于读写的方式,它是。
有点像在两个进程之间创建管道,但不完全是进程,而是我们在创建这种两台。 计算机之间的"管道",它就是互联网的工作原理,也是万维网。 的工作原理,等等,我们今天会稍微介绍一下如何实现。
如果你真的喜欢这些内容,CS144就是网络课程,对,144是网络课程,内容显然比我们在这门课上讲的要详细得多,我想你会喜欢的,顺便说一句,如果你喜欢其他课程,那么操作系统就是143,不对,应该是140,哦不,143是编译原理课程。
对了,编译原理课程也是一门很棒的课程,然后140是操作系统课程,它讲述了如何构建线程库,如何将多进程集成到操作系统中之类的内容。如果你真的不喜欢这些内容,那么也许可以考虑修其他的课程。
如果你确实感兴趣的话,后续还有很多课程可以跟进,我们很快会花些时间讨论这些课程的内容。
所以我们来谈谈网络。正如我刚才说的,网络就是把两台计算机连接起来,而这个网络可以是非常局部的,事实上,网络通信甚至可以发生在同一台计算机上的两个进程之间,就像管道一样,但它是
使用网络协议,而不是底层的管道协议。你可以在一台计算机上进行进程之间的网络通信,但我们大多数时候是在谈论多台计算机之间的通信。好的,工作的方式是,你需要让其中一台计算机充当服务器,而服务器其实只是一个。
计算机就像是等待其他计算机向它询问问题的存在。我在等待其他计算机说“我可以连接到你吗?”。如果你没有设置好服务器,你可以整天问,但另一台计算机不会响应,好吧,所以下面是你必须要做的,你必须拥有一个。
服务器,然后你必须有一个或多个客户端连接到服务器。好了,万维网就是通过这种方式工作的,如果你连接到CS110.stanford.edu,你连接的是一个网页服务器,而你就是客户端,而且你们很多人可以同时进行这个操作。所以当我说我们可以同时做这件事时,我们确实能。
你必须考虑效率,因为服务器必须能够快速地将信息反馈给客户端,而且可能会有很多很多的客户端。所以我们首先需要考虑这一点,但我们现在不用太担心这个问题。
但最终我们将更多地思考:网络是如何工作的。它通过一种叫做套接字的东西来工作,套接字就是一个数字,实际上它只是一个整数,实际上它的范围是从零到六五五三五,换句话说,你最多只能同时打开64,000个套接字。
一次可以打开 000 个套接字,这比你可能想象的要多得多。大多数计算机并不会一次打开这么多套接字,但它只是一个 16 位整数,所以它的限制是 65,35,因此你基本上设置你的程序,让它说“嘿,监听这个套接字或端口”,这就是我们所说的另一种方式。
当另一台计算机(客户端)想要连接到你的服务器时,操作系统会注意到这一点。操作系统和硬件对此有很大的支持。操作系统注意到这一点后,会向你的服务器发送一个小的唤醒信息,告诉它“嘿,你有一个客户端需要处理”。
使用它,正是我们想要做的事情。你可以把端口号或套接字号当作一个虚拟的进程 ID 来理解。你可以这样想,它代表着某个套接字,它把你连接到服务器上的特定进程。好吧,事情就是这么发生的。
对,为什么我们不直接使用进程 ID 来做套接字 ID 呢?是因为你希望能够说“嘿,告诉大家去监听这个特定的端口”,而每次你启动一个新进程时,端口都会变化。所以,这就是我们要做的事,我们如何在。
让你的计算机查看你连接到的内容。我们可以做以下操作。我们可以,嗯,稍等一下。好了,我们可以在这里使用 studio,然后输入 next stat,接着。
我总是忘记它是如何运行的,我们用 PLNT - PLNT 来做得更大一点,所以你。
你可以看到整个内容,好的,让我调整一下,让它变得更清晰。
最终就会像这样美好地呈现出来,来吧,这非常不错。那么,你可以在中间服务器上执行这个操作,看起来这些中间服务器正在监听许多端口,你可以看到它们在监听哪些端口。它们在监听端口 25,我看看能不能找到。
看看我的光标在哪里,那里有端口 25。好的,它们在监听端口 587,这些恰好与邮件服务器相关,所以所有的中间服务器都在监听一个邮件服务器,你的计算机也在监听一个邮件服务器。好了,这个在监听。
在 53 上,每次 53 就是域名服务(DNS),所以当你访问时,
www.google.com 好吧,有人需要把这个翻译成一个数字,因为你的计算机处理的是数字,而人们更喜欢处理文字。所以这里有一个翻译层,它由域名服务(DNS)处理,该服务在你的计算机上运行,并连接到另一台计算机,再连接到。
连接到另一台计算机,这台计算机上有所有域名的信息,就像你的计算机一样,保留了一些我首先访问的地方,然后这个查询会发到另一台计算机,最终返回给你名称。从某种意义上说,这就像是为你的文件系统程序做的目录搜索,这就是。
发生的事情。好,端口22是用于Telnet的,也适用于SSH。我们稍后会讨论Telnet,它是用于SSH的,默认情况下SSH会连接到端口22。有些人喜欢将他们的计算机设置为在其他端口上监听SSH,因为端口22是一个很多恶意行为者尝试攻击的端口。
连接到它们,因为它们知道,如果能够搞定密码,就能进入电脑,所以它们会更改这个,但我们的是默认的22和631。恰好是打印机服务器,事实上我们可能不知道,但你可以。
一般来说,在你的电脑上,我们可以在一个新窗口中进行操作,你通常可以。
让我们把它放大一点,这里是127.0.0.1,也就是你的本地电脑的IP地址。如果一切正常,应该看到一个小打印机。
细节是,实际上你正在监听一个网络服务器来进行操作。
打印功能,而且它通常在许多电脑上都已经设置好了,如果你看到一个小消息说没有设置好,它会告诉你如何设置,如果还没有设置的话。我们也可以输入本地主机的地址,即localhost:631,本地主机,顾名思义。
你自己的电脑应该没问题,它也应该在那里显示出来,就是这个情况。
发生了什么呢?好,我们来看看,我们有另一个小的22和631。因此,TCP 6。结果是,正常的IP地址是32位数字。它们中有这些冒号,就像127.0.0.1那样,那是一个32位数字,只是被分隔成了小的两字节数字。
数字是32位的数字,32位数字的话,我们可以有多少个不同的IP地址呢?大约是四十亿。实际上互联网上有超过四十亿的计算机。回想一下互联网设计的时候,它实际上是一个当时由国防部发起的项目,事实上我不。
我不知道之前有没有给你展示过这个,原始互联网,看看这里应该有一些。
这里的图片,我应该能够找到,看看我一直喜欢用的那个图,嗯,这可能是它,看看那里,稍微有点不对,哦,稍等一下,那是我想的那个,哦不,那个是最小的,你根本看不见。
你大致可以看到,斯坦福大学在其中,我们是最初互联网的一部分,挺好的,对吧?注意到没有,谁不在其中呢?其他地方的相对本地的地方没有,麻省理工学院也在其中,BBN 也在,它是最早的参与者之一,这是一家起步于此的公司。让我们看看,卡内基梅隆大学也在其中,当时它还只是卡内基研究所。
伊利诺伊大学,犹他大学也在其中,相信与否,犹他大学在 1970 年代时有一个非常棒的计算机科学系,他们恰好加入了互联网。
很早以前,很多人都在进行早期的思考,不过当时他们说过:32 位,互联网上有四十亿台计算机,那不会有那么多计算机的,这太荒谬了。所以他们就开始使用 32 位数字,然而现在我们不这么做了,怎么做呢?
现在我们所面临的是路由器,它们在这栋大楼里可能只有一个 IP 地址,然后路由器给你们每个人分配一个本地 IP 地址,因此它是一个间接的层级。这就是目前发生的情况,我们正在努力解决这个问题,以便每台计算机都能拥有。
它有自己的电子邮件或者自己的 IP 地址,我们通过 IPv6 实现这一点,而不是 IPv4,IPv6 有 128 位地址,可以支持更多的计算机,实际上,我认为如果你愿意的话,几乎可以给宇宙中的每个原子分配一个 IP 地址,而不需要很多数字。
里面有很多内容,因此这里有足够的空间来扩展,但这需要一段时间来让每个人都跟上进度。一些服务器正在监听这一过程。好了,现在如果你自己运行一个程序,注意看看这一边,它说 PID 程序,它没有任何信息,因为它实际上…
它隐藏了非超级用户的访问权限,我们作为普通用户并不具备这个权限。我们实际上可以让我运行一些东西,我们将创建时间服务器描述符,我想应该就是这样,所以如果我这样做,就会在同一台计算机上运行,看看我们现在在做什么,神话 58,我们就这样在同一台计算机上执行。
好吧,我们再来一次,看,这里说的是 2 6 5 5 / 时间服务器,虽然没有给出完整的信息,但这就是我现在正在运行的那个程序。好吧,那个恰好是端口一二三四五,这是个不错的端口,为什么是这个端口呢?
用零零零代替127,我不知道为什么,老实说我也不确定为什么。那为什么是127.0.0.1呢?互联网把它当作本地计算机使用,这只是它给每台计算机的数字,你的计算机和每台其他计算机都是一样的。我不知道,这是个好问题,我不确定为什么它是这样的。
就像上面那个零点零一样,不过端口是正确的,不过你说得对。
另外一部分没有问题。那么关于这些地址,我还能告诉你什么呢?
我说过,有65,000或64,000个,实际上你只应该在一个特定的范围内使用一些。嗯,为什么它没有显示出我建立的SSH连接呢?
SSH是一个神话,这是在神话计算机上,但它没有显示出什么,没有显示实际的连接,可能有很多连接,这只是给你看。
哪些服务器是可用的并在监听,这就是它展示给你的内容。现在,好的问题,嗯,实际上你可以去查看一份完整的常用端口列表。
端口,好的,常用端口,如果我们去看,那里有很多端口,好的,这就像。
这实际上只是其中一个很小的列表,我们来看看我是否可以进入那里。
这里是我常用的端口,我把它放大一点,好了,非常大。所以基本上,端口0到1023通常是用来做系统相关的事情,比如你。不要使用那些非常低的端口,注意我们之前提到的所有端口都比较低,它们是非常明确的,不要使用这些。
因为它们都为SSH准备了,哦,顺便说一句,80是用于网页的。网页等,因此你希望能够,你不想使用。
那些端口,好吧,但是它们的列表比较小,就像我在上面说的,其他的端口是什么,现在我们有了很多,如果我们看看80,80是用于HTTP,代表网页等,所以有很多端口你可以查看,它们是明确的,不要使用它们,如果你可以避免的话。我们可以去。
接下来的列表,从1024到49151,你可以看到为什么它们选择了这个数字,基于一些二的幂,这些是分配的端口,但它们有点模糊,基本上就是有人说“嘿,我想用这个端口,可以分配给我吗?”然后IAN A这个组织会这样做。
这实际上决定了你是否有合法的理由,你是公司,还是你知道你在做一些有趣的事情。它们会说,好吧,你可以拥有一个,我们会说这是你的端口,一些端口。
它们做的是开放VPN,用来通过虚拟专用网络连接你的电脑,比如1194端口,等等,还有很多这样的端口。
这些端口有成千上万,你不必局限于那些。对吧?如果你自己设定端口,没人会说“嘿,你破坏了我的设置”。如果你运行一个程序需要那个端口,而你又运行了另一个程序在那个端口上,它们不能同时监听同一个端口,这就是问题所在。
这里的一个大问题是,我们刚才展示的就是使用了端口一二三四。
一二三四五,这个设置是为方块世界小战斗机和NetBus设定的,它们都有些相似,或者我猜它们假设大家都需要端口一二三四五。我们通常使用这个端口,因为它简单好记,反正输入也方便,但如果你必须在你的…
计算机,那可能会和你想做的事冲突,所以我…
不知道是一个方块世界,还是其他的,反正可以这么说,或者一个小战斗机 - 随便吧。
无论如何,超过四万或其他什么数字的端口,它们是上层端口,未预定义的,你可以根据需要使用这些端口,没人会说“嘿,这是我们想要的那个”。好吧,那就是端口号,让我们继续吧。
这些就是你将看到的内容,你会看到很多普通的端口,然后我们将使用我们自己的问题,嗯,那是个好问题,端口是服务器上的一个监听端口。端口和套接字有点可以互换,套接字基本上是,你一旦设置好套接字,可以把它想象成你设置了一个管道。
就是这么回事,但端口号就是套接字号,你使用的是套接字和端口号,可以这么理解,嗯,服务器只是台电脑,对吧?所以并不是说你发送消息到另一个计算机时,你实际上用的是一个端口,但不一定是你连接到的那个端口。比如我想要…
连接到你的服务器,使用端口一二三或五,而我的外发端口可能是1800,或者其他什么端口,它是通过那个端口出去的,然后,另一个计算机会说:“哦,当我回应时,就返回到那个端口。”我们实际上隐藏了这部分信息,你不需要担心这些。
对于这个不需要太多细节,它几乎都是内置到库里的。我们使用的它很有意义,是的,其他人有问题吗?这里有很多细节。你可以深入了解,好,看看还有什么,嗯,接下来让我们创建。我们的第一个服务器,好,我们将创建一个服务器,所有的服务器就是。
要做的是,你就在这里做,它叫做时间服务器,我们将使用。描述符来开始,好,做这个有很多像设置的东西。显然在这里,我们要做的是我们将设置一个服务器,基本上获取服务器的时间,结果它将获取 GMT 时间,。
是格林威治标准时间(Greenwich Mean Time)在英格兰的时间,它将获得这个,嗯,它获取时间然后将其转换为我们想要的时间,在这种情况下。我们将其转换为格林威治标准时间,这个时间是比较标准的。事实证明它是最容易输入的,但我们要做的是获取。
时间,它将赢得连接,当你接收到连接请求时。它将查找时间,然后将时间格式化为字符串,再将其推送。回连接的计算机,所以基本上像一个时间服务器,它只是告诉你。时间,好,这就是所有的了,好,我们怎么做呢?嗯,你可以。
做 int 服务器同样记住服务器只是一个井,在这种情况下它是一个。 在这种情况下它是一个与服务器关联的数字,它有点像文件描述符。好的,然后我们将使用创建。服务器套接字,好,1,2,3,4,5,就这些,我们将做的就是这些。
现在为了创建服务器套接字使用一些底层的内建系统调用。等等,你可以去查看,我相信它在 server socket dot cpp 中。实际上我们来看看,看看能否找到 server socket。server socket。dot cpp,不是,等一下。
server socket dot c 哦,这个容易,不如在这里做 server socket dot cc。
好,创建服务器套接字使用一些底层的函数,一个叫做。socket 的函数来做它,它有一些参数,我们现在暂时不需要担心,我们将一直使用这些。你可以自由查看这些,我们稍后会讨论一些细节,这个是。
很快就会完成,好,顺便说一下,你注意到我做了控制 Z 然后。Fg 吗?现在你知道这些都是什么意思了,我这一年一直在用,有些人。现在知道它是什么了,好,我们创建它,创建服务器,然后我们。基本上做一个小的 while 循环,简单地等待连接。
好的,我们现在就坐在这里等待连接,当我们找到时,我们将会做客户端的部分,client = accept,然后我们将要求它在服务器端接受连接,并传递一些no's,如果我们想获取客户端的IP地址。
我们可以使用这些,我们可以用no's作为一些参数,比如像我们这样。一个是像状态,另一个是像你想要得到什么的那种。等一下PID,我们可以用它来获取我们想要的关于客户端的信息。好的,然后我们只是要调用这个函数,我们将在a中编写。
第二步是将时间发布给客户端,这对我来说就是等待它接受,顺便说一句,accept会阻塞,直到你得到一个实际的客户端连接。好的,它就会停在这里,然后当一个客户端请求连接时,它会进入下一行,创建连接,然后进入下一行,继续。
然后它会重复,就这样。好的,现在如果publish time需要很长时间,那么你可能会开始丢失连接。这是我们需要担心的事情,我们稍后会再讨论。是的,它永远不会中断,无线网络也没有问题,这个服务器会一直运行,直到你按下Ctrl+C。
大多数服务器都是这样的,它们就会一直运行,然后你想要终止它们时,你停止程序就行了。就是这么简单。现在你当然可以用其他方式来做到这一点,优雅地停止它,但在这种情况下其实没关系。对,这是个好问题,是的,好问题,为什么我们。
想要这样做,这是为了我们可以获取时间,然后我可以请求时间,你可以请求时间,这样它就会在这里等待,任何请求时间的人都会得到时间回应。所以它会一个接一个地这样做,这就是为什么我们要让它在。
当它一直运行的时候,只要我们写得好,它就永远有效。对了,一个好问题是:等等,这看起来似乎比所有的文件描述符之类的东西更隐藏。至少现在来说,我们真正需要知道的是,写程序时,你只需要知道:看,我是否。
我已经通过create server socket设置了一个服务器,然后我接受了这个服务器并从中得到了客户端,这就是你需要查看的水平。如果你想更深入了解,随时可以查看实际C/C++代码中的头文件。我们稍后会看到如何让它更简单一点。
过几分钟我们就会看到如何让它变得更简单,老实说,那不是我们关心的层面。我们关心的是创建服务器,正确响应并发送东西,是的,好问题,试试看。现在我正在监听端口,问题是。
等一下,如果我创建两个程序,我尝试监听同一个端口,会怎么样?嗯,这个程序现在就在运行,一二三四……
在58时,如果我们再做一次,并且在124时再做一个,同样会发生错误,它会说不能启动服务器,告诉你端口已经被使用了,抱歉,就是这样。所以,当你设置这些时,得小心一点。
所以举个例子,当你用这些网络接口进行作业时,我们会根据你用户名的哈希值来设置你使用的端口,这个哈希值不太可能与其他人匹配,因此应该能正常工作。但是对,你的问题很好,确实我们不能在同一台机器上重复使用这个端口。
不太好,问题很好。好的,我们来写一个发布时间的函数。为了发布时间,我们必须先获取时间,好的,时间,然后它实际上是time_t类型的,我们稍后会看到,它是一个为你构建的结构体,原始时间我不需要结构体time_t,因为它是C++的,也不需要。
在C++中,我们基本上会使用time函数来传入一个指向原始时间的指针,这样就会填充它。然后我们将使用,实际上我们需要一个不同的时间函数,实际上可能需要这样做,但总之,我们用这种方式,PT是指向时间的指针。
我们要做的是说使用gm时间,并且我们还会传入原始时间,这会帮我们做一些转换,好吧,还有一个叫做TM的结构体,我们要处理这个,gm时间将会执行这些操作。现在我们来谈谈时间。事实证明,如果你查看时间t……
抱歉,我说错了,这不是一个结构体,它其实只是一个整数类型time t。如果你查看time t,你会发现它只是一个整数,当你请求时间时,调用time,它会基于自纪元以来的秒数来填充这个整数。事实上,如果我们去查找一下,来做一下演示……
这里我们来关闭一下时间,我相信就是这个,来看一下,时间。
返回的时间是从纪元开始的秒数,纪元根据你的发音不同可以有不同的说法,它恰好是Unix世界中的时间起点——1970年1月1日。那么,谁提出的这个数字呢?为什么选择这个数字作为时间的起点呢?实际上,Unix是在那个时候创建的。
1970年和Unix的时间系统是因为他们这样做了,他们说既然我们必须要有时间,那为什么不现在就开始呢?我不知道。如果他们当时没有考虑到基于1970年前的时间数字可能会存在问题,我也不知道,反正他们没想那么多,反正就这么用了。
因为这是自1970年起的时间,而如今许多计算机依然使用时间下划线T,一个整数,它实际上是一个四字节或32位整数。 再次强调,这表示自1970年1月19日的秒数。 我们来算算那是多少秒,32位能表示多少数字。
四十亿,没错,我们想要的就是这样,我们就这么做吧,假设。 所以有2的32次方,这是我们拥有的位数,没错,这也是我们可以表示的不同数字的数量。 事实证明,这是一个有符号数,所以实际上是2的31次方。 好吧,2的31次方,实际上是减1,因为你不能得到。
结果并不能获得整个范围,因为秒数从0开始。所以,应该没有零吧,减1,这就是不同数字的数量。
我们得到的是大约20亿不同的数字,即自1970年以来的秒数。 好吧,如果我们想弄清楚这是多少年。 我们该怎么做呢,如何将秒数转换成年数,如何计算出它的年数。
在这种情况下,可能需要除以365.25,好吧,闰年考虑进去了。 假设是对的,好的,这将是每年的天数,现在我们想知道的是秒数每一天是多少。 现在我们想知道多少年,然后再乘以24乘以60乘以60。 哎呀,我是不是忘记打印了?
好的,68年,从1970年算起,68年是2038年。 到2038年时,仍然使用这个时间表示方式的计算机会发生什么? 它会回绕,对吧? 这应该是类似的7个问题,猜猜看,会回绕,之后的任何时间都会重新从1970年开始。所以到2038年,我们最终会得到。
目前有些计算机将不会更新,直到那时。因为它们可能不会更新,谁知道呢,看看吧,实际上你们有些人比2000年还年轻,谁是2000年后出生的? 几个人对吧? 几个人。 好吧,顺便说一句,记得2000年吗,谁还记得。
Y2K问题,或者有人知道吗? 不记得了,Y2K问题,但发生了什么呢? 在1997年左右,人们意识到,许多程序中存在类似的问题,程序员只用了两位数来表示年份,因为他们当时编写这些程序是在1970年代。
像是,谁会在25年后使用这些计算机呢,哈哈哈。 没错,银行,对吧,银行仍然在使用这些计算机,使用相同的程序,等等。 他们做的是,他们只用了两位数来表示日期。 主要是为了节省内存,因为当时他们。
在70年代建造这些电脑时,内存实际上是有意义的。当时,每个日期如果保存两个数字,那实际上节省了大量的内存,回到过去的时代,节省了很多内存。所以他们这么做了。当2000年到来时,日期会回绕并回到零。
不是2000年,而是1900年,然后这会成为一个大问题。我第一次听到这个问题是在1997年左右,虽然一些人比那时稍早就考虑过这个问题,但1997年是世界上大多数人都突然意识到,哦,我们有麻烦了,整个世界可能都会陷入困境。
如果所有银行的计算机软件都认为现在是1900年而不是2000年,整个系统就会崩溃,世界也会陷入混乱。这在1997年左右是一个大问题,很多人检查这些电脑说:“好吧,我们能修复这些问题,我们只需要雇佣人来处理。”
重写代码并重新编译等等,相信与否,很多代码仍然是用COBOL编写的,这是一种在1950年代设计的语言。结果证明,这种说法有一定的真实性,比如许多银行软件就恰好是用COBOL编写的,而大多数COBOL程序员已经退休。
住在佛罗里达州的人们都很疲惫,于是他们开始拿起手机,有人说:“嘿,我们会付你一百万美元,来我们银行修复这个问题。”他们都坐飞机去了,赚了很多钱,并且大部分解决了这个问题,但其实人们在12月31日时依然感到不安。
就像存放金枪鱼罐头和一些东西一样,存放大量水和所有这些东西,因为他们认为世界因为这个原因会崩溃。当然他没有做这件事,我记得我曾访问过一个网站,上面写着日期是101年1月1日,或者是类似那种奇怪的日期。
这个问题可能就是2000年时发生的,但不管怎样,那个问题发生在2000年,我们又面临着另一个Y2K问题,像是更好、更大或更快的挑战,反正2023年依然有一些电脑在处理这个问题。
也就是说,我们仍然使用这种时间下划线T,一些我们现在使用的电脑使用64位数字,顺便说一下,64位数字将是从2的31次方到63次方,这样我们就能使用到…这给我们带来了,嗯,这是什么?两。
一百九十二亿年之后,所以如果人们现在谈论这个问题,不是九十二亿年,而是现在也许不再会有这样的问题了,也不会再有电脑谈论这个问题,因为你知道,很可能再也没有人类在讨论这些问题了,他们会笑哈哈地说,我们已经处理好了,这个问题已经解决。
我想太阳会在那之前变成红巨星,然后我们所有人……地球会受到影响,我想所以可能不会有电脑了,或许会把一些电脑送到其他地方去。
谁知道呢,反正他们得面对这个问题,两百九十二亿年之后,但就是这样,所以回到我们之前谈论的话题。
离题不谈,我们正在获取时间,把它转换成这个 struct TM,这有点拆解了时间,问题来了,为什么会使用 time TV?抱歉,可能是因为你可以得到一个负的返回值,像"哦,干脆就这样,能返回负值"。
这可能就是原因,嗯,谁知道呢,但他们也想过,嗯,再过68年,如果他们用了32位系统,或许可以再多用68年,那是个不错的选择,但他们没想到这一点,可能只是因为他们认为返回值会是负数。
可能的原因是,是的,哦,时间,真是个好问题,你问的是电脑里有没有计时器,对吧?我的意思是,是不是有一个时钟在更新,是的,当然有,计时器的运行频率就是你电脑时钟的频率,然后他们只是将其除以某个数字。
如果你的电脑运行在一千兆赫兹或者类似的频率,只需要除以一千兆,就能得到秒数,这样每秒更新一次。在这种情况下,时间就使用信号来更新,顺便说一下,基本上是操作系统来处理的。
每秒都会发生一次信号,这是一种计时器,每秒触发一次,它实际上比这更频繁地发生,因为有些比一秒更精确的计时器,但确实有一个计时器会更新一切。哦,是的,好的问题,如何同步这些计时器呢?
计时器使用时间服务器,基本上就像格林威治英国的某个人说,“这是准确时间”,然后就是这样,而这实际上是由Strata Mercer来执行的,顺便提一下,所有的时间服务器都说"这是准确的时间",而且每隔几年就会有闰秒什么的,然后它们就说这是时间。
这是一个真实时间,接着一堆时间服务器连接到它,然后它们有自己的本地版本,然后我们再把这些连接起来,虽然并不是每次都能完全协调,但还是挺协调的。是的,是的,大家没听到的话,艾米拿到了一台来自德国的电脑。
或者每次她开机时,它都会重置到德国的时区,或者……是的,当他们设置这个电脑的 BIOS 时,它可能会设置成,如果无法获取时间,就查找并设置为某个时区的时间,可能是它最初制造时所在的时区。
这是一个硬件问题,顺便提一下,可能是因为你的电池。计算机内部有一个小电池,可能已经没电了,这可能是它会发生这样的原因。然后它连接到服务器,没问题,一旦完成就好了。
好的,好,接下来让我们完成这个程序,然后再开始下一个程序。我们将使用这个 gm time 函数来获取时间,然后我们将使用这个 gm time 函数将其转换,接下来我们将需要将其转换为字符串,让我们就用 char* time string 来表示,128 是。
绝对足够长,以保持我们的小时间字符串,然后我们要做的是 STR F 时间,这就是将时间转换为实际字符串的函数。在许多语言中都有这个 str time 函数,它接受时间字符串,接下来它还需要什么?它还需要接受大小。
它是时间字符串的大小,所以它会覆盖过去的东西,百分号 C 在这个情况下其实并不是表示一个字符,而是表示以本地定义的时间来做。这是它将使用的 GMT 时间,然后它需要接受指向实际时间结构体 TM 的指针,好,接下来我们来看看。
我们要做的是,我们将执行这个操作,这可能会让你回想起一些回忆,也许不是好的回忆,回到期中考试的那个字节数,已写入字节数等于零,大小。T,要写入的字节数等于我们发送的时间字符串的字符串长度。结果我们不需要在这里加上零,实际上我们不需要发送它。
然后当写入的字节数小于要写入的字节数时,num bytes 是什么?我总是能理解这一点,num bytes written。加上等于写入,然后我们会尝试客户端,然后是时间字符串加上已写入的字节数,再减去剩余的字节数。这是一长串。
成为一行长的代码,已写入的字节数,好,接下来。
在 while 循环之后,我们完成了写入,接下来我们需要关闭客户端,好,这实际上现在变得非常重要,要正确地在循环中进行写入,因为当你处理网络时,可能非常可能不会把所有字节都推送到网络文件,这个过程相当复杂。
很可能你会把它们都推送出去,但在这种情况下,除非你有一个已填充的管道,否则可能会让你从期中考试中稍微“扭动”一下。但这就是发生的事情,我们需要把这个写出来,实际上写出所有字节,但是请注意,在这种情况下客户端,好,写入。
函数像是没有使用的,将它当作一个文件描述符,在 Unix 中,它确实是一个文件描述符,结果它变成了一个指向套接字的文件描述符,套接字会将数据发送到客户端,好,我们来看我是不是做对了。
这里有任何错误吗?时间服务器描述看起来还行,然后我们运行它。运行另一个在这边的时间,我们之前已经看过了服务器。描述符,我们其实没有看到它工作,时间服务器描述符正在运行。好了,你实际上可以通过连接到这个东西来获取那个信息。
我写了一个小客户端,我们可能下次再谈,但你也可以通过一种有趣的方式来做,使用一个叫做 telnet 的函数,稍后我们会详细谈一下。telnet 可以说是 SSH 之前的版本,虽然它的安全性远不如 SSH。
安全外壳(secure shell,telnet)是非常不安全的,但它所做的所有工作就是。它连接到网络并建立连接,然后你可以与网络进行双向通信。所以如果我们输入 telnet myth 58 然后给它端口号。1 2 3 4 5,好了,一切顺利,它会告诉你一些。
细节说明,尝试使用 myth 58 时,实际上是它转到了来自 58 的 IP 地址。连接到它时,它会告诉你一些关于如何退出的事项,所有你要做的就是。建立连接,一旦你建立了连接,神话时钟服务器。就会返回时间,关闭连接,这就是这里发生的情况。
就这样了。没有来回的交流,稍后我们会涉及到来回的交流,但就是这样。注意,现在是 5 月 13 日 9:14,实际上是 9:14,因为。那是格林尼治(Greenwich)英国的当前时间。结果是这样的,21:14:9:14,我相信是正确的,可能是差不多的数字,嗯,一些人可能会觉得不同之类的,嗯,所以,某些人。
发送消息“嗨”,你没有做那件事,谢谢,但你不需要。好了,所以,反正一旦我们启动了服务器,现在这个 while 循环发生了什么。只需要一个连接,顺便说一下你们都可以检查一下,你们现在可以去访问 myth 58 的端口 1 3 4 5,获取实际的时间。
myth 58 好的,你实际上可以这么做并检查一下。嗯,我们可以在这个 while 循环中做一些操作,while 循环中做什么呢?实际上让我来运行一下。我现在需要运行时间客户端,输入 myth 58,然后是 1 3 4 5,好了,系统就会去除其他的垃圾信息。
我们在 while 循环中这样做,你可以看到它在更新。什么是 while 循环呢?时间客户端的 myth 58 1 3 4 5,然后我们让它暂停一秒。好了,就是这样,然后每秒钟我就会更新一次。我的跳过一秒可能不精确,或者它可能会做得不对,但你。
你可以看到每秒钟它在更新,因为它会在我们进行时不断更新。好了,你还可以做一些有趣的事,我写了一个小脚本,叫做 corner time.sh,它看起来有点奇怪,事实上是这样的。
它基本上在调用这个,等等,让我修改一下哦,确实是 myth 58,挺好的。
这是 corner time 的作用,让我先给你们运行一下。
corner time基本上是让时间一直显示在你shell的左上角,对吧?你有没有想过Emacs是怎么做到的,让你可以在shell的上方而不是下方输入?实际上它是利用了这些。
这些叫做转义字符,所以如果我们看看这个,嗯,让我看看。
转义序列,搞定了,下面是一个所有这些转义序列的表格。
这些序列基本上是这些约定的东西,你必须发送给终端,以让光标移动。对,你可以做到这一点,你可以制作类似吃豆人游戏之类的东西,或者做任何你想做的事,你可以让它在屏幕上移动,这挺有趣的,它需要…
这些东西叫做转义命令,嗯,如果我们再看看这里。
转义字符,实际上输入那个字符有点奇怪,实际上它是一个字符,你实际上必须输入Ctrl + v,然后输入转义字符,它就会把那个字符插入给你。然后每次你想要打印一些东西时,你就打印转义字符。
然后是一些命令,基本上在这个情况下,它保存了光标的位置,然后移动到左上角,然后,嗯,设置为白色背景和红色前景。所以我想这么做,然后它调用时间客户端并更新它,然后它只在这种情况下休眠0.2秒,如果你在。
背景好了之后,你可以执行LS或者其他命令,它会不断地返回。
如果你想保持顶部的小计时器当然,反正你在电脑的其他部分也有一个,然后我们可以,我们可以做那个,嗯,它不一定在像它们这样的程序上工作得很好,呃它工作得还可以,但如果你想要像这样,它会工作得还不错,但然后…
有时候它会出现这种情况,如果你回到顶部,它可能不会工作。好了,搞定了,是的,它不是世界上最稳健的东西,但…
嗯,就是这样,看看,我们把它去掉。好了,嗯,你可以使用这个时间服务器,如果你想的话,好的,那有谁尝试连接到它吗?连接上了吗?如果你连接到时间服务器,可以连接到它了,对吗?
程序稍微好一些,呃我们想要做的第一件事是。
屏幕更大了,呃我们要做的第一件事是,我们想要这个。已发布的时间我们之前已经讨论过了,嗯,我们实际上是想要去掉这个。
这个整个while循环,业务逻辑部分,是期中作业的内容,我们不想写那么多。好了,我们想要让它更容易打印输出到服务器。让我们使用一个名为socket++的库来实现这个功能。这个socket++库只是一个封装了那个命令并完成所有工作的库。
缓冲和其他必要的操作都已为你处理好,这使得实现变得更容易。所以,让我们实际快速地重写一下。核心部分——不,我们要做的是——时间服务器,保持相同的内容,这样我们就不需要再重新输入所有的差异了。所以我们需要做的就是,你只需要。
pat,你可以使用它,它不是内建于C++的库,但你可以使用它,叫做socket++/sock stream,这很好。然后,你所需要做的就是在这里。删除我们不喜欢写的所有东西,只需要设置它。
要知道如何连接并与我们的客户端交互,我们做以下操作。我们说,sock buff sb,它是套接字缓冲区,然后我们告诉它,这是我们希望你写入的文件描述符。好的,然后我们做iosock stream ss,它现在是一个套接字流,属于这个库,接着我们可以做一些类似的操作。
ss,然后这些应该看起来非常熟悉,时间字符串。它处理所有的工作,运行得很好,另一个好处是,我们也因为套接字的工作方式,当函数退出时,哦,套接字流退出作用域时,套接字缓冲描述符就会被关闭。
抱歉,析构函数会关闭客户端,所以你不需要再关闭它了。一旦它退出作用域,客户端会自动关闭。这非常好,这样你就不需要再写那些关闭代码了,真的不需要写那些,真的很麻烦。
再次写入时间服务器描述符,哦不,套接字,套接字流,看看这里是不是对的。时间服务器必须做的事情。
它是socket++/sock stream,做对了。哦哦,点h,哦,好的,谢谢,比我自己看这些东西要好。好了,行了,所以它将是相同的,完全相同的套接字在那里。好了,接下来我们要做的就是。
这将大大简化操作,这样你就不需要处理写入和等待了。只要我们有一个C++程序,你就可以使用这个库,我们将在接下来的部分中使用它。
项目和其他事情会让它变得更简单。好的,还有什么呢?嗯,我们会担心什么呢?好吧,事实证明,实际上我们可以尝试一些事情,假设我们添加了一些东西,假设我们让发布时延非常慢。好的,假设时间服务器描述符是cc,让我们假设它可能会很慢。
发布时很慢,但它会非常慢,所以我们可以说类似,休眠之类的,哦,那不行,怎么不试试两个呢,来吧,如果我们这样做。
看,做到了,然后假设我再次运行这个程序。如果我们在这儿做我们的每秒操作,嗯,等一下,它会每秒尝试获取数字,但是应该停一下,让我们看看我们应该做什么,改为去掉那个休眠看看是否有区别。
它应该好了,所以现在它不再休眠了,谁在休眠呢?是服务器本身有问题,当你尝试请求一个客户端时,它会阻塞客户端请求,所以实际上要执行这个操作需要时间。我们想要去除这个延迟,因为你不希望在别人完成谷歌搜索之前等待他们的搜索。
对,你希望它运行得更快。嗯,实际上在我们做其他事情之前,让我们先去掉那个休眠。好吧,我猜我们可以稍后再做,但我们要怎么做呢?线程。好的,现在你了解了线程,每当有人想连接时,你就可以启动一个线程,并且记住线程是如何工作的,你现在就可以做了。线程会说,好,我将开始。
在某些情况下,假设我启动了线程,忘记它,让它自己去做事情。然后我会返回到我的while循环顶部,或者其他什么地方,然后我可以接受另一个连接。好的,这就是我们实际要做的事情,我们将使用一个叫做线程池的东西,我已经提到过几次线程池,线程池就是你将要使用的东西。
为下一个作业构建,好的,现在我们要使用它,但你将为下一个作业进行构建,你将会看到它在下一个作业中的工作原理。但这使得实际操作相对简单,关于我们必须做什么来实际完成这项任务。让我们来做这个,我们需要去做。
到主程序,而这个while循环就是问题所在,对吧?我们基本上是在阻塞,直到发布时返回。所以我们想要做的是,我们想说,哦,好,让我们创建一个叫做线程池的东西,它允许我们每当收到请求时就设置这些线程,每个请求,我们将把它交给一个线程,最多允许多少线程在池中运行。
这里我们就用四个线程,结果证明你确实需要另一个 #include,看我能不能这次正确输入,#include,我相信是 thread_pool.h,好了,就这样,然后我们在主函数中要做的就是,我们还是会做 accept,但是一旦 accept 发生,我们要尽快回到那个 accept,尽量快。
这样我们就不会阻塞,也不会让用户等待,好的,在这种情况下我们要做的是。我们将说 pool dot schedule,当你写代码时,你会明白这一切,我们基本上是要调度一个池,这需要一个函数。很好,所以我们这里将使用一个匿名的 lambda 函数,我们需要做的是客户端,然后我们需要做的是。
发布时间,发布时间给客户端,这就是它的全部,然后我们就不再需要那个了。好了,现在这会立即发生,然后返回到 accept,你将能够更快速地接受更多的客户端,它们会各自进入自己的线程,最多四个线程,在这种情况下我们可以将其设为四十个,但哦,我们还没有做。
我们还没有设置好这个,嗯,我们需要设置,来看,线程池 pool 四,挺简单的,你设置好线程池,然后你可以调度线程去做任务。你可以安排任务让这些线程去执行,好的,这就是发生的事情。有关于这个的问题吗?是的,像线程不会像其他的那样,嗯,所以会更高效/节省资源。
不像其他的东西,问题是我们这么做是因为它不占用那么多资源,虽然它们占用的资源更多,事实证明它们的资源消耗更多,但有一个好处,嗯,就是它能更快地完成。所以每当有人请求时间时,我们就会启动一个新的线程,或者说指派给一个空闲的线程,让它去发布时间。
就是这样,我们使用的方式就是这样,好的,使用线程池其实非常简单,事实证明它很方便,线程池自动完成了所有的工作,自动进行线程池的操作,它会自动做所有的 join 操作,你在使用线程池时会自己构建这些功能,好的,但我们需要更改的一点是,你有没有问题,为什么我们需要创建线程?
我们需要在下载时创建线程,所以下一个任务你将看到如何使用线程池让事情变得比现在更快,尽管对于任务五你可能不容易看出差异,但基本上,拥有已经在运行的线程意味着你不需要在每次使用时重新启动它们。
这样做的原因是它的速度更快,在那种情况下它们都只是在等待,直到给它们一些工作。就像你在作业三中做的那样,我们的选择是四个,因为它比一个大。你知道你需要多少吗?好问题,这取决于你预期的负载是多少。谷歌会运行成千上万的线程。
因为他们知道成千上万的人会使用,每台服务器都有很多很多线程。对于我们来说,我只是做了一个小的操作,我们本可以设为40,也不会有什么不同。通常情况下,你的线程数会受到特定机器上线程数的限制。如果你不是超级用户的话,大概会限制到几千个线程之类的。所以我们现在不想这样做,接下来是。
我们不想做的是让系统的带宽变得过载,我们不希望它被完全占用。这就是在作业五中你要做的事情:你会说,我只想要总共24个线程,并且每个特定的服务器最多只有10个线程,因为你不想把所有请求都发给它。我们正在尝试做的就是类似的操作。
我们也需要限制这些东西,所以我们来做这件事。我们还需要做另外一件事。我们需要将发布时刻变成线程安全的,现在它不是。你可能不会意识到这一点,因为你可能没考虑过,但我们现在的做法是使用一种全局可用的指针,或者说我们使用的指针实际上是试图。
我们可以使用一个不同的函数来处理这个共享相同时间结构的情况。前两行代码会完全相同,我们现在要做的是,我们不再创建指向时间结构的指针,而是直接创建时间结构的本地副本。然后接下来。
我们要做的是使用另一个函数 gm_time_r,其中 r 代表可重入(reentrant)。这基本上意味着它是线程安全的。为什么它是线程安全的?因为你将会把时间复制到你本地的线程副本中,这样它们就不会都在共享这个副本。好,这就是我们这样做的原因,并且它需要一些额外的信息。
这个原始时间函数需要一个指向 tm 结构的指针,这是我们要做的。除此之外,我想除了这个是可重入的(reentrant)之外,我认为我们所需的更改就这些了。再说一遍,我做错了吗?你不认为它需要因为它是一个指向时间的指针吗?之前它是指向 ptm 的指针。所以现在我认为它需要,我们到时候看看吧。不管怎样,要使其可重入,只需这样做就行了。
当你使用带线程的函数时,你必须考虑这些问题。你总是要担心,能否安全地进行函数调用,像是 strtok 就不是线程安全的。为什么?因为 strtok 有它自己的内部状态,这通常不是你想要的。为什么不直接使用另一个呢?例如,我们查找 man gm time,它应该有很多不同的版本。
这里有时间,嗯,就是 gm time 和 gm time underscore r,我们来看看吧。
这是可重入版本,嗯,gm time r 函数就像是存储数据和用户提供的结构。所以基本上,它是,我不知道它是否可以查找线程。好了,它会告诉你是否是线程安全的,你必须考虑这些问题。
你在做线程哦,嗯,问题是为什么我必须从指针改成局部变量?首先,gm time underscore r 函数接受一个指向该对象的指针,但它会为你填充数据。所以在这种情况下,它为你填充数据,而不是给你一个指针。和它的局部副本,所以这是不同之处。普通的 gm time 会说哦,这是这是。
我保持时间的指针就是那个,或者是我创建的指针,并将它给你,然后如果你再次调用它,它会重新设定时间,所以你不希望线程共享那个值,你希望每个线程都有自己的副本。好问题,嗯,有没有什么方法可以手动处理线程?你不能做线程池吗?不太行,我是说你可以用线程。
如果你根本不知道你的负载会怎样,或者你只是想要一个临时的线程之类的,可能不需要为它设置整个线程池。一旦你写完作业,你就会看到在什么时候应该使用它或者不使用它。好问题,嗯,如果它不改变值,为什么它不安全?它会改变。
当你再次调用函数时,你想要知道的值,如果两个函数调用相同的 gm time,它将会被更新,然后就不再是相同的,它们试图使用相同的值,而你只是存储一次。那么假设另一个线程一秒后过来,第一个线程可能会拿到更新后的时间,如果它还没有发布它的话,或者其它原因,所以会有一些问题。
是的,伙计,好吧,是的,我们去掉了线程使其变成多线程的。嗯,关于你的问题,像是我们是否应该再试一次加上线程休眠?如果我把休眠加回去,它会。
那么这是我们之前做的事情,我们只是放了一个小的线程,我们放了一个小的。
在这里我们加入了一个sleep或者其他什么东西,对吧?所以每次一个服务器请求时,该服务器将会接收到它,然后需要等一会儿才关闭连接,两秒钟。结果发现,如果多个客户端同时到来,那么它们都会各自接收,并且它们都需要等两秒钟。结果发现,每个客户端不必等到两秒钟才接收到它。
一个独立地等待两秒钟,不需要等另一个完成,这就是它的区别。好的,所有这些说完了,现在我们有了我们的好版本,它使用了。
线程池,事实证明它允许服务器接受一个连接,迅速启动一个线程,然后接受另一个连接。所以如果有100个连接。涌入,这里有四个连接同时到达,它会非常迅速地处理它们,已经相当快了,处理它们时不需要等待已发布的。
时间函数结束。好的,这就是大致情况。现在,呃,我们已经讨论了已发布的时间,那就是更新的时间。好的,呃,考虑到现在的时间,我想我们将讨论一下构建客户端。我不确定我们是否有机会构建客户端,但我想再给你们展示一下更多关于呃,我将使用townnet来向你们展示。
了解更多关于互联网如何运作的知识。好的,这就是我想展示给你们的内容。
我们用townnet连接到我们的服务器,实际上它仍在运行townnet,呃,myth 58。一二三四五,也许没有,等一下,也许我们仍然有。
我们仍然可能有延迟,然后就这样吧,延迟还在。嗯,所以我们用townnet来做这个,townnet再次简单地打开连接,然后与服务器进行纯文本通信。好的,所以你可以这样做:townnet google.com,端口80。我们知道互联网是通过端口80的,如果你这样做,你就能做到。
我们现在已连接到谷歌。好的,现在我们并没有连接到谷歌的支付服务器或其他什么。你知道,我们没有连接到内部网络,而是连接到网站,就像你的浏览器一样,这正是你的浏览器做的事情。好的,所以就是这样,可能有一点超时,希望我能足够快地完成其余的部分,但你怎么做呢?
做这个,如果我们真的想请求一个网页,你必须遵循一个非常特定的协议。而这个协议是HTTP,你或许听说过,但它是整个互联网都在使用的协议。好的,实际上它有几个不同的版本。你可以说的是你可以说“get /”,这意味着主页。
然后你说,我正在使用http/1.1,1.0有,1.1也有,2.0现在也有,2.0你不能再使用了,因为它是二进制格式,太糟糕了,伤心。但就是这样,如果你再按一次回车键,你就能看到谷歌的首页。好的,这就是我们现在获得的谷歌首页,它有点……
事实上,信不信由你,它比你预期的要小,里面大部分是JavaScript。好的,它其实是被压缩过的。你可以看到谷歌不会在意换行符。对,为什么谷歌要在每个查询中加入换行符呢?浏览器并不在意换行符,浏览器对换行符毫不关心,它只是……
知道,进入下一个环节。对吧,如果它发送了一千个换行符,那就意味着需要发送一千个字节给你以及现在正在请求谷歌的十亿其他人。谷歌希望最小化发送给你的内容,因为它必须支付带宽费用。我的意思是,这不是免费的,对吧,它必须支付带宽费用,而十亿次乘以一百就是一个非常大的数字。
你知道,很多字符被额外发送,每天或者每两小时发送一次,或者其他任何时间间隔。它还想要快速处理,所以越多的换行符发送到浏览器中,那些不重要的换行符只是浪费了时间,浪费了传送查询的时间,你不希望为查询等待。那么谷歌是怎么做的呢?它们删除了实际的换行符,运行了一个叫做“压缩器”的工具来处理它们。
JavaScript,某种程度上是将这一切为你做好的,像程序员并不会这么写。对吧,你不会去谷歌上写“我不能在谷歌上使用换行符”,对吧,他们是不会这么做的。对,你可以随意使用换行符,然后当它们遇到这个压缩器时,实际上会生成这样的代码,所以你得到的就是这个结果,好的,但这就是你获取细节的方式。
来自谷歌的,好的,我们也可以切换到web.stanford.edu端口80,对,我在这里做。好的,差不多一样的事情,实际上这个有一个稍微小一点的超时。如果我们执行“get”然后你可以获取到实际的一个类,比如“/cs110”,它实际上要求在最后加一个斜杠,这非常讲究,你总是……
想知道为什么在106a他们告诉你fv必须完全匹配,或者在106b它必须是完全的输出,这就是原因。计算机之间的通信正是这个原因,让我们设置超时,然后如果我们不加slash直接输入1.1,大多数网页服务器要求你必须明确指定你想要访问哪个主机。谷歌出于某种原因不要求这样做,尽管我们本可以在斯坦福做同样的事。
edu 然后再来一次,另一个,然后看它怎么说,它说,哦,对不起,你请求的文档,就是没有slash的那个,slash class slash cs 110没有slash的,已经变更并转移到了slash class slash cs 110 slash,它已经为你搞定了,告诉你你做错了。
所以这就是为什么我们这样做的原因,我们再试一次,然后这仍然是斯坦福,我们要访问。slash class slash cs 110 slash 好的 http slash 1.1 然后是 host web.stanford.edu 然后就这样。
这是我们课程的网页,对吧,你可以查看课程的具体html代码以及像JavaScript之类的内容,嗯,实际上可能并不是JavaScript,许多网页也会加载外部资源,你的浏览器必须把这一切都搞清楚。浏览器会说,让我加载主页,哦,还有什么需要加载的吗?
它会去加载网页,然后这会告诉你加载更多的内容,它很大,这就是为什么有时候这些网页加载起来会比较慢,比如cnn.com,加载了成千上万的不同文件,主要是广告,我想说,然后它需要一些时间来完成这些加载。好了,没问题,有人对这个协议有问题吗?
我们将构建一个小客户端来执行这个协议,实际上我们不如直接做客户端,我们还将开始构建一个名为wget的程序,这是内置在你的程序中的,如果你输入wget google.com,它实际上会去请求网页。
就像我们刚刚做的那样,实际上这很简单,如果我们查看index.html,那就是我们刚才做的谷歌页面,你可以从终端拉取网页,随便做。问题是,当你在网页中输入URL时,它实际上是做了相同的事情。好吧,因为我在cs 110中省略了slash,它会自动搞定。
无论如何,是的,好问题,那个返回的消息说你的东西已经永久移动,浏览器会想,哦,这太傻了,我需要加上slash,结果它就发出了另一个get请求,并且加上了slash,问题解决了。浏览器会隐藏许多错误信息,因为它会自行解决这些问题。
这是其中之一,好问题。好的,呃,我要做的是客户端。starter 哦,转到 time client.cc,好的,time client.cc,这其实相对简单。我们没有太多内容。至于时间部分,第一部分只是说怎么从命令行获取信息,或者如何获取参数,好的,我们要做的是这个。
我们要做的是客户端套接字,这次不是客户端,如果我能打对字的话,客户端套接字 =。创建客户端套接字 rv1,然后我们把另一个转换成 rv2,转成整数。好的,这样就创建了客户端套接字,好的,我们只需要断言它实际上成功了。客户端大于等于零,好的,如果你愿意,可以做更好的错误检查。
在这个,呃,我们来看看,嗯,客户端,是的,你说得对,应该是客户端套接字,你说得对。我不知道为什么那个错了,应该是客户端套接字,谢谢,你说得对。应该大于零,呃,好的,然后我们要做 sock 上面和 sb,因为这是我们要设置的,就像我们之前做的,因为我们所做的就是需要能够获取。
从服务器获取信息,好的,iosockstream 和 sb 就像那样,然后我们要做字符串。timeline,我们说,使用 get line,这很棒,所以我们只需要做 get line,它接收套接字,然后接收 timeline,它基本上就是读取它,好的,然后我们打印出 timeline nl,嗯,就这样。那就是我们的第一个客户端,好的,如果我没有犯错,那应该是它的样子。
好的,安排时间,客户端哦,希望很多,不管怎样,嗯,客户端和线程池,我们有他们的目标吗?我们不应该在这里有他们的目标,等一下。安排时间,客户端哦,不行,哦,断言没有声明,嗯,还有很多其他的东西。我们来看看,怎么用#include assert,怎么样,c 断言,看看能不能行。没错,我做错了什么吗?嗯,让我们看看。
谢谢,稍等,看看软的那个。
我从未声明过 s s,你说得对,非常感谢。那我们怎么做呢,哦,不,我们做这个。哦,你说得对,谢谢,怎么做呢,我们需要调用构造函数,我在调用它的变量。timeline,timeline,timeline,哦,完美,好的,谢谢。哦,不,还是一样。稍等,现在它似乎说是最新的,哦,也许之前的那个有问题。
time client,然后我们是不是还在运行,我们确实在运行 myth 58。12345,行了,现在我们得到了时间,我们做到了。但是不管怎样,重点是客户端实际上并不难构建,对吧?你,呃,你真正需要做的只是创建一个客户端套接字,连接到一个特定的机器,特定的端口,然后获取信息。
然后它将信息发送回去,就这样。 现在你可以连接起来,然后再发送更多信息,这也没问题,但你仍然可以这样做。所以设置客户端相对简单。现在你有了两台不同的计算机进行通信,嗯,事实上我认为它们都在同一台计算机上,但你也可以从你的计算机上做这件事。
你也可以在家里做,父母可以在任何地方做。是的,所以当我们设置客户端时,基本上就像是设置一个服务器,看起来它们差不多,虽然它并不完全对称。 但它是一个相对对称的东西,你设置了,我将设置监听服务器,我设置。 我只想建立一个连接给你,并说请给我一些细节,发送一些。
这就是不同之处,一个是设置服务器,然后等待连接,而另一个是客户端设置连接,并立即连接上。这就是它们之间的区别。 好的,如果有其他问题,我们下次见。
星期三。Coq你。
P16:第15讲 网络与客户端 - 主 - BV1ED4y1R7RJ
好的,那么我们将继续讨论网络。希望作业进展顺利。今天下课后我会有一个小型辅导会,大概一个小时或一个半小时。所以如果你愿意,可以随时过来。但今天我们将继续研究网络,我认为我们将会进一步深入。
今天你会到达一个点,你会说:“哦,我明白这里的波动互联网是怎么回事了。它确实有效一点。”希望这会很有趣。不过有个讲义,因为有很多代码,今天又是那种我不会只是疯狂输入代码的一天。
实际上我在上个季度做了这次讲座,那简直是噩梦,因为到最后我的手指都快裂开了,人们都说这是一堆荒谬的代码,根本不可能把这些代码写在黑板上或是试图现场做。所以你会看到很多代码。我们会慢慢地在黑板上讲解,让你可以记笔记或者。
提出任何问题。所以希望一切能顺利。
那么让我们回到周一讲座结束时的地方,也就是我们构建了第一个客户端,这个客户端基本上非常简单,就是说:“好,我们要创建一个客户端套接字,我们会深入了解这些细节。今天我们会做得更手动一些,你会看到更多的细节,但。
今天我们会看到一些细节,我不希望你们过于关注。你可以看一下这些细节,心想‘哦,那些是一些细节’,但我不希望你去思考。我们下周再讲这些。”但这就是连接是如何建立的,然后我们基本上设置了一个小的流,以便能够从服务器读取服务器告诉我们的内容。
就是这样,然后我们打印出来。所以这是最基础的服务器。现在通常你可能会希望实际上向服务器发送一些信息,我们今天就通过实际的 URL 来实现,然后当然你还想获取更多的信息,可能是以其他特定的形式返回。我们将通过几个例子来看看如何实现。
那么我们要看的第一个例子,我提到过,我在周一的时候稍微谈了一下它是如何工作的,并且展示了一个例子,就是这个 WGet 函数。这个功能是 Linux 内置的,你可以通过输入 WGetGoogle.com 来看到它实际的。
从 Google.com 下载结果到一个文件中。在这个例子中,它将结果保存为 index.html,就这样。所以这里就是 Google 首页,保存在一个叫做 index.html 的文件中,我刚刚用 WGet 获取的。这是一个非常简单的操作。你只需要向网站请求,网站会把它发送回来并保存。
所以我们要构建的就是这个。接下来我们要做的是,我们必须记住,我们将构建的是客户端部分,而不是服务器部分,抱歉,之前说错了。我们将构建客户端部分,它将接收一个 URL,并解析它。
它将把 URL 拆解成各个部分。好吧,像 Google 并不是一个很好的例子,但 web.stanford.edu/class110 就像是你可能传递给它的 URL。它需要做的是,连接到 web.stanford.edu 这一部分,剩下的基本上就是路径名。所以我们需要将它拆解成部分。我们可以使用部分 URL 函数来完成这项工作。
我们的大多数 URL 都会以 HTTP:// 开头。事实证明,HTTP 基本上告诉你“嘿,你正在访问万维网”。如今,你访问的大多数网站都是 HTTPS,代表“安全”,这是一种更好的做法,因为数据实际上是经过加密的。
因为你可能知道也可能不知道,我的意思是,当无线电信号发送到路由器时,除非是加密站点,否则来回发送的所有文本都是完全未加密的。如果你有密码,或者有类似的东西,它们是可以加密的,当然,也可以通过其他方式进行加密。但一般来说,HTTP 网站是不加密的。
对于某些网站来说这并不重要。但这意味着你可能会遭遇所谓的中间人攻击,在这种攻击中,你以为从一个不安全的站点获取数据,实际上数据来自另一个站点。这可能不是你想要的。因此,你应该更倾向于使用 HTTPS。
目前我们只使用 HTTP,因为我们不想处理加密。实际上,大部分处理发生在我们所处理的层次以下。但现在我们将使用 HTTP。默认路径就是斜杠。这意味着如果你输入 google.com,默认路径就是斜杠。
所以我们在这里做的其实是将 URL 拆解开来,我们只是说,如果它以 HTTP 开头,那么我们实际上需要获取它的其余部分,这就是这行 substring 代码的作用。然后你会寻找斜杠,如果有斜杠,那么斜杠后面的就是实际的路径名。所以大概就是这样,其余的部分是主机部分,比如 google。
com,或者是 web.stanford.edu 这类的网址,实际上就是这样做的。没什么复杂的,就是返回一个对。为什么返回一个对?因为 C++ 只允许返回一个东西。所以在这种情况下,我们返回的是主机和路径。因此,我们返回一个对。你也可以返回一个向量或数组,或者类似的东西,随便。
但关键是,在这种情况下,我们只是传递一个键值对,然后处理这个键值对的第一个和第二个部分。好的,挺直接的。顺便说一下,如果你尝试在字符串上使用find方法,如果没有找到,你会得到string::npos,这意味着你将使用。
默认路径就是斜杠,这意味着你只是说“嘿,我想要google.com”,没有任何附加的内容。好的。至于这个,主要部分将只调用一个函数,就是poll content,它将实际发起请求,获取结果并保存到文件中。这就是我们要做的。我们会在后续将这些步骤拆分成其他部分。
其实我们所做的就是去一个网站,发出请求,接着请求,获取数据并保存到文件,这就是Wget的工作原理。结果是这样的。好吧,有什么问题吗?是的。太棒了。是的。这就像Google.com/,就是你的问题所在。斜杠部分是默认路径,意味着没有额外的内容。
在你去到实际的网站后,我们稍后会看到为什么这有区别。这对做类似class/CS 110这种事有影响。这是我们的课程,它是一个共享网站,整个斯坦福的网络服务上都可以访问。
好的,还有其他人吗?好的。那么,poll content将做实际的工作,比如“嘿,我们需要设置一个客户端套接字”。好的,稍后我们会看到一些不同的实现方式。但现在我们只是设置客户端套接字。然后我们将进行一些错误检查,看看是否正确设置。
接着我们正在设置一个流,这个流将实际向服务器发送请求,说“嘿,给我数据”。好的。所以我们正在设置流,然后发出请求。我们将跳过所有的头部信息。记住,当一个网站响应时,它会返回很多详细信息,我们。
我们会稍后看到一些具体细节,这些细节对我们这个网站来说不重要,但你可能会在其他网站上在意这些。你可以要求网站返回经过压缩的数据,如果你愿意的话。那时,服务器会在头部告知你压缩类型。
你得自己解压,但我们现在不需要担心这些,我们直接从服务器拿回原始文本。好的。然后我们还得把它保存到文件中。好的。所以我们将创建一个客户端套接字。顺便说一句,为了这个,我们只需要URL的第一部分,因为。
这就是你如何设置它。我们不在乎它是/class/CS110,我们关心的是web.stanford.edu。这就是我们要连接的目标,连接到特定的端口上。互联网的端口通常是80端口,那是主要的互联网端口。有时你也会看到80 80端口,或者8000端口等,这些都是一些默认端口。
万维网(World Wide Web)运行在80端口上供服务器使用。这通常就是它所在的位置。好吧。如果你有一个家庭网络,且不希望其他人知道,你可以设置将流量转发到另一个端口,从而使得他人无法直接知道如何连接到它,除非他们知道。
端口号就是这样工作的。好的,关于这方面你有什么问题吗?是的,什么是组件?组件就是我们传递的那一对参数。它是主机名和实际的路径名或路径。是的,好的问题。
还有其他问题吗?好吧,到目前为止还是挺直观的。接下来,我们需要开始处理一些细节。记得我前几天做的Telnet吗?好的,实际上,Telnet命令或者说是实际的HTTP请求非常明确。它以GET开始,然后是路径,再到它所使用的协议。
在这种情况下,HTTP 1.0或1.1,使用哪个版本大概没有太大关系。接着,它会有一个有趣的\r\n。我们知道\n是什么意思,它表示换行。嗯,它其实更准确地说是“换行”,但它的真正含义是“直接跳到下一行”。\r是我们所说的回车符。
回车(carriage return)这一概念源自打字机时代,当你按下回车键时,实际上是按下一个小杠杆,推动打字机的纸张来回移动。打字机就是这样工作的。你可以找时间看看打字机。来我办公室看看,我有一个很酷的打字机项目,或者几个。
总之,回车意味着整个纸张会“嘟嘟”地回到另一侧。从你的角度看,它是“嘟嘟”回到另一侧。这就是\r的作用。\r\n应该是你每次告诉服务器“我完成这一行了”时应该发送的内容。这就是它的工作方式,它就是这么运作的。
Unix系统通常不会经常使用回车符\r。它的意思是“去下一行并回到行首”。但你仍然需要在这里使用回车符\r。是的,有问题吗?为什么不使用\n来换行?
好问题。N 到 L 只会发送反斜杠 N,事实证明是这样。是的,虽然是个好问题。反斜杠 N 只是做了 Unix 的方式,就是发送反斜杠 N。这个不是我们想要的。好的。所以它这么做了。然后下一行它会显示你所在的主机,和实际的主机地址。
然后是另一个反斜杠 R 反斜杠 N。告诉我们会正确地为你发送这个。然后我们发送另一个空行,表示我们已经完成了请求,接着我们刷新。现在我们必须刷新实际的数据,这意味着如果它在系统某处被缓冲,确保它实际到达另一台计算机。好的。
有时,如果你在做 printf 或者 N 时没有加反斜杠 N,有时字符不会显示出来,直到你加上那个 N。这就是刷新所做的事情。它说:“看,确保数据已经发送出去,否则 Web 服务器将不知道何时响应。”
好的,让我们再次做这个例子。是的,Davis。我会拔掉这些。不是,刷新并不会检查任何东西,它只是实际上说:“哦,如果我有一些缓冲数据,还没发送,确保它发出去。”这才是它的作用。它只是说我可以缓冲它,并保留它给自己。
我要收集所有数据,一次性发送出去。它说,看,现在我想让你发送它。现在一定要发送。太酷了,是的。你本来可以早点刷新,没错,实际上没关系。Web 服务器的成员会期待所有数据,它不会做任何事情,直到接收到所有数据。好问题,是的,好问题。
为什么需要刷新?事实证明,可能会有一种情况,如果没有刷新,单纯的反斜杠 N 可能不会起作用。实际上可能不会说“我有足够的数据可以发送”,谁知道呢,但刷新肯定会说:“看,我准备好发送数据了,确保它发送出去。”从这一点开始,所有内容都会被发送出去。就是这么运作的。这是什么?
从这个角度来看,这是一个安全保障。我们可以试着不使用它,看看会不会有问题,但也许它能工作。新行通常会发送一个刷新,但你永远无法预知,这种情况可能会发生。好的,你会得到一个 O。那是一个新词吗?是的,让我展示给你看。如果我们做 telnet google.com 80,然后我们说 get,我们需要路径名。那就是实际的路径名。
我会再次展示给你看,你可以在代码中查看,但我会再展示一次。然后我们说我们要告诉它一个斜杠一,我猜是 1.0,在这个例子中就是这样。然后我们发送一个回车,接着说主机,在这个例子中是 www.google.com,然后是你。
点击换行,然后它就会把所有内容输出。那就是我们需要发送的三件事,这也是我们在程序中发送的内容。让我们来看一下。我们在这里说get。我们在说。每次我连接到我的这个平板电脑时,似乎在我尝试在程序之间来回切换时它不起作用。不太知道为什么。但是它还是开始了。好了。
所以我们先执行get,然后执行路径,在这种情况下是斜杠。Google没有路径,我们也不想使用原始的index dot HTML。原始的路径是斜杠,然后我们说哦,我们正在使用HTTP 1.0,换行。接下来的那一行我们告诉它我们所在的主机。这是Google.com,www.google。
com,然后我们发送几个换行。顺序是重要的。是的,它确实按这个顺序来期待。为什么你想要主机放第二位?我不知道。这是他们设计协议的方式。我的意思是,你已经连接到Google.com了。你已经知道你连接到那里,但它仍然会说嘿,哪个特定的主机。
可能是因为你连接的网站有多个IP地址,但在该IP地址上有不同的主机。但是。是的,比如max。是的。它可能是别的,比如google.com docs.google。也可能是另一个。也有可能是这样。是的。那么这个双重操作呢?告诉那个部分的双重操作?不。
好问题。告诉它只是通过终端连接的一种方式。仅此而已。W get是通过网络执行这个操作的程序。我们正在写它。你会看到实际的单词,但我们正在写程序W get,它请求网站,获取它,下载它,保存到文件。这就是W get的作用。还有其他人吗?好的。
所以这就是请求函数要做的所有事情。它基本上就是在说嘿,获取请求。
然后我们需要做什么?我们需要跳过所有这些头部信息。跳过头部信息相对简单。你读取一堆行,直到遇到空行。好,来自网页服务器的第一部分内容是。
所有这些头部信息。它的定义方式是头部,头部,头部,头部,空行。剩下的就是数据。就是这么简单。在这种情况下,我们不关心这些头部信息是什么。如果我们愿意,可以将它们打印出来,或者查看它们,但对我们来说没关系。所以我们所做的就是执行一堆get操作,直到行为空为止。
我们只是继续读取它们。现在有些服务器不太友好,它们只发送换行,因为有些编写这些程序的人没有意识到你还需要反斜杠R。如果我们只获取一行,其中有反斜杠R,我们也会理解它。那只是一个hack,只是为了看看最常见的问题是什么。
为什么我们要用带有通配符条件的do,就像一条直线一样,通配符?
我们为什么要这么做呢?我想这可能是为了节省一行代码。我的意思是,你必须设置它,基本上得设置这一行使其不为空,然后还得以某种其他方式设置它,给它一个默认值,然后……做一个wild操作,再去获取它。我的意思是,你可以把它做得更简短,但在这种情况下,这样做只是节省了几行代码。
那么这件事是什么呢?最有趣的部分是那个小语言,但也有其他人熟悉get,我们应该了解吗?是的,这是个好问题。那么,为什么使用get呢?
好的,get是你可以用来从Web服务器请求信息的一种方式。你也可以使用post,这意味着我正在向Web服务器发送数据,比如我有一个文件,我想发送,或者我有一个名字或其他什么。你还可以使用……有另一个方法是你将在下一个作业中使用的,我忘记是什么了。
还有更多这样的情况,但这只是HTTP语言的一部分。就这么简单。它就是HTTP协议。你做的其中一件事就是使用get,或者使用post,或者使用77。追踪。那么如果你只用/r呢?如果你只用/r而不使用/n,Web服务器可能会搞混。我是说,它可能会问你“你这一行已经结束了吗?”。我不知道。你知道,这就是为什么你需要。
协议要求你必须同时有两者,所以我们发送了这两者。这就是它的工作方式。再次强调,这也是为什么当计算机相互通信时,它们需要知道确切的……正确的信息。这就是为什么我们在106A、B、107等课程中一再强调这一点。它的输出必须完全一致,这不仅仅是因为我们对它挑剔。
这是为了让计算机能够……因为我们的自动评分程序是计算机。它们也在试图进行通信,但你会一次又一次地看到这一点。你需要精确你的输出。所以这就是确切的要求。抱歉,头部信息到底包含什么?头部信息包含了它正在返回的是什么类型的数据?
无论是否,我们将看到这个例子,我们稍后会看到一些我们关心的头部数据。它是否压缩了。必须保留。无论网站是否可以在不在网站上的情况下请求数据。我的意思是,很多时候,网站并不会轻易把数据提供给其他计算机。
通过某些方法。在这种情况下,头部可以告诉你,嗯,可以,或者也许我们不想这么做。无论它是否被缓存。我的意思是,有很多不同的头部。你可以查找HTTP头部,看看有多少种。是的,头部是一行一行的。一个头部,一个头部,再一个头部,又一个头部。
然后最后一行是一个空行。然后我们进入数据部分。这就是协议。说起来很简单。考虑到所有的因素。一堆头部,换行数据。就是这些。好了。好了。我们看一下下一个例子。那就这样。哦,这不是我想要做的。等一下。
那我们开始吧。好的。那么我们接下来要看看获取文件名的函数。好的。顺便提一下,这一切都是因为你可以查看代码并看到发生了什么。但基本上我们只是将它分解到这里。获取文件名的功能将接受路径,并将路径转换为。
如果路径的最后部分是文件名,就返回文件名。如果没有文件名,它将使用 index.html,作为网页的默认文件名。如果你从未做过网页写作,如果你做过网页写作,可能已经见过 index.html,它基本上是你访问网页时看到的第一页。通常是这样。
这就是它的工作原理。好的。所以做起来很简单。它基本上就是说,如果路径为空或者路径末尾有斜杠,就返回 index.html。好的。否则,你会找到最后一个斜杠后的部分。这就是 R find 的作用,返回那部分。这就是全部。好的。
所以就是找到实际的文件和锚点。我们即将写出文件。好的。那么我们需要做什么来保存有效载荷?我们需要读取。好的。我们将会得到一个文件名,保存到其中,这个文件名将通过获取文件名来获得。我们将会得到一个流,我们将从中读取数据。
这个流基本上是网络流。好的。我们将设置并获取我们要读取的网络流,这样我们就可以读取所有的数据。好的。那我们将如何做到这一点?
基本上我们会说,直到我们没有读取完所有数据。我们用的是 while do,因为对于这种类型的流,我们可以这样做。然后我们就读取所有数据。对。我们会尽可能多地将数据读取到我们的缓冲区,它的长度是固定的。为什么是2014?
我不知道。它是2014年做的,可能是我们想要的任何东西。然后我们统计所有数据的数量,然后将其写入我们实际的缓冲区,抱歉,是写入缓冲区,然后让我们看看。抱歉。我们是将数据读取到缓冲区。抱歉。我们将缓冲区写入输出,写出我们读取的数据量。
这就是全部。好的。然后统计出多少字节,以便知道读取了多少字节。是的。对。这个问题很有意思。为什么我们需要获取 finally 和 function?
记住,如果我问最终的结果是什么,假设我们已经准备好使用了。slash class slash CS 110,那会是什么?class slash CS 110 听起来并不太合适。作为文件名。所以我们只想要它的最后部分。所以在这种情况下,我们看到的是 us 110,但实际上对于我们来说,这并不可行,因为我们会。
请求 CS 110 slash,但是如果我们这么做,那就是最后的部分。所以我们得到了路径,我们要请求的路径,但是最后的名称只是路径的最后部分。你知道的,我们只是创建这个,因为这就是 W get 工作的方式。对,那什么是 I/O sock 流?好的。这是你知道的,像你可以做 cout,你可以那样做,你也可以做 cout。
这些东西,它有效。好吧,这就是流,流是 C++ 中表示“嘿,这里有一些相对简单的方法来进行输入输出”的方式。I/O sock 流是一个类,它被创建出来包装网络文件描述符成流,以便更容易使用。就这些,我有的就是 sock。只是一个类,我们用它来简化操作,这样我们就可以做一些像输出这样的事情。对吧?
我们可以做一些事情,比如从 SS 读取数据,像这样,SS.read。这就像从流中读取数据等等。而且它会为你做所有的缓冲,你不需要担心要有一个 while 循环去处理,像是只处理一定量的数据,它会为你处理所有这些。David,你有问题吗?[听不清],SS.Gcount 是你获得的返回数据量,我相信。
我认为就是这个。所以它基本上会显示我们刚才读了多少,嗯,我们刚才到底读了多少对吧,然后它就会从读取中不断更新这些数据。好了,现在我们正在处理的是一些帮助函数。所以让我们看看,那个值是不是唯一的,实际上就是它,我们实际上有的。
做好了所有的事情,没错,我们没有漏掉任何东西,实际上我们都做到了。
因为我们需要先发出请求,我们已经看到如何在这里发出 get 请求。
然后我们跳过所有的头部,读取所有的头部,然后是新的一行,然后。我们保存有效负载,也就是所有头部之后的内容。
我们是不是没有这个?我是不是跳过了跳过头部的功能?
不,它就在那儿,没错,就是这样,好的,所以就读取了所有内容。好了,这就是我们在这种情况下要做的全部。就是这样,让我们检查一下,看看它是否工作。
好的,看看这个,看看我们的一十春季直播讲座,先把这个弄清楚。好了,然后如果我们做网络连接,做 wwebget,我们试一下 Google.com。对吧?它说是两百一十九个字节,这意味着那里出了点问题。让我们看看,可能意味着它没有正确的东西。看看它们的索引。
html 是的,它显示它已移至 Google.com/,所以我们需要做斜杠。哦,我们还需要做的是,可能需要加上 wwew。好了,现在我们有足够的字节,得到了 46,000 字节,所以实际上这有效。如果我们没有加 wwew 直接使用 Google.com 会发生什么呢?如果我们看一下它显示的内容,它说嘿,文档已经移至 wwew。
google.com。所以这就是它,基本上 Google.com 和 wwew.google.com 之间有区别,所以 Google 已经设置了,当你请求 Google.com 时,它会说不不不,请求 wwew.google.com,浏览器会为你自动完成。如果你只输入 Google.com,它会转到 Google.com 获取这个消息,然后说哦,我得做这个。
另一个请求并获取实际版本返回。这就是所有的,没错。关于保存有效载荷函数的问题。
是的,让我拿出保存有效载荷函数。对了,那个反斜杠零(backslash zero)是用来干什么的?
那么,那个做了什么?啊,好问题,所以在这种情况下,因为我们正在尝试读取,问题是为什么有一个终止的反斜杠。我想我跳过了那部分。当你在读取时,实际上它说嘿,创建一个满是零的完整缓冲区。它在这种情况下做的是这样,所以它们已经是零了,这并不意味着你不需要。
当你读取所有数据时,你必须在末尾加上一个。所以它已经有了。好问题。好吧。是的。你还需要关闭输出吗?不。在这种情况下,对于 OF 流,你不需要关闭它们。像这种情况,是的,我认为实际上它会在超出作用域时自动关闭,因为它的工作方式就是这样。好了,现在我们来看看更有趣且有点酷的示例,直到最后。
我认为你会看到哦,这很酷,这就是互联网的工作方式。而且,这不是你创建网页的方式,但实际上它非常接近。事实证明,你不一定需要创建自己的 web 服务器。大多数情况下,当你将页面放在服务器上时,web 服务器会自动为你运行它。
在你的计算机上。我们现在要做一个更底层的版本,但基本上,你可以看到 web 服务器需要做什么才能获取你的数据并将其传输给请求它的其他客户端。好吧,我们要做的就是,我们要看一个示例,它是一个 API,基本上有一个与之关联的 API。
一个应用程序接口(API),基本上就是说我们知道需要发送什么数据到服务器,它将以我们能理解的某种格式返回数据。好的,谁听说过JSON?JSON。好的,几乎每个人都听说过,虽然不是每个人。JSON是一种机器可读但也相当人类可读的格式。好的,我们到时会看到实际的格式,但我们将以这种格式输出我们的。
我将展示的结果是这个格式,这样如果我们需要,最终我会展示一个我构建的小网页,利用这个我们可以实际获取格式化的数据,以便我们的计算机可以理解并做些有趣的事情。比如把它显示在屏幕上,或者用来做一些计算,或者其他。
但这就是我们最终会创建的。我们要做的是创建这个小程序,它会使用一个叫做Scrabble单词查找的程序。这个程序你当然可以去查看,但它是一个106B程序,基本上是接受你可能有的字母(比如从某个地方抓取的字母)。基本上是任何单词查找程序,像是“朋友连连看”之类的,你拥有一个数字。
这些字母和你用来组成单词的字母数量,字母都是混合在一起的,这个程序会从这些字母中找到字典中所有可以组成的单词。好的,这就是它要做的。让我给你展示一个例子,看看Scrabble单词。
查找斯坦福单词。好的,它会打印出你可以用斯坦福字母组成的所有不同单词。好的,这就是程序做的事情。好的,任何字典中存在的单词它都会生成。好的,Scrabble单词查找网络。好的,它会打印出所有你能从中构成的单词,而这些正是我们要做的。
假设我们有一个网站,想要实现这个功能。那么我们可能会创建一个斯坦福Scrabble单词查找程序,它正是做这个的,它接收作为参数的字母,然后输出所有由这些字母组成的单词。好的,我们要做的是创建一个使用这个程序的服务器。
或者做相同的事情,除了不再输出到屏幕,而是将结果以我们客户端能理解的形式发送出去,即JSON格式。好的,你可能会想,好的,让我们进入Scrabble单词查找程序,不管它在哪里做输出,我们设置一个网络连接,做一个客户端或者我们做一个小的服务器连接,然后就有了一个。
接受所有这些,那我们为什么要这么做呢?如果我们已经有了一个完全能正常工作的程序。你们做过一个作业,使用其他程序并从那些程序获取结果,那个程序你们写的是subprocess,对吧?现在,让我们利用subprocess来实际从这个程序中获取单词并通过我们的服务器。
这需要是一个非常小的功能,基本上就是:请求到来时,使用Scrabble Word Finder获取单词,然后将其发送回客户端,故事就这样结束了。好的,这很公平。
我们将经历的细节数量有很多,但正如你所见,这就是我们想做的。我们不想修改已经运行得非常好的程序。
我们可以使用子进程来完成这个任务,好的,接下来这是JSON的样子,顺便提一下。
这是我们将要做的事情的结果,我们将运行这个程序,启动这个服务器,假设它在端口13133上,位于myth 54,当我们请求myth 54:13133/lexical时,我们将把lexical传入Scrabble Word Finder,将字母传入。所以我们需要弄清楚这一点,并且需要解析出其中的各个部分。
我们可以从中获取字母,所以我们将做这个,然后我们将。
要生成像这样的内容,我们需要说明处理请求花费了多长时间。
我们将说明它是否已被缓存。所以这不是这个作业,而是下一个作业,实际上是一个非常酷的大型网络作业,你将要做一个Web缓存程序。实际上,缓存的概念在网络中是这样的:如果你之前做过某件事,就不要再做一次,因为网络本身相对来说是。
这很慢,但在这种情况下,实际上并不是这个问题,真正的原因是我们已经获得了一堆字母,并且已经将它们转换成了单词,既然如此,我们不妨将这些单词保存下来,以防有人再次请求相同的字母,放到缓存里,然后我们只需要报告它是否被缓存,客户端为什么会在乎呢?
谁知道呢,或许客户端在乎这个,但是我们将告诉他们它是否被缓存,然后它就会变成一个包含所有内容的数组。
好的,所以这其实是一个小小的内容,它基本上是一个映射,时间字段后面接时间,逗号后面是缓存状态false,然后是可能性,它是一个数组,这就是JSON的样子。注意,一旦你明白了哦,这就是JSON中的映射,你就能读懂它了,这非常直观,而且你的JavaScript程序。
结果表明可以非常轻松地读取这个内容,这也是你可能会请求的内容。服务器获取客户端数据,好的,明白了,所以这就是我们最终要做的事情。
通过这样做,让我们看看我们到底是如何做到这一点的。正如我所说,我们将利用子进程来完成这个任务,它只会使用它来获取输出并执行。我们需要向“scrap a word finder”发送任何数据吗?不,我们通过命令行来进行操作,因此实际上不需要指定字母。
我们本来可以设置程序来自动完成,但我们只是通过命令行来做这件事。
这个将会是相当直接的,所以这是主函数,主函数是做什么的呢?
我们的服务器应该和其他服务器相似,让我们看看,找找看。
这部分你们可以在第三页复制,可能不在第三页,在第八页。哦对了,它在程序的最后部分,没错,就是第八页,抱歉,实际的主函数就在第八页。所以,我们现在在做的事情是设置服务器,就像我们以前做的那样。我们将创建… 对的,就是这个。
放大这里,我们将创建服务器套接字。好的,我们首先提取端口,这实际上是一个相对简单的任务。我们基本上会从命令行获取这个端口号。你可以在运行时指定你想使用的端口。
运行它时,如果没有指定,它将会使用默认值,然后我们将创建服务器套接字。好的,然后我们将指定监听的端口。没什么大不了的,我们将设置一个叫做线程池的东西。记住,我们这样做的原因是为了能非常快速地接受最多16个连接。
然后根据这些连接启动线程。好的,所以你们会为下一个任务构建一个线程池。然后我们将有一个缓存,在这种情况下,缓存将是那些字母映射到结果字符串向量的内容。你认为我们应该对这些字符串做什么,以确保如果我们…
获取A B C D,然后我们得到D C B A,难道这些不会创建完全相同的单词集合吗?我们应该对得到的单词做些什么,才能在将它们放入映射之前进行处理?对它们进行排序。只是将它们排序,效果是一样的,但我们还是会这么做。好了,我们还需要一个互斥量,因为我们在处理线程时,需要一个互斥量。
这里是缓存锁,基本上是因为如果我们正在从缓存读取,而其他人恰好在此时写入缓存,这可能会带来麻烦,我们可能会得到错误的数据,或者可能会导致某些内容损坏,写入可能会出现问题。虽然可能不会真的损坏任何东西,但你可能会得到错误的数据。
它可能最终会进入一个状态未知的情况,因此每当我们读取或写入缓存时,都需要加锁。所以这是一个演示。接下来,我们将有一个常规的while循环,用于我们的服务器来接受连接。我们将以不同的方式处理这些事情。
在这里,这是我之前说过的不要过多关注的部分,让我们看一下,先大致了解下,我们下周会学习这个内容。我们将设置一个叫做struct socket address _in的东西来获取实际的IP地址。这样做的原因是每当我们得到一个客户端时,我们希望能够知道它是从哪个IP地址连接的。
为什么我们可能关心这个?我们可能想记录它,或许我们是一个非常关注Scrabble单词的程序,可能会记录它或者其他什么内容。相信我,每一个你访问的网站都会记录你的数据,记录你的IP地址,记录你请求的内容。看,这就是人们如何收集你的数据,始终记录登录细节。
在这种情况下,我们关心的是IP地址是什么。我们将通过这段代码来实现这一点,虽然我现在不会详细讲解,但我们下周会看到,这段代码基本上用于获取客户端的IP地址。
通过响应,我们将说明我们从哪里获取数据,并且我们需要正确地打印出来等等,但基本上会说嘿,接收到来自IP地址的连接请求,这就是我们关心的内容,好吗?然后我们将调度线程,以便能够快速返回并进行另一次接收。
一旦我们通过这些复杂的搜索找到IP地址,我们将通过调用我们已发布的Scrabble单词函数来实现,而该函数本身会调用自处理(self.process)。我们需要做的是传入客户端,我们将为此传入文件脚本,并通过引用传入缓存,缓存锁也通过引用传入。这就是我们要做的,好的,这是我们程序的核心思想。
然后一切都会在已发布的Scrabble单词中发生,所有的操作都从那里开始。对此有问题吗?别问细节,我们下周会讲解。好的,没问题。
就像我之前说的,这一切关于地址大小、I net 和 Top 的内容,我们下周会讨论。这挺酷的,好吗?发布的 Scrabble 单词将依赖于子进程,我们最终将创建 JSON 输出,然后我们将看到它是如何工作的。所以让我们来看看,接下来的这个函数非常详细,我们会逐行分析,它的名字是。
发布的 Scrabble 单词,它在第七页,所以我很熟悉这个内容,好了。
好了,下面是它的内容,你已经看到了,我希望你能从后面看到它。下面是我们在发布 Scrabble 单词时要做的事情。我们将使它变得简单,让我们可以轻松地写入客户端,或者像我们发现的那样,从客户端轻松地读取和写入。我们将调用一个函数。
叫做 get letters,这个函数会请求我们获得的请求,找出网页 URL 中斜杠后的部分,并把它们作为字母。然后,正如我们所说,我们将对它们进行排序,因为我们不在乎顺序,但这对我们的缓存来说很重要。接下来我们会。
定时它,那么我们为什么要关心定时呢?这就像是我们的小小好处,嘿,我花了多长时间来做这个,也许它会花太长时间,或者什么的。猜猜看,缓存一次应该比程序实际运行花费的时间要少得多。希望是这样的情况,随着我们的进展,我们将看看是否真的如此,好吗,我们继续。
获取开始时间,这就是你怎么做的。这是我上周展示给你看的,当时我们做了时间服务器客户端类似的事情。现在我们准备更新缓存,或者更准确地说,我们将检查字母是否在缓存中。首先,我们锁住它,然后在映射上进行查找,然后立刻解锁,不要锁得太长时间。
不要锁得比必要的时间还长。我们本来可以只做一个锁守卫,对函数的其余部分仅保护那个变量或者映射,但是我们不想这么做,因为我们希望其他线程也能读取它,并且其他线程也能更新它。所以不要锁得比必须的时间还长,这是一个明确的风格问题。
这也只会影响你的程序一次,所以要非常小心这样做。我们基本上是创建了一个布尔值,表示我们是否找到了缓存,或者是否在缓存中找到了字符串,然后我们将创建一个向量,把那些单词放进去。如果缓存命中,我们就完成了,对吧?我们基本上说“太好了”。
向量中的单词来自我们的缓存,故事好的方式是,find 函数的工作原理是,它给你返回一对,返回对中的第二个部分,迭代器会返回一个迭代器,而迭代器中的第二项是实际的向量,在这种情况下是映射中的值。好的,所以它会这样做,否则我们。
如果没有在缓存中找到,那么我们现在需要使用子进程。所以我们将调用我们的子进程,并且我们将设置子进程命令,我们怎么做呢?抓取一个单词查找器是程序的名称,letters.dot c string。这是我们刚刚获取的字母,作为第二个参数,第一个参数是。
通过我们的程序,然后没有什么特别的应该是你从斯坦福的变化中看到的内容。shell 好的,那么我们将调用一个名为 poll formable words 的函数,我们将传入向量的引用,并且我们将传入从子进程获取到的文件描述符。这基本上是在说,我们要获取所有的单词。
从子进程中获取它们,一次一行放入向量中,十分简单。别忘了,我们必须等到完成,对吧?如果你在做多进程,你必须做等待 PID。这个例子结合了我们到目前为止所做的一切,结果证明是这样的。然后一旦我们得到结果,我们知道之前没有缓存过,我们最好。
把我们的向量放入缓存中,所以我们锁定我们的变量,锁定我们的映射,我们应该通过缓存锁来锁定映射,然后我们根据字母的键更新缓存,字母已经排序好了。就这样,我们可以使用锁保护,因为当这个作用域结束时,锁会自动释放,即使我们之前是这样说的。
再次强调,我们之前故意说做什么哦,没错,没错,没错,我们不应该调用子进程,你可以调用,看看,你可以在这个情况下调用,哦,好的问题。问题是等等,我们不应该,我们不应该混合线程和多进程。在这种情况下,它实际上是可以的,因为子进程我们非常小心。
子进程仅允许它仍在同一个线程中,这种情况下是可以的。好的,我的意思是线程,现在你确实有多个线程,你有多个进程。但是我们在正确等待它,并且我们正在这样做。好的,所以在这种情况下没问题,是的,我想让我看一下更多的细节,是的,这是个不错的。
不过,这是个好问题,因为我们总是说不要混合,不能以这种方式混合它,虽然它确实能工作,但这是个好问题。所以我会进一步调查一下,是的,好问题,当然,我们现在要沿着这条路走。哦,是的,为什么我们通常不应该混合呢,如果你在一个线程中,并且你调用多线程,你在调用它,你在进行分叉。
在一个进程中,对吧,那么线程现在就算是在进程之外了,就像是它独立出来一样。线程必须协调,现在要等,等一下,我有另一个进程在运行,但我在进程中,线程也在运行,这就不一定是好事了。真是让人疑惑,为什么这样就能工作,但从这个角度看,你并不想这样做。
有一个线程管理器和一个进程管理器,它们并不总是能够很好地协同工作。一个进程中你应该能够调用线程,但我们这里做的是相反的,所以我会查一下原因,看看为什么。嗯,我不是这样做的,但总的来说,问题在于你有两个调度机制,它们不一定会很好地协同工作。
一起讨论这个问题,非常好的问题,这是一个很好的背景知识点。所以问题是,再讲一遍,为什么我们在这里使用锁而不是锁守卫?但我们可以在下面使用锁守卫,你完全正确,你的评论是在这里,我们需要在任何作用域结束前解锁。
如果使用锁守卫,它会在作用域外面不能正确释放锁,直到作用域结束后锁才会释放,而这不是我们想要的。但在这里,因为我们接下来的操作是更新缓存,然后要确保它肯定会正确退出作用域,所以我们可以使用锁守卫,没错。
你不必这样做,你可以在 catch 后面加上 lock 和 unlock,这样也完全可以,你并不会因此标记出问题,不,这不是风格问题,只是加了一个风格,它是风格上的选择,但并不是什么不好的做法,lock 和 unlock 完全没问题。追问一下,你有问题吗?没有?好,我就交给你了。这里有一些非常好的问题。
这就是关键部分了,获取到单词之后,就是花了多久的时间。然后我们获取结束时间,好,然后我们执行几个命令或者函数来计算持续时间,单位是秒。这是通过另一个时间函数来计算的,在这种情况下它只是用了秒数。
再加上毫秒或者微秒除以一百万,这样就得到了秒数。就是这样运作的。然后我们将设置负载,在这种情况下,我们基本上是根据是否已经处理完成,以及时间来构造负载,然后我们传入这个字符串,我们需要的。
我们要发送回客户端的字符串必须自己构造,而不是自动生成的。我们得从中创建一个 JSON 字符串。好,一旦构造好这个负载,我们就会使用一个名为 send response 的函数发送它。好的,负载是我们要发送回客户端的字符串,它是一个 JSON 格式的。
看看我们如何构建它。好的,这是一个很长的函数。其他问题?关于多线程、多进程的非常好的问题。其他问题?好的问题,很多问题。是的,问题是:嘿,这个时间是如何工作的?基本上,在这里我们创建了一个非常局部的变量,叫做 start,它恰好是一个结构体类型。
时间值,然后我们用当前时间填充,获取一天中的时间。我们会说,现在是什么时间,然后填充上这个时间。然后无论何时,我们都会保持这个时间本地保存。接下来,在做完所有检查或创建单词等操作后,我们再说,花了这么多时间,再次获取时间。
现在减去这两者,我们就能得到精确的持续时间,这正是你在期中考试中会做的事情。再说一遍,一切都要用时间来衡量。顶部的 sock 流基本上是用于读取数据的。我们会把数据写出来,接下来看看我们是否在其他地方读取它,或者它是否已经准备好,是的,获取字母就是。
我们将从那个地方读取,o string stream 是我们将构建一个字符串。这就是你如何做的,实际上它允许你逐步构建一个字符串。它的工作原理就像 Java 中的字符串构建器,你可能用过。你将看到它是如何工作的。
它是如何工作的,稍后你将看到它是如何工作的。对此还有其他问题吗?好的,让我们继续。
好的,所以词汇的轮询形式,这是我们需要使用从子进程中获得的文件描述符。我们将实际创建所有这些单词并将单词放入一个向量中。这其实很简单,对吧?我们将做这个,这是你之前可能没有见过的,它基本上是在创建一个数据结构。
它允许你为 C++ 创建一个输入流,仅此而已。然后我们从子进程中一次读取一行,直到读取到最后一行。然后我们将它推入向量中,这就是全部内容。那个向量是按引用传递的,所以你不需要返回或做其他操作。好的,发送响应时,请记住。
我们已经构建了有效负载,稍后会看到。好的,发送响应做了以下操作。它实际上是获取命令的反向操作。它是服务器发送数据回来的过程。之前我们做获取时间服务器时,我们并不关心格式是什么,它是我们自己特殊的格式。现在我们希望它是这样构建的,像一个 web 服务器一样。
客户端或网页浏览器可以使用我们的服务器。现在我们实际上是在提升这个问题的复杂度。我们说,“好吧,让我们从浏览器来做这个事情”,所以浏览器期望收到的响应格式如下。它期待我们说“我们正在使用HTTP 1.1”或“1.1”,然后它显示状态是什么,好的,在这种情况下是200。如果你去查阅的话,看看…
在这种情况下,HTTP 200表示,HTTP 200 OK状态响应码表示。
请求已成功,因此你会收到一个200状态码,表示“嘿,你的请求成功了”。有很多事情可能出错,比如服务器超时,这时会有不同的状态码。服务器也可能已经移动,记得我们之前看到的那个状态码是300,它的意思是服务器。
已经移动,你可以查阅所有的状态码,所有的。
状态码就在这里,200表示正常,201意味着有某些内容被创建。还有一个是accepted,基本上是服务器向客户端发送数据,告诉你当前发生了什么,通常你会看到200,这表示一切顺利。
一切顺利,那些执行重定向的状态码就是我们之前看到的状态码,接下来是客户端错误,400表示客户端说“哦,我搞砸了”或者“是你搞砸了”。然后服务器回应:“嘿,你搞砸了,你没有请求正确的东西。”
没有给他们正确的表单,或者是错误的请求、未经授权等等。500系列的错误意味着“我搞砸了”或者“服务器搞砸了”,它们通常出现在这里,内部服务器错误可能是其中之一。
你给了错误的请求,可能是超时之类的,或者是像418这样的状态码。让我们来看一下,有一个叫“我是一个茶壶”的状态码是418,它出现在这里。嗯,最早的一个网页服务器基本上是说办公室里的咖啡是否准备好,听起来很平常,但这就是事情的运作方式,当时就是这样。
创建这些东西,第一个网页服务器就是这个。所以某人编写了这个代码并说,如果你收到418,意味着你是一个茶壶。为什么?嗯,这就是事情的运行方式,这算是一个复活节彩蛋。结果证明,这样做没问题。好吧,我们会返回200,表示一切正常,然后你还会收到一个表示一切顺利的回应。
很好,然后你发送你的小反斜杠,然后你发送一些。头部,这是服务器发送给客户端的头部,好吧,这里的头部是text slash其中一个叫做content type,你在做后端服务器工作时经常会看到它,content type是我返回的数据是什么类型。
在这种情况下,我们发送的是text slash JavaScript,而且他们还说了我们使用的字符集。这是好事,因为全球的计算机知道如何翻译我们发送的内容。text slash JavaScript有点奇怪,我查了一下,我有点困惑。为什么我们要使用这个,为什么我们要自己使用它,所以我就查了一下,像是。
哎呀,JSON应该是什么样的内容类型呢。
如果你查找一下,当然第一个链接就是Stack Overflow,它说。我一直在这里乱搞,这是我使用过的,看到过的,哪个是。正确的,当我们使用的是application slash不,它是text slash JavaScript,所以你。
你可以用这个,但有人说不不不,你应该使用application不,抱歉,你应该。使用application slash JSON,那是我们应该使用的,所以我们可以改变它,然后。基本上你的浏览器如果能弄明白,它就知道大多数这些它会。
弄清楚了,这是我们在尝试的事情,在这种情况下,它说嘿,响应的格式会是什么样的。这就是我们正在做的事情,好吧,接下来我们。说数据有多长,所以内容长度是我要发送多少字节。你为什么需要发送数据,为什么不能直接在结尾加个零,它可能是二进制的。
数据可以是JPEG或其他不是文本的格式,而且你不能随意使用某些特定。字符来标识数据何时结束,你必须发送长度,这样客户端就知道。继续读取那么多数据,好吧,现在它可以一直读取直到结束,这也可能。没问题,但最好知道要发送数据的大小,这样它就知道数据的多少。
它在期待好吧,通常它会一直读取,直到没有剩下的,然后。说哦,所有数据都已经发送完了,记住,在我们的头部后面有换行,所以我们发送。了那个换行符,然后发送整个有效载荷,然后刷新以确保。数据通过网络发送过去,好吧,就这样,有问题吗。
这个,这些函数,这个响应是我们必须发送回去的,这样网站。或者说网页浏览器就知道该期待什么,好吧,接下来我们要做其他几件事。
看一下这里的获取字母功能,基本上就是从客户端获取字母。好的,怎么做呢?记住,如果你说的是 myth 58 /abcde,好吧,你还需要在其中包含端口号,但我们要抓取的是abcde部分。让我们看看我们是怎么做的,好吗?我们将做什么呢?我们获得了套接字。
我们之前已经设置的流就是这里,我们之前设置它的原因是因为我们要读取它,我们将创建一个方法字符串、路径字符串和协议字符串。因为记得有GET方法,然后是路径abcde,再到http 1.1等。好的,所以我们需要做的是方法、路径和协议,我们将读取它们,这就是流的一个好处。
你可以一个接一个地读取它们,噼里啪啦地连续读取,忽略其中的空白符,或者遇到空白符时就进行分割。好了,然后我们需要获取剩下的部分,也就是再来一行,因为我们知道在那之后我们还会有一行。好的,然后我们就使用路径。
是我们唯一需要用来获取路径末尾的部分,从中提取出我们的字母。所以基本上我们利用了你发送到特定路径这一事实,并使用该路径作为字母,这就是我们这么做的方式。好的,就这些。返回值是字符串,如果没有找到任何东西,那么我们要返回什么?
我们将返回整个路径,否则我们将只返回斜杠后的部分。是的,斜杠在这种情况下是暂停符。好的,然后构建负载部分将处理JSON部分。JSON是一种非常特定的格式,它以大括号开始,并以大括号结束,内部包含一些复杂的内容。
实际上,这里幻灯片上的那个我稍微修改了一下,以便我能稍后向你展示。基本上,真实的JSON实际上需要在所有不同部分周围加上引号,所以基本上所有部分都需要加上引号,这里没有反映出来,所以我想我应该更新一下。但是基本上,我们将会说返回。
时间退出时间,无论它是否被缓存,我们会设置为false,然后是我们从状态“OK”中返回的整个字符串向量。
我们说如果我们已经到了这一点,我们准备发送一个有效字符串,这意味着状态是“OK”。所以如果你已经收集了所有数据,一切准备好,你准备发送响应,且一切看起来都正常,你就发送“OK”,并说“是的,我准备好了,这是给你的有效响应。”
就是这样。如果中间有什么问题,比如网络断开,或者其他原因,你将无法收到响应,浏览器会超时,等等。
好的,好问题。那么我们已经准备好进行测试了,我还想展示一些其他有趣的东西。好了,我已经把它都写好了,它会是 Scrabble。Scrabble 单词查找服务器,我在 myth 59 上,哦不,我猜我在那上面,如果可以的话。
我们再做一下 12345,看看这样是否有效。好了,现在我们在 12345 了。所以如果在另一个窗口中我们访问 Talmet myth 59 12345。好的,在这里,注意到它说收到了来自 171.64.15.17 的连接请求。那就是日志,它说这是我们收到请求的来源,你的 IP 地址被发送。
当然,这个请求需要发送到服务器并进行记录。好的,我们来做一下我们的 get 命令,我们想要获取斜杠,然后假设是 Stanford。接着我们会写 HTTP colon slash 1.1,然后我们希望像 host myth 59 I 这样的一些信息。我认为它只是忽略了这些信息,在我们的情况中就是这样,接着就是这样,看看我们得到了什么。
我们得到了回来的所有单词,这也挺酷的,对吧?到目前为止都很好。那么我曾经承诺你们,我们可以从一个网页浏览器中做到这一点,看看它是否有效。
好的,如果我在浏览器中输入 myth 59 colon two three four five slash。有人给我提供一堆。
字母 a b c d e f j 好的,这一切都运作得很好,我们收到了我们的 JSON 响应。好了。
但这就是它的实际情况,这也是我们从网络服务器得到的结果。所以我们创建了一个网络服务器,实际上做到了这一点。好的,现在这有点有趣。顺便说一下,注意它花了多长时间?0.04秒,这就是它执行所有操作所花的时间。如果我们再次执行相同的请求,它花了 1.5 到 10 毫秒,甚至有时是负 5 毫秒,因为它是。
我们的服务器已经为我们缓存了这些单词,它们已经被缓存好了。我们的服务器只是直接提供了这些单词,不需要重新运行、也不需要调用那个子进程。Eva 告诉我们,这样做可能不是一个好主意,所以它就这样工作了。这就是它的工作方式。好的,现在你可能会想,这看起来并不是什么大事。
我访问过的网页是这样做的。所以你可以做的是,你可以,抱歉,是的,没错。那么你可以做的是让我打开,去到,好的,我现在去上面。
在这里,我们再做一次,但是这次是 myth 59 colon one three four five。好的,我这次没有加上那个。我在 JavaScript 中你实际上可以从 JavaScript 请求网站。好的,那我们就来做一下。你能做的事情,关于你的浏览器其实有一个很酷的功能,我也正是因此使用它。
因为我恰好知道如何使用这些工具,所以我决定使用 Chrome,看看这个。我们可以关闭这个,那么看看,这个可以变大吗?是的,我可以。好的,我们将实际使用一个叫做 fetch 的命令来做这个,所以如果我输入 fetch,它实际上有从网站获取数据的能力。好,如果我输入 HTTP://myth59。
dot Stanford dot edu colon 一二三四五,因为我们是 slash,让我们做一下呃,leland,假设我们有那些字母,好,然后方法,你采取这个方法。在这种情况下,我拼错了吗?为什么?哦,天哪,像这样,抱歉,抱歉,我现在感觉很糟糕。好吧,我在这里不会工作太久了,别告诉别人,拜托,好,继续。
我们将在这里使用 get 方法,我确实需要它,非常感谢,语法高亮很不错。好了,我们在这种情况下会使用 get,然后我们实际上会做一件叫做 then 的事情,这是请求的部分,告诉我们怎么做这个,你不需要了解这些细节,我只是展示给你看它是如何工作的,我们将返回数据。
dot JSON 因为我们知道它是 JSON,这就是我们所期望的。然后我们再做一次。然后在这种情况下,这是那个实际上做出响应的部分,好,我们可以做的是,我们可以通过控制台输出打印它:console dot log 和原则性日志响应。像这样,然后我们还可以实际捕捉错误,如果有错误的话,应该没有。
如果出错了,可能会有谁知道的错误,我们可以看看控制台日志,看看错误是什么。好的,如果一切顺利,我们来看看,好的,给了我们所花的时间,给了我们这些内容,就这样。所以我们刚刚写了一个小的 JavaScript 函数。再做一次,你自己会想,那看起来不像我见过的任何网页。
好的,但你可以做的是,我想去哪里?我觉得我把它放在了这里,是的,我放在那里了。顺便说一句,你们可以从你们的浏览器去做这个,我不认为他们在过去的五分钟内尝试过,不过那是什么?哦,是的,人们在做这个。谢谢,我猜大家都在做,你们可以做这个,我猜你们正在做。
类别 CS 110 WWW 好,看看它叫做 Scrabble word finder dot HTML,我写了一个小的 HTML 程序来实现这个,而我现在要更改它是因为我硬编码了。你不应该这么做,但我硬编码了错误的内容,我不知道它会在那里。123 或 5 在这里,好,基本上它是在设置一个小网页。
所以如果你做过任何网页工作,你会看到我只是设置了网页的标题和几个放置细节的地方,等等。如果一切顺利,我们应该能把这个放大一点,哎呀,我们应该能访问 web dot Stanford dot edu slash class slash CS 110 slash Scrabble word finder word finder。
.HTML文件,接下来我们可以继续,好的,我们有了字母,如果我们输入 L E L A N D 正确的话。没错吧,我们应该会得到返回,这就是我们的网页。所以我们创建了一个,顺便说一下,使用了 fetch 和其他 JavaScript 功能,说“哦,我知道这些是什么”,它将其转化为我们依然看起来很笨的网页,但它现在是可用的。
它就像任何你可能关心的其他网页一样正常工作,好的,那么里面还有什么奇怪的词吗?嗯,你可以从 Leland 获取 Dorns 吗?那是什么?不,你不能。你说得对,等等,怎么回事,刚才怎么不对了?再说一遍。哦,我可能说错了,不,我没,我应该是做了。哦,我的天,我做错了,你说得对。
好的,我会修复的,我会修复的,哇,好提醒,这没道理,好的,嗯,某种原因我以为应该能搞定,为什么没做到呢,反正我没做成。哦,找到了,就在那里,现在我们可以修复它,因为我们可以,这就是字母,我知道。我把它拿出来了,只是忘了做那个。让我们看看加号,让我们做这个斜杠加号。
字母,好了,实时改变网站,看看这个是否能更好地工作。好的,好的。好的,我不知道这是件好事,当你犯了个愚蠢的错误后还得到掌声。我要修复它,可能这是件好事。好的,你还有什么问题吗?今天有很多代码需要查看,但都在里面,里面有很多东西。
多年来,我会了解多进程的问题,提了个好问题。然后我们会做这个,好的,好的,好的,好的,好的。所以如果你只输入 myth 59,网络服务器就会是另一台完全不同的计算机。实际的斯坦福网络服务器并不在我们创建的机器上。我们创建了自己的小服务器,这部分很酷吧?对,但我们创建了自己的小服务器。
斯坦福大学使用的网络服务器是一个专业构建的开源服务器,但它是一个经过专业构建的服务器。它为你访问的所有常规斯坦福网页提供服务,比如你访问的 web.stanford.edu,这就是我放置这个页面的地方。但记住,这个页面是指向 myth 59 的,我做了一个硬编码,指向 myth 59,好的,这个是错的,这个标签页是我的计算机直接与 myth 59 对话的方式。
记得我前几天提到的 DNS 服务器吗?就是那个。域名服务器,你让它去访问 myth 59,你的计算机通过浏览器基本上就会查找出那个 IP 地址来进行连接,然后直接到达那里。
如果我现在在斯坦福的网站上,我就在访问斯坦福网站,浏览器正在与斯坦福网站交互,斯坦福网站通过 JavaScript 和它与我互动,然后它告诉我需要去访问 myth 59。所以基本上我正在从 web.stanford.edu 加载这个网页。
这是斯坦福大学的主网站,然后我自己直接在我的电脑上与 myth 59 进行通信。为了获取实际的单词,你也许可以通过另一种方式操作,让这个网站或我的电脑与斯坦福网站进行通信,之后斯坦福网站再与 myth 59 进行通信,这样就多了一个间接层。但是在这个例子中,我们跳过了这个间接层。
那里发生了很多事情。是的,端口关闭就在里面发生了,确实发生了。
当客户端套接字超出作用域时,它基本上会回到我们创建它的地方。我认为我们就是在这里创建它的,所以它会在这里结束时超出作用域。
这个函数,然后它关闭了它。这就是我们案例中,服务器关闭与客户端的连接的地方。好的,问题很棒。好吧,正如我所说,很高兴为大家解答。我……抱歉,之后随时可以停下来或者课后过来提问,期待见到大家。
P17:第16讲 网络系统调用 - main - BV1ED4y1R7RJ
屏幕和转播,你错过了大约30秒。好的,问题是:“嘿,我们可以在线程中使用多处理吗?”
答案是,你必须小心一点。我能找到的最好答案是那个几乎可以算作神圣的答案,它基本上说,“看,你可以在多线程程序中使用fork。”那么这到底是什么意思呢?这意味着,当你使用fork时,两个副本会被创建,两个进程都有与之关联的线程。所以,你得小心点,知道吗?
你得有办法处理这个问题。但从能够做到这一点的角度来看,最重要的是,在你进行fork之后,执行exec BP之前,不要做任何内存分配、new或delete操作。这是我找到的最好的答案。你可以做到这一点,只是不要做任何会以某种方式破坏内存的操作,导致问题发生。
摆弄调度器之类的东西。这是我找到的最好的答案。所以答案是,如果你想在多线程中使用多处理,你是可以的。你只需要小心一点。显然,测试总是非常重要的。好的,这是个好问题。
这是我找到的答案。如果你有兴趣,可以自己再深入研究一下。但是是可以的,只是得小心。好了,接下来我们继续。
网络系统调用、库函数等等。就像我几分钟前说的那样,我觉得这有点像是CS107的回顾,因为与C语言相关的一些不规范操作会传播到这些函数中,这些函数用于获取主机名、解析它们以及使用这些底层函数。
以一种方式来做,使得你可以支持IP版本4、IP版本6以及任何你想要的其他版本。结果证明,套接字编程——我们在这里讨论的,就是通过套接字或者基本上是端口,两个计算机相互通信的方式。套接字,实际上就是一个文件描述符,但它是一个非常特殊的文件描述符。
它仍然会在打开的文件表中有一个条目。信不信由你,它仍然会在特定进程的文件描述符中有一个条目,但它允许你在两个进程或两台计算机之间通过网络进行双向通信。明白了吗?所以它就像一个文件描述符,但它还有更多的内容。
你不一定需要知道它的其他细节,除了一个事实,即尽管它是一个文件描述符,实际上你不能像平常那样进行读写操作。明白了吗?
你使用像accept这样的函数,还会使用其他函数。我猜你可以在文件描述符上使用write,但不能。还有一些其他的细微差别,你不能只是说,哦,它就像每一个其他的文件。它有点像文件,但并不完全是。好了,让我们来谈谈具体的细节。
因为我们是人类,我们喜欢使用像www.facebook.com这样的东西,而不是31.13.75.17,对吧?
你不想记住数字。我们在本课程的很多地方都谈过这一点。所以,有一些函数可以根据名称获取数字。如果你知道像www.facebook.com这样的名称,它是很容易记住的,你可以使用一个名为gethostbyname的函数。你也可以使用另一个gethostbyaddress,它们两个从技术上来说已经不推荐使用了。
换句话说,应该有其他你应该使用的东西。不过,你的书中谈到这些函数,它们仍然被足够多地使用,以至于你应该习惯使用它们。特别是,我不太确定实际的新函数是什么。我并不说。比起看到这些,我更常看到那个,我也忘了实际的新函数是什么。
你现在应该使用的是这个。所以,反正我们将会讨论这两个。它们所接收的参数是gethostbyname。它接受一个像www.facebook.com这样的名称。并返回一个名为host的结构体,一个叫做hostint的结构体。我们稍后会看到这个特别的结构体。
它会用你需要的关于IP地址等的信息来填充它。你也可以在这里传入一个地址。事实证明,我不认为这实际上是一个char*。虽然它看起来像一个char*。你必须把它转换成一个int,相信我,或者在这个情况下是其他类型。之所以这么做,是因为你需要,实际上这个可能不是。这个。
我甚至不认为我们会在例子中使用这个。这个可能不是。也有些是你必须这么做的。再说一次。这是因为C语言的历史悠久,还有一些我们在1977年做的事情,那时候甚至没有void*指针。所以,一切都变成了char*指针,尽管你可以这么做。在这种情况下。
我猜在这种情况下,嗯,这实际上应该是,我认为,是一个数字。一个指向数字的指针。然后你将会说它是多少字节,接着还有另一个变量,它描述了连接的类型,是否是一个IP地址,或者是IPv6等等。好的,所以有很多关于这方面的细节是你在使用这个函数时需要了解的。好的。
一个struct hostint包含以下内容,嗯,我们会稍后看看它是什么。让我们来看看,gethostname。是的,来了。这个是一个名为i-n-a-d-d-r的结构体,它恰好是一个int。这是我见过最奇怪的结构体了。它只有一个值,就是一个int。好吧。我们稍后会更多地了解它。
但我们主要关注的是get host by name。所以,事实证明。好的,明白。那么,这是什么?这是什么?哎呀,哦,不,这不是我想做的。等一下。我现在没有正确设置我的平板,等一下。我回到这里,我们使用光标。好了。明白了。所以。
这是结构体host int的样子。首先,它内部包含了一个名为struct i-n-a-d_ad-dr的结构体,在这个例子中是一个无符号整数s-a-d-d-r。正如我所说,这是一个奇怪的结构体,因为它只包含一个值。你通常不会去管只有一个值的结构体,它想要的是指针。
也许这是为了,嘿,他们当时想,也许会以其他方式使用它。而他们可能有一些其他的想法,他们也许有足够的前瞻性,但它从未实现过。而且它也从未改变过。所以,这就是里面的全部内容。好的。这是一个无符号整数。然后,结构体host int有一个普通的旧指针星号,最后是一个普通的旧指针星号。
这是名称。好的。那是正式的名称。事实证明,我会给你看一个例子。正式名称可能与实际输入的名称不同。很多时候,它们最终会指向同一个IP地址,但名称可能稍微不同。然后你会看到一堆别名。好的。
别名是其他字符串,也指向相同的IP地址。好的。所以有一个官方的名称,还有其他指向相同地址的别名。你很少看到别名字段被填充。我猜也许真的没有人那么关心它。但你很少会看到它被填充。但它确实是一个指针星号星号。
这意味着它基本上就像我们通常使用的其他指针星号。它是一个。事实证明,是一个没有终止符的指向字符串的指针列表。所以字符串,字符串,字符串,字符串,字符串,然后最后一个是,没错,你知道你已经到了末尾。这就是那个。好的。地址类型将根据你关心的IP地址类型有所不同。
所以在这个例子中,我们可能主要使用AFI net,这意味着Internet中的IPV4地址。好的。这个其实是挺重要的。我们会看到为什么,当我们进入一些奇怪的多态性细节时,它们被硬塞进了C语言中。正如我们所见。好的。然后这里是一个指针星号星号,每个地址列表。
这其实是另一个非指针星号星号。它基本上是一个void指针星号星号。应该是void指针星号星号。但当他们构建这个函数时,要么他们当时没有void指针星号星号,要么由于某些原因不想使用它。但是当你使用它时,应该将其转换为适当的类型。
你将通过分析这个来了解。我们再看一下它是如何工作的。这就像是非常低级的107类的东西。问题是,这些东西应该是为了做一个durant(译注:durant可能是个打字错误或专有名词)吗?
这些东西是杜兰特做的吗?可能吧。应该是同一批人。对,你看。当你在1970年代设计某个东西时,它跟今天的设计方式有点不同。也许吧。但没错,可能是同一批人做的。或者是有相同思维方式的人。好吧。
无论如何,过一会我们就会知道为什么这会变得有趣。好吧。好的。S-A-D-D-R 字段就是我们所说的点四表示法。好吧。基本上,它是四个字节。一个,二,三,四。好吧,四个字节。这就是你们希望曾经见过的那种类似的IP地址形式。你有它。嗯,这就是IP地址的样子。一个IP版本4地址就长这样。171。
64,64,136。然后这些IP地址会被分成四段,因为比记住那个长长的数字要容易一些。首先,数字一。然后第二,每个小字节表示不同的意义。171。我相信斯坦福的意思是第一个字节,所有斯坦福地址都是这样。然后,其他的字节逐渐指向你正在查看的实际机器或路由器。
所以它是这么工作的。意思是,因为这些只是字节,IP版本4地址就是四个字节,或者32位,它是171,64,64,136。那么,这些字节的顺序会是什么?换句话说,当你有……这也是我刚才提到的107的部分。当你有一个四字节的数字时。
我们可以使用两种不同的字节顺序。记得它们是什么吗?小端字节序和大端字节序,对吧?我们的机器通常是小端字节序的机器。意思是数字的低位字节实际上会先存储在内存中。因此,136 实际上会是内存中第一个字节,然后是64,接着是另一个64。
然后 171 将是最后存储的内容。我们本来可以按完全相反的顺序来做。你可以用大端字节序来做。对于那些学习了计算机科学的同学们,嗯,107E。我相信树莓派一般使用的是大端字节序。虽然也许它们……我觉得我实际上可以在两者之间切换,结果是这样。
但重点是,在那种情况下它会是不同的顺序。嗯,由于标准化,你的计算机使用的是小端字节序,你需要与另一台可能使用大端字节序的计算机通信。当你实际上通过网络发送数据时,你必须决定字节的顺序。
好吗?所以我们必须这么做的原因是为了让每个人都能一起交流。因此,如果你的计算机上的字节序不正确,在你准备发送数据时,最好进行转换。我们稍后会看到实际操作。我现在只是为你们做个铺垫,思考一下。稍后我会展示给你们看。好吧?我们来看看。对,接着。
对于非IP版本4的情况,我们也可以,嗯,让我看看,稍等,看看这个。看看这个。对于非IP v4的情况,我们有不同的信息在所有这些内容中。它包含了其他信息。H-atter类型将会是一个不同的实际数字。如果你有128位,长度也会不同。你需要报告这个。
列表中也可以包含不同类型的信息。所以你得小心一点,稍微小心一点。好吧?好了。那么,为什么它不让我再做一次呢?好了。好了。目前为止,关于这个有什么问题吗?
我们正在接近实际的代码部分。
事实上,让我们实际写些代码,然后我会在过程中向你展示。这个字体太小了。好吧。那么,我们开始。好吧。那么。我们将继续查看代码。然后我们将继续查看代码。
然后我们将继续查看代码。然后我们将继续查看代码。然后我们将继续查看代码。然后我们将继续查看代码。然后我们将继续查看代码。然后我们将继续查看代码。好了。所以,我们必须先声明一个struct host end。这是一个由操作系统维护的静态构建变量。
如果你在代码中多次使用这个,最好创建一个副本,如果你想跟踪不同的内容。保持一份副本,因为它不是线程安全的,或者其他的原因,因为你只是获取了指向数据的实际指针,而这个数据存活在你调用的函数内部。好了。这不是最好的做法。
但这就是静态变量的工作方式,也就是它是如何工作的。get host by name是一个C函数。所以我们需要转换,如果我们有一个C++字符串,我们只需将其转换为C字符串。好了。然后调用get host by name会填充这个。好了。所以,不,意味着我们无法解析这个名称。结果证明,这可能是多种原因造成的。
可能是你的DNS服务器出现故障,可能是在你的电脑、网络、路由器或你连接的其他地方,或者可能是其他原因。所以如果你输入了一个,恰好它不存在,这并不一定意味着它不存在,而是意味着你的程序无法解析它。
这是网络的问题。有时候,其他你无法依赖的东西会出现故障,而你对此无能为力。好了。然后,H E是我们之前谈到的那个名称,接着所有这些IP地址,我们会逐一处理,首先说,好的,让我们将H地址列表转换成它真正的样子,我们知道它实际上是一个IN地址双指针。为什么?因为我们知道。
我们正在请求IP版本四地址。换句话说,我们调用get host by name,意味着我们期待的是IPV4。如果你想要IPV6,有一个不同的函数可以调用。
好的。我一会儿给你看。不过,反正,你通过循环直到得到IP地址。得到后就不再继续。好的。那我们实际上是如何获取IP地址的呢?嗯,记住,它现在只是一个数字,仅仅是一个四字节的数字。所以我们做的是调用另一个叫做I met network to printable的函数,本质上就是。
NTOP代表什么,它的意思是它会说,嘿,你在使用哪种类型的IP地址?在这种情况下,是IP版本四?好。它说,给我实际指向地址本身的指针。好。然后给我一个字符串来填充它。这就是我们在上面所做的。
我们知道I met underscore ADDR,Sterling是IP地址的最大长度。事实证明是这样。所以我们知道那是可以的。然后,它的长度也是传入的,以避免溢出缓冲区。一旦你做到这一点,它就会把那个数字转换成一个漂亮的172点什么,什么,什么。好啦,关于这个有什么问题吗?嗯。是的。A of I net是IP版本四。是的。
A of I net是IP版本四。我们快速看一下。Resolve host name six, C C。是的。
这是用于IP版本六的那个。
A of I net six,等等。我们还会调用一个不同的函数,叫做get host by name two。
他们在命名这些时并不是特别聪明,我猜。你认为它会是……你觉得它会是像get host by name six之类的名字。但其实是……你可以传入这个。这个你传入的是实际的类型。所以我相信我们本来也可以使用这个来处理IPV6版本。
而且它也会正常工作。因为我们本来会传入IP版本,抱歉,是IP版本四。它们也写了两个两个吗?它们也写了两个两个吗?
他们可能也写了两个两个。是的。是的。如果你曾经构建过一个大型系统,你会很快意识到,你有些必须做出的决定,你从心底讨厌这些决定。但你必须做这些决定,因为这就是事情的进行方式。现在,如果他们必须这样做,可能不会。
但他们可能是某个委员会决定了这些,事情就是这样发展的。
无论如何,让我们看看这个程序如何运行。好的。我觉得它已经做好了。让我们看一下。Resolve host name。是的。好的。如果我们输入一个主机名,试试,试试www.stanfer.edu。好吧。所以它显示了那是实际的主机名,就是官方的主机名。现在,幸好我们不需要每次都输入这个才能访问斯坦福。
或者是给Stanford,或者给你。为什么它会那么长?嗯。它实际上告诉你,Stanford依赖于亚马逊的AWS服务,嗯。如果你想想,其实这也不是个糟糕的选择。如果不必要,为什么要全都自己做呢,对吧?
只依赖一个拥有数十亿台服务器的巨头公司。它应该会保持较高的在线频率等等。我相信如果我们只输入Stanford.edu,我们就能看到,没错,看到的是官方名称,就是Stanford.edu,它恰好指向了一个稍微不同的网页地址。我的猜测是,如果你输入www。
stanford.edu,你的浏览器可能,试试这个。
我要试试别的。Ping。Stanford.edu,好吧,它显示171,67,215,200。
好吧,就是这样。如果我们ping一下www,看看会发生什么。没有。它给了你另一个。所以它们略有不同。它们最终可能会指向同一个地方,但没错,确实有些不同,www实际上在这种情况下稍微有些差别。
如果你输入这两个,你会最终到达同一个地方。某个地方,它们会重新路由到同一个网页。让我们再试几个。试试google.com。Google.com有一个IP地址,我相信它实际上是基于你的位置的。它好像知道谁在请求,然后返回一个基于你位置的IP地址。
比如本地的,更接近你的位置。结果是,看看,facebook.com。嗯,还是一样的。给你。哦,其他一些,看看,www.facebook.com。给你。那个,www.stanford1,实际上我认为它有两个,那里有。它确实有两个不同的IP地址关联。我想那些是亚马逊的地址。
然后还有一个Jerry喜欢使用的。
好吧,Cupid.com。我不知道Jerry是否喜欢用那个,但Jerry确实给我看过。它有很多。我不确定为什么。并不是因为像数十亿人使用它,而Google只有一些人偶尔使用。所以我不知道为什么。这里有一些。似乎有些问题,导致它们的主机服务器说,“嘿,这些都是你的IP”。
地址出现后它会显示在这个列表中,然后,它有点像黑魔法。现在。github.com,让我试试。github.com。只有一个。我要不要试试www.github.com?一样。所以它是一样的。那个正好是。官方名称是同一个,指向的是同一个。所以,没错。
这里绝对有一些黑魔法在发生。我知道,你知道。我不清楚所有的细节。所以让我们做一件事。
我想给你展示一件事。gdb。我告诉你它不像107。gdb。解析。主机名。
好的。发布时中断。IP 地址信息。我想现在可以了。好了。
再运行一次。我们做 Stanford.edu。其实,我们做吧。我们做吧。我们做吧。好了。
让我们做这个,做这个,好,保留一个只是看看。好了,qubit.com。
好的。所以如果我们进入代码并获取主机名,好,如果我们打印出 HE,它就是。
一个指针。好了。
如果我们像那样打印出主机 HE,它会告诉你所有的详细信息。好了。所以它在这种情况下告诉你名字,那只是指向名字的指针。然后别名在里面,结果显示没有别名。我相信如果我们做。我们看看怎么做。我们要做一个星号。HE,我们做星号。HE,箭头。
HE,别名是。
看看是否有效。是的。所以第一个那里没有别名,结果显示没有。然后我们看看。H 地址类型恰好是二。在这种情况下,那意味着 IP 版本四。然后长度是四个字节。所以我们知道地址的长度。然后地址列表是。记得我们说它是一个 char star。
但是如果我们只是说,尝试像这样做这个,ADDR 列表,那会有点乱吧,对吧?
因为我们并不真正知道它是什么。我想你可能得像这样做。看看这是否有效。让我们看看我们是否能做到这一点。我们将不得不进行强制类型转换。我知道。它变得很丑,对吧?我们得把它强制转换成一个——它是什么?是一个 struct,i-n-a-d-d-r。
Struct,i-n-a-d-d-r star,也许?
不行。[听不清],它会是——不行,哦不。我们看看。我早些时候做过这个。我之前弄明白了。
好的。就是那个。也许我们需要做——。
我们看看。我们直接去——[听不清],等一下。只有这个——像这个吗?
如果我做了——那将会是一样的事情。
如果我——如果我做了——等一下。好了。所以是那个。然后如果我们想要再——。
我们看看。所以让我们打印出每一个那是什么。
P0x84——132。那是其中之一吗?好了,保持一次吗?
它是。132。
然后让我们看看。84d0——。
208。对了。其他的呢?是41和198。所以我们应该得到41应该是29。
然后C6应该是198。
好的。注意它的顺序。它的顺序是错的。是反过来的。好吧。我们之前调用的那个i net,_ntop函数实际上知道它的顺序是错的——不,实际上是小端格式。然后给我们返回正确的字符串。好的。但我们还没有真正转换过来——。
还没有把数据发送到网络上。对。[听不清],是的。它知道一旦它到达那个数字,它就会变成计算机的表示形式。所以在这个情况下是小端。当我们实际将它发送到网络上时,我们必须将它转变为大端数值。你必须这么做,大家才能知道如何处理——。
[听不清],为什么是大端而不是小端?有人做了这个决定。我是说。你们知道小端和大端的来历吗?你们需要了解更多。看。这 supposedly 是斯坦福大学的一个文理学院大学。这是我所理解的。但它来源于《格列佛游记》。所以在《格列佛游记》里,有…
小端和大端的故事,关于他们是如何打破鸡蛋的,无论是从大端还是从小端打破,或者是硬煮蛋或软煮蛋,还是小端。而他们为此大打出手。所以无论是谁创造了这个,都会说,哦,看起来小端在那里。哦,我记得这个。哦。
这也会引起一场大争论。这就是为什么我们会问问题,比如,为什么?
这有什么关系,等等。所以这实际上是一个完美的类比,事实证明它的确来源于此。好的,既然我们在这儿,我们来看一下主机名6。cc。
这是——哎呀——。
然后解析主机名6。cc。我们来看一下这个。当我们稍微看一下这个时。如果我们想要处理IP版本6地址,我们实际上可以查看它们。我们必须实际使用一个获取主机名的命令。我们得到了主机名2并告诉它。我们在找I和6地址。然后我们需要检查并确保。
那是I和6地址,即IP版本6地址。然后这里是同样的事情。我们可以使用这个函数。它知道如何转换IP版本6。
让我们看看当我们运行它时。我们实际在DDB上做一下,看看有什么不同。
解析主机名6。
让我们先运行它,看看主机名,google.com。这是google.com的地址。现在,这里有128位。这是一个相当大的数,128除以8是16。那就意味着16。这里不是16,有一个,两个,三个,四个,五个,六个,七个,基本上是七,八,因为它大约需要9、10位。而且里面还有一些额外的双冒号。
这是他们做出的一个决定,试图让IP版本6的数字变得更小。如果有一串零,你其实可以放两个冒号,然后就看着剩下的所有零。我还是觉得人类几乎不可能搞明白所有这些意味着什么。虽然不是不可能,但就是像这样。
你得动动脑筋,想一想零在哪里,它们是如何适配的?
对我来说,这可能是一个没有灵感的决定。但是实际上有些地方,记住一个IP版本6地址时,会有128位可用,也就是说,有2的128次方个不同的地址。实际上,让我去这里看看。我把数字找到了。就在这儿。
现在你可以拥有这么多不同的IP地址。这个数字比宇宙中的质子或原子还要大,我相信。
所以,如果你愿意,你就能为宇宙中的每一个原子分配一个IP版本6地址。我怀疑我们至少在我们的有生之年是不会用完这些地址的。但谁知道呢,我想。反正他们是这么看的。如果你是一个足够大的公司,并且你有足够的云计算资源,你实际上可以申请一个特定的IP版本6地址。那你为什么关心这个呢?Facebook.com。
看看Facebook的IP版本6地址。
它里面确实写了Facebook,这就像,哦,真聪明,真棒。而且我还不明白为什么他们不直接写两个双冒号。我依然不理解这些东西是如何工作的。
这些东西是怎么被算出来的。但也许有一天他们会直接使用花哨的IP地址。
你可以为你的手机或其他设备获取一个地址。
虽然你可能不在乎,但我猜,Facebook的人会在乎这些事。让我们继续运行它。我们照以前一样在相同的函数里中断。发布IP地址信息。
然后——哦,不。它不在那儿吗?等一下。然后。然后。然后解析主机名6。
他们说这被称作——让我们看看。
发布——哦,当然。发布 IPV6。信息。让我来做一下。好了,打断这个,然后运行它。是的,我们要启动它,再试试 Google。注意,我没有做 Stanford。等一下,让我给你看一下 Stanford。是的。Stanford.edu。继续。我不知道它是否已经有了。真是太遗憾了。不过,反正就是这么回事。
我们再看看。主机名。我们再试一次。Google.com。好吧。Google.com。好。然后你执行获取主机名,然后是这里,然后是这里。然后我们必须——再说一遍,我们得把它转换为结构体 i 和地址 6。然后我们必须真正执行它。所以如果我们再输入一次——让我们看看,pi_print_out_he。
它会告诉我们,它的字节数长度为 16,实际的地址列表等等。所以这里有一些不同的地方,你得了解。是的,它必须这样。当你把它转换为结构体 i 和 6 地址指针指针时,是因为你能操作地址列表。嗯,那是对的。没错。好问题。
看一下这个结构体——。
它去哪了?在那里。好吧。所以这是强制转换为 char 指针指针。
它没有强制转换为它真正的类型。为什么要强制转换为它真正的类型?因为我们希望它足够通用,能兼容 IP 版本 4、IP 版本 6,甚至你想要的其他任何版本。实际上还有另一个 IP。我想它只是有一个——我想是 i——这里是什么?
它不是 i 和地址。
它是 i 和 addr_unix。它是它自己的类型,意味着你可以像内部使用计算机的套接字一样使用它。这是另一种做法。所以再说一遍,这个服务器功能很强大。但它足够强大,里面有些奇怪的细节,你得去了解。
但是这让你明白了吗?为什么你必须这么做?先生——,[无法听清]。
为什么你要做地址列表加加?是的,你可以做地址列表加加,因为现在它知道是这种强类型,对吧?
一旦你这样做了,如果你没有这么做,它会尝试一次处理一个字符,然后就会乱掉。再次提到 107 的内容。是的。[无法听清]。
我不知道发生了什么,版本 5 怎么了。嗯,这是个好问题。
这可能是致命的。但是,IPV5 怎么了?IP——为什么没有 IP?
是的,这可能就像 Windows 9。让我们看看。不存在,所以没有 IP 版本 5。让我们看看。它是故意跳过的,以避免混淆。曾经有一个实验性协议叫做互联网流协议,在 1190 中定义。因此,它被分配了 IP 版本 5。但我们现在不再使用它。所以他们说,哦,让我们跳过它,使用 6。
跟那四个字节或六个字节什么的没有关系。如果你曾经这么想过,那就不对了。就这样。好的,行。知道这些东西确实有点疯狂。
总之,我们已经运行了很多这样的操作。现在,让我们来谈谈插座本身。当你创建一个插座时——记得我们做过吗?我们有 accept 命令,也有创建插座的命令等等。我们将稍微详细地查看它们,看看它们到底是如何构建的。我们将查看创建客户端插座。
以及创建服务器插座。记住,它们是两件完全不同的事情。当你创建客户端插座时,你要做的事情是,你要去连接到另一台计算机,连接到那台计算机的 IP 地址和端口。所以你实际上是要去连接另一台计算机。
那时你就是在创建客户端插座。你在创建客户端插座时。如果你是主机,如果你要做的是创建服务器插座,你所需要做的就是获取你本地的端口号,并试着分配给自己。我们叫这个绑定。我们将看到它是如何工作的。所以你实际上并不去连接任何人。
当你在做服务器时。你基本上是在说,嘿,我在这里。请连接我,或者其他计算机连接我。这是一个很大的区别。现在,当然,我们这里有不同类型的插座。这里又是它变得有点复杂的地方。我们有一个通用插座。
通用插座是 struct-socket-dress。它的第一个元素——它的第一个成员是一个无符号短整型,叫做 SA family。它是一个两字节的值,叫做 SA family。它会告诉我们——这将表示它是什么协议。然后我们有一个非常奇怪的 SA data,是 14 字节。但就这些了。
它没有再说明其他内容,除了说那里有 14 字节。然后你可能会想,哦,好的,也许这 14 字节将来会有用。我其实看不出它有什么用处。接下来是 struct-socket-dress-in,用于互联网。这是 IP 版本的表示方式。它的前两个字节是家庭,互联网插座的互联网家庭。
然后它有一个与之关联的端口号。接着它有一个我们之前看到的 struct-i 地址,叫做 ADDR,表示实际的四字节互联网地址。记住,它是奇怪的。它是四字节而不是——它实际上是一个结构体,这挺奇怪的。
然后它有八个字节的零值。这些零值被定义为零。事实证明,它们完全被忽略,尽管大多数人实际上会将它们设置为零,因为名字里有零。所以他们认为应该把它设为零。实际上,这可能完全不重要。它们完全被忽略。那么这是什么?
我不知道。我们拭目以待。首先,先数一下字节数。看看这是否真的有作用。一个短整型占多少字节?两个。另一个短整型再占两个。总共四个字节。无符号整数(INRS)占多少字节?四个。所以总共有八个字节。然后还有八个字节。那就是十六个字节。接着在这里,我们加上两个字节,再加上十四个字节。
所以那是 16。好了,听起来好像有些道理。我们来看一下互联网版本。第六版。它同样也将前两个字节作为家庭类型。然后它的前两个字节是端口。记住,端口只有两个字节。不管你是使用 IPv6 还是 IPv4。接着它包含一个 IN6 地址结构。
那将会是签名的 sin 6 地址。128 字节有多大?128 位转换成字节是多少?16个字节。16 加 4 等于 20,再加 2 等于 22,再加 2 等于 24。再加上另外 4 个字节是 28。这个是 16 吗?不是。
我不知道为什么会是这样。事实证明,这其实并不重要。就算有一个 14 字节的部分在那里,似乎也没关系。我认为必须有些东西在那里,以使编译能够正常工作。这是我能理解的全部了。我查阅的各种资料也说,这似乎并不重要。所以无所谓。
[听不清],什么?[听不清],哦,对不起。流信息和范围 ID。我甚至不确定它们是什么。它们是专门针对 IPv6 的一些东西。这部分的好处在于它足够通用,所以你可以在其中添加额外的内容。流信息可能与实际的后端有关。
服务器和客户端之间来回交换。也许它比其他某些方式更高效。于是他们想把它加进去。范围 ID 可能是其他的东西。我只是不知道。但是这超出了本课程的范围。没有恶意的意思。只是一些与之相关的附加信息。好了。
这就是一个 SOC 地址的样子。我们有这个通用的,似乎对我们没有太大帮助的地址,除了它包含了这个家庭类型。然后我们还有这些其他的地址,包含了家庭类型以及前两个字节,接着还有额外的信息。这才是关键的部分。实际上,不止两个字节。
还有其他的 Unix 类型,以及其他的类型。Socket 是一种非常通用的结构类型。好了,就是这些。不管怎样,正如我所说,版本 6.1 中包含了其他一些内容。你很少会声明这种类型的变量。好吧,这有点像 Java 中的抽象类或者类似的东西。
这里有一个定义,你实际上永远不会使用它。它只是存在,为了让其他从中继承的东西可以被使用。好的。所以你几乎不会实际做一个SOC ADDR。你会做你想要的那个,针对特定插槽你正试图创建的。好的。实际上 Linux 会为两者做一种设置。
因为他们想要使其通用。好的。你必须做的事情——我们在实际写代码时会看到——是你必须进行一些与这些相关的类型转换,才能获取正确的值。现在,如果你还记得 CS107 中的内容,你就会知道,你必须做这些类型转换,特别是在编写通用函数时。
有时候,比如你正在写一个函数,它有两个 void* 指针。在写函数时你知道类型,但编译器没有任何概念,因为它期望的是 void* 指针。你知道的,因为你是编写函数的人,哦,实际上这些是 char** 指针。
或类似的东西,在函数内部,你实际上会进行类型转换。当我们实际编写这两个函数时,它们将会非常相似。好的。我们将写两个函数,一个是创建客户端插槽。
另一个是创建服务器插槽。客户端插槽稍微简单一点,尽管看起来似乎有更多的事情要做。实际上你是试图联系另一台计算机,并与其连接。但实际上并没有那么多事情需要做。你知道你需要知道端口号和地址。
然后你必须设置插槽来执行它。这就是我们要做的事情。我们要确认,我们实际上能够与 IP 地址进行通信。嗯,确认该主机的 IP 地址存在。我们将尝试连接某个主机,我们需要这个 IP 地址。
让我们检查它是否存在。然后我们将分配一个新的描述符。这与常规的文件描述符非常相似,但又非常不同。它就像文件描述符一样,存在于文件中。
描述符表,它有一个打开的文件描述符等等。但你并不是用它的方式与其他描述符一样使用它。它是双向的。它是一种双向通信,而我们其他大多数描述符是单向的。你会使用一个叫做 socket 的系统调用,来实际配置一个插槽描述符。
当你使用 socket 时,它实际上并不会与其他计算机通信。它只是设置插槽,以便你可以用正确的细节填充它。然后使用它。接着,如果我们在做 IPv4,我们必须创建一个 socket 地址实例,命名为 IN,然后它将主机和端口号打包起来。
它打包了所有我们要连接的细节。我们会做这个。然后现在你已经设置好了这个套接字。现在你可以真正去连接另一个计算机了。我们将要做的就是这个。如果一切顺利,你就将那个套接字返回给任何需要的人。
程序或者其他请求它的功能。明白吗?问题是。这就结束了吗?从概念上讲,这和我们之前设置服务器和客户端的方式有何不同?是的,这是个好问题。问题是,从概念上讲,这和设置服务器和客户端有何不同?
现在我们正在设置的细节。所以我们之前实际上已经调用了这些函数。现在我们真正要做的是深入挖掘,看看我应该怎么做?
这就是我们需要获取的所有其他内容所在的地方。好问题。所以套接字描述符,文件的千分之一。套接字描述符位于文件描述符表中。它的类型是套接字。所以它不是一个只读文件、只写文件等等。它是一个套接字。
它有更多的细节,因为它需要双向连接并可能连接其他计算机等等。但由于几乎所有的Unix系统中都将一切都视为文件,它们仍然把它当作一个文件,尽管你不能像在本地文件描述符中那样使用它。好吧。
让我们实际操作一下。好的,现在我们来做这个。
让我们看看。我们想做客户端套接字。cc。
好的。这个是这里。你还有这段代码,这样你就能跟着我们一起操作了。我们将做 struct host 和 hv = get host by name。然后是我们传入的名称,host.cster,就像这样。这正是我们之前谈了10分钟的内容。因为我们只是在试图搞清楚它是否存在。好吧。如果它不存在。
我们只是返回-1,这表示,看看,我们没有获得它。我们不知道你说的是哪个地址。然后希望调用 create client circuit 的人注意到返回值,然后知道,哎呀,我没有获得我请求的套接字。接着我们会做 in s =。
这就是我们调用套接字函数来设置套接字的地方。在这种情况下,我们使用的是afinet。然后我们使用的是SOC stream。接下来我会稍微讲一下。SOC stream 基本上是在告诉操作系统,拜托,拜托,拜托,按照传统的互联网方式来处理这个。你可能会说,好吧。
普通的互联网是怎么工作的?嗯,我来告诉你。当你在两台计算机之间发送数据时,你并不是一次性发送一大串数据直到发送完毕。我的意思是,你有时确实会这样做。但是他们的设置方式是,它会分包发送。一个包的大小可以非常不同。我想大多数——。
我现在不太确定一个包裹有多大。可能像128字节之类的东西。它相对较小,但可能有512字节。但它包含了数据的信息。它有一个包裹号。还有关于数据本身的信息。然后,它会有一些数据。
你把这些包裹发送到某台计算机,而它们可能走不同的网络路径。很多包裹会走相同的路径,因为恰好是最短的路径。但也可能不是。有些包裹可能会走一条路径,而有些可能走另一条路径。
它们最终都会在计算机那里汇聚,那里是你要把它们发送到的地方。而且它们可能会以错误的顺序到达。如果它们的顺序错了,实际上也没关系,因为另一台计算机会说:“好吧,我要等所有这些包裹。如果我先收到包裹二。”
我就先把它搁在一边,直到我收到包裹一。然后我就知道我收到了包裹一和包裹二。等我把它们都收到以后,我会把它们排序。有时候包裹会丢失。事实上,包裹经常丢失。有些计算机宕机了,或者某个地方出了故障。
或者其他的。如果一个包裹丢失了,双方都有一个超时的机制。结果是,接收计算机收到一个包裹后,会发回一个确认包裹。它会说:“我收到了你的包裹三。”然后发送计算机就会说:“好的,包裹三收到了。”然后它会把它勾掉。如果接收计算机没有收到包裹三,它就……
等待一小段时间。然后发送——实际上,它只是等待。我相信。它只是静静地坐着等。是发送计算机说:“哦,我没有收到确认信号。我得再发送一个包裹。”它会一直发送,直到发送成功。这有时候就是为什么我能接收你的文件,这也是为什么你的文件会有缓存等问题。
问题来了。但是,接受包裹丢失了怎么办?是的,这是个好问题。真是个好问题。如果确认包裹丢失了怎么办?
这完全是一样的。发送计算机说:“哦,他们没有收到我的东西。”然后再发送一个。接收计算机如果收到了两个包裹,三个,它就会忽略其中一个。它忽略第二个包裹,因为我已经收到了。所以从这个意义上来说,它是健壮的。不过,也有一个问题,叫做——。
它叫——为什么我现在想不起来了?让我想一想。再说一遍?
这是前进的问题吗?不,不,不。是拜占庭皇帝问题。就是这个。你知道,拜占庭将军问题。再一次,文科教育。拜占庭将军问题是,如果有两个将军站在两个山头上,他们都想——他们想协调攻击一个山谷。并且他们必须就攻击时间达成一致。
他们必须攻击那个山谷。假设一个将军给另一个将军发送消息,说我们将在早上7点发动攻击。另一个将军回信说,好的,我们知道我们将在7点攻击。但是如果你从未收到那些确认消息呢?
即使你做到了,发送确认的人怎么知道另一个人收到了确认呢?然后就像——所以你永远不能完全精确地协调。所以总是会有一点小问题在那里。但通常情况下,你在开始时发送你需要的总字节数,希望它能够传送成功。
然后你收到一个确认消息,什么的。结果证明它运行得还不错,正如它的工作原理一样。所以基本上,你会发送一堆这样的数据包。Sockstream做的事情是,Sockstream告诉操作系统,“请为我处理这些数据包来回的所有细节”。
如果你不想让操作系统来处理这些事,假设你想自己处理所有的事情。你只想发送数据包,让它们按顺序发送并去做。那样你就需要做更多的工作,特别是如果你想要确保一切都保持一致。
比如确保另一端收到了数据包。你必须做所有这些。你可能会想这么做。假设你正在发送视频数据或者其他什么,而你不在乎每个字节或每个数据包。也许你想这么做。你可以说,“嘿,我来处理这些事情”。
因为即便视频的几帧变得模糊一秒钟,也没关系。谁在乎呢,至少它继续播放,而不是因为确认的问题被迫减速。所以这样做是有原因的。在这种情况下,我们只会坚持使用SOC字符串。好吧?好的,那就是如果S小于0的情况。再次说明,这意味着我们遇到了问题。
返回负1。现在我们必须做struct SOC address,以及地址。
我们需要填充我们在这里创建的这个地址,因为我们实际上会用它来进行连接。所以我们将这样做。现在,首先我们要做的是使用memset并填充地址为0,大小为struct SOC address。就像这样——实际上,我想我们可以直接做——我已经在私有部分写好了,但你也可以直接做。
做地址的大小。
我认为两者都可以。我们这么做只是为了清除内存。再次说明,这可能并不重要,结果可能没差,但我们这么做是因为它要求这些内容应该为0。
这其实没什么关系。我觉得我们反正会填充所有字节,因为事实证明。好的。那么接下来我们要做的是,地址。internet,基本上,套接字互联网族等于affinet。然后我们要做的是,地址。sonport等于。现在。就在这里我们需要将它转变为网络号码。当我们设置套接字时。
我们必须确保它符合网络顺序。我不确定为什么他们不能直接让连接函数来做这个。但是你需要实际执行以下操作。你需要直接说,htonsport。它代表–它代表–,让我看看,h–现在忘记了。主机–是的。
主机到网络短。它将把端口转换为正确的地址。实际上会翻转这两个字节。如果你在一个大端机器上构建这个,那将是一个无操作的函数。它实际上什么也不做。因为它已经是那个顺序了。
就是这么回事。好的。所以我们必须这么做。
好的。那么我们接下来要做的是地址等于。好了,准备好了吗?
我们现在必须适当地进行转换。第二,我们必须将其转换为一个结构体,internet,a,d,d,r。像这样,h,e,h,e,r。
为什么我们要这么做–哎呀,我在这里做了什么?等一下。一个,a,d,d,r,等于。好了。难道我忘了那个数字了吗–是的,我忘了。我一定在这里忘记了,但是我们现在不管它。所以现在我们要将那个4字节整数转换为一个实际的结构体。确保它正确地进入结构体。为什么?因为它是一个结构体,而不是一个整数。
所以它是这样的。然后我们调用这个连接函数。如果,连接。我们传入的是那个我们还没有–与之关联的套接字。我们马上就要做那个。我们在这里还有一个转换需要做,结构体,sock,address,star。这基本上是向下转换。它在大图中丢失了信息。
当你这么做时,它会丢失信息。因为编译器将会查找一个套接字地址,而不是一个套接字地址i n。你可能会问,嗯,如何把这些信息找回来呢?嗯。我们在你执行这一行之后再讨论这个问题,地址的大小,等于0。
这就是实际的连接行。那这个套接字–或者说,连接函数。怎么知道我们在使用互联网版本4?这是问题。它怎么知道?
我们之前说过,每个这些的相同之处是什么?前两个字节总是地址。族。所以如果我们知道前两个字节总是地址,我是说。我们知道结构体的成员总是必须按顺序排列。我们实际上可以查看第一个成员,并说这个成员必须是该类型。
然后在 connect 函数内部,如果类型是互联网地址4,我们就按这种方式处理。如果是6,我们就按这种方式处理。如果是单位,我们就按这种方式处理,等等。所以它通过知道是什么来决定的。好了,好的,如果返回值是0,意味着我们——或者说,返回值是 s,意味着我们实际上获得了正确的套接字。
[不清楚],对。那么,它怎么知道的?我们在这里创建它。现在,它是互联网地址族的一种类型,像结构体一样设置,对吧?
前两个字节必须是这样的。实际上,我们在这里设置了它们。前两个字节表示的是族。因此,每当你遇到这些通用的套接字地址时,如果你查看前两个字节,它会是一个族。然后你就可以说,哦,我知道有哪些不同的族。因此,它必须是这种类型,套接字地址 in。所以他们就会以这种方式处理它。
所以 connect 函数需要知道不同的类型。它会查看前两个字节,然后说,哦,那个是2,或者那个是4,那个是8,或者其他什么的,然后根据这个数字,它会在内部决定,哦,我知道这是什么。这就是它的工作方式。现在,你不能在 C 语言中这么做,因为你不能重载函数声明类型。所以因此。
你必须以这种不太规范的方式来做。CS107,耶。这就是所有这些的根源。好了,现在,如果出于某种原因你没有连接——比如说连接实际上被断开了——你必须关闭我们创建的那个套接字,因为我们打开了——或者那个描述符,因为我们实际上通过套接字打开了一个描述符。因此。
你必须关闭它。所以如果你没有——如果你到达这一行。
这意味着某些地方出了问题,你需要关闭它,然后返回一个负数,比如这样。问题。之前我注意到在决定 SI 和地址之前,那个旧的——[不清楚],对。只是你选择的路径。我本来希望没人问这个问题。这个问题是,非常好的观点。在这种情况下,你不需要做字节序的问题。我觉得因为——。
让我想想。嗯,我不太确定。就是为什么——我不知道为什么——你必须在这里做,而在这里却不需要做。我不完全确定为什么。嗯,我不确定。我会查一下的。我会尽量看一下,看看。这个是有效的。我们可以尝试一下。我的意思是,它在我们课堂上使用的每个例子中都有效,所以我假设它在这个例子中也能工作。
好的,那我们得到了什么?
所以这是客户端连接函数。
好的,让我们实际创建并看看。有人看到客户端套接字了吗?
看看这是否有效。一个客户端套接字。好的,哦不,哦,我知道为什么,因为它没有——它只是需要把它变成一个——如果我们在这种情况下输入make,它就能工作。实际上这是一个库函数,不需要一个main函数。我们没有main函数。好的,还有其他问题吗?是的。[听不清],为什么我们要关闭它?[听不清]。
如果调用成功,我们把它传回。用户需要关闭它。因为你在设置套接字,对吧?所以在这里,你说让我们实际连接到那台计算机并获取——现在套接字是一个打开的文件。对于——它是一个打开的文件描述符。或者更准确地说,它是一个已连接的文件描述符。
然后你将它传回给使用它的用户。[听不清],是的,是的,问题是,是否有点像子进程传回一个打开的文件描述符,所以用户必须关闭它。没错。注意我们在使用描述符时会关闭它们。结果是,屏幕,套接字流会为我们关闭它。但它会关闭它。是的。
好的,重点是使用的函数需要实际关闭它。因为只有它知道何时完成。好了,接下来我们快速看一下服务器套接字文件。
好的,这个会稍微相似一些。但是我们有一些额外的细节需要处理。好的,在创建服务器套接字时,我们将基本上做相同的事情。顺便说一下,我们这里只有使用互联网 IPV4。AF,iNet,OK。Sockstream,0。所以在这种情况下,我们不需要获取任何IP地址。因为IP地址就是我们的IP地址。
我们在尝试在计算机上设置一个服务器。所以实际上结果是,没关系。我们可以使用我们其中一个IP地址。我说“其中一个IP地址”,是因为你的计算机通常会有多个IP地址。如果你有蓝牙,如果你有Wi-Fi,如果你有网线以太网线插入电脑。
你将会有多个IP地址。所以你实际上可以说,能不能只允许在这个Wi-Fi上连接,或者如你所见,你可以说任何地址。我们马上就会看到。好的。好了,如果这不起作用,我们将返回负一。它甚至还没有打开,所以我们不需要实际关闭它。
但是如果它确实打开了,我们就可以继续。我们可以继续,结构体,和之前一样,SOC,地址。高地址和地址。这个看起来应该相对熟悉。然后我们将调用M,设置地址,0,地址的大小。好的。接下来我们将设置家庭的标志。再一次,这是套接字函数将如何——或者在这种情况下是bind函数。
它将知道如何解释我们下转的地址,或者下转的SOC地址。好的。我们将做到这一点。SOC family = a,f,i net。
好的。然后我们需要设置地址。son 地址。地址。地址。好的。在这种情况下,我们确实需要做这个。所以我在想之前是否不需要做这个。没有,我觉得之前不需要,因为它一直都能工作。我得看看,为什么现在需要这么做。所以我们现在正在做的,是 HTML。
现在我们要将我们正在处理的 IP 地址转换成主机到网络的格式。这个情况下,所以现在,使用 ADDR,任意地址。我们本来可以使用我们自己的 IP 地址,但我们不想这么做。因为我们希望,至少在这种情况下,允许它连接到任何可用的端口。如果你不想这么做,你也可以不这么做。然后是地址。son。端口等于 hto_n 短端口。
就像那样。
然后现在我们需要确保我们已经连接到操作系统的那个端口。我们还需要确保我们在监听那个端口。我们必须做两件事。我们需要做的就是所谓的 bind。我们将套接字绑定到地址。再次地,你需要进行类型转换。套接字,地址。然后同样的,如果绑定成功,bind 等于 0。
现在让我们做另一件事,叫做 listen。listen 系统调用表示,好,现在你已经绑定了这个端口。现在你实际上说,当有连接尝试时,把它转发给我的程序。你可以通过 listen 来做到这一点。然后是 backlog。我们在这里输入了 backlog 吗?我们有 backlog 吗?哦。
它是传入的。是的,我不确定那是什么——哦,我想通常我们会传入零或没有,或者其他的。其实这没什么关系。我不确定 backlog 这个参数到底做什么。返回 S。好的。换句话说,如果你不能绑定到套接字,或者你可以绑定并监听它。
返回我们现在已经正确设置的套接字,作为服务器套接字。否则,我们关闭套接字,因为遇到了问题。
然后我们返回负值。就这样运作,是的,我们不。第 22 行是什么?
第 22 行是做什么的?好的,它设置了我们正在使用的地址——基本上,它告诉绑定和监听函数你可以在我所有的 IP 地址上进行监听。这就是它的意思。它与端口不同,因为你的电脑通过 Wi-Fi 也有一个 IP 地址。
你的电脑,信不信由你,也有一个通过蓝牙的 IP 地址。如果你连接了一个以太网端口,它也有一个 IP 地址。所以在这种情况下,不同的 IP 地址与连接相关联。事实证明,我相信,ADDR 任意地址实际上是零。所以你可能根本不需要做这个。
但在另一个地方情况并非如此。所以我会再查一下,解决其他的遗漏问题。现在我们已经设置好了开始监听。还有一个问题。是的?[听不清],好的。那么我们来看看你的问题。问题是,是什么阻止客户端连接?
然后重新分配或者其他什么的。所以记住,客户端只是——它是一个非常不透明的过程。客户端是另一台计算机,它尝试向服务器请求某些东西。它只是说,“嘿,我想连接到端口1234”。然后,如果你的服务器在监听这个端口。
它就建立一个连接,表示,“好的,开始给我数据”。它不会在其他服务器计算机上做任何本地操作。你不能跨越服务器计算机,去更改那里的任何端口。[无法听清],哦,它可以尝试与其他端口通信。[无法听清],嗯,它可以尝试——好的。
假设你发送了一条消息,一条随机的消息,给一个位于80端口的Web服务器。你正在尝试从它那里获取信息——它只会处理Web请求。再一次,它不会尝试其他的,它也无法获取其他的信息。你可以把这个信息传递给其他程序,或者做其他操作。
然后它们就能与Web服务器通信。其实这没什么大不了的,因为它不会——你在电脑上做的事情,跟那个套接字无关。如果它只是Web HTTP通信,那就没问题。也许多个程序可以同时通信,当然。但是它并不——也许我误解了你的问题。[无法听清],[无法听清]。
服务器可以被分配一个没有直接请求的套接字吗?[无法听清],不。我不能。我的意思是,套接字是本地的。套接字是本地于机器的。它可能是连接到另一台机器,连接到另一台机器的特定IP地址。但就是这样。
如果没有其他的——你不能请求一个没有监听的套接字。首先,你不能请求一个没有在监听的端口。而且你不能请求任何——,停下来准备上课了。我们检查一下。回来吧,我们准备好上课了。不过,其他问题呢?抱歉,情况不太好。对,[无法听清],嗯,我不完全清楚回溯参数的作用。
我们来查一下。绑定。然后绑定。我们来看一下。绑定到一个套接字。我们来看一下。绑定或者——这是那个——是绑定吗?等等。哦,它在监听。抱歉,监听。监听一个操作。对不起,回溯。好的。回溯参数定义了队列中待处理连接的最大长度。基本上,如果有连接请求产生。
队列已满,客户端排队。好的。是的。这是说你会允许多少个不同的连接。我想最大值是128,实际上。我不认为你能指定更高的数量。但它会让你的程序稍微花点时间来建立连接。然后操作系统会。
保持其他连接的积压,然后将它们一个接一个地转发给你。我相信就是这样。是的。[听不清],是的,好问题。好的。问题是,我对支持、套接字和IP地址有些困惑。好的。IP地址——我们从反向顺序开始。IP地址是你电脑的地址。
这是全世界都知道的。好的,我在这台机器上的IP地址是Myth 64,具体来说,是IP地址。
IF config会给你IP地址。它就在这里。IP前地址是171641529。IPv6地址是这里的这个长长的地址。然后是范围——哦,范围。范围就是这样。它是一个链接,出于某种原因。所以,这就是你的IP地址。它是与全世界通信的地址。当外部世界的某人想要与您通信时。
它们可以请求你某个特定的端口。你的一个程序正在监听该端口。所以也许我们想要SSH连接到Myth。那是22端口。所以我们查看IP地址,然后查看22端口,这样就能建立SSH连接。如果我们查看80端口,那就是网页连接。如果查看443端口,那就是邮件连接。
或类似的东西。这就是端口的定义。它指的是你电脑上用于监听各种事情的端口。现在,套接字是与连接相关联的文件描述符,无论是连接到另一个电脑,还是你正在监听的端口。套接字本身就是文件描述符,你可以从中读取和写入,并且可以同时进行两者。
结果显示是这样。那有帮助回答你的问题吗?是的。好的,先生。所以我们可以有多个。比如,我们可以连接到其他电脑,像其他计算机那样,它们会连接到一个服务器,然后它们会像什么一样,是什么不同的多个套接字?然后套接字呢?啊,好问题。所以是的,这是个非常好的问题。
我还没谈到这个问题。问题是,等等,假如多个计算机试图与你在特定端口上通信,因为这意味着它们都有不同的套接字,实际上发生了什么。这点我们之前略过了。一旦你与另一台计算机建立了连接。
你实际上是在完全不同的端口上操作。你在原始端口上做的唯一事情就是监听连接。然后你设置另一个端口,通过另一个端口与客户端连接,你可以在该端口上监听并建立连接。所以,一旦你在初始端口上设置好了,你就会实际交接连接。
连接到不同的端口来建立连接。假设你有1000台不同的计算机连接到你,你将会有1000个其他端口,它们在这些端口上连接并相互通信,而你仍然在监听80端口,等待下一个连接到来。这就是发生的事情。好问题。是的。[听不清]。
当你想监听多个端口时,你必须做——嗯,你必须做一些进程。其实也不完全是。我的意思是,你可以在一个特定的进程中监听多个端口。就像你可以打开多个文件一样。[听不清],哦,线程会共享套接字吗?
我相信它们会共享。是的,我相信线程会共享套接字。现在,这其实并不会,像在你的网络线程池中,比如说,当我们做这个时,我们会建立一个新的连接。那是一个不同的端口,当我们做所有这些——当我们进行设置时。连接回客户端时。那是完全不同的端口。好的,还有其他问题吗?
好的,我们周三见。
729

被折叠的 条评论
为什么被折叠?



