《Linux 网络编程二:UDP 与 TCP 的差异、应用及问题应对》

一、UDP 与 TCP 对比表

对比项 UDP TCP
连接方式 无需建立连接 有连接(三次握手建立,四次挥手断开)
传输可靠性 尽最大努力交付,可能丢包 安全可靠的数据传输机制
面向对象 面向数据包 面向数据流
传输模式 一对一、一对多传输 本质一对一,并发实现一对多
协议机制 机制简单,资源开销小,实时性高 机制复杂,网络资源开销大
报文头部 8 字节(源端口、目标端口、长度、校验和) 更复杂,包含更多控制信息
应用场景 视频流、音频流等实时性要求高的场景 文件传输、HTTP 等准确性要求高的场景
编程复杂度 相对简单 相对复杂,需处理连接建立与断开

二、UDP 相关内容

1.UDP 丢包原因

UDP 存在接收缓冲区,当发送数据速度过快,致使接收缓冲区满时,后续数据包会丢失。UDP 无需建立连接,进一步增加了丢包可能性。

2.UDP 特点

  • 面向数据包:以数据包为基本传输单位。
  • 无需建立连接:通信前无需预先建立连接。
  • 尽力交付:是不安全可靠的数据协议,存在数据丢包情况。
  • 传输模式多样:能够实现一对一、一对多的传输。
  • 机制简单高效:机制简单,资源开销小,数据实时性高。

3.避免 UDP 丢包方法

  • 发送延时:在发送时使用 usleep() 函数延时,让接收方有足够时间处理数据。
  • 模仿 TCP 应答:发送一个数据后,等待接收端回应,收到回应后再发送下一包数据。

4.UDP 报文头部字段

字段 字节数 含义
源端口号 2 字节 发送方进程端口号
目标端口号 2 字节 接收方进程端口号
长度 2 字节 UDP 报文长度(头部加正文)
校验和 2 字节 用于数据差错校验

UDP 报文头部共 8 字节。


三、TCP 相关内容

1.TCP 协议概述

TCP 即传输控制协议,位于传输层,采用流式套接字。

2.TCP 特点

  • 面向数据流:将数据作为连续的字节流处理。
  • 有连接:通信前需通过三次握手建立连接。
  • 安全可靠:具备安全可靠的数据传输机制。
  • 机制复杂:机制复杂,网络资源开销大。
  • 通信模式:本质只能实现一对一通信,可通过 TCP 并发方式实现一对多通信。

3.TCP 机制

三次握手:TCP 建立连接时需进行三次握手,确保双方通信前都已准备就绪。SYN 为请求建立连接标志位,ACK 为响应报文标志位。

四次挥手:确保断开连接时需进行四次挥手,保证断开连接前双方都已通信结束。FIN 为请求断开连接标志位,ACK 为响应报文标志位。ACK 和 FIN 不能一起,防止客户端断开后服务端还想发送信息。

4.TCP 编程流程

1)客户端流程

socket() 创建套接字
connect() 请求建立连接
send() 发送
recv() 接收
close() 关闭

2)服务端流程

socket() 创建监听套接字
bind() 绑定
listen() 监听要建立连接的客户端
accept() 接受完成三次握手的客户端并产生通信套接字
recv()
send()
close()

5.相关函数接口

  • connect():请求与服务端建立连接。

    cs 复制代码
     int connect(int sockfd, const struct sockaddr *addr,
                       socklen_t addrlen);
    • 参数:sockfd(套接字)、addr(服务端地址信息)、addrlen(服务端地址大小)。
    • 返回值:成功返回 0,失败返回 -1。
  • send():发送网络数据。

    cs 复制代码
     ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    • 参数:sockfd(网络套接字)、buf(发送数据首地址)、len(发送字节数)、flags(默认 0)。
    • 返回值:成功返回实际发送字节数,失败返回 -1。
  • listen():监听建立三次握手的客户端。

    cs 复制代码
    int listen(int sockfd, int backlog);
    • 参数:sockfd(监听套接字)、backlog(最大监听客户端数)。
    • 返回值:成功返回 0,失败返回 -1。
  • accept():接收建立三次握手的客户端并产生通讯套接字。

    cs 复制代码
    int accept(int socket, struct sockaddr *restrict address,
               socklen_t *restrict address_len);
    • 参数:socket(监听套接字)、address(客户端地址信息)、address_len(客户端地址长指针)。
    • 返回值:成功返回通讯套接字,失败返回 -1。
  • recv():从网络套接字接收数据。

    cs 复制代码
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    • 参数:sockfd(通讯套接字)、buf(接收数据首地址)、len(期望接收字节数)、flag(默认 0)。
    • 返回值:成功返回实际接收字节数,失败返回 -1,对方断开连接返回 0。

6.TCP 粘包问题

问题描述

发送方应用层发送的多包数据,在接收方可能一次读到,多包数据产生粘连。

原因

  • 发送方速度快,TCP 底层可能对多包数据重新组帧。
  • 接收方数据处理速度慢,多包数据在接收缓冲区缓存,应用层读时一次读出。

7.解决方法

  • 调整发送速率:控制发送速度,避免粘包。
  • 发送指定大小数据:发送方发送指定大小数据,接收方也接收指定大小数据。注意跨平台数据传输时结构体对齐问题。
  • 增加分隔符:在应用层为发送的数据增加分隔符,接收方利用分隔符解析数据。
  • 自定义数据帧格式:封装自定义数据帧格式(协议)进行发送,并严格按协议解析。

四、代码部分

1.客户端不断从终端接收数据,使用TCP发送给服务端,服务端输出。

head.h

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

client.c

cs 复制代码
#include "head.h"

int main(int argc, char const *argv[])
{
    int sockfd = socket(AF_INET, SOCK_STREAM,0);
    if(sockfd < 0)
    {
        perror("socket error");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;//种类
    seraddr.sin_port = htons(50000);//端口号
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.192");//ip地址

    int ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0)
    {
        perror("connect error");
        return -1;
    }

    char buff[1024] = {0};
    while (1)
    {
        fgets(buff, sizeof buff, stdin);
        ssize_t cnt = send(sockfd, buff, strlen(buff), 0);
        if(cnt < 0)
        {
            perror("send error");
            return -1;
        }
        printf("cnt = %ld\n", cnt);
        memset(buff, 0, sizeof(buff));
    }
    

    close(sockfd);

    return 0;
}

recv.c

cs 复制代码
#include "head.h"

int main(int argc, char const *argv[])
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("socket error");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.192");
    int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0)
    {
        perror("bind error");
        return -1;
    }

    ret = listen(sockfd, 10);
    if(ret < 0)
    {
        perror("listen error");
        return -1;
    }

    int connfd = accept(sockfd, NULL, NULL);
    if(connfd < 0)
    {
        perror("accept error");
        return -1;
    }

    char buff[1024] = {0};
    while(1)
    {   
        ssize_t cnt = recv(connfd, buff, sizeof(buff), 0);
        if(cnt <= 0)
        {
            printf("recv error");
            return -1;
        }

        printf("cnt = %ld, buff = %s\n", cnt, buff);
        memset(buff, 0, sizeof(buff));
    }
    

    close(connfd);
    close(sockfd);

    return 0;
}