Linux系统网络编程

网络编程

咱们之前所学的进程间通信(IPC)有很多种方式,管道(匿名管道,命名管道,标准流管道),消息队列,共享内存,信号量,信号等基本上都是在同一台PC 上进行的。

那么在不同PC 的进程间该如何通信呢?一般的我们是有两种方法,一种是借助硬件的通讯端口(比方说串口),但是由于这些端口一般上都存在传输距离的限制,而且也不太方便多机相互通讯,二一种就是使用网线,借助网络来实现不同计算机之间的数据传输,今天我们就来一起学习网络通信。

OSI

OSI 系统模型是国际化标准组织(ISO)为了实现计算机网络标准化颁布的参考模型,根据网络中数据传输的过程,将该模式分为7个层,每一层都向上一层提供服务,同时使用下层提供的服务。

层次

名称

功能

7

应用层

计算机网络和最终用户的接口

6

表示层

数据编码,数据压缩,数据加密

5

会话层

提供面向用户的连接服务,并为会话活动提供有效组织和同步的手段,为数据传输提供控制和管理。

4

传输层

实现通讯子网端到端的可靠传输,传输层下面的三层属于通讯子网。

3

网络层

实现分别位于不同网络的源节点与目的节点的数据包传输。

2

数据链路层

通过物理层提供的BIT 流服务,在相邻节点之间建立链路,对传输中的差错进行检错和纠错,向网络层提供无差错的传输

1

物理层

提供建立,维护和释放物理连接的方法,实现物理通道上的BIT 流传输。

TCP/IP

TCP/IP 协议的四层结构模型获得了更广泛的使用。TCP/IP 协议是Internet 事实上的工业标准。

TCP三次握手(建立)和四次挥手(断开)

优点

可靠传输:采用 “三次握手” 建立连接、“四次挥手” 释放连接,确保通信双方状态同步;同时通过确认应答(ACK) 、重传机制(超时重传、快速重传),保障数据无丢失交付。

有序交付:为每个数据段分配序号,接收端按序号重组数据,避免因网络延迟导致的乱序问题。

流量控制:基于滑动窗口机制,接收端根据自身缓存能力动态告知发送端可接收的数据量,防止发送端因速率过快导致接收端数据溢出。

拥塞控制:通过慢启动、拥塞避免、快重传、快恢复等算法,实时感知网络拥塞状态并调整发送速率,减少网络拥塞对整体传输的影响。

缺点

传输延迟较高:连接建立 / 释放的握手 / 挥手过程、确认应答、重传等机制会增加额外的网络开销,导致传输延迟高于 UDP,不适合对实时性要求严苛的场景。

资源占用大:需维护连接状态(如序号、窗口大小、拥塞窗口等),且每个连接需独立的资源管理,在高并发场景下会消耗更多的服务器 CPU 与内存资源。

灵活性低:固定的可靠传输机制无法按需简化,对于可容忍少量数据丢失的场景,会造成资源浪费。

TCP的连接建立过程又称为三次握手。

我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。

(1)TCP服务器准备好接受连接,进入LISTEN状态,这一过程称为被动打开。

(2)第一次握手:客户端发送SYN标志为1(表示这是一个同步报文段),且seq随机的报文段,请求建立连接。此时的seq记为ISN(c)(Initial Sequence Number,初始序列号),括号中的c表示这是和客户端的序列号。客户端发送后变为SYN-SENT状态。

(3)第二次握手:服务端收到客户端的第一次握手信号,变为SYN-RCVD状态。随即确认客户端的SYN报文段,发送一个ACK和SYN标志均为1的报文段。该报文段中ack=ISN(c)+1,seq随机,标记为ISN(s),此处的s表示这是服务端的序列号。服务端变为SYN-RCVD状态。

(4)第三次握手:客户端收到服务端的第二次握手信号,变为ESTABLISHED状态,随即确认服务端的报文段,发送ACK标志为1的报文段。该报文段中ack=ISN(s)+1,seq=ISN(c)+1。服务端收到客户端的第三次握手信号之后变为ESTABLISHED状态。

TCP连接释放过程

TCP的连接释放过程又称为四次挥手。

四次挥手可以由客户端发起,也可以由服务端发起。此处假设连接请求的断开操作由客户端发起。连接断开前,双方都处于ESTABLISHED状态。需要注意的是,连接建立后,即客户端和服务端处于ESTABLISHED时,双方发送的报文段ACK标志均为1。

我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。

(1)第一次挥手:客户端发送FIN标志为1(即FINISH,表示通信结束)的报文段,请求断开连接,执行主动关闭(active close)。此时,报文段中包含对于服务端数据的确认,ACK为 1,假设ack=V。连接断开前已经历了一系列的数据传输,seq取决于之前已发送的报文段,假设seq=U。客户端状态变为FIN-WAIT-1。

(2)第二次挥手:服务端接收到第一次挥手信息,切换为CLOSE-WAIT状态,随即发送ACK标志为1,ack=U+1的报文段,此时seq=V。客户端接收到服务端的第二次挥手信号,变为FIN-WAIT-2状态。第二次挥手后,服务端仍可发送数据,客户端仍可接收。

(3)第三次挥手:服务端完成数据传送后,发送FIN标志和ACK标志均为1的报文段,ack=U+1,seq大于V,假设为W,请求断开连接,这一过程称为被动关闭。服务端发送第三次挥手信号后,变为LAST-ACK状态。

(4)第四次挥手:客户端收到第三次挥手信号,随即发送ACK标志为1,seq=U+1,ack=W+1的报文段,变为TIME-WAIT状态。服务端收到第四次挥手信号,变为CLOSED状态。客户端从变为TIME-WAIT状态开始计时,等待2MSL(2倍最大报文时长,约定值)后进入CLOSED状态。四次挥手结束。

套接字描述符

名称(功能)

核心作用

生命周期

server_fd

监听套接字(被动)

只负责 “等待客户端连接”,不参与实际数据传输

socket()

创建到

close(server_fd)

销毁,贯穿服务器整个运行过程

new_socket

通信套接字(主动)

专门和 “已连接的客户端” 收发数据

accept()

创建到

close(new_socket)

销毁,仅对应一次客户端连接

案例

在Linux C编程中,socket编程是实现网络通信的核心技术。下面详细介绍socket编程中常用的函数及其相关数据类型:

### 1. socket() - 创建套接字
`socket()`函数用于创建一个套接字描述符,是网络通信的起点。

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

- **参数说明**:
  - `domain`:协议族,常用的有`AF_INET`(IPv4)、`AF_INET6`(IPv6)
  - `type`:套接字类型,`SOCK_STREAM`(TCP)、`SOCK_DGRAM`(UDP)
  - `protocol`:协议,通常为0(自动选择对应类型的默认协议)

- **返回值**:成功返回套接字描述符(非负整数),失败返回-1

### 2. 地址相关数据类型(用于bind等函数)

```c
// IPv4地址结构
struct in_addr {
    in_addr_t s_addr;   32位IPv4地址(网络字节序)
};

struct sockaddr_in {
    sa_family_t    sin_family;   地址族,AF_INET
    in_port_t      sin_port;     16位端口号(网络字节序)
    struct in_addr sin_addr;     IPv4地址
    unsigned char  sin_zero[8];  填充字段,通常设为0
};

// 通用地址结构(用于函数参数)
struct sockaddr {
    sa_family_t sa_family;   地址族
    char        sa_data[14];  地址数据
};

// 字节序转换函数
uint32_t htonl(uint32_t hostlong);   主机字节序转网络字节序(32位)
uint16_t htons(uint16_t hostshort);  主机字节序转网络字节序(16位)
uint32_t ntohl(uint32_t netlong);    网络字节序转主机字节序(32位)
uint16_t ntohs(uint16_t netshort);  网络字节序转主机字节序(16位)

IP地址转换函数
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr)
 这句代码的作用,就是将 SERVER_IP(字符串)转换为 server_addr.sin_addr(二进制)
int inet_pton(int af, const char *src, void *dst);  字符串转网络字节序
af
(address family,地址族):
指定要转换的 IP 地址类型,常用值:
AF_INET:表示转换 IPv4 地址(32 位);
AF_INET6:表示转换 IPv6 地址(128 位)。
作用:告诉函数要处理的 IP 地址版本,因为 IPv4 和 IPv6 的字符串格式和二进制长度不同。
src
(source,源):
指向一个字符串,存储待转换的 IP 地址(如 "127.0.0.1" 或 "::1")。
这是人类可读的形式,例如 IPv4 的点分十进制(a.b.c.d)或 IPv6 的冒分十六进制。
dst
(destination,目标):
指向一块内存,用于存储转换后的二进制 IP 地址(网络字节序)。
对于 IPv4(AF_INET):dst 通常指向 struct in_addr 类型的变量(内部是 32 位整数);
对于 IPv6(AF_INET6):dst 通常指向 struct in6_addr 类型的变量(内部是 128 位整数数组)。





const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);  网络字节序转字符串
```

### 3. bind() - 绑定地址和端口
`bind()`函数将套接字与特定的IP地址和端口号绑定。

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

- **参数说明**:
  - `sockfd`:socket()返回的套接字描述符
  - `addr`:指向`sockaddr`结构的指针,包含要绑定的地址信息
  - `addrlen`:地址结构的长度

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

### 4. listen() - 监听连接(TCP服务器)
`listen()`函数将套接字设为监听状态,准备接收客户端连接。

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

- **参数说明**:
  - `sockfd`:绑定后的套接字描述符
  - `backlog`:等待连接队列的最大长度

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

### 5. accept() - 接受连接(TCP服务器)
`accept()`函数从连接队列中取出一个连接请求,创建新的套接字用于与客户端通信。

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

- **参数说明**:
  - `sockfd`:监听状态的套接字描述符
  - `addr`:用于存储客户端地址信息的结构体指针
  - `addrlen`:地址结构长度的指针

- **返回值**:成功返回新的套接字描述符,失败返回-1

### 6. connect() - 建立连接(TCP客户端)
`connect()`函数用于客户端与服务器建立连接。

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

- **参数说明**:
  - `sockfd`:客户端套接字描述符
  - `addr`:指向服务器地址结构的指针
  - `addrlen`:地址结构的长度

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

### 7. send() - 发送数据
`send()`函数用于发送数据。

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

- **参数说明**:
  - `sockfd`:已连接的套接字描述符
  - `buf`:指向要发送数据的缓冲区
  - `len`:要发送的数据长度
  - `flags`:发送标志,通常为0

- **返回值**:成功返回实际发送的字节数,失败返回-1

### 8. recv() - 接收数据
`recv()`函数用于接收数据。

```c
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
```

- **参数说明**:
  - `sockfd`:已连接的套接字描述符
  - `buf`:用于存储接收数据的缓冲区
  - `len`:缓冲区的最大长度
  - `flags`:接收标志,通常为0

- **返回值**:成功返回实际接收的字节数,0表示连接关闭,失败返回-1

### 9. shutdown() - 关闭连接
`shutdown()`函数用于关闭套接字的读、写或两者都关闭。

```c
#include <sys/socket.h>
int shutdown(int sockfd, int how);
```

- **参数说明**:
  - `sockfd`:套接字描述符
  - `how`:关闭方式,`SHUT_RD`(关闭读)、`SHUT_WR`(关闭写)、`SHUT_RDWR`(关闭读写)

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

### 10. close() - 关闭套接字
`close()`函数用于关闭套接字描述符,释放相关资源。

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

- **参数说明**:
  - `fd`:要关闭的套接字描述符

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

这些函数是Linux C中实现TCP/IP网络通信的基础,通常TCP服务器的流程是:
socket()→bind()→listen()→accept()→recv()/send()→close(),
而TCP客户端的流程是:socket()→connect()→send()/recv()→close()。

服务端

#include <stdio.h>          // 标准输入输出函数(printf, perror等)
#include <stdlib.h>         // 标准库函数(exit等)
#include <string.h>         // 字符串处理函数(memset, strlen等)
#include <unistd.h>         // Unix标准函数(close, shutdown等)
#include <sys/socket.h>     // 套接字相关函数(socket, bind等)
#include <netinet/in.h>     // 定义网络地址结构(sockaddr_in等)
#include <arpa/inet.h>      // IP地址转换函数(inet_ntoa等)

 宏定义 - 常量参数
#define PORT 8080           服务器监听端口
#define BUFFER_SIZE 1024     数据缓冲区大小
#define BACKLOG 5            最大等待连接队列长度

int main() {
    // 声明变量
    int server_fd;          服务器监听套接字描述符
    int new_socket;         与客户端通信的新套接字描述符
    struct sockaddr_in address;  存储IP地址和端口的结构
    int addrlen = sizeof(address);   地址结构的长度
    char buffer[BUFFER_SIZE] = {0};   数据缓冲区,初始化为0
    const char *response = "Hello from server";   要发送给客户端的响应信息

     1. 创建套接字
    // AF_INET: 使用IPv4协议
    // SOCK_STREAM: 使用TCP协议(面向连接的可靠传输)
     0: 自动选择对应的默认协议
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");  // 输出错误信息
        exit(EXIT_FAILURE);       // 退出程序
    }

    // 设置服务器地址结构
    address.sin_family = AF_INET;                使用IPv4地址族
    address.sin_addr.s_addr = INADDR_ANY;         绑定到所有可用的网络接口
    address.sin_port = htons(PORT);               将端口号转换为网络字节序

     2. 将套接字绑定到指定的端口和地址
    // server_fd: 要绑定的套接字
    // (struct sockaddr *)&address: 转换为通用地址结构的指针
    // sizeof(address): 地址结构的大小
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");   // 输出绑定失败的错误信息
        exit(EXIT_FAILURE);      // 退出程序
    }

    3. 开始监听连接请求
    // server_fd: 要监听的套接字
    // BACKLOG: 最大等待连接队列长度
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");  // 输出监听失败的错误信息
        exit(EXIT_FAILURE);       // 退出程序
    }
    printf("Server listening on port %d...\n", PORT);  // 显示服务器开始监听

     4. 接受客户端的连接请求
     server_fd: 监听套接字
     (struct sockaddr *)&address: 用于存储客户端地址信息
    // (socklen_t*)&addrlen: 地址长度的指针
    // 注意:accept会阻塞程序,直到有客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept failed");  // 输出接受连接失败的错误信息
        exit(EXIT_FAILURE);       // 退出程序
    }
    // 显示客户端连接信息
    // inet_ntoa: 将网络字节序IP地址转换为字符串
    // ntohs: 将网络字节序端口号转换为主机字节序
    printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));

    5. 接收客户端发送的数据
   new_socket: 与客户端通信的套接字
     buffer: 存储接收数据的缓冲区
    BUFFER_SIZE: 缓冲区大小
     0: 无特殊标志
    ssize_t valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
    printf("Received from client: %s\n", buffer);  // 显示接收到的数据

     6. 向客户端发送响应
     new_socket: 与客户端通信的套接字
     response: 要发送的数据
    strlen(response): 数据长度
    // 0: 无特殊标志
    send(new_socket, response, strlen(response), 0);
    printf("Response sent to client\n");  // 显示响应已发送

    7. 关闭连接
    shutdown(new_socket, SHUT_RDWR);  关闭读写方向的连接
    close(new_socket);                关闭与客户端通信的套接字
    close(server_fd);                 关闭服务器监听套接字
    
    return 0;
}

服务器工作流程:

  1. 使用socket()创建一个 TCP 套接字
  2. 使用bind()将套接字绑定到指定端口 (8080)
  3. 使用listen()进入监听状态,等待客户端连接
  4. 使用accept()接受客户端的连接请求
  5. 使用recv()接收客户端发送的数据
  6. 使用send()向客户端发送响应数据
  7. 使用shutdown()和close()关闭连接

客户端

#include <stdio.h>          // 标准输入输出库,提供printf、perror等函数
#include <stdlib.h>         // 标准库,提供exit等进程控制函数
#include <string.h>         // 字符串处理库,提供memset、strlen等函数
#include <unistd.h>         // Unix系统调用库,提供close、shutdown等函数
#include <sys/socket.h>     // 套接字核心库,提供socket、connect等网络函数
#include <netinet/in.h>     // 网络地址结构定义,提供sockaddr_in等结构体
#include <arpa/inet.h>      // IP地址转换库,提供inet_pton、htons等函数

// 宏定义常量(便于统一修改和维护)
#define PORT 8080           服务器监听的端口号
#define BUFFER_SIZE 1024    数据缓冲区大小(1KB)
#define SERVER_IP "127.0.0.1"   服务器IP地址(本地回环地址,用于本地测试)

int main() {
    int sock = 0;               // 客户端套接字描述符(类似文件句柄)
    struct sockaddr_in serv_addr;  // 存储服务器地址信息的结构体
    char buffer[BUFFER_SIZE] = {0};  // 接收数据的缓冲区,初始化为0
    const char *message = "Hello from client";  // 要发送给服务器的消息

     1. 创建客户端套接字
    // 参数说明:
    // AF_INET:使用IPv4协议
    // SOCK_STREAM:使用TCP协议(面向连接的可靠传输)
    // 0:自动选择对应类型的默认协议(此处为TCP)
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");  // 输出错误信息(如"socket creation error: Permission denied")
        exit(EXIT_FAILURE);  // 发生错误时退出程序
    }

    // 将服务器地址结构体清零初始化(避免内存中残留的垃圾数据影响)
    memset(&serv_addr, '0', sizeof(serv_addr));

    配置服务器地址信息
    serv_addr.sin_family = AF_INET;                // 使用IPv4地址族
    serv_addr.sin_port = htons(PORT);              // 将端口号从主机字节序转为网络字节序
                                                   // (网络中统一使用大端字节序,不同主机可能有差异)

     将服务器IP地址从字符串形式(如"127.0.0.1")转换为网络字节序的二进制形式
    // 参数说明:
    AF_INET:地址族(IPv4)
     SERVER_IP:字符串形式的IP地址
     &serv_addr.sin_addr:存储转换结果的地址
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("invalid address/address not supported");  // 地址无效或不支持时的错误提示
        exit(EXIT_FAILURE);  // 退出程序
    }

     2. 尝试与服务器建立TCP连接
     参数说明:
     sock:客户端套接字描述符
     (struct sockaddr *)&serv_addr:转换为通用地址结构的服务器地址
    // sizeof(serv_addr):服务器地址结构体的大小
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");  // 连接失败时的错误提示(如服务器未启动)
        exit(EXIT_FAILURE);  // 退出程序
    }
    
    // 连接成功后,打印服务器信息
    printf("Connected to server %s:%d\n", SERVER_IP, PORT);

     3. 向服务器发送数据
    // 参数说明:
     sock:已连接的套接字描述符
    // message:要发送的数据
    // strlen(message):数据长度(不包含字符串结束符'\0')
    // 0:无特殊标志(默认阻塞发送)
    send(sock, message, strlen(message), 0);
    printf("Message sent to server\n");  // 提示数据已发送

     4. 接收服务器返回的响应数据
    // 参数说明:
    // sock:已连接的套接字描述符
    // buffer:存储接收数据的缓冲区
    // BUFFER_SIZE:缓冲区最大容量(避免溢出)
    // 0:无特殊标志(默认阻塞接收)
    ssize_t valread = recv(sock, buffer, BUFFER_SIZE, 0);
    printf("Received from server: %s\n", buffer);  // 打印接收到的服务器响应

    5. 关闭连接释放资源
    shutdown(sock, SHUT_RDWR);  关闭套接字的读写通道(通知对方连接即将关闭)
    close(sock);                 彻底关闭套接字描述符,释放系统资源
    
    return 0;  // 程序正常结束
}

客户端工作流程:

  1. 使用socket()创建一个 TCP 套接字
  2. 使用connect()连接到服务器
  3. 使用send()向服务器发送数据
  4. 使用recv()接收服务器的响应
  5. 使用shutdown()和close()关闭连接
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define  SERVER_IP "127.0.0.1"
int main(int argc, char const *argv[])
{
    int client_fd = 0;
    char message[100] = "你好我是客户端";  // 要发送给服务器的消息,修改为数组形式
    // 移除对常量字符串的memset操作,避免错误
   
    struct sockaddr_in server;//存储服务端的信息
    memset(&server,0,sizeof(server));// 服务端地址清零初始化
    
    client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd< 0)
    {
        perror("套接字创建失败");
        exit(EXIT_FAILURE);  // 添加退出机制,避免错误后继续执行
    }
   
    //用于连接服务端的信息
    server.sin_family=AF_INET;
    server.sin_port=htons(8084);

    // 将服务器IP地址从字符串形式转换为网络字节序的二进制形式
    if (inet_pton(AF_INET, SERVER_IP, &server.sin_addr) <= 0) {
        perror("invalid address/address not supported");
        exit(EXIT_FAILURE);
    }
    
    int connect_res=connect(client_fd, (struct sockaddr *)&server, sizeof(server));
    if (connect_res<0)
    {
        perror("连接服务端失败");  // 改用perror输出具体错误原因
        exit(EXIT_FAILURE);
    }
    
    //向服务端发送数据
    send(client_fd, message, strlen(message), 0);
    printf("已向服务端发送消息: %s\n", message);

    // 补充:接收服务端响应
    char buffer[100] = {0};
    ssize_t valread = recv(client_fd, buffer, sizeof(buffer), 0);
    if (valread > 0)
    {
        printf("收到服务端响应: %s\n", buffer);
    }

    // 补充:关闭连接
    shutdown(client_fd, SHUT_RDWR);
    close(client_fd);

    return 0;
}

TCP多个客服端对应一个服务端

服务端

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_CLIENTS 5

void *client_read_write(void *arg)
{
    ssize_t count = 0, send_count = 0;
    // 修复1:用临时变量接收参数,避免主线程clinet_fd覆盖(原代码传地址会导致多线程共享同一个变量)
    int client_fd = *(int *)arg;
    free(arg); // 主线程malloc的内存,这里要释放
    char *read_buf = malloc(1024 * sizeof(char));
    char *write_buf = malloc(1024 * sizeof(char));

    if (!read_buf) {
        printf("内存申请失败\n");
        close(client_fd);
        pthread_exit(NULL);
    }
    if (!write_buf) {
        printf("内存申请失败\n");
        free(read_buf);
        close(client_fd);
        pthread_exit(NULL);
    }

    while ((count = recv(client_fd, read_buf, 1023, 0)) > 0) { // 留1字节存字符串结束符
        // 给接收的字符串加结束符,避免乱码
        read_buf[count] = '\0';
        printf("receive message from client_fd: %d: %s\n", client_fd, read_buf);

        strcpy(write_buf, "received~\n");
        send_count = send(client_fd, write_buf, strlen(write_buf), 0);
        if (send_count < 0) {
            perror("send");
            break;
        }
    }

    if (count < 0) perror("recv"); // 接收错误时打印
    shutdown(client_fd, SHUT_WR);
    close(client_fd);
    free(read_buf);
    free(write_buf);
    pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
    // 修复2:server_listen未赋值(原代码socket创建后没存结果,导致后续bind失败)
    int server_listen = socket(AF_INET, SOCK_STREAM, 0); 
    int clinet_fd = 0;
    struct sockaddr_in server_adder, clinet_adder;
    memset(&server_adder, 0, sizeof(server_adder));
    memset(&clinet_adder, 0, sizeof(clinet_adder));
    // 修复3:addrlen应该是客户端地址长度(原代码用server_adder的长度,不影响但逻辑更严谨)
    socklen_t addrlen = sizeof(clinet_adder); 

    pthread_t client_threads[MAX_CLIENTS];
    int client_count = 0;

    if (server_listen < 0) { // 检查socket创建结果
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    server_adder.sin_addr.s_addr = INADDR_ANY;
    // 修复4:端口转换用htons(原代码用htonl,会导致端口错误,客户端连不上)
    server_adder.sin_port = htons(8081); 
    server_adder.sin_family = AF_INET;

    if (bind(server_listen, (struct sockaddr *)&server_adder, sizeof(server_adder)) < 0) {
        perror("绑定失败"); // 用perror显示具体错误(如端口被占用)
        close(server_listen);
        exit(EXIT_FAILURE);
    }

    if (listen(server_listen, 6) < 0) {
        perror("监听失败");
        close(server_listen);
        exit(EXIT_FAILURE);
    }
    printf("服务端启动成功,监听端口8081...\n"); // 增加启动提示

    while (1)
    {
        if (client_count >= MAX_CLIENTS) {
            printf("已超出最大客户端数量(%d),等待中...\n", MAX_CLIENTS);
            sleep(1);
            continue;
        }

        // 修复5:accept的第三个参数必须是socklen_t*类型(原代码直接传sizeof,会编译警告+运行错误)
        clinet_fd = accept(server_listen, (struct sockaddr *)&clinet_adder, &addrlen);
        if (clinet_fd < 0) {
            perror("新的请求socket创建失败");
            continue;
        }

        printf("客户端连接成功:\n");
        printf("  IP地址: %s\n", inet_ntoa(clinet_adder.sin_addr));
        printf("  端口: %d\n", ntohs(clinet_adder.sin_port));
        client_count++;
        printf("当前客户端数量:%d\n", client_count);

        // 修复6:用malloc分配客户端fd(原代码传&clinet_fd地址,多线程会共享同一个变量,导致fd混乱)
        int *p_clinet_fd = malloc(sizeof(int));
        *p_clinet_fd = clinet_fd;
        if (pthread_create(&client_threads[client_count-1], NULL, client_read_write, (void *)p_clinet_fd) != 0) {
            perror("线程创建失败");
            close(clinet_fd);
            free(p_clinet_fd);
            client_count--;
            continue;
        }

        pthread_detach(client_threads[client_count-1]);
    }

    close(server_listen); // 理论上不会执行,但加上更规范
    return 0;
}

客户端

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERVER_PORT 8081
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[])
{
    int client_socket;
    struct sockaddr_in server_addr;
    char send_buf[BUFFER_SIZE];
    char recv_buf[BUFFER_SIZE];
    ssize_t send_len, recv_len;

    // 创建客户端套接字
    if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket创建失败");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    
    // 转换服务器IP地址(默认为本地回环地址,可修改为实际服务器IP)
    if (argc > 1)
    {
        // 如果提供了命令行参数,则使用该参数作为服务器IP
        if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0)
        {
            perror("无效的服务器IP地址");
            close(client_socket);
            exit(EXIT_FAILURE);
        }
    }
    else
    {
        // 否则使用本地回环地址
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    }

    // 连接到服务器
    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("连接服务器失败");
        close(client_socket);
        exit(EXIT_FAILURE);
    }

    printf("成功连接到服务器,端口: %d\n", SERVER_PORT);
    printf("请输入要发送的消息(输入exit退出):\n");

    // 循环发送和接收数据
    while (1)
    {
        // 读取用户输入
        if (fgets(send_buf, BUFFER_SIZE, stdin) == NULL)
        {
            perror("读取输入失败");
            break;
        }

        // 检查是否要退出
        if (strncmp(send_buf, "exit", 4) == 0)
        {
            printf("准备断开连接...\n");
            break;
        }

        // 发送数据到服务器
        send_len = send(client_socket, send_buf, strlen(send_buf), 0);
        if (send_len < 0)
        {
            perror("发送数据失败");
            break;
        }

        // 接收服务器响应
        recv_len = recv(client_socket, recv_buf, BUFFER_SIZE, 0);
        if (recv_len < 0)
        {
            perror("接收响应失败");
            break;
        }
        else if (recv_len == 0)
        {
            printf("服务器已断开连接\n");
            break;
        }

        // 打印服务器响应
        recv_buf[recv_len] = '\0';  // 确保字符串结束
        printf("服务器响应: %s", recv_buf);
    }

    // 关闭连接
    shutdown(client_socket, SHUT_WR);
    close(client_socket);
    printf("已断开与服务器的连接\n");

    return 0;
}
  

运行结果

setsockopt()

在 Linux C 编程中,setsockopt( )是一个非常重要的系统调用,用于设置套接字(socket)的各种选项。它允许开发者在创建套接字后,对其行为进行精细化控制,以适应不同的网络通信需求。

函数原型

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

- **参数说明**:

- `sockfd`:套接字描述符,即通过 `socket( )` 函数创建的文件描述符

- `level`:选项所在的协议层(如 SOL_SOCKET、IPPROTO_TCP 等)

- `optname`:要设置的具体选项名称

- `optval`:指向存放选项值的缓冲区指针

- `optlen`:缓冲区的长度(以字节为单位)

- **返回值**:成功返回 0,失败返回 -1 并设置 errno

主要功能

`setsockopt()` 用于配置套接字的各种属性,这些属性控制着套接字的行为,包括:

- 数据包的发送和接收方式

- 连接超时设置

- 地址重用

- 缓冲区大小调整

- 广播和多播设置

- TCP 协议特定选项(如是否启用 Nagle 算法)

常见使用场景

1. **地址和端口重用**

当服务器程序关闭后,通常需要等待一段时间才能重新使用相同的端口。使用 `SO_REUSEADDR` 选项可以避免这个问题:

int reuse = 1;
   if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
       perror("setsockopt");
       exit(EXIT_FAILURE);
   }

2. **设置发送/接收缓冲区大小**

可以调整套接字的发送和接收缓冲区大小以优化性能:

    int bufsize = 65536;   64KB
  设置接收缓冲区
   setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
   设置发送缓冲区
   setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));

3. **设置连接超时**

结合 `connect()` 使用,可以设置连接的超时时间:

 struct timeval timeout;
   timeout.tv_sec = 10;   10秒
   timeout.tv_usec = 0;
   
   设置连接超时
   setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

4. **禁用 Nagle 算法**

在某些实时性要求高的场景(如游戏),可以禁用 Nagle 算法减少延迟:

int nodelay = 1;
   setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));

5. **允许广播**

要发送广播数据包,需要先设置 `SO_BROADCAST` 选项:

 int broadcast = 1;
   setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

6. 设置 IP 包的 TTL(生存时间)

控制数据包在网络中能够经过的路由器数量:

int ttl = 64;  常见的TTL值
   setsockopt(sockfd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));

使用注意事项

1. 不同的选项需要在特定的协议层(level)设置

2. 有些选项需要在 `bind()` 或 `connect()` 之前设置才有效

3. 选项值的类型可能是整数、结构体等,需要根据具体选项确定

4. 对同一个选项的设置可能会有系统限制(如缓冲区大小的最大值)

案例

通过setsockopt ( )函数实现伪超时

伪超时的运行结果意义在于:

  1. 提供即时准确的错误信息

(不是模糊的"超时")

  1. 实现快速失败机制,提高系统响应性
  2. 优化资源利用率,避免不必要的等待
  3. 支持高效的服务发现和故障处理

服务端

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    int opt = 1;

    char buffer[1024] = {0};
    const char *response = "我是服务端 ";

    // 服务端监听套接字描述符
    int server_listen = socket(AF_INET, SOCK_STREAM, 0);
    if (server_listen < 0)
    {
        printf("socket创建失败");
        exit(EXIT_FAILURE); // 添加退出机制
    }

    struct sockaddr_in server_bind;
    int addrlen = sizeof(server_bind);

    server_bind.sin_addr.s_addr = INADDR_ANY;
    server_bind.sin_family = AF_INET;
    server_bind.sin_port = htons(8084); // 修改为与客户端一致的端口

    // 设置套接字选项,允许重用端口和地址(修正重复的SO_REUSEADDR)
    if (setsockopt(server_listen, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
    {
        perror("setsockopt 创建失败");
        exit(EXIT_FAILURE); // 添加退出机制
    }

    // 绑定地址
    if (bind(server_listen, (struct sockaddr *)&server_bind, sizeof(server_bind)) < 0)
    {
        perror("绑定失败");
        exit(EXIT_FAILURE); // 添加退出机制
    }

    // 监听连接请求,最大等待队列长度为5
    if (listen(server_listen, 5) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE); // 添加退出机制
    }

    printf("服务端启动,监听端口 8084...\n");

    while (1)
    {
        // 等待客户端申请连接服务端
        int newsocket = accept(server_listen, (struct sockaddr *)&server_bind, (socklen_t *)&addrlen);

        if (newsocket < 0)
        {
            perror("连接服务端失败");
            continue; // 继续等待下一个连接
        }

        // 读取客户端发送的数据
        ssize_t valread = read(newsocket, buffer, 1024);

        if (valread > 0)
        {
            printf("收到客户端消息:%s\n", buffer);
            // 向客户端发送消息
            send(newsocket, response, strlen(response), 0);
            printf("已发送响应给客户端\n");
        }
        else if (valread == 0)
        {
            printf("客户端正常关闭连接\n");
        }
        else
        {
            perror("读取数据失败");
        }

        // 关闭客户端连接
        close(newsocket);
        printf("客户端连接已关闭\n");
    }

    // 实际不会执行到这里,可用于后续扩展
    close(server_listen);
    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/time.h>

#define SERVER_IP "127.0.0.1" //服务端IP地址
#define SERVER_PORT 8084     //服务端端口号(与服务端保持一致)
#define TIMEOUT_SEC 5        // 连接超时时间
#define BUFFER_SIZE 1024     // 添加缓冲区定义

int main(int argc, char const *argv[])
{
    int client = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_bind; //客户端接受绑定服务端的协议和端口

    if (client < 0)
    {
        perror("客户端套接字创建失败");
        exit(EXIT_FAILURE); // 添加退出机制
    }

    //设置服务器地址结构
    memset(&server_bind, 0, sizeof(server_bind));
    server_bind.sin_family = AF_INET;
    server_bind.sin_port = htons(SERVER_PORT);

    //将IP转化为网络字节序
    if (inet_pton(AF_INET, SERVER_IP, &server_bind.sin_addr) <= 0)
    {
        perror("网络字节序转换失败");
        close(client);
        exit(EXIT_FAILURE);
    }

    //设置连接超时时间
    struct timeval timeout;
    timeout.tv_sec = TIMEOUT_SEC; //5秒
    timeout.tv_usec = 0;          // 微秒
     
    //使用SO_SNDTIMEO选项设置发送超时(影响connect)
    if (setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0)
    {
        perror("setsockopt 设置失败");
        close(client);
        exit(EXIT_FAILURE);
    }
    
    //尝试链接服务器
    printf("尝试连接到 %s:%d, 超时时间 %d 秒...\n", SERVER_IP, SERVER_PORT, TIMEOUT_SEC);

    // 尝试连接服务器
    int ret = connect(client, (struct sockaddr *)&server_bind, sizeof(server_bind));
    if (ret < 0) {
        // 检查错误是否为超时
        if (errno == EINPROGRESS || errno == EWOULDBLOCK || errno == ETIMEDOUT) {
            fprintf(stderr, "连接超时! 超过 %d 秒未响应\n", TIMEOUT_SEC);
        } else {
            perror("连接失败");
        }
        close(client);
        exit(EXIT_FAILURE);
    }

    printf("成功连接到服务器!\n");

    // 添加发送消息的功能
    const char *message = "你好,服务端!";
    send(client, message, strlen(message), 0);
    printf("已向服务端发送消息: %s\n", message);

    // 接收服务端响应
    char buffer[BUFFER_SIZE] = {0};
    ssize_t valread = read(client, buffer, BUFFER_SIZE);
    if (valread > 0) {
        printf("收到服务端响应: %s\n", buffer);
    } else if (valread == 0) {
        printf("服务端关闭了连接\n");
    } else {
        perror("读取响应失败");
    }

    // 关闭套接字
    close(client);
    return 0;
}

服务端不开启,只开启客户端(体现了伪超时)

UDP

在 Linux 系统中,UDP 协议的开发主要基于 Socket 编程接口,核心函数围绕“创建套接字、绑定地址、发送数据、接收数据”四个核心操作展开。以下是 UDP 开发中最常用的函数及其用法:

1. 创建 UDP 套接字:`socket()`

- **功能**:创建一个 UDP 类型的套接字(文件描述符),作为网络通信的端点。

#include <sys/socket.h>
  int socket(int domain, int type, int protocol);
  - **参数**:
  - `domain`:协议族,UDP 用 `AF_INET`(IPv4)或 `AF_INET6`(IPv6)。
  - `type`:套接字类型,UDP 必须指定为 `SOCK_DGRAM`(数据报套接字)。
  - `protocol`:具体协议,UDP 填 `0`(自动匹配 `SOCK_DGRAM` 对应的 UDP 协议)。
- **返回值**:成功返回套接字描述符(非负整数),失败返回 `-1`(需检查 `errno`)。

- **示例**:

  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sockfd == -1) {
      perror("socket 创建失败");
      exit(1);
  }

2. 绑定地址与端口:`bind()`

- **功能**:将套接字与本地 IP 地址和端口绑定(服务器必用,客户端可选)。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  - **参数**:
  - `sockfd`:`socket()` 返回的套接字描述符。
  - `addr`:指向本地地址结构的指针(需用 `struct sockaddr_in` 填充 IPv4 信息)。
  - `addrlen`:地址结构的长度(`sizeof(struct sockaddr_in)`)。
- **返回值**:成功返回 `0`,失败返回 `-1`。

- **示例**(绑定 IPv4 地址):

 struct sockaddr_in local_addr;
  memset(&local_addr, 0, sizeof(local_addr));
  local_addr.sin_family = AF_INET;               // IPv4
  local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地网卡
  local_addr.sin_port = htons(8080);             // 端口 8080(需转换为网络字节序)

  if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) == -1) {
      perror("bind 失败");
      close(sockfd);
      exit(1);
  }
  ```
  > 注:`htonl()`/`htons()` 用于将主机字节序转换为网络字节序(大端序),必须使用。

3. 发送 UDP 数据:`sendto()`

- **功能**:向指定的目标地址发送 UDP 数据报(无需建立连接)。

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`:发送标志,通常填 `0`(阻塞发送)。
  - `dest_addr`:目标主机的地址结构(含 IP 和端口)。
  - `addrlen`:目标地址结构的长度。
- **返回值**:成功返回发送的字节数,失败返回 `-1`。

- **示例**:

struct sockaddr_in dest_addr;
  memset(&dest_addr, 0, sizeof(dest_addr));
  dest_addr.sin_family = AF_INET;
  dest_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 目标 IP
  dest_addr.sin_port = htons(8080);                       // 目标端口

  char *msg = "Hello UDP";
  ssize_t send_len = sendto(sockfd, msg, strlen(msg), 0,
                           (struct sockaddr*)&dest_addr, sizeof(dest_addr));
  if (send_len == -1) {
      perror("sendto 失败");
  } 

4. 接收 UDP 数据:`recvfrom()`

- **功能**:从套接字接收 UDP 数据报,并获取发送方的地址信息。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                   struct sockaddr *src_addr, socklen_t *addrlen);
  ```
- **参数**:
  - `sockfd`:套接字描述符。
  - `buf`:接收数据的缓冲区。
  - `len`:缓冲区最大长度(避免溢出)。
  - `flags`:接收标志,`0` 表示阻塞等待数据。
  - `src_addr`:输出参数,存储发送方的地址信息(可填 `NULL` 忽略)。
  - `addrlen`:输入输出参数,传入 `src_addr` 缓冲区大小,返回实际地址长度。
- **返回值**:成功返回接收的字节数,失败返回 `-1`,连接关闭返回 `0`(UDP 一般不涉及)。

- **示例**:

char buf[1024];
  struct sockaddr_in src_addr;
  socklen_t src_len = sizeof(src_addr);

  ssize_t recv_len = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
                             (struct sockaddr*)&src_addr, &src_len);
  if (recv_len == -1) {
      perror("recvfrom 失败");
  } else {
      buf[recv_len] = '\0'; // 确保字符串结束
      printf("收到来自 %s:%d 的数据:%s\n",
             inet_ntoa(src_addr.sin_addr), // 转换 IP 为字符串
             ntohs(src_addr.sin_port),     // 转换端口为本地字节序
             buf);
  }

5. 关闭套接字:`close()`

- **功能**:释放套接字资源,终止网络通信。

- **原型**:

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

- **示例**:

 close(sockfd); // 关闭 UDP 套接字

辅助函数(地址转换)

- `inet_addr(const char *cp)`:将点分十进制 IP 字符串(如 `"192.168.1.1"`)转换为网络字节序的 32 位整数(IPv4)。
- `inet_ntoa(struct in_addr in)`:将网络字节序的 32 位整数转换为点分十进制 IP 字符串(IPv4)。
- `htons(uint16_t hostshort)`/`htonl(uint32_t hostlong)`:将 16 位/32 位主机字节序转换为网络字节序。
- `ntohs(uint16_t netshort)`/`ntohl(uint32_t netlong)`:将网络字节序转换为主机字节序(常用于解析接收的端口/IP)。

案例

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8085
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[])
{
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUFFER_SIZE];
    socklen_t client_len = sizeof(client_addr);

    // 创建UDP套接字

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        perror("套接字创建失败");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有可用的接口
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);

    // 绑定套接字到端口
    if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("绑定失败");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("UDP服务器启动,监听端口 %d...\n", PORT);

    while (1)
    {
        // 接受客服端的消息
        ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0, (struct sockaddr *)&client_addr, &client_len);

        if (recv_len < 0)
        {
            perror("接收失败");
            continue;
        }

        // 准备回复消息
        char reply[BUFFER_SIZE];
        snprintf(reply, BUFFER_SIZE, "服务器已收到: %s", buffer);

        // 回复客户端
        ssize_t send_len = sendto(sockfd, reply, strlen(reply), 0,
                                  (const struct sockaddr *)&client_addr, client_len);
        if (send_len < 0)
        {
            perror("发送回复失败");
        }

        // 如果收到"exit",服务器退出
        if (strcmp(buffer, "exit") == 0)
        {
            printf("收到退出命令,服务器关闭...\n");
            break;
        }
    }

    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8085
#define BUFFER_SIZE 1024
#define SERVER_IP "127.0.0.1"  // 服务器IP地址,本地测试用回环地址

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    socklen_t server_len = sizeof(server_addr);

    // 创建UDP套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("套接字创建失败");
        exit(EXIT_FAILURE);
    }

    // 初始化服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);

    // 转换服务器IP地址
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("无效的服务器IP地址");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("UDP客户端启动,连接到 %s:%d\n", SERVER_IP, PORT);
    printf("请输入消息(输入exit退出):\n");

    while (1) {
        // 读取用户输入
        printf("> ");
        fflush(stdout);
        if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
            perror("读取输入失败");
            break;
        }

        // 移除换行符
        buffer[strcspn(buffer, "\n")] = '\0';

        // 发送消息到服务器
        ssize_t send_len = sendto(sockfd, buffer, strlen(buffer), 0,
                                 (const struct sockaddr *)&server_addr, server_len);
        if (send_len < 0) {
            perror("发送消息失败");
            continue;
        }

        // 如果输入exit,客户端退出
        if (strcmp(buffer, "exit") == 0) {
            printf("客户端退出...\n");
            break;
        }

        // 接收服务器回复
        ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                                   (struct sockaddr *)&server_addr, &server_len);
        if (recv_len < 0) {
            perror("接收回复失败");
            continue;
        }

        // 确保字符串以null结尾
        buffer[recv_len] = '\0';
        printf("服务器回复: %s\n", buffer);
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

核心流程总结

1. **服务器**:`socket()` 创建 UDP 套接字 → `bind()` 绑定端口 → `recvfrom()` 接收数据 → `sendto()` 回复数据 → `close()` 关闭。

2. **客户端**:`socket()` 创建 UDP 套接字 → `sendto()` 发送数据 → `recvfrom()` 接收回复(可选)→ `close()` 关闭。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值