“深入浅出”系列之Linux篇:(13)socket编程实战+TCP粘包解决方案

从日常使用的APP,到背后支撑的各类服务器,网络通信无处不在,而socket作为实现网络通信的关键技术,更是开发者们必须掌握的核心知识。但在socket编程的道路上,TCP粘包问题宛如一只拦路虎,让无数开发者头疼不已。它隐蔽又棘手,导致数据传输混乱,程序运行异常。

一:socket编程实战基础

1:什么是socket

socket可以看成是用户进程与内核网络协议栈的编程接口。TCP/IP协议的底层部分已经被内核实现了,而应用层是用户需要实现的,这部分程序工作在用户空间。用户空间的程序需要通过套接字来访问内核网络协议栈。

套接口是全双工的通信,它不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信。

套接字还可以异构系统间进行通信,异构系统指的是在硬件或软件上有所差别的系统,例如安卓系统的手机与windows系统的PC机上都可以实现QQ通信,套接字可以实现在这两个设备上的通信。

2:IPV4套接口地址结构

套接口既然能够连接两个端系统,那它就需要一个地址来标记该端系统,例如两个电话需要电话号码来标记才可以进行拨号。这抽象成套接口的地址结构。IPV4套接口地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在头文件< netinet/in/h >中。

struct sockaddr_in{uint8_t sin_len;sa_family_t sin_family;in_port_t sin_port;struct in_addr sin_addr;char sin_zero[8];
};

说明:

  1. sin_len:整个sockaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family。

  2. sin_family:指定带地址家族,在这里必须设置为AF_INET。socket在设计时不仅可以用于TCP/IP协议,它还可以用于其他协议,例如unix域协议,地址家族用于指定该套接字用于哪种协议。AF_INET表示用于IPV4协议。

  3. sin_port:端口号,16位的无符号整数,能够表示到65535。2个字节。

  4. sin_addr: IPV4的地址。4个字节的整数。

  5. sin_zero:暂不使用,一般将其设置为0。

其中,struct in_addr仅仅是一个32位的无符号整数,可以在终端下输入man 7 ip进行查看:

图片

接下来看一下通用的地址结构。上面说过,socket可以用于不同的协议上,通用的地址结构可以用于任何协议的socket编程。

struct sockaddr{uint8_t sin_len;sa_family sin_family;char sa_data[14];
};

说明:

  1. sin_len:整个sockaddr结构大小

  2. sin_family:指定该地址家族

  3. sa_data:由sin_family决定它的形式

可以看到,在通用地址结构中sa_data是14个字节,而在IPV4的地址结构中,sin_port、sin_addr、sin_zero三个变量加起来也等于14个字节。也即是说,这两种结构是兼容的。

3:网络字节序

字节序可以分为大端字节序与小端字节序:

  • 大端字节序(Big Endian) :最高有效位存储于最低内存地址处,最低有效位存储于最高地址内存处。

  • 小端字节序(Little Endian):刚好与大端字节序倒过来,最高有效位存于最高内存地址处,最低有效位存储于最低内存地址处。

这样说起来挺抽象,通过一幅图来说明:

图片

上面说过,socket可以用于异构系统之间的通信。而不同的系统采用的字节序可能是不同的,有的系统采用大端字节序,例如Motorola 6800;有的采用小端字节序,如X86。因此,在进行字节传输时,应该同一一个字节序,称为网络字节序。网络字节序采用大端字节序。如果主机A为小端字节序的系统,那么在传输时需要先将小端字节序转换成网络字节序。这需要一些字节序的转换函数。

我们可以编写程序来测试自己的主机是什么字节序:​​​​​​​

#include<stdio.h>int main(void)
{unsigned int x = 0x12345678;unsigned char *p = (unsigned char*)&x;printf("%0x,%0x,%0x,%0x\n",p[0],p[1],p[2],p[3]);return 0;
} 

输出结果:

图片

在我的电脑上输出结果为:78,56,34,12. 因此我的主机为小端字节序。

4:字节序转换函数

如果主机的字节序与网络字节序不同,那么需要进行字节序的转换。下面是一些字节序转换函数:​​​​​​​

   # include < arpa/inet.h >uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);

说明:h代表host;n代表network;s代表short;l代表long

描述:

  • htonl()函数将无符号整数hostlong从主机字节序转换成网络字节序。

  • htons()函数将无符号短整型hostshort从主机字节序转换成网络字节序。

  • ntohl()函数功能与 htonl()函数相反

  • ntohs()函数功能与htons()函数相反

我们可以进行验证,刚才已经通过程序测试出我的主机是小端字节序,接下来使用函数 htonl()将整数0x12345678转换成网络字节序。

#include<stdio.h>#include <arpa/inet.h>
int main(void){        unsigned int x = 0x12345678;        unsigned char *p = (unsigned char*)&x;        printf("转换前:%0x,%0x,%0x,%0x\n",p[0],p[1],p[2],p[3]);        unsigned int y = htonl(x);        p = (unsigned char *) &y;        printf("转换后:%0x,%0x,%0x,%0x\n",p[0],p[1],p[2],p[3]);
        return 0;

输出结果:

图片

5:地址转换函数

对于IP地址,我们通常采用点分十进制的形式进行直观的认识,而程序更多的时候是处理32位的地址,因此需要有函数在点分十进制与32位地址这两种形式间进行转换。

   # include < sys/socket.h>   # include < netinet/in.h>   # include < arpa/inet.h>
   int inet_aton(const char *cp, struct in_addr *inp);   in_addr_t inet_addr(const char *cp);   char *inet_ntoa(struct in_addr in);

描述:

  • inet_addr()函数:表示将点分十进制的IP地址转换成32位的ip地址(整数)。

  • inet_ntoa()函数:将32位ip地址(网络字节序)转换成点分十进制的ip之地。

实战例程:

​​​​​​​

#include<stdio.h>#include<arpa/inet.h>
int main(){        unsigned long addr = inet_addr("192.168.0.100");//将点分十进制转换为32bit地址        printf("addr = %u\n",htonl(addr));                 struct in_addr ipaddr;        ipaddr.s_addr = addr;        printf("ipaddr = %s\n",inet_ntoa(ipaddr)); //网络字节序地址转换为点分十>进制
        return 0;}

输出结果:

图片

6:套接字类型

套接字类型主要有三种:

  1. 流方套接字(SOCK_STREAM):它对应TCP协议,它提供面向连接的、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。

  2. 数据报套接字(SOCK_DGREAM):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。

  3. 原始套接字(SOCK_RAW):它提供一种能力,让我们直接跨越传输层,直接对IP层进行数据封装,通过该套接字,我们可以直接将数据封装成IP层能够认识的协议格式。

二:socket编程实战API接口

1:socket()函数

socket()函数用于创建一个套接字。这就好像购买了一个电话。不过该电话还没有分配号码。

 #include <sys/types.h> #include <sys/socket.h>  int socket(int domain, int type, int protocol)

参数说明:

  • domain:指定通信的协议族,这些协议族定义在头文件< sys/socket.h >中。使用IPV4协议族时,该参数设置为AF_INET

  • type :指定socket的类型。在上一篇文章中介绍过,套接字常用的有三种类型:流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW

  • protocol : 该参数指定了一种协议类型用于所选择的套接字。如果仅有一种协议支持某种套接字类型,那么该参数可以定义为0,此时使用默认协议;如果一种套接字类型可能有多种协议类型,那么必须显式指定协议类型。关于具体细节,可以man socket进行查阅。

socket()的返回值:成功时返回非负整数;失败时返回-1;

2:bind() 函数

bind()函数绑定一个本地地址到套接字上,这相当于为电话绑定了号码。当一个套接字通过socket()被创建,它并没有绑定到具体的地址上,bind()来完成这个步骤。 bind()函数的函数原型如下:

 #include <sys/types.h>    #include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数说明:

  • sockfd:socket()函数创建后成功返回的套接字

  • addr : 需要绑定的地址

  • addrlen:套接字的大小

这里需要使用到sockaddr_in结构来表示一个地址,该结构如下:

struct sockaddr_in{sa_family_t  sin_family;in_port_t    sin_port;struct in_addr sin_addr;};
struct in_addr{uint32_t s_addr;}

sockaddr_in需要强制转换为struct sockaddr*类型,传递给bind()函数的第二个参数。下面是一段例程:

int main(){
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if(listenfd == -1)
err_exit("socket error");
struct sockaddr_in addr;
//填充结构
addr.sin_family  = AF_INET;
addr.sin_port= htons(8001); //主机字节序转换为网络字节序
addr.sin_addr= htonl(INADDR_ANY);//绑定主机的任一个IP地址
/*下面两句具有相同的功能:都是绑定到本机ip地址*/
//inet_aton("127.0.0.1",&addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(bind(listenfd,(const struct sockaddr*)&addr,sizeof(addr))==-1)
err_exit("bind error");}

3:listen()函数

当使用socket()创建了一个套接字时,该套接字默认是主动套接字。使用listen()函数会使套接字称为一个被动套接字,也就是说,该套接字将被用来接受连接的数据,这些数据通过accept()函数接收。

listen()函数的函数原型如下:

 #include <sys/types.h>  #include <sys/socket.h>
 int listen(int sockfd, int backlog);

参数说明:

  • sockfd : 套接字。

  • backlog: 指定连接队列的长度。

对于给定的监听套接字,内核需要维护两个队列:

  1. 已完成连接队列:该队列中的连接处于ESTABLISHED状态,也即是已经完成了三次握手过程。

  2. 未完成连接队列:该队列中的连接处于SYN_RCVD状态,还未建立连接。

两个队列的长度之和不能够超过backlogi。如果一个连接请求到达时未完成队列已满,客户端可能接收到一个错误指示ECONNREFUSED。服务器使用accept()函数从已完成连接队列的队头返回一个连接。下面是TCP为监听套接口维护的两个队列:

4:accept()函数

accept()函数用于从已完成队列的队头返回一个连接。它的函数原型为:

#include <sys/types.h> #include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  • sockfd : 服务器套接字

  • addr :用于接收对等方(客户端)的套接字地址。该参数填充为NULL时,不接收任何信息。

  • addrlen:返回对等方的套接字地址长度。如果不关心可以设置为NULL,否则一定要初始化。

函数返回值:成功返回一个非负整数,代表一个套接字;失败返回-1;

5:connect()函数

该函数用于建立一个连接到指定的套接字。函数的原型为:

#include <sys/types.h>#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd : 未连接的套接字

  • addr:未连接的套接字地址

  • addrlen:addr的长度

6:简单的socket通信例程

客户端代码:

#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<arpa/inet.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>
#define ERR_EXIT(m)\        do \        {\                perror(m);\                exit(EXIT_FAILURE);\        }while(0)
int main(){        /*创建一个套接字*/        int sock = socket(AF_INET,SOCK_STREAM,0);        if(sock == -1)              ERR_EXIT("socket");
        /*定义一个地址结构*/        struct sockaddr_in servaddr;        memset(&servaddr,0,sizeof(servaddr));        servaddr.sin_family = AF_INET;        servaddr.sin_port = htons(5888);        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
        /*进行连接*/        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)        {                ERR_EXIT("connect");        }        else        {                printf("连接成功\n");        }        char sendbuf[1024]={0};        char recvbuf[1024]={0};        /*从标准输入中读入*/        while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)        {                write(sock ,sendbuf,strlen(sendbuf));                if(read (sock,recvbuf,sizeof(recvbuf))>0)                {                        printf("从服务器接收信息:\n");                        fputs(recvbuf,stdout);                }                memset(&sendbuf,0,sizeof(sendbuf));                memset(&recvbuf,0,sizeof(recvbuf));        }        close(sock);                return 0;    } 

服务器端代码:

#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<arpa/inet.h>#include<netinet/in.h>#include <sys/types.h>#include <sys/socket.h>#include<string.h>#define ERR_EXIT(m)\        do \        {\                perror(m);\                exit(EXIT_FAILURE);\        }while(0)
int main(){        /* 创建一个套接字*/
        int listenfd= socket(AF_INET ,SOCK_STREAM,0);        if(listenfd==-1)                ERR_EXIT("socket");
        /*定义一个地址结构并填充*/        struct sockaddr_in addr;        addr.sin_family = AF_INET;   //协议族为ipv4        addr.sin_port = htons(5888); //绑定端口号        addr.sin_addr.s_addr = htonl(INADDR_ANY);//主机字节序转为网络字节序
        /*将套接字绑定到地址上*/        if(bind(listenfd,(const struct sockaddr *)&addr ,sizeof(addr))==-1)        {                ERR_EXIT("bind");        }        /*监听套接字,成为被动套接字*/        if(listen(listenfd,SOMAXCONN)<0)        {                ERR_EXIT("Listen");        }        struct sockaddr_in peeraddr;        socklen_t peerlen = sizeof(peeraddr);
        /*定义一个套接字,通常称为已连接套接字*/        int conn ;        conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);        if(conn <0)            ERR_EXIT("accept error");        else            printf("连接到服务器的客户端的IP地址是:%s,端口号是:%d\n",inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));
        /*循环获取数据、发送数据*/        char recvbuf[1024];        while(1)        {                memset(recvbuf,0,sizeof(recvbuf));                int ret = read(conn,recvbuf ,sizeof(recvbuf));                fputs(recvbuf,stdout);                write(conn,recvbuf,sizeof(recvbuf));        }        /*关闭套接字*/        close(listenfd);        close(conn);        return 0;}

三:多连接服务器实现,P2P程序实战例程

在该例程序中,使用"Ctrl+c"结束通信后,服务器是无法立即重启的,如果尝试重启服务器,将被告知:

bind: Address already in use

原因在于服务器重新启动时需要绑定地址:

bind (listenfd , (struct sockaddr*)&servaddr, sizeof(servaddr));

而这个时候网络正处于TIME_WAIT的状态,只有在TIME_WAIT状态退出后,套接字被删除,该地址才能被重新绑定。TIME_WAIT的时间是两个MSL,大约是1~4分钟。若每次服务器重启都需要等待TIME_WAIT结束那就太不合理了,好在选项SO_REUSEADDR能够解决这个问题。
服务器端尽可能使用REUSEADD,在bind()之前调用setsockopt来设置SO_REUSEADDR套接字选项,使用SO_REUSEADDR选项可以使不必等待TIME_WAIT状态消失就可以重启服务器。

/*设置地址重复使用*/int on = 1; //on为1表示开启if(setsockopt(listenfp ,SOL_SOCKET,SO_REUSEADDR,&on,sieof(on))<0)    ERR_EXIT("setsockopt error");

处理多客户的服务器

在上一篇文章例程中,服务器端只能够连接一个客户端,并不能处理多个客户端的连接。原因在于服务器使用accept从已连接队列中获取一个连接后,便进入了对该连接的服务中,处于while循环状态。当一个新的客户端连接已经放入已连接队列时,服务器并不能执行到accpet的代码去获取队列中的连接。
为了解决这个问题,我们可以fork()一个子进程,让子进程来处理一个客户端的连接,而父进程循环执行accept的代码,获取新的连接:

        int conn ;        while(1)        {                conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);                if(conn <0)                        ERR_EXIT("accept error");                else                       printf("连接到服务器的客户端的IP地址是:%s,端口号是:%d\n",inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));                pid_t pid ;                pid = fork();//创建一个新进程                if (pid ==0) //子进程                {                        close(listenfd); //子进程不需要监听套接字,将其关闭                        /*循环获取数据、发送数据*/                        char recvbuf[1024];                        while(1)                        {                                memset(recvbuf,0,sizeof(recvbuf));                                int ret = read(conn,recvbuf ,sizeof(recvbuf));                                fputs(recvbuf,stdout);                                write(conn,recvbuf,sizeof(recvbuf));                        }                        exit(EXIT_SUCCESS);                }                if(pid >0) //父进程                {                        close(conn);//父进程无需该连接套接字,它的任务是执行accept获取连接                      }                else                 {                        close(conn);                }        }

启动服务器端,使用多个客户端进行连接,可以看到服务器能够同时处理多个连接:

图片

实现一个P2P简单聊天程序

为了实现聊天的功能,客户端与服务器端都需要有一个进程来读取连接,另一个进程来处理键盘输入。使用fork()来完成这个简单的聊天程序。
客户端程序:

//p2pcli.c#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<signal.h>#include<arpa/inet.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#define ERR_EXIT(m)\        do \        {\                perror(m);\                exit(EXIT_FAILURE);\        }while(0)void handler(){        exit(EXIT_SUCCESS);}int main(){        /*创建一个套接字*/        int sock = socket(AF_INET,SOCK_STREAM,0);        if(sock == -1)        {                ERR_EXIT("socket");        }        /*定义一个地址结构*/        struct sockaddr_in servaddr;        memset(&servaddr,0,sizeof(servaddr));        servaddr.sin_family = AF_INET;        servaddr.sin_port = htons(5888);        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");        /*进行连接*/        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)        {                ERR_EXIT("connect");        }        else        {                printf("连接成功\n");        }        pid_t pid ;        pid = fork();        if(pid == -1)                ERR_EXIT("fork");        if(pid == 0) //子进程复制接收数据并显示出来        {                char recvbuf[1024]={0};                while(1)                {                        memset(recvbuf,0,sizeof(recvbuf));                        int ret = read(sock ,recvbuf,sizeof(recvbuf));                        if(ret == -1)                        {                                ERR_EXIT("read");                        }                        if(ret == 0) //连接关闭                        {                                printf("连接关闭\n");                                kill(getppid(),SIGUSR1);                                break;                        }                        else                        {                                printf("接收到信息:");                                fputs(recvbuf,stdout);                        }                }        }        else //父进程负责从键盘接收输入并发送        {                signal(SIGUSR1,handler);                char sendbuf[1024]={0}  ;                while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)                {                        write(sock,sendbuf,strlen(sendbuf));                        memset(&sendbuf,0,sizeof(sendbuf));                }        }        close(sock);        return 0;}

服务器端程序:

// p2pser.c#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<signal.h>#include<arpa/inet.h>#include<netinet/in.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#define ERR_EXIT(m)\        do \        {\                perror(m);\                exit(EXIT_FAILURE);\        }while(0)/*信号处理函数*/void handler(int sig){        exit(EXIT_SUCCESS);}int main(){        /* 创建一个套接字*/        int listenfd= socket(AF_INET ,SOCK_STREAM,0);        if(listenfd==-1)                ERR_EXIT("socket");        /*定义一个地址结构并填充*/        struct sockaddr_in addr;        addr.sin_family = AF_INET;   //协议族为ipv4        addr.sin_port = htons(5888); //绑定端口号        addr.sin_addr.s_addr = htonl(INADDR_ANY);//主机字节序转为网络字节序        /*重复使用地址*/        int on = 1;        if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)        {                ERR_EXIT("setsockopt");        }        /*将套接字绑定到地址上*/        if(bind(listenfd,(const struct sockaddr *)&addr ,sizeof(addr))==-1)        {                ERR_EXIT("bind");        }        /*监听套接字,成为被动套接字*/        if(listen(listenfd,SOMAXCONN)<0)        {                ERR_EXIT("Listen");        }        struct sockaddr_in peeraddr;        socklen_t peerlen = sizeof(peeraddr);        int conn ;        conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);       if(conn <0)            ERR_EXIT("accept error");        else            printf("连接到服务器的客户端的IP地址是:%s,端口号是:%d\n",inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));        pid_t pid ;        pid = fork();//创建一个新进程        if(pid == -1)        {                ERR_EXIT("fork");        }        if(pid == 0)//子进程        {                signal(SIGUSR1,handler);                char sendbuf[1024] = {0};                while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)                {                        write(conn,sendbuf,sizeof(sendbuf));                        memset(sendbuf,0,sizeof(sendbuf));                }                exit(EXIT_SUCCESS);        }        else    //父进程 用来获取数据        {                char recvbuf [1024]={0};                while(1)                {                        memset(recvbuf,0,sizeof(recvbuf));                        int ret = read(conn ,recvbuf,sizeof(recvbuf));                        if(ret == -1)                        {                                ERR_EXIT("read");                        }                        if(ret == 0) //对方已关闭                         {                                printf("对方关闭\n");                                break;                        }                        fputs(recvbuf,stdout);                }                kill(pid,SIGUSR1);                exit(EXIT_SUCCESS);        }        /*关闭套接字*/        close(listenfd);        close(conn);        return 0;

四:TCP粘包问题及5种完美解决方案

① TCP是个流协议,它存在粘包问题

TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的。这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的。TCP的发送方无法保证对等方每次接收到的是一个完整的数据包。主机A向主机B发送两个数据包,主机B的接收情况可能是

图片

产生粘包问题的原因有以下几个:

  • 第一 。应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。

  • 第二种情况是,TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。

  • 第三种情况由于链路层最大发送单元MTU,在IP层会进行数据的分片。

这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。

② 粘包的问题的解决思路
粘包问题的最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪。我们通过使用某种方案给出边界,例如:

  • 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。

  • 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。

  • 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。

  • 使用更加复杂的应用层协议。

③ 粘包解决方案一:使用定长包
这里需要封装两个函数:

ssize_t readn(int fd, void *buf, size_t count)ssize_t writen(int fd, void *buf, size_t count)

这两个函数的参数列表和返回值与readwrite一致。它们的作用的读取/写入count个字节后再返回。其实现如下:

ssize_t readn(int fd, void *buf, size_t count){        int left = count ; //剩下的字节        char * ptr = (char*)buf ;        while(left>0)        {                int readBytes = read(fd,ptr,left);                if(readBytes< 0)//read函数小于0有两种情况:1中断 2出错                {                        if(errno == EINTR)//读被中断                        {                                continue;                        }                        return -1;                }                if(readBytes == 0)//读到了EOF                {                        //对方关闭呀                        printf("peer close\n");                        return count - left;                }                left -= readBytes;                ptr += readBytes ;        }        return count ;}
/*writen 函数写入count字节的数据*/ssize_t writen(int fd, void *buf, size_t count){        int left = count ;        char * ptr = (char *)buf;        while(left >0)        {                int writeBytes = write(fd,ptr,left);                if(writeBytes<0)                {                        if(errno == EINTR)                                continue;                        return -1;                }                else if(writeBytes == 0)                        continue;                left -= writeBytes;                ptr += writeBytes;        }        return count;}

有了这两个函数之后,我们就可以使用定长包来发送数据了,我抽取其关键代码来讲诉:​​​​​​​

char readbuf[512];readn(conn,readbuf,sizeof(readbuf));  //每次读取512个字节

同理的,写入的时候也写入512个字节​​​​​​​

char writebuf[512]; fgets(writebuf,sizeof(writebuf),stdin); writen(conn,writebuf,sizeof(writebuf); 

每个消息都以固定的512字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。

④ 粘包解决方案二:使用结构体,显式说明数据部分的长度

在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。发送端的对等方接收报文时,先读取前四个字节,获取数据的长度,由长度来进行数据的读取。定义一个结构体

struct packet{        unsigned int msgLen ;  //4个字节字段,说明数据部分的大小        char data[512] ;  //数据部分 }

读写过程如下所示,这里抽取关键代码进行说明:

//发送数据过程struct packet writebuf;memset(&writebuf,0,sizeof(writebuf));while(fgets(writebuf.data,sizeof(writebuf.data),stdin)!=NULL){      int n = strlen(writebuf.data);   //计算要发送的数据的字节数writebuf.msgLen =htonl(n);    //将该字节数保存在msgLen字段,注意字节序的转换writen(conn,&writebuf,4+n);   //发送数据,数据长度为4个字节的msgLen 加上data长度memset(&writebuf,0,sizeof(writebuf)); }

下面是读取数据的过程,先读取msgLen字段,该字段指示了有效数据data的长度。依据该字段再读出data。

  memset(&readbuf,0,sizeof(readbuf));int ret = readn(conn,&readbuf.msgLen,4); //先读取四个字节,确定后续数据的长度if(ret == -1){err_exit("readn");}else if(ret == 0){printf("peer close\n");break;
}int dataBytes = ntohl(readbuf.msgLen); //字节序的转换int readBytes = readn(conn,readbuf.data,dataBytes); //读取出后续的数据if(readBytes == 0){printf("peer close\n");break;}if(readBytes<0){err_exit("read");
}

⑤ 粘包解决方案三:按行读取
ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:​​​​​​​

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
与read函数相比,recv函数的区别在于两点:
  1. recv函数只能够用于套接口IO。

  2. recv函数含有flags参数,可以指定一些选项。

recv函数的flags参数常用的选项是:

  1. MSG_OOB 接收带外数据,即通过紧急指针发送的数据

  2. MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据

为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。

*
* 封装了recv函数返回值说明:-1 读取出错 
*/
ssize_t read_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;}
}

下面是按行读取的代码:

/*
*读取一行内容
* 返回值说明:== 0 :对端关闭== -1 : 读取错误其他:一行的字节数,包含\n
* 
**/
ssize_t readLine(int sockfd ,void * buf ,size_t maxline)
{int ret ;int nRead = 0;int left = maxline ;char * pbuf  = (char *) buf;int count  = 0;while(true){//从socket缓冲区中读取指定长度的内容,但并不删除ret = read_peek(sockfd,pbuf,left);// ret = recv(sockfd , pbuf , left , MSG_PEEK);if(ret<= 0)return ret;nRead = ret ;for(int i = 0 ;i< nRead ; ++i){if(pbuf[i]=='\n') //探测到有\n{ret = readn (sockfd , pbuf, i+1);if(ret != i+1)exit(EXIT_FAILURE);return ret + returnCount;}}//如果嗅探到没有\n//那么先将这一段没有\n的读取出来ret  = readn(sockfd , pbuf , nRead);if(ret != nRead)exit(EXIT_FAILURE);pbuf += nRead ;left -= nRead ;count += nRead;}return -1;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/29410.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机操作系统】操作系统的功能和目标

1、操作系统的功能和目标---作为系统资源的管理者 作为系统资源的管理者提供的功能&#xff1a; &#xff08;1&#xff09;处理机管理 &#xff08;2&#xff09;存储器管理 &#xff08;3&#xff09;文件管理 &#xff08;4&#xff09;设备管理 作为系统资源的管理者…

“深入浅出”系列之Linux篇:(10)基于C++实现分布式网络通信RPC框架

分布式网络通信rpc框架 项目是分布式网络通信rpc框架&#xff0c; 文中提到单机服务器的缺点&#xff1a; 硬件资源的限制影响并发&#xff1a;受限于硬件资源&#xff0c;聊天服务器承受的用户的并发有限 模块的编译部署难&#xff1a;任何模块小的修改&#xff0c;都导致整…

Aws batch task 无法拉取ECR 镜像unable to pull secrets or registry auth 问题排查

AWS batch task使用了自定义镜像&#xff0c;在提作业后出现错误 具体错误是ResourceInitializationError: unable to pull secrets or registry auth: The task cannot pull registry auth from Amazon ECR: There is a connection issue between the task and Amazon ECR. C…

机器学习之无监督学习

无监督学习&#xff08;Unsupervised Learning&#xff09;是机器学习的一个重要分支&#xff0c;其特点是在训练过程中不使用标签数据。与有监督学习不同&#xff0c;无监督学习的目标是从未标记的数据中发现隐藏的结构、模式或关系。无监督学习广泛应用于聚类、降维、异常检测…

自然语言处理:朴素贝叶斯

介绍 大家好&#xff0c;博主又来和大家分享自然语言处理领域的知识了。按照博主的分享规划&#xff0c;本次分享的核心主题本应是自然语言处理中的文本分类。然而&#xff0c;在对分享内容进行细致梳理时&#xff0c;我察觉到其中包含几个至关重要的知识点&#xff0c;即朴素…

HTML label 标签使用

点击 <label> 标签通常会使与之关联的表单控件获得焦点或被激活。 通过正确使用 <label> 标签&#xff0c;可以使表单更加友好和易于使用&#xff0c;同时提高整体的可访问性。 基本用法 <label> 标签通过 for 属性与 id 为 username 的 <input> 元素…

Ubuntu20.04双系统安装及软件安装(五):VSCode

Ubuntu20.04双系统安装及软件安装&#xff08;五&#xff09;&#xff1a;VSCode 打开VScode官网&#xff0c;点击中间左侧的deb文件下载&#xff1a; 系统会弹出下载框&#xff0c;确定即可。 在文件夹的**“下载”目录**&#xff0c;可看到下载的安装包&#xff0c;在该目录下…

SparkSQL全之RDD、DF、DS ,UDF、架构、资源划分、sql执行计划、调优......

1 SparkSQL概述 1.1 sparksql简介 Shark是专门针对于spark的构建大规模数据仓库系统的一个框架Shark与Hive兼容、同时也依赖于Spark版本Hivesql底层把sql解析成了mapreduce程序&#xff0c;Shark是把sql语句解析成了Spark任务随着性能优化的上限&#xff0c;以及集成SQL的一些…

Linux总结

1 用户与用户组管理 1.1 用户与用户组 //linux用户和用户组 Linux系统是一个多用户多任务的分时操作系统 使用系统资源的用户需要账号进入系统 账号是用户在系统上的标识&#xff0c;系统根据该标识分配不同的权限和资源 一个账号包含用户和用户组 //用户分类 超级管理员 UID…

【AI深度学习网络】卷积神经网络(CNN)入门指南:从生物启发的原理到现代架构演进

深度神经网络系列文章 【AI深度学习网络】卷积神经网络&#xff08;CNN&#xff09;入门指南&#xff1a;从生物启发的原理到现代架构演进【AI实践】基于TensorFlow/Keras的CNN&#xff08;卷积神经网络&#xff09;简单实现&#xff1a;手写数字识别的工程实践 引言 在当今…

Qt之QGraphicsView图像操作

QGraphicsView图像操作:旋转、放大、缩小、移动、图层切换 1 摘要 GraphicsView框架结构主要包含三个主要的类QGraphicsScene(场景)、QGraphicsView(视图)、QGraphicsItem(图元)。QGraphicsScene本身不可见,是一个存储图元的容器,必须通过与之相连的QGraphicsView视图来显…

【Azure 架构师学习笔记】- Azure Databricks (14) -- 搭建Medallion Architecture part 2

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (13) – 搭建Medallion Architecture part 1 前言 上文搭建了ADB 与外部的交互部分&#xff0c;本篇搭建ADB 内部配置来满足medallion 架构。…

AI视频领域的DeepSeek—阿里万相2.1图生视频

让我们一同深入探索万相 2.1 &#xff0c;本文不仅介绍其文生图和文生视频的使用秘籍&#xff0c;还将手把手教你如何利用它实现图生视频。 如下为生成的视频效果&#xff08;我录制的GIF动图&#xff09; 如下为输入的图片 目录 1.阿里巴巴全面开源旗下视频生成模型万相2.1模…

PostgreSQL 安装与使用

下载地址: EDB: Open-Source, Enterprise Postgres Database Management 安装图形化安装界面安装。安装完后将bin目录配置到系统环境变量 执行psql -h localhost -p 5432 -U postgres 密码在安装过程中设置的 ​ 0、修改密码 ALTER USER sonar WITH PASSWORD 123456; 1、新…

Go加spy++隐藏窗口

最近发现有些软件的窗口就像狗皮膏药一样&#xff0c;关也关不掉&#xff0c;一点就要登录&#xff0c;属实是有点不爽了。 窗口的进程不能杀死&#xff0c;但是窗口我不想要。思路很简单&#xff0c;用 spy 找到要隐藏的窗口的句柄&#xff0c;然后调用 Windows 的 ShowWindo…

[内网安全] Windows 域认证 — Kerberos 协议认证

&#x1f31f;想系统化学习内网渗透&#xff1f;看看这个&#xff1a;[内网安全] 内网渗透 - 学习手册-CSDN博客 0x01&#xff1a;Kerberos 协议简介 Kerberos 是一种网络认证协议&#xff0c;其设计目标是通过密钥系统为客户机 / 服务器应用程序提供强大的认证服务。该认证过…

服务器数据恢复—raid5阵列中硬盘掉线导致上层应用不可用的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某公司一台服务器&#xff0c;服务器上有一组由8块硬盘组建的raid5磁盘阵列。 磁盘阵列中2块硬盘的指示灯显示异常&#xff0c;其他硬盘指示灯显示正常。上层应用不可用。 服务器数据恢复过程&#xff1a; 1、将服务器中所有硬盘编号…

π0源码解析——一个模型控制7种机械臂:对开源VLA sota之π0源码的全面分析,含我司的部分落地实践

前言 ChatGPT出来后的两年多&#xff0c;也是我疯狂写博的两年多(年初deepseek更引爆了下)&#xff0c;比如从创业起步时的15年到后来22年之间 每年2-6篇的&#xff0c;干到了23年30篇、24年65篇、25年前两月18篇&#xff0c;成了我在大模型和具身的原始技术积累 如今一转眼…

Dify+DeepSeek | Excel数据一键可视化(创建步骤案例)(echarts助手.yml)(文档表格转图表、根据表格绘制图表、Excel绘制图表)

Dify部署参考&#xff1a;Dify Rag部署并集成在线Deepseek教程&#xff08;Windows、部署Rag、安装Ragan安装、安装Dify安装、安装ollama安装&#xff09; DifyDeepSeek - Excel数据一键可视化&#xff08;创建步骤案例&#xff09;-DSL工程文件&#xff08;可直接导入&#x…

由麻省理工学院计算机科学与人工智能实验室等机构创建低成本、高效率的物理驱动数据生成框架,助力接触丰富的机器人操作任务

2025-02-28&#xff0c;由麻省理工学院计算机科学与人工智能实验室&#xff08;CSAIL&#xff09;和机器人与人工智能研究所的研究团队创建了一种低成本的数据生成框架&#xff0c;通过结合物理模拟、人类演示和基于模型的规划&#xff0c;高效生成大规模、高质量的接触丰富型机…