首页 >excel操作 > 内容

web-socket

2023年7月8日 12:38

TCP/IP

TCP/IP协议https://blog.csdn.net/L_fengzifei/article/details/123482374

见脚本笔记

1. 概念

socket接口连接应用层和传输层,具体来说属于传输层的内容

网络数据传输过程

发送信息的应用程序,通过socket变成接口把信息传给操作系统的TCP/IP协议栈通信模块
通过TCP/IP协议栈通信模块一层层传递给其他通信模块,最后再通过网卡等硬件设备发送到网络上去
经过网络上路由器的一次次转发,最终到了目标程序所在的计算终端设备,再通过终端的操作系统的TCP/IP协议栈通信模块一层层的上传
最终接收信息的程序,通过socket编程接口接收到了传输的信息

requsets库底层也是使用socket编程接口发送http请求信息
http传输的消息,底层也是通过TCP/IP协议传输的

1.1 消息格式

消息:消息头+消息体
消息头:长度、类型、状态
消息体:数据

特别针对TCP协议传输,格式定义一定要明确规定消息边界
TCP传输的是字节流,如果没有指定边界或成都,接收方对数据的处理存在歧义(开始和结束)

TCP数据传输过程

发送和接收不一定是完整的消息
https://www.bilibili.com/video/av74106411/?p=82&spm_id_from=pageDriver

应用程序发送数据(字节流),数据存在本机的发送缓冲中,然后根据网络传输协议(四层TCP/IP协议),再发送给对方。socket.send()会返回实际上本次存储到发送缓冲中的字节长度(返回值是要发送的字节数量,该数量可能小于string的字节大小)
达到对方主机中,先将数据存储到接收缓冲中,socket.recv(bufsize)定义要接收的最大数量

解决方法:定义消息头或消息尾部
指定消息边界的方法

用消息内容中不可能出现的字节串作为消息的结尾字符
定义消息头,直接指定消息长度

2. socket - tcp

https://blog.csdn.net/qq_37193537/article/details/91043580
https://blog.csdn.net/weixin_40230682/article/details/80511150 UDP

socket(套接字)

应用程序通过套接字向网络发出请求或应答网络请求,使主机间火车一台计算机上的进程间可以通信

服务端一般先于客户端启动
服务端和客户端都可以收发消息

##### TCP#服务端socket.bind() # 绑定IP+端口号socket.listen() # 开启监听,最大等待数量socket.accept() # 阻塞式等待接收 返回一个socket# 客户端socket.connect() # 连接服务端端口号 IP+端口号# 服务端/客户端socket.close() # 关闭socketsocket.recv() # 接收数据,bufsize指定最大接收数量,TCP协议socket.send() # 发送数据,TCP协议##### UDPsocket.bind((IP,port)) # 本地socket.sendto(data,(IP,port)) # 发送数据,UDP协,同样返回发送的字节数,目标端口socket.recvfrom(buffersize) # 接受数据,UDP,返回接收到的数据和发送端的端口地址
# 创建对象# version1import socket sockect.socket()# version2from socket import socketsocket([family,[type[,proto]]])# family: 套接字家族:AF_UNIX 或 AF_INET(IP协议)# type: 套接字类型#面向连接:SOCK_STREAM --TCP#面向非连接:SOCK_DGRAM --UDP# protocol: 默认为0

2.1多线程响应???

python多线程

2.2 TCP/UDP

https://www.byhy.net/tut/py/etc/socket/

UDP是无连接协议

无需事先建立虚拟连接,可以直接给对方地址发消息
缺点:不安全,UDP协议本身没有重传机制;TCP协议底层有消息验证是否到达,如果丢失,发送会重传
数据消息发送是独立的报文:TCP协议通信双方的信息数据有明确的先后顺序(发送方应用先发送的信息肯定是先被接收方应用先接收的)。UDP协议发送的是一个个独立的报文,接收方应用接收到的次序不一定和发送的次序一致

系统设计时要确定应用语义中的最大报文长度,从而可以确定一个对应长度的应用程序接收缓冲,防止只接收一部分的数据

TCP socket字节流协议,如果应用接收缓冲不够大,只接收了一部分数据,后面可以继续接收,然后搜索找到边界拼接就可以
UDP socket数据报协议,如果只接收了数据报的一部分,剩余的消息就会被丢弃,下次接收只能接收
补充说明–没看???https://www.byhy.net/tut/py/etc/socket/

3. socket

  • 字节流 - TCP协议

面向连接
可靠、双全工

  • 数据在传输过程中不会消失(校验、重传)
  • 数据按顺序传输
  • 数据的发送和接收不同步

SOCK_STREAM 内部有一个缓冲区(字符数组),通过socket传输的数据将保存到这个缓冲区,接收端在接收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性的读取,也可能分好几次读取

校验与重连、顺序
(发送端为每个数据包分分配一个ID,接收端接收到数据以后,再给发送端返回一个数据包,告诉发送端接收到了该ID的数据包,且必须得到该确认信息后,发送端才会发送下一个数据包,如果数据包发出去了,一段时间以后没有得到接收端的回应,那么发送端会重新再发送一次,知道接收端响应)

socket 序号、确认号、数据偏移、控制标志、窗口、校验和、紧急指针、选项等

  • 数据报 - UDP协议

无连接,无校验
非顺序传输
数据可能都是或损毁
每次显示传输数据的大小
传输效率高
数据的接受和发送是同步的,即接收次数和发送次数应该是相同的

socket: 长度、校验和

3.1 windows - sockect

网络连接:文件句柄

/* client.c */#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>#pragma comment(lib,"WS2_32.Lib") // 加载ws2_32.dll// https://blog.csdn.net/MasterSaMa/article/details/90406827int main(){    // 初始化DLL    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 创建套接字    SOCKET sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);    // 向服务器发送请求    struct sockaddr_in sockAddr;    memset(&sockAddr,0,sizeof(sockAddr));     sockAddr.sin_family=PF_INET;    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234);        connect(sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));    // 接收服务器传回的数据    char szBuffer[MAXBYTE]={0}; // #define MAXBYTE 0xff;    recv(sock,szBuffer,MAXBYTE,0);    // 输出接收到的数据    printf("message from server: %s\n",szBuffer);    // 关闭套接字    closesocket(sock);    // 终止使用dll    WSACleanup();    system("pause");    return 0;}   
/* server.c */#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>// #pragma comment(lib,"ws2_32.lib") // 加载ws2_32.dll// #pragma comment(lib,"./WS2_32.Lib") // 加载ws2_32.dllint main(){    // 初始化dll    /*        WSAStartup 指明WinSock规范的版本        int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);        MAKEWORD(1,2) // 主版本号为1,副版本号为2,返回0x0201 也就是低字节为主版本号,高字节为副版本号        MAKEWORD(2,2) // 主版本号为2,副版本号为2        WSAStartup函数执行成功后,会将ws2_32.dll有关信息写入WSAData结构体变量中        typedef struct WSAData{}WSADATA,*LPWSADATA;    */    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 创建套接字    SOCKET servSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);    // 绑定套接字    struct sockaddr_in sockAddr;    // 每个字节都用0填充    memset(&sockAddr,0,sizeof(sockAddr));    sockAddr.sin_family=PF_INET; // 使用ipv4地址 等价于AF_INET    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234); // 设置端口    // 绑定套接字    bind(servSock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));    // 进入监听状态    listen(servSock,20);    // 接收客户端请求    SOCKADDR clientAddr;    int nSize=sizeof(SOCKADDR);    SOCKET clientSock=accept(servSock,(SOCKADDR*)&clientAddr,&nSize);    // 向客户端发送数据    char *str="hello world";    send(clientSock,str,strlen(str)+sizeof(char),0);    // 关闭套接字    closesocket(clientSock);    closesocket(servSock);    WSACleanup();    return 0;}

3.2 linux - socket

网络连接:文件描述符

在这里插入图片描述

/* client.c */#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>#include <netinet/in.h>int main(){    // 创建套接字    // int sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);    int sock=socket(AF_INET,SOCK_STREAM,0);    // 绑定IP和端口    struct sockaddr_in serv_addr;    memset(&serv_addr,0,sizeof(serv_addr)); // sizeof(sockaddr_in)    serv_addr.sin_family=AF_INET; // 使用ipv4    serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); // ip地址    serv_addr.sin_port=htons(1234); // 端口    // 向服务器发起请求    connect(sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr));    // 读取服务器传回的数据    char buffer[40];    read(sock,buffer,sizeof(buffer)-1); // 保证\0    printf("%s\n",buffer);    // 关闭套接字    close(sock);        return 0;}
/* server.c */#include <stdio.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/socket.h>#include <netinet/in.h>int main(){    // 创建套接字    // AF_INET 使用ipv4地址    // IPPROTO_TCP 使用tcp协议    int serv_sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);    // 绑定IP和端口    struct sockaddr_in serv_addr;    memset(&serv_addr,0,sizeof(serv_addr)); // sizeof(sockaddr_in)    serv_addr.sin_family=AF_INET; // 使用ipv4    serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); // ip地址    serv_addr.sin_port=htons(1234); // 端口// socket函数确定套接字的各种属性// bind函数让套接字与指定的ip和端口绑定起来    bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr));    // 进入监听状态,等待用户发起请求    /*        套接字处于被动监听状态: 也就是套接字一直处于睡眠状态,直到客户端发起请求才会被唤醒,    */    listen(serv_sock,20);    // 接受客户端请求    struct sockaddr_in client_addr;    socklen_t client_addr_size=sizeof(client_addr);    /*        正常情况下,程序运行到accept()函数会阻塞,直到客户端发起请求    */    int client_sock=accept(serv_sock,(struct sockaddr*)&client_addr,&client_addr_size);    // 想客户端发送数据    char str[]="http://baidu.com";    write(client_sock,str,sizeof(str));    // 关闭套接字    close(client_sock);    close(serv_sock);        return 0;}

3.3 补充

3.3.1 bind - sockaddr_in/sockaddr

sockaddr_in 专门用来保存ipv4地址的结构体
sockaddr 通用结构体,可以用来保存多种类型的ip地址和端口号

struct sockaddr_in {shortsin_family; // 地址类型u_shortsin_port; // 16位(2字节)端口号 (0-65536) 0-1023系统自动分配,1024-65536自定义struct in_addrsin_addr; // 32位(4字节)IP地址charsin_zero[8]; // 一般不使用 用0填充(8字节)};struct in_addr{in_addr_t  s_addr;  //32 位的 IP 地址 unsigned long 4个字节};// 将字符串转换为整数sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");/* 强制转换 */// 占用的内存长度相同,强制类型转换不会有字节丢失typedef struct sockaddr {u_shortsa_family; // 地址类型charsa_data[14]; // IP地址和端口号} SOCKADDR;sizeof(sockaddr_in): 2+2+4+8sizeof(sockaddr): 2+14

3.3.2 listen/accept

listen只是让套接字处于监听状态,并没有接收请求
accept 接收请求
listen后面的代码会继续执行,直到遇到acceptaccept会阻塞程序执行,直到有新的请求到来

listen

listen(SOCKET sock, int backlog);backlog 表示请求队列的最大长度

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把他放进缓冲区,待当前请求处理完毕后,再从缓冲区读取出来处理,如果不断有新的请求进来,就按照先后顺序在缓冲区中排队,直到缓冲区满。当请求队列满时,不再接收新的请求,在发送请求则客户端会受到错误
缓冲区:请求队列
缓冲区的长度:存放多少个客户端请求
blocklog: SOMAXCONN表示由系统决定请求队列长度

accept

返回新的套接字来和客户端通信

SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windowsaddr 保存了客户端的ip地址和端口号

3.3.3 write/read/recv/send

write

nbytes 表示要写入的字节数
写入成功返回写入的字节数,失败返回-1

read

nbytes 表示要读取的字节数
成功则返回读取到的字节数,遇到文件末尾返回0,失败返回-1

3.4 进阶-始终监听

/* client.c */#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>// #pragma comment(lib,"WS2_32.Lib") // 记载ws2_32.dll#define BUF_SIZE 100// https://blog.csdn.net/MasterSaMa/article/details/90406827int main(){    // 初始化DLL    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 想服务器发送请求    struct sockaddr_in sockAddr;    memset(&sockAddr,0,sizeof(sockAddr));     sockAddr.sin_family=PF_INET;    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234);           // 发送给服务端数据    char bufSend[BUF_SIZE]={0};    // 接收服务器传回的数据    char bufRecv[BUF_SIZE]={0}; // #define MAXBYTE 0xff;        while (1)    {        // 创建套接字        SOCKET sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);        // 连接服务端        connect(sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));        // 发送数据        printf("input data to send:\n");        gets(bufSend);        send(sock,bufSend,strlen(bufSend),0);        // 接收数据        recv(sock,bufRecv,BUF_SIZE,0);        // 输出接收到的数据        printf("message from server: %s,%d\n",bufRecv,strlen(bufRecv));        memset(bufSend,0,BUF_SIZE); // 重置发送缓冲区        memset(bufRecv,0,BUF_SIZE); // 重置接收缓冲区        // 关闭套接字        closesocket(sock);    }   // 终止使用dll    WSACleanup();    system("pause");    return 0;}   
/* server.c */#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>#include <string.h>#include <stdlib.h>#define BUF_SIZE 100// #pragma comment(lib,"ws2_32.lib") // 记载ws2_32.dll// #pragma comment(lib,"./WS2_32.Lib") // 记载ws2_32.dllint main(){    // 初始化dll    /*        WSAStartup 指明WinSock规范的版本        int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);        MAKEWORD(1,2) // 主版本号为1,副版本号为2,返回0x0201 也就是低字节为主版本号,高字节为副版本号        MAKEWORD(2,2) // 主版本号为2,副版本号为2        WSAStartup函数执行成功后,会将ws2_32.dll有关信息写入WSAData结构体变量中        typedef struct WSAData{}WSADATA,*LPWSADATA;    */    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 创建套接字    SOCKET servSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);    // 绑定套接字    struct sockaddr_in sockAddr;    // 每个字节都用0填充    memset(&sockAddr,0,sizeof(sockAddr));    sockAddr.sin_family=PF_INET; // 使用ipv4地址    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234); // 设置端口    // 绑定套接字    bind(servSock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));    // 进入监听状态    listen(servSock,20);    // 接收客户端请求    SOCKADDR clientAddr;    int nSize=sizeof(SOCKADDR);    // SOCKET clientSock=accept(servSock,(SOCKADDR*)&clientAddr,&nSize);    // 接收数据    char buffer[BUF_SIZE]={0}; // 缓冲区        // 始终监听    while (1)    {        // 接收客户端请求        SOCKET clientSock=accept(servSock,(SOCKADDR*)&clientAddr,&nSize);                // 接收数据        int strlength=recv(clientSock,buffer,BUF_SIZE,0);        printf("message from client %s,size %d\n",buffer,strlen(buffer));                // 向客户端发送数据        send(clientSock,buffer,BUF_SIZE,0);        // 关闭套接字        closesocket(clientSock);        memset(buffer,0,BUF_SIZE); // 关闭套接字重置缓冲区    }    closesocket(servSock);    WSACleanup();    return 0;}

3.5 socket缓冲区/阻塞/非阻塞

https://blog.csdn.net/summer_fish/article/details/121740570
https://zhuanlan.zhihu.com/p/405794790
https://blog.csdn.net/mayue_web/article/details/82873115 !!!

3.5.0 阻塞/非阻塞/同步/异步

阻塞/非阻塞:针对的是接收方(函数应对返回的方式)(阻塞:没有得到(内部处理线程)结果不返回;非阻塞:函数立即返回,循环查询)
同步/异步:针对的是发送方(函数调用的方式)(同步:没有结束就死等;异步;功能结果未知,结束后通知我)(通常用于请求方)

(自我理解:同步的表现形式是阻塞,异步的表现形式是非阻塞)

进一步理解:
同步/异步:表示的读写(访问)数据的方式
阻塞/非阻塞:线程/进程 在等待 读写(访问)数据的状态

并发/并行 和 同步/异步之间 并没有一个明确的关系

3.5.0.1 并发/并行/同步/异步

  • 并发

计算机能够同时执行多项任务;
并发的形式有许多不同:

单核处理器:时间分片的形式,一个任务执行一段时间,也就是任务交替进行。也被称为进程或者线程的上下文切换
多核处理器:在多个核心上,真正并行的执行任务,也就是以并行的形式实现并发

  • 并行

多核心并行执行任务
单核心没有并行
在这里插入图片描述

  • 同步

同步:必须等到前一个任务执行完毕之后,才能执行下一个任务
在同步中,没有并发和并行的概念

在这里插入图片描述

  • 异步

不同任务之间,并不会相互等待,先后执行(即在执行任务A的时候,也可以同时执行任务B)
也就多线程编程
多线程是异步并发的:如果是多个核心,则是并行执行;如果在当个核心上,就是通过分配时间片的方法,交替实现并发
在这里插入图片描述
补充
多线程编程:多核心并发,适用于计算密集型应用程序
单线程异步编程:强制单核心并发,适用于I/O操作密集型应用程序

3.5.1 缓冲区

每个socket被创建后,都被分配两个缓冲区:输入缓冲区输出缓冲区

输出缓冲区

wirte/send 并不会立即向网络中传输数据,而是现将数据写入缓冲区,再由TCP协议将数据从缓冲区发送到目标IP
一旦将输入写入缓冲区,函数就返回成功(注意阻塞和非阻塞模式),不管数据有没有到达目标IP,也不管何时被发送到网络,(数据是否被发送、是否到达都是TCP协议负责的
TCP协议独立于write/send函数,数据有可能刚被写入缓冲区就发送到网络,也有可能在缓冲区中挤压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,这些由系统控制

输入缓冲区

read/recv 从输入缓冲区中读取数据,而不是直接从网络中读取

在这里插入图片描述
缓冲区特点

缓冲区不共享,在每个套接字中单独存在
缓冲区在创建套接字时自动生成
即使关闭套接字,也会继续传输(输出)缓冲区中遗留的数据完全发送
关闭套接字,将丢失输入缓冲区中的数据不完全读取

3.5.2 阻塞模式

  • 输出缓冲区

TCP及其套接字,首先TCP会检查缓冲区,如果缓冲区的可用空间长度小于要发送(写入缓冲区)的数据,那么write/send就会被阻塞(暂停执行),直到缓冲区中的数据被TCP发送到目标IP,腾出足够的空间,才会唤醒write/send函数继续写入数据
如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write/send会被阻塞,直到数据发送完毕,缓冲区被解锁,wirte/send才会被唤醒 ???
如果要写入的数据大于缓冲区的最大长度,将分批写入
直到所有数据被写入缓冲区,write/send才能返回

补充(阻塞模式下)

  • 如果缓冲区的可用大小 比 要写入的数据大小 要大,则write/send立即返回,
  • 如果缓冲区没有足够的缓冲区容纳数据,(和上面说的一样,阻塞等待确认(不是ACK确认)再返回(接收端只要将数据收到接收缓冲区中就会确认,并不一定等待应用程序调用read/recv)),(相当于就是程序在那干等、死等,直到释放新的缓冲区空间,然后继续把未写入的拷贝到缓冲区中,然后write/send返回)

返回值<0表示出错,=0连接关闭,>0为发送的字节大小

  • 输入缓冲区 read/recv

首先会检查缓冲区,如果缓冲区中有数据,就读取,否则函数被阻塞,直到网络上有数据到来
如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读取,剩余数据将不断积压,直到read/recv函数被调用,然后再次读取
当缓冲区中的数据长度小于期望读取的数据量时,返回实际读取的字节数
当缓冲区中的数据长度大于期望读取的数据量时,读取期望读取的字节数,返回实际读取的长度
直到读取到数据后read/recv函数才返回,否则一致被阻塞

补充(阻塞模式)
-(和上面一个意思)如果缓冲区为空,则程序会在那干等、死等,直到输入缓冲区有数据,就把数据从缓冲区中拷贝出来,然后返回
返回值<0表示出错,=0连接关闭,>0为接收到的字节大小

  • connect

阻塞模式下,connect进行三次握手,建立成功后(也就是先发送SYN包,然后接收到服务端的ACK包后)connect返回,否则一直阻塞

  • accept

阻塞模式下调用accept()函数,没有新连接时,进程会进入睡眠状态,直到有可用的连接才返回

3.5.3 非阻塞模式

  • 输出缓冲区 write/send

非阻塞模式下,write/send函数的过程仅仅是将数据拷贝到内核协议栈的缓冲区中

  • 如果缓冲区可用空间不够,则尽能力的拷贝,返回实际成功拷贝的大小
  • 如果缓冲区可用空间为0,则立刻返回-1,同时设置EAGAIN,(相当于try again 等会再试),如果错误号是别的,则表明发送失败

补充
非阻塞模式下,<0且满足一定条件时,认为连接时正常的,因此需要循环发送数据

// 例子ssize_t writen(int connfd, const void *pbuf, size_t nums){int32 nleft = 0;int32 nwritten = 0;char *pwrite_buf = NULL;if ((connfd <= 0) || (NULL == pbuf) || (nums < 0)){return -1;}pwrite_buf = (char *)pbuf;nleft = nums;while(nleft>0){if (-1 == (nwritten = send(connfd, pwrite_buf, nleft, MSG_NOSIGNAL))){if (EINTR == errno || EWOULDBLOCK == errno || EAGAIN == errno){nwritten = 0;}else{errorf("%s,%d, Send() -1, 0x%x\n", __FILE__, __LINE__, errno);return -1;}}nleft -= nwritten;pwrite_buf += nwritten;}return(nums);}
  • 输入缓冲区

非阻塞模式下,如果输入缓冲区中为空,没有可以读取的数据,程序就会立刻返回一个EAGIN
如果缓冲区中有数据,则与阻塞模式一样,返回实际读取的长度

补充
返回值<0表示出错,=0连接关闭,>0为接收到的字节大小
非阻塞模式下,<0且满足一定条件时,认为连接时正常的,因此需要循环读取数据

// 读取指定个字节大小的例子ssize_t readn(int fd, void *vptr, size_t n){int32 nleft = 0;int32 nread = 0;int8 *pread_buf = NULL;pread_buf = (int8 *)vptr;nleft = n;while (nleft > 0){nread = recv(fd,  (char *)pread_buf, nleft, 0);if (nread < 0){if (EINTR == errno || EWOULDBLOCK == errno || EAGAIN == errno){nread = 0;}else{return -1;}}else if (nread == 0){break;}else{nleft -= nread;pread_buf += nread;}}return (ssize_t)(n - nleft);}
  • connect

非阻塞模式下,connect启动三次握手,但是会立即返回(函数不等待连接建立好才返回),返回的错误码位EINPROGRESS(表示正在进行某种过程)

  • accept

非阻塞模式下调用accept()函数,函数立即返回,有连接时返回客户端的套接字描述符或句柄,没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞

3.5.4 总结???

阻塞:connet/accept/write导致线程阻塞,(多线程中,不代表不能执行其他线程)
阻塞:recv读取数据长度不确定

???

阻塞模式,线程处于sleep休眠状态,此时不占用CPU,CPU就可以调度别的线程或进程(调用者需要返回查询做不用功(如果所有设备都一致没有数据到达))
非阻塞模式,虽然立即返回,但是调用者需要反复查询做不用功(while循环)(如果所有设备都一致没有数据到达),也就是不能执行其他线程!!!
通过select函数等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题),也就是阻塞的同时监视多个设备,还可以设定阻塞等待的超时时间timeout

使用阻塞socket,通过select函数等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题)
读写接口:套用封装好的readn/writen函数。(指定时间读不到数据/读不到指定数据算作异常)

3.5.5 shutdown ???

https://blog.csdn.net/renwotao2009/article/details/51484872
https://blog.csdn.net/u011391629/article/details/71939248

int shutdown(int sock,int howto); // linuxint shutdown(SOCKET s,int howto); // windowshowto:// linuxSHUT_RD : 断开输入流,套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数SHUT_WR: 断开输出流,套接字无法发送数据,但是如果输出缓冲区中还有未传输的数据,则将传递到目标主机SHUT_RDWR: 同时断开I/O流,相当于分两次调用shutdown()// windowsSD_RECEIVE: 关闭接收操作,断开输入流SD_SEND: 关闭发送操作,断开输出流SD_BOTH: 同时关闭接收和发送操作

close/shutdown

close/closesocket 关闭套接字,将套接字描述符/句柄从内存清楚,之后不能在使用该套接字,TCP会自动触发关闭连接的操作
shutdown 关闭连接,并不关闭套接字,套接字依然存在,直到调用close/closesocket将套接字从内存清楚

调用close/closesocket关闭套接字时,或调用shutdown关闭输出流时,都会想对方发送FIN包,FIN标志位表示数据传输完毕
默认情况下,close/closesocket会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据;shutdown会等输出缓冲区的数据传输完毕再发送FIN包,???调用close/closesocket将会丢失输出缓冲区的数据,调用shutdown不会丢失???

3.6 例子思考

/* server.c 部分代码 */#define BUF_SIZE 5// 接收数据char buffer[BUF_SIZE]={0}; // 缓冲区// 接收客户端请求SOCKET clientSock=accept(servSock,(SOCKADDR*)&clientAddr,&nSize);/* 下面两行是伪造的数据 */ buffer[5]=0x61;buffer[6]=0x62;// 接收数据int strlength=recv(clientSock,buffer,BUF_SIZE,0);printf("message from client %s,buffersize %d,strlength %d\n",buffer,strlen(buffer),strlength);// 向客户端发送数据// send(clientSock,buffer,BUF_SIZE,0);
/* client.c 部分代码 */// 创建套接字SOCKET sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);// 连接服务端connect(sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));// 发送数据printf("input data to send:\n");gets(bufSend);send(sock,bufSend,strlen(bufSend),0);
/* debug */客户端输入:hello world服务端输出结果: message from client helloab,buffersize 7,strlength 5客户端输入:hel服务端输出结果: message from client hel,buffersize 3,strlength 3可见:1. %d 必须遇到\0才结束2. recv只能接收有限的数据量

3.6 数据粘包

read/recv函数对接收数据没有区分性,可能将write/send发送的多个独立的数据包当做一个数据包接收(数据的无边界性)
read/recv函数不知道数据包的开始和结束标志,只是把他们当做是连续的数据流来处理

**例子1 **

/*server.c*/#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>#include <string.h>#include <stdlib.h>#define BUF_SIZE 5// #pragma comment(lib,"ws2_32.lib") // 记载ws2_32.dll// #pragma comment(lib,"./WS2_32.Lib") // 记载ws2_32.dllint main(){    WSADATA wsaData;    WSAStartup(MAKEWORD(2, 2), &wsaData);    // 创建套接字    SOCKET servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 绑定套接字    struct sockaddr_in sockAddr;    // 每个字节都用0填充    memset(&sockAddr, 0, sizeof(sockAddr));    sockAddr.sin_family = PF_INET; // 使用ipv4地址    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");    sockAddr.sin_port = htons(1234); // 设置端口    // 绑定套接字    bind(servSock, (SOCKADDR *)&sockAddr, sizeof(SOCKADDR));    // 进入监听状态    listen(servSock, 20);    // 接收客户端请求    SOCKADDR clientAddr;    int nSize = sizeof(SOCKADDR);    // SOCKET clientSock=accept(servSock,(SOCKADDR*)&clientAddr,&nSize);    // 接收数据    char buffer[BUF_SIZE] = {0}; // 缓冲区    // 接收客户端请求    SOCKET clientSock = accept(servSock, (SOCKADDR *)&clientAddr, &nSize);    // 始终监听    while (1)    {        // 接收数据        int strlength = recv(clientSock, buffer, BUF_SIZE, 0);        printf("message from client %s,buffersize %d,strlength%d\n", buffer, strlen(buffer), strlength);    }    // 关闭套接字    closesocket(clientSock);    memset(buffer, 0, BUF_SIZE); // 关闭套接字重置缓冲区    closesocket(servSock);    WSACleanup();    return 0;} 
/* client.c */#include <stdio.h>#include <stdlib.h>// #include <winsock.h>#include <winsock2.h>// #pragma comment(lib,"WS2_32.Lib") // 记载ws2_32.dll#define BUF_SIZE 100// https://blog.csdn.net/MasterSaMa/article/details/90406827int main(){    // 初始化DLL    WSADATA wsaData;    WSAStartup(MAKEWORD(2, 2), &wsaData);    // 想服务器发送请求    struct sockaddr_in sockAddr;    memset(&sockAddr, 0, sizeof(sockAddr));    sockAddr.sin_family = PF_INET;    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");    sockAddr.sin_port = htons(1234);    // 发送给服务端数据    char bufSend[BUF_SIZE] = {0};    // 接收服务器传回的数据    char bufRecv[BUF_SIZE] = {0}; // #define MAXBYTE 0xff;    // 创建套接字    SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 连接服务端    connect(sock, (SOCKADDR *)&sockAddr, sizeof(SOCKADDR));    while (1)    {        // 发送数据        printf("input data to send:\n");        gets(bufSend);        send(sock, bufSend, strlen(bufSend), 0);        memset(bufSend, 0, BUF_SIZE); // 重置发送缓冲区    }    // 关闭套接字    closesocket(sock);    // 终止使用dll    WSACleanup();    system("pause");    return 0;}
/* 输入输出结果显示 */// 第一次> 客户端:he> 服务端:message from client he,buffersize 2,strlength2// 第二次> 客户端:hello > 服务端:message from client hello,buffersize 6,strlength5 // 6 是因为内存中第七个才是\0// 第三次/*接收缓冲区的数据始终存在首先读取5个 >hello然后再读取5个覆盖buffer 其中缓冲区中第一个是空格(0x20)也被读取到最后还差一个d,覆盖了buffer[0]位置,其余位置还是上一次的worl 所以输出是dworl,但是recv返回的读取到的字节数就是1*/> 客户端:hello world> 服务端:> message from client hello,buffersize 6,strlength5 // 6 是因为内存中第七个才是\0message from client  worl,buffersize 6,strlength5 // 注意是 ‘空格worl’message from client dworl,buffersize 6,strlength1 // 实际缓冲区中的剩余为读取的字符就有1个// 第四次/* 与第三次的原理相似 */> 客户端:helloworldqtcmd> 服务端:> message from client hello,buffersize 6,strlength5message from client world,buffersize 6,strlength5message from client qtcmd,buffersize 6,strlength5

3.6.1 数据粘包处理

粘包分析

MSS:应用层传给传输层(tcp)的数据包长度,(注意:应用层将消息传给传输层时会被切分为一个个数据包)
TCP提交给IP层最大分段大小,不包含TCP header和tcp option 只包括tcp payload,MSS是tcp用来限制应用层最大的发送字节

MTU:网络接口层(数据链路层)能够接收数据的最大长度
MTU为最大传输单元,由网络接口层(数据链路层)提供给网络层最大一次传输数据的大小(这里就包括了ip header)

对于MTU:如果ip层传给网络接口层的数据大于1500,就需要分片完成发送,分片后的ipheader ID相同
对于mss,mss=1500-ipheader-tcpheader,如果应用层要发送的数据量大于Mss,就需要切片
在这里插入图片描述

应用层传到tcp协议的数据,不是以消息报为单位想目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各种数据包,接收端收到这些数据包后没有正确换源原来的消息,因此出现粘包问题

发送机制 - nagle算法 (现代网络机制 nagle不开启):

  1. 如果数据包长度达到MSS或者有FIN包,立即发送,否则等到下一个包到来,如果下一个包到来后,两个包的总长度超过MSS,就会进行拆分发送
  2. 等到超时,第一个包没到MSS长度,但是又迟迟等不到第二个包到来,就立即发送
    值得注意的是:即使关闭了nagle算法,还是会出现粘包问题

粘包处理

数据粘包本质上是不确定消息边界,因此只要在发送端发送消息的时候给消息 带上识别消息边界的信息,接收端就可以根据这些信息识别出消息的边界,从而区分每个消息

  • 特殊标志作为消息头尾边界

如0xffffe
问题:可能实际的数据中也会出现该标志位

  • 加入消息长度信息

在收到头标志时,里面还可以带上消息长度,表明在这之后多少个字节属于这个消息,如果长度不够则等待一会,接收完全

  • 校验字段

针对标志位的问题,发送端在发送时还会加入各种检验字段(校验和 或者对整段完整数据进行CRC之后获得的数据)放在头标志位后面
即,在接收端拿到整段数据后,检验下确保它就是发送端发来的完整数据

在这里插入图片描述

3.6.2 数据粘包处理例子

‘web - socket 数据粘包处理’

3.7 文件传输

服务端,文件读到末尾,fread返回0,结束循环
服务端的rev并没有收到客户端的数据,而是当客户端调用close/closesocket后,服务端会收到FIN包,recv就会返回

客户端:
文件传输完毕后,让recv() 返回0,结束while循环
但是读取完缓冲区中的数据recv并不会返回0,而是阻塞,直到缓冲区再次有数据
客户端何时结束循环:
recv() 返回0的唯一时机就是收到FIN包时
FIN包表示数据传输完毕,计算机收到FIN包后,就知道对方不会再向自己传输数据,当调用read()/recv函数时,如果缓冲区中没有数据,就返回0,(间接表示读到了socket文件的末尾)
(这里先调用shutdown手动发送FIN包,如果服务端直接调用close/closescoket会使输出缓冲区中的数据失效,文件内容可能没有传输完毕连接就断开了,而调用shutdown会等待输出缓冲区中的数据传输完毕)

/* == server.c == */#include <stdio.h>#include <stdlib.h>#include <winsock2.h>#define BUF_SIZE 1024int main(){    // 检查文件是否存在    char *filename="./test.mp4";    // 以二进制方式打开文件    FILE *fp=fopen(filename,"rb");    if (fp==NULL)    {        printf("ERROR:connot open file");        system("pause");        exit(-1);    }    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    SOCKET serverSock=socket(AF_INET,SOCK_STREAM,0);    struct sockaddr_in sockAddr;    memset(&sockAddr,0,sizeof(sockAddr));    sockAddr.sin_family=AF_INET;    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234);    bind(serverSock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));    listen(serverSock,20);    SOCKADDR clientAddr;    int nSize=sizeof(SOCKADDR);    SOCKET clientSock=accept(serverSock,(SOCKADDR*)&clientAddr,&nSize);    // 循环发送数据,直到文件结尾    char buffer[BUF_SIZE]={0}; // 缓冲区    int nCount;    while((nCount=fread(buffer,1,BUF_SIZE,fp))>0)    {        send(clientSock,buffer,nCount,0);    }    // 文件读取完毕,断开输出流,向客户端发送FIN包    shutdown(clientSock,SD_SEND);    // 阻塞,等待客户端接收完毕    recv(clientSock,buffer,BUF_SIZE,0);    fclose(fp);        closesocket(clientSock);    closesocket(serverSock);    WSACleanup();    return 0;}
/* == client.c == */#include <stdio.h>#include <stdlib.h>#include <winsock2.h>#define BUF_SIZE 1024int main(){    // 创建文件    char *filename="test_copy.mp4";    FILE *fp=fopen(filename,"wb");    if (fp==NULL)    {        printf("ERROR:cannot open file");        system("system");        exit(-1);    }    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    SOCKET sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);    struct sockaddr_in sockAddr;    memset(&sockAddr,0,sizeof(sockAddr));    sockAddr.sin_family=AF_INET;    sockAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    sockAddr.sin_port=htons(1234);        connect(sock,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR));    // 循环接收数据,直到文件传输完毕    char buffer[BUF_SIZE]={0}; // 文件缓冲区        int nCount;    while ((nCount=recv(sock,buffer,BUF_SIZE,0))>0)    {        fwrite(buffer,nCount,1,fp);    }    printf("file copy done\n");    // 文件接收完毕后直接关闭套接字,无需调用shutdown()    fclose(fp);    closesocket(sock);    WSACleanup();    return 0;}

4. socket属性

4.1 网络字节序

CPU:通常是小端序存储
socket网络:通常是大端序传输,即在发送数据前,要将数据转换为大端序的网络字节序,接收端先转换为自己的格式再进行解析

主机字节序/网络字节序转换

sockaddr_in成员赋值时,需要显示的将主机字节序转换为网络字节序
(我的理解)write/send函数会自动转换为网络字节序,不用手动转换
(我的理解)read/recv函数会自动转换为主机字节序,不用手动转换
(字节序的转换只设计IP网络层(ip地址和端口号,即IP网路层的TCP头部信息),而具体要发送的信息,并不被网络层所读取,这是为了传输,所以只要保证发送方和接收方使用的字节序相同,就不需要进行转换)
https://blog.csdn.net/weixin_33905037/article/details/117089771
https://blog.csdn.net/m0_67390788/article/details/124465173

h:host主机字节序n:network网络字节序s:short类型 2字节 用于端口号转换l:long类型 4字节 用于IP地址转换htons // h -> n 将short类型数据从主机字节序转换为网络字节序ntohs // n -> h 将short类型数据从网络字节序转换为主机字节序htonl // h -> n 将long类型数据从主机字节序转换为网络字节序ntohl // n -> h 将long类型数据从网络字节序转换为主机字节序// sockaddr_in 中IP地址是32位整数// inet_addr 将字符串表示的ip地址转换为32位整数,同时还进行网络字节序的转换// 还函数还能检查无效的IP地址
#include <stdio.h>#include <stdlib.h>#include <winsock2.h>int main(){    unsigned short host_port = 0x1234,net_port;    unsigned long host_addr = 0x12345678,net_addr;    net_port=htons(host_port);    net_addr=htonl(host_addr);        printf("主机端口号:%#x\n",host_port); // 主机端口号:0x1234    printf("主机端口号:%#x\n",net_port); // 主机端口号:0x3412    printf("主机IP: %#x\n",host_addr); // 主机IP:0x12345678    printf("主机IP: %#x\n",net_addr); // 主机IP:0x78563412    char *addr1="192.168.0.312";    char *addr2="127.0.0.1";    unsigned long ip1=inet_addr(addr1);    if (ip1==INADDR_NONE)    {        printf("ERROR conversion wrong\n"); // ERROR conversion wrong 无效地址    }    else    {        printf("ip1: %#lx\n",ip1);    }    unsigned long ip2=inet_addr(addr2);    if (ip2==INADDR_NONE)    {        printf("ERROR conversion wrong\n");    }    else    {        printf("ip2: %#lx\n",ip2); // ip2: 0x100007f    }    return 0;}

4.2 域名相关

域名/IP

可以通过多个域名访问同一主机
同一ip地址可以绑定多个域名
同一域名可以有多个IP地址

host ->- 域名1- ip1- ip2- 域名2- ip3- ip4

域名解析

域名-> ip地址

struct hostent *gethostbyname(const char *hostname);struct hostent{char *h_name;  //official namechar **h_aliases;  //alias listint  h_addrtype;  //host address typeint  h_length;  //address lenghtchar **h_addr_list;  //address list}
#include <stdio.h>#include <stdlib.h>#include <winsock2.h>int main(){    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    struct hostent *host=gethostbyname("www.baidu.com");    if (!host)    {        printf("ERROR: get ip address invalied");        return 0;    }    // 域名    for (int i=0;host->h_aliases[i];i++)    {        printf("域名:%d, %s\n",i+1,host->h_aliases[i]);    }    // 地址类型    if (host->h_addrtype==AF_INET)    {        printf("AF_INET\n");    }     else    {        printf("AF_INET6\n");    }    // ip地址    for (int i=0;host->h_addr_list[i];i++)    {        printf("ip: %d, %s\n",i+1,inet_ntoa(*(struct in_addr*)(host->h_addr_list[i])));    }    return 0;}/*    域名:1, www.baidu.com    AF_INET    ip: 1, 112.80.248.76    ip: 2, 112.80.248.75*/

5. socket - udp

udp套接字不会保持连接状态,每次传输数据都要添加目标地址信息

在这里插入图片描述

// 发送ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);  //Linuxint sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen);  //Windowsbuf // 保存待传输数据的缓冲区地址nbytes // 带传输数据的长度,单位:字节to // 包含目标地址信息的sockaddr结构体变量的地址addrlen // 传递给参数to的地址值 结构体变量的长度// 接收ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);  //Linuxint recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen);  //Windowsbuf // 保存接收数据的缓冲区地址nbytes // 可接收的最大字节数 不能超过buf缓冲区大小from // 包含发送端地址信息的sockaddr结构体变量的地址addrlen // 保存参数from的结构体变量长度的变量地址值

例子

/* == server.c == */#include <stdio.h>#include <winsock2.h>#define BUF_SIZE 100int main(){    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 创建套接字    SOCKET sock=socket(AF_INET,SOCK_DGRAM,0);    // 绑定套接字    struct sockaddr_in servAddr;    memset(&servAddr,0,sizeof(servAddr));    servAddr.sin_family=AF_INET;    servAddr.sin_addr.s_addr=htonl(INADDR_ANY); // 自动获取ip地址    servAddr.sin_port=htons(1234);    bind(sock,(SOCKADDR*)&servAddr,sizeof(SOCKADDR));    // 接收客户端请求    // 客户端地址信息    SOCKADDR clientAddr;    int nSize=sizeof(SOCKADDR);    // 接收缓冲区    char buffer[BUF_SIZE]={0};    // 发什么返回什么    while (1)    {        int strLen=recvfrom(sock,buffer,BUF_SIZE,0,&clientAddr,&nSize);        sendto(sock,buffer,strLen,0,&clientAddr,nSize);    }    closesocket(sock);    WSACleanup();    return 0;}
/* == client.c == */#include <stdio.h>#include <winsock2.h>#define BUF_SIZE 100int main(){    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    // 创建套接字    SOCKET sock=socket(AF_INET,SOCK_DGRAM,0);    // 服务器地址    struct sockaddr_in servAddr;    memset(&servAddr,0,sizeof(servAddr));    servAddr.sin_family=AF_INET;    servAddr.sin_addr.s_addr=inet_addr("127.0.0.1");    servAddr.sin_port=htons(1234);    // 不断获取用户输入并发送给服务器,然后接收服务器数据    struct sockaddr fromAddr;    int addrLen=sizeof(fromAddr);    while(1)    {        char buffer[BUF_SIZE]={0};        printf("input a string:\n");        gets(buffer);        sendto(sock,buffer,strlen(buffer),0,(SOCKADDR*)&servAddr,sizeof(servAddr));        int strlen=recvfrom(sock,buffer,BUF_SIZE,0,&fromAddr,&addrLen);        buffer[strlen]=0; // 手动加\0        printf("message form server: %s\n",buffer);    }    return 0;}

5.1 数据粘包

基于数据包是指无论应用层交给UDP多长的报文,UDP传输层都照样发送,即一次发送一个报文,如果数据包太长,需要分片,也是IP层的事情,大不了效率低一些
UDP对应用层传递下来的报文,即不合并也不拆分,而是保留这些报文的边界
而接收方在接收数据爆时,也不会像面对TCP无穷无尽的二进制流那样不清楚什么时候能结束

UDP不存在数据粘包的问题

(自我理解):虽然UDP本身没有数据粘包的问题,但是如果手动发送的数据就不是一个根据协议定制好的数据报,那么还是需要进行手动的处理粘包问题

TCP/UDP

正是因为基于数据报和基于字节流的差异,TCP发送端发送10次字节流数据,而这时候接收端可以分100次去取数据,每次取数据的长度可以根据处理能力做调整
UDP发送端发送了10次数据报,那么接收端就要在10次收完,且发了多少,就取多少,确保每次都是一个完整的数据报

IP报头

16位总长度,表明IP报头里记录了整个IP包的总长度

在这里插入图片描述
UDP报头

根据16位的数据报文长度,可以作为数据边界,接收端的应用层能够清晰地将不同的数据报文区分开,从报头开始取n位,就是一个完整的数据报,从而避免粘包和拆包的问题
UDP data长度=IP总长度-IPheader长度-UDPheader长度

在这里插入图片描述
在这里插入图片描述

TCPB报头

TCPheader中没有长度信息
TCP data长度=IP总长度-IPheader长度-TCPheader长度
但是,注意:由于TCP发送端在发送的时候就不保证发的是一个完整的数据报,仅仅看成一连串无结构的字节流,这串字节流在接收端收到时哪怕知道长度也没有,因为它很可能只是某个完整消息的一部分

在这里插入图片描述

IP层分包

IP层不会造成数据粘包
如果消息数据过长,IP层会按MTU长度把消息分成N个切片,每个切片自带有自身在包里的位置offset和同样的IP头信息
各个切片在网络中进行传输,每个数据包切片可以在不同的路由中流转,然后在最后的中点汇合后再组装
在接收端接收到第一个切包时会申请一块新内存,创建IP包的数据结构,等待其他切分包数据到位,等消息全部到位后就把真个消息包传递给上层(传输层)进行处理

在这里插入图片描述

6. 数据粘包与接受发送问题

从Qt框架探索

udp readAll() 和 readDatagram()

readAll()用于读取当前可用的所有数据,但是在UDP协议传输数据时,数据可能分散到多个数据包中,一个数据包的大小也可能比较大,因此使用readAll()函数不能保证能够完整的读取一条完整的消息
readDatagram()函数读取数据报文,这个函数可以指定缓存区大小,保证每次只读取一个数据报文

在UDP通信中,发送发将数据按照MTU分割成若干个数据报,每个数据报都有一个标识,接收方将这些数据报按照标识符进行重组,重组后的数据就是原始的数据
udp的readAll()函数是将接收缓冲区中的所有数据读取出来,而readDatagram函数是读取一个完整的数据报,因此,如果一个数据报被分割成了多个数据包发送,readAll函数可能无法读取完成的数据报,而readDatagram可以

当数据包是一个分片数据包,那么qt框架会将数据包缓存起来,并等待接收其他分片数据包来进行组合,当所有的分片数据包都被接收时,qt会将这些数据包组合成一个完整的数据报,并将其返回给用户
需要注意的是,udp协议并不保证数据的顺序性,因此在组合分片数据包时,需要根据数据头中的标识符将数据报进行排序,以保证数据的正确性,这个过程有qt框架自动完成,不需要手动进行处理

writeDatagram

writeDatagram会自动将数据报分割成多个数据包进行发送,当发送数据报大小超过MTU时,UDP协议会将数据报分割成多个数据包进行传输,以保证数据的可靠性和完整性,这个过程被称为分片
qt中可以通过设置UDP的最大数据报大小MTU来控制分片的大小,默认值是512个字节,如果需要发送的数据报的大小超过MTU,则需要将其分割成多个数据包进行发送,这个过程有qt框架自动完成,不需要手动分片

待看

https://baijiahao.baidu.com/s?id=1748893920220092816&wfr=spider&for=pc
https://blog.csdn.net/u010429831/article/details/119932832


参考文章:https://blog.csdn.net/L_fengzifei/article/details/124192461

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时候联系我们修改或删除,在此表示感谢。

特别提醒:

1、请用户自行保存原始数据,为确保安全网站使用完即被永久销毁,如何人将无法再次获取。

2、如果上次文件较大或者涉及到复杂运算的数据,可能需要一定的时间,请耐心等待一会。

3、请按照用户协议文明上网,如果发现用户存在恶意行为,包括但不限于发布不合适言论妄图

     获取用户隐私信息等行为,网站将根据掌握的情况对用户进行限制部分行为、永久封号等处罚。

4、如果文件下载失败可能是弹出窗口被浏览器拦截,点击允许弹出即可,一般在网址栏位置设置

5、欢迎将网站推荐给其他人,网站持续更新更多功能敬请期待,收藏网站高效办公不迷路。

      



登录后回复

共有0条评论