AKTK-Lab

Back

网络编程基础 API#

创建 socket#

socket 是:可读 可写 可控制 可关闭的文件描述符

#include<sys/socket.h>
int socket(int domain, int type, int protocol)
c

成功时返回文件描述符,失败时返回-1 并设置errno

  • domain: 套接字中使用的协议族信息
  • type : 套接字数据传输类型信息,主要有SOCK_STREAM 流服务 SOCK_DGRAM 数据报
  • protocol : 在前两个参数构成的协议集合下,再选择一个具体的协议,大部分情况可以向第三个参数传输 0,除非同一协议族中存在多个数据传输方式相同的协议

协议族信息#

协议族含义
PF_INETIPv4
PF_INET6IPv6
PF_LOCAL本地通信的 UNIX 协议族
PF_PACKET底层套接字的协议族
PF_IPXIPX Novell 协议族
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//tcp套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
//udp套接字
c

地址信息的表示#

通用 socket 地址#

struct sockaddr {
	sa_family_t sa_family;
	char sa_data[14];
}
c

专用 socket 地址#

通用 socket 地址在设置/获取 ip 地址和端口号的时候要进行位操作,很不方便。Linux 为各个协议族提供了专用的结构体:

TCP/IP:ipv4#

struct sockaddr_in{
	sa_family_t sin_family; //地址族
	uint16_t sin_port; //16位TCP/UDP端口号,以网络字节序保存
	struct in_addr sin_addr; //32位IPv4地址,以网络字节序保存
}

struct in_addr{
	uin32_t s_addr; //32位IPv4地址
}
c

TCP/IP:ipv6#

struct sockaddr_in6{
	sa_family_t sin6_family; //地址族
	uint16_t sin6_port; //16位TCP/UDP端口号,以网络字节序保存
	uint32_t sin6_flowinfo; // 流信息 应设置为0
	struct in6_addr sin6_addr; //ipv6地址结构体
	uint32_t sin6_scope_id; // scope ID 尚处于试验阶段
}

struct in6_addr{
	unsigned char sa_addr[16]; //ipv6地址
}
c

协议族 地址族#

地址族对应协议族含义
AF_INETPF_INETIPv4 使用的地址族
AF_INET6PF_INET6IPv6 使用的地址族
AF_UNIXPF_UNIX本地通信中采用 UNIX 协议的地址族

字节序转换#

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
c

h 代表主机字节序,n 代表网络字节序,长整型用于 IP 地址,短整型用于端口

网络地址的初始化与分配#

ipv4 地址是点分十进制字符串,ipv6 地址是十六进制字符串,编程中先转换为整数(二进制数), 记录日志时把整数 ip 地址转换为字符串

ipv4 字符串信息转换为网络字节序整数型#

点分十进制字符串转换为网络字节序的 ipv4 地址

#include<arpa/inet.h>
in_addr_t inet_addr(const char* string);
c

成功时返回 32 位大端序整数值,失败时返回INADDR_NONE

inet_addr相同,但储存在指针中

#include<arpa/inet.h>
int inet_aton(const char* string, struct in_addr* addr);
c

成功时返回 1,失败时返回 0

  • string:含需转换的 IP 地址信息的字符串地址值
  • addr:将保存转换结果的 in_addr 结构体变量的地址值

将网络字节序表示的 ipv4 地址转换为点分十进制字符串表示,用静态变量存储,不可重入

#include<arpa/inet.h>
char* inet_ntoa(struct in_addr addr);
c

成功时返回转换的字符串地址值,失败时返回-1,若需要长期保存,调用完该函数后应立即将字符串信息复制到其他的内存空间

同时适用于 ipv4 和 ipv6 的函数#

将字符串表示的 IP 地址 src 转换成网络字节序整数表示的 IP 地址,结果储存于dst指向的内存中

#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);
c

成功时返回 1,失败时返回 0 并设置errno

  • af:地址族
#include <arpa/inet.h>
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt)
c

成功时返回目标存储单元地址,失败返回NULL并设置errno

  • cnt:指定目标存储单元的大小

自动获取运行服务器端的 IP 地址#

INADDR_ANY 自动获取运行服务器端的 IP 地址,若同一计算机中已分配多个 IP 地址(多宿主计算机,例如路由器),只要端口号一致就可以从不同 IP 地址接收数据,服务器端优先考虑这种方式

向 socket 分配网络地址#

#include<sys/socket.h>
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
c

成功时返回 0,失败时返回-1

  • sockfd:要分配地址信息的 socket 文件描述符
  • myaddr:存有地址信息的结构体变量地址
  • addrlen:该结构体变量的长度

监听 socket#

创建一个监听队列来存放待处理的客户连接

#include<sys/socket.h>
int listen(int sock, int backlog);
c

成功时返回 0,失败时返回-1 并设置errno

  • sock:希望进入等待连接请求状态的 socket 文件描述符
  • backlog:连接监听队列的长度,表示最多使几个连接请求进入队列,与服务器端的特性有关,典型参数为 5,频繁接收的 web 服务器端至少要 15。超过长度服务器不受理新的客户端连接,客户端收到ECONNREFUSED信息。内核 2.2 之前指半连接状态和完全连接状态的 socket 的上限,2.2 之后只表示完全连接状态的 socket 上限。

服务器接受连接#

从监听队列中接受一个连接(不论连接处于什么状态,不关心网络状态变化)

#include<sys/socket.h>
int accept(int sock, struct sockaddr* addr, socklen_t *addrlen);
c

成功时返回创建的套接字文件描述符,失败时返回-1 并设置errno

  • sock:执行listen调用的监听 socket
  • addr:获取被接受连接的远端 socket 地址,长度由 addrlen 指出

客户端主动连接#

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
c

成功时返回 0,sockfd唯一的标识这个连接失败时返回-1 并设置errno,常见的有:

  • ECONNREFUSED:目标端口不存在,连接被拒绝
  • ETIMEOUT:连接超时

关闭连接#

并非立即关闭,而是将fd的引用次数-1,当引用次数为 0 时才是真正关闭连接

#include<unistd.h>
int close(int fd);
c

成功时返回 0,失败时返回-1

  • fd:需要关闭的文件或套接字的文件描述符

如果需要立即关闭终止连接而不是引用次数-1 可以使用shutdown

#include<sys/socket.h>
int shutdown(int sock, int howto)
c

成功时返回 0,失败时返回-1(Win:SOCKET_ERROR)

  • sock:需要断开的 socket 文件描述符
  • howto:传递断开方式信息

howto 的可选值

可选值含义
SHUT_RD断开输入流,输入缓冲收到数据也会被消除
SHUT_WR断开输出流,输出缓冲如果还有未传输的数据,则传递至目标主机
SHUT_RDWR同时断开 I/O 流,等于调用SHUT_RD,SHUT_WR各一次

TCP 数据读写#

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
c

成功时返回发送的字节数, 失败时返回-1 并设置errno

  • sockfd: 表示与数据传输对象的连接的套接字文件描述符
  • buf: 保存待传输数据的缓冲地址值
  • nbytes: 待传输的字节数
  • flags: 指定的可选项信息
#include <sys/socket.h>
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags);
c

成功时返回接收的字节数, 收到 EOF 返回 0, 失败时返回-1 并设置errno

  • sockfd: 表示数据接收对象的连接的套接字文件描述符
  • buf: 保存接收数据的缓冲地址值
  • nbytes: 能接收的最大字节数
  • flags: 指定的可选项信息

flags(可选项)可利用位或(bit OR)运算同时传递多个信息, 不同操作系统对可选项的支持有不同, 不受操作系统影响的:

选项含义sendrecv
MSG_OOB发送/接受紧急数据YY
MSG_PEEK窥探读缓存中的数据,此次操作不会导致数据被清除NY
MSG_DONTWAIT对 socket 的此次操作是非阻塞的YY

紧急模式工作原理#

MSG_OOB的意义在督促数据接收对象尽快处理数据, TCP”保持传输顺序”的传输特性仍然成立.

MSG_OOB:发送/接受紧急数据#

用于创建特殊发送方法和通道, 来发送优先级更高的紧急消息(带外数据, out-of-band data: 通过完全不同的通信路径传输的数据)

./oob_recv.out 9190
./oob_send.out 127.0.0.1 9190

Urgent message: 0
12356789
shell

通过MSG_OOB, urge_handler读取数据只能读 1 字节, 剩下的用普通输入读取, 因为 TCP 不存在真正意义上的带外数据, oob 应该要用单独的通信路径高速传输,但是 TCP 不提供, 只能用紧急模式传输

输出缓冲#

send(sock, "890", strlen("890"), MSG_OOB) 偏移量 3 的位置存着紧急指针, 指向紧急消息的下一位置, 向对方主机传送消息: 紧急指针指向偏移量 3, 之前的部分是紧急消息

偏移量0123
890紧急指针

实际只用 1 个字节表示紧急消息信息

TCP 头数据
URG = 1, URG 指针 = 3890
  • URG=1: 载有紧急消息的数据包
  • URG 指针: 紧急指针位于偏移量 3 的位置 我们无法得知具体消息内容, 紧急消息的意义在于督促消息处理, 而不是紧急传输形式受限的消息.

MSG_PEEKMSG_DONTWAIT:检查输入缓冲#

同时设置MSG_PEEKMSG_DONTWAIT来验证输入缓冲中是否存在接收的数据.

  • 设置MSG_PEEK并调用recv(), 即使读取了输入缓冲的数据也不会删除, 所以通常和MSG_DONTWAIT一起用, 以非阻塞方式验证待读数据存在与否.
./peek_recv.out 8190
./peek_send.out 127.0.0.1 8190

Buffering 3 bytes: 123
Read again: 123 # 设置MSG_PEEK 数据被读取2次
bash

UDP 数据读写#

UDP 通信没有连接的概念,每次读取数据都要获取发送端的 socket 地址。 这两个函数也可以用于面向连接的数据读写,只要把最后两个参数都设置NULL以忽略发送端/接收端 socket 地址

#include<sys/socket.h>
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
c

成功时返回传输字节数,失败时返回-1

  • sock:传输数据的 UDP 套接字文件描述符
  • buf:保存待传输数据的缓冲地址值
  • nbytes:待传输的数据长度,以字节为单位
  • flags:可选参数,默认 0
  • to:存有目标地址信息的 sockaddr 结构体变量的地址值
  • addrlen:传输给参数 to 的结构体变量长度的变量地址值
#include<sys/socket.h>
ssize_t recvfrom(int sock, void* buf, size_t nbytes, int flags, struct sockaddr* from, socklen_t* addrlen);
c

成功时返回传输字节数,失败时返回-1

  • sock:接收数据的 UDP 套接字文件描述符
  • buf:保存数据的缓冲地址值
  • nbytes:可接收的最大字节数,无法超过参数 buff 所指的缓冲大小
  • flags:可选参数,默认 0
  • from:存有发送端地址信息的 sockaddr 结构体变量的地址值
  • addrlen:保存参数 from 的结构体变量长度的变量地址值

通用数据读写#

不仅用于 TCP,也可以用于 UDP

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
c
struct msghdr {
	void* msg_name; // socket地址
	socklen_t msg_namelen; // socket地址长度
	struct iovec* msg_iov; // 分散的内存块
	int msg_iovlen; // 分散内存块数量
	void* msg_control; // 指向辅助数据的起始位置
	socklen_t msg_controllen; // 辅助数据大小
	int msg_flags; // 复制函数中的flag参数 调用过程中更新
};

struct iovec {
	void* iov_base; // 内存起始地址
	size_t iov_len; // 内存长度
}
c

成功时返回发送的字节数, 失败时返回-1 并设置errno

recvmsg 时数据将被读取,存放在msg_iovlen块分散的内存中,位置和长度由msg_iov指向的数组指定,这是分散读(scatter read)

sendmsgmsg_iovlen块分散内存中的数据将被一并发送,称为集中写(gather write)

  • sockfd: 被操作目标 socket
  • msg:指针
  • flags:可选参数,默认

带外标记#

Linux 内核检测到 TCP 紧急标志的时候,先通知应用程序:有带外数据需要接受 有两种方式:

  1. I/O 复用产生的异常事件
  2. SIGURG信号 还需要知道带外数据在数据流中的具体位置:

判断sockfd是否处于带外标记(下一个被读取到的数据是否是带外数据)

#include <sys/socket.h>
int sockatmark(int sockfd);
c

如果是则返回 1,此时可以利用带MSG_OOBrecv来接受,不是则返回 0

地址信息函数#

获取sockfd对应的本端 socket 地址,将其存储于address参数指定的内存中,该 socket 地址的长度则存储于address_len指向的变量中(实际 socket 地址长度比address所指内存大的时候,地址会被截断)

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
c
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
c

成功时返回 0,失败时返回-1 并设置 errno

socket 的可选项#

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname,  void *optval, socklen_t *optlen );
c

成功时返回 0,失败时返回-1

  • sock:查看选项的 socket 文件描述符
  • level:查看选项的协议层
  • optname:查看的可选项名
  • optval:保存查看结果的缓冲地址值
  • optlen:向 optval 传递的缓冲大小,调用函数后,该变量保存通过 optval 返回的可选项的字节数

对可选项修改:

#include <sys/socket.h>
int setsockopt(int sock, int level, int optname,  const void *optval, socklen_t optlen );
c

成功时返回 0,失败时返回-1

  • sock:更改选项的 socket 文件描述符
  • level:更改选项的协议层
  • optname:更改的可选项名
  • optval:保存更改结果的缓冲地址值
  • optlen:向 optval 传递的可选项的字节数

SO_REUSEADDR#

[[TIME_WAIT]] 服务器可以通过设置 socket 选项 SO_REUSEADDR 来强制使用被处于 TIME_WAIT 状态的连接占用的 socket 地址

int sock = socket(PF_INET, SOCK_STREAM, 0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
c

SO_RCVBUFSO_SNDBUF#

修改 TCP 缓冲区大小。使用setsockopt 设置 TCP 发送和接收的缓冲区大小时,系统会将其加倍且不得小于某个最小值,来确保一个 TCP 连接有足够的空闲缓冲区来处理拥塞(比如快速重传期望 TCP 接收缓冲区至少容纳 4 个大小为 SMSS 的 TCP 报文段)

SO_RCVBUF:接收缓冲区大小(min:256byte 不同系统可能不同) SO_SNDBUF:发送缓冲区大小(max:2048byte)

SO_RCVLOWATSO_SNDLOWAT#

SO_RCVLOWAT:接收缓冲区的低水位标记 (1byte) SO_SNDLOWAT:发送缓冲区的低水位标记 (1byte)

一般被 I/O 复用系统调用来判断 socket 是否可读或者可写:

TCP 接收缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以从对应的 socket 上读取。 TCP 发送缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以往对应的 socket 上写入。

SO_LINGER#

控制close在关闭 TCP 连接的行为

设置SO_LINGER时我们给setsockopt / getsockopt 调用传递一个linegr 类型结构体:

#include <sys/socket.h>
struct linger {
	int l_onoff; // 开启/关闭
	int l_linger; // 滞留时间
}
c
  • l_oneoff = 0SO_LINGER不起作用,close 默认关闭 socket
  • l_oneoff != 0 l_linger = 0close立即返回,TCP 模块丢弃关闭的 socket 对应的 TCP 发送缓冲区中残留的数据,给对方发一个复位报文,提供了异常终止一个连接的方法
  • l_oneoff != 0, l_linegr > 0close 行为取决于:被关闭的 socket 对应的 TCP 发送缓冲区是否有残留数据, 该 socket 是否非阻塞。阻塞 socket 等待时间为l_linger直到 TCP 模块发送完所有残留数据并得到对方确认,如果没有成功就返回-1 并设置errno,非阻塞则立即返回

网络信息 API#

获取主机完整信息#

根据主机名称获取主机完整信息,先在本地/etc/hosts配置文件寻找主机,找不到就访问 DNS 服务器

#include<netdb.h>
struct hostent* gethostbyname(const char* hostname)
c
  • hostname: 域名字符串

根据 IP 地址获取主机完整信息

#include<netdb.h>
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family)
c
  • addr:含有 ip 地址信息的 in_addr 结构体指针,为了同时传递 IPv4 地址以外的信息,应该声明为 char 指针
  • len:向第一个参数传递的地址信息的字节数,IPv4:4 IPv6:16
  • family:地址族信息

都返回hostent结构体类型的指针:

struct hostent {
	char* h_name; //官方域名
	char** h_aliases;//同一IP绑定的其他域名
	int h_addrtype; //地址族信息(eg. IPv4:AF_INET)
	int h_length; // IP地址长度(byte)(IPv4:4 IPv6:16)
	char** h_addr_list //以整数形式保存域名对应的IP地址,用户较多的网站有可能分配多个IP给同一域名,利用多个服务器进行负载均衡。
}
c

获取某个服务完整信息#

#include <netdb.h>
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
c
  • name: 指定服务名字
  • port:指定服务的端口号
  • proto:服务类型("tcp":流服务 "udp":数据报服务NULL:所有类型)
struct servent {
  char* s_name; // 服务名
  char** s_aliases; // 服务别名
  short s_port; // 端口号
  char* s_proto; // 服务类型 tcp/udp
};
c

getaddrinfo#

既能通过主机名获得 ip 地址,也能通过服务名获取端口号

#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
c
  • hostname:主机名/字符串 ip 地址
  • service:服务名/字符串端口
  • hints:应用程序给这个函数的一个提示,可设NULL
  • result:指向存储getaddrinfo反馈结果的链表
typedef struct addrinfo {
	int ai_flags;  //AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST
	int ai_family;        //AF_INET,AF_INET6
	int ai_socktype;    //SOCK_STREAM,SOCK_DGRAM
	int ai_protocol;    //IPPROTO_IP, IPPROTO_IPV4,IPPROTO_IPV6 etc.
	size_t ai_addrlen;            //must be zero or a null pointer
	char* ai_canonname;            //must be zero or a null pointer
	struct sockaddr* ai_addr;    //must be zero or a null pointer
	struct addrinfo* ai_next;    //must be zero or a null pointer
}
c

我们使用hints时只设置前四个字段,后面设NULL

该函数隐式地分配堆内存,因为res原本没有指向一块合法内存,所以调用结束我们要用配对的函数释放这块内存

#include <netdb.h>
void freeaddrinfo(struct addrinfo* res);
c

getnameinfo#

通过 socket 地址同时获得以字符串表示的主机名和服务名

#include <netdb.h>
int getnameinfo (const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
c

将返回的主机名存储在host指向的缓存中,将服务名存储在serv指向的缓存中

  • hostlen servlen:指定两块缓存的长度
  • flag: 指定行为
UNIX网络编程 part.1: 基础API
https://astro-pure.js.org/blog/unix-network-porg-api
Author Akatsuki
Published at February 13, 2026