[关闭]
@Dale-Lin 2023-03-24T11:59:26.000000Z 字数 5077 阅读 200

Transport

计算机网络


Transport 层协议为运行在不同主机上的应用协议提供了逻辑通信功能。

运输层协议是在端系统中实现的(不是路由器)。在发送端,运输层将从应用程序进程接收到的报文转换成运输层报文段(segment)。实现的方法是把 APP 报文划分为较小的块,并为每个块加上一个运输层首部生成 segment。然后,在发送端系统中,运输层将这些报文段传递给 Network 层,网络层将其封装成网络层分组(datagram)。

每个进程具有一个或多个套接字,当生成一个套接字时,就为他分配一个端口号。发送进程需要为分组附上目的地址,通常是目的 IP 和目的端口号。此外,通常还需要附带上源 IP 和源端口号。然而,将源地址附加到分组上通常不是由应用程序代码所为,而是由底层的操作系统自动完成的。

下面使用简单的客户-服务器App来演示 UDP 和 TCP 的套接字编程,实现以下功能:

  1. 客户从其键盘读取一行字符并发送给服务器。
  2. 服务器接收这些字符并转成大写。
  3. 服务器将修改的数据发送给客户。
  4. 客户接收修改的数据并显示。

UDP套接字编程

UDP 通信的服务器端

需要实现:

  1. 在 serverIP 上创建 UDP 套接字,port = x;serverSocket = socket(AF_INEL, SOCK_DGRAM)
  2. 从套接字 serverSocket 接收到 UDP 报文段。
  3. 向 serverSocket 写响应,指定客户 IP 和客户端口号。

UDPServer.py

  1. from socket import *
  2. serverPort = 12000
  3. serverSocket = socket(AF_INET, SOCK_DGRAM)
  4. serverSocket.bind(('', serverPort))
  5. print("The server is ready to receive")
  6. while True:
  7. message, clientAddress = serverSocket.recvfrom(2048)
  8. modifiedMessage = message.decode().upper()
  9. serverSocket.sendto(modifiedMessage.encode(), clientAddress)

注意到创建套接字类型是 SOCK_DGRAM(一种 UDP 套接字);且使用 AF_INET 表示底层网络使用了 IPv4。

使用 while 循环来使 UDPServer 无限期接收并处理来自客户端的分组。

UDP 通信的客户端

需要实现:

  1. 创建 UDP 套接字,clientSocket = socket(AF_INET, SOCKET_DGRAM)
  2. 创建具有目的地址 serverIP 和 port = x 的数据报,通过 clientSocket 发送。
  3. 从 clientSocket 读取数据报
  4. 关闭 clientSocket

UDPClient.py

  1. from socket import *
  2. serverName = 'hostname'
  3. serverPort = 12000
  4. clientSocket = socket(AF_INET, SOCK_DGRAM)
  5. message = raw_input('Input lowercase sentence;')
  6. clientSocket.sendto(message.encode(), (serverName, serverPort))
  7. modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
  8. print(modifiedMessage.decode())
  9. clientSocket.close()

第一行将变量 serverName 设置为字符串 "hostname",这里需要提供的是服务器的 IP 地址(如"128.138.32.126")或包含服务器的主机名(如"cis.poly.edu")的字符串。如果使用主机名,则自动执行 DNS lookup 从而获得 IP 地址。

创建客户的套接字时并没有手动指定端口,而是让操作系统来选择。

通过 UDP 客户套接字发送消息时,需要每次指定目的 IP 和目的端口。

TCP套接字编程

与 UDP 不同,TCP 是一个面向连接的协议。意味着在客户和服务器开始互相发送数据之前,它们需要先握手和创建一个 TCP 连接。

TCP 连接一端和客户套接字连接,另一端和服务器套接字连接。

一端使用创建的 TCP 连接互相发送消息时,只需要将分组丢进 TCP 连接即可。与 UDP 每次都需要指定目的地址不同。

在服务器进程运行的情况下,客户进程可以向服务器发起一个 TCP 连接,这是由客户程序通过创建一个 TCP 套接字完成的。当客户生成其 TCP 套接字时,它指定了服务器中的 listenSocket(监听套接字)的地址(IP + port);客户生成 TCP 套接字后,客户和 listenSocket 发起一个三次握手,随后创建和服务器的 connectionSocket 的一个 TCP 连接。

生成 TCP 套接字时——指定 server IP & listenSocket;
生成 TCP 套接字后——发生运输层的三次握手, TCP 连接创建。

三次握手是客户进程向 server 的 listenSocket 发起的。随后服务器会生成一个新的套接字 connectionSocket(连接套接字)用于通信。connectionSocket 是服务器专为客户进行连接所使用的套接字,TCP 连接的两端是客户 TCP Socket 和服务器 connectionSocket。

TcpClient.py

  1. from socket import *
  2. serverName = 'servername'
  3. serverPort = 12000
  4. clientSocket = socket(AF_INET, SOCK_STREAM)
  5. clientSocket.connect((serverName, serverPort))
  6. sentence = raw_input('Input lowercase sentence')
  7. clientSocket.send(sentence.encode())
  8. modifiedSentence = clientSocket.recv(1024)
  9. print('From Server: ', modifiedSentence.decode())
  10. clientSocket.close()

clientSocket 创建时 clientSocket = socket(AF_INET, SOCK_STREAM) 表示底层网络仍使用 IPv4(AF_INET),但使用 TCP 套接字(SOCK_STREAM)。

仍没有指定客户端口,让操作系统选择。

clientSocket.connect((serverName, serverPort)) 发起客户和服务器之间的 TCP 连接。connect() 方法的参数是这条连接中服务器端的地址(IP + listenSocket)。这行代码执行后,会进行三次握手,并在客户和服务器之间创建起一条 TCP 连接。

TCPServer.py

  1. from socket import *
  2. serverPort = 12000
  3. serverSocket = socket(AF_INET, SOCK_STREAM)
  4. serverSocket.bind(('', serverPort))
  5. serverSocket.listen(1)
  6. print('The server is ready to receive')
  7. while True:
  8. connectionSocket, addr = serverSocket.accept()
  9. sentence = connectionSocket.recv(1024).decode()
  10. capitalizedSentence = sentence.upper()
  11. connectionSocket.send(capitalizedSentence.encode())
  12. connectionSocket.close()

和 UDPServer 类似,serverSocket.bind(('', serverPort)) 将 serverSocket 和端口号 serverPort 关联起来。

serverSocket 即是 listenSocket。

serverSocket.listen(1) 表示让服务器监听来自客户的 TCP 请求。其中参数定义了请求连接的最大数(至少为1)。

当客户消息到来时,程序调用 accept() 方法。connectionSocket, addr = serverSocket.accept() 创建了一个 connectionSocket 的新套接字,由该特定的客户专用。此时客户和服务器则完成了握手,建立了一个 TCP 连接。

运输层服务

因特网网络层协议 IP 是一种尽力交付服务模型。不保证报文的交付,不保证报文的按序交付,不保证报文数据的完整性。因此,IP 被称为不可靠服务。

UDP 和 TCP 最基本的职责是,将两个端系统间 IP 的交付服务扩展成运行在端系统上的两个进程间的交付服务。这种把主机间的交付服务扩展到进程间的交付服务的行为,称为运输层的多路复用和多路分解。UDP 和 TCP 还能通过报文首部提供报文的完整性检查。

报文的完整性检查和进程和进程间的数据交付也是 UDP 仅能提供的两种服务。UDP 也是一种不可靠服务。

TCP 为进程提供了附加服务:

多路复用和多路分解

计算机同时允许多个网络应用进程运行,每个进程有一个或多个套接字。接收主机的运输层实际上是把数据交付给对应的套接字。

为了把运输层报文定向到合适的套接字,运输层报文段中有几个首部字段用来做此标识。这个过程称为多路分解(demultiplexing)。

源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(用于分解)生成报文段,然后将报文段传递到网络层。这个过程称为多路复用(multiplexing)。

运输层多路复用的要求有:

  1. 套接字有唯一标识符;
  2. 每个报文段有特殊字段来指示该报文要交付的套接字。

这些标识称为端口号,包括源端口号字段(source port number field)和目的端口号字段(destination port number field)。

端口号是一个 16 比特的数,大小在 0~65535 之间。0~1023 范围的端口号称为周知端口号(well-known port number),是受限制的,这是指它们被保留给诸如 HTTP 和 FTP 之类的周知应用层协议来使用。

运输层报文段中首部有源端口号字段和目的端口号字段

周知端口号列表

当我们开发一个应用程序时,必须为其分配一个端口号。如果是一个周知协议的服务器端,那么开发者就必须为其分配一个相应的周知端口号。

运输层实现多路分解的方式:在主机上的每个套接字分配了一个端口号,当报文到达主机时,运输层检查报文中的目的端口号,并将其定位到对应的套接字。然后报文段的数据通过套接字进入所连接的进程。

无连接服务的多路复用和多路分解

一个 UDP 套接字是由一个二元组标识的,二元组包含一个目的 IP 地址和目的端口号。因此如果两个 UDP 报文段有不同的源 IP 地址或源端口号,但具有相同的目的 IP 地址和目的端口号,这两个报文段将通过相同的目的套接字定向到相同的目的进程。

源端口号和源 IP 地址作为返回地址,当需要回发一个报文段时使用。

面向连接的多路分解和多路复用

TCP 套接字和 UDP 套接字的一个区别是,TCP 套接字是由一个四元组(源 IP 地址,源端口号,目的 IP 地址,目的端口号)来标识的。因此,当一个 TCP 报文段从网络到达一台主机时,该主机使用全部 4 个值来定向(分解)到相应的目的套接字。除非 TCP 报文段携带了初始创建连接的请求,否则任意一个元组不同,将会被定向到不同的目的套接字。

Web服务器和TCP

考虑一台运行 Web 服务器的主机,例如在 80 端口上运行一个 Apache Web 服务器。当客户向该服务器发送报文段时,所有报文段的目的端口都是 80,特别是初始建立连接报文段和承载 HTTP 请求的报文段。服务器能够根据源 IP 和源端口号区分来自不同客户的报文段。

前面说一台 Web 服务器为每一条连接生成一个新的进程,这样每个进程都有自己的连接套接字,通过这些套接字接收 HTTP 请求和发送 HTTP 响应。事实上,高性能服务器通常只使用一个进程,但是为每个客户连接创建了一个具有新连接套接字的新线程。对于这样一个服务器,在任意时刻都可能有不同标识的连接套接字连接到相同的进程

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注