socket流协议与粘包
2016-01-03 11:28
513 查看
一、原理讲解
粘包的产生:发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据包排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
解决方案:本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)
1、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度
4、更复杂的应用层协议
二、代码
第一种方法:定长包
运行结果不再详述。
第二种方法:包尾加\r\n
这和之前的代码都大同小异,所以无需多言。
三、补充函数
gethostbyname
gethostbyaddr
写个小程序验证一下:
给出一个博客的连接,讲的比较详细。
http://andylin02.iteye.com/blog/662965
粘包的产生:发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据包排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
解决方案:本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)
1、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度
4、更复杂的应用层协议
二、代码
第一种方法:定长包
[code]/*packsrv.c*/ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/errno.h> #define ERR_EXIT(m)\ \ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); /*自定义包*/ struct packet{ int len; char buf[1024]; }; /*ssize_t:有符号整数。size_t:无符号整数*/ ssize_t readn(int fd,void* buf,size_t count){ size_t nleft = count;//剩余的字节数 ssize_t nread;//已接受的字节数 char* bufp = (char*)buf; while(nleft > 0){ if((nread = read(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nread == 0){ /*对等方关闭了*/ return count - nleft; } /*指针偏移*/ bufp += nread; nleft -= nread; } return count; } ssize_t nwritten(int fd,void* buf,size_t count){ size_t nleft = count;/*剩余要发送的字节数*/ ssize_t nwritten; char* bufp = (char*)buf; while(nleft > 0){ if((nwritten = write(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nwritten == 0){ continue; } /*指针偏移*/ bufp += nwritten; nleft -= nwritten; } return count; } void do_service(conn){ struct packet recvbuf; int n; while(1){ memset(&recvbuf,0,sizeof(recvbuf)); int ret = readn(conn,&recvbuf.len,sizeof(int)); /*int ret = readn(conn,recvbuf,sizeof(recvbuf));*/ if(ret == -1) ERR_EXIT("read"); if(ret < 4){ printf("client close...\n"); break; } ntohl(recvbuf.len); printf("包头数据的值是%d,",n); ret = readn(conn,recvbuf.buf,n); if(ret == -1){ ERR_EXIT("read"); } else if(ret < n){ printf("client close\n"); break; } printf("读入的字节即包的大小是%d\n",ret); fputs(recvbuf.buf,stdout); //printf("the sizeof your messge is %d\n:",ret); nwritten(conn,&recvbuf,sizeof(int)+n); } } int main(void){ int listenfd; if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0) ERR_EXIT("setsockopt"); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) ERR_EXIT("bind"); if(listen(listenfd,SOMAXCONN) < 0) ERR_EXIT("listen"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; pid_t pid; while(1){ if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0) ERR_EXIT("accept"); printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid = fork(); if(pid == -1) ERR_EXIT("fork"); if(pid == 0){ close(listenfd); do_service(conn); exit(EXIT_SUCCESS); } else close(conn); } return 0; }
[code]/*packcli.c*/ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/errno.h> #define ERR_EXIT(m)\ \ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); struct packet{ int len; char buf[1024]; }; ssize_t readn(int fd,void* buf,size_t count){ size_t nleft = count; ssize_t nread; char* bufp = (char*)buf; while(nleft > 0){ if((nread = read(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nread == 0){ return count - nleft; } bufp += nread; nleft -= nread; } return count; } ssize_t nwritten(int fd,void* buf,size_t count){ ssize_t nleft = count; size_t nwritten = count; char* bufp = (char*)buf; while(nleft > 0){ if((nwritten = write(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nwritten == 0){ continue; } bufp += nwritten; nleft -= nwritten; } return count; } int main(void){ int sock; if((sock = socket(PF_INET,SOCK_STREAM,0)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) ERR_EXIT("connect"); struct packet sendbuf; struct packet recvbuf; memset(&sendbuf,0,sizeof(sendbuf)); memset(&recvbuf,0,sizeof(recvbuf)); int n; while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL){ n = strlen(sendbuf.buf); //统一转换为网络字节序 sendbuf.len = htonl(n); /*发送定长字节*/ /*nwritten(sock,senbuf,sizeof(sendbuf));*/ nwritten(sock,&sendbuf,sizeof(int)+n);//包头和message /*writen(sock,sendbuf,strlen(sendbuf));*/ int ret = readn(sock,&recvbuf.len,sizeof(int)); if(ret == -1){ ERR_EXIT("read"); } else if(ret < 4){ printf("client close\n"); break; } /*转换成本机字节序*/ n = ntohl(recvbuf.len); ret = readn(sock,recvbuf.buf,n); if(ret == -1){ ERR_EXIT("read"); } else if(ret < n){ printf("client close\n"); break; } fputs(recvbuf.buf,stdout); memset(&sendbuf,0,sizeof(sendbuf)); memset(&recvbuf,0,sizeof(recvbuf)); } close(sock); return 0; }
运行结果不再详述。
第二种方法:包尾加\r\n
[code]/*peeksrv.c*/ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/errno.h> #define ERR_EXIT(m)\ \ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); /*ssize_t:有符号整数。size_t:无符号整数*/ ssize_t readn(int fd,void* buf,size_t count){ size_t nleft = count;//剩余的字节数 ssize_t nread;//已接受的字节数 char* bufp = (char*)buf; while(nleft > 0){ if((nread = read(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nread == 0){ /*对等方关闭了*/ return count - nleft; } /*指针偏移*/ bufp += nread; nleft -= nread; } return count; } ssize_t nwritten(int fd,void* buf,size_t count){ size_t nleft = count;/*剩余要发送的字节数*/ ssize_t nwritten; char* bufp = (char*)buf; while(nleft > 0){ if((nwritten = write(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nwritten == 0){ continue; } /*指针偏移*/ bufp += nwritten; nleft -= nwritten; } return count; } /*只是从缓冲区中获取数据,并不移除 * read函数读完了以后会把数据从缓冲区移除*/ ssize_t recv_peek(int sockfd,void* buf,size_t len){ while(1){ int ret = recv(sockfd,buf,len,MSG_PEEK); if(ret == -1 && errno == EINTR){ continue; } return ret; } } /*这里要用到readline函数,如果我们一个字符一个字符的判断有没有换行符 * 会非常麻烦。这里我们用偷窥的方法*/ ssize_t readline(int sockfd,void* buf,size_t maxline){ int ret; int nread; char* bufp = (char*)buf; int nleft = maxline; while(1){ ret = recv_peek(sockfd,bufp,nleft); if(ret < 0) return ret; else if(ret == 0) return ret; nread = ret; int i; for(i = 0;i < nread; i++){ if(bufp[i] == '\n'){ /*i+1因为后面有一个\n*/ ret = readn(sockfd,bufp,i+1); if(ret != i+1) exit(EXIT_FAILURE); return ret; } } /*如果读取的字节数超过剩下的字节数,这不可能,是错的*/ if(nread > nleft) exit(EXIT_FAILURE); nleft -= nread; ret = readn(sockfd,bufp,nread); if(ret != nread) exit(EXIT_FAILURE); bufp += nread; } return -1; } void do_service(conn){ char recvbuf[1024]; while(1){ memset(&recvbuf,0,sizeof(recvbuf)); int ret = readline(conn,recvbuf,1024); if(ret == -1) ERR_EXIT("read"); if(ret == 0){ printf("client close\n"); break; } fputs(recvbuf,stdout); //printf("the sizeof your messge is %d\n:",ret); nwritten(conn,recvbuf,strlen(recvbuf)); } } int main(void){ int listenfd; if((listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0) ERR_EXIT("setsockopt"); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) ERR_EXIT("bind"); if(listen(listenfd,SOMAXCONN) < 0) ERR_EXIT("listen"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; pid_t pid; while(1){ if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0) ERR_EXIT("accept"); printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid = fork(); if(pid == -1) ERR_EXIT("fork"); if(pid == 0){ close(listenfd); do_service(conn); exit(EXIT_SUCCESS); } else close(conn); } return 0; }
[code]/*peekcli.c*/ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/errno.h> #define ERR_EXIT(m)\ \ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); ssize_t readn(int fd,void* buf,size_t count){ size_t nleft = count; ssize_t nread; char* bufp = (char*)buf; while(nleft > 0){ if((nread = read(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nread == 0){ return count - nleft; } bufp += nread; nleft -= nread; } return count; } ssize_t nwritten(int fd,void* buf,size_t count){ ssize_t nleft = count; size_t nwritten = count; char* bufp = (char*)buf; while(nleft > 0){ if((nwritten = write(fd,bufp,nleft)) < 0){ if(errno == EINTR) continue; return -1; } else if(nwritten == 0){ continue; } bufp += nwritten; nleft -= nwritten; } return count; } ssize_t recv_peek(int sockfd,void* buf,size_t len){ while(1){ int ret = recv(sockfd,buf,len,MSG_PEEK); if(ret == -1 && errno == EINTR) continue; return ret; } } ssize_t readline(int sockfd,void* buf,size_t maxline){ int ret; int nread; char* bufp =(char*)buf; int nleft = maxline; while(1){ ret = recv_peek(sockfd,bufp,nleft); if(ret < 0){ return ret; } else if(ret == 0){ return ret; } nread = ret; int i; for(i = 0;i < nread;i++){ if(bufp[i] == '\n'){ ret = readn(sockfd,bufp,i+1); if(ret != i+1) exit(EXIT_FAILURE); return ret; } } if(nread > nleft) exit(EXIT_FAILURE); nleft -= nread; if(ret != nread) exit(EXIT_FAILURE); bufp += nread; } return -1; } int main(void){ int sock; if((sock = socket(PF_INET,SOCK_STREAM,0)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int conn; if((conn = connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))) < 0) ERR_EXIT("connect"); /*获取套接口本地的端口*/ struct sockaddr_in localaddr; socklen_t addrlen = sizeof(localaddr); if(getsockname(sock,(struct sockaddr*)&localaddr,&addrlen) < 0) ERR_EXIT("getsockname"); printf("ip = %s,port = %d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port)); /*获取对方的端口*/ struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); if(getpeername(conn,(struct sockaddr*)&peeraddr,&peerlen) < 0) ERR_EXIT("getpeername"); printf("ip = %s,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL){ nwritten(sock,sendbuf,strlen(sendbuf)); int ret = readline(sock,recvbuf,sizeof(recvbuf)); if(ret == -1){ ERR_EXIT("read"); } else if(ret == 4){ printf("client close\n"); break; } fputs(recvbuf,stdout); memset(sendbuf,0,sizeof(sendbuf)); memset(recvbuf,0,sizeof(recvbuf)); } close(sock); return 0; }
这和之前的代码都大同小异,所以无需多言。
三、补充函数
[code]int gethostname(char* name,size_t len); struct hostent* gethostbyname(const char* name); struct hostent* gethostbyaddr(const void* addr,socklen_t len,int type); /*gethostname -- 服务器程序使用了gethostname()函数来获取本机的名称,这个函数在实现上,会先查找/etc/hosts文件的内容,然后查询DNS服务器。如果/etc/hosts文件没有配置,返回的主机名就是localhost,也就是127.0.0.1 。所以,使用bind函数绑定的网络地址其实是一个127.0.0.1 。客户端想connect的话,就找不到连接了*/ /*gethostbyname() -- 用域名或主机名获取IP地址*/
gethostbyname
gethostbyaddr
写个小程序验证一下:
[code]#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/errno.h> #include <netdb.h> #include <unistd.h> #define ERR_EXIT(m)\ \ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0); int getlocalip(char*ip){ char host[100] = {0}; if(gethostname(host,sizeof(host)) < 0) ERR_EXIT("gethostname"); struct hostent* hp; if((hp = gethostbyname(host)) == NULL) return -1; strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr_list[0])); return 0; } int main(void){ char host[100] = {0}; if(gethostname(host,sizeof(host)) < 0) ERR_EXIT("gethostname"); struct hostent* hp; /*给该结构体指针分配安全的地址*/ hp = malloc(sizeof(struct hostent)); if((hp = gethostbyname(host)) < 0) ERR_EXIT("gethostbyname"); int i; i = 0; /*hp->h_addr_list[i] * hp->h_addr_list是一个二级指针 * hp->h_addr_list[i]是一个一级指针,它里面存放的是一个地址*/ while((hp->h_addr_list) != NULL){ printf("%s\n",inet_ntoa(*(struct in_addr*)hp->h_addr_list[i])); i++; } char ip[16] = {0}; getlocalip(ip); printf("localip = %s\n:",ip); return 0; }
给出一个博客的连接,讲的比较详细。
http://andylin02.iteye.com/blog/662965
相关文章推荐
- hibernate 各历史版本下载 spring各历史版本下载
- windows-docker开发我常用命令 docker-machine ssh default
- CentOS7 增加tomcat 启动,停止,使用systemctl进行配置
- Android开发学习之路-Service和Activity的通信
- nefu643teacher Li【字符位异或】
- cityhash 32位,64位和128位介绍及函数列表 http://www.169it.com/article/17600204886416241764.html
- iOS 关于屏幕旋转shouldAutorotate
- grep命令
- Ant基本使用指南
- html中offsetTop、clientTop、scrollTop、offsetTop各属性介绍
- R语言
- Redhat7 autofs自动挂载nfs共享的home目录
- maven配置编译环境
- 移动互联网相关的大会
- 数据可视化(data visualization)—— seaborn
- c#下使用webdriver WebDriverWait不能正确控制超时的问题
- 工作流——顺序工作流
- 第四部分 关系映射
- 如何学习html画布呢(canvas)
- iOS之block