socket允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据
概述
在一个典型的客户端/服务器场景中,应用程序使用socket 进行通信的方式如下:
- 各个应用程序创建一个socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它。
- 服务器将自己的socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。
使用socket()
系统调用能够创建一个socket,它返回一个用来在后续系统调用中引用该socket 的文件描述符。
fd = socket(domain, type, protocol);
在本书介绍的所有应用程序中,protocol 参数总是被指定为0。
通信domain
通信domain可以确定:
- 识别出一个 socket 的方法(即socket“地址”的格式);
- 通信范围(即是在位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间)。
现代操作系统支持的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类型
有流(SOCK_STREAM)和数据报(SOCK_DGRAM)两种类型,在UNIX和Internet domain中都得到支持,属性总结如下表:
属性 | 流 | 数据报 |
---|---|---|
可靠地递送? | 是 | 否 |
消息边界保留? | 否 | 是 |
面向连接? | 是 | 否 |
流 socket(SOCK_STREAM)提供了一个可靠的双向的字节流通信信道,一个流socket 只能与一个对等socket 进行连接。
在 Internet domain 中,数据报socket 使用了用户数据报协议(UDP),而流socket 则(通常)使用了传输控制协议(TCP)。
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())来完成。
在 Linux 上可以通过调用ioctl(fd, FIONREAD, &cnt)
来获取文件描述符fd 引用的流
socket 中可用的未读字节数。对于数据报socket 来讲,这个操作会返回下一个未读数据报中的字节数(如果下一个数据报的长度为零的话就返回零)或在没有未决数据报的情况下返回0.
创建一个socket: socket()
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
// Returns file descriptor on success, or -1 on error
将socket 绑定到地址:bind()
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
// Returns 0 on success, or -1 on error
- addr 参数是一个指针,它指向了一个指定该socket 绑定到的地址的结构。传入这个参数的结构的类型取决于socket domain。
- addrlen 参数指定了地址结构的大小。addrlen 参数使用的socklen_t 数据类型在SUSv3 被规定为一个整数类型。
通用socket 地址结构:struct sockaddr
每种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)
};
流socket
流socket系统调用流程如下图:
监听接入连接:listen()
#include <sys/socket.h>
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 规定实现应该通过在<sys/socket.h>中定义SOMAXCONN常量来发布这个限制。在Linux 上,这个常量的值被定义成了128。但从内核2.4.25 起,Linux允许在运行时通过Linux 特有的/proc/sys/net/core/somaxconn 文件来调整这个限制。
接受连接: accept()
#include <sys/socket.h>
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)会保持打开状态,并且可以被用来接受后续的连接.
连接到对等socket:connect()
#include <sys/socket.h>
int connect(int fd, const struct sockaddr *addr, socklen_t addrlen);
// Returns 0 on success, or -1 on error
流socket I/O
要执行 I/O 需要使用read()和write()系统调用(或在61.3 节中描述的socket 特有的send()和recv()调用)。由于socket 是双向的,因此在连接的两端都可以使用这两个调用。
一个 socket 可以使用close()系统调用来关闭或在应用程序终止之后关闭。
连接终止:close()
终止一个流socket 连接的常见方式是调用close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。
数据报socket
交换数据报:recvfrom 和sendto()
#include <sys/socket.h>
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 上使用connect()
尽管数据报socket 是无连接的,但在数据报socket 上应用connect()系统调用仍然是起作用的。在数据报socket 上调用connect()会导致内核记录这个socket 的对等socket 的地址。
当一个数据报 socket 已连接之后:
- 数据报的发送可在socket 上使用write()(或send())来完成并且会自动被发送到同样的对等socket 上。与sendto()一样,每个write()调用会发送一个独立的数据报;
- 在这个 socket 上只能读取由对等socket 发送的数据报。
注意 connect()的作用对数据报socket 是不对称的。上面的论断只适用于调用了connect()数据报socket,并不适用于它连接的远程socket(除非对等应用程序在其socket 上也调用了connect())
为一个数据报socket 设置一个对等socket的优势:在该socket 上传输数据时可以使用更简单的I/O 系统调用,无需使用指定了dest_addr 和addrlen 参数的sendto(),而只需要使用write()即可
对等socket的应用场景: 主要对那些需要向单个对等socket(通常是某种数据报客户端)发送多个数据报的应用程序是比较有用的。