主要内容:
1、read,write 与 recv,send函数。 recv函数只能用于套接口IO ssize_t recv(int sockfd,void * buff,size_t len,int flags) ssize_t send(int sockfd,const void *buff,size_t len,int flags) flags为0或者为常值的或 MSG_OOB:发送或接收带外数据 (紧急数据)
MSG_PEEK:窥看外来消息(接收缓冲区数据,但并不将缓冲区数据清除) 2、readline函数实现 3、用readline实现回射客户/服务器 。利用readline解决粘包问题
客户端程序:
1 //利用readline函数解决粘包问题,每次读取一行。 换行 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/socket.h> 5 #include<string.h> 6 #include<stdlib.h> 7 #include<stdio.h> 8 #include<errno.h> 9 #include<netinet/in.h> 10 #include<arpa/inet.h> 11 #include<signal.h> 12 #define ERR_EXIT(m) 13 do 14 { 15 perror(m); 16 exit(EXIT_FAILURE); 17 }while(0) 18 ssize_t readn(int fd,void *buf,size_t count) 19 { 20 size_t nleft=count; 21 ssize_t nread; 22 char *bufp=(char*)buf; 23 while(nleft>0) 24 { 25 if((nread=read(fd,bufp,nleft))<0) 26 { 27 if(errno==EINTR) 28 continue; 29 else 30 return -1; 31 } 32 else if(nread==0) 33 return (count-nleft); 34 bufp+=nread; 35 nleft-=nread; 36 } 37 return count; 38 } 39 ssize_t writen(int fd, const void *buf, size_t count) 40 { 41 size_t nleft=count; 42 ssize_t nwritten; 43 char *bufp=(char*)buf; 44 while(nleft>0) 45 { 46 if((nwritten=write(fd,bufp,nleft))<0) 47 { 48 if(errno==EINTR) 49 continue; 50 return -1; 51 }else if(nwritten==0) 52 continue; 53 bufp+=nwritten; 54 nleft-=nwritten; 55 } 56 return count; 57 58 } 59 ssize_t recv_peek(int sockfd,void *buf,size_t len) 60 { 61 while(1) 62 { 63 int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf(len是buf的长度),但不去清空sockfd,偷窥 64 if(ret==-1&&errno==EINTR) 65 continue;//信号中断 66 return ret; 67 } 68 } 69 //偷窥方案实现readline,避免一次读取一个字符 70 ssize_t readline(int sockfd,void * buf,size_t maxline) 71 { 72 int ret; 73 int nread; 74 size_t nleft=maxline; 75 char *bufp=(char*)buf; 76 while(1) 77 { 78 ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看 79 if(ret<0) 80 return ret; 81 else if(ret==0) 82 return ret; 83 nread=ret; 84 int i; 85 for(i=0;i<nread;i++) 86 { 87 if(bufp[i]==' ') 88 { 89 ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空 90 if(ret!=i+1) 91 exit(EXIT_FAILURE); 92 return ret; 93 } 94 } 95 if(nread>nleft) 96 exit(EXIT_FAILURE); 97 nleft-=nread; 98 ret=readn(sockfd,bufp,nread); 99 if(ret!=nread) 100 exit(EXIT_FAILURE); 101 bufp+=nread;//移动指针继续窥看 102 } 103 return -1; 104 } 105 int main(void) 106 { 107 int sock;//客户端创建套接字 108 if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) 109 ERR_EXIT("socket error"); 110 111 struct sockaddr_in servaddr;//本地协议地址赋给一个套接字 112 memset(&servaddr,0,sizeof(servaddr)); 113 servaddr.sin_family=AF_INET; 114 servaddr.sin_port=htons(5188); 115 116 servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器段地址 117 //inet_aton("127.0.0.1",&servaddr.sin_addr); 118 119 if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) 120 ERR_EXIT("connect"); 121 122 //利用getsockname获取客户端本身地址和端口,即为对方accept中的对方套接口 123 struct sockaddr_in localaddr; 124 socklen_t addrlen=sizeof(localaddr);//需要初始化 125 if(getsockname(sock,(struct sockaddr *)&localaddr,&addrlen)<0) 126 ERR_EXIT("getsockname error"); 127 printf("local IP=%s, local port=%d ",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port)); 128 //使用getpeername获取对方地址 129 130 char sendbuf[1024]={0}; 131 char recvbuf[1024]={0}; 132 while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)//默认有换行符 133 { 134 writen(sock,sendbuf,strlen(sendbuf));//对方通过 来分割 135 int ret=readline(sock,recvbuf,1024); 136 if(ret==-1) 137 ERR_EXIT("readline"); 138 else if(ret==0) 139 { 140 printf("service closed "); 141 break; 142 } 143 fputs(recvbuf,stdout); 144 memset(sendbuf,0,sizeof(sendbuf)); 145 memset(recvbuf,0,sizeof(recvbuf)); 146 } 147 close(sock); 148 return 0; 149 }
服务器程序:
/* 服务端程序 */ #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<string.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<netinet/in.h> #include<arpa/inet.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; else return -1; } else if(nread==0) return (count-nleft); bufp+=nread; nleft-=nread; } return count; } ssize_t writen(int fd, const 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; }
//封装一个recv_peek函数 ssize_t recv_peek(int sockfd,void *buf,size_t len) { while(1) { //recv只能用于套接口IO。通过flags可以指定接收选项。MSG_PEEK接收缓冲区数据但不清除sockfd数据。read会清除。 int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf,但不去清空sockfd,偷窥 if(ret==-1&&errno==EINTR) continue; return ret; } } //偷窥方案实现readline(读取一行),避免一次读取一个字符.maxline 一行最多的字节数。 ssize_t readline(int sockfd,void * buf,size_t maxline) { int ret; int nread; size_t nleft=maxline;//不能超过maxline char *bufp=(char*)buf; while(1) { ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看。接下来可以去读sockfd内容。 if(ret<0) return ret;//失败 else if(ret==0) return ret;//对方关闭套接口 nread=ret; int i; //判断缓冲区中是否有 for(i=0;i<nread;i++) { if(bufp[i]==' ') { ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空sockfd这一行 if(ret!=i+1) exit(EXIT_FAILURE); return ret; } } //查看数据的时候缓冲区没有 .还不满一条消息,先将其读出来 if(nread>nleft) exit(EXIT_FAILURE);//字节数大于nleft(maxline) nleft-=nread;//这行剩余最多的字节数 ret=readn(sockfd,bufp,nread);//这些字符先从sockfd读走(还没有遇到 ) if(ret!=nread) exit(EXIT_FAILURE);//readn出错 bufp+=nread;//移动recv_peek缓冲区的指针,继续窥看。 } return -1; } void do_service(int conn) { int ret; char recvbuf[1024]; while(1) { memset(&recvbuf,0,sizeof(recvbuf)); //读取一行消息 ret=readline(conn,recvbuf,1024); //客户端关闭 if(ret==-1) ERR_EXIT("readline"); else if(ret==0) { printf("client close "); break;//不用继续循环等待客户端数据 } fputs(recvbuf,stdout); writen(conn,recvbuf,strlen(recvbuf)); } } int main(void) { int listenfd; if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) ERR_EXIT("socket error"); //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) //本地协议地址赋给一个套接字 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); //开启地址重复使用,关闭服务器再打开不用等待TIME_WAIT int on=1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0) ERR_EXIT("setsockopt error"); //绑定本地套接字 if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) ERR_EXIT("bind error"); if(listen(listenfd,SOMAXCONN)<0)//设置监听套接字(被动套接字) ERR_EXIT("listen error"); 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 error"); //连接好之后就构成连接,端口是客户端的。peeraddr是对端 printf("ip=%s port=%d ",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; }
当我们传输如文件这种数据时,流式的传输非常适合,但是当我们传输指令之类的数据结构时,流式模型就有一个问题:无法知道指令的结束。所以粘包必须问题是必须解决的。
短连接
最简单的方法就是短连接,也就是需要发送数据的时候建立TCP连接,发送完一个数据包后就断开TCP连接,这样接收端自然就知道数据结束了。
但是这样的方法因为会多次建立TCP连接,性能低下。随便用用还可以,只要稍微对性能有一点追求的人就不会使用这种方法。
长连接
使用长连接能够获得更好的性能但不可避免的会遇到如何判断数据结构的开始与结束的问题。
而此时的处理方式根据数据结构的类型分两种方式。
定长结构
因为粘包问题的存在,接收端不能想当然的以为发送端一次发送了多少数据就能一次收到多少数据。如果发送端发送了一个固定长度的数据结构,接收端必须每次都严格判断接收到额数据的长度,当收到的数据长度不足时,需要再次接收数据,直到满足长度,当收到的数据多于固定长度时,需要截断数据,并将多余的数据缓存起来,视为长度不足需要再次接收处理。
不定长结构
定长的数据结构是一种理想的情况,真正的应用中通常使用的都是不定长的数据结构。
对于发送不定长的数据结构,简单的做法就是选一个固定的字符作为数据包结束标志,接收到这个字符就代表一个数据包传输完成了。
但是这只能应用于字符数据,因为二进制数据中很难确定结束字符到底是结束还是原本要传输的数据内容(使用字符来标识数据的边界在传输二进制数据时时可以实现的,只是实现比较复杂和低效。想了解可以参考以太网传输协议)。
目前最通用的做法是在每次发送的数据的固定偏移位置写入数据包的长度。
接收端只要一开始读取固定偏移的数据就可以知道这个数据包的长度,接下来的流程就和固定长度数据结构的处理流程类似。
所以对于处理粘包的关键在于提前获取到数据包的长度,无论这个长度是提前商定好的还是写在在数据包的开头。
因为在每次发送的数据的固定偏移位置写入数据包的长度的方法是最通用的一种方法,所以对这种方法实现中的一些容易出错误的地方在此特别说明。
- 通常我们使用2~4个字节来存放数据长度,多字节数据的网络传输需要注意字节序,所以要注意接受者和发送者要使用相同的字节序来解析数据长度。
- 每次新开始接收一段数据时不要急着直接去解析数据长度,先确保目前收到的数据已经足够解析出数据长度,例如数据开头的2个字节存储了数据长度,那么一定确保接收了2个字节以上的数据后才去解析数据长度。
- 如果没做到这一点的服务器代码,收到了一个字节就去解析数据长度的,结果得到的长度是内存中的随机值,结果必然是崩溃的
- 有些非法客户端或者有bug的客户端可能会发出错误的数据,导致解析出的数据长度异常的大,一定要对解析出的数据长度做检查,事先规定一个合适的长度,一旦超过果断关闭SOCKET,避免服务器无休止的等待下去浪费资源。
- 不要妄想说自己写的客户端不会出错,哪怕客户端不出错,只要其他任何一个使用TCP的客户端写错了端口,也足以让你崩溃,毕竟管得了自己管不了别人
- 处理完一个完整的数据包后一定检查是否还有未处理的数据,如果有的话要对这段多余的数据再次开始解析数据长度的过程。不要忙着去继续接受数据。
- 这应该是最常犯的一个错误,很多人以为完整的处理了一个数据包后就万事大吉,可以重新开始处理流程,但是别忘了,收到的数据有可能带着下一个数据包的数据,别把他们忘掉