您的位置:首页 > 理论基础 > 计算机网络

【网络】(二)流协议粘包问题

2015-09-12 15:35 477 查看
1、流协议与粘包

  TCP协议是基于字节流的,数据无边界,无边界反应在对方接收消息时无法保证一次性返回多少个字节,有可能收到的不是一个完整的消息,可能是半个消息、或者多于一个消息,这就是TCP的粘包问题

  UDP协议基于数据报,数据有边界,体现在对方一次收到的是一个完整的消息

产生粘包的因素



  如上图所示,应用层首先用write函数将要发送的数据拷贝到套接字发送缓冲区中,该缓冲区的大小由SO_SNDBUF来指定,如果应用程序缓冲区的大小超过了套接字发送缓冲区大小,这时候消息就会被分割,第一部分消息从发送缓冲区中发送出去后,后面的数据才进去发送缓冲区,这样对方在接收数据时,就先收到数据的一部分,另一部分就有延时,这样粘包问题就产生了;

  再者,TCP传输层协议传输的数据段有最大段MSS的限制,数据如果太大,那么会被分割为MSS大小的数据节,这也会导致粘包问题;

  或者,数据链路层有最大传输单元MTU的限制,如果数据超过了MTU,那么在IP就要对数据进行分组(分片),这也会造成粘包问题!另外,TCP的流量控制,拥塞控制,延迟发送机制,都可能会造成粘包问题!

2、解决粘包问题

既然TCP没有自己维护数据的边界,那么就需要我们自己在应用层维护!方法有几种:

定长包

包尾加上\r\n (FTP协议就这么干的)

包头指明包的数据长度

设计更复杂的应用层协议

一些问题:

如果采用定长包,那么两方通信会很浪费带宽,比如:定长定为1024字节,而实际的数据只有10个字节,而还是会填充成1024个字节去发送,这就浪费了1014个字节,因此最合适的方法还是在包头指明包的数据长度,这样灵活高效!

3、代码实现

公用函数的定义

#define handle_error(msg)   \
do{perror(msg);exit(EXIT_FAILURE);}while(0)

//包结构的定义
typedef struct package
{
int len;            //包实际的数据长度
char buf[1024];     //包的数据存储区
}Package;

//接收指定数目的数据
ssize_t readn(int fd, void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count;   //剩余字节数
ssize_t nread = 0;      //已读字节数
char *pbuf = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if (nread == 0)
return count - nleft;
pbuf += nread;
nleft -= nread;
}
return count;
}

//发送指定数目的数据
ssize_t writen(int fd, const void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count;   //剩余字节数
ssize_t nwritten = 0;      //已发送字节数
char *pbuf = (char*)buf;
while (nleft > 0)
{
if((nwritten = write(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue;
pbuf += nwritten;
nleft -= nwritten;
}
return count;
}


服务端代码

server.c

编译命令:gcc -Wall -g -std=gnu99 server.c -o server

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

//忽略子进程的SIG_CHLD信号
void handle_SIGIGN(void)
{
struct sigaction act_chld;
act_chld.sa_handler = SIG_IGN;
act_chld.sa_flags = 0;
sigemptyset(&act_chld.sa_mask);

if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕获终端中断信号
handle_error("sigaction SIGCHLD");
}

void do_work(int sock);

int main(void)
{
handle_SIGIGN();

int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");

//使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
int on = 1;
if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
close(sk_fd);
handle_error("setsockopt");
}

struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);

if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("bind");
}

//被动套接字
if(listen(sk_fd, SOMAXCONN) < 0)     //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
{
close(sk_fd);
handle_error("listen");
}

struct sockaddr_in cl_addr;
socklen_t cl_length = sizeof(cl_addr);
pid_t pid;
while (true)
{
memset(&cl_addr,0,sizeof(cl_addr));
int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
if(ac_sk < 0)
{
if(errno == EINTR)
continue;
close(sk_fd);
handle_error("accept");
}
printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));

//每个客户端对应一个子进程
pid = fork();
if(pid == -1)
handle_error("fork");
if(pid == 0)
{
close(sk_fd);
do_work(ac_sk);
close(ac_sk);
exit(EXIT_SUCCESS);
}
else
close(ac_sk);
}
close(sk_fd);
return 0;
}

void do_work(int sock)
{
Package recvbuf;
while(true)
{
memset(&recvbuf,0,sizeof(recvbuf));
int iret = readn(sock,&recvbuf.len,sizeof(recvbuf.len));    //获取包数据长度
if(iret == -1)
handle_error("read");
else if(iret < sizeof(recvbuf.len))
{
printf("Client was closed!\n");
break;
}
int n = ntohl(recvbuf.len);     //网络字节序转换为主机字节序
iret = readn(sock, recvbuf.buf, n);     //正式接收数据
if(iret == -1)
handle_error("read");
else if(iret < n)
{
printf("Client was closed!\n");
break;
}
printf("recv = %d\n", n);
fputs(recvbuf.buf,stdout);
writen(sock, &recvbuf, sizeof(recvbuf.len) + n);    //回传数据
}
}


客户端代码

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>

int main(void)
{
int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
//sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);

if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("connect");
}

Package sendbuf;
Package recvbuf;
while (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
{
int n = strlen(sendbuf.buf);
sendbuf.len = htonl(n);     //主机字节序转换为网络字节序
writen(sk_fd, &sendbuf, sizeof(sendbuf.len) + n);   //发送数据

memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
int iret = readn(sk_fd, &recvbuf.len, sizeof(recvbuf.len)); //接收包数据长度
if(iret == -1)
handle_error("read");
else if(iret < sizeof(recvbuf.len))
{
printf("Server was closed!\n");
break;
}
n = ntohl(recvbuf.len);     //网络字节序转换为主机字节序
iret = readn(sk_fd, recvbuf.buf, n);        //正式接收数据
if(iret == -1)
handle_error("read");
else if(iret < n)
{
printf("Server was closed!\n");
break;
}
printf("recv = %d\n", n);
fputs(recvbuf.buf, stdout);
}
close(sk_fd);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: