socket编程(4)
tcp基于字节流,无边界 接收方不能保证一次是整个消息 产生粘包
udp传输数据包,有边界
协议流与粘包
一、粘包问题可以用下图来表示:
假设主机A send了两条消息M1和M2给主机B,由于主机B一次接收的字节数是不确定的,接收方收到数据的情况可能是:
1.第一次读M1的全部,第二次读M2 的全部
2.第一次读M1和M2的全部
3.第一次读M1的全部和M2的一部分,第二次读M2的剩余部分。
4.第一次读M1的一部分,第二次读M1的剩余部分和M2的全部。
5.其他
粘包产生的原因

1从过程分析 应用缓冲区>套接口发送缓冲区
2TCP有MSS 分割 IPMTU分片
3TCP 流量控制 拥塞控制 延迟发送机制 等。。。
记住结论: TCP有粘包问题
粘包问题的解决方案
本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)
1、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度
4、更复杂的应用层协议
TCP协议在传输层没有维护消息与消息之间的边界,那么我们就需要在应用层维护消息与消息之间的边界
这些解决方案有一个重要的问题--定长包的接收,我们怎样保证一次接收是一个定长的接收
我们刚刚说过了TCP是一个流协议,无法保证接收的长度。
那我们就需要一个函数,接收确定字节数的读操作
readn接收确切数目的读操作
writen发送确切数目的写操作
对于条目2,缺点是如果消息本身含有\r\n字符,则也分不清消息的边界。
对于条目1,即我们需要发送和接收定长包。因为TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用(套接字标志为阻塞),如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,如下所示。
readn和writen
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; } else if (nread == 0) //对方关闭或者已经读到eof 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; }
需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。
此时条目3是比较好的解决办法,其实也可以算是自定义的一种简单应用层协议。比如我们可以自定义一个包体结构
struct packet {
int len;
char buf[1024];
};
先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。服务器端在前面的fork程序的基础上把do_service函数更改如下:
回射客户/服务器
void do_service(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 error"); else if (ret < 4) //客户端关闭 { printf("client close\n"); break; } n = ntohl(recvbuf.len); ret = readn(conn, recvbuf.buf, n); if (ret == -1) ERR_EXIT("read error"); if (ret < n) //客户端关闭 { printf("client close\n"); break; } fputs(recvbuf.buf, stdout); writen(conn, &recvbuf, 4 + n); } }
客户端程序的修改与上类似,不再赘述。
通信正常
这次我们不是以定长来收发数据,而是加上了一个头部协议,头部当中规定了数据包的长度
发送的数据包是,包长+包体 。而接受方,第一次先接受长度,长度我们规定是4个字节,接受
完之后,在把数据包的长度计算出来,这样无形之中就将消息与消息的边界区分,从而结局了粘包问题。
之前我们没有考虑粘包问题,如果是在局域网,一般不会有粘包问题,如果放到广域网,就会出现。
这就是readn() writen()函数的封装 并且进一步完善了回射客户/服务器,给它加上了自定义协议包,
不是以定长的方式来接收。

浙公网安备 33010602011771号