CS162 Lecture 24: Networking and TCP/IP (Con't), RPC, Distributed File Systems
Watch on YouTubeVideo summary
This lecture explores the foundational principles of distributed systems, beginning with how nodes collaborate to reach a consensus despite potential failures. A key mechanism discussed is Two-Phase Commit (2PC), which ensures atomicity by guaranteeing that all participants either commit or abort an operation together; however, this approach relies on stable logs and can be hindered if a single node crashes, blocking the entire system. The discussion then transitions to the layered architecture of network protocols, distinguishing between the unreliable physical layers that handle raw data transmission and the higher-level transport layer which enables process-to-process communication. Within this framework, the lecture contrasts User Datagram Protocol (UDP), a fast but unreliable protocol suitable for high-bandwidth applications like video streaming, with Transmission Control Protocol (TCP), which provides a reliable byte stream by managing connections, retransmitting lost packets, and ensuring data arrives in the correct order without duplication.
To achieve reliability over an inherently unreliable network, TCP employs several sophisticated mechanisms including acknowledgments, sequence numbers for ordering and duplicate detection, and flow control via sliding windows to prevent overwhelming slow receivers. The protocol also implements congestion avoidance strategies that dynamically adjust transmission rates to match the bandwidth-delay product of the network path, preventing routers from dropping packets due to overload. The lifecycle of a TCP connection is carefully managed through a three-way handshake for establishment and a four-step process involving FIN flags for graceful shutdown, while missing data is handled by retransmitting specific bytes rather than resetting the entire connection upon timeout. These features collectively allow applications to build robust communication channels on top of best-effort IP delivery, effectively abstracting away the complexities of packet loss and network variability.
Building on these networking fundamentals, the lecture introduces Remote Procedure Calls (RPC) as a method to simplify distributed programming by allowing programs to invoke procedures on remote machines as if they were local function calls. This abstraction hides critical implementation details such as serialization, where objects are converted into standardized byte formats for transmission, and endianness conversion, ensuring data is in network byte order regardless of the host machine's native representation. RPC stubs automate these marshalling and unmarshalling tasks based on Interface Definition Languages, supporting dynamic binding that enables load balancing and failover capabilities. Despite introducing overhead from kernel crossings and complicating failure handling due to partial failures, RPC offers significant benefits like location transparency and modularity, making it ideal for microkernel architectures where user-space processes communicate to maintain fault isolation.
The session concludes by examining distributed file systems like NFS and AFS, which allow clients to access shared data across a network regardless of physical location, leading naturally to the CAP Theorem. This theorem highlights a fundamental trade-off in distributed systems, stating that it is impossible to simultaneously guarantee Consistency (all nodes see the same data), Availability (every request gets a response), and Partition Tolerance (operation continues during network splits). When network partitions occur, system designers must choose between maintaining consistency and partition tolerance at the cost of availability, or prioritizing availability and partition tolerance while sacrificing consistency. This theoretical framework underscores the difficult architectural decisions required when building scalable distributed storage solutions that must balance these competing properties to function effectively in real-world environments.
Read the full video transcript
Well, welcome everybody
uh
to CS 162. We're getting down to the
very end here. Um
and uh
there's no class on Wednesday and just
so you all know. Um I would uh like to
pick up where we left off and we were uh
talking about a number of things in uh
extending operating systems out to the
uh network as a whole. And so we talked
about the distributed consensus making
idea. And um that that was basically
a situation which you have several
different nodes spread throughout the
network. They all propose a value. Some
nodes might crash or stop responding,
but eventually all the nodes decide on
the same value
uh from some set of proposed values. So
that's the general consensus problem.
There's a simpler version which is
distributed decision-making and that's
where uh you choose between true and
false or commit and abort or uh one of
two options and essentially the job of
all the nodes that are participating in
some protocol here for con- consensus um
are basically collaborating and
eventually coming up to exactly the same
decision.
Um equally important to the
initial process of making consensus is
making sure that that's recorded for
posterity and so that's basically um
you know, how do you make the deci- sure
the decisions can't be forgotten. So the
simplest thing of course is recording on
discs. But in a global scale system, you
could start talking about replicating
much more widely um somewhat like a
blockchain application. So the
particular uh type of uh distributed
decision-making that we spent a little
time on to talking last time was
two-phase commit. And basically the key
behind two-phase commit is there's a
stable log on every participant uh to
keep uh track of whether a commit is
going to happen or not. And uh if
machines crash in the middle of the
protocol and they wake up uh they can
look at the log to see what they've
committed to in the past. The two phases
of course are the prepare phase is the
first one um where the a global
coordinator requests that all
participants make a decision to either
commit uh or not. And um
so basically you ask each participant
what they want to do. They either say
commit or abort. And they make sure to
record their decision in the log as we
mentioned so that if they crash in the
middle, they can come up and they will
never come up with a different decision
than the one that they've committed to,
so to speak. Um and then during the
commit phase, if everybody has said
commit, uh then the coordinator will
tell everybody to go ahead and do the
actual commit, at which point uh they
all record that the final decision was
commit and they go forward. And of
course, if any one participant decides
to abort, then they all abort. And the
crucial idea here is either it's atomic
atomic decision-making. Either everybody
decides to commit or everybody decides
to abort. And uh there's no mixing of
the two, okay? And so that was kind of
uh the simplest example of this. And we
talked about some of the downsides of
two-phase commit, among other things
being that a crashed machine can prevent
everybody from moving forward. And so
then we started talking about
alternatives after that.
Okay.
Um
Let's see here. So the log is basically
a crucial part of that. So if you go
back and look at um several of the
slides that I had walking through the
protocol, you can see how the log make
sure that we always have that atomicity
property of everybody decides to do
commit or everybody decides to do abort.
The uh second topic that we just started
with uh
toward the the end of the lecture was we
were talking about network protocols and
we mentioned that there are many layers
in the network protocols. There's the
physical level which is the ones and
zeros.
Um could be optical uh phases, could be
any number of things. We talked about
the link level which is packets being
sent down a single link um with their
formats and error control for instance.
We talked about network level
communication where you put a bunch of
links together for a path. We talked
about transport level. We just started
about that which is for reliable message
trans- um message delivery and we're
going to spend a lot uh
a good chunk of today figuring that out
as well.
And so this is a rough diagram uh to
keep in your mind here. The physical and
link layers down at the lower level can
be any number of technologies like
Ethernet or Wi-Fi or LTE or 5G or
whatever you like. Um and those get you
one hop in the network. IP typically
gets you more hops, okay? So once you
got the IP protocol, then you could
route from here to Beijing for instance,
as long as you knew the right IP
address, uh things would be forwarded
hop by hop through the network. Above
that level is the transport layer where
we actually start doing better than just
talking about machine-to-machine
communication. We can actually start
talking about process-to-process
communication. And then of course, you
build applications on top.
Okay. RPC stands for remote procedure
call. We'll show you that a little bit
later in the lecture.
Okay.
So um
and a lot of things are built on top of
remote procedure calls. So we'll talk
more about that. So this layering uh
is building complex services from
simpler ones and each layer provides
services needed by higher layers uh that
utilize those services. So this is uh
something that you've known for all the
time you've been in computer science at
Berkeley. Layering can be a good thing.
Uh the physical link layer is typically
very limited. So it's one hop and not
only is it one hop, but it's it's uh
unreliable typically. There's a maximum
transfer unit. So somewhere between 200
and 1,500 bytes are very common. Um it's
only
uh high performance networks inside of
cloud uh processing that might have what
are called um larger packets that might
be 9,000 uh bytes or so, but typical
1,500 is the max you see. Um routing is
limited uh with a physical link uh pa-
possibly through a switch. Okay? Um what
we're going to try to figure out now in
the next uh bit of the lecture is if we
have these limited messages that are of
limited size, how do we basically build
something we can use? So the physical
reality is packets. The abstraction is
one of messages so we can build our
decision-making algorithm. So we can
build distributed storage which we will
hopefully get to by the end of the
lecture today. Um the physical reality
is that packets not only are they
limited in size, but they're unordered.
Um so sometimes the packets might arrive
in a different order than you sent them.
Uh the typical abstraction is that um
random ordering is not good for us. We'd
like things to be ordered. Um physical
reality
is that packets are unreliable.
Remember when we talked about the
end-to-end uh philosophy, we said um
gee, the network ought to not do things
that the end points still have to do
anyway. And so uh datagram
networks where the packets are not
guaranteed to make it to the destination
are the typical thing in the middle
because at the end points, we have to
have some reliability protocols. We'll
talk a little bit about that today.
Um physical reality is that packets go
from one machine to another which is
only sometimes useful. It's much more
useful to be process-to-process.
Um the reality is that uh packets only
go on one link over the local network.
We'd like to route them anywhere. Uh the
reality is that
packets are asynchronous. They kind of
go when they can. We'd like them to be
more synchronous so that we know when
something is completed. Um and then of
course, packets are insecure and we'd
like them to be secure. So the reality
of the physical pieces on the left are
ones that we would like to be able to
basically uh hide under a virtual
communication abstraction giving us a
much cleaner messaging abstraction.
Okay? Now just to uh I I showed you this
last time, but I just want to pop this
up really quickly. Um
IP V4 for instance, basically has a
header that's wrapped around data. So
you put this on the front of it and this
20 bytes have a bunch of fields
including the source and destination
address. So where am I going? Where am I
coming from? And then a protocol which I
have highlighted in red here is
typically what type of IP packet is this
and we'll show you a couple of those.
Now um
my process-to-process,
the question is do I mean on different
machines? I certainly mean
process-to-process on different machines
is something we would like to achieve.
Um sometimes you use the IP protocol
abstraction and go to from
process-to-process on the same machine.
Um and uh but by the you know, the thing
that's much more interesting here for
this discussion is going from one
machine to another. Okay? And from one
process on one machine to another
process on another machine.
So now, doesn't the protocol field
violate abstraction somehow? Um
you might think of it that way, but it
turns out it's gives you enough
information that when an IP packet comes
in, you can put it and demultiplex it to
the right protocol uh handlers. And so
that's kind of a minimal
um
call it an abstraction violation if you
like, but it's kind of a minimal
uh requirement there in order to very
rapidly process incoming IP packets.
Now protocol can be TCP, it could be
UDP, it could be ICMP, could be any
number of things. So today we're going
to
um talk about UDP and TCP, yes. Um in
particular,
how do we build process-to-process
communication from machine-to-machine?
So looking back at this header again,
notice that it's 32-bit source address
and destination address. So, the source
is where I'm going. Let's say that's in
Beijing somewhere. The destination is
where I am. That's my local machine.
These are two machines. They don't say
anything about which processes on those
machines, like a web browser for
instance or a web server. Doesn't say
anything about who would like to
communicate, okay? And so
the simplest thing we can do is called
UDP, which is a type of
process-to-process communication we get
by taking this IP header. This is the
one I showed you earlier, 20 bytes, and
adding a UDP header,
which
basically has a source and destination
ports. These are 16-bit numbers, a
length for the data, and a checksum.
But, these two ports, the source and
destination ports, are part of that
five-tuple. If you remember when I said
you create a socket from machine to
machine, remember it was source address
destination address, source port,
destination port, and protocol. So, all
five of these things that you see here
together work, okay, to give you a
unique connection between two processes.
UDP is very simple, okay? It's another
type of datagram, but one that basically
goes from process to process. And so, if
you see here, this is IP protocol 17,
which was we put a 17 in that header.
It's a datagram, so it's fully
unreliable as we use it. It goes from
source to destination, and it's really
low overhead. And it's really low
overhead cuz we just put a few extra
bytes,
eight bytes, on top of the IP header to
get the um
the UDP header, okay? And it's often
used for high-bandwidth video streams
and so on. And it's a a very good way to
sometimes overuse network bandwidth if
you're not careful
because there are no restrictions on how
many packets you can try to force into
the network. And so a number of uses of
UDP can be considered antisocial almost
if you use them incorrectly.
All right? And we'll we'll see how TCP
is is different than that.
So, all right. Now, here's this layering
that we just talked about. See in the
gray at the bottom here is the physical,
you know, ones and zeros. And the data
link layer above is that link-to-link.
And so, basically going one hop goes
over the the data link physical
combination here to go from say a host
to a router to a destination host.
But, that's not going to get us very far
without being able to route. So, this
actual hop, the data link physical gets
us from host A to to the router or from
the router to host B. It's this network
layer on top that's doing IP, for
instance, that decides how to go hop to
hop to hop using routing tables to get
you from your source to your
destination.
Above that is going to be the transport
layer, which is going to be, for
instance, UDP or TCP. And then
applications on top of that. And of
course, applications are the ones that
open the sockets. So, the reason I've
got these arrows the way I do here is
you think when you're writing your
application that it's communicating
directly with an application at the
destination. In reality, what's going on
is your application sends
something through a socket, and it
really goes through the different layers
in host A. It goes across the physical
and data link layers to the router,
which goes up to the network layer. The
router makes a decision of what next hop
to go, and so on. And eventually, you
get to the destination host, and then it
comes on up through the application
through the various layers in the
operating system at the host side, and
eventually into the sockets and the
application. So,
um
these these
arrows represent communication, but at
an abstract level, it's only the very
lowest ones that represent direct
connections.
Okay? And so the way we can look at, uh
for instance, this communication is we
can think, well, we've got an
application with some data. What happens
is it goes through a transport layer
where we wrap a transport header around
it. So, that's like the UDP ports, for
instance. And then we wrap a network
header on it, which adds the IP address
and so on. And then we put a frame
header, which is the
the MAC addresses for, let's say,
Ethernet, like I said. And then that
goes down the physical layer. There's
some bits that are transmitted the other
side, and then things are unwrapped. So,
this is like adding
an envelope that then you put it inside
another envelope inside another
envelope. It gets transmitted, and then
you pull it out of the envelopes and
eventually get back to the other
application.
Okay.
Um
So,
this wrapping is something that
is basically this layering that we're
using for abstraction. It can get
expensive, and sometimes
really high-performance routers are
going to completely violate all of these
layers, and they'll squash everything
out and process everything at once in
parallel in an FPGA or whatever. But,
it's important to try to understand the
process as the way I've given it to you
here you here where it's putting a
series of envelopes together, and then
taking the series of envelopes apart.
And the other thing I wanted to point
out here is this from the network layer
to the network layer. This is machine to
machine. It's really this transport
layer that hands off to the right
process. Okay? So, that's where we
demultiplex based on port, and then
eventually the right application gets it
because we've demultiplexed it
at the transport layer once we've gotten
through the network layer.
Questions?
Now
let's look at these transport protocols
a little bit. So, transport protocols
are things that we put on top of IP. We
gave you UDP earlier. That's protocol
17. That really means that you put a 17
in that red field I showed you earlier.
This is a no-frills extension of
best-effort IP to be process-to-process
rather than just machine-to-machine like
IP is. TCP, which is something
which we'll talk in more detail about in
the next number of slides, is more
reliable, okay? It's so it's got
connection setup and teardown. You
discuss you discard corrupted packets.
You retransmit lost packets. You make
sure there's flow control so you never
overflow anybody's buffers. There's
congestion control so that if too many
people are trying to use a link in the
middle, everybody fairly backs off.
And so on. And so, that's
going to be a slightly different animal
than UDP's. And furthermore, TCP is a
stream, which I'll show you in a moment.
There's a lot of examples. Obviously,
there's eight bits there in that
protocol field. So, there's many
different things other than UDP and TCP.
There's, for instance, DCCP, which is
another datagram protocol. There's RDP
for
the reliable data protocol. There's
SCTP, which is a pretty cool
multi-stream version of TCP
that isn't used all that much. But, so
there's many different things you can
put in that TC in that protocol field.
So
just a flashback
to,
I don't know, a month and a half ago, we
were talking about this client-server
example for a web server. And if you
remember, we talked through the various
setups and and so on where server gets a
listen port, the client connects, and
then there's a socket that's set up, and
so on. And ultimately, once everything's
set up, we somehow are able to write and
read through the socket,
and everything just works reliably as a
stream. And so, we're going to talk
about how that works. Now, the question
here that's in the chat is sort of how
many non-TCP and UDP protocols are
actually used. You know, they um
they're used for a lot of things that
you might not normally encounter. Like,
for instance, if you're um if you have a
an encrypted VPN from point A to point
B,
some of the one of those protocols is
used basically for um
for the encrypted packets. And there's
other versions,
port 500, that's actually
a UDP. So, that's not that's a UDP
packet, but that's used to set things
up, and then it's in the encrypted
the encrypted IP after you're done.
There's a number of other protocols
there that are actually used
in ways that help manage. So, they're on
the outside of the typical connections
that you run into. But, obviously, TCP
and UDP are extremely common. But, once
you get into another thing, I guess
another good example would be when you
get into some of the streaming
multimedia then
when you get into streaming multimedia
connections, those are also
other protocols. So, data link is
talking about the
the part of the protocol that gets you
one hop. That's part of the networking
protocol. Datagram is just a
packet that gets tossed through the
network, and that might or might not
make it all the way. So, those are
different things. Data link is the layer
in the networking layer. Datagram is the
thing that we're sending. It's a packet.
So, back to our sockets here. Let's take
a look at kind of what is involved in
this middle part here in actually
communicating. And then we'll talk about
setup and teardown.
So, the problem of getting reliable
delivery is that all physical networks
garble or drop packets. We said that
already.
So, the physical media
has lots of problems. Like, the packets
might not be transmitted or received. It
might be that multiple people try to
talk at once, in which case there's an
exponential backoff that has to happen.
If the if If transmit close to the
maximum rate,
you might get more throughput, but you
might start losing packets, okay? And so
there's sometimes there's this trade-off
between throughput and
and absolute reliability. There's also
if you're in a very low uh
power scenario, you might transmit at
extremely low voltage right on the edge
of a bunch of errors
occurring,
but you put a heavy forward error
correction code on it to make up for
that. And so there's there's a lot of
playing with the fact that these packets
are unreliable.
Okay? And if you remember from the
end-to-end principle again, if we put
reliability by retransmitting on the
endpoints, it means that things don't
have to be perfect in the middle and in
fact we may not want them to be perfect.
We just want them to be good enough that
we can retransmit and get the data
through eventually.
Um the other thing that's going to be a
big deal is congestion. So if too many
people try to go through too small of a
pipe in the middle of the network, then
they're going to have to stop dropping
start dropping because the routers will
have more input than they can for their
outputs and so they're going to have to
drop packets. That's kind of the IP
idea.
Okay.
So um and there's many options I kind of
give here uh insufficient queue space,
a broadcast link with hosts going at the
same time,
um
buffer space at the destination, rate
mismatches, you're sending it too fast
and so on can cause congestion.
Uh and then the way we So we want to
start with that. We have to start with
that. We want to make reliable message
delivery on top of that. So what are we
going to do? So we're going to need to
have some way to make sure the packets
actually make it
uh so that every packet's received at
least once and every packet's received
at most once.
Um and that because
uh if we get duplication that we're not
aware of or we get dropping that we're
not aware of, then all of our
applications that are relying on that
are going to start having problems,
okay? Or they're going to have to do the
all the work on their own. And uh
this is a level of uh
this reliability is common enough need
that we're going to want to make sure
that we can do that in a common facility
like TCP rather than having everybody
roll their own.
Okay. And we're going to show how
dealing with misordering in the network
and dealing with dropped packets and
dealing with duplication are all
actually handled by similar mechanism.
So that'll be nice.
So TCP is really a stream, okay? So the
idea is you this is the alphabet, right?
A B C D. So you stream the alphabet in
you know, or your bytes in on one side,
they show up on the other side. Uh every
byte that goes in comes out
uh the other side and it you know, we
don't see duplication. And the other
thing about it being a stream is there's
really no
um we're not packetizing it. It's just
you send bytes in and bytes come out.
And if you care about packets, it's
going to be up to you to to make a
packet protocol where you say, well,
every message in my connection is going
to start with a length and then the
data's going to be after that. And now
I've got a packet. Okay, but that's
that's up to you you the user to
packetize on your own.
Um now of course underneath the covers
is all the IP packets, but this trans
the TCP view is really that bytes go in
and bytes come out.
Okay? And there may be many routers in
the middle and it just works, okay? Now
this is a protocol six in that little
red IP protocol point that I showed you
earlier.
Uh it's a reliable byte stream between
two processes on different machines over
the internet.
Okay? And we get read, write, flush,
etc. And you know, that's exactly with
our web server web client example that
we gave you with sockets, the sockets
are going to be the things that connect
on either end of the TCP and um this is
basically we're going to talk about
what's inside uh inside that process.
So some details which we're going to go
into in a bit, but um
since the underlying system is uh got a
you know, a
limited packet size and so on, it's
going to be up to TCP to take your large
streams worth of data and fragment it
into lots of little pieces. Sometimes in
the middle of the network IP will
fragment into further pieces. And so
we're going to need to make sure that
after we've fragmented it, we can
reassemble it at the other side and we
can reassemble it in order.
Um it's going to use a window based uh
acknowledgement protocol and I'm going
to show you a lot more about that in a
second to minimize the state at the
sender and receiver and make sure that
um the sender never sends more data than
the receiver has space for and the
sender never sends things so quickly
that it clogs up the routers and
prevents other people from using this,
okay? And so this windowing is going to
be important uh for both reliability and
for being a good citizen in the network.
And obviously automatically
retransmitting lost packets.
Okay? And being a good citizen. So
without further ado,
so one of the problems is dropped
packets. How do we deal with that? And
again, we've said multiple times that we
uh all physical net networks can garble
or drop packets. And so IP can garble or
drop packets as well.
And uh
so that means we got to build reliable
message on top of that. And so the
question is how are we going to do that?
Well, the obvious thing to do or or
maybe not so obvious, what the thing
that we do is typically use something
called acknowledgements.
Okay? And so the idea here is you've got
A communicating with B and so A sends a
packet to B and then B sends an
acknowledgement back.
Okay? And what is the acknowledgement
good for? Well, it says first of all, B
got it. It says, hi I'm B and I got this
packet.
Okay? And assuming that we put a
checksum on the packet, then B can also
detect garbled packets and just throw
them out. And um
in those instances, you could imagine B
sending back a NACK or a negative
acknowledgement. In fact, what happens
is B just treats a garbled packet as one
that just never arrived. And uh so
that's going to cause the other
mechanism to come into play. So if A
sends a packet to B which gets lost
along the way or garbled, eventually
there'll be a timeout at A and then A
will send the packet again and
eventually we get an ACK.
Okay?
So
um
some questions about this.
If the sender doesn't get an ACK, does
that mean the receiver didn't get the
original message? What do you think?
So just because A doesn't get an ACK
back,
okay?
Right. So I see no, I see I know, I say
who knows. Good, this is very
philosophical tonight. So just because
you don't get an ACK doesn't mean that A
uh
didn't successful successfully transmit
something to B. Like for instance, the
ACK could have gotten lost on the way
back. So what that means is once we do a
timeout and retransmission, suddenly
we've got duplication as an issue.
Okay? So um
what if the ACK gets dropped or the
message gets delayed? Same idea. So now
all of a sudden we've got issues here.
Now I see somebody asking about
Byzantine
uh so
um we're going to assume here in the
moment that the network is trying to do
its best to act in uh
the way it's supposed to. Um so we're
not going to worry about malicious
components in the middle or B being
malicious. So let's just look at the
underlying message transmission
um and then the way we we get Byzantine
agreement on top of that is we
uh build something on top of unreliable
messages. But let's at least see whether
we can
um get our messages to make it from A to
B. All right?
So
um what we've just talked about here is
what I would call stop and wait. So we
send, we wait for an ACK, repeat. Okay?
This is like, you know, put it into the
washer, turn it on, wash, repeat over
and over again, right? So
uh
we call the round trip time is the time
from the sender to get to the receiver
and the ACK to get back.
The round trip time uh represents
basically twice of the transit time, of
course.
And um the receiver, we can talk about a
one-way path, which is the time from
when the sender sent it to when the
receiver got it. And so two times D is
going to be our round trip time.
Okay.
Um and
uh
we keep doing that. And as you can
imagine, the problem with this is
there's a lot of lost opportunities here
because we have one packet kind of going
at a time.
Okay? And
how fast can we send data? Well, we can
actually use Little's Law of all things.
If we've got a
um B
uh bandwidth and a times a round trip
time kind of tells us something about
the number of packets that are uh on the
wire or waiting in the queue.
But in fact, uh we've set this up so
that we only have one going at it once.
And so um the bandwidth is basically one
packet per round trip time.
And this depends only on latency, not on
the network capacity. So it doesn't
matter. You could you could basically
have uh strings and two cans on it on
either side here for all it matters
because, you know, we're not sending
very fast. This doesn't have to be a
gigabit link.
Okay? In fact, you could do this
computation pretty simply. Like suppose
the round trip time is 100 milliseconds,
uh the packet's 1,500 bytes, you come up
with about 120 kilobits per second,
which is pretty slow.
Okay? So this is clearly this stop and
wait is clearly not what we want to do.
We got to get some more packets going.
Okay? So if you have a 100 megabits per
second link, you're wasting a lot of it,
you know, almost almost 1,000 times.
So um and the other thing is how do we
know when to timeout and and uh
retransmit, right? So, here's a case
where the sender sent something, the act
didn't make it or it got lost somewhere
along the way. Clearly, the timeout
needs to be at least as long as the
round trip time before we start
re-sending uh because otherwise, you
know, we'll re-send before getting the
act back. So, that's not so good. So,
we're going to need to be estimating
this timeout somehow
with knowledge of the round trip time.
And um you know, if the round if the
timeout is too short, you get a huge
amount of duplication. It's too long,
then the packet loss really becomes
disruptive even if you just happen to
lose one packet, you wait a huge amount
of time to keep going,
um you're going to really suffer for
your communication.
Okay?
So, and then how to deal with
duplication? I mean, here's a situation
maybe where the act just got delayed and
we went and re-transmitted but then the
act comes in and we get another act and
now we got two copies at the receiver.
Okay.
So, how do we deal with message
duplication? Well, we put a sequence
number in, okay? And this is a very
simple bit bit sequence number where uh
the sequence number is either a zero or
a one and the idea is um the sender is
going to keep a copy of the data in its
local buffer until it sees an act for
that uh sequence number.
Okay? And then furthermore, the receiver
is going to track
uh packets and by having exactly two
options, a zero or a one, then the
receiver can figure out if the um if
there's a re-transmission cuz it'll see
two packet zeros in a row and it can
know to throw one out because it's a
duplicate.
Okay? So, that when we start putting
some numbering
acknowledgement numbering or sequence
numbering onto the packets, we can start
getting rid of duplication at the
receiver and figuring out how long the
sender needs to hold on to things to
re-transmit.
Okay? We're going to call this the
alternating bit protocol.
Um so, the pros of this of course is
it's very simple, it's one bit. The con
is really
uh
if if the network can delay things
arbitrarily, then you and you had a
packet zero that might got
might have gotten stuck in some router
in the middle and then got transmitted
later, you might not be able to
disambiguate uh the um duplication with
only one bit. So, clearly that's a
problem and furthermore, we're still
doing one packet at a time in the
network. So, this this doesn't look
great. So, what should we do here?
To uh
up our bandwidth and deal with
uh
more uh unexpected delays in the
network.
Okay, don't wait and send more packets.
All right, I'll buy that. But, that
would seem to make the problem of
disambiguating uh duplicates at the
receiver worse.
So, what else do we have to do?
Okay, yep, we're going to sort packets
later. So, what do we need in order
sequence numbers? Yeah, so we're going
to need more than a bit, right? Cuz one
bit,
you know, distinguishing between packet
zero and packet one and then repeating
with packet zero, that's clearly not
enough, okay? So, we need a bigger
space. Larger space of acknowledgements.
Okay, so that seems simple, right? It's
sequence numbers.
Um and now we've got pipelining
possibilities cuz we don't have to wait
for each act before we send more. Okay,
so here's here's what we had before, you
know, sender sends, receiver receives,
but now we have the potential to have
many outstanding packets
uh and many
received packets in a way that basically
allows us to fill up the network. Okay,
so if you look during this round trip
time, what you see is during that round
trip time, we have many packets that are
on their way to the receiver and many
acts that are on their way back and as a
result, we can actually fill up the
network pipe and uh start getting our
actual network bandwidth back rather
than something that depends on the round
trip time.
Okay? So, the acts also are going to
serve a dual purpose here. So, one, if
assuming that every one of these uh
outgoing packets has a unique sequence
number on it, then um clearly, we can
confirm that a particular packet got
back here because we see its sequence
number
and we can do deal with ordering. So, if
we have packet zero, one, two, three,
four, five, six, seven, whatever and
they arrive out of order, we can reorder
them at the receiver side back into
sequence number order and deal with
misordering, okay? And so, the acts uh
in addition to this reliability aspect
also help us with ordering.
Okay, so this seems like we're going
into a good possibility here. Now, how
much data is in flight? Well, if you
take round trip time times whatever your
actual bandwidth is,
okay, that's going to give you uh the
window, the sending window that
basically makes sure that you um you
have a lot of data out in the network
and um
basically lets you fill up the pipe uh
both in the forward and reverse
direction.
Okay? And so, B in this case is bytes
per second. Remember, this is the
something uh we learned in chemistry in
uh high school. Basically, you got to
match up your your um
units. So, round trip time is in
seconds, B is in bytes per second, the
total here is in bytes. So, in this
case, W send is how many bytes do I want
to have in the network uh
at once in order to make sure that um
nobody is waiting for packets. Okay? And
so, this W send is like the sender's
window size.
Um and packets in flight, if we wanted
to count packets instead, we could take
this sending size divided by the packet
size and that tells you how many packets
we need to have outstanding to fill
everything up.
Okay?
So, how long does the sender have to
keep packets around? So, that's an
interesting question, right? Um
Ah, so uh
let's uh so, the question is how long do
we need to hold on to this? And the
answer is, well, until we know that a
particular packet has been acknowledged,
right? And so, certainly, we need to
have enough buffer space in the sender
to have at least uh a round trip time,
probably a little bit more in order to
allow us to lose some packets and cause
some re-transmission.
Okay? Now, the other question is would a
timeout result in starting over from the
beginning?
Um
Well, what do you think? Do we need to
re-send every packet if uh we lose just
one?
So, good. So, it seems on the face of it
that we'd want to only send the ones
that haven't been act. And because we
have labeled every packet with a
sequence number, then in principle, we
can figure out which ones haven't been
received and which ones need to be
acknowledged again, okay? And so, that's
certainly uh
plausible for us.
Um now,
it depends on your protocol whether you
always uh have the ability to
individually transmit packets or not um
or whether you have to go back and do a
certain range of them or whatever, but
at least in principle, we have enough
information to uh re-send only the
things that were lost.
Okay? Now, how long does the receiver
have to keep the packets data? So, the
data at the receiver side certainly has
to be there long enough to do
reordering. So, if we get a bunch of the
later packets, we need to make sure we
have enough space to absorb the early
ones so that we can wait, absorb the
early ones and then send them in order
to the actual application at the
receiving side. So, we need to have
enough space for that.
Um and
also, we're going to need to store data
until the application's ready. So,
perhaps it has it's you know, it's busy
doing something else and it hasn't
executed a read against the socket yet.
So, we need to hold on to data at the
receiver as well.
Um and then of course, you have to worry
about the following. What if the sender
is blasting packets at the receiver and
the receiver just
is too slow and as a result, a bunch of
the data that was sent actually made it
to the receiver only to be thrown out at
the receiver. So, that seems like a
probably a bad idea, right?
So, here's a bunch of interesting
questions.
Okay, so let me talk a little
administrivia here. Just remember
um
got a midterm
not this Thursday cuz uh
folks are going to be hopefully
over-indulging in food on Thursday, um
but next uh week from Thursday is going
to be uh midterm three.
Okay?
And uh camera zoom screen sharing just
like in midterm two, we'll mail out all
your links. There's going to be a review
session link that'll come out in the
next day or so.
Um and everything up to lecture 25. So,
it's this lecture and the um next
Monday's lecture and uh we have no
lecture on Wednesday this week, okay?
And lecture 26 will be a fun lecture.
So, if there's any topics in particular
you want to
uh cover, let me know.
And I don't think I have too much more
to say
on this. Um I
have um
a so, a question about uh is this closer
to a final or closer like midterm two?
Uh as I think I've said before is every
midterm is in principle cumulative
uh in the sense that you need to not
have forgotten everything that you
learned, but we will certainly focus on
material uh in the last third of the
class. But, um we certainly
will potentially ask you questions that
would require you to have not forgotten
everything uh from earlier parts in the
term.
Now, um
I'm not going to go in this in great
detail, but
please be careful with collaborations.
I realize we're getting on to the end of
the term, but remember that
explaining things to something at a high
someone at a high level
and discussing things at a high level,
but not sitting down line by line going
through everything.
Um,
you know,
if there's a lot of individual syntax
transfer on homeworks and
in between project groups, it's probably
too much sharing, okay? And so just be
careful.
All right, and don't get don't get
friends into trouble by asking them for
their code over and over again
cuz you'll put them in a bad position as
well of having violated our policy. So
try not to do that.
Okay, I've talked about this last couple
of lectures, so I don't want to go into
it in greater detail.
So,
let's keep going on this a little bit.
So, I think the idea of having a big
acknowledgement space or big sequence
number space and sending a bunch of
messages into the network to get
pipelining sounds like a good idea, but
if you remember here when we set up
um,
queues or pipes between processes on a
local machine, we had a queue in the
middle and we had blocking cuz the queue
had fixed capacity. So if you wrote
and the queue was full, the writer would
actually get put to sleep or if you went
to read and there was nothing in the
queue, the reader would get put to
sleep. And so we would
we would like to have something similar
to what we had with pipes, but across
the network. And using TCP, the question
is
how do we go about that?
Okay, so buffering in a TCP connection,
we have process A and process B. There's
a send queue on A's side and a receive
queue for that particular stream.
And then there's also one going the
other direction. So typically, if you
remember sockets are bidirectional and
when we set them up, we have queues on
both sides
and we want to make sure there's proper
blocking, so no data gets overwritten or
otherwise lost.
Okay?
And so single TCP connection needs four
in memory queues
as we just said here.
And the host's window size for a
connection is sort of how much remaining
space it has in the receive queue. So
for instance, in this case, if this
receive queue has 100 bytes left in it,
the host is really only allowed to send
another 100 bytes until things start
becoming acknowledged because
we never want the host to basically
overwrite the receive queue. And
furthermore,
just acknowledging that they've been
received is not enough because the
receive queue could still be full
because host B hasn't pulled things out.
So we really need to say is we need some
way to for the receive queue on either
side to tell the sender how much space
it's actually got left in its queue and
make sure that the sender never sends
more than that. And that'll prevent us
from overriding at the destination.
Okay? And so host advertise its window
size
at the receive queue and every packet
going the other direction. It's keep
saying, well, here's how much I have in
my queue now. Here's how much I have in
my queue now. And as a result, we can do
this uh,
buffer management so that we never
overflow a host or lose data.
Okay.
So the idea is we're going to build a
sliding window protocol, so the TCP
sender knows the receiver's window size,
tries never to exceed it.
Packets that it previously sent may
arrive filling the window up, but we
want to make sure there's never more in
transit than there is buffer size at the
destination.
And you're allowed to keep sending data
as long as there uh,
there's enough space guaranteed at the
destination.
Okay, and I'm going to show you how that
works in a second here. So the idea here
is I'm not I'm going to talk about
packets of space at the receiver even
though normally it's bytes.
And so the window size to fill is
you know, we have a let's say we have a
bandwidth of packets per second times
the round trip time is going to tell us
how much we want to have in flight at
once. And this is a form of little law
is again little's law again to figure
out sort of how much we can go with. But
for instance, here's a case where we
have an
act packet which we're going to call
packet one that got sent. Um, another
uh,
packet. So now the send window is got it
says it one and two are outstanding.
Here it says one, two, and three are
outstanding and we're going to assume
that we're not allowed more than three
packets at the destination. Eventually,
what happens is because one came in in
order,
we've received it and we potentially
sent it up to the application. At that
point, the receiver will say, well, I
actually now have space in my
destination for another one. Okay, at
which point we'll send another one and
so on.
Okay? And so this explicit tracking of
and here the receive queue is basically
never
holding on to anything. It's sending it
up. So each one of these acts is
basically saying, well, I still have
three available. I still have three
available. I still have three available.
But you can imagine if the receiver was
basically um,
holding on to it, but the application
the receiver wasn't absorbing it, then
this queue would start filling up. Now,
what if you never get an act from the
receiver? So what happens in that case
is that
if we go back to this point,
um,
this sender will stop sending because it
only knows that it's got three packets
worth of space and it'll stop. And if
there are no acts that comes back, then
at that point I'll resend I'll start
resending from the earliest one that's
missing. So I'll start resending one
and then two and three over and over
again
waiting to finally get an act back. And
once I got an act, then I can go
forward. So the the short answer to what
happens if you never get an act is you
you go up to the point at which the
receiver has enough buffer space and
then stop.
Um, timeout doesn't necessarily it it
might reset a little except for the fact
that if I timeout at this point, I'm
going to keep resending stuff that's in
my send buffer and when it gets to the
receiver, the receiver knows it there is
space for it cuz it's the first slot in
the receiver's buffer queue. So I'm
never going to get past sending packets
one, two, or three until I get one of
them actually act and then I can send
packet four.
So it isn't a full reset on timeout. It
really is a oh, some of the stuff that I
thought that you thought you sent must
not have gotten there cuz I got a
timeout. I'm going to resend things.
Okay, so the difference between timeout
and acknowledgement is timeout is a
resend. Acknowledgement is move forward.
And notice how this window here is
advancing. So once I've got this first
act, now I've got two, three, and four
here are in my sending sliding window
and
at the receiving side, potentially I've
got these guys have came come in here,
but I'm forwarding things up as quickly
as I can and so we're never building up
any buffer space at the destination. I'm
going to show you in a moment what
happens if you do build up at the
destination.
Now, here we go. So TCP windows in
bytes, not packets.
Okay, so if you look
um,
we can think of the space of sequence
numbers now in TCP is not a packet
count, it's a byte count. So what you
can imagine, remember TCP is a stream,
so there's a continuous stream going in.
We have an arbitrary sequence number
that we start at and then we can look at
this space of sequence numbers where
each sequence number represents another
byte in the stream.
Okay? And so we have the set of sequence
numbers representing bytes that have
been sent and already acknowledged. We
have the set of bytes that have been
sent but not acknowledged and the set
bytes that haven't been sent yet. But
this is a continuous stream from the
initial sequence number incrementing by
one each time.
And then at the receiver, we have the
same set of sequence numbers. Okay? And
so we have this side are
parts of the sequence numbers that have
been received and potentially given to
the act to the application.
Here we have ones that have been
received and are being buffered and
these are ones that have not yet been
received yet. So this buffer here in the
middle is the thing that we want to make
sure we never overflow.
Okay? And I'm going to show you how that
works in a moment. Okay?
All right.
Questions?
So we're not acting on packets, we're
acting on bytes. And that means we can
act a whole group of bytes at once by
giving the sequence number of the end of
the bytes.
Let me show you and this is this is
where packets come back into play. But
here's an example
of
the receiver's
um,
receive queue.
This is an acknowledgement that came
back from the receiver to the sender.
Okay, and what it's saying here is we're
on sequence number 100 is the next
sequence number that I'm expecting and
there's 300 bytes worth of space in my
queue.
Okay, and so now we're going to send
a packet in TCP that says here's
sequence number 100. It's got 40 bytes
in it. So that means that after this
packet's received, what I acknowledge is
I'm going to acknowledge 140
is my sequence number because I've
received 40 new bytes from what I had
before and furthermore, notice that what
I'm saying here is that the
the buffer now only has 260 bytes free,
no longer 300. And as I go again, you'll
notice that the number of bytes free
keeps going down. So, what that tells
the sender is that the buffer on the
receiver side is filling up and it's
never going to send out more into the
network than it knows is available. So,
at this point at 210, it knows that um
sequence number 190, it can do another
210 bytes above 190 and be okay.
Okay? Now, here's an example where
something happened to a packet in here,
the one that was sending between
sequence number 190 and 230, and it's
got lost somehow. But, we sent another
one which was sequence number 230 with
size 30 and we got back an
acknowledgement which might not be what
you expected. If you look here, what you
see is the acknowledgement says, "Well,
the the latest most sequential
thing I've received is at sequence
number 190."
Okay? And there's 210 after that that's
available. So, this particular base TCP
protocol acknowledges the sequence
number that represents a solid set of
bytes up to that point and ignores holes
and other things that might have been
received beyond it.
Okay? Now, um
this is useful
if you can imagine because what it
really says is it's yes, it's uh it's
got back a you know, it got back some
data and received some data, but it
doesn't make sense necessarily to
acknowledge this fully yet because uh
it's not useful to anybody in the
streaming protocol. Now, let's look a
little further. You can see that this
continues for a while and we haven't
changed anything about our
acknowledgements. And the reason for
that is we were missing bytes between
190 and 230. And eventually there'll be
a timeout. We're going to retransmit
the missing data.
And if you notice what happened there,
we fully filled in the hole because the
buffer at the receiver is doing the
right thing. And the acknowledgement
that comes back now is, "Oh, I've
received everything up to sequence
number 340 and by the way, I only have
60 left." And so then we can finish this
up, etc. And at some point when we start
feeding these up to the application
because say it did a read of 40 or
30 or whatever it is, then these
acknowledgements will start coming back
and saying, "Oh, here there's more space
in my buffer." So, if you ever wondered
why when you set up a TCP channel and
you start sending data and the other
side freezes and doesn't the other
application isn't absorbing the data,
then the TCP channel will literally shut
down because it knows that there's no
buffer space at the receiver.
All right.
So, um
All right. And at that point basically
we've shut down because we've filled up
all the buffer space and the application
at the receiver side isn't absorbing any
and so the sender is is stopping at that
point. And the way this worked out for
us is
all of the information we need is in
this queue size at a given sequence
number and so that'll allow us to put in
as many bytes into the network as we
want in a way that won't violate this
notion that all the bytes in flight
would fit in buffer space at the
receiver. So, we have enough information
to never violate that. The only other
thing now is to only send enough data
out into the network to try to meet that
round trip time times bandwidth
requirement. It's actually the bandwidth
of the slowest link in the middle.
Uh
and no more because otherwise we'll
start causing congestion.
Okay?
So, here's a question. So, during the
time when the 190 packet's missing,
let's just go back here.
What if the sender sends too many
packets and causes the receiver buffer
to be full since
So, the thing here is it's not going to
send uh
too much It's not going to send 210
bytes past the one it's sent. It's
sending 210 up to 210 bytes past the
190. So, it knows that this is the space
that's free and it's that means it knows
that past 340 it doesn't have
more than 60 here available. So, it's
not going to send anything past what
would fit in this. And it's up to the
receiver to reorder
based on sequence numbers to put things
back in the buffer. Okay? Now, what if
you already go beyond 400 before
retransmit? Again, that's not going to
happen because we are never going to get
the go-ahead to transmit beyond 400
until
the the buffer space opens up here.
Because when we get to
this point, um we will never have sent
beyond 400 because we will know that
that would bring us down to past zero
and so it'll never happen. And it's only
when this opens up again after these
have been absorbed by the client that we
can start sending again.
Good.
So, congestion's an issue. So,
congestion is because we have too much
data flowing through the network.
Okay? And if you look, all of this
different data is all using shared
links. And so IP's solution here is to
drop packets.
And the question might be what happens
to a TCP connection? Well, you end up
with lots of retransmissions. So, if you
drop lots of packets, what you saw there
is you end up with lots of
retransmissions. By the way, I should
say back here on this particular
example, I want you to notice that the
sender knows
where the data was missing because it
knows that it was at sequence number
190. And the moment it sends that
missing data, notice that the
acknowledgement went way all the way up
to where
it's still missing. And so,
at that point the sender is not going to
retransmit the remaining stuff. It's
going to pick up where it left off. And
so, we don't get duplication there.
Okay?
And there are protocols that let you
know more about more holes than one at a
time, but now we won't go into that now.
So, with congestion,
we need to limit congestion. Okay? And
so, why do we get congestion? Well,
there's shared links in the middle and
there's too much data going into the
shared link. And so, whatever router is
at one of these shared points starts
dropping packets. And so, what we really
want to do is we want to back off so
that we don't um send too much data. And
so, we want to back off so that
everybody that's sending together the
rate uh doesn't exceed the rate of the
router and outgoing links.
Okay? And so, that's a congestion
avoidance property. And so, we can
really figure this out like how long
should a timeout be for resending
messages?
Um so, clearly if it's too long, we
waste time if the message is lost. If
it's too short, we retransmit even
though an ack will arrive shortly. So,
we need to be tracking the round trip
time clearly.
But, there's a bit of a stability
problem here. So, if there's more
congestion, then acks are delayed and
you start getting timeouts which send
more traffic which cause even more
congestion and you start um getting this
positive feedback loop that causes
everything to break down.
Okay? And so, you got to be very careful
to choose the sender's window size, not
the receiver, but the sender, how much
data it's going to allow to be
outstanding so as to avoid congestion.
To avoid this positive feedback loop.
And obviously the amount of data the
sender can have outstanding has got to
be less than what's at the receiver so
we don't overflood it, but it's probably
going to be less because we're going to
be trying to match the amount of data we
have in the network with the round trip
time and the bandwidth of the slowest
link in the middle.
So, we're going to try to match the rate
of sending packets with the rate of the
slowest link.
Um there's an adaptive algorithm which
is going to adjust the sender's window
size. And there's a lot of interesting
things a lot of interesting algorithms
that have been developed over the years
to deal with that. I have one up on the
reading for for tonight. The Van
Jacobson paper starts talking about this
a little bit if you're interested. Um
but the basic technique is going to be
I'm going to start small and I'm going
to slowly increase the window
uh until I start getting
acknowledgements missing. So, once I've
got that to be too big, I know that I'm
sending too fast and I'm going to back
off. And that's the basic
way that these adaptive algorithms try
to get enough data in the network to
make maximal use of that slowest link,
but without causing congestion.
Okay? This is called slow start which is
you start sending slowly.
And typically what happens is when you
start receiving um
uh
when you start receiving acks being
lost, then you cut in half and you work
your way up. And so, typically there's a
sawtooth
uh behavior as it's trying to adapt and
figure out what the right amount of data
to be in the network. Um the cool thing
about these kind of adaptive algorithms
is that if a new person comes along all
of a sudden,
the ack the acks will be lost, you'll
start losing packets. Both will back off
until they hit a situation where they're
both equally sharing the link in the
middle. And that's kind of the way these
congestion avoidance algorithms work.
Um and so you can take a look if you
actually measure what TCP does, you get
this uh typical sawtooth behavior around
uh around the right bandwidth for that
middle link.
So, um the question here is aren't acks
more more likely to be timed out with
smaller windows? I'm not sure I fully
understand there. The acks are coming
back in the other direction. Um and the
acks uh are
basically reflective. What's happening
is when you see that the same ack comes
back over and over again, you know that
the data you sent out got lost. And so,
that's that's the notification that the
forward packets have been lost. And
that's the point at which you make some
decision to back off the amount of data
you have in the network.
Okay?
Now,
um so if you recall the setup, remember
this where you request a connection, the
server socket's got to is listening, um
it it takes the connection, it
constructs a new five-tuple style uh
of connection between two sockets, and
then it lets you go. And so um remember
the five-tuple is a source IP address,
destination IP address, source port, uh
destination port, and protocol like TCP.
Um and that setup is really setting up a
TCP channel.
Okay? And so what does that mean? So to
establish, we have to open a connection,
that's a three-way handshake. Then we do
what we've just been talking about,
which is transmitting data back and
forth, and then we tear everything down
when we're done.
Okay? And so here we're back to this
client server, but now let's look at uh
this part, which is the setup.
Okay? And it's really a three-way
handshake. So the client uh so the
server is causing a calling listen over
here. The client calls connect, which
sends a uh
a request over, all right? And it looks
like this. It's a SYN, synchronous uh
bit is set in the header. It it proposes
a sequence number for communication from
client to server. The server
uh accepts the connection, it sends back
um an acknowledgement on that forward
SYN and a new SYN for the other
direction.
Okay? With its proposed sequence number.
All right? And then finally there's an
ACK coming back. So this last ACK is
ACK'ing the server's
uh connection um from server to client.
So it's three uh three messages. And
when you're done, you've both agreed on
a sequence number in the forward
direction, a starting sequence number in
the reverse direction, and you've both
agreed that this is a connection that's
going forward. Okay?
Great.
Um
the other thing is just to show you the
shutdown. So shutdown's actually a a
four uh hop thing here. So when host one
is done, it sends a FIN bit in the
header.
The host ACKs the FIN bit, but it also
reacts the FIN bit, and it sends its own
FIN bit.
Um
So this is a FIN ACK, excuse me. Um and
the remaining data
uh and then eventually it closes things
down with a FIN and you get a FIN ACK on
the other direction. So there's actually
four uh control messages to shut down.
Okay?
And then eventually after a timeout,
everything's deallocated.
So.
All right. And I'm not going to um
not going to go any further on this, but
just like regular files, if you have
multiple file descriptors open on a
socket, then the socket's only really
shut down when all of them close.
Okay.
So how do we actually program a
distributed application? So we need to
synchronize multiple threads on
different machines.
Um so if you remember this is from last
time I was talking about messages.
And so now we've got this idea of
how to build a reliable stream in both
directions. Um and so the question is
now,
what next? Well, suppose we want to
build an application on top of this.
Well, one of the things that comes up is
what's the data representation? So an
object in memory on one side has a very
machine-specific binary representation
that may mean nothing at the other side.
So if you're trying to send data from
host A to host B, and you want it to be
understood on host B,
what are you going to do? Well, you're
going to have to agree on some
standardized way of communicating with
each other.
Okay? And so the absence of shared
memory,
um externalizing an object require us
requires us to basically take an object,
which think of a linked list for a
minute, right? It's a bunch of uh
objects that are all linked together
with addresses and all that sort of
stuff, and we need to serialize that
into bytes so that it can be sent over
the uh over the link. Okay? And the
serializing into bytes and then
marshalling it together into an object
uh the object together into a message
and then sending it off uh is what you
do at the sender side. On the other
side, you unmarshal, so you take it
apart, and you deserialize it back into
a local representation on the other
side. And it's possible that the two
communications are um or excuse me, the
two hosts have different
representations. Like one might be big
endian and the other small endian. I'll
remind you what that is for a moment. So
this serializing and marshalling process
has to be done in a way that allows the
two hosts to communicate no matter what
their representation for various things
are.
Okay? So simple data type, let me just
show you this for instance. Suppose you
got a 32-bit integer, and I want to
write it to a file. So let's back off
from sockets for a second. Um you open
the file, okay, that's all fine and
dandy. And then you have a couple of
choices. One, you could actually print
it out as an integer
in ASCII text. The other is you could
write it as a binary in with four bytes.
Okay? And um
those two things look very different in
the file, and the person the person the
the application that reads it back in uh
needs to know which it is, otherwise
it's not going to be able to interpret
them. Okay? So neither of these two
things are wrong, but the receiver needs
to be consistent.
Okay? And this gets even more tricky
when you're going across the network,
because uh if I'm trying to send uh
you know, a four-byte number, 32 bits,
across the network, uh how do we know
that uh the recipient has X in the same
way? Okay? Like for instance, if you
remember uh from 61C, they talked about
endianness. Like uh several of these
different types of machines uh are big
endian, a number are little endian.
Um and the question is sort of how do we
match those up if we're trying to
communicate.
Okay? Here's a good example of a of a
little endian machine where we take a an
integer uh 0x12345678,
and then we uh we scan through the uh
actually the uh the least significant
byte of the integer is actually in the
first byte in memory. So this is clearly
a little endian machine.
Okay?
And you can write this endianness uh
uh routine on your own and try running
it. Okay? And see what you get.
So what endianness is the internet?
Well, the internet has chosen big endian
as the standardized network byte order.
And so typically what happens is when
you're sending something across the
internet, you actually put network byte
order uh you put things in network byte
order, and then the other side unpacks
them from network byte order into its
local host order.
So um so you have to decide on wire
endianness. We just decided for
instance, if we're talking across the
network, it's typically big endian. And
then we convert from the uh native
endianness to the on-wire format that's
in the source side of the communication,
and then we unpack it on the other side
from the on-wire endianness to the local
format. Um
now,
a downside of this perhaps is the fact
that if you take two little endian
machines, and they communicate over the
network, they're both going to uh
convert and uh convert to and from big
endian to make that communication
happen.
So the question is what's the uh is
there a rationale for big endian versus
little endian on the web, or do you mean
in different processors?
Uh you know, the web
if you're asking why why it was big
endian network byte order,
um
I think the good thing about big endian
is you can look at uh numbers in
if you were to take a hex dump of some
memory, and you look at a big endian
number, you can just read it directly
out. So big endian kind of has that nice
property that it's uh it requires a
little bit less brain gymnastics to read
through a memory dump.
Um
that would be my
uh my only
uh explanation of why that was
preferred. I don't know.
Um I guess at this point, it's all about
standards, and so I I could just say,
well, it is what it is, and we got to
stick with it. But um I think probably
people like big endian cuz you can read
it directly out. Now, I grew up with
little endian processor assembly
language design when I was young er, and
so um
I'm not as thrown for a loop when I see
little endian numbers, cuz I rescrabble
them in my brain, and it mostly works
okay. But anyway, I think that's the
reason people like the big endian, cuz
you don't have to rescrabble.
What about richer objects like
lists and whatever? What do you do?
Well, if you want to transfer a linked
list of structures from point A to point
B, you got to come up with some
standards for serializing that so that
they can be packed and unpacked. And
there's lots of serialization formats.
There's JSON and XML, you name them. In
fact, if you were to Google data
serialization, you'd find a whole bunch
of different types of serialization. So
there are many languages, there are many
serialization formats.
Um
so of course this is a new issue with
standardization. You have to make sure
that when you're using a serialization
mechanism from point A to point B, you
actually do the right thing to uh
to do that serialization.
Okay?
Now, um so
raw messaging, where you just send a
message from one side to another and
then you build something out of it is
pretty low-level for programming.
You have to do a whole bunch of stuff on
your own and you also have to deal with
machine representation by hand and
calling the things we said back there.
The alternative is a remote procedure
call idea which you call a procedure on
a remote machine
and the idea is to make communication
look like an ordinary function call. And
you're going to automate all the
complexity of translating between
representations.
Okay, and so uh for instance, the client
might call uh remote file system read
rutabaga and at the
uh remote side, the thing reads the file
rutabaga and sends the results back and
as far as the client and even the server
is concerned, they're just um executing
a function call and getting a return.
Okay, so that's called a remote
procedure call.
And the concept here is pretty simple.
So here's the client. Um
it wants to execute this function of two
arguments which turns out it's going to
be on a remote machine. What's going to
happen is to call, it's going to go
through what's called a stub which is
going to marshal all of these arguments
V1 and V2 and put it into a standardized
serialization format of some sort, send
it to the receiver. The receiver stub is
going to unpack it,
call the the function on the server
side. Server is going to give a return
value. We're going to go back the other
direction.
Okay, and then return at the client. If
you notice um
really these stubs
are things that are just linked into the
client and the server like regular uh
library function calls and they have
this nice property that when you link
function F with this stub, what really
happens is when you call F, it ends up
sending and receiving messages, okay?
And the server when it links with the
server stub is really going to end up uh
giving its functions to be called by
remote clients and um however when you
write the code inside the server, you're
going to just be writing normal
functions.
Okay, and so this is um
this is basically the uh
this is basically the idea of remote
procedure call. So as far as the
client's concerned, they're making a
procedure call but it's happening
remotely.
Okay, and so really
um we can talk about the client stub
interacting with handlers that send
messages across a network
um on multiple machines and that this is
really a machine machine boundary.
Okay. And really there's also a um
application application boundary. So
we're going to wrap some ports in here
on as well.
Now, can you use RPC for interprocess
communication on the same machine?
Absolutely. Okay, and what's kind of
cool about this, that's a good question,
is really that uh you could start out
with this server on the same machine and
then if the machine got overloaded, you
could migrate the server to another
machine and as long as you clean up the
packet handling stuff so that the
packets are now directed at that remote
machine instead of the local one, you
don't even have to change the code. All
you see is a change in performance.
Okay.
Now, um so the way that this
implementation works in general is
request response mess message passing
under the covers. The stubs on both
sides are providing glue on the client
and server side to glue functions into
the network. So the client stub is
marshalling the arguments and
unmarshalling the return values. Uh
where marshalling is putting into a
packet, taking out of the packet.
They're also responsible for um doing
the data representation serialization we
talked about. The server stub stub does
the the opposite.
Okay, so marshalling involves converting
values to canonical form, serializing
the objects, copying them to
uh be passed by reference, etc.
Um and so
some details here. Um there's an
equivalence
uh really between, you know, the
parameters of the function call uh go
into a request message. The result is a
reply message. The name of the procedure
is typically passed in the request
message
um and is used to decide in the receiver
stub which function gets called. Um
there mailboxes on either side, so you
need to know both the IP address and the
port on each side in order to do this
connection.
The interesting part about this is
there's a stub generator which is really
a compiler that generates stubs. So what
you typically do is you define your RPC
with a um interface definition language
or IDL which contains among other things
the types of arguments, the return
values, etc. The output is going to be
stubs in the appropriate source language
um and
and when you you design your interface
by writing in the IDL and then when you
produce that of the compiler, you now
have code that you can link in at both
the client and the server side and now
you're able to do RPC.
Okay.
Um so the way we deal with class
cross-platform issues is exactly what we
just talked about. We're going to
convert everything to and from a
canonical form and this is where your
particular type of RPC, so there are
many types of RPC out there,
will define as part of it what is the
canonical form or what is the way that
things are serialized. Okay, so that's a
part of the RPC package.
So how does the client know what they're
connecting to?
Um typically you translate just like
with regular DNS and IP, you're
translating the name of the remote
service into a network endpoint,
remote machine, port, maybe some other
information.
And the process of binding is the
process of converting basically a user
visible name for that service like a
file server or something else into a
network endpoint like an IP address and
port
and then um connecting it all up and so
then once you do that, now the client
can just be doing procedure calls and
they're going to the remote machine.
Okay, this is another word for naming
and you could either compile in the
destination machine or you could um
have a dynamic check at runtime. Now the
question is when are the stubs
initialized? So the stubs get linked
into the program and they get
initialized at the um
kind of before you actually start
executing code that has the RPC in it.
So there is this initialization process
which you would call into the RPC
library to do the initialization stuff
and once it's now connected, then you
can make your calls.
So um this dynamic binding uh is good
because most RPCs use dynamic binding
via some name service just like uh if
you were interested in, you know,
www.berkeley.edu,
you go to a dynamic DNS service to find
the current IP address.
Um there's uh most RPC systems have a
dynamic binding service where you say
what service you're interested in,
certain file service of a certain name
and it will figure that out for you
through a binding process and decide
what the actual IP address is and so on,
what the port is.
Why do we do this? One, we can do access
control um to basically not even give
back the names of machines if people are
don't have access. The other uh is
failover. So if the server fails, we can
um
basically failover to another one just
by changing the binding.
Um if there's multiple servers, you can
have flexibility of binding time. So I
mentioned uh last time or the time
before that Google does this a lot. When
you go and do a Google search and you do
it from uh
Northern California versus um I don't
know, Boston, you're going to get
different places for Google. Um in fact,
you're even going to get different times
of the day. You might get different
service server names or the same server
name, different IP addresses from the
Google resolution and what they're doing
is they're balancing load that way. And
so that's why a dynamic RPC service is
good that way as well.
Okay.
I think that's all I wanted to say
there.
So what are some problems with this
idea? So this seems really cool. Um
different failure modes in a distributed
system than on a single machine. So, you
know, think about the number of
different failures. If you're um
maybe a user level bug causes an address
space to crash at the other side or a
machine failure, kernel bug causes all
processors on the same machine to fail
or some machine is compromised by a
malicious party. So in the old before
RPC, what you would end up with is um a
crash is a crash is a crash. Pretty much
everything fails. After RPC, you're now
reaching out to different services on
the network and it could be that you get
partial failures because only some of
them are working.
Okay, now the question here, does RPC
usually run over TCP? Uh it it's uh
either runs over TCP or if it runs over
um UDP which it can occasionally, um
it's got to have its own reliability
protocol underneath to make sure things
work.
So
um it often uh running over TCP is
certainly the simplest thing for it to
do.
So before RPC, the whole system would
crash and die. After
you got partial failures, okay? And so
you end up with an inconsistent view of
the world and um you're not sure if your
cache data got written back or not.
You're not sure if your server did what
you want. And so the handling of failure
gets much more complicated in an RPC
world but you gain the ability to have
uh your services handled from many
places.
Okay, so the problem that RPC is a
solution to, again, is that RPC
basically gives you a nice clean
uh
way of looking at remote communication
just as a file as a system call. Excuse
me, strike that. It basically lets you
look at uh remote communication as a
procedure call
and that procedure call uh you don't
have to worry about marshalling the
arguments. You don't have to to about
serializing. You get the return value
back into your code, is nice and clean.
It looks like a bunch of uh function
calls.
Okay? The downside is you need to make
sure that you uh are able to track
failure modes carefully.
And I will point out, by the way, that
there are a lot of services that use RPC
precisely because of the cleanliness of
its interface and because it's very
easy, as I said, to migrate where the
services are from the local machine to
remote machines without changing any of
the programming. It's just that there
are potentially more complicated failure
modes that you have to be careful about.
And you can do all sorts of interesting
things with distributed transactions and
Byzantine commit and stuff we've already
talked about to make your RPC uh
much more uh much less failure prone.
So, RPC is not performance transparent,
right? So, the cost of a procedure call
is very much less than the cost of same
machine RPC, which is very much less
than network RPC. So, there's overheads
of marshalling and stubs and kernel
crossings and communication that come
into play. So, there is a cost to RPC,
but the transparency of location is a
pretty powerful benefit.
Um and so, while programmers need to be
aware that RPC is not free, it still is
used in a large number of circumstances.
Um and for one thing that I will point
out here is um
now we have a new way for uh
communication between domains. Um we
talked about shared memory with
semaphores and monitors. We talked about
file systems. We talked about pipes. And
now remote procedure calls can be a way
to even do local communication.
Um and so, uh
you can use this communicate between
things on the local machine or remote
machines. And just to give you a few,
there's many RPC systems. Um there's
CORBA,
the common uh object request broker.
There's uh DCOM, which is distributed
com. You'll see that in Windows machines
a lot. There's RMI, which is Java's
remote method invocation. There's a lot
of different ones out there.
Um and one thing I will point out is uh
in the
early
80s, I would say, there was this notion
of microkernels, which we haven't talked
a lot about in this cur uh this term
yet, but um
basically this monolithic kernel that
we've been talking about pretty much
puts all the protected code into uh the
kernel address space. And applications
run on top of that and they make system
calls into the kernel.
The microkernel is a little different.
The only thing that's in the kernel
itself is uh thread
uh multiplexing, address space control,
and an RPC service. And so, in addition
to regular applications, all of these
things that we used to think belonged
inside the kernel, we now put as
processes running on top of the
microkernel and using RPC to communicate
with one another. And so, if the
application goes to read a file, what
happens is it doesn't open by doing an
RPC into the microkernel, which then um
talks with the file system. That file
system does the open, sends back a
handle to the application, etc. And so,
the application is reading and writing
from the file system, but doing so uh
basically through an RPC mechanism to
other user level uh processes. Okay? And
why do this? Well, um fault isolation.
So, if there's a bug in the file system,
it won't crash the whole the whole um
microkernel, right? It's only going to
crash part of what's going on. Or if
there's a bug in the windowing system.
Okay? Or other parts of the kernel, we
basically have isolated the ability of
faults to propagate because we we
isolate them in their own user level
address space and we use RPC back and
forth.
Okay? And it enforces a level of
modularity as well.
Okay?
So, this is a good example of using RPC
on local machine to
uh to help with the overall structure in
the kernel.
Okay?
All right. Now, if you'll bear with me
for just uh one or two more slides, I
want to set the stage for what we'll
talk about on Monday. Um once we've got
a good messaging service and a good way
to do uh
you know
uh
serialization and deserialization across
the network, we can now start talking
about how to build distributed storage.
And the basic distributed storage
problem is the following. We have a
network with a lot of storage in it, so
you guys can start thinking about all
the cloud storage that you have out
there. And we have a series of clients
that are all using that storage. And
the we can start asking some interesting
questions about this. So, first of all,
why bother with this? Well, this is the
ultimate sharing scenario because these
clients can be using that data that's in
the middle of the network, no matter
where they are. So, they could be in the
the West Coast here using some data and
then they get an airplane and uh
hopefully are careful with their social
distancing and their masks and they get
on the East Coast and now they can read
their same data or they can can be
traveling and their data can be read and
written while it's going. And so, this
idea of network attached storage is a
very powerful one.
Okay? But it's a little different than
the type type of file systems that we've
talked to in this term about in this
term so far. So, among other things,
there's a what's
colloquially called the CAP theorem.
Okay, this was from Eric Brewer in the
early 2000s. And the idea is that there
can only be three There are three ideas,
okay? Consistency, availability, and
partition tolerance. And you can only
have two of them at a time in any real
system. So, what consistency means is
that changes to a file or a data
uh base or whatever appear to the same
uh to everybody in the same serial
order.
That's consistency. Availability says
you can get a result at any time. And
partition tolerance says that the system
will keep working even when the network
gets split in half.
Okay? And the problem that you encounter
when you have a distributed system like
uh
distributed network storage is you start
worrying about partition tolerance.
Uh you know, what happens if the network
is split? And you know, if you are going
to be able to keep going while the
network is split, then you're going to
lose one of consistency or availability.
Okay? So, you can't have all three at
the same time.
This is also otherwise known as Brewer's
theorem. Um so, you can think pretty
easily think about this for a moment.
So, suppose that I want to um always
have availability, so I can always use
my file system, and I want to be able to
deal with partitions when the network is
split.
You can see why consistency might not
work, right? Cuz if I split the network
in half and these clients over here are
busy writing data and these clients over
here are busy writing data, then I'm not
really getting consistency because the
file system's not consistent. It's got
two different views of it on different
coasts. Okay? So, that's one example of
being able to only have two things. Um
if I want to have consistency and
partition tolerance, for instance, I
want to be able to make sure I always
see a consistent view, but I can deal
with partitions in the middle. Can
anybody explain to me why I lose out on
availability when I do that?
Why would I lose out on availability?
Yep. The reason I lose out on
availability is because to be consistent
and deal with splits in the network,
then I can't write anymore and so, it's
no longer available to write because I
can't allow there to be an inconsistent
view. Very good. All right. So, we're
going to pursue this next Monday on our
last official class. We're going to talk
a lot about distributed storage
solutions like the NSF and
NFS and AFS. We'll talk about key-value
stores and and probably in the final
lecture on Wednesday of a week next
week, um which won't be responsible for
on the exam, but we'll talk about uh
things like
Chord and CAN and um some of the other
um distributed storage systems out
there. All right. So, in conclusion, we
talked a lot about TCP, which is a
reliable byte stream between two
processes on different machines over the
internet. So, you get basically a stream
and it doesn't matter whether it's local
or remote, you get the same uh view of
it. And we talked about how to use
acknowledgements with window space
acknowledgement protocol and congestion
avoidance to make sure that um this
works well and represents good
citizenship. We talked about remote
procedure calls, which is how to call a
procedure on a remote machine or in a
remote domain
and uh give us the same interface as
procedures, but uh remote. Okay?
Um we started talking about the
distributed file system and the CAP
theorem. Okay? And next time we're going
to talk about uh virtual file system
layer and cache consistency and how we
can basically build a file system into
the network. Um
All right. I'm going to end there. I
hope everybody has a great Thanksgiving.
Uh we will see you
a week from today back on Monday.
And uh hope everybody gets a little bit
of a break and enjoys themselves.