【网络】(二)流协议粘包问题
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、代码实现
公用函数的定义
服务端代码
server.c
编译命令:gcc -Wall -g -std=gnu99 server.c -o server
客户端代码
client.c
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; }
相关文章推荐
- P2P中DHT网络介绍
- virtualbox复制虚拟机网络问题
- 对stdin,stdout 和STDOUT_FILENO,STDIN_FILENO的学习 http://www.cnblogs.com/hoys/archive/2011/05/11/2043044
- http的内置对象 Session Application Global.aspx文件
- (5.2.2)TCP和UDP的区别(转)
- TCP三次握手、四次挥手
- 网络管理技术(二)
- ios网络编程(二)之网络连接
- linux c 检测网络状态
- HttpRequest信息内容介绍
- HTTP协议中POST、GET、HEAD、PUT等请求方法以及一些常见错误(转载)
- 关于《JavaScript DOM 编程艺术》一书中getHTTPObject.js 的一点看法
- IOS 获取网络地址的html字符串 本地获取网络图片
- HttpClient使用HttpGet进行json数据传输
- Linux下常用命令-网络命令
- 网络电视精灵2
- VMware虚拟机上网络连接(network type)的三种模式--bridged、host-only、NAT
- http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html
- web service中配置tcp/ip监视器
- 网络请求之——————post