socket编程(一):
socket介绍
1、概述
socket 是一种 IPC 方法,它允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据。
1.1 通信domain
socket存在于一个通信 domain 中,它确定:
- 识别出一个 socket 的方法(即 socket“地址”的格式);
- 通信范围(即是在位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间)。
现代操作系统至少支持下列 domain。
- UNIX (AF_UNIX) domain 允许在同一主机上的应用程序之间进行通信。( POSIX.1g 使用名称 AF_LOCAL 作为 AF_UNIX 的同义词 )
- IPv4 (AF_INET) domain 允许在使用因特网协议第 4 版( IPv4)网络连接起来的主机上的应用程序之间进行通信。
- IPv6 (AF_INET6) domain 允许在使用因特网协议第 6 版(IPv6)网络连接起来的主机上的应用程序之间进行通信。
在一些代码中可能会看到名称诸如 PF_UNIX 而不是AF_UNIX 的常量,AF 表示“地址族( address family)”, PF 表示“协议族( protocol family)”。在一开始的时候,设计人员相信单个协议族可以支持多个地址族。但在实践中,没有哪一个协议族能够支持多个已经被定义的地址族, 并且所有既有实现都将 PF_常量定义成对应的 AF_常量的同义词。
Domain | 执行的通信 | 应用程序间的通信 | 地址格式 | 地址结构 |
---|---|---|---|---|
AF_UNIX | 内核中 | 同一主机 | 路径名 | sockaddr_un |
AF_INET | 通过 IPv4 | 通过 IPv4 网络连接起来的主机 | 32 位 IPv4 地址+16 位端口号 | sockaddr_in |
AF_INET6 | 通过 IPv6 | 通过 IPv6 网络连接起来的主机 | 128 位 IPv6 地址+16 位端口号 | sockaddr_in6 |
1.2 socket类型
每个 socket 实现都至少提供了两种 socket:流和数据报。这两种 socket 类型在 UNIX 和Internet domain 中都得到了支持
属性 | socket 类型 | |
---|---|---|
流 | 数据报 | |
可靠地递送? | 是 | 否 |
消息边界保留? | 否 | 是 |
面向连接? | 是 | 否 |
- 可靠的:表示可以保证发送者传输的数据会完整无缺地到达接收应用程序(假设网络链接和接收者都不会崩溃)或收到一个传输失败的通知。
- 双向的:表示数据可以在两个 socket 之间的任意方向上传输。
- 字节流:表示与管道一样不存在消息边界的概念。
一个流 socket 类似于使用一对允许在两个应用程序之间进行双向通信的管道,它们之间的差别在于( Internet domain) socket 允许在网络上进行通信。
数据报 socket( SOCK_DGRAM)允许数据以被称为数据报的消息的形式进行交换。在数据报 socket 中,消息边界得到了保留,但数据传输是不可靠的。消息的到达可能是无序的、重复的或者根本就无法到达。
1.3 socket系统调用
关键的 socket 系统调用包括以下几种。
- socket()系统调用创建一个新 socket。
- bind()系统调用将一个 socket 绑定到一个地址上。通常,服务器需要使用这个调用来将其 socket绑定到一个众所周知的地址上使得客户端能够定位到该 socket 上。
- listen()系统调用允许一个流 socket 接受来自其他 socket 的接入连接。
- accept()系统调用在一个监听流 socket 上接受来自一个对等应用程序的连接, 并可选地返回对等 socket 的地址。
- connect()系统调用建立与另一个 socket 之间的连接。
socket I/O 可以使用传统的 read()和 write()系统调用或使用一组 socket 特有的系统调用(如send()、 recv()、 sendto()以及 recvfrom())来完成。在默认情况下,这些系统调用在 I/O 操作无法被立即完成时会阻塞。
2、创建一个socket
socket()系统调用创建一个新的socket。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain 参数指定了 socket 的通信 domain。 type 参数指定了 socket 类型。创建流 socket 时会被指定为SOCK_STREAM,创建数据报 socket 时会被指定为SOCK_DGRAM。
protocol 当创建SOCK_STREAM时值为IPPROTO_TCP,在创建SOCK_DGRAM时值为IPPROTO_UDP。也可以直接指定为0,系统会自动推演出应该使用什么协议。一般 socket 类型中总会被指定为 0。在一些 socket 类型中会使用非零进行描述。
socket()在成功时返回一个引用在后续系统调用中会用到的新创建的 socket 的文件描述符。
3、将socket绑定到地址
bind()系统调用将一个 socket 绑定到一个地址上。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd 参数是在上一个 socket()调用中获得的文件描述符。
addr 参数是一个指针,它指向了一个指定该 socket 绑定到的地址的结构。传入这个参数的结构的类型取决于 socket domain。
addrlen 参数指定了地址结构的大小。
3、通用socket地址结构
每种 socket domain 都使用了不同的地址格式。如 UNIX domain socket 使用路径名,而Internet domain socket 使用了 IP 地址和端口号。sockaddr 结构通常被定义成如下所示的结构,来满足任意类型的地址结构。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
4、流socket
一个应用程序调用 bind()以将 socket 绑定到一个地址上,然后调用listen()通知内核它接受接入连接。使用 accept()接受连接。
其他应用程序通过调用 connect()建立连接,同时指定需连接的 socket 的地址。
如果在对等应用程序调用 connect()之前执行了 accept(),那么 accept()就会阻塞。
一旦建立了一个连接之后就可以在应用程序之间进行双向数据传输直到其中一个使用 close()关闭连接为止。通信是通过传统的 read()和 write()系统调用或通过一些提供了额外功能的 socket 特定的系统调用(如 send()和 recv())来完成的。
主动和被动socket
- 在默认情况下, 使用 socket()创建的socket 是主动的。一个主动的 socket 可用在connect()调用中来建立一个到一个被动 socket 的连接。这种行为被称为执行一个主动的打开。
- 一个被动 socket(也被称为监听 socket)是一个通过调用 listen()以被标记成允许接入连接的socket。接受一个接入连接通常被称为执行一个被动的打开。
4.1 监听接入连接
listen()系统调用将文件描述符 sockfd 引用的流 socket 标记为被动。这个 socket 后面会被用来接受来自其他(主动的) socket 的连接。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd为需要进入监听状态的套接字,backlog 为请求队列的最大长度。
无法在一个已连接的 socket(即已经成功执行 connect()的 socket 或由 accept()调用返回的socket)上执行 listen()。
客户端可能会在服务器调用 accept()之前调用connect()。如服务器可能正忙于处理其他客户端。这将会产生一个未决的连接,如下图:
内核必须要记录所有未决的连接请求的相关信息,这样后续的 accept()就能够处理这些请求了。backlog 参数允许限制这种未决连接的数量。在这个限制之内的连接请求会立即成功。 之外的连接请求就会阻塞直到一个未决的连接被接受(通过 accept()),并从未决连接队列删除为止。
这里怎么理解呢:就是当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
4.2 接受连接
accept()系统调用在文件描述符 sockfd 引用的监听流 socket 上接受一个接入连接。如果在调用accept()时不存在未决的连接,那么调用就会阻塞直到有连接请求到达为止。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
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。
4.3 连接到对等socket
connect()系统调用将文件描述符 sockfd 引用的主动 socket 连接到地址通过 addr 和 addrlen指定的监听 socket 上。在主动socket或者客户端使用。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr 和 addrlen 参数的指定方式与 bind()调用中对应参数的指定方式相同。
4.4 流socket I/O
一对连接的流 socket 在两个端点之间提供了一个双向通信信道。
要执行 I/O 需要使用 read()和 write()系统调用,或使用socket 特有的 send()和 recv()调用。由于 socket 是双向的,因此在连接的两端都可以使用这两个调用。
4.5 连接终止
终止一个流 socket 连接的常见方式是调用close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。
当对等应用程序试图从连接的另一端读取数据时将会收到文件结束(当所有缓冲数据都被读取之后)。 如果对等应用程序试图向其 socket 写入数据, 那么它就会收到一个 SIGPIPE信号,并且系统调用会返回 EPIPE 错误。忽略 SIGPIPE 信号并通过 EPIPE 错误找出被关闭的连接。
5、数据报socket
简要说明数据报的流程
- 所有需要发送和接收数据报的应用程序都需要使用 socket()创建一个数据报 socket。
- 为允许另一个应用程序发送其数据报,服务端程序需要使用 bind()将其 socket 绑定到一个地址上。客户端会通过向该地址发送一个数据报来发起通信。
- 要发送一个数据报,一个应用程序需要调用sendto(),它接收的其中一个参数是数据报发送到的 socket 的地址。
- 为接收一个数据报,一个应用程序需要调用recvfrom(),它在没有数据报到达时会阻塞。由于 recvfrom()允许获取发送者的地址,因此可以在需要的时候发送一个响应。
- 当不再需要 socket 时,应用程序需要使用close()关闭 socket。
5.1 数据报的发送和接收
recvfrom()和 sendto()系统调用在一个数据报 socket 上接收和发送数据。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
这两个系统调用的返回值和前三个参数与 read()和 write()中的返回值和相应参数是一样的。第四个参数 flags 是一个位掩码, 它控制着了 socket 特定的 I/O 特性。
对于 recvfrom()来讲, src_addr 和 addrlen 参数会返回用来发送数据报的远程socket 的地址。对于 sendto()来讲, dest_addr 和 addrlen 参数指定了数据报发送到的 socket。
对于 recvfrom()来讲如果不关心发送者的地址,那么可以将 src_addr 和 addrlen 都指定为 NULL。在这种情况下, recvfrom()等价于使用 recv()来接收一个数据报。也可以使用 read()来读取一个数据报,这等价于在使用 recv()时将 flags 参数指定为 0。不管 length 的参数值是什么, recvfrom()只会从一个数据报 socket 中读取一条消息。如果消息的大小超过了 length 字节,那么消息会被静默地截断为 length 字节。
5.2 数据报使用connect
数据报 socket 是无连接的,但在数据报 socket 上应用 connect()系统调用仍然起作用。在数据报 socket 上调用 connect()会导致内核记录这个socket 的对等 socket 的地址。
当一个数据报 socket 已连接之后:
- 数据报的发送可在 socket 上使用 write()(或 send())来完成并且会自动被发送到同样的对等 socket 上。与 sendto()一样,每个 write()调用会发送一个独立的数据报;
- 在这个 socket 上只能读取由对等 socket 发送的数据报。