TLPI读书笔记第56章-SOCKET介绍2
流 socket 的运作与电话系统类似。
1.socket()系统调用将会创建一个 socket,这等价于安装一个电话。为使两个应用程序能够通信,每个应用程序都必须要创建一个 socket。
2.通过一个流 socket 通信类似于一个电话呼叫。 一个应用程序在进行通信之前必须要将其 socket 连接到另一个应用程序的 socket 上。两个 socket 的连接过程如下。 (a) 一个应用程序调用 bind()以将 socket 绑定到一个众所周知的地址上,然后调用listen()通知内核它接受接入连接的意愿。这一步类似于已经有了一个为众人所知的电话号码并确保打开了电话,这样人们就可以打进电话了。 (b) 其他应用程序通过调用 connect()建立连接,同时指定需连接的 socket 的地址。这类似于拨某人的电话号码。 (c) 调用 listen()的应用程序使用 accept()接受连接。这类似于在电话响起时拿起电话。如果在对等应用程序调用 connect()之前执行了 accept(),那么 accept()就会阻塞(“等待电话”)。
3.一旦建立了一个连接之后就可以在应用程序之间(类似于两路电话会话)进行双向数据传输直到其中一个使用 close()关闭连接为止。通信是通过传统的 read()和 write()系统调用或通过一些提供了额外功能的 socket 特定的系统调用(如 send()和 recv())来完成的。 图 56-1 演示了如何在流 socket 上使用这些系统调用。
主动和被动 socket
流 socket 通常可以分为主动和被动两种。
1.在默认情况下, 使用 socket()创建的 socket 是主动的。 一个主动的 socket 可用在 connect()调用中来建立一个到一个被动 socket 的连接。这种行为被称为执行一个主动的打开。 2.一个被动 socket(也被称为监听 socket)是一个通过调用 listen()以被标记成允许接入连接的 socket。接受一个接入连接通常被称为执行一个被动的打开。 在大多数使用流 socket 的应用程序中,服务器会执行被动式打开,而客户端会执行主动式打开。在后面的小节中将会假设这种场景,因此不会再说“执行主动 socket 打开的应用程序”,而是直接说“客户端”。类似地, “服务器”等价于“执行被动 socket 打开的应用程序
56.5.1 监听接入连接: listen()
listen()系统调用将文件描述符sockfd 引用的流 socket 标记为被动。这个 socket 后面会被用来接受来自其他(主动的) socket 的连接。
无法在一个已连接的 socket(即已经成功执行 connect()的 socket 或由 accept()调用返回的socket)上执行listen()。 要理解backlog参数的用途首先需要注意到客户端可能会在服务器调用 accept()之前调用connect()。这种情况是有可能会发生的,如服务器可能正忙于处理其他客户端。这将会产生一个未决的连接,如图 56-2 所示。
内核必须要记录所有未决的连接请求的相关信息,这样后续的 accept()就能够处理这些请求了。 backlog 参数允许限制这种未决连接的数量。在这个限制之内的连接请求会立即成功。 之外的连接请求就会阻塞直到一个未决的连接被接受(通过 accept()),并从未决连接队列删除为止。
SUSv3 允许一个实现为 backlog 的可取值规定一个上限并允许一个实现静默地将 backlog 值向下舍入到这个限制值。 SUSv3 规定实现应该通过在<sys/socket.h>中定义 SOMAXCONN常量来发布这个限制。在 Linux 上,这个常量的值被定义成了 128。从内核 2.4.25 起, Linux允许在运行时通过 Linux 的/proc/sys/net/core/somaxconn 文件来调整这个限制。(在早期的内核版本中, SOMAXCONN 限制是不可变的。 )
56.5.2 接受连接: accept()
accept()系统调用在文件描述符 sockfd 引用的监听流 socket 上接受一个接入连接。如果在调用 accept()时不存在未决的连接,那么调用就会阻塞直到有连接请求到达为止。
理解 accept()的关键点是它会创建一个新 socket, 并且正是这个新 socket 会与执行 connect()的对等 socket 进行连接。
accept()调用返回的函数结果是已连接的 socket 的文件描述符。监听socket( sockfd)会保持打开状态,并且可以被用来接受后续的连接。
一个典型的服务器应用程序会创建一个监听 socket,将其绑定到一个众所周知的地址上,然后通过接受该 socket 上的连接来处理所有客户端的请求。 传入 accept()的剩余参数会返回对端 socket 的地址。 addr 参数指向了一个用来返回 socket地址的结构。这个参数的类型取决于 socket domain(与 bind()一样)。 addrlen 参数是一个值-结果参数。它指向一个整数,在调用被执行之前必须要将这个整数初始化为 addr 指向的缓冲区的大小,这样内核就知道有多少空间可用于返回 socket 地址了。 当 accept()返回之后,这个整数会被设置成实际被复制进缓冲区中的数据的字节数。 如果不关心对等 socket 的地址,那么可以将 addr 和 addrlen 分别指定为 NULL 和 0。
56.5.3 连接到对等 socket: connect()
connect()系统调用将文件描述符 sockfd 引用的主动 socket 连接到地址通过 addr 和 addrlen指定的服务端监听 socket 上。
addr 和 addrlen 参数的指定方式与 bind()调用中对应参数的指定方式相同。如果 connect()失败并且希望重新进行连接,那么 SUSv3 规定完成这个任务的可移植的方法是关闭这个 socket,创建一个新 socket,在该新 socket 上重新进行连接。
56.5.4 流 socket I/O
一对连接的流 socket 在两个端点之间提供了一个双向通信信道,图 56-3 给出了 UNIXdomain 的情形。
图 56-3: UNIX domain 流 socket 提供了一个双向通信信道 连接流 socket 上 I/O 的语义与管道上 I/O 的语义类似。 1.要执行 I/O 需要使用 read()和 write()系统调用(或在 61.3 节中描述的 socket 特有的 send()和 recv()调用)。由于 socket 是双向的,因此在连接的两端都可以使用这两个调用。 2.一个 socket 可以使用 close()系统调用来关闭或在应用程序终止之后关闭。之后当对等应用程序试图从连接的另一端读取数据时将会收到文件结束(当所有缓冲数据都被读取之后)。 如果对等应用程序试图向其 socket 写入数据, 那么它就会收到一个 SIGPIPE信号,并且系统调用会返回 EPIPE 错误。在 44.2 节中曾提及过处理这种情况的常见方式是忽略 SIGPIPE 信号并通过 EPIPE 错误找出被关闭的连接。
56.5.5 连接终止: close()
终止一个流 socket 连接的常见方式是调用 close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。 假设在关闭一个连接之后,对等应用程序崩溃或没有读取或错误处理了之前发送给它的数据。在这种情况下就无法知道已经发生了一个错误。如果需要确保数据被成功地读取和处理,那么就必须要在应用程序中构建某种确认协议。这通常由一个从对等应用程序传过来的显式的确认消息构成。 在 61.2 节将会描述 shutdown()系统调用,它为如何关闭一个流 socket 连接提供了更加精细的控制。
56.6 数据报 socket
数据报 socket 的运作类似于邮政系统。
1.socket()系统调用等价于创建一个邮箱。 (这里假设一个系统与一些国家的农村中的邮政服务类似,取信和送信都是在邮箱中发生的。 )所有需要发送和接收数据报的应用程序都需要使用 socket()创建一个数据报 socket。
2.为允许另一个应用程序发送其数据报(信),一个应用程序需要使用 bind()将其 socket 绑定到一个众所周知的地址上。一般来讲,一个服务器会将其 socket 绑定到一个众所周知的地址上,而一个客户端会通过向该地址发送一个数据报来发起通信。 (在一些 domain中——特别是 UNIX domain——客户端如果想要接受服务器发送来的数据报的话可能还需要使用 bind()将一个地址赋给其 socket。 )
3.要发送一个数据报,一个应用程序需要调用 sendto(),它接收的其中一个参数是数据报发送到的 socket 的地址。这类似于将收信人的地址写到信件上并投递这封信。
4.为接收一个数据报,一个应用程序需要调用 recvfrom(),它在没有数据报到达时会阻塞。由于 recvfrom()允许获取发送者的地址,因此可以在需要的时候发送一个响应。(这在发送者的 socket 没有绑定到一个众所周知的地址上时是有用的,客户端通常是会碰到这种情况。)这里对这个比喻做了一点延伸,因为已投递的信件上是无需标记上发送者的地址的。
5.当不再需要 socket 时,应用程序需要使用 close()关闭 socket。
与邮政系统一样,当从一个地址向另一个地址发送多个数据报(信)时是无法保证它们按照被发送的顺序到达的,甚至还无法保证它们都能够到达。数据报还新增了邮政系统所不具备的一个特点:由于底层的联网协议有时候会重新传输一个数据包,因此同样的数据包可能会多次到达。 图 56-4 演示了数据报 socket 相关系统调用的使用。
56.6.1 交换数据报: recvfrom 和 sendto()
recvfrom()和 sendto()系统调用在一个数据报 socket 上接收和发送数据报。
这两个系统调用的返回值和前三个参数与 read()和 write()中的返回值和相应参数是一样的。 第四个参数 flags 是一个位掩码, 它控制着了 socket 特定的 I/O 特性。 在 61.3 节中介绍 recv()和 send(系统调用时将对这些特性进行介绍。 如果无需使用其中任何一种特性, 那么可以将 flags指定为 0。 src_addr 和 addrlen 参数被用来获取或指定与之通信的对等 socket 的地址。 对于 recvfrom()来讲, src_addr 和 addrlen 参数会返回用来发送数据报的远程 socket 的地址。(这些参数类似于 accept()中的 addr 和 addrlen 参数,它们返回已连接的对等 socket 的地址。 ) src_addr 参数是一个指针,它指向了一个与通信 domain 匹配的地址结构。与 accept()一样,addrlen 是一个值-结果参数。在调用之前应该将 addrlen 初始化为 src_addr 指向的结构的大小;在返回之后,它包含了实际写入这个结构的字节数。 如果不关心发送者的地址,那么可以将 src_addr 和 addrlen 都指定为 NULL。在这种情况下, recvfrom()等价于使用 recv()来接收一个数据报。也可以使用 read()来读取一个数据报,这等价于在使用 recv()时将 flags 参数指定为 0。 不管 length 的参数值是什么, recvfrom()只会从一个数据报 socket 中读取一条消息。如果消息的大小超过了 length 字节,那么消息会被静默地截断为 length 字节。
对于 sendto()来讲, dest_addr 和 addrlen 参数指定了数据报发送到的 socket。这些参数的使用方式与connect()中相应参数的使用方式是一样的。 dest_addr 参数是一个与通信 domain 匹配的地址结构,它会被初始化成目标 socket 的地址。 addrlen 参数指定了 addr 的大小。
56.6.2 在数据报 socket 上使用 connect()
尽管数据报 socket 是无连接的,但在数据报 socket 上应用 connect()系统调用仍然是起作用的。在数据报 socket 上调用 connect()会导致内核记录这个 socket 的对等 socket 的地址。
术语已连接的数据报 socket 就是指此种 socket。术语非连接的数据报 socket 是指那些没有调用connect()的数据报 socket(即新数据报 socket 的默认行为)。 当一个数据报 socket 已连接之后:
1.数据报的发送可在 socket 上使用 write()(或 send())来完成并且会自动被发送到同样的对等 socket 上。与 sendto()一样,每个 write()调用会发送一个独立的数据报;
2.在这个 socket 上只能读取由对等 socket 发送的数据报。
注意 connect()的作用对数据报 socket 是不对称的。上面的论断只适用于调用了 connect()数据报 socket,并不适用于它连接的远程 socket(除非对等应用程序在其 socket 上也调用了connect())。
通过再发起一个 connect()调用可以修改一个已连接的数据报 socket 的对等 socket。此外,通过指定一个地址族(如 UNIX domain 中的 sun_family 字段)为 AF_UNSPEC 的地址结构还可以解除对等关联关系。但需要注意的是,其他很多 UNIX 实现并不支持将 AF_UNSPEC 用于这种用途。
为一个数据报 socket 设置一个对等 socket,这种做法的一个明显优势是在该 socket 上传输数据时可以使用更简单的 I/O 系统调用,即无需使用指定了 dest_addr 和 addrlen 参数的sendto(),而只需要使用 write()即可。设置一个对等 socket 主要对那些需要向单个对等 socket(通常是某种数据报客户端)发送多个数据报的应用程序是比较有用的。