流协议与粘包
首先说明的是发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据包排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
假设主机A send了两条消息M1和M2各10k给主机B,由于主机B一次接收的字节数是不确定的,接收方收到数据的情况可能是:
• 一次性收到20k 数据
• 分两次收到,第一次5k,第二次15k
• 分两次收到,第一次15k,第二次5k
• 分两次收到,第一次10k,第二次10k
• 分三次收到,第一次6k,第二次8k,第三次6k
• 其他任何可能
粘包产生的原因
产生的原因主要有3个:一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数
1、用于程序写入的大小大于套接字发送缓冲区大小
2、进行MSS大小的TCP分段;
3、以太网帧的payload大于MTU进行IP分片时。
粘包处理方案
粘包本质是要在应用层维护消息与消息的边界 — 在传输层没有维护消息与消息的边界
* 定长包
* 包尾加\r\n(ftp采用这种方式--如果消息中本身就要\r\n,系统就无法区分)
* 包头加上包体长度
* 更复杂的应用层协议
定包长
readn、writen
以write函数为例,解释为何用readn和writen函数:
ssize_t write(int fd, const void*buf,size_t nbytes);
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数.失败时返回-1. 并设置errno变量. 在网络程序中,当我们向套接字文件描述符写时有两可能.
1)write的返回值大于0,表示写了部分或者是全部的数据. 这样我们用一个while循环来不停的写入,但是循环过程中的buf参数和nbyte参数得由我们来更新。也就是说,网络写函数是不负责将全部数据写完之后在返回的。
2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.
如果错误为EINTR表示在写的时候出现了中断错误.
如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).
为了处理以上的情况,我们自己编写一个写函数来处理这几种情况.
readn、writen—写入确切数目写操作、读取确切数目读操作
/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count) //参数和read相同-- ssize_t 有符号整数 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) // 信号中断--不认为出错
countinue;
return -1;
}
else if(nread == 0) //对等方关闭
return count - nleft; //返回已经读取的字节数
/*读到数据*/
bufp += nread; //偏移
nleft -= nread; //剩余还要读取的数据
}
return count;
}
说明:
EINTR — Linux中函数的返回状态,在不同的函数中意义不同
-》write—表示:由于信号中断,没写成功任何数据
-》read—表示:由于信号中断,没读到任何数据
包头加上包体长度
需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。
比较好的解决办法,其实也可以算是自定义的一种简单应用层协议–即封包,然后解包。比如我们可以自定义一个包体结构
struct packet {
int len;
char buf[1024];
};
先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。
更改—-socket编程–TCP客户/服务器模型 (c/s)—服务器端函数中的do_serverce程序
/* 回射服务器,客户端不停从标准输入接收数据
* 发送给服务器端,然后服务器端接收回射回去*/
void do_severce(int conn)
{
char recvbuf[1024];
while(1){
memset(recvbuf, '0', sizeof(recvbuf));
int ret = readn(conn, recvbuf, sizeof(recvbuf));
if(ret == 0){
printf("client close\n");
break;
}
else if(ret == -1){
fputs(recvbuf, stdout);
writen(conn, recvbuf, ret);
}
}
结果:
可看到无法读取客户端发过来的数据,因为在于do_serverce中char recvbuf[1024];–读取为1024个字节。对方如果发送的数据不足1024个字节,就会阻塞,一直在readn函数中循环。
解决方案为发送定长包。
回射客户/服务器
记录:
/服务器/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
struct packet
{
int len;
char buf[1024];
};
/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count) //参数和read相同-- ssize_t 有符号整数 size_t 无符号整数
{
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;
}
/*writen函数包装了write函数,用于写入定长包*/
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)//没有写满buf缓冲区,继续写入数据
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
/* 回射服务器,客户端不停从标准输入接收数据,
* 发送给服务器端,然后服务器端接收回射回去*/
void do_severce(int conn)
{
struct packet recvbuf;
int n;
while(1){
memset(&recvbuf, '0', sizeof(recvbuf));
int ret = readn(conn, &recvbuf.len, 4);
/*ret = 0是可判断客户端关闭*/
if(ret == -1)
exit(EXIT_FAILURE);
else if(ret < 4){
printf("client close\n");
break;
}
n = ntohl(recvbuf.len);
ret = readn(conn, recvbuf.buf, n);
if(ret == -1){
exit(EXIT_FAILURE);
}
else if(ret < n){
printf("client close\n");
break;
}
fputs(recvbuf.buf, stdout);
writen(conn, &recvbuf, 4 + n);
memset(&recvbuf, 0, sizeof(recvbuf));
}
}
int main()
{
int listenfd ;
if((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0){ //小于0表示创建失败
exit(EXIT_FAILURE);
printf("socket failed\n");
}
/*初始化*/
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr)); //
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5188);
seraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 第一种方式:INADDR_ANY表示本机的任意地址
/*seraddr.sin_addr.a_addr = inet_addr("127.0.0.1");*/ //第二种方式
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0){
exit(EXIT_FAILURE);
printf("setsockopt fialed");
}
/*绑定*/
if(bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) < 0){ //sockaddr_in强制转换为socketaddr
exit(EXIT_FAILURE);
printf("bind failed\n");
}
if(listen(listenfd, SOMAXCONN)){ //SOMAXCONN表示队列的最大值
exit(EXIT_FAILURE);
printf("listen failed\n");
}
struct sockaddr_in peeraddr;//定义对方地址
socklen_t peerlen = sizeof(peeraddr); //对方的地址长度,必须有初始值,否则accept会失败
int conn;
pid_t pid;
while(1){
/*父进程accept其他客户端,子进程处理连接*/
if((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0){
exit(EXIT_FAILURE);
printf("listen failed\n");
}
printf("ip = %s port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if(pid == -1){
exit(EXIT_FAILURE);
printf("fork failed\n");
}
if(pid == 0){//子进程
close(listenfd); //子进程不需要监听
do_severce(conn);
exit(EXIT_SUCCESS);//do_serverce一旦返回,这个进程就没有用---销毁进程
}
else{ //父进程
close(conn);
}
}
return 0;
}
/客户端/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
struct packet
{
int len;
char buf[1024];
};
/*readn函数包装了read函数,用于读取定长包*/
ssize_t readn(int fd, void *buf, size_t count) //参数和read相同-- ssize_t 有符号整数 size_t 无符号整数
{
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;
}
/*writen函数包装了write函数,用于写入定长包*/
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)//没有写满buf缓冲区,继续写入数据
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
int main()
{
int sock ;
if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0){ //小于0表示创建失败
exit(EXIT_FAILURE);
printf("socket failed\n");
}
/*初始化*/
struct sockaddr_in seraddr;
memset(&seraddr, 0, sizeof(seraddr)); //
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(5188);
seraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器地址
if(connect(sock, (struct sockaddr*)&seraddr, sizeof(seraddr)) < 0){
exit(EXIT_FAILURE);
printf("connect failed");
}
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);
writen(sock, &sendbuf, 4 + n);
int ret = readn(sock, &recvbuf.len, 4);
if(ret == -1){
exit(EXIT_FAILURE);
}
else if(ret < 4){
printf("client close\n");
break;
}
n = ntohl(recvbuf.len);
ret = readn(sock, recvbuf.buf, n);
if(ret == -1){
exit(EXIT_FAILURE);
}
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;
}