由于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个字节
//因此我们需要循环进行读取
//直到读取的字节数为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)
{
//我们每次希望写入的字节数为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;
}
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,所以第三个参数可以置0
ERR_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;
}
#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,所以第三个参数可以置0
ERR_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个字节
//因此我们需要循环进行读取
//直到读取的字节数为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)
{
//我们每次希望写入的字节数为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)
{
//读取过程不一定要读取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;
}
}
//没有遇到\n
if(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,所以第三个参数可以置0
ERR_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;
}
#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;
}
}
//没有遇到\n
if(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,所以第三个参数可以置0
ERR_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进行读取并将已读取的数据移除缓冲区,这样我们就能够实现数据的按行读取。