回射客户-服务器模型(4)

来源:互联网 发布:怎样定投网络理财投资 编辑:程序博客网 时间:2024/05/16 14:12

由于TCP是一种基于字节流的传输,属于无边界传输,所以它不能够处理消息与消息之间的边界问题,因此存在粘包问题。

如下图所示:


M1和M2是从主机A传送到主机B的两条消息,那么中间可能有几种传输情况:

a. 两条消息刚好分别完整的传输;

b. 先传输M1和M2的一部分,然后M2的另一部分单独传输;

c. 先传输M1的一部分,然后M1的另一部分和M2一起传输;


粘包产生的原因


有这几个原因会导致粘包问题:

a. 应用进程缓冲区的大小大于套接口缓冲区的大小,因此缓冲区中的数据一次性发送不完,产生粘包;

b. TCP层传递的数据段的最大限制为MSS,因此高层的数据如果超过这个值,也需要进行分割处理;

c. 链路层最大传输单元为MTU,因此,当上层的数据包的大小超过该值,需要分片;

d. 其他的导致粘包问题,比如TCP的流量控制,拥塞控制,延迟发送机制等都有可能导致粘包问题;


粘包解决方案

a. 本质上是要在应用层维护消息与消息的边界;

b. 发送定长包(让数据以定长的方式发送和接收);

c. 包尾加\r\n等分割符;

d. 设计更复杂的应用层协议;


下面我们以定长包的方式发送和接收数据包,主要封装了readn和writen函数。

服务器端:echosrv.c

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <errno.h>#include <unistd.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#define ERR_EXIT(m)\do\{\perror(m);\exit(EXIT_FAILURE);\}while(0)struct package{int len;char buf[1024];};ssize_t readn(int fd, void* buf, size_t count){//由于不能保证一次能够读取count个字节//因此我们需要循环进行读取//直到读取的字节数为countsize_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;}if(nread == 0)//表示对等方关闭,这里直接返回return count-nleft;nleft -= nread;//每次读取后剩余的字节数bufp += nread;}return count;}ssize_t writen(int fd, void* buf, size_t count){//我们每次希望写入的字节数为countsize_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;}if(nwritten == 0)//什么都没发生continue;nleft -= nwritten;//每次写后剩余要写的字节数bufp += nwritten;}return count;}void do_service(int conn){struct package recvbuf;int n;while(1){memset(&recvbuf, 0, sizeof(recvbuf));//包头与包体分开读取//先读取包头4个字节,进而确定包体的长度int ret = readn(conn,  &recvbuf.len, 4);if(ret == -1)ERR_EXIT("read failure");else if(ret < 4){printf("client close\n");break;}//再读取包体//包体的长度n存放在结构体的len中n = ntohl(recvbuf.len);ret = readn(conn, recvbuf.buf, n);if(ret == -1)ERR_EXIT("read failure");else if(ret < n){printf("client close\n");break;}fputs(recvbuf.buf, stdout);writen(conn, &recvbuf, 4+n);}}int main(void){//创建一个套接字int listenfd;if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0//if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0ERR_EXIT("socket_failure");//初始化地址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);//表示本机的任意地址,推荐使用(转换成网络字节序)//也可以自己显式指定//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//或者//inet_aton("127.0.0.1", &servaddr.sin_addr);//绑定之前开启地址重复利用int on = 1;if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)ERR_EXIT("setsockopt_failure");//接下来进行绑定,将该套接字与一个本地地址进行绑定//需要将IPv4地址结构强制转换为通用地址结构if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)ERR_EXIT("bind_failure");//绑定失败//接下来是监听,将socket从close状态转为监听状态才能够接受连接if(listen(listenfd, SOMAXCONN) < 0)ERR_EXIT("listen_failure"); //定义一个对方的地址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_failure");//输出客户端的地址和端口printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));//一旦获得一个连接,就创建一个进程pid = fork();if(pid == -1)ERR_EXIT("fork_failure");if(pid == 0){//让子进程处理已有的通信过程//不再需要监听套接口close(listenfd);do_service(conn);//一旦do_service函数返回,那么该进程就没有存在的价值了exit(EXIT_SUCCESS);//此时,为客户端开辟的进程也销毁了}else//父进程进行accept//不再需要连接套接口了,即conn(父子进程共享文件描述符)close(conn);}//实现一个回射客户/服务器模型//即客户端从标准输入获取数据,发送给服务器端,服务器端再回射过去return 0;}


客户端:echocli.c

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <errno.h>#include <unistd.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#define ERR_EXIT(m)\do\{\perror(m);\exit(EXIT_FAILURE);\}while(0)struct package{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;}if(nread == 0)//对等方关闭return count-nleft;nleft -= nread;bufp += nread;}return count;}ssize_t writen(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;}if(nwritten == 0)continue;nleft -= nwritten;bufp += nwritten;}return count;}int main(void){int sock;//创建一个套接字if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0//if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0ERR_EXIT("socket_failure");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");//客户端不需要绑定(bind),也不需要监听(listen)//直接连接过去就可以if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)ERR_EXIT("connect_failure");//如果连接成功,就可以进行通信struct package sendbuf;struct package 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);writen(sock, &sendbuf, 4+n);//先读取包头四个字节int ret = readn(sock, &recvbuf.len, 4);if(ret == -1)ERR_EXIT("read error");else if(ret == 0){printf("peer close\n");break;}//再读取包体,头部长度存储在recvbuf.len中n = ntohl(recvbuf.len);ret = readn(sock, recvbuf.buf, n);if(ret == -1)ERR_EXIT("read error");else if(ret == 0){printf("peerclose\n");break;}//显示出来fputs(recvbuf.buf, stdout);//这里需要清空缓冲区memset(&sendbuf, 0, sizeof(sendbuf));memset(&recvbuf, 0, sizeof(recvbuf));}//关闭套接口close(sock);return 0;}
说明:对于写writen,我们发送一个包的总长度为4+n,包括4字节的包头和n字节的包体;

            对于读readn,我们分别读取包头和包体,因为我们在发送的时候,将包体的长度n存放在了结构体的len变量中,因此需要先               读取包头,然后才能够获取包体的长度;


下面介绍另外一种方法解决粘包问题,我们封装一个readline函数,即按行读取消息。

先看代码:

服务器端:ehcosrv.c

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <errno.h>#include <unistd.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#define ERR_EXIT(m)\do\{\perror(m);\exit(EXIT_FAILURE);\}while(0)ssize_t readn(int fd, void* buf, size_t count){//由于不能保证一次能够读取count个字节//因此我们需要循环进行读取//直到读取的字节数为countsize_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;}if(nread == 0)//表示对等方关闭,这里直接返回return count-nleft;nleft -= nread;//每次读取后剩余的字节数bufp += nread;}return count;}ssize_t writen(int fd, void* buf, size_t count){//我们每次希望写入的字节数为countsize_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;}if(nwritten == 0)//什么都没发生continue;nleft -= nwritten;//每次写后剩余要写的字节数bufp += 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){//读取过程不一定要读取maxline个字节//只要遇到\n就可以返回int ret;int nread;char* bufp = 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'){//我们的recv_peek只是偷窥一下数据//并没有一走数据//所以这里用readn从缓冲区中移除已偷窥的数据ret = readn(sockfd, bufp, i+1);if(ret != i+1)exit(EXIT_FAILURE);return ret;}}//没有遇到\nif(nread > nleft)exit(EXIT_FAILURE);//把读到的数据nread个字节从缓冲区中移走nleft -= nread;ret = readn(sockfd, bufp, nread);if(ret != nread)exit(EXIT_FAILURE);//继续下一次的偷窥,需偏移bufp += nread;}return -1;}void do_service(int conn){char recvbuf[1024];int n;while(1){memset(&recvbuf, 0, sizeof(recvbuf));int ret = readline(conn,  recvbuf, 1024);if(ret == -1)ERR_EXIT("read failure");if(ret == 0){printf("client close\n");break;}fputs(recvbuf, stdout);writen(conn, recvbuf, strlen(recvbuf));}}int main(void){//创建一个套接字int listenfd;if((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0//if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0ERR_EXIT("socket_failure");//初始化地址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);//表示本机的任意地址,推荐使用(转换成网络字节序)//也可以自己显式指定//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//或者//inet_aton("127.0.0.1", &servaddr.sin_addr);//绑定之前开启地址重复利用int on = 1;if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)ERR_EXIT("setsockopt_failure");//接下来进行绑定,将该套接字与一个本地地址进行绑定//需要将IPv4地址结构强制转换为通用地址结构if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)ERR_EXIT("bind_failure");//绑定失败//接下来是监听,将socket从close状态转为监听状态才能够接受连接if(listen(listenfd, SOMAXCONN) < 0)ERR_EXIT("listen_failure"); //定义一个对方的地址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_failure");//输出客户端的地址和端口printf("IP=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));//一旦获得一个连接,就创建一个进程pid = fork();if(pid == -1)ERR_EXIT("fork_failure");if(pid == 0){//让子进程处理已有的通信过程//不再需要监听套接口close(listenfd);do_service(conn);//一旦do_service函数返回,那么该进程就没有存在的价值了exit(EXIT_SUCCESS);//此时,为客户端开辟的进程也销毁了}else//父进程进行accept//不再需要连接套接口了,即conn(父子进程共享文件描述符)close(conn);}//实现一个回射客户/服务器模型//即客户端从标准输入获取数据,发送给服务器端,服务器端再回射过去return 0;}


客户端:echocli.c

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <errno.h>#include <unistd.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>#define ERR_EXIT(m)\do\{\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;}if(nread == 0)//对等方关闭return count-nleft;nleft -= nread;bufp += nread;}return count;}ssize_t writen(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;}if(nwritten == 0)continue;nleft -= nwritten;bufp += 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){char* bufp = buf;int nleft = maxline;int nread;int ret;while(1){ret = recv_peek(sockfd, bufp, nleft);if(ret < 0)return ret;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;}}//没有遇到\nif(nread > nleft)exit(EXIT_FAILURE);nleft -= nread;ret = readn(sockfd, bufp, nread);if(ret != nread)exit(EXIT_FAILURE);//继续下一次偷窥bufp += nread;}return -1;}int main(void){int sock;//创建一个套接字if((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0//if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) //用AF_INET和PF_INET都可以,前两个参数已经可确定是TCP,所以第三个参数可以置0ERR_EXIT("socket_failure");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");//客户端不需要绑定(bind),也不需要监听(listen)//直接连接过去就可以if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)ERR_EXIT("connect_failure");//连接成功,查看本地的端口和地址struct sockaddr_in localaddr;socklen_t addrlen = sizeof(localaddr);if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)ERR_EXIT("getsockname error");printf("IP=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));//如果连接成功,就可以进行通信char sendbuf[1024] = {0};char recvbuf[1024] = {0};while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL){writen(sock, sendbuf, strlen(sendbuf));//先读取包头四个字节int ret = readline(sock, recvbuf, 1024);if(ret == -1)ERR_EXIT("read error");else if(ret == 0){printf("peer close\n");break;}//显示出来fputs(recvbuf, stdout);//这里需要清空缓冲区memset(sendbuf, 0, sizeof(sendbuf));memset(recvbuf, 0, sizeof(recvbuf));}//关闭套接口close(sock);return 0;}

说明:我们首先利用recv函数封装了一个recv_peek函数,recv函数的特点就是,它仅仅从套接口缓冲区中接收数据到buffer中,但是并不会将数据从该缓冲区中移除,而read函数在读取完数据后会将缓冲区中的数据移除。因此,我们这里就可以先用recv_peek函数先对缓冲区中的内容进行“偷窥”,然后就能知道所“偷窥”内容中有没有分隔符(我们这里是\n),“偷窥”后再利用readn进行读取并将已读取的数据移除缓冲区,这样我们就能够实现数据的按行读取。



1 0
原创粉丝点击