web-socket

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() # 关闭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

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/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 <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.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;
}

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 {
	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

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);  //Windows

addr 保存了客户端的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/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 <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.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;
}

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); // 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不会丢失???

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.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 <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/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

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 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 <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#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;
}

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 name
	char **h_aliases;  //alias list
	int  h_addrtype;  //host address type
	int  h_length;  //address lenght
	char **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);  //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 <stdio.h>
#include <winsock2.h>

#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 <stdio.h>
#include <winsock2.h>

#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;
}

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值