目录
一、前言
1.1 什么是粘包
1.2 为什么UDP不会粘包
二、编写程序
文件树
客户端程序
服务器程序
tcp程序
头文件
makefile
三、 实验现象
四、改进实验
五、小作业
一、前言
最近在做网络芯片的驱动,验证功能的时候需要借助wireshark这个工具,今天就来回顾下网络编程相关的知识。
1.1 什么是粘包
在网络通信过程中,数据包往往是连续发送的,尤其是在稳定且高速的网络连接中。这种连续传输可以提高数据传输的效率,减少因等待发送或接收数据包而产生的延迟。
在TCP/IP协议中,由于TCP是一个面向连接的、可靠的、基于字节流的传输层通信协议,它不保留消息边界。这意味着在发送端连续发送的多个数据包,在接收端可能会被合并成一个大的数据包接收(粘包),或者一个完整的数据包被拆分成多个小数据包接收(拆包)。
|eth header|IP header|tcp header| data |
[12 bytes] | 20 bytes| 20 bytes |"abc" |
|
data只占4个字节,
而为了发送这4个字节,需要12+20+20,至少52个字节,会造成极大的资源浪费
1.2 为什么UDP不会粘包
- 独立的传输机制:由于UDP数据报的独立性,每个数据报都是单独发送和接收的,不会与其他数据报混合在一起。因此,在接收端,每个UDP数据报都可以被清晰地识别和处理,不会出现TCP中可能遇到的粘包问题。
- 没有面向连接的数据流:UDP不像TCP那样提供面向连接的数据流服务。TCP为了保证数据的可靠传输,会对数据进行拆分、重排和合并等操作,这些操作可能会导致粘包现象。而UDP则没有这些操作,它直接发送和接收完整的数据报,因此不会出现粘包问题。
- 基于数据报的传输模式:UDP的传输模式是基于数据报的,即每个数据报都是一个完整的单元,具有独立的传输路径和生命周期。这种传输模式使得UDP能够避免TCP中可能出现的粘包和拆包问题。
了解了以上概念我们开始验证这个问题。
二、编写程序
文件树
这是我们的目录结构分为服务器和客户端
客户端程序
#include "tcp.h"
int main(int argc, char *argv[])
{
int fd;
int ret, i = 2;
char buf[BUFSIZ] = {"===test===\n"};
/*检查参数*/
Argment(argc, argv);
fd = SocketInit(argv, false);
/*发送数据*/
while(i--){
do {
ret = send(fd, buf, strlen(buf), 0);
}while(ret < 0 && errno == EINTR); //如果信号导致的错误,继续执行
if(ret < 0)
ErrExit("recv");
else if(!ret)
break;
printf("send data:%s", buf);
fflush(stdout);
}
close(fd);
return 0;
}
我们进行两次连发看看效果是什么样的
服务器程序
#include "tcp.h"
int main(int argc, char *argv[])
{
int fd, newfd;
int ret;
char buf[BUFSIZ];
Addr_in client_addr;
socklen_t addrlen = sizeof(Addr_in);
/*检查参数*/
Argment(argc, argv);
/*创建服务端套接字*/
fd = SocketInit(argv, true);
/*接收客户端连接*/
do {
newfd = accept(fd, (Addr *)&client_addr, &addrlen);
}while(newfd < 0 && errno == EINTR); //如果信号导致的错误,继续执行
if(newfd < 0)
ErrExit("accept");
/*接收客户端数据*/
while(1){
do {
ret = recv(newfd, buf, BUFSIZ, 0);
}while(ret < 0 && errno == EINTR); //如果信号导致的错误,继续执行
if(ret < 0)
ErrExit("recv");
else if(!ret)
break;
else
printf("[%s:%d]buf:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), buf);
printf("getchar()\n");
getchar();
}
close(newfd);
close(fd);
return 0;
}
tcp程序
#include "tcp.h"
void Argment(int argc, char *argv[]){
if(argc < 3){
fprintf(stderr, "%s <addr><port>\n", argv[0]);
exit(EXIT_FAILURE);
}
}
int SocketInit(char *argv[], bool server){
int fd;
Addr_in addr;
func_t func = server?bind:connect;
/*创建套接字*/
if( (fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0)
ErrExit("socket");
/*设置通信结构体*/
bzero(&addr, sizeof(addr) );
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
if (inet_aton(argv[1], &addr.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
/*地址快速重用*/
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int) );
/*发起连接请求或绑定地址*/
if( func(fd, (Addr *)&addr, sizeof(addr) ) )
ErrExit("connect or bind");
if(server){
/*监听模式*/
if( listen(fd, BACKLOG) )
ErrExit("listen");
}
return fd;
}
头文件
#ifndef _TCP_H_
#define _TCP_H_
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <string.h>
#define BACKLOG 5
#define ErrExit(msg) do { perror(msg); \
exit(EXIT_FAILURE); } while(0)
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
typedef int (* func_t)(int, const Addr *, socklen_t);
void Argment(int argc, char *argv[]);
int SocketInit(char *argv[], bool server);
#endif
makefile
all:server client
CC=gcc
CFLAGS=-g -Wall
server:tcp.c server.c
client:tcp.c client.c
clean:
rm server client
三、 实验现象
在ubuntu22.04和ubuntu18.04上都能实现。
但是发现有个问题
还有发两次的现象
四、改进实验
先ping下百度,看看百度的ip是多少
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin = {
.sin_family = AF_INET,
.sin_port = htons(80),
};
if (inet_aton("110.242.68.66", &sin.sin_addr) == 0) {
fprintf(stderr, "Invalid address\n");
exit(EXIT_FAILURE);
}
if(connect(fd, (struct sockaddr *)&sin, sizeof(sin) ) < 0) {
perror("connect");
exit(0);
}
send(fd, "hello", 5, 0);
send(fd, "hello", 5, 0);
send(fd, "hello", 5, 0);
send(fd, "hello", 5, 0);
send(fd, "hello", 5, 0);
send(fd, "hello", 5, 0);
close(fd);
return 0;
}
写完程序后运行wireshark
选择上互联网用的网卡
我们会发现6个hello被划分到了两个包里一个1个hello另一个5个,但是我们用了6次send正常应该6个包的,这就是粘包现象。
注意:PSH代表有数据包,FIN代表没有数据包了
五、小作业
兄弟们可以试试分包现象的验证,搞一个大的包看看是不是会被分开。