引言
本文继 Linux 系统编程之后的第一篇博客,也是 Linux 网络编程专栏的开端,主要介绍的是socket编程的内容。需要注意的是学习 Linux 网络编程还需具备 Linux 系统编程的知识储备,以及计算机网络的相关知识储备,如TCP协议、UDP协议的具体实现等。
本文主要介绍 TCP socket 编程核心 API 及其参数详解;声明:这篇博客借鉴了尚硅谷的 Linux 应用层开发课程内容及文档。
一、什么是 Socket?
套接字(Socket)是计算机网络数据通信的基本概念和编程接口 ,允许不同主机上的进程(运行中的程序)通过网络进行数据交换。它为应用层软件提供了发送和接收数据的能力,使得开发者可以在不用深入了解底层网络细节的情况下进行网络编程 。在学习计算机网络的过程中,我们知道 socket 就是IP 地址 + 主机端口号 + 对应协议,而在 Linux 网络编程中,
一句话理解:
Socket 是一个文件描述符,它允许你的进程在网络上收发数据,就像读写普通文件一样。
在 Linux 中一切皆文件,因此 socket 返回的本质是一个 文件描述符 (int)。
对于 TCP 协议,我们使用的是 流式套接字(SOCK_STREAM) ,它提供面向连接、可靠、有序的数据传输服务。数据像流水一样连续传输,接收方按发送顺序接收数据,适用于需要准确无误传输数据的应用,如网页服务器。
对于 UDP 协议,我们则使用 数据报套接字(SOCK_DGRAM) ,它提供的则是无连接的不可靠 数据传输服务。每个报文段独立传输,可能会丢失或无法保证顺序,适用于对传输速度要求高但可以容忍一定丢包率的应用,如在线视频会议、直播视频。
二、核心 Socket API 详解
下面将按 socket 编程流程顺序介绍所有重要 API,并包括参数/返回值/错误码等。
2.1 socket() --- 创建套接字
函数原型:
cpp
#include <sys/socket.h>
#include <sys/types.h>
/**
* @brief 在通信域中创建一个未绑定的socket,并返回一个文件描述符,该描述符可以在后续对socket进行操作的函数调用中使用
*
* @param domain 指定要创建套接字的通信域。
* AF_UNIX:本地通信,通常用于 UNIX 系统间的进程间通信。
* AF_LOCAL:AF_UNIX 的别名。
* AF_INET:IPv4 互联网协议。
* AF_AX25:业余无线电 AX.25 协议。
* AF_IPX:IPX - Novell 协议。
* AF_APPLETALK:AppleTalk 协议。
* AF_X25:ITU-T X.25 / ISO-8208 协议。
* AF_INET6:IPv6 互联网协议。
* AF_DECnet:DECnet 协议套接字。
* AF_KEY:密钥管理协议,最初用于 IPsec。
* AF_NETLINK:内核用户接口设备。
* AF_PACKET:低级报文段接口。
* AF_RDS:可靠数据报套接字(RDS)协议。
* AF_PPPOX:通用 PPP 传输层,用于设置 L2 隧道(L2TP 和 PPPoE)。
* AF_LLC:逻辑链路控制(IEEE 802.2 LLC)协议。
* AF_IB:InfiniBand 本机寻址。
* AF_MPLS:多协议标签交换。
* AF_CAN:控制器区域网络汽车总线协议。
* AF_TIPC:TIPC,"集群域套接字"协议。
* AF_BLUETOOTH:蓝牙低级套接字协议。
* AF_ALG:与内核加密 API 接口。
* AF_VSOCK:VSOCK(最初为"VMWare VSockets")协议,用于虚拟机和宿主机之间的通信。
* AF_KCM:KCM(内核连接复用器)接口。
* AF_XDP:XDP(快速数据通道)接口。
* @param type 指定要创建的socket类型
* SOCK_STREAM:提供序列化、可靠的、双向的、基于连接的字节流。可以支持带外数据传输机制。
* SOCK_DGRAM:支持数据报(无连接、不可靠的固定最大长度的消息)。
* SOCK_SEQPACKET:为固定最大长度的数据报提供了一个序列化的、可靠的、双向的基于连接的数据传输路径;消费者需要在每次输入系统调用中读取整个报文段。
* SOCK_RAW:提供原始的网络协议访问。
* SOCK_RDM:提供一个不保证顺序的可靠数据报层。
* 自 Linux 2.6.27 以来,type 参数还具有第二个目的:除了指定套接字类型外,它还可以包含任何以下值的按位 OR,以修改 socket() 的行为:
* SOCK_NONBLOCK:在新文件描述符引用的打开文件描述符上设置 O_NONBLOCK 文件状态标志(参见 open(2))。使用此标志可以节省调用 fcntl(2) 来实现相同结果的额外调用。
* SOCK_CLOEXEC:在新文件描述符上设置关闭时执行(FD_CLOEXEC)标志。有关为什么可能有用的原因,请参阅 open(2) 中 O_CLOEXEC 标志的描述。
* @param protocol 指定要与socket一起使用的特定协议。指定协议为 0 会导致 socket() 使用适用于所请求的socket类型的未指定的默认协议
* @return int 文件描述符,如果失败返回-1
*/
int socket(int domain, int type, int protocol);
该函数的功能就是创建一个 socket 套接字。之前介绍过,在 Linux 下 socket 可以看成一个文件进行读写,那么该函数的返回值就是一个文件描述符。
可以看到该函数的可用参数有很多,但是在实际的网络编程及通讯过程中,我们通常使用的如下:
| 参数 | 常用 |
|---|---|
domain |
常用:AF_INET(IPv4)、AF_INET6(IPv6) |
type |
套接字类型:TCP 使用 SOCK_STREAM,UDP 使用 SOCK_DGRAM |
protocol |
通常设为 0,表示自动选择(TCP 自动为 IPPROTO_TCP) |
2.2 bind() ------ 绑定地址信息
函数原型:
cpp
/**
* @brief 当使用 socket(2) 创建套接字时,它存在于一个名称空间(地址族)中,但没有为其分配地址。bind() 将由 addr 指定的地址分配给文件描述符 sockfd 所引用的套接字。addrlen 指定了 addr 指向的地址结构的大小(以字节为单位)。传统上,这个操作被称为"给套接字分配一个名称"
*
* @param sockfd 套接字文件描述符
* @param addr 指定的地址。地址的长度和格式取决于socket的地址族
* @param addrlen addr 指向的地址结构的大小(以字节为单位)。
* @return int 成功 0
* 失败 -1
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数说明:
当我们通过上一节的 socket() 函数创建套接字之后,它存储在一个命名空间(地址族)中,**但是并未绑定地址。**bind() 函数将 addr 指定的地址绑定到文件描述符 sockfd 对应的套接字。addrlen 记录了addr指针指向的地址结构体占用的字节大小。
下面介绍 IP 地址存储过程中涉及的相关数据结构。
IP 地址相关数据类型:
① struct sockaddr 结构体:
cpp
struct sockaddr {
sa_family_t sa_family; // 地址家族,如 AF_INET、AF_INET6、AF_UNIX 等
char sa_data[14]; // 用于存储具体地址数据的数组,其布局取决于地址
}
/*
此结构的唯一目的是将 addr 中传递的结构指针进行转换,以避免编译器警告。
*/
② sockaddr_in 结构体:
cpp
/*
IP 套接字地址被定义为 IP 接口地址和一个 16 位端口号的组合。基本的 IP 协议不提供端口号,它们由高层协议如 udp(7) 和 tcp(7) 实现。在原始套接字中,sin_port 被设置为 IP 协议。
*/
struct sockaddr_in {
sa_family_t sin_family; /* 地址族:AF_INET */
in_port_t sin_port; /* 端口号,网络字节顺序 */
struct in_addr sin_addr; /* 互联网地址 */
};
/*
sin_family 总是设置为 AF_INET。
sin_port 包含端口号,以网络字节顺序表示。低于 1024 的端口号称为特权端口(或有时称为:保留端口)。只有特权进程(在 Linux 中:具有 CAP_NET_BIND_SERVICE 用户命名空间中的权限,控制其网络命名空间)可以绑定到这些套接字。注意,原始 IPv4 协议本身没有端口的概念,它们仅由 tcp(7) 和 udp(7) 等高级协议实现。
sin_addr 存储的是 IP 主机地址。
*/
注意: 网络传输使用大端字节序 (Big Endian) ,而x86 CPU通常是小端字节序 (Little Endian) 。因此在填充端口和IP时,必须使用 htons (host to network short) 等函数进行转换。
③ in_addr结构体:
cpp
/* 互联网地址 */
struct in_addr {
uint32_t s_addr; /* 网络字节顺序中的地址 */
};
struct sockaddr 结构体是所有特定地址结构体(如struct sockaddr_in和struct sockaddr_un)的基类,这些特定结构体继承了 struct sockaddr 的结构并添加了各自所需的额外信息。
由于 struct sockaddr 是所有 API 通用结构体,所以在实际使用中,通常会根据地址类型将其转换为相应的具体结构体类型(如在IPv4 TCP编程中,我们实际填充的是 struct sockaddr_in,最后强制类型转换过去。),以方便访问和操作地址信息。
所以我们需要声明的地址结构体类型应为struct sockaddr_in,并按照网络字节序去设置IP地址和端口。
④ 其他数据类型:
socklen_t相关宏定义如下
cpp
typedef __socklen_t socklen_t;
__STD_TYPE __U32_TYPE __socklen_t;
# define __STD_TYPE typedef
#define __U32_TYPE unsigned int
由上述定义可得,socklen_t实际上是__socklen_t的别名,而__socklen_t实质上是无符号整型的别名
cpp
typedef unsigned int __socklen_t;
综上,socklen_t是 unsigned_int的别名。
2.3 listen() --- 服务端开始监听
函数原型:
cpp
/**
* @brief 将 sockfd 指定的套接字标记为被动套接字,即将用于使用 accept(2) 接受传入的连接请求。
*
* @param sockfd 套接字文件描述符
* @param backlog 指定还未accpet但是已经完成链接的队列长度
* @return int 成功 0
* 失败 -1
*/
int listen(int sockfd, int backlog);
listen()是 Linux 提供的用于TCP网络编程的系统调用,作用是让一个套接字进入监听状态,准备接受连接请求。
当listen()函数被调用之后,sockfd指定的套接字会从一个主动套接字转变为一个被动套接字,表明它将被用来接受进来的连接请求,而不是主动发起连接。accept()函数随后用于响应连接请求。
注意!backlog 是未被及时响应的连接可以被放入队列等待连接,该参数指定等待队列可以容纳的最大连接数
2.4 accept() ------ 服务端端接收连接
函数原型:
cpp
/**
* @brief 从监听套接字 sockfd 的待处理连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回指向该套接字的新文件描述符。新创建的套接字不处于监听状态。原始套接字 sockfd 不受此调用的影响。
*
* @param sockfd 一个使用 socket(2) 创建、使用 bind(2) 绑定到本地地址,并在 listen(2) 后监听连接的套接字。
* @param addr 要么是一个空指针,要么是一个指向 sockaddr 结构的指针,用于返回连接socket的地址。
* @param addrlen 如果 address 是空指针,则为一个空指针;如果 address 不是空指针,则为一个指向 socklen_t 对象的指针,该对象在调用前指定提供的 sockaddr 结构的长度,并在调用后指定存储地址的长度。
* @return int 返回一个新的套接字文件描述符,用于与客户端通信,如果失败返回-1,并设置errno来表示错误原因
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
注意:服务端通信时不再使用监听套接字,而是使用 accept 返回的通信套接字。
2.5 connect() ------ 客户端发起连接
函数原型:
cpp
/**
* @brief 由客户端调用,来与服务端建立连接。
*
* @param sockfd 客户端套接字的文件描述符
* @param addr 指向sockaddr结构体的指针,包含目的地地址信息
* @param addrlen 指定addr指向的结构体的大小
* @return int 成功 0
* 失败 -1,并设置errno以指示错误原因
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数与之前介绍的accept、bind函数参数都相同,只不过是传入 client 客户端自己的 addr 所存储的IP地址。
2.6 send() / recv() ------ 数据收发
数据发送:
cpp
/**
* @brief 用于向另一个套接字传输消息。
*
* @param sockfd 发送套接字的文件描述符。
* @param buf 发送缓冲区,并非操作系统分配为服务端和客户端分配的缓冲区,而是用户为了发送数据,自己维护的字节序列。const修饰表名它是"只读"的,即send函数不会修改这块内存的内容。
* @param len 要发送的数据的字节长度。它决定了从buf指向的缓冲区中将发送多少数据。
* @param flags flags 参数是以下标志之一或多个的按位或。对于大多数应用,这个参数被设置为0,表示不使用任何特殊行为。
* MSG_CONFIRM 告知链路层发生了前进:您从另一端收到了成功的回复。如果链路层没有收到此消息,它将定期重新探测邻居(例如,通过单播 ARP)。仅对 SOCK_DGRAM 和 SOCK_RAW sockets 有效,当前仅对 IPv4 和 IPv6 实现。有关详情,请参阅 arp(7)。
* MSG_DONTROUTE 不要使用网关发送报文段,只发送到直接连接的网络中的主机。通常仅由诊断或路由程序使用。仅为具有路由功能的协议族定义;报文段套接字不支持。
* MSG_DONTWAIT 启用非阻塞操作;如果操作会阻塞,则返回 EAGAIN 或 EWOULDBLOCK。这提供了类似于设置 O_NONBLOCK 标志(通过 fcntl(2) F_SETFL 操作)的行为,但不同之处在于 MSG_DONTWAIT 是一个每次调用的选项,而 O_NONBLOCK 是对打开文件描述符(参见 open(2))的设置,将影响调用进程中的所有线程以及持有引用相同打开文件描述符的其他进程。
* MSG_EOR 终止记录(当支持此概念时,例如 SOCK_SEQPACKET 类型的套接字)。
* MSG_MORE 调用方有更多数据要发送。此标志与 TCP sockets 一起使用,以获得与 TCP_CORK 套接字选项相同的效果(请参阅 tcp(7)),不同之处在于此标志可以基于每次调用设置。自 Linux 2.6 起,此标志还适用于 UDP sockets,并告知内核将使用此标志设置的所有调用发送的数据打包到单个数据报中,仅在执行不指定此标志的调用时才传输。(另请参阅 udp(7) 中描述的 UDP_CORK 套接字选项。)
* MSG_NOSIGNAL 如果面向流的套接字的对等端关闭了连接,则不生成 SIGPIPE 信号。仍会返回 EPIPE 错误。这提供了类似于使用 sigaction(2) 忽略 SIGPIPE 的行为,但 MSG_NOSIGNAL 是每次调用的特性,而忽略 SIGPIPE 设置了一个影响进程中的所有线程的进程属性。
* MSG_OOB 在支持此概念的套接字上发送带外数据(例如,类型为 SOCK_STREAM 的套接字);底层协议还必须支持带外数据。
* @return ssize_t成功发送的字节数。如果出现错误,它将返回-1,并设置errno以指示错误的具体原因。
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send() 的返回值
-
> 0:实际写入到 内核发送缓冲区 的字节数。并不是 表示对端已经收到或确认(只是进入内核缓冲区)。-
对阻塞 socket:通常会尽量写入最多
len字节,但仍可能写入少于len(例如内核缓冲区剩余空间不足、被信号中断等)。 -
对非阻塞 socket:若内核缓冲区空间不足可能返回
-1并errno == EAGAIN/EWOULDBLOCK,或者返回写入的部分字节数 (>0)。
-
-
0:对于send()很少见(对流式 socket 一般不会返回 0,除非 len==0)。 -
-1:出错,检查errno。
数据接收:
cpp
/**
* @brief 从套接字关联的连接中接收数据。
*
* @param sockfd 套接字文件描述符。
* @param buf 接收缓冲区,同样地,此处也并非内核维护的缓冲区。
* @param len 缓冲区长度,即buf可以接收的最大字节数。
* @param flags flags 参数是以下标志之一或多个的按位或。对于大多数应用,这个参数被设置为0,表示不使用任何特殊行为。
* MSG_DONTWAIT 启用非阻塞操作;如果操作会阻塞,则调用失败
* MSG_ERRQUEUE 此标志指定应该从套接字错误队列中接收排队的错误。
* MSG_OOB 此标志请求接收在正常数据流中不会接收到的带外数据。
* MSG_PEEK 此标志导致接收操作从接收队列的开头返回数据,而不从队列中删除该数据。因此,后续的接收调用将返回相同的数据。
* MSG_TRUNC 对于原始(AF_PACKET)、Internet 数据报、netlink和 UNIX 数据报套接字:返回报文段或数据报的实际长度,即使它比传递的缓冲区长。
* MSG_WAITALL 此标志请求操作阻塞,直到满足完整的请求。
* @return ssize_t 返回接收到的字节数,如果连接已经正常关闭,返回值将是0。如果出现错误,返回-1,并且errno变量将被设置为指示错误的具体原因。
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv() 的返回值
-
> 0:已从内核接收缓冲区读到的字节数(应用层得到了这些数据)。 -
0:对端已执行正常关闭 ------ 即连接被有序关闭,表示"EOF"。之后不应再recv()(除非对端新连)。 -
-1:出错,检查errno。
2.7 shutdown() ------ 关闭部分套接字
函数原型:
cpp
/**
* @brief关闭套接字的一部分或全部连接
*
* @param sockfd 套接字文件描述符
* @param how 指定关闭的类型。其取值如下:
* SHUT_RD:关闭读。之后,该套接字不再接收数据。任何当前阻塞在recv调用上的操作都将返回0,表示连接的另一端已经关闭。
* SHUT_WR:关闭写。之后,试图通过该套接字发送数据将导致错误。如果使用此选项,TCP连接将发送一个FIN包给连接的对端,表明此方向上的数据传输已经完成。此时对端的recv调用将接收到0。
* SHUT_RDWR:关闭读写。同时关闭套接字的读取和写入部分,等同于分别调用SHUT_RD和SHUT_WR。之后,该套接字既不能接收数据也不能发送数据。
* @return int 成功 0
* 失败 -1,并设置errno变量以指示具体的错误原因。
*/
int shutdown(int sockfd, int how);
shutdown()函数的作用是关闭某一端的读写功能。
2.8 close() ------ 关闭套接字
该函数在之前的 Linux 系统调用的 I/O 介绍过,本质是将文件描述符关闭。
cpp
#include <unistd.h>
/*
用于关闭一个之前通过open()、socket()等函数打开的文件描述符
int __fd: 这是一个整数值,表示要关闭的文件描述符
return: 成功关闭文件描述符时,close()函数返回0
发送失败,例如试图关闭一个已经关闭的文件描述符或系统资源不足,close()会返回-1
*/
int close(int __fd);
说明:
TCP通信中,套接字也是通过文件描述符操控的,底层同样存储在 struct file 结构体中,socket 相关的数据存在该类型结构体实例的私有数据字段,因此,我们通过close()关闭套接字,效果和关闭文件是类似的,都是使得底层文件描述的引用计数减一,若引用计数减为0则释放套接字相关的资源。
总结
Socket 编程是 Linux 网络开发的基石。通过理解 TCP 、UDP 连接的生命周期、掌握核心 API 的使用细节、以及正确处理字节序问题,你已经迈出了坚实的一步。
希望这篇博客能成为你学习路上的"知识锚点"。欢迎点赞、收藏、评论交流!