01-UDP基础编程:Linux下C语言Socket通信实战

本文通过完整C语言示例,详细讲解在Linux环境下使用UDP协议实现客户端与服务器通信,涵盖核心函数、实现步骤及常见问题排查。适合网络编程初学者。

本文通过一个完整的C语言示例,详细讲解如何在Linux环境下使用UDP协议实现基本的客户端与服务器通信,涵盖核心函数、实现步骤及常见问题排查。

一、实验目的

利用UDP协议,实现一个简单的客户端-服务器(Client-Server)通信程序,理解UDP无连接通信的基本模型。

二、核心原理与概念

UDP(用户数据报协议)是一种无连接不可靠高效的传输层协议。

通信特点

  • 无连接:每次数据传输都是独立的,必须指定目标IP和端口。

  • 面向数据报:以完整的报文为单位进行收发,保留边界。

  • 适用场景:适用于对实时性要求高、可容忍少量丢包的场景,如视频流、音频通话、DNS查询等。

通信流程

通信双方(A和B)的流程是对称的,但在典型客户端/服务器模型中角色稍有不同:

服务端: socket() -> bind() -> recvfrom() -> sendto()

客户端: socket() -> sendto() -> recvfrom()

网络通信的三种情况

  1. 本机通信:使用回环地址 127.0.0.1

  2. 局域网内跨机通信:使用目标主机的局域网IP(如 192.168.1.100)。

  3. 跨网/公网通信:必须知道目标主机的公网IP,且需要路由器进行端口转发(NAT)。

三、核心函数详解

3.1 关键函数

1.socket() - 创建通信端点
1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

作用:创建通信端点,返回一个文件描述符用于后续操作。

参数:

domain:协议族,如 AF_INET (IPv4)。

type:套接字类型,SOCK_DGRAM 表示UDP。

protocol:通常设为 0,系统自动选择。

返回值:成功返回文件描述符(>=0),失败返回-1。

2.bind()-绑定地址(服务器必需)

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

作用:将一个套接字 (sockfd) 与一个特定的 IP 地址和端口号(由 addr 指定)绑定在一起。服务器程序必须调用此函数来声明它在哪里监听。

参数解释:
sockfd:socket() 函数返回的套接字描述符。

addr:指向 struct sockaddr 的指针,包含了要绑定的地址和端口信息。对于 IPv4,我们实际使用 struct sockaddr_in 并强制转换。

addrlen:addr 结构体的长度,通常为 sizeof(struct sockaddr_in)。

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

3.sendto()- 发送UDP数据报

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

作用:向指定的目标地址 (dest_addr) 发送一个数据报。

参数解释:

sockfd:套接字描述符。

buf:指向待发送数据缓冲区的指针。

len:要发送数据的长度(字节数)。

flags:控制标志,通常设为 0。

dest_addr:指向目标地址结构体的指针,决定了数据发往哪里。

addrlen:目标地址结构体的长度。

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

4.recvfrom()-接收UDP数据报

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

作用:从套接字接收一个数据报,并获取发送者的地址信息 (src_addr)。

参数解释:

sockfd:套接字描述符。

buf:指向接收数据缓冲区的指针。

len:缓冲区的最大容量。

flags:控制标志,通常设为 0。

src_addr:指向 struct sockaddr 的指针,用于存放发送者的地址信息。如果不需要,可设为 NULL。

addrlen:这是一个 值-结果参数。调用前,需将其初始化为零

src_addr: 缓冲区的长度;返回时,它会被设置为实际存放的地址长度。 返回值:成功返回接收到的字节数,失败返回 -1。返回 0 在UDP中不表示连接关闭,而是收到了一个长度为0的数据报。

3.2 辅助函数与字节序转换

1.地址转换函数

1
2
3
4
5
#include <arpa/inet.h>
// 字符串IP -> 网络字节序二进制 (Presentation to Network)
int inet_pton(int af, const char *src, void *dst);
// 网络字节序二进制 -> 字符串IP (Network to Presentation)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

2.字节序转换函数

网络字节序采用大端序,主机字节序可能不同,必须转换。

1
2
3
4
5
6
7
#include <arpa/inet.h>
// 主机 -> 网络 (Host to Network)
uint16_t htons(uint16_t hostshort); // 用于端口
uint32_t htonl(uint32_t hostlong);  // 用于IP地址
// 网络 -> 主机 (Network to Host)
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

3.3关键数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,如 AF_INET
    in_port_t      sin_port;   // 16位端口号(网络字节序)
    struct in_addr sin_addr;   // 32位IP地址(网络字节序)
    char           sin_zero[8]; // 填充字段,通常置零
};
struct in_addr {
    in_addr_t      s_addr;     // 32位IPv4地址
};

四.实现步骤分析

服务器端实现步骤

理解了核心函数后,我们按步骤构建服务器。

1.创建udp套接字
1
2
3
4
5
6
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
    perror("socket failed");
    exit(1);
}
2.准备并绑定服务器地址

这里需要理解 struct sockaddr_in 这个重要的结构体。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族,必须为 AF_INET
    in_port_t      sin_port;   // 16位端口号,必须使用网络字节序 (htons)
    struct in_addr sin_addr;   // 32位IPv4地址
    char           sin_zero[8]; // 未使用,填充为0
};
struct in_addr {
    in_addr_t      s_addr;     // 32位IPv4地址 (网络字节序)
};

配置并绑定地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct sockaddr_in saddr;
bzero(&saddr, sizeof(saddr)); // 清空结构体

saddr.sin_family = AF_INET; // IPv4
saddr.sin_port = htons(SER_PORT); // 端口转为网络字节序
saddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到本机所有IP

if (bind(sfd, (struct sockaddr*)&saddr, sizeof(saddr)) == -1) {
    perror("bind failed");
    close(sfd);
    exit(1);
}
printf("服务器已启动,正在监听端口 %d...\n", SER_PORT);
3.循环接收与处理数据址
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct sockaddr_in caddr;// 用于存储客户端地址
socklen_t clen = sizeof(caddr);
char buf[MAXLINE];

while (1)
{
    ssize_t n = recvfrom(sfd, buf, MAXLINE, 0,(struct sockaddr*)&caddr, &len);
    if (n > 0)
    {
        buf[n] = '\0';
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &caddr.sin_addr, client_ip, INET_ADDRSTRLEN);
        printf("来自[%s:%d]的消息:%s\n", client_ip, ntohs(caddr.sin_port), buf);
         // ... (后续可在此添加 sendto 回复客户端的逻辑)
    }
}

3.3客户端实现步骤

客户端流程与服务器类似,但不需要 bind 固定端口。

1.创建udp套接字

同服务端

2.设置目标服务器地址
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct sockaddr_in saddr;
bzero(&addr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(SER_PORT);// 服务器端口
// 将字符串形式的IP(如“127.0.0.1”)转换为网络字节序的二进制形式,argv[1]是main()传进来的参数
if (inet_pton(AF_INET, argv[1], &saddr.sin_addr) <= 0)
{
    perror("invalid address or address not supported")
    exit(1);
}
3.发送与接收循环
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char send_buf[MAXLINE], recv_buf[MAXLINE];
socklen_t addr_len = sizeof(saddr);

while (1) 
{
    printf("请输入要发送的消息: ");
    fgets(send_buf, MAXLINE, stdin);
    
    // 使用 sendto 将数据发送到步骤二中设定的 saddr
    if (sendto(cfd, send_buf, strlen(send_buf), 0, (struct sockaddr*)&saddr, addr_len) == -1) 
    {
        perror("sendto failed");
        continue;
    }
    
    // 使用 recvfrom 接收回复。注意,这里最后一个参数可以是NULL,因为我们已经知道服务器地址。
    ssize_t n = recvfrom(cfd, recv_buf, MAXLINE, 0, NULL, NULL);
    if (n > 0) 
    {
        recv_buf[n] = '\0';
        printf("服务器回复: %s\n", recv_buf);
    }
}

四、完整代码实现

以下是将前面步骤整合后的、可直接编译运行的完整程序代码。

1.服务端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<ctype.h>
#define SER_PORT 2500
#define MAXLINE 80

int main()
{
    struct sockaddr_in saddr,caddr;
    socklen_t clen;
    int sfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    char rbuff[50] = {0},sbuff[50] = {0};
    int n;

    sfd = socket(AF_INET,SOCK_DGRAM,0);
    bzero(&saddr,sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(SER_PORT);
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(sfd,(struct sockaddr*)&saddr,sizeof(saddr));
    printf("accept connections...\n");
    //处理客户端信息
    while(1)
    {
        clen = sizeof(caddr);
        n = recvfrom(sfd,buf,MAXLINE,0,(struct sockaddr*)&caddr,&clen);
        if(n > 0)
        {
            buf[n] = '\0';
            printf("client say:%s\n",buf);
        }
        printf("say sth to client:\n");
        bzero(sbuff,sizeof(sbuff));
        fgets(sbuff,sizeof(sbuff),stdin);
        n = sendto(sfd,sbuff,strlen(sbuff),0,(struct sockaddr*)&caddr,sizeof(caddr));
        if(n == -1)
             perror("sendto error");
        bzero(buf,sizeof(buf));
    }
    close(sfd);
    return 0;
}

2.客户端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<net/if.h>
#define SER_PORT 2500
#define MAXLINE 80

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("./client IP");
        exit(1);
    }
    struct sockaddr_in saddr;
    int cfd,n;
    char sbuff[256]={0},rbuff[256]={0};
    cfd = socket(AF_INET,SOCK_DGRAM,0);
    bzero(&saddr,sizeof(saddr));

    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(SER_PORT);
    inet_pton(AF_INET,argv[1], &saddr.sin_addr.s_addr);
    socklen_t len = sizeof(saddr);

    while(1)
    {
        printf("say sth to server:\n");
        fgets(sbuff,MAXLINE,stdin);
        n = sendto(cfd,sbuff,strlen(sbuff),0,(struct sockaddr*)&saddr,sizeof(saddr));
        if(n == -1)
        {
            perror("sendto error");
            exit(1);
        }
        n = recvfrom(cfd,rbuff,MAXLINE,0,(struct sockaddr*)&saddr,&len);
        if(n > 0)
        {
        printf("server say:%s\n",rbuff);
        bzero(rbuff,sizeof(rbuff));// 清空接收缓冲区,为下次准备
        }
    }
    return 0;
}

3.编译运行:

1
2
3
4
5
6
7
8
9
# 编译服务器
gcc -o udp_server server.c
# 编译客户端
gcc -o udp_client client.c

# 运行(需要两个终端窗口)
 ./udp_server
# 运行客户端,并指定服务器IP(本地测试用127.0.0.1)
 ./udp_client 127.0.0.1

五.遇到的问题

问题:缓冲区污染
现象:客户端有时会显示上一次的旧消息。
根因:在最初的代码中,发送和接收复用了同一个字符数组 sbuff/rbuff,recvfrom 可能没有覆盖全部旧数据。
解决:严格区分 send_buf (专用于发送) 和 recv_buf (专用于接收),并在每次接收后使用 bzero 或 memset 清空接收缓冲区。

六.学习总结

✅ 无连接:每次通信都需指定目标地址,没有“连接”状态。

✅ 服务端必备 bind():固定端口,等待数据。

✅ 客户端指定地址:通过 sendto 的目标地址参数确定服务器。

✅ 缓冲区分离:发送与接收缓冲区独立,避免数据混乱。

✅ 循环处理:双方通常使用 while(1) 循环持续通信。

下一步

掌握了基础通信后,可以探索: 02-UDP广播编程,学习如何向局域网内所有主机发送消息。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
使用 Hugo 构建
主题 StackJimmy 设计