TCP是字节流协议,原始数据之间是没有边界的。
发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。
所谓粘包问题本质还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
粘包问题解决方案:
0、发送和接收都定义固定大小。
1、发送方接收方都协商定义数据结构,每次发送后,接收方先解消息头确定,消息内容长度。
2、消息包之间定义明确结束标志,例如\n。
方案1实现:
客户端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
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;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
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;
}
else
return -1;
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
int main(void)
{
int sock;//被动套接字
if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<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(&recvbuf,0,sizeof(recvbuf));
memset(&sendbuf,0,sizeof(sendbuf));
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");
}
else if(ret < 4)
{
printf("srv_close\n");
break;
}
else
{
n = ntohl(recvbuf.len);//转换主机字节序
ret = readn(sock,&recvbuf.buf,n);
if(ret == -1)
{
ERR_EXIT("read");
}
else if(ret < n)
{
printf("srv_close\n");
break;
}
else
{
fputs(recvbuf.buf,stdout);//打印
memset(&recvbuf,0,sizeof(recvbuf));
memset(&sendbuf,0,sizeof(sendbuf));
}
}
}
close(sock);
return 0;
}
服务端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
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;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
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;
}
else
return -1;
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
void echo_srv(int conn)
{
struct packet recvbuf;
int n;
while(1)
{
memset(&recvbuf,0,sizeof(recvbuf));
int ret = readn(conn,&recvbuf.len,4);//先取长度,接收包头
if(ret == -1)
{
ERR_EXIT("read");
}
else if(ret < 4)
{
printf("client_close\n");
break;
}
else
{
n = ntohl(recvbuf.len);
ret = readn(conn,&recvbuf.buf,n);
if(ret == -1)
{
ERR_EXIT("read");
}
else if(ret < n)
{
printf("client_close\n");
break;
}
else
{
fputs(recvbuf.buf,stdout);//打印
writen(conn,&recvbuf,4+n);//回射-这里!!
}
}
}
}
int main(void)
{
/*signal(SIGCHLD,SIG_IGN);*/
int listenfd;//被动套接字
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
/* 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);//表示本机的任意地址
/*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");
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;//主动套接字
//父子进程可以共享文件描述符
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));
int pid = fork();
if(pid == -1)
ERR_EXIT("frok");
if(pid == 0)
{//子进程不需要处理监听套接字
close(listenfd);
echo_srv(conn);
exit(EXIT_SUCCESS);//如果通信结束(客户端关闭)直接结束进程,否则子进程也会去accept
}
else
{//父进程不需要处理连接套接字
close(conn);
}
}
return 0;
}
方案2实现:
客户端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
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;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
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;
}
else
return -1;
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
//从套接口接收数据,但并不把数据从缓冲区清除
ssize_t recv_peek(int sockfd,void *buf,int len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if((ret == -1) && (errno == EINTR))
continue;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxlen)//一行最大的字节数
{//只要遇到/n就返回
int ret;
int nread;
char *bufp = (char *)buf;
int nleft = maxlen;
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);//下标i,总共有i+1个字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果没有找到结束符,就读出来先缓存起来
if(nread > nleft)//ret = recv_peek(sockfd,bufp,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(PF_INET,SOCK_STREAM,IPPROTO_TCP))<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");
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,sizeof(recvbuf));
if(ret == -1)
{
ERR_EXIT("readline");
}
else if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
memset(recvbuf,0,sizeof(recvbuf));
memset(sendbuf,0,sizeof(sendbuf));
}
close(sock);
return 0;
}
服务端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
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;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
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;
}
else
{
return -1;
}
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
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;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxline)//一行最大的字节数
{//只要遇到/n就返回
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);//下标i,总共有i+1个字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果没有找到结束符,就读出来先缓存起来
if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
exit(EXIT_FAILURE);
nleft -= nread;//剩余的字节数
ret = readn(sockfd,bufp,nread);
if(ret != nread)//偷窥到的数据是可以全部读取出来的
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_srv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = readline(conn,recvbuf,1024);//按行接收
if(ret == -1)
{
ERR_EXIT("readline");
}
if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
writen(conn,recvbuf,strlen(recvbuf));//回射-这里!!
}
}
int main(void)
{
/*signal(SIGCHLD,SIG_IGN);*/
int listenfd;//被动套接字
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
/* 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);//表示本机的任意地址
/*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");
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;//主动套接字
//父子进程可以共享文件描述符
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));
int pid = fork();
if(pid == -1)
ERR_EXIT("frok");
if(pid == 0)
{//子进程不需要处理监听套接字
close(listenfd);
echo_srv(conn);
exit(EXIT_SUCCESS);//如果通信结束(客户端关闭)直接结束进程,否则子进程也会去accept
}
else
{//父进程不需要处理连接套接字
close(conn);
}
}
return 0;
}