TCP/IP协议:https://blog.csdn.net/L_fengzifei/article/details/123482374
socket接口连接应用层和传输层,具体来说属于传输层的内容
网络数据传输过程
发送信息的应用程序,通过socket变成接口把信息传给操作系统的TCP/IP协议栈通信模块
通过TCP/IP协议栈通信模块一层层传递给其他通信模块,最后再通过网卡等硬件设备发送到网络上去
经过网络上路由器的一次次转发,最终到了目标程序所在的计算终端设备,再通过终端的操作系统的TCP/IP协议栈通信模块一层层的上传
最终接收信息的程序,通过socket编程接口接收到了传输的信息
requsets库底层也是使用socket编程接口发送http请求信息
http传输的消息,底层也是通过TCP/IP协议传输的
消息:消息头+消息体
消息头:长度、类型、状态
消息体:数据
特别针对TCP协议传输,格式定义一定要明确规定消息边界
TCP传输的是字节流,如果没有指定边界或成都,接收方对数据的处理存在歧义(开始和结束)
TCP数据传输过程
发送和接收不一定是完整的消息
https://www.bilibili.com/video/av74106411/?p=82&spm_id_from=pageDriver
应用程序发送数据(字节流),数据存在本机的发送缓冲中,然后根据网络传输协议(四层TCP/IP协议),再发送给对方。
socket.send()
会返回实际上本次存储到发送缓冲中的字节长度(返回值是要发送的字节数量,该数量可能小于string的字节大小)
达到对方主机中,先将数据存储到接收缓冲中,socket.recv(bufsize)
定义要接收的最大数量
解决方法:定义消息头或消息尾部
指定消息边界的方法用消息内容中不可能出现的字节串作为消息的结尾字符
定义消息头,直接指定消息长度
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() # 关闭socket
socket.recv() # 接收数据,bufsize指定最大接收数量,TCP协议
socket.send() # 发送数据,TCP协议
##### UDP
socket.bind((IP,port)) # 本地
socket.sendto(data,(IP,port)) # 发送数据,UDP协,同样返回发送的字节数,目标端口
socket.recvfrom(buffersize) # 接受数据,UDP,返回接收到的数据和发送端的端口地址
# 创建对象
# version1
import socket
sockect.socket()
# version2
from socket import socket
socket([family,[type[,proto]]])
# family: 套接字家族:AF_UNIX 或 AF_INET(IP协议)
# type: 套接字类型
# 面向连接:SOCK_STREAM --TCP
# 面向非连接:SOCK_DGRAM --UDP
# protocol: 默认为0
python多线程
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/
面向连接
可靠、双全工
- 数据在传输过程中不会消失(校验、重传)
- 数据按顺序传输
- 数据的发送和接收不同步
SOCK_STREAM 内部有一个缓冲区(字符数组),通过socket传输的数据将保存到这个缓冲区,接收端在接收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性的读取,也可能分好几次读取
校验与重连、顺序
(发送端为每个数据包分分配一个ID,接收端接收到数据以后,再给发送端返回一个数据包,告诉发送端接收到了该ID的数据包,且必须得到该确认信息后,发送端才会发送下一个数据包,如果数据包发出去了,一段时间以后没有得到接收端的回应,那么发送端会重新再发送一次,知道接收端响应)
socket 序号、确认号、数据偏移、控制标志、窗口、校验和、紧急指针、选项等
无连接,无校验
非顺序传输
数据可能都是或损毁
每次显示传输数据的大小
传输效率高
数据的接受和发送是同步的,即接收次数和发送次数应该是相同的
socket: 长度、校验和
网络连接:文件句柄
/* client.c */
#include
#include
// #include
#include
#pragma comment(lib,"WS2_32.Lib") // 加载ws2_32.dll
// https://blog.csdn.net/MasterSaMa/article/details/90406827
int 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
#include
// #include
#include
// #pragma comment(lib,"ws2_32.lib") // 加载ws2_32.dll
// #pragma comment(lib,"./WS2_32.Lib") // 加载ws2_32.dll
int 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;
}
网络连接:文件描述符
/* client.c */
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
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;
}
sockaddr_in 专门用来保存ipv4地址的结构体
sockaddr 通用结构体,可以用来保存多种类型的ip地址和端口号
struct sockaddr_in {
short sin_family; // 地址类型
u_short sin_port; // 16位(2字节)端口号 (0-65536) 0-1023系统自动分配,1024-65536自定义
struct in_addr sin_addr; // 32位(4字节)IP地址
char sin_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_short sa_family; // 地址类型
char sa_data[14]; // IP地址和端口号
} SOCKADDR;
sizeof(sockaddr_in): 2+2+4+8
sizeof(sockaddr): 2+14
listen
只是让套接字处于监听状态,并没有接收请求
accept
接收请求
listen
后面的代码会继续执行,直到遇到accept
,accept
会阻塞程序执行,直到有新的请求到来
listen
listen(SOCKET sock, int backlog);
backlog 表示请求队列的最大长度
请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把他放进缓冲区,待当前请求处理完毕后,再从缓冲区读取出来处理,如果不断有新的请求进来,就按照先后顺序在缓冲区中排队,直到缓冲区满。当请求队列满时,不再接收新的请求,在发送请求则客户端会受到错误
缓冲区:请求队列
缓冲区的长度:存放多少个客户端请求
blocklog: SOMAXCONN表示由系统决定请求队列长度
accept
返回新的套接字来和客户端通信
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows
addr 保存了客户端的ip地址和端口号
write
nbytes 表示要写入的字节数
写入成功返回写入的字节数,失败返回-1
read
nbytes 表示要读取的字节数
成功则返回读取到的字节数,遇到文件末尾返回0,失败返回-1
/* client.c */
#include
#include
// #include
#include
// #pragma comment(lib,"WS2_32.Lib") // 记载ws2_32.dll
#define BUF_SIZE 100
// https://blog.csdn.net/MasterSaMa/article/details/90406827
int 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
#include
// #include
#include
#include
#include
#define BUF_SIZE 100
// #pragma comment(lib,"ws2_32.lib") // 记载ws2_32.dll
// #pragma comment(lib,"./WS2_32.Lib") // 记载ws2_32.dll
int 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;
}
https://blog.csdn.net/summer_fish/article/details/121740570
https://zhuanlan.zhihu.com/p/405794790
https://blog.csdn.net/mayue_web/article/details/82873115 !!!
阻塞/非阻塞:针对的是接收方(函数应对返回的方式)(阻塞:没有得到(内部处理线程)结果不返回;非阻塞:函数立即返回,循环查询)
同步/异步:针对的是发送方(函数调用的方式)(同步:没有结束就死等;异步;功能结果未知,结束后通知我)(通常用于请求方)
(自我理解:同步的表现形式是阻塞,异步的表现形式是非阻塞)
进一步理解:
同步/异步:表示的读写(访问)数据的方式
阻塞/非阻塞:线程/进程 在等待 读写(访问)数据的状态
并发/并行 和 同步/异步之间 并没有一个明确的关系
计算机能够同时执行多项任务;
并发的形式有许多不同:单核处理器:时间分片的形式,一个任务执行一段时间,也就是任务交替进行。也被称为进程或者线程的上下文切换
多核处理器:在多个核心上,真正并行的执行任务,也就是以并行的形式实现并发
同步:必须等到前一个任务执行完毕之后,才能执行下一个任务
在同步中,没有并发和并行的概念
不同任务之间,并不会相互等待,先后执行(即在执行任务A的时候,也可以同时执行任务B)
也就多线程编程
多线程是异步并发的:如果是多个核心,则是并行执行;如果在当个核心上,就是通过分配时间片的方法,交替实现并发
补充
多线程编程:多核心并发,适用于计算密集型应用程序
单线程异步编程:强制单核心并发,适用于I/O操作密集型应用程序
每个socket被创建后,都被分配两个缓冲区:输入缓冲区、输出缓冲区
输出缓冲区
wirte
/send
并不会立即向网络中传输数据,而是现将数据写入缓冲区,再由TCP协议将数据从缓冲区发送到目标IP
一旦将输入写入缓冲区,函数就返回成功(注意阻塞和非阻塞模式),不管数据有没有到达目标IP,也不管何时被发送到网络,(数据是否被发送、是否到达都是TCP协议负责的)
TCP协议独立于write/send函数,数据有可能刚被写入缓冲区就发送到网络,也有可能在缓冲区中挤压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,这些由系统控制
输入缓冲区
read
/recv
从输入缓冲区中读取数据,而不是直接从网络中读取
缓冲区不共享,在每个套接字中单独存在
缓冲区在创建套接字时自动生成
即使关闭套接字,也会继续传输(输出)缓冲区中遗留的数据(完全发送)
关闭套接字,将丢失输入缓冲区中的数据(不完全读取)
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
进行三次握手,建立成功后(也就是先发送SYN包,然后接收到服务端的ACK包后)connect
返回,否则一直阻塞
阻塞模式下调用
accept()
函数,没有新连接时,进程会进入睡眠状态,直到有可用的连接才返回
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启动三次握手,但是会立即返回(函数不等待连接建立好才返回),返回的错误码位EINPROGRESS(表示正在进行某种过程)
非阻塞模式下调用accept()函数,函数立即返回,有连接时返回客户端的套接字描述符或句柄,没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞
阻塞:connet/accept/write导致线程阻塞,(多线程中,不代表不能执行其他线程)
阻塞:recv读取数据长度不确定
???
阻塞模式,线程处于sleep休眠状态,此时不占用CPU,CPU就可以调度别的线程或进程(调用者需要返回查询做不用功(如果所有设备都一致没有数据到达))
非阻塞模式,虽然立即返回,但是调用者需要反复查询做不用功(while循环)(如果所有设备都一致没有数据到达),也就是不能执行其他线程!!!
通过select函数等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题),也就是阻塞的同时监视多个设备,还可以设定阻塞等待的超时时间timeout
使用阻塞socket,通过select函数等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题)
读写接口:套用封装好的readn/writen函数。(指定时间读不到数据/读不到指定数据算作异常)
https://blog.csdn.net/renwotao2009/article/details/51484872
https://blog.csdn.net/u011391629/article/details/71939248
int shutdown(int sock,int howto); // linux
int shutdown(SOCKET s,int howto); // windows
howto:
// linux
SHUT_RD : 断开输入流,套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数
SHUT_WR: 断开输出流,套接字无法发送数据,但是如果输出缓冲区中还有未传输的数据,则将传递到目标主机
SHUT_RDWR: 同时断开I/O流,相当于分两次调用shutdown()
// windows
SD_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不会丢失???
/* 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只能接收有限的数据量
read/recv
函数对接收数据没有区分性,可能将write/send
发送的多个独立的数据包当做一个数据包接收(数据的无边界性)
read/recv
函数不知道数据包的开始和结束标志,只是把他们当做是连续的数据流来处理
**例子1 **
/*server.c*/
#include
#include
// #include
#include
#include
#include
#define BUF_SIZE 5
// #pragma comment(lib,"ws2_32.lib") // 记载ws2_32.dll
// #pragma comment(lib,"./WS2_32.Lib") // 记载ws2_32.dll
int 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
#include
// #include
#include
// #pragma comment(lib,"WS2_32.Lib") // 记载ws2_32.dll
#define BUF_SIZE 100
// https://blog.csdn.net/MasterSaMa/article/details/90406827
int 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 是因为内存中第七个才是\0
message from client worl,buffersize 6,strlength5 // 注意是 ‘空格worl’
message from client dworl,buffersize 6,strlength1 // 实际缓冲区中的剩余为读取的字符就有1个
// 第四次
/* 与第三次的原理相似 */
> 客户端:helloworldqtcmd
> 服务端:
> message from client hello,buffersize 6,strlength5
message from client world,buffersize 6,strlength5
message from client qtcmd,buffersize 6,strlength5
粘包分析
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不开启):
- 如果数据包长度达到MSS或者有FIN包,立即发送,否则等到下一个包到来,如果下一个包到来后,两个包的总长度超过MSS,就会进行拆分发送
- 等到超时,第一个包没到MSS长度,但是又迟迟等不到第二个包到来,就立即发送
值得注意的是:即使关闭了nagle算法,还是会出现粘包问题
粘包处理
数据粘包本质上是不确定消息边界,因此只要在发送端发送消息的时候给消息 带上识别消息边界的信息,接收端就可以根据这些信息识别出消息的边界,从而区分每个消息
如0xffffe
问题:可能实际的数据中也会出现该标志位
在收到头标志时,里面还可以带上消息长度,表明在这之后多少个字节属于这个消息,如果长度不够则等待一会,接收完全
针对标志位的问题,发送端在发送时还会加入各种检验字段(校验和 或者对整段完整数据进行CRC之后获得的数据)放在头标志位后面
即,在接收端拿到整段数据后,检验下确保它就是发送端发来的完整数据
‘web - socket 数据粘包处理’
服务端,文件读到末尾,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
#include
#include
#define BUF_SIZE 1024
int 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
#include
#include
#define BUF_SIZE 1024
int 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;
}
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
#include
#include
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;
}
域名/IP
可以通过多个域名访问同一主机
同一ip地址可以绑定多个域名
同一域名可以有多个IP地址
host ->
- 域名1
- ip1
- ip2
- 域名2
- ip3
- ip4
域名解析
域名-> ip地址
struct hostent *gethostbyname(const char *hostname);
struct hostent{
char *h_name; //official name
char **h_aliases; //alias list
int h_addrtype; //host address type
int h_length; //address lenght
char **h_addr_list; //address list
}
#include
#include
#include
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
*/
udp套接字不会保持连接状态,每次传输数据都要添加目标地址信息
// 发送
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); //Linux
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen); //Windows
buf // 保存待传输数据的缓冲区地址
nbytes // 带传输数据的长度,单位:字节
to // 包含目标地址信息的sockaddr结构体变量的地址
addrlen // 传递给参数to的地址值 结构体变量的长度
// 接收
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen); //Linux
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen); //Windows
buf // 保存接收数据的缓冲区地址
nbytes // 可接收的最大字节数 不能超过buf缓冲区大小
from // 包含发送端地址信息的sockaddr结构体变量的地址
addrlen // 保存参数from的结构体变量长度的变量地址值
例子
/* == server.c == */
#include
#include
#define BUF_SIZE 100
int 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
#include
#define BUF_SIZE 100
int 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;
}
基于数据包是指无论应用层交给UDP多长的报文,UDP传输层都照样发送,即一次发送一个报文,如果数据包太长,需要分片,也是IP层的事情,大不了效率低一些
UDP对应用层传递下来的报文,即不合并也不拆分,而是保留这些报文的边界
而接收方在接收数据爆时,也不会像面对TCP无穷无尽的二进制流那样不清楚什么时候能结束
UDP不存在数据粘包的问题
(自我理解):虽然UDP本身没有数据粘包的问题,但是如果手动发送的数据就不是一个根据协议定制好的数据报,那么还是需要进行手动的处理粘包问题
TCP/UDP
正是因为基于数据报和基于字节流的差异,TCP发送端发送10次字节流数据,而这时候接收端可以分100次去取数据,每次取数据的长度可以根据处理能力做调整
UDP发送端发送了10次数据报,那么接收端就要在10次收完,且发了多少,就取多少,确保每次都是一个完整的数据报
IP报头
16位总长度,表明IP报头里记录了整个IP包的总长度
根据16位的数据报文长度,可以作为数据边界,接收端的应用层能够清晰地将不同的数据报文区分开,从报头开始取n位,就是一个完整的数据报,从而避免粘包和拆包的问题
UDP data长度=IP总长度-IPheader长度-UDPheader长度
TCPB报头
TCPheader中没有长度信息
TCP data长度=IP总长度-IPheader长度-TCPheader长度
但是,注意:由于TCP发送端在发送的时候就不保证发的是一个完整的数据报,仅仅看成一连串无结构的字节流,这串字节流在接收端收到时哪怕知道长度也没有,因为它很可能只是某个完整消息的一部分
IP层分包
IP层不会造成数据粘包
如果消息数据过长,IP层会按MTU长度把消息分成N个切片,每个切片自带有自身在包里的位置offset和同样的IP头信息
各个切片在网络中进行传输,每个数据包切片可以在不同的路由中流转,然后在最后的中点汇合后再组装
在接收端接收到第一个切包时会申请一块新内存,创建IP包的数据结构,等待其他切分包数据到位,等消息全部到位后就把真个消息包传递给上层(传输层)进行处理
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