在一个典型的客户端/服务器场景中,应用程序使用socket 进行通信的方式如下:
socket()
系统调用能够创建一个socket,它返回一个用来在后续系统调用中引用该socket 的文件描述符。fd = socket(domain, type, protocol);
在本书介绍的所有应用程序中,protocol 参数总是被指定为0。
通信domain可以确定:
现代操作系统支持的domain如下表:
Domain | 执行的通信 | 应用程序间的通信 | 地址格式 | 地址结构 |
---|---|---|---|---|
AF_UNIX | 内核中 | 同一主机 | 路径名 | sockaddr_un |
AF_INET | 通过IPv4 | 通过 IPv4 网络连接起来的主机 | 32 位IPv4 地址+16 位端口号 | sockaddr_in |
AF_INET6 | 通过IPv6 | 通过 IPv6 网络连接起来的主机 | 128 位IPv4 地址+16 位端口号 | sockaddr_in6 |
补充:在Socket编程中,AF_INET和PF_INET都代表IPv4协议族(AddressFamily),它们的含义是相同的。AF_INET是Address Family的缩写,而PF_INET是Protocol Family的缩写,它们都指的是IPv4协议族。
在实际应用中,AF_INET更常用一些,而PF_INET则更多地用于内核编程中。在Linux系统中,AF_INET和PF_INET的定义是一样的,它们的值都为2,所以在使用时可以互换使用。
总的来说,AF_INET和PF_INET没有本质区别,只是从不同的角度来描述IPv4协议族。
有流(SOCK_STREAM)和数据报(SOCK_DGRAM)两种类型,在UNIX和Internet domain中都得到支持,属性总结如下表:
属性 | 流 | 数据报 |
---|---|---|
可靠地递送? | 是 | 否 |
消息边界保留? | 否 | 是 |
面向连接? | 是 | 否 |
流 socket(SOCK_STREAM)提供了一个可靠的双向的字节流通信信道,一个流socket 只能与一个对等socket 进行连接。
在 Internet domain 中,数据报socket 使用了用户数据报协议(UDP),而流socket 则(通常)使用了传输控制协议(TCP)。
关键的调用包括以下几种:
socket I/O 可以使用传统的read()和write()系统调用或使用一组socket 特有的系统调用(如send()、recv()、sendto()以及recvfrom())来完成。
在 Linux 上可以通过调用ioctl(fd, FIONREAD, &cnt)
来获取文件描述符fd 引用的流
socket 中可用的未读字节数。对于数据报socket 来讲,这个操作会返回下一个未读数据报中的字节数(如果下一个数据报的长度为零的话就返回零)或在没有未决数据报的情况下返回0.
#include
int socket(int domain, int type, int protocol)
// Returns file descriptor on success, or -1 on error
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
// Returns 0 on success, or -1 on error
每种socket domain 都使用了不同的地址格式,但是诸如bind()之类的系统调用适用于所有socket domain,因此它们必须要能够接受任意类型的地址结构。
为支持这种行为,socket API 定义了一个通用的地址结构struct sockaddr。
这个类型的唯一用途是将各种domain 特定的地址结构转换成单个类型以供socket 系统调用中的各个参数使用。
struct sockaddr
{
sa_family_t_sa_family; // addr family
char sa_data[14]; // sock addr (size varies according to socket domain)
};
#include
int listen(int sockfd, int backlog);
// Returns 0 on success, or -1 on error
无法在一个已连接的socket(即已经成功执行connect()的socket 或由accept()调用返回的socket)上执行listen()。
客户端可能会在服务器调用accept()之前调用connect()。这种情况是有可能会发生的,如服务器可能正忙于处理其他客户端。这将会产生一个未决的连接。内核必须要记录所有未决的连接请求的相关信息,这样后续的accept()就能够处理这些请求了。
backlog 参数允许限制这种未决连接的数量。在这个限制之内的连接请求会立即成功。之外的连接请求就会阻塞直到一个未决的连接被接受(通过accept()),并从未决连接队列删除为止。
SUSv3 规定实现应该通过在
#include
int accept(int sockfd, struct sockaddr *addr, socktlen_t *addrlen);
// Returns file descriptor on success, or -1 on error
理解 accept()的关键点是它会创建一个新socket,并且正是这个新socket 会与执行connect()的对等socket 进行连接。accept()调用返回的函数结果是已连接的socket 的文件描述符。监听socket(sockfd)会保持打开状态,并且可以被用来接受后续的连接.
#include
int connect(int fd, const struct sockaddr *addr, socklen_t addrlen);
// Returns 0 on success, or -1 on error
要执行 I/O 需要使用read()和write()系统调用(或在61.3 节中描述的socket 特有的send()和recv()调用)。由于socket 是双向的,因此在连接的两端都可以使用这两个调用。
一个 socket 可以使用close()系统调用来关闭或在应用程序终止之后关闭。
终止一个流socket 连接的常见方式是调用close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。
#include
ssize_t recvfrom(int sockfd, void *buffer, size_t length, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
// Returns number of bytes sent, or -1 on error
不管 length 的参数值是什么,recvfrom()只会从一个数据报socket 中读取一条消息。如果消息的大小超过了length 字节,那么消息会被静默地截断为length 字节。
尽管数据报socket 是无连接的,但在数据报socket 上应用connect()系统调用仍然是起作用的。在数据报socket 上调用connect()会导致内核记录这个socket 的对等socket 的地址。
当一个数据报 socket 已连接之后:
注意 connect()的作用对数据报socket 是不对称的。上面的论断只适用于调用了connect()数据报socket,并不适用于它连接的远程socket(除非对等应用程序在其socket 上也调用了connect())
为一个数据报socket 设置一个对等socket的优势:在该socket 上传输数据时可以使用更简单的I/O 系统调用,无需使用指定了dest_addr 和addrlen 参数的sendto(),而只需要使用write()即可
对等socket的应用场景: 主要对那些需要向单个对等socket(通常是某种数据报客户端)发送多个数据报的应用程序是比较有用的。