网络编程

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-exec
    • MSG_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: IPv4
    • AF_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: 是否启用keepalive
      • SO_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);

详情见于解析DNS与服务名地址,IP地址与端口号

5.4 freeaddrinfo() - 释放getaddrinfo()分配的内存

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

void freeaddrinfo(struct addrinfo *res);

详情见于解析DNS与服务名地址,IP地址与端口号

5.5 gai_strerror() - 获取getaddrinfo()错误描述

#include <netdb.h>

const char *gai_strerror(int errcode);

详情见于解析DNS与服务名地址,IP地址与端口号

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);

详情见于解析DNS与服务名地址,IP地址与端口号

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

回复

This is just a placeholder img.