1. 套接字的地址结构
1.1 通用的地址结构:
/* POSIX.1g 规范规定了地址族为 2 字节的值. */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址 */
struct sockaddr{
sa_family_t sa_family; /* 地址族. 16-bit*/
char sa_data[14]; /* 具体的地址值 112-bit */
};
地址族表示使用什么样的方式保存地址:
- AF_LOCAL:表示的是本地地址,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成 AF_UNIX、AF_FILE;
- AF_INET:因特网使用的 IPv4 地址;
- AF_INET6:因特网使用的 IPv6 地址。
sockaddr
是一个通用的地址结构,通用的意思是适用于多种地址族。
1.2 IPV4套接字
/* IPV4 套接字地址,32bit 值. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/* 描述 IPV4 的套接字地址格式 */
struct sockaddr_in
{
sa_family_t sin_family; /* 16-bit */
in_port_t sin_port; /* 端口号 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */
/* 这里仅仅用作占位符,不做实际用处 */
unsigned char sin_zero[8];
};
sin_family字段
port最大支持 2 的 16 次方,这个数字是 65536,端口号0~65535
1.3 IPV6套接字
struct sockaddr_in6
{
sa_family_t sin6_family; /* 16-bit */
in_port_t sin6_port; /* 传输端口号 # 16-bit */
uint32_t sin6_flowinfo; /* IPv6 流控信息 32-bit*/
struct in6_addr sin6_addr; /* IPv6 地址 128-bit */
uint32_t sin6_scope_id; /* IPv6 域 ID 32-bit */
};
1.4 本地套接字
struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 最多108字节*/
};
2. 套接字建立连接
2.1 创建套接字
int socket(int domain, int type, int protocol)
domain表示使用什么样的套接字
type:
- SOCK_STREAM: 表示的是字节流,对应 TCP;
- SOCK_DGRAM: 表示的是数据报,对应 UDP;
- SOCK_RAW: 表示的是原始套接字。
protocol 已经废弃,目前一般写成 0
2.2 bind
int bind(int fd, sockaddr * addr, socklen_t len);
bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析(套接字的具体类型),len 字段表示的就是传入的地址长度,它是一个可变值。
// 可以理解为通用类型
int bind(int fd, void * addr, socklen_t len);
fd表示使用的套接字
2.3 地址族
对于使用者来说,每次需要将 IPv4、IPv6 或者本地套接字格式转化为通用套接字格式:
struct sockaddr_in name;
bind(sock, (struct sockaddr *) &name, sizeof(name))
使用通配设置套接字的地址
struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4 通配地址 */
指定端口和地址族
name.sin_family = AF_INET; /* IPV4 */
name.sin_port = htons (port);
2.4 listen
int listen (int socketfd, int backlog)
套接字+队列长度
2.5 accept
int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
参数:listensockfd:监听套接字
返回:
-
cliaddr:客户端地址
-
addrlen:地址的大小
-
socket's descriptor:已连接套接字描述符
监听套接字一直都存在,它是要为成千上万的客户来服务的,直到这个监听套接字关闭;而一旦一个客户和服务器连接成功,完成了 TCP 三次握手,操作系统内核就为这个客户生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信处理。
2.6 connect
客户在调用函数 connect 前不必非得调用 bind 函数
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
如果是 TCP 套接字,那么调用 connect 函数将激发 TCP 的三次握手过程,而且仅在连接建立成功或出错时才返回。其中出错返回可能有以下几种情况:
- 无法建立握手,于是返回 TIMEOUT 错误。
- 客户端收到了 RST(复位)回答,这时候客户端会立即返回 CONNECTION REFUSED 错误。
- SYN 包在网络上引起了"destination unreachable"。
2.7 总结
- 服务器端通过创建 socket,bind,listen 完成初始化,通过 accept 完成连接的建立。
- 客户端通过场景 socket,connect 发起连接建立请求。
3. 套接字收发数据
3.1 发送数据
write、send 和 sendmsg
ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
write
就是常见的文件写函数,如果把 socketfd 换成文件描述符,就是普通的文件写入send
函数多了一个flag,可以发送带外数据(特定场景的紧急处理)sendmsg
可以以缓冲区的方式发送数据
3.2 缓冲区
TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。
发送缓冲区的大小可以通过套接字选项来改变,调用write
时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
- 缓冲区足够大,可以直接容纳这份数据, write 调用退出。
- 不足以容纳数据,挂起。write阻塞调用返回。
3.3 读取数据
read函数
ssize_t read (int socketfd, void *buffer, size_t size)
内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中
返回实际读取的字节数
4. UDP
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t *addrlen);
4.1 recvfrom
sockfd 是本地创建的套接字描述符,buff 指向本地的缓存,nbytes 表示最大接收数据字节。
flags 是和 I/O 相关的参数
from 和 addrlen,实际上是返回对端发送方的地址和端口等信息(UDP 报文每次接收都会获取对端的信息,也就是说报文和报文之间是没有上下文的。)
4.2 sendto
sockfd 是本地创建的套接字描述符,buff 指向发送的缓存,nbytes 表示发送字节数。第四个参数 flags 依旧设置为 0。
后面两个参数 to 和 addrlen,表示发送的对端地址和端口等信息
5. 本地套接字
本地套接字,严格意义上说提供了一种单主机跨进程间调用的手段,减少了协议栈实现的复杂度,效率比 TCP/UDP 套接字都要高许多。类似的 IPC 机制还有 UNIX 管道、共享内存和 RPC 调用等。
5.1 本地字节流套接字
服务端创建
// AF_LOCAL, SOCK_STREAM 字节流
listenfd = socket(AF_LOCAL, SOCK_STREAM, 0);
// 地址 对应于 IPv4、IPv6 地址
// 关键在于设置一个本地文件路径
char *local_path = argv[1];
unlink(local_path);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, local_path);
5.2 本地数据报套接字
服务端
sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0);
// 本地套接字 bind 到本地一个路径上
bzero(&client_addr, sizeof(client_addr)); /* bind an address for us */
client_addr.sun_family = AF_LOCAL;
strcpy(client_addr.sun_path, tmpnam(NULL));
5.3 总结
- 本地套接字的编程接口和 IPv4、IPv6 套接字编程接口是一致的,可以支持字节流和数据报两种协议。
- 本地套接字的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报套接字实现。