Linux 网络编程详解五(TCP/IP协议粘包解决方案二)

时间:2022-09-04 16:18:51
ssize_t recv(int s, void *buf, size_t len, int flags);
--与read相比,只能用于网络套接字文件描述符
--当flags参数的值设置为MSG_PEEK时,recv可以从socket缓存中读取数据,但是不会将缓存中该部分数据清除
使用read函数直接读取socket缓存区中的内容,会清空缓存区中的内容。假设两段报文粘包,read会清空缓存
区中所有内容,从而导致后一段报文中的粘包的部分数据丢失
--强调:粘包解决方案包尾加\n,必须使用recv()函数,并且设置参数flags的值是MSG_PEEK
//粘包解决方案--包尾加\r\n
//服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> /*
* 思想,客户端发送报文时,每段报文都以\n结尾,服务端先用recv函数中的flag参数查看缓存区中的数据
* 以\n分割每个报文
* 注意,包尾加\r\n这个方案和包头加上包体长度方案最大的区别是包尾加\r\n这个方案并不清楚每次的包体长度是多少,
* 而包头加上包体长度方案确定确定包体的长度
* */ ssize_t readn(int fd, const void * buf, ssize_t count)
{
if (buf == NULL || fd < )
{
printf("readn() params not allow NULL!\n");
return -;
}
//定义剩余字符数
ssize_t lread = count;
//定义每次读取字符个数
ssize_t nread = ;
//定义字符串移动指针
char *pbuf = (char *) buf;
while (lread > )
{
nread = read(fd, pbuf, lread);
if (nread == -)
{
//屏蔽信号
if (errno == EINTR)
continue;
return -;
} else if (nread == )
{
printf("client is closed !\n");
return count - lread;
}
//重置剩余字节数
lread -= nread;
//指针后移
pbuf += nread;
}
return count;
} ssize_t writen(int fd, const void * buf, ssize_t count)
{
if (buf == NULL || fd < )
{
printf("writen() params not allow NULL!\n");
return -;
}
//定义剩余字符数
ssize_t lread = count;
//定义每次写入字符数
ssize_t nread = ;
//定义临时指针变量--假设pbuf的大小大于网络传输字符串的长度
char * pbuf = (char *) buf;
while (lread > )
{
nread = write(fd, pbuf, lread);
if (nread == -)
{
//屏蔽信号
if (errno == EINTR)
continue;
return -;
} else if (nread == )
{
printf("client is closed !\n");
return count - lread;
}
//重置剩余字节数
lread -= nread;
pbuf += nread;
}
return count;
} ssize_t recv_peek(int fd, const void * buf, ssize_t count)
{
int ret = ;
while ()
{
/*
* 当recv中flags参数的值是MSG_PEEK时,
* recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存)
* 遍历缓存区中的数据,找到\n,分割报文数据
* */
ret = recv(fd,(void *)buf, count, MSG_PEEK);
if (ret == - && errno == EINTR)
{
continue;
}
return ret;
}
return -;
} //读取缓存区中以\n结尾的字符串
ssize_t mreadline(int fd, const void *buf, ssize_t count)
{
//定义临时指针变量
char *pbuf = (char *) buf;
//定义自定义buf剩余的字节数
ssize_t lread = count;
//定义recv每次使用的字节数
ssize_t nread = ;
int ret = , i = ;
while ()
{
ret = recv_peek(fd, pbuf, lread);
if (ret < )
{
printf("recv_peek() failed !\n");
return -;
} else if (ret == )
{
printf("client is closed !\n");
return -;
}
nread = ret;
//遍历读取到的数据
for (i = ; i < ret; i++)
{
if (pbuf[i] == '\n')
{
//清空缓存区
memset(pbuf, , lread);
ret = readn(fd, pbuf, i + );
if (ret != i + )
return -;
//返回已经读取到的数据
return ret;
}
}
//如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
//recv()函数的返回值只可能小于或者等于count
if (nread == lread)
{
printf("自定义缓存buf的长度太小!\n");
return -;
}
//说明自定义buf还有空间,可以再接收一次
//为了读取下一段报文,需要先把socket缓冲区数据全部读取完成
ret = readn(fd, pbuf, nread);
if (ret != nread)
return -;
lread -= nread;
pbuf += nread;
}
return -;
} int main(int arg, char *args[])
{
//create socket
int listenfd = socket(AF_INET, SOCK_STREAM, );
if (listenfd == -)
{
perror("socket() err");
return -;
}
//reuseaddr
int optval = ;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
== -)
{
perror("setsockopt() err");
return -;
}
//bind
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (bind(listenfd, (struct sockaddr *) &addr, sizeof(addr)) == -)
{
perror("bind() err");
return -;
}
//listen
if (listen(listenfd, SOMAXCONN) == -)
{
perror("listen() err");
return -;
}
//accept
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen);
if (conn == -)
{
perror("accept() err");
return -;
}
char buf[] = { };
while ()
{
//获取一段报文
int rc = mreadline(conn, buf, );
if (rc == -)
{
exit();
}
//打印报文数据
fputs(buf, stdout);
//将原来的报文数据发送回去
rc = writen(conn, buf, strlen(buf));
if (rc == -)
{
exit();
}
}
return ;
}
//粘包解决方案--包尾加\r\n
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> ssize_t readn(int fd, const void *buf, ssize_t count)
{
//定义临时指针变量
char *pbuf = (char *)buf;
//定义每次已读数据
ssize_t nread = ;
//定义剩余数据
ssize_t lread = count;
while (lread > )
{
nread = read(fd, pbuf, lread);
/*
* 情况分析:假设b缓冲区buf足够大
* 如果nread==count,说明数据正好被读完
* nread<count,说明数据没有被读完,这种情况就是由于粘包产生的
* socket只接收了数据的一部分,TCP/IP协议不可能出现丢包情况
* nread==0,说明对方关闭文件描述符
* nread==-1,说明read函数报错
* nread>count,这种情况不可能存在
* */
if (nread == -)
{
//read()属于可中断睡眠函数,需要做信号处理
if (errno == EINTR)
continue;
perror("read() err");
return -;
} else if (nread == )
{
printf("client is closed !\n");
//返回已经读取的字节数
return count - lread;
}
//重新获取 剩余的 需要读取的 字节数
lread = lread - nread;
//指针后移
pbuf = pbuf + nread;
}
return count;
} /* fd:文件描述符
* buf:数据缓存区
* count:读取字符数
* */
ssize_t writen(int fd, const void *buf, ssize_t count)
{
//定义临时指针变量
char *pbuf = (char *)buf;
//每次写入字节数
ssize_t nwrite = ;
//剩余未写字节数
ssize_t lwrite = count;
while (lwrite > )
{
nwrite = write(fd, pbuf, lwrite);
if (nwrite == -)
{
if (errno == EINTR)
continue;
perror("write() err");
return -;
} else if (nwrite == )
{
printf("client is closed !\n");
//对方关闭文件描述符,返回已经写完的字节数
return count - lwrite;
}
lwrite -= nwrite;
pbuf += nwrite;
}
return count;
} ssize_t recv_peek(int fd, const void * buf, ssize_t count)
{
int ret = ;
while ()
{
/*
* 当recv中flags参数的值是MSG_PEEK时,
* recv函数会将socket缓存区中的数据读取到内存,并且不会清空socket缓存区(read()获取recv()不加参数时读取完数据后,会清空缓存)
* 遍历缓存区中的数据,找到\n,分割报文数据
* */
ret = recv(fd, (void *)buf, count, MSG_PEEK);
if (ret == - && errno == EINTR)
{
continue;
}
return ret;
}
return -;
} //读取缓存区中以\n结尾的字符串
ssize_t mreadall(int fd, const void *buf, ssize_t count)
{
//定义临时指针变量
char *pbuf = (char *) buf;
//定义自定义buf剩余的字节数
ssize_t lread = count;
//定义recv每次使用的字节数
ssize_t nread = ;
int ret = , i = ;
while ()
{
ret = recv_peek(fd, pbuf, lread);
if (ret < )
{
printf("recv_peek() failed !\n");
return -;
} else if (ret == )
{
printf("client is closed !\n");
return -;
}
nread = ret;
//遍历读取到的数据
for (i = ; i < ret; i++)
{
if (pbuf[i] == '\n')
{
//清空缓存区
memset(pbuf, , lread);
ret = readn(fd, pbuf, i + );
if (ret != i + )
return -;
//返回已经读取到的数据
return ret;
}
}
//如果没有读到\n,需要判断自定义buf是否还有空间--这种情况是一段报文被分割在多个包体中发送
//recv()函数的返回值只可能小于或者等于count
if (nread == lread)
{
printf("自定义缓存buf的长度太小!\n");
return -;
}
//说明自定义buf还有空间,可以再接收一次
//为了读取下一段报文,需要先把socket缓冲区数据全部读取完成
ret = readn(fd, pbuf, nread);
if (ret != nread)
return -;
lread -= nread;
pbuf += nread;
}
return -;
} int main(int arg, char *args[])
{
//create socket
int sockfd = socket(AF_INET, SOCK_STREAM, );
if (sockfd == -)
{
perror("socket() err");
return -;
}
//connect
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -)
{
perror("connect() err");
return -;
}
int rc = ;
char buf[]={};
while (fgets(buf, sizeof(buf), stdin) != NULL)
{
//发送数据
rc=writen(sockfd,buf,strlen(buf));
if (rc != strlen(buf))
{
return -;
}
//接收数据
memset(buf, , sizeof(buf));
rc = mreadall(sockfd,buf,sizeof(buf));
if(rc==-)
{
return -;
}
//打印包体
fputs(buf,stdout);
memset(buf, , sizeof(buf));
}
return ;
}

Linux 网络编程详解五(TCP/IP协议粘包解决方案二)的更多相关文章

  1. Linux 网络编程详解四(流协议与粘包)

    TCP/IP协议是一种流协议,流协议是字节流,只有开始和结束,包与包之间没有边界,所以容易产生粘包,但是不会丢包. UDP/IP协议是数据报,有边界,不存在粘包,但是可能丢包. 产生粘包问题的原因 . ...

  2. Linux 网络编程详解一(IP套接字结构体、网络字节序,地址转换函数)

    IPv4套接字地址结构 struct sockaddr_in { uint8_t sinlen;(4个字节) sa_family_t sin_family;(4个字节) in_port_t sin_p ...

  3. Linux 网络编程详解六(多进程服务器僵尸进程解决方案)

    小结:在点对点p2p程序中,服务器端子程序退出,子进程会主动发送信号,关闭父进程,但是这种模式导致服务器只能支持一个客户端连接,本章节中使用新的框架,子进程退出,不主动发送信号关闭父进程,而是父进程安 ...

  4. TCP&sol;UDP Linux网络编程详解

    本文主要记录TCP/UDP网络编程的基础知识,采用TCP/UDP实现宿主机和目标机之间的网络通信. 内容目录 1. 目标2.Linux网络编程基础2.1 嵌套字2.2 端口2.3 网络地址2.3.1 ...

  5. Linux 网络编程详解九

    TCP/IP协议中SIGPIPE信号产生原因 .假设客户端socket套接字close(),会给服务器发送字节段FIN: .服务器接收到FIN,但是没有调用close(),因为socket有缓存区,所 ...

  6. Linux 网络编程详解二(socket创建流程、多进程版)

    netstat -na | grep " --查看TCP/IP协议连接状态 //socket编程提高版--服务器 #include <stdio.h> #include < ...

  7. UNIX&sol;Linux网络编程基础:图解TCP&sol;IP协议栈

    目录 1.主机到网络层协议:以太网协议 2.IP协议 3.网际控制报文协议(ICMP) 4.传输控制协议(TCP) 5.用户数据报文协议(UDP) 6.流控制传输协议(SCTP) 7.地址解析协议(A ...

  8. Linux 网络编程详解十一

    /** * read_timeout - 读超时检测函数,不含读操作 * @fd:文件描述符 * @wait_seconds:等待超时秒数,如果为0表示不检测超时 * 成功返回0,失败返回-1,超时返 ...

  9. Linux 网络编程详解八

    TCP/IP协议三次握手机制 TCP/IP是全双工通道,两端都可以读写,三次握手机制就是验证TCP/IP是否是全双工通道 1.客户端调用connect()函数,阻塞客户端进程,客户端向服务器发送数据包 ...

随机推荐

  1. HTML5、微信、APP:创业寒冬只能选其一,该选哪个?

    HTML5手机网站 优势:开发技术简单,研发周期短,用户接触成本低 劣势:功能实现相比APP存在差距,用户重复使用难度大,用户粘性差 适合场景:把手机网站当成网络上的“电子产品介绍手册”.手机网站更适 ...

  2. 轻松搞定laravel的curd操作搞定简易留言版&lpar;四&rpar;

    一:目的开发laravel简易留言板 二:路由操作routes.php <?php //GET /msg/index 展示留言列表 //GET /msg/add 展示表单 //POST /msg ...

  3. AngularJS之代码风格36条建议【一】(九)

    前言 其实在新学一门知识时,我们应该注意下怎么书写代码更加规范,从开始就注意养成一个良好的习惯无论是对于bug的查找还是走人后别人熟悉代码都是非常好的,利人利己的事情何乐而不为呢,关于AngularJ ...

  4. Switch语句的case穿透

    Switch语句的case穿透 一 switch语句几点说明: 1. case后面只能是常量,不能是变量,而且,多个case后面的值不能出现相同的. 2.case后面表达式可以接受: 基本数据类型,b ...

  5. Python 获得对象内存占用内存大小 sys&period;getsizeof

    from sys import getsizeof class A(object): pass class B: pass for x in (None, 1, 1L, 1.2, 'c', [], ( ...

  6. Fiddler使用教程(收藏)

    Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许你监视,设置断点,甚至修改输入输出数据. 使用Fiddler无论对开发还是测试来说,都有很大 ...

  7. bzoj 2724 蒲公英 分块

    分块,预处理出每两个块范围内的众数,然后在暴力枚举块外的进行比较 那么怎么知道每一个数出现的次数呢?离散后,对于每一个数,维护一个动态数组就好了 #include<cstdio> #inc ...

  8. react native头部标题样式修改

    navigationOptions: ({navigation}) => ({ headerTitle:'评估记录', headerBackTitle:null, headerLeft:null ...

  9. Maven项目配置logback

    首先,在pom.xml中加入maven依赖 <!-- log start --> <dependency> <groupId>org.slf4j</group ...

  10. Android对接微信支付体验

    在写正文之前我不得不吐槽一下:微信支付所提供的参考文档以及技术支持真心太烂了. 微信的坑: 1.在生成prepay_id向微信服务器传递参数时<body>不支持中文.需要对其进行转码,否则 ...