Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

时间:2023-03-09 20:18:57
Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

一、Linux发送网络消息的过程

(1) 应用程序调用write()将消息发送到内核中

( 2)内核中的缓存达到了固定长度数据后,一般是SO_SNDBUF,将发送到TCP协议层

(3)IP层从TCP层收到数据,会加上自己的包头然后发送出去。一般分片的大小是MTU(含IP包头),而IPV4下IP的包头长度为40,而IPV6下为60,因此,TCP中分片后,有效的数据长度为MSS = MTU - 40 或 MSS = MTU -60

(4)最终经过其他层的包装,发送到公网上,跑来跑去,这时候,你的数据可能几段连为一条,一条可能分为几段。

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

二、粘包问题

上一篇文章中,我们用write()系统调用来读取数据,但是这个调用需要指定长度,例如上文中的1024,那么问题来了:

(1)报文有效数据长1025怎么办 ? 对方发“Hi,I like you!” 你期望收到“Hi, I like ”吗

(2)报文有效数据长度300怎么办? 对方发“Hi, I like you!” "You are befutiful" 你期望收到“Hi, I like you!You are”么? 你不想知道 you are 什么,还有,明明对方发送了两条消息,而你。。。收到了一条半,还当作了一条。

三、解决

3.1主要有两种解决方案,分别为

(1)认为的加边界 例如以\R\N为界限,FTP协议就是用的这种方法。

(2)建立一个数据结构,如下:

 struct packet
{
int len;
char buff[];
};

发送前,将packet.len设置好,然后将该数据结构的一个实例发送过去,读的时候先读取int长度即4个字节的数据,获得buff的有效长度,然后循环读,直到读够len字节的数据为止。

本文主要介绍第二种设定数据结构的方案。该方案的一个小缺点是,单次写不会超过buff[1024]的大小限制。。。

3.2  readn函数:

 ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}

readn

3.3  writen函数;

 ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}

writen

3.4利用这两个函数,即可完成读写。下文将介绍利用这两个函数完成的一个P2P程序,服务端与客户端互相发送用户输入的数据:程序的架构如下:

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

3.4.1 服务端:

 #include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h> #include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h> #define ERR_EXIT(m) \
do { \
perror(m);\
exit(EXIT_FAILURE);\
}while() struct packet
{
int len;
char buff[];
}; ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}
ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}
void handle(int sig)
{
printf("recv sig = %d\n", sig);
exit();
}
int main(void)
{
signal(SIGUSR1,handle); int sockfd;
// 创建一个Socket
sockfd = socket(AF_INET,SOCK_STREAM,);
if(sockfd == -){
perror("error");
exit();
} ///////////////////////////////////////////////////////////
// struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
struct sockaddr_in sockdata;
sockdata.sin_family = AF_INET;
sockdata.sin_port = htons();
sockdata.sin_addr.s_addr = inet_addr("192.168.59.128"); int optval = ;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -)
{
perror("error");
exit();
}
if(bind(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) < ){
perror("error");
exit();
} ////////////////////////////////////////////////////////////
if(listen(sockfd,SOMAXCONN) == -){ //变成被动侦听套接字。
perror("error");
exit();
} //////////////////////////////////////////////////////////
struct sockaddr_in peeradr;
socklen_t peerlen = sizeof(peeradr); // 得有初始值 /////////////////////////////////////////////////////////
int conn = ;
conn = accept(sockfd,(struct sockaddr *)&peeradr,&peerlen);
if(conn == -){
perror("error");
exit();
} printf("收到的IP %s\n 客户端端口是:%d\n,conn == %d\n",inet_ntoa(peeradr.sin_addr),ntohs(peeradr.sin_port),conn); pid_t twopid;
twopid = fork(); if(twopid == -){
perror("error");
exit();
}
if(twopid > ){ // father , 接受数据
struct packet recvBuff;
memset(&recvBuff,,sizeof(recvBuff));
int ret = ;
int rn;
while(){
ret = readn(conn,&recvBuff,); // 先获得长度
if(ret == -){
ERR_EXIT("READ");
}
if(ret < ){
printf("client close\n");
break;
}
rn = ntohl(recvBuff.len);
ret = readn(conn,recvBuff.buff,rn);
if(ret == -){
ERR_EXIT("READ");
}
if(ret < rn){
printf("client close\n");
break;
}
fputs(recvBuff.buff,stdout);
memset(&recvBuff,,sizeof(recvBuff));
}
printf("client closed"); // may this create a guer process
// send signal to child
kill(twopid, SIGUSR1);
close(conn);
close(sockfd);
sleep();
exit();
}
if(twopid == ){ // child send data
close(sockfd);
int n;
struct packet sendBuff;
memset(&sendBuff,,sizeof(sendBuff));
while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){
n = strlen(sendBuff.buff);
sendBuff.len = htonl(n);
writen(conn,&sendBuff,+n);
memset(&sendBuff,,sizeof(sendBuff));
}
exit();
}
return ;
}

server.c

3.4.2 客户端:

 #include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h> #include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_EXIT(m) \
do { \
perror(m);\
exit(EXIT_FAILURE);\
}while() struct packet
{
int len;
char buff[];
}; ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}
ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}
int main(void)
{
int sockfd;
// 创建一个Socket
sockfd = socket(AF_INET,SOCK_STREAM,);
if(sockfd == -){
perror("error");
exit();
} ///////////////////////////////////////////////////////////
// struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
struct sockaddr_in sockdata;
sockdata.sin_family = AF_INET;
sockdata.sin_port = htons();
sockdata.sin_addr.s_addr = inet_addr("192.168.59.128");
if(connect(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) == -){
perror("error");
exit();
}
pid_t pid = ;
pid = fork();
if(pid == -){ perror("error");
exit();
}
if(pid > ){ // father // ccept data from keyboad
struct packet sendBuff;
memset(&sendBuff,,sizeof(sendBuff));
int n;
while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){ n = strlen(sendBuff.buff);
// 设置发送消息到长度。
sendBuff.len = htonl(n);
// 将结构体实例写入。
writen(sockfd,&sendBuff,+n); // 清零
memset(&sendBuff,,sizeof(sendBuff));
} }
if(pid == ){ // child recv data
struct packet recvBuff;
memset(&recvBuff,,sizeof(recvBuff));
// 从服
int ret;
int rn;
while(){
// 首先获得要读取到长度,前4个字节
ret = readn(sockfd,&recvBuff.len,);
if(ret == -){
ERR_EXIT("READ");
}
if(ret < ){
printf("server close\n");
break;
} // 读取4个字节开始到数据。
rn = ntohl(recvBuff.len);
ret = readn(sockfd,recvBuff.buff,rn);
if(ret == -){
ERR_EXIT("read error");
}
if(ret < rn ){
printf("server close\n");
break;
}
// put it to screen
fputs(recvBuff.buff,stdout);
// 清零
memset(&recvBuff,,sizeof(recvBuff));
} } close(sockfd);
return ;
}

client.c

后记:

由于上文中获得len的大小的方式是 测试buff[1024]中有效数据的长度的,所以实际上len每一个不会超过1024。

但是,当fgets函数接受的一行长度大于1023的时候,它会将剩下的(1023以后的)字符串作为下一次的输入。然后发送端会发送两个Packet实例。

而接收端接收到的两个pocket都有正确的长度,所以可以安全的接受,但是不幸的是,会将一条报文分成多条。。。

该程序是单进程的,读者可以自行改成多进程的。