【Linux】网络编程基础—套接字

一、套接字基本概念

套接字(Socket)是计算机网络中用于进程间通信的一种机制,通常用于不同主机之间的数据传输。它抽象了底层网络协议的细节,为应用程序提供统一的接口。套接字是IP地址与端口号的组合,用于唯一标识网络中的一个通信端点。

二、套接字工作原理

套接字通过绑定特定的IP地址和端口号,实现数据的发送和接收。通信双方各自创建一个套接字,并通过网络协议(如TCP或UDP)建立连接。数据通过套接字进行传输,发送方将数据写入套接字,接收方从套接字读取数据。

三、协议认识-UDP/TCP

UDP和TCP特点

特性 TCP(传输控制协议) UDP(用户数据报协议)
连接性 面向连接(三次握手建立连接,四次挥手断开) 无连接(无需握手,直接发数据)
可靠性 可靠传输(保证数据不丢、不重、有序到达) 不可靠传输(不保证送达,可能丢包 / 乱序)
有序性 保证数据按发送顺序到达(有序列号机制) 不保证有序(先发的可能后到)
重传机制 丢包会重传(超时重传、快速重传) 无重传(丢包就丢了,不处理)
流量控制 有(滑动窗口机制,避免发送方发太快压垮接收方) 无(发送方只管发,不管接收方能不能处理)
拥塞控制 有(慢启动、拥塞避免等,适配网络拥堵) 无(网络再堵也照常发)
数据边界 无(流式数据,比如发 2 次 10 字节,接收可能一次收 20 字节) 有(数据报边界,发 1 次 10 字节,接收只能收 10 字节)
开销 高(头部 20~60 字节,加握手 / 重传 / 控流等逻辑) 低(头部仅 8 字节,无额外控制逻辑)
速度 慢(可靠性和控流导致延迟高) 快(无额外开销,延迟低)
适用场景 对可靠性要求高的场景 对速度 / 实时性要求高的场景

前置:addrsock结构体

cpp 复制代码
struct sockaddr {
    unsigned short sa_family; // 地址族(如 AF_INET、AF_INET6)
    char sa_data[14];         // 协议特定的地址信息
};
/* sa_family:指定地址族,常见值包括:
AF_INET:IPv4 地址。
AF_INET6:IPv6 地址。
AF_UNIX:Unix 域套接字。
sa_data:存储具体的地址和端口信息,格式由协议族决定。 */

sockaddr 是网络编程中用于表示套接字地址的通用结构体,定义在 <sys/socket.h> 头文件中。它通常用于存储 IP 地址和端口信息,支持多种协议族(如 IPv4、IPv6 等)。

结构体常见派生类

sockaddr_in(IPv4)
cpp 复制代码
struct sockaddr_in {
    short sin_family;         // 地址族(AF_INET)
    unsigned short sin_port;  // 端口号(网络字节序)
    struct in_addr sin_addr;  // IPv4 地址
    char sin_zero[8];         // 填充字段(未使用)
};
sockaddr_in6(IPv6)
cpp 复制代码
struct sockaddr_in6 {
    unsigned short sin6_family;   // 地址族(AF_INET6)
    unsigned short sin6_port;     // 端口号(网络字节序)
    unsigned long sin6_flowinfo;  // 流信息
    struct in6_addr sin6_addr;    // IPv6 地址
    unsigned int sin6_scope_id;   // 作用域 ID
};

注意事项

  • 字节序:端口和 IP 地址需转换为网络字节序(大端)。
  • 地址族匹配:确保 sa_family 与使用的套接字类型一致。
  • 通用性:sockaddr 设计为通用结构体,实际使用时应优先选择具体的派生结构体。

UDP

相关接口认识

UDP 作为无连接、轻量级的传输层协议,其编程接口相比 TCP 更简洁,核心就是 socket()bind()sendto()recvfrom() 四个接口。

1. 字节序转换核心接口(htons/htonl/ntohs/ntohl)

功能定位

网络协议规定所有传输的多字节数据(如端口、32 位 IP 整数)必须使用网络字节序(大端序),而不同机器的本地字节序可能是小端(主流)或大端,因此在绑定 / 指定地址时,必须通过这些接口完成本地序 ↔ 网络序的转换,否则会出现端口 / IP 解析错误。

接口说明
  • uint16_t htons(uint16_t hostshort):Host to Network Short,将 16 位本地序端口号转为网络序(UDP/TCP 端口都是 16 位,必用);
  • uint32_t htonl(uint32_t hostlong):Host to Network Long,将 32 位本地序 IP 整数转为网络序(如 INADDR_ANY 转换);
  • uint16_t ntohs(uint16_t netshort):Network to Host Short,将 16 位网络序端口号转回本地序(接收数据后解析端口时用);
  • uint32_t ntohl(uint32_t netlong):Network to Host Long,将 32 位网络序 IP 整数转回本地序(极少用,通常用 inet_ntop 直接转字符串)。
核心使用场景
  • 绑定端口:local_addr.sin_port = htons(8080);
  • 指定目标端口:server_addr.sin_port = htons(8080);
  • 绑定所有网卡:local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  • 解析接收的客户端端口:ntohs(client_addr.sin_port);

2.int socket(int domain, int type, int protocol);

功能定位

这是网络编程的第一步,作用是创建一个套接字描述符(简称 sockfd),相当于给程序分配一个专属的 "通信管道",后续所有的发送、接收操作都要基于这个 fd 完成。

参数解读
  • domain:指定通信使用的地址族,决定网络协议版本。日常开发中最常用的是 AF_INET(对应 IPv4 协议),如果需要支持 IPv6 则用 AF_INET6
  • type:指定套接字的通信类型,UDP 必须用 SOCK_DGRAM(数据报类型),这个参数直接决定了通信是无连接、有数据边界的 UDP 模式;如果填 SOCK_STREAM 则是 TCP 模式。
  • protocol:指定具体的传输协议,通常填 0 即可 ------ 系统会根据前面的 type 参数自动匹配对应的协议(比如 SOCK_DGRAM 对应 UDP,SOCK_STREAM 对应 TCP),无需手动指定 IPPROTO_UDPIPPROTO_TCP
返回值

成功时返回一个非负整数(就是套接字 fd,可理解为 "管道编号");失败时返回 -1,此时可以用 perror("socket") 打印具体的错误原因,比如权限不足、参数填错等。

3.int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

功能定位

把创建好的套接字 fd 绑定到本地的 IP 地址和端口号上。对 UDP 来说,服务端必须调用 bind 绑定固定端口(比如 8080),否则客户端无法找到对应的服务端;客户端则可选调用 ------ 如果不绑定,系统会在发送数据时自动分配一个随机端口。

参数解读
  • sockfd:要绑定的套接字 fd,必须是 socket() 调用成功返回的有效 fd,且未被关闭。
  • addr:指向存储本地地址信息的结构体,虽然参数类型是通用的 struct sockaddr,但实际开发中我们会用更贴合 IPv4 的 struct sockaddr_in 结构体,传参时强转为 struct sockaddr* 即可。这个结构体需要提前初始化,包括地址族(和 socket()domain 一致)、端口号(必须转网络序)、IP 地址(通常填 INADDR_ANY 表示绑定本机所有网卡,一般不进行手动绑定特定IP)。
  • addrlen:地址结构体的长度,直接填 sizeof(struct sockaddr_in) 就能满足 IPv4 场景的需求。

4.ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

功能定位

UDP 最核心的发送接口,作用是把缓冲区里的数据发送到指定的目标地址(对方的 IP + 端口)。因为 UDP 是无连接的,所以每次发送都需要明确指定目标地址,这也是它和 TCP 的 send() 最核心的区别。

参数解读
  • sockfd:发送数据用的套接字 fd,需是有效且未关闭的。
  • buf:指向要发送的数据缓冲区,比如存储字符串、字节数组的内存地址,这个参数是只读的,函数不会修改缓冲区内容。
  • len:要发送的数据长度,单位是字节,比如发送字符串时可用 strlen(buf),发送字节数组则填数组的实际长度。
  • flags:发送标志位,日常开发中填 0 即可(表示阻塞发送,直到数据发出或出错);如果需要非阻塞发送,可填 MSG_DONTWAIT,但新手建议先掌握默认的阻塞模式。
  • dest_addr:指向目标地址的结构体,和 bind()addr 类似,需提前初始化对方的 IP 地址和端口号(端口同样要转网络序)。
  • addrlen:目标地址结构体的长度,填 sizeof(struct sockaddr_in) 即可。
返回值

成功时返回实际发送的字节数(UDP 特性:要么发送全部数据,要么失败,不会出现发送部分数据的情况);失败时返回 -1,常见错误有网络不可达、目标地址错误、fd 无效等。

5.ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

功能定位

UDP 核心的接收接口,作用是从套接字中读取接收到的数据,同时获取发送方的地址(IP + 端口)------ 这也是 UDP 无连接特性的体现:接收数据时才知道数据是谁发的。

参数解读
  • sockfd:接收数据用的套接字 fd,需是有效且绑定了端口的(服务端必须 bind,客户端若未 bind 则系统自动分配端口)。
  • buf:指向存储接收数据的缓冲区,函数会把接收到的数据写入这个缓冲区,需提前分配足够的内存(比如定义 char buf[1024])。
  • len:缓冲区的最大长度,避免数据溢出,比如 sizeof(buf)
  • flags:接收标志位,默认填 0(阻塞接收,直到有数据到来);MSG_DONTWAIT 同样用于非阻塞接收。
  • src_addr:指向存储发送方地址的结构体,调用后会自动填充对方的 IP 和端口,方便后续回复数据。
  • addrlen:传入传出参数,调用前要初始化为地址结构体的长度(比如 sizeof(struct sockaddr_in)),函数会根据实际地址长度修改这个值,因此必须传指针。
返回值

成功时返回实际接收的字节数;失败时返回 -1,常见错误有 fd 被关闭、缓冲区过小(但不会溢出,只会截断数据)等;如果是非阻塞模式,无数据时也会返回 -1,需结合 errno 判断。

UDP套接字创建流程-client-server通信实现

创建UDP套接字实现网络通信的步骤简单并且公式化

server端:创建套接字->绑定端口->循环收取数据

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUF_LEN 1024

int main() {
    // 1. 创建UDP套接字:socket(AF_INET, SOCK_DGRAM, 0)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 绑定地址:bind(sockfd, &addr, sizeof(addr))
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 网络序转换:htonl()
    serv_addr.sin_port = htons(PORT); // 网络序转换:htons()
    bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    // 3. 接收数据:recvfrom(sockfd, buf, len, 0, &cli_addr, &len)
    char buf[BUF_LEN];
    struct sockaddr_in cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    while (1) {
        int n = recvfrom(sockfd, buf, BUF_LEN, 0, (struct sockaddr*)&cli_addr, &cli_len);
        buf[n] = '\0';
        printf("收到客户端[%s:%d]:%s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
    }
    close(sockfd);
    return 0;
}

client端:创建套接字->指定服务端IP->发送数据

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define BUF_LEN 1024

int main() {
    // 1. 创建UDP套接字:socket(AF_INET, SOCK_DGRAM, 0)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 配置服务端地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
    serv_addr.sin_port = htons(SERV_PORT); // 网络序转换:htons()

    // 3. 发送数据:sendto(sockfd, buf, len, 0, &serv_addr, sizeof(addr))
    char buf[BUF_LEN] = "Hello UDP Server!";
    sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    printf("已发送:%s\n", buf);

    close(sockfd);
    return 0;
}

TCP

相关接口认识

TCP 作为面向连接、可靠的传输层协议,其编程接口相比 UDP 更复杂,核心新增接口包含 listen ()、accept ()、connect ()、send ()、recv (),基础的 socket ()、bind () 及字节序转换接口与 UDP 通用(不再重复)。

1.int listen(int sockfd, int backlog);

功能定位

服务端调用,将已绑定的套接字从 "主动态" 转为 "被动态",使其监听客户端的连接请求。TCP 是面向连接的协议,服务端必须先监听端口,才能接收客户端的连接。

参数解读

sockfd:已绑定地址的套接字 fd(由 socket () 创建、bind () 绑定);backlog:指定内核为该套接字维护的 "未完成连接队列 + 已完成连接队列" 的最大长度(如 5、10),超出的连接请求会被拒绝,新手填 5 即可满足基础场景。

返回值

成功返回 0;失败返回 -1,可通过 perror ("listen") 排查错误(如 fd 未绑定、参数非法)。

2.int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能定位

服务端阻塞等待客户端的连接请求,当有客户端发起连接(三次握手完成),会创建一个新的套接字 fd 用于与该客户端通信,原监听 fd 继续监听其他客户端。

参数解读

sockfd:监听状态的套接字 fd(listen () 调用后的 fd);addr:指向存储客户端地址信息的结构体,调用后会自动填充客户端的 IP 和端口,可传 NULL 表示不关心客户端地址;addrlen:传入传出参数,调用前初始化为地址结构体长度(sizeof (struct sockaddr_in)),调用后会更新为实际地址长度,必须传指针。

返回值

成功返回新的通信套接字 fd(用于和该客户端收发数据);失败返回 -1(如 fd 未监听、被信号中断)。

3.int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能定位

客户端调用,主动向服务端发起 TCP 连接(触发三次握手)。只有连接建立成功后,才能通过该套接字收发数据。

参数解读

sockfd:客户端创建的套接字 fd(socket () 返回);addr:指向服务端地址结构体,需初始化服务端的 IP 和端口(端口转网络序);addrlen:服务端地址结构体的长度(sizeof (struct sockaddr_in))。

返回值

成功返回 0(三次握手完成,连接建立);失败返回 -1(如服务端未监听、网络不可达、端口错误)。

4.ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能定位

TCP 核心发送接口,向已建立连接的对端发送数据。因 TCP 是流式协议,send () 可能返回小于 len 的值(如内核缓冲区满),需循环发送确保数据全部发出。

参数解读

sockfd:已建立连接的通信套接字 fd(客户端 connect () 成功后的 fd、服务端 accept () 返回的新 fd);buf:要发送的数据缓冲区(只读);len:要发送的数据长度(字节);flags:默认填 0(阻塞发送),与 UDP sendto () 的 flags 用法一致(如 MSG_DONTWAIT 为非阻塞)。

返回值

成功返回实际发送的字节数(可能小于 len);失败返回 -1(如连接断开、fd 无效)。

5.ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能定位

TCP 核心接收接口,从已建立连接的套接字中读取数据。因 TCP 是流式无边界协议,recv () 读取的字节数可能小于缓冲区长度,需循环读取直到获取完整数据。

参数解读

sockfd:已建立连接的通信套接字 fd;buf:存储接收数据的缓冲区;len:缓冲区最大长度(避免溢出);flags:默认填 0(阻塞接收),无数据时会阻塞等待。

返回值

成功返回实际接收的字节数;返回 0 表示对端关闭连接(四次挥手完成);失败返回 -1(如连接中断、fd 无效)。

TCP套接字创建流程-client-server通信实现

TCP 通信需遵循 "服务端监听→客户端连接→双向收发" 的固定流程,核心是三次握手建立连接、基于新 fd 通信。

server 端:创建套接字→绑定端口→监听端口→接受连接→循环接收数据

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUF_LEN 1024

int main() {
    // 创建TCP套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);
    bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    // 监听端口
    listen(listen_fd, 5);

    // 接受连接
    struct sockaddr_in cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
    printf("客户端[%s:%d]已连接\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));

    // 接收数据
    char buf[BUF_LEN];
    while (1) {
        int n = recv(conn_fd, buf, BUF_LEN-1, 0);
        if (n <= 0) {
            printf("客户端断开连接\n");
            break;
        }
        buf[n] = '\0';
        printf("收到客户端:%s\n", buf);
    }

    close(conn_fd);
    close(listen_fd);
    return 0;
}

client 端:创建套接字→连接服务端→发送数据

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define BUF_LEN 1024

int main() {
    // 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 连接服务端
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(SERV_IP);
    serv_addr.sin_port = htons(SERV_PORT);
    connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    // 发送数据
    char buf[BUF_LEN] = "Hello TCP Server!";
    send(sockfd, buf, strlen(buf), 0);
    printf("已发送:%s\n", buf);

    close(sockfd);
    return 0;
}
相关推荐
Java小白笔记2 小时前
Linux中使用systemd服务单元定时任务
linux·服务器·网络
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- 补充包】
开发语言·前端·数据结构·数据库·c++·算法·蓝桥杯
夏乌_Wx2 小时前
Linux 进程间通信 IPC 总结:管道 + 信号量 + 共享内存 + 消息队列(附代码)
linux·数据结构·算法
迁 凉2 小时前
怎么把一台ubuntu主机作为服务器,给别的xshell连接
运维·服务器·ubuntu
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(三):日志宏与Buffer缓冲区模块
运维·服务器·网络·高并发·muduo库·日志宏·缓冲区设计
~莫子2 小时前
Docker镜像构建
运维·docker·容器
lucia_zl2 小时前
linux收集进程性能数据
linux·运维·chrome
Byte不洛2 小时前
手写一个C++ TCP服务器实现自定义协议(顺便解决粘包问题)
linux·c++·操作系统·网络编程·tcp
董可伦2 小时前
Flink DataStream2Table 总结
服务器·python·flink