Internet Domain Socket 编程:字节流与数据报套接字详解

Internet Domain Socket 编程完全指南
:&(蛆音娘_不愧是我)
1. 套接字基础与核心函数
1.1 socket() - 创建套接字
创建一个通信端点并返回一个文件描述符。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
domain
: 指定通信域(网络层)AF_INET
: IPv4 Internet 协议AF_INET6
: IPv6 Internet 协议AF_UNIX
: 本地通信
type
: 指定通信语义(传输层)SOCK_STREAM
: 提供有序、可靠、双向、基于连接的字节流(TCP)SOCK_DGRAM
: 支持数据报(无连接、不可靠、固定最大长度)(UDP)SOCK_RAW
: 提供原始网络协议访问SOCK_SEQPACKET
: 提供有序、可靠、双向、基于连接的数据报传输路径
protocol
: 通常为0,表示自动选择与type对应的默认协议
返回值:
- 成功: 返回新套接字的文件描述符
- 失败: 返回-1,并设置errno
1.2 bind() - 绑定地址到套接字
将地址(主机名+端口号)分配给套接字。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
: socket()返回的文件描述符addr
: 指向要绑定的地址结构体的指针addrlen
: 地址结构体的长度
地址结构体(一般不直接用struct sockaddr,而是使用后面带in或in6的结构体):
IPv4 (
struct sockaddr_in
):struct sockaddr_in { sa_family_t sin_family; // AF_INET in_port_t sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IPv4地址 char sin_zero[8]; // 填充 }; struct in_addr { uint32_t s_addr; // IPv4地址(网络字节序) };
IPv6 (
struct sockaddr_in6
):struct sockaddr_in6 { sa_family_t sin6_family; // AF_INET6 in_port_t sin6_port; // 端口号(网络字节序) uint32_t sin6_flowinfo; // IPv6流信息 struct in6_addr sin6_addr; // IPv6地址 uint32_t sin6_scope_id; // 作用域ID }; struct in6_addr { unsigned char s6_addr[16]; // IPv6地址 };
地址结构体在使用前一定要做的事:
调用memset将其内存清零。即
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 清0
原因:防止结构体字段的内存忘记添加终止符\0
,也防止一些系统需要将因内存对齐而填充的字节设置为0的要求
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
1.3 listen() - 监听连接
将套接字标记为被动套接字(监听套接字),用于接受连接请求并诞生连接套接字。
一个监听套接字可以调用accept()诞生无穷个连接套接字,除非超过系统资源限制。
但内核中为该监听套接字保留的已连接但未被监听套接字调用accept的未决连接数量最大值为backlog。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明:
sockfd
: socket()返回的文件描述符backlog
: 挂起连接队列的最大长度
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
1.4 accept() - 接受连接
从监听套接字的挂起连接队列中接受一个连接。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd
: 监听套接字的文件描述符addr
: 用于存储对端地址的缓冲区(可为NULL)addrlen
: 输入时为addr缓冲区大小,输出时为实际地址长度(可为NULL)
返回值:
- 成功: 返回新套接字的文件描述符
- 失败: 返回-1,并设置errno
1.5 connect() - 发起连接
发起与监听套接字的连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
: socket()返回的文件描述符addr
: 目标地址结构体指针addrlen
: 地址结构体的长度
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
1.6 close() - 关闭套接字
关闭文件描述符。注意在对底层IO的深度总结中介绍,他只是断开该文件描述与内核的打开文件句柄之间的指向关系并删除该文件描述符。当内核的套接字文件打开句柄没有任何进程的文件描述符指向它时,内核向对端发送FIN。shutdown
系统调用的关闭写端功能直接通知内核发送FIN。(FIN用于通知对方自己不会再发送有实质内容的TCP报文且等待对方先回传ACK再回传FIN)
#include <unistd.h>
int close(int fd);
参数说明:
fd
: 要关闭的文件描述符
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
1.7 shutdown() - 关闭套接字的部分功能
关闭全双工连接的一部分。与close
不同之处在于其功能是若关闭了写端(或读写两端一起关闭)立即触发内核发送FIN,并不对文件描述符、文件句柄做任何操作。注意关闭读端只是内核不让read,但是内核并不发送FIN。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
参数说明:
sockfd
: 套接字文件描述符how
: 关闭方式SHUT_RD
: 关闭读端SHUT_WR
: 关闭写端SHUT_RDWR
: 关闭读写两端
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
2. 数据传输函数
2.1 send() - 发送数据
通过已连接的套接字发送数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
sockfd
: 已连接的套接字文件描述符buf
: 要发送的数据缓冲区len
: 要发送的数据长度flags
: 标志位(可位或组合)MSG_CONFIRM
: 确认链路有效MSG_DONTROUTE
: 不通过网关发送MSG_DONTWAIT
: 非阻塞操作MSG_EOR
: 结束记录MSG_MORE
: 与下次调用封装在一个TCP报文中MSG_NOSIGNAL
: 不产生SIGPIPE信号MSG_OOB
: 发送带外数据
返回值:
- 成功: 返回发送的字节数(正文)
- 失败: 返回-1,并设置errno
2.2 recv() - 接收数据
从已连接的套接字接收数据。特别注意,网络中的数据(包括管道、FIFO等任何IPC)不单单为C语言服务。即不会遵循字符串最后以0字符结尾。传给printf打印要么自己手动加上'\0'
,要么使用格式化输出方式"%.*s"来动态指定字符串长度。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
sockfd
: 已连接的套接字文件描述符buf
: 接收数据的缓冲区len
: 缓冲区长度flags
: 标志位(可位或组合)MSG_CMSG_CLOEXEC
: 设置接收的文件描述符为close-on-execMSG_DONTWAIT
: 非阻塞操作MSG_ERRQUEUE
: 接收错误信息MSG_OOB
: 接收带外数据MSG_PEEK
: 查看数据但不从缓冲区移除MSG_TRUNC
: 即使数据包被截断也返回实际长度MSG_WAITALL
: 等待所有请求数据到达
返回值:
- 成功: 返回接收的字节数(正文)(0表示对端关闭连接)
- 失败: 返回-1,并设置errno
2.3 sendto() - 发送数据报
通过无连接套接字发送数据报。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
sockfd
: 套接字文件描述符buf
: 要发送的数据缓冲区len
: 要发送的数据长度flags
: 同send()dest_addr
: 目标地址结构体指针(可为NULL)addrlen
: 目标地址结构体长度(可为0)
返回值:
- 成功: 返回发送的字节数(正文)
- 失败: 返回-1,并设置errno
2.4 recvfrom() - 接收数据报
从无连接套接字接收数据报。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd
: 套接字文件描述符buf
: 接收数据的缓冲区len
: 缓冲区长度flags
: 同recv()src_addr
: 用于存储源地址的缓冲区(可为NULL)addrlen
: 输入时为src_addr缓冲区大小,输出时为实际地址长度(可为NULL)
返回值:
- 成功: 返回接收的字节数(正文)(0表示对端关闭连接)
- 失败: 返回-1,并设置errno
2.5 sendmsg() - 发送消息
通过套接字发送消息(支持分散/聚集I/O和控制消息)。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数说明:
sockfd
: 套接字文件描述符msg
: 消息结构体指针flags
: 同send()
msghdr结构体:
struct msghdr {
void *msg_name; // 可选地址
socklen_t msg_namelen; // 地址长度
struct iovec *msg_iov; // 分散/聚集数组
int msg_iovlen; // iov元素个数
void *msg_control; // 辅助数据
socklen_t msg_controllen; // 辅助数据长度
int msg_flags; // 接收消息的标志
};
struct iovec {
void *iov_base; // 缓冲区起始地址
size_t iov_len; // 缓冲区长度
};
返回值:
- 成功: 返回发送的字节数
- 失败: 返回-1,并设置errno
2.6 recvmsg() - 接收消息
通过套接字接收消息(支持分散/聚集I/O和控制消息)。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数说明:
sockfd
: 套接字文件描述符msg
: 消息结构体指针flags
: 同recv()
返回值:
- 成功: 返回接收的字节数(0表示对端关闭连接)
- 失败: 返回-1,并设置errno
3. IP地址转换函数
3.1 inet_addr() - 将IPv4点分十进制字符串转换为网络字节序整数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
参数说明:
cp
: IPv4点分十进制地址字符串(如"192.168.1.1")
返回值:
- 成功: 返回网络字节序的32位IPv4地址
- 失败: 返回INADDR_NONE(通常为-1)
3.2 inet_aton() - 将IPv4点分十进制字符串转换为网络字节序结构体
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
参数说明:
cp
: IPv4点分十进制地址字符串inp
: 存储转换结果的in_addr结构体指针
返回值:
- 成功: 返回非零
- 失败: 返回0
3.3 inet_ntoa() - 将网络字节序IPv4地址转换为点分十进制字符串
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
参数说明:
in
: 网络字节序的in_addr结构体
返回值:
- 返回指向静态缓冲区的字符串指针(非线程安全)
3.4 inet_pton() - 将可打印地址字符串转换为网络格式(支持IPv4/IPv6)
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数说明:
af
: 地址族AF_INET
: IPv4AF_INET6
: IPv6
src
: 源地址字符串INET_ADDRSTRLEN
: IPv4presentation字符串最大长度INET6_ADDRSTRLEN
: IPv6presentation字符串最大长度
dst
: 存储转换结果的缓冲区(in_addr或in6_addr结构体的地址)
返回值:
- 成功: 返回1
- 无效输入: 返回0
- 失败: 返回-1
3.5 inet_ntop() - 将网络格式地址转换为可打印字符串(支持IPv4/IPv6)
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明:
af
: 地址族(AF_INET或AF_INET6)src
: 网络格式的地址指针(in_addr或in6_addr结构体的地址)dst
: 目标缓冲区(长度至少是上述两个宏定义)size
: 目标缓冲区大小
缓冲区大小建议:
- IPv4: 至少INET_ADDRSTRLEN(16)字节
- IPv6: 至少INET6_ADDRSTRLEN(46)字节
返回值:
- 成功: 返回dst
- 失败: 返回NULL
4. 套接字选项控制
4.1 getsockopt() - 获取套接字选项
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
参数说明:
sockfd
: 套接字文件描述符level
: 选项级别SOL_SOCKET
: 通用套接字选项IPPROTO_IP
: IPv4选项IPPROTO_IPV6
: IPv6选项IPPROTO_TCP
: TCP选项
optname
: 选项名称SOL_SOCKET级别:
SO_ACCEPTCONN
: 套接字是否在监听SO_BROADCAST
: 是否允许发送广播SO_DEBUG
: 是否启用调试SO_DONTROUTE
: 是否绕过标准路由SO_ERROR
: 获取并清除错误状态SO_KEEPALIVE
: 是否启用keepaliveSO_LINGER
: 延迟关闭时间SO_OOBINLINE
: 是否将带外数据放入普通数据流SO_RCVBUF
: 接收缓冲区大小SO_REUSEADDR
: 是否允许重用本地地址,分为两个层面。第一是使得连接套接字关闭后监听套接字可以不等待2MSL的时间调用accept而不发生EADDRINUSE错误;第二是使得多个监听套接字可以绑定同一个端口。SO_SNDBUF
: 发送缓冲区大小SO_TYPE
: 获取套接字类型
IPPROTO_IP级别:
IP_TOS
: 服务类型字段IP_TTL
: 生存时间字段
IPPROTO_IPV6级别:
IPV6_V6ONLY
: 是否仅限IPv6
IPPROTO_TCP级别:
TCP_NODELAY
: 禁用Nagle算法
optval
: 存储选项值的缓冲区optlen
: 输入时为optval缓冲区大小,输出时为实际选项长度
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
4.2 setsockopt() - 设置套接字选项
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数说明:
- 同getsockopt()
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
5. 主机名与地址解析
5.1 gethostbyname() - 通过主机名获取主机信息(IPv4)
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
hostent结构体:
struct hostent {
char *h_name; // 主机正式名
char **h_aliases; // 别名列表
int h_addrtype; // 地址类型(AF_INET)
int h_length; // 地址长度
char **h_addr_list; // 地址列表(网络字节序)
};
#define h_addr h_addr_list[0] // 向后兼容
返回值:
- 成功: 返回hostent结构体指针
- 失败: 返回NULL,并设置h_errno
5.2 gethostbyaddr() - 通过地址获取主机信息(IPv4)
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
参数说明:
addr
: 指向网络字节序地址的指针len
: 地址长度(IPv4为4)type
: 地址类型(AF_INET)
返回值:
- 同gethostbyname()
5.3 getaddrinfo() - 地址无关的主机名解析(支持IPv4/IPv6)
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
5.4 freeaddrinfo() - 释放getaddrinfo()分配的内存
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
void freeaddrinfo(struct addrinfo *res);
5.5 gai_strerror() - 获取getaddrinfo()错误描述
#include <netdb.h>
const char *gai_strerror(int errcode);
5.6 getnameinfo() - 地址无关的地址到名称转换(支持IPv4/IPv6)
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
6. (端口、正文中的整数)字节序转换函数
字节序转换函数用于诸如信息里的整数、端口号等多字节信息单元的传递。当每个信息仅占一个字节时,如字符串,内存低位一定是字符串的前几个字符。而对于整数,大端存储——先存高位,即内存低地址存储整数高位;小端存储——先存低位,即内存低地址存储整数低位。网络字节序为大端,而本地主机由于硬件架构的不同,有的是大端而有的是小端。
6.1 htons() - 主机字节序转网络字节序(16位)
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
参数说明:
hostshort
: 主机字节序的16位整数
返回值:
- 返回网络字节序的16位整数
6.2 htonl() - 主机字节序转网络字节序(32位)
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
参数说明:
hostlong
: 主机字节序的32位整数
返回值:
- 返回网络字节序的32位整数
6.3 ntohs() - 网络字节序转主机字节序(16位)
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
参数说明:
netshort
: 网络字节序的16位整数
返回值:
- 返回主机字节序的16位整数
6.4 ntohl() - 网络字节序转主机字节序(32位)
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
参数说明:
netlong
: 网络字节序的32位整数
返回值:
- 返回主机字节序的32位整数
7. 其他辅助函数
7.1 getsockname() - 获取套接字本地地址
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
sockfd
: 套接字文件描述符addr
: 存储本地地址的缓冲区addrlen
: 输入时为addr缓冲区大小,输出时为实际地址长度
返回值:
- 成功: 返回0
- 失败: 返回-1,并设置errno
7.2 getpeername() - 获取对端地址
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
- 同getsockname()
返回值:
- 同getsockname()
7.3 sockatmark() - 检查是否处于带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
参数说明:
sockfd
: 套接字文件描述符
返回值:
- 处于带外标记: 返回1
- 不处于带外标记: 返回0
- 失败: 返回-1,并设置errno