《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()
:请求与服务端建立连接。int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 参数:
sockfd
(套接字)、addr
(服务端地址信息)、addrlen
(服务端地址大小)。 - 返回值:成功返回 0,失败返回 -1。
- 参数:
send()
:发送网络数据。ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 参数:
sockfd
(网络套接字)、buf
(发送数据首地址)、len
(发送字节数)、flags
(默认 0)。 - 返回值:成功返回实际发送字节数,失败返回 -1。
- 参数:
listen()
:监听建立三次握手的客户端。int listen(int sockfd, int backlog);
- 参数:
sockfd
(监听套接字)、backlog
(最大监听客户端数)。 - 返回值:成功返回 0,失败返回 -1。
- 参数:
accept()
:接收建立三次握手的客户端并产生通讯套接字。int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
- 参数:
socket
(监听套接字)、address
(客户端地址信息)、address_len
(客户端地址长指针)。 - 返回值:成功返回通讯套接字,失败返回 -1。
- 参数:
recv()
:从网络套接字接收数据。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
#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
#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
#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;
}