【linux(四)】套接字编程--socket套接字及其接口认识

🎬 个人主页:HABuo

📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、Socket套接字是什么

📚二、网络套接字分类

📚三、sockaddr结构体

📚四、网络套接字接口

[📖4.1 socket](#📖4.1 socket)

[📖4.2 bind](#📖4.2 bind)

[📖4.3 listen](#📖4.3 listen)

[📖4.4 accept](#📖4.4 accept)

[📖4.5 connect](#📖4.5 connect)

[📖4.6 sendto、write](#📖4.6 sendto、write)

[📖4.7 recvfrom、read](#📖4.7 recvfrom、read)

📚五、总结


前言:

本篇博客我们正式进入网络通信编程的章节,今天我们主要来了解套接字及其接口,这些接口可能比较复杂,但是没有关系,大家先看懂,我将在后面的博客中,逐步地实现各个协议版本的服务端客户端代码,经过几轮的使用给,你也定会熟悉的!


📚一、Socket套接字是什么

网络套接字(Socket)是应用程序与内核网络协议栈之间的"通信门户"。它是一个抽象概念,允许程序像操作普通文件一样(通过文件描述符)发送和接收网络数据。可以说,Socket就是实现网络通信的编程接口

Socket的本质:一种特殊的文件描述符

在Linux中,"一切皆文件"。当你创建一个Socket时,内核会返回一个非负整数 ,称为套接字描述符(Socket Descriptor) ,本质上与文件描述符(File Descriptor)是同一概念。你可以用read()write()close()等系统调用来操作它,就像操作文件一样。

  • 对于普通文件:read()从磁盘读数据,write()写入磁盘。

  • 对于Socket:read()从网络接收缓冲区读数据,write()将数据写入发送缓冲区,由内核协议栈负责发送出去。

验证 :在Linux终端输入ls -l /proc/$$/fd,可以看到当前进程打开的文件描述符,其中也包括socket类型的fd。

📚二、网络套接字分类

套接字是包含多种数据类型

  • 域间套接字,用于同一个主机内编程的使用,原理是使用文件路径进行标识,看到同一个文件
  • 原始套接字,通常用于在网络协议栈中,跨过传输层,直接封装或调用网络层,数据链路层的系统调用接口,通常是用于编写一些网络工具,例如抓包工具等
  • 网络套接字,用户间的网络通信,即跨主机的在网络中进行通信的要使用到的数据类型,基于网络套接字进行网络编程

三种套接字对应的套接字编程的函数接口就要有三套,三套使用也太不方便了,利用多态的思想将三种套接字的使用合并为同一套函数,因此下面我们就来解读一套函数应对不同场景的原理:

Socket的分类(根据协议族和类型)

分类角度 类型 说明
协议族 AF_INET(IPv4) AF_INET6(IPv6) AF_UNIX(本地进程间通信) 决定使用哪种网络协议
套接字类型 SOCK_STREAM(流式) SOCK_DGRAM(数据报) SOCK_RAW(原始套接字) 决定传输层行为
传输协议 TCP(IPPROTO_TCP) UDP(IPPROTO_UDP) 实际使用的协议

常用组合

  • AF_INET + SOCK_STREAM → TCP

  • AF_INET + SOCK_DGRAM → UDP

  • AF_UNIX + SOCK_STREAM → 本地进程间通信(不经过网络协议栈,高性能)

使用示例:

cpp 复制代码
int socket(int domain, int type, int protocol);
// 例如:创建TCP socket
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
// 创建UDP socket
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

📚三、sockaddr结构体

struct sockaddrstruct sockaddr_instruct sockaddr_un 是三个紧密相关的数据结构,用于传递套接字地址信息 。它们的设计是为了让套接字API(如bindconnectaccept)能够统一处理不同协议族的地址(IPv4、IPv6、Unix域等)。

struct sockaddr ------ 通用套接字地址结构

实际作用 :几乎从不直接操作 sockaddr 变量。它仅仅作为函数参数的类型,要求具体地址结构强制转换后传入。例如:

cpp 复制代码
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

struct sockaddr_in ------ IPv4地址结构

cpp 复制代码
struct sockaddr_in {
    sa_family_t    sin_family;   // 总是 AF_INET
    in_port_t      sin_port;     // 端口号(16位,网络字节序)
    struct in_addr sin_addr;     // IPv4地址(32位,网络字节序)
    unsigned char  sin_zero[8];  // 填充,使结构体大小与sockaddr一致(16字节)
};

struct in_addr {
    in_addr_t s_addr;            // 32位IPv4地址
};
  • sin_family :必须设为 AF_INET

  • sin_port :端口号,必须用 htons() 转换为网络字节序。

  • sin_addr.s_addr :IPv4地址,必须用 htonl()inet_pton() 转换为网络字节序。

  • sin_zero[8] :填充字段,通常用 memset 全部置0,目的是让 sockaddr_in 的大小正好等于 sockaddr(16字节),这样两种结构可以安全地进行强制转换。

示例(服务器绑定本地地址):

cpp 复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0
// 或指定具体IP
// inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

示例(客户端连接):

cpp 复制代码
struct sockaddr_in server_addr;
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

struct sockaddr_un ------ Unix域套接字地址结构

cpp 复制代码
struct sockaddr_un {
    sa_family_t sun_family;     // 总是 AF_UNIX 或 AF_LOCAL
    char        sun_path[108];  // 文件系统路径(以'\0'结尾)
};
  • sun_family :设为 AF_UNIXAF_LOCAL

  • sun_path:一个文件路径字符串(长度不超过108字节,包括结尾NUL)。这个路径会在文件系统中创建一个特殊文件,用于进程间通信。

特点

  • 不涉及网络协议栈,纯内存/文件系统通信,效率高。

  • 支持流式SOCK_STREAM)和数据报SOCK_DGRAM)。

  • 路径可以是抽象地址 (以 \0 开头,不占用文件系统名),比如 "\0hidden_socket"

示例(Unix域服务器监听):

cpp 复制代码
struct sockaddr_un addr;
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
unlink("/tmp/mysocket");  // 删除旧文件
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);

示例(客户端连接):

cpp 复制代码
struct sockaddr_un addr;
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));

三者的关系与使用规则

结构 对应的协议族 大小 主要字段 适用函数参数类型
struct sockaddr 通用(不直接使用) 16字节 sa_family, sa_data 所有套接字函数的形参类型
struct sockaddr_in IPv4(AF_INET) 16字节 sin_family, sin_port, sin_addr 需强制转换为 (struct sockaddr*)
struct sockaddr_un Unix域(AF_UNIX) 110字节+ sun_family, sun_path 需强制转换为 (struct sockaddr*)

上面那么多只需记住下面几句

  • struct sockaddr:通用类型,仅用于函数形参和类型转换,不直接实例化。

  • struct sockaddr_in:IPv4专用,用得最多,必须设置端口和IP的网络字节序。

  • struct sockaddr_un:Unix域专用,用于本地高效IPC,使用文件系统路径。

sockaddr 是"抽象基类",sockaddr_insockaddr_un 是"具体派生类";在C语言中通过强制转换实现多态,所有套接字函数都用 struct sockaddr* 作为地址参数。

📚四、网络套接字接口

socket函数的作用是创建套接字, 套接字的本质是一个文件描述符, 这个套接字会在后续中起重要作用. 第二个函数bind, 它的作用是: 将本服务的IP和端口号绑定到操作系统内部,供外部来访问. 而最后三个函数: listen和accept是TCP通信中需要用到的, 它们表示: listen用于开始监听是否有请求到来. accept用于将到来的请求拿到内存当中做解析. 而connect函数用于发送TCP请求的一方调用,与服务器建立连接

📖4.1 socket

socket() ------ 创建一个通信端点

cpp 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 作用 :创建一个套接字,内核返回一个文件描述符(非负整数),后续操作均使用这个fd。

  • 参数

    • domain:协议族(地址族)。

      • AF_INET:IPv4

      • AF_INET6:IPv6

      • AF_UNIX:本地进程间通信(Unix域)

    • type:套接字类型。

      • SOCK_STREAM流式套接字,提供可靠、面向连接的字节流(通常对应TCP)。

      • SOCK_DGRAM数据报套接字,提供不可靠、无连接的消息(通常对应UDP)。

      • SOCK_RAW:原始套接字,可直接操作IP层。

    • protocol:指定具体协议,通常设为0,表示使用给定domaintype的默认协议(TCP或UDP)。也可以显式指定IPPROTO_TCPIPPROTO_UDP

  • 返回值 :成功返回文件描述符(>=0),失败返回-1并设置errno

示例:

cpp 复制代码
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
    perror("socket");
    exit(EXIT_FAILURE);
}

📖4.2 bind

bind() ------ 将套接字绑定到本地地址

cpp 复制代码
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用 :将套接字与一个本地IP地址和端口关联起来。服务器必须bind一个众所周知的端口,客户端一般不需要显式bind(内核会自动分配临时端口)。

  • 参数

    • sockfd:由socket()返回的套接字描述符。

    • addr:指向struct sockaddr的指针,实际上根据不同协议使用不同的结构体(如struct sockaddr_in),需要强制转换。

    • addrlenaddr结构体的长度,使用sizeof即可。

  • 返回值:成功返回0,失败返回-1。

重点 :需要构造sockaddr_in结构体,并注意端口号和IP地址必须使用网络字节序 (通过htonshtonl转换)。

cpp 复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);                    // 端口8888
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);      // 0.0.0.0,监听所有网卡

if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("bind");
    close(listen_fd);
    exit(EXIT_FAILURE);
}

📖4.3 listen

listen() ------ 监听连接

cpp 复制代码
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • 作用 :将套接字从主动连接状态转变为被动监听状态,告诉内核该套接字用于接受传入连接。同时指定连接队列的最大长度

  • 参数

    • sockfd:已经bind但尚未监听的套接字。

    • backlog:已完成连接队列(全连接队列)的最大长度。内核通常会为这个值加上一些余量。典型值设置为128或更高(视系统限制SOMAXCONN而定)。

  • 返回值:成功返回0,失败返回-1。

内核队列机制(后面会详细介绍,这里只见识见识这个接口知道怎么用即可)

  • 半连接队列(SYN队列):收到SYN但尚未完成三次握手的连接。

  • 全连接队列(Accept队列):已完成三次握手,等待accept()取走的连接。

当全连接队列满时,新的连接行为取决于系统设置(默认会丢弃或发送RST)。

示例

cpp 复制代码
if (listen(listen_fd, 128) == -1) {
    perror("listen");
    close(listen_fd);
    exit(EXIT_FAILURE);
}

📖4.4 accept

accept() ------ 接受一个连接

cpp 复制代码
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 作用 :从监听套接字的全连接队列 中取出第一个连接,并返回一个新的套接字描述符,专门用于与该客户端通信。原监听套接字继续监听新连接。

  • 参数

    • sockfd:监听套接字。

    • addr:输出参数,返回客户端的地址结构(如IP和端口)。如果不需要,可以设为NULL

    • addrlen:输入输出参数,传入时指向addr结构体的大小,返回时填充实际地址长度。

  • 返回值 :成功返回新的连接套接字fd,失败返回-1。如果监听套接字是非阻塞模式且没有等待的连接,会返回-1并设置errnoEAGAINEWOULDBLOCK

注意 :默认accept()会阻塞直到有连接到来。若要非阻塞,需先设置O_NONBLOCK标志。(通常设置就是默认)

示例:

cpp 复制代码
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
    perror("accept");
    continue; // 或退出
}
printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr),
       ntohs(client_addr.sin_port));

📖4.5 connect

connect() ------ 建立连接(客户端)

cpp 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:客户端主动发起TCP三次握手,与服务器建立连接。

  • 参数

    • sockfd:客户端的套接字描述符。

    • addr:服务器地址(IP+端口),使用网络字节序。

    • addrlen:地址结构体长度。

  • 返回值:成功返回0,失败返回-1。

阻塞行为 :默认阻塞直到连接成功或失败(超时通常75秒)。可设置为非阻塞模式,并用select/epoll等待。

示例:

cpp 复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("connect");
    close(sockfd);
    exit(EXIT_FAILURE);
}

注意 :客户端通常不需要bind,内核会自动选择一个临时端口和本机IP。如果需要指定源IP(多网卡),可以显式bind后再connect

📖4.6 sendto、write

sendto()write() ------ 发送数据

cpp 复制代码
#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);
ssize_t write(int sockfd, const void *buf, size_t len);
  • 作用sendto() 是 UDP 数据发送的核心系统调用 。它最大的特点是:每次发送时都直接指定目标地址(IP + 端口),无需预先建立连接

  • 参数

    • sockfd :套接字描述符(通常是 UDP 类型 SOCK_DGRAM)。

    • buf:要发送的数据缓冲区(用户态)。

    • len:要发送的字节数(不能超过底层限制,UDP 一般建议不超过 64KB-头部)。

    • flags :控制发送行为(常用 0MSG_DONTWAITMSG_NOSIGNAL)。

    • dest_addr :目标地址结构(IP 和端口),采用网络字节序

    • addrlendest_addr 指向结构体的长度(例如 sizeof(struct sockaddr_in))。

  • 返回值>0:实际发送的字节数(UDP 下通常等于 len,除非发生错误)。-1:发生错误,错误码存入 errno

📖4.7 recvfrom、read

recvfrom()read() ------ 接收数据

cpp 复制代码
#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);
ssize_t read(int sockfd, void *buf, size_t len);
  • 作用recvfrom() 是 Linux 网络编程中用于从套接字接收数据的系统调用,最重要的特点是它能够同时接收数据和获取发送方的地址。它与 read()recv() 的最大区别在于:recvfrom() 不仅返回数据,还会填充数据来源的地址结构(例如 IP 和端口),使其成为 UDP 服务器 以及任何需要知道数据来源场景的标准选择。

  • 参数

    • sockfd:套接字描述符(UDP 或 TCP 都可以使用,但 UDP 更常用)。

    • buf:用户缓冲区指针,用于存放接收到的数据。

    • len:缓冲区大小(最多可接收的字节数)。

    • flags :控制接收行为的标志(常用 0MSG_DONTWAITMSG_PEEK 等)。

    • src_addr :输出参数,返回数据发送方的协议地址(如 IP + 端口) 。如果不需要,可以填 NULL

    • addrlen值-结果参数传入时指向 src_addr 的缓冲区长度的地址返回时填充实际地址长度

  • 返回值

    • >0:成功接收的字节数。

    • 0:对端已关闭连接(仅对 TCP 有意义;UDP 收到长度为 0 的数据报可能返回 0,但极少见)。

    • -1:发生错误,错误码存于 errno

TCP流式读取建议:由于TCP没有消息边界,应用层协议通常需要自行定义分隔(如固定长度头、特殊分隔符)。循环读取直到满足要求。

示例(简单接收)

cpp 复制代码
char buffer[1024];
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);

上述讲了那么多的接口,它们是不是系统调用呢?

实际上它们大多数是标准C库函数中封装的,但是这些套接字API在底层依然是通过系统调用实现的,只不过这个映射过程并不是简单的一对一 ,本质的调用逻辑:将调用包裹成一个请求,再通过软中断指令等底层机制触发一个"陷入内核"的动作来执行真正的系统调用

cpp 复制代码
你的代码: socket()
    │
    ▼
用户态 (User Space)      内核态 (Kernel Space)
┌─────────────────┐      ┌────────────────────────────────────┐
│  glibc 封装函数  │      │            Linux Kernel            │
│   (socket())    │      │                                    │
│        │        │      │                                    │
│  触发系统调用中断  │──────│──► 系统调用表查找函数入口            │
└─────────────────┘      │   │                               │
                         │   ▼                               │
                         │  1. (旧) socketcall() 统一入口      │
                         │  2. (新)独立的 sys_socket() 接口    │
                         │                                    │
                         │  随后执行协议栈代码完成工作           │
                         └────────────────────────────────────┘

因此,虽然你调用的是C库函数,但从本质上讲,这些套接字接口的全部核心工作,确实是由内核的系统调用完成的。


📚五、总结

本篇博客我们主要了解了套接字的定义及其接口使用,小结一下:

socket套接字的本质:一种网络文件描述符,通过这种方式,可以类似文件操作,从而完成网络数据的发送与接收。是应用层到传输层的中间件!

**socketaddr结构体:**通过一套方法,应对三种套接字种类的编程,体现多态的思想!

套接字接口使用总结:

cpp 复制代码
socket(AF_INET, SOCK_DGRAM, 0)
1. AF_INET针对IPv4(常用),AF_INET6针对IPv6
2. SOCK_DGRAM针对数据报,SOCK_STREAM针对字节流

bind(_sockfd, (struct sockaddr *)&local, (socklen_t)sizeof(local))
1. _sockfd:socket的返回值
2. local:绑定到对应的IP地址和端口(而这些信息我们已经初始化这个结构体中)

recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len)
1. _sockfd:socket的返回值(这是服务端(接收信息端)创建的套接字的返回值不要理解错误)
2. buffer:用来接收数据存储的缓冲区
3. sizeof(buffer) - 1:文件字符串的读取不默认带'\0',-1为了下面的代码在结尾处+0
4. 0:阻塞式接收
5. (struct sockaddr *)&peer:输出型参数,给一个空的结构体,去拿接收客户端的IP和端口
6. &len:输入输出型参数:拿对应填充结构体的大小

sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server))
1. _sockfd:socket的返回值(这是客户端(发送信息端)创建的套接字的返回值不要理解错误)
2. message.c_str():发送的信息,这里.c_str()是string里的成员函数,只是转成c式的字符串,后面带'\0'而已
3. message.size():发送信息的大小
4. 0:常用参数为0
5. (struct sockaddr*)&server:这里不是输出型参数,是为了告诉服务端发送信息的这一端的IP和端口
6. sizeof(server):这个结构体的字段长度,用sizeof不计算后面'\0'
相关推荐
流年如夢1 小时前
顺序表 -->增、删、查、改等详细操作
c语言·数据结构
凤年徐1 小时前
命令行进度条完全指南:倒计时、缓冲区刷新与动态下载
linux
cany10001 小时前
C++ -- 模板使用进阶
开发语言·c++
MetrixAeroCore2 小时前
全球物联网卡管理平台是什么?定制化服务赋能企业出海运维
运维·物联网
我不是懒洋洋2 小时前
手写一个布隆过滤器:从原理到工业级实现
c语言
北山有鸟2 小时前
address-cell& size-cell
linux·网络
小年糕是糕手2 小时前
【C/C++刷题集】栈、stack、队列、queue核心精讲
c语言·开发语言·数据结构·数据库·c++·算法·蓝桥杯
机跃2 小时前
指针(c++)
开发语言·c++
小则又沐风a2 小时前
基础的开发工具(Linux)
linux·运维·服务器