Linux-网络通信02-UDP 与 TCP Socket

一、UDP vs TCP 特征对比

特征 UDP TCP
连接方式 无连接 有连接(需三次握手)
可靠性 不可靠 可靠(应答、超时重传、拥塞控制)
传输延迟 较高
网络开销 大(需维护链路状态)
工作模式 半双工 全双工
套接字类型 SOCK_DGRAM SOCK_STREAM 流式套接字
读/写阻塞 有读阻塞,无写阻塞 有读阻塞,有写阻塞(64K)
数据顺序 不保证顺序到达 保证顺序(有序到达)
发送与接收次数 必须一一对应 不需要对应,数据连续
协议头大小 8 字节 20 字节

二、通用 Socket 函数

以下函数 UDP 和 TCP 都会用到。

2.1 创建套接字

cs 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
参数 取值 说明
domain AF_INET / PF_INET 互联网程序(IPv4)
domain AF_UNIX / PF_UNIX 单机程序
type SOCK_STREAM 流式套接字 → TCP
type SOCK_DGRAM 用户数据报套接字 → UDP
type SOCK_RAW 原始套接字 → IP
protocol 0 自动适应应用层协议

返回值: 成功返回套接字 fd,失败返回 -1

2.2 绑定地址

cs 复制代码
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
  • 服务器端:将套接字与指定接口地址关联,用于从该接口接收数据
  • 客户端:可省略,由系统默认接口发送

地址结构体:

bash 复制代码
// 网络地址结构(传参时强转为 struct sockaddr*)
struct sockaddr_in 
{
    u_short        sin_family;  // 地址族,AF_INET
    u_short        sin_port;    // 端口号(网络字节序)
    struct in_addr sin_addr;    // IP 地址
    char           sin_zero[8]; // 填充位
};

三、UDP 编程

3.1 UDP 特性说明

  1. 发送次数和接收次数必须一一对应

  2. 发送和接收的大小需保持一致(若接收 buf 小于发送大小,超出部分丢失)

  3. 每次发送数据,链路都可能不同

  4. 有读阻塞(没有数据时会阻塞等待)

  5. 无写阻塞(发送太快时,接收方来不及处理会丢包)

  6. 半双工

3.2 UDP 编程步骤

cs 复制代码
服务器端:socket() → bind() → recvfrom() → sendto() → close()
客户端:  socket() → sendto() → recvfrom() → close()

3.3 UDP 收发函数

复制代码
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数 说明
sockfd 本地套接字 fd
buf 要发送的数据
len 数据长度
flags 0 表示阻塞发送
dest_addr 必填,目标主机地址结构体
addrlen 目标地址长度

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

bash 复制代码
// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数 说明
sockfd 本地套接字 fd
buf 存储接收数据的内存
len 要接收的数据长度(一般为 buf 大小)
flags 0 表示阻塞接收
src_addr 可选,对方地址信息(NULL 表示不关心)
addrlen 对方地址结构体大小(src_addr 为 NULL 时也为 NULL)

返回值: 成功返回接收到的字节数,失败返回 -1

3.4 UDP 示例代码框架

cs 复制代码
typedef struct sockaddr * (SA);
int main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }
    // 2. 给套接字 绑定ip ,端口号
    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser, cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;     // ipv4
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    ser.sin_addr.s_addr = inet_addr("192.168.31.149");
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {z
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);
    while (1)
    {   
        char buf[1024]={0};
        recvfrom(udpfd,buf,sizeof(buf),0,(SA)&cli,&len);
        time_t tm;
        time(&tm);
        sprintf(buf,"%s %s",buf,ctime(&tm));
        sendto(udpfd,buf,strlen(buf),0,(SA)&cli,len);
    }

    return 0;
}
cs 复制代码
// ===== UDP 客户端 =====
typedef struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    //1创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return 1;
    }
    struct sockaddr_in ser,cli;
    // 清空结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");
    //2主动连接服务器
    int ret = connect(sockfd,(SA)&ser,sizeof(ser));
    if (-1 == ret)
    {
        perror("connect");
        return 1;

    }
    while (1)
    {
        char buf[512] = "hello,woshihuihui";

    //3发送数据 返回值>0,实际发送的字节数 ==0 网络状态不好,没有发出数据 -1错误
    send(sockfd, buf, strlen(buf), 0);

    bzero(buf, sizeof(buf));
    //4接收数据 >0 实际接收的字节数,==0表示断开,-1表示错误
    int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
    if (rd_ret<=0)
    {
        break;
    }
    printf("from ser:%s\n",buf);
    sleep(1);
    }
    //5断开连接
    close(sockfd);
    return 0;
}

四、TCP 编程

4.1 TCP 特性说明

复制代码
1. 有连接:通信前需建立链路(三次握手),通信中保持,断开时四次挥手
2. 可靠传输:应答机制 + 超时重传机制 + 拥塞控制
3. 全双工:同一时刻可以同时收发
4. 流式套接字:数据连续,发送/接收次数不需要一一对应
5. 有读阻塞,有写阻塞(缓冲区 64K)
6. 保证数据顺序到达

4.2 TCP 编程步骤

复制代码
服务器端:socket() → bind() → listen() → accept() → recv()/send() → close()
客户端:  socket() → connect() → send()/recv() → close()

4.3 TCP 专用函数

listen() - 监听
复制代码
int listen(int sockfd, int backlog);
参数 说明
sockfd 套接字 fd
backlog 允许连接的最大客户端数量

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

accept() - 接受连接
复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 说明
sockfd 监听套接字 fd(listfd)
addr NULL 表示不获取客户端信息;否则传入地址变量地址
addrlen addr 为 NULL 时也为 NULL;否则 len = sizeof(struct sockaddr)

返回值: 成功返回新的通信套接字 fd(connfd),失败返回 -1

⚠️ 注意:listfd 负责监听,connfd 负责实际通信,两者分开!

connect() - 发起连接(客户端专用)
复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 说明
sockfd 本地 socket() 创建的套接字 fd
addr 远程目标主机地址信息
addrlen 参数2的长度

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

recv() / send() - 收发数据
复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *msg, size_t len, int flags);
参数 说明
sockfd 服务器用 connfd(accept返回值),客户端用 sockfd(socket返回值)
buf/msg 数据存储/发送的内存
len 数据长度
flags 0 表示阻塞

4.4 TCP 示例代码框架

cs 复制代码
// ===== TCP 服务器端 =====
int main() {
    // 1. 创建监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 绑定地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 3. 监听
    listen(listfd, 5);

    // 4. 接受连接
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int connfd = accept(listfd, (struct sockaddr*)&client_addr, &len);

    // 5. 收发数据(循环)
    char buf[1024] = {0};
    while (1) {
        int n = recv(connfd, buf, sizeof(buf), 0);
        if (n <= 0) break;  // 客户端断开
        printf("recv: %s\n", buf);
        send(connfd, buf, n, 0);
    }

    // 6. 关闭
    close(connfd);
    close(listfd);
    return 0;
}
cs 复制代码
// ===== TCP 客户端 =====
int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    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, "192.168.1.100", &server_addr.sin_addr);

    // 发起连接(三次握手)
    connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    char buf[] = "hello";
    send(sockfd, buf, strlen(buf), 0);

    char recv_buf[1024] = {0};
    recv(sockfd, recv_buf, sizeof(recv_buf), 0);
    printf("recv: %s\n", recv_buf);

    close(sockfd);
    return 0;
}

五、三次握手与四次挥手

5.1 三次握手(建立连接)

cs 复制代码
Client                    Server
  |  ---- SYN, seq=1000 ---->  |   第1次:客户端请求连接
  |  <-- SYN+ACK, seq=8000 --- |   第2次:服务器确认并请求
  |  ---- ACK 8001 ----------> |   第3次:客户端确认
  |       [连接建立]           |

5.2 四次挥手(断开连接)

cs 复制代码
Client                    Server
  |  ---- FIN, ACK ---------> |   第1次:客户端请求断开
  |  <---- ACK --------------- |   第2次:服务器确认
  |  <---- FIN, ACK ---------- |   第3次:服务器也请求断开
  |  ---- ACK ----------------> |   第4次:客户端确认
  |       [连接关闭]           |

六、通信套接字与监听套接字

套接字类型 说明
listfd(监听套接字) 绑定 IP + Port,专门监听客户端连接请求
connfd(通信套接字) 每当有客户端连接成功,服务端生成一个新的 connfd 用于实际通信
复制代码
服务器:
  listfd(3) → 192.168.31.177:50000(监听)
  connfd(4) → 与客户端 a 通信
  connfd(5) → 与客户端 b 通信

七、TCP 黏包问题

7.1 什么是黏包?

发送方多次发送 数据,接收方因为流式传输特性,无法正确区分每次发送的数据边界,导致无法正常解析

7.2 解决方案

方案 说明
设置结束标志 在数据末尾添加特殊标记,如 \r\n
发送固定大小 使用 struct 固定报文大小,收发都按 sizeof(struct) 操作
自定义协议(变长) 报文头包含数据长度字段,先读头再按长度读数据
cs 复制代码
// 方案2:固定大小结构体示例
typedef struct {
    char buf[1024];  // 正文数据
    int  buf_len;    // 实际数据长度
    int  type;       // 类型:1=chat, 2=file
} MSG;

// 发送
MSG msg = {0};
strcpy(msg.buf, "hello");
msg.buf_len = strlen(msg.buf);
msg.type = 1;
send(connfd, &msg, sizeof(MSG), 0);

// 接收
MSG recv_msg;
recv(connfd, &recv_msg, sizeof(MSG), 0);

八、API 速查表

函数 TCP/UDP 作用
socket() 通用 创建套接字
bind() 通用 绑定地址(服务器必用)
listen() TCP 服务器 开始监听连接
accept() TCP 服务器 接受客户端连接
connect() TCP 客户端 发起连接
send() TCP 发送数据
recv() TCP 接收数据
sendto() UDP 发送数据(含目标地址)
recvfrom() UDP 接收数据(可获取来源地址)
close() 通用 关闭套接字

上一篇:网络通信(一)------ 基础概念与网络模型 下一篇:网络通信(三)------ 协议头结构、HTTP 与 Wireshark 抓包

相关推荐
gfdhy1 小时前
【Linux】服务器网络与安全核心配置|静态IP+SSH加固+防火墙,公网服务器必学实操
linux·服务器·网络·tcp/ip·算法·安全·哈希算法
Hello World . .2 小时前
Linux:网络编程-UDP通信
linux·网络·udp
susu10830189112 小时前
ubuntu重做系统后无法apt update
linux·运维·ubuntu
努力搬砖的鱼2 小时前
校园网运维-生成树协议实战
运维·网络
蜕变的小白2 小时前
Linux系统编程:揭秘网络通信 IP与端口号的奥秘
linux·网络·网络协议·tcp/ip
津津有味道2 小时前
易语言数据报UDP通讯接收RFID网络读卡器获取卡号驱动设备播报TTS语音
网络·udp·rfid·读卡器·易语言·数据报
爱倒腾的老唐2 小时前
02、STM32——嵌入式芯片
linux·stm32·嵌入式硬件
战族狼魂2 小时前
基于SpringBoot+Vue的基因调控网络推断系统
网络·vue.js·spring boot
AryShaw2 小时前
macOS 上搭建 RK3568 交叉编译环境
linux·macos