c/c++自定义通讯协议(TCP/UDP)

这篇具有很好参考价值的文章主要介绍了c/c++自定义通讯协议(TCP/UDP)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 前言:TCP与UDP是大家耳熟能详的两种传输层通信协议,本质区别在于传输控制策略不相同:使用TCP协议,可以保证传输层数据包能够有序地被接受方接收到,依赖其内部一系列复杂的机制,比如握手协商,ACK确认,超时重传,拥塞控制等; 而UDP基本上没有额外的控制策略,所以接收方的传输层能不能接收到数据包是无法保证的。正是因为不能保证每一个数据包有序到达,UDP数据包与包之间,必须是相互独立的,每一个都应该是有意义的可以被解析出完整应用层报文的数据块,因此UDP又被称为面向(单个)报文的协议;而每一个TCP数据包则可以是应用层报文的某一部分,多个有序的数据包就可以拼接出完整的应用层报文,因此TCP被称作面向流的协议。

        我们知道,网络层(即IP层)数据包是有最大长度MTU限制的(因为物理层大包丢包概率很高),所以不论是发送UDP包还是TCP包,如果突破了该限制,数据包将会被IP层切片,接收方的IP层会根据分片id对传输层数据片段的进行重组。分片和重组都会占用cpu和内存资源,降低通讯效率,尤其是在使用TCP通讯的时候,因为有重发机制,ip层的分片重组会做很多无用功。所以通信双方若采用TCP通信,会在握手连接阶段协商MSS,即一个TCP最大包含的数据量,有了MSS约定,TCP层交付给IP层的数据包就不会超过IP层的MTU限制,也就是说分片工作在TCP传输层完成。而使用UDP时,一旦UDP数据包被IP层分片,接收方大概率是无法组成完整的UDP数据包的,因为就算某些片段丢失了,发送方也不会对整个UDP包进行重发,因此UDP通讯是禁止IP层分片的(一旦超过MTU,会直接丢弃)。

        所以,如果我们的应用需要传输大的数据包,就没办法使用单纯的UDP协议传输了,除非基于UDP在应用层自行实现一种类似于TCP内部的分片控制机制,完成数据的可靠传输。

一. 自定义网络协议

        假设我们的应用层报文一般比较小,不超过底层的MTU限制,这样一来,我们既可以使用TCP,也可以使用UDP来进行传输。下面是一个最简单的协议定义示例,包含消息头定义和消息体定义:

struct SmHeader
{
    int m_length;// 消息头长度+消息体长度
    int m_request_type;//请求类型
    int m_reply_type;//响应类型
    int m_body_type;//消息体类型
};

struct Body1
{
  int m_int_b1;
  float m_float_b1;
  char m_char_b1;
  char m_reserve[3];//字节对齐
};

struct Body2
{
  int m_int_array_b2[12];
  float m_float_array_b2[15];
};

struct Body3
{
  char m_char_array_b3[512];
};

struct Body1Assemble
{
    int m_count;
    Body1 m_b1[0];
};

union SmBody
{
  Body1 b1;
  Body2 b2;  
  Body3 b3;
  Body1Assemble b4;
};

struct SmMessage
{
  SmHeader head;
  SmBody body;
};

由上述协议的定义可知,这样一个完整的消息最少有sizeof(SmHeader) = 16字节(一个消息可以没有消息体,比如PING/PONG心跳包,只有消息头即可),由于Body1Assemble类型的数据体长度不确定,因此用TCP的话,可以传递很长的消息。

二. 消息的接收

由于发送方的发送速率与接收方的接收速率很难匹配,在接收方的接收缓冲区内会形成数据包累积,所以我们需要上述定义的消息头协助完成数据包的提取,有效处理接收端粘包问题。

char buffer[1024];//在应用层定义一个数据缓冲区,至少能够放得下最大的数据包
int bfsize = 1024;//缓冲区长度
int legacy_bytes = 0;//上一次解析处理剩余的字节数
bool skip_recv = false;//是否可以直接使用上一次剩余数据解析出完整数据

while(1)
{
    int current_size = 0;
    if(!skip_recv)
    {
        current_size = recv(fd,buffer+legacy_bytes,bfsize-legacy_bytes);
        if(current_size<=0)
            break;
        current_size += legacy_bytes;
        legacy_bytes = 0;
    }
    else
    {
        current_size = legacy_bytes;
        legacy_bytes = 0;
        skip_recv = false;
    }
    int expected_size = -1;
    if(current_size>=sizeof(SmHeader))
    {
        const SmHeader* head = (SmHeader*)buffer;
        expected_size = head->m_length;
        if(expected_size<=0 || expected_size>bfsize)
        {
            printf("Invalid message header or buffer insufficient.\r\n");
        }
        else if(current_size>=expected_size)
        {
            /***********process a complete message******/
            // 解析buffer中的消息;
            SmMessage* msg = (SmMessage*)buffer;
            // 处理消息 balabala...
            if(head->m_body_type == BODY1ASSEMBLE)
            {
                struct Body1* data;
                for(int i=0;i<msg->body.b4.m_count;i++)
                {
                    data = &msg->body.b4.m_b1[i];
                    //printf("data id: %d, data member1: %d",i,data->m_int_b1);
                }
            }
            /********************end********************/
            if(current_size>expected_size)
            {
                legacy_bytes = current_size-expected_size;
                memmove(buffer,buffer+expected_size,legacy_bytes);
                if(legacy_bytes>=sizeof(SmHeader))
                {
                    const SmHeader* next_head = (SmHeader*)buffer;
                    int next_expected_size = next_head->m_length;
                    if(next_expected_size>0)
                        skip_recv = legacy_bytes>=next_expected_size;
                }
            }
            else
            {
                legacy_bytes = 0;
            }
        }
        else
        {
            legacy_bytes = current_size;
            printf("Incompleted message.\r\n");
        }
    }
    else
    {
        legacy_bytes = current_size;
    }
}

三. python版本的TCP/UDP报文收发

import socket
import struct
import copy
import threading

buffer = bytearray()
buffer_size = 0
skip_recv = False  #之前遗留的数据是否可以直接解出完整的数据包
legacy_Bytes_count=0  #之前遗留的数据长度


def case_reply_1():
    fmt = '5ifc'
    return fmt


def case_reply_2():
    fmt = '16i15f'
    return fmt


def case_reply_3():
    fmt = '4i512c'
    return fmt

def case_reply_4():
    fmt = '5i'
    return fmt


def case_reply_default():
    print("No implementation for processing this type of message.")
    return None


def case_request_0():
    reqType = 0
    repType = -1
    msgType = 1
    """
        struct RequestBody1
        {
            char name[8];
            int idx;
            float account;
        }
    """
    name = b"Jhon\n\n\n\n"
    idx = 101
    account = 1361.58
    length = 16 + len(name) + 4 + 4
    fmt = "4i8sif" 
    st = struct.Struct(fmt)
    data = st.pack(length,reqType,repType,msgType,name,idx,account)
    return data

def case_request_1():
    reqType = 1
    repType = -1
    msgType = 2
    """
        struct RequestBody2
        {
            bool female;
            char reserve[3];
            int age;
        }
    """
    female = True
    age = 25
    length = 16 + 8
    fmt = "4i?3xi"
    st = struct.Struct(fmt)
    data = st.pack(length,reqType,repType,msgType,female,age)
    return data


def case_request_default():
    data = bytes()
    return data


def unpack_messages(num:int, msg:bytearray)->tuple:
    numbers = {
        0:   case_reply_1,
        1:   case_reply_2,
        2:   case_reply_3,
        3:   case_reply_4
    }
    method = numbers.get(num, case_reply_default)
    if method:
        fmt = method()
        cnt = 0
        if num == 3:
            cnt = struct.calcsize(fmt)
            if len(msg) < cnt:
                return tuple()
            assemble = struct.unpack(fmt,msg[:cnt])
            extra_fmt = 'ifc3s'
            fmt += assemble[4]*extra_fmt
        if fmt:
            cnt = struct.calcsize(fmt)
        # python的struct字节对齐和c/c++中的字节对齐处理方式不同,可能由于字节对齐的问题解析出错,
        # 此时可以根据msg内部字节排列情况,逐个解析
        if cnt != len(msg):
            print("message parse error.")
            return tuple()
        
        structured_msg = struct.unpack(fmt,msg)
        return structured_msg
    else:
        return tuple()


def connect_to_motion_server(IP:str, PORT:int, LPORT:int=-1):
    client = None
    try:
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        if LPORT > 0:
            client.bind(('',LPORT))#客户端绑定本地端口
        client.connect((IP, PORT))
        print('Connect Success!')
    except socket.error as msg:
        print(msg)
        client.close()
        client = None
    return client


def pack_messages(num:int)->bytes:
    numbers = {
        0:   case_request_0,
        1:   case_request_1
    }
    method = numbers.get(num, case_request_default)
    if method:
        data = method()
        return data
    else:
        return bytes()


def send_message_to_server(_client, msgtyp:int, scktype:str="TCP", address: tuple = None):
    data = pack_messages(msgtyp)
    size = send_to_socket(_client, data, scktype, address)
    return size


def send_to_socket(_client, data: bytes, sock_type: str = 'TCP', address: tuple = None):
    size = 0
    if len(data) < 1:
        return size
    if sock_type == 'TCP':
        size = _client.send(data)
    elif sock_type == 'UDP':
        if address:
            size = _client.sendto(data, address)
        else:
            pass
    return size

    
def process_message(c):
    while True:
        leng, packet = get_reply_from_server(c)
        if leng < 0:
            break
        print("receive message: ",packet)
    return


def get_reply_from_server(c) -> (int, tuple):
    global buffer, buffer_size, skip_recv, legacy_Bytes_count
    pkt_len, pkt, skip_recv,legacy_Bytes_count = read_from_socket(c, buffer,buffer_size,legacy_Bytes_count,skip_recv,
                                                                    min_size=16,min_fmt='iiii')
    return pkt_len, pkt


def read_from_socket(_client, recv_buffer:bytearray, bfsize:int, legacy_size:int, skip_flag:bool, sock_type:str='TCP', min_size=4,min_fmt='i'):
    assert bfsize > 0 and legacy_size >= 0, "please initialize recv_buffer first."
    complete_pkg = None
    header = tuple()
    if not skip_flag:
        extra_to_read = bfsize - legacy_size
        recv_buffer[legacy_size:bfsize] = b'\x00'
        tmp = None
        if sock_type == 'TCP':
            tmp = _client.recv(extra_to_read)
        else:
            tmp, addr = _client.recvfrom(extra_to_read)
        if not tmp or len(tmp) <= 0:
            return -1, None, False, 0 # socket disconnect
        csize = len(tmp)
        csize += legacy_size
        recv_buffer[legacy_size:csize] = tmp
        legacy_size = 0
    else:
        csize = legacy_size
        legacy_size = 0
        skip_flag = False

    if csize >= min_size:
        hst = struct.Struct(min_fmt)
        prefix = hst.unpack(recv_buffer[:min_size])
        if len(prefix) == 0 or prefix[0] <= 0:
            print("Invalid message header, drop it.")
        elif csize >= prefix[0]:
            esize = prefix[0]
            complete_pkg = copy.deepcopy(recv_buffer[:esize])
            if csize > esize:
                legacy_size = csize - esize
                recv_buffer[:legacy_size] = recv_buffer[esize:csize]
                if legacy_size >= min_size:
                    next_prefix = hst.unpack(recv_buffer[:min_size])
                    skip_flag = legacy_size >= next_prefix[0]
            else:
                legacy_size = 0
            header = prefix
        else:
            legacy_size = csize
            print("Incomplete message. Ignore it.")
    else:
        legacy_size = csize
    if header and complete_pkg:
        # header = (length, reqType, resType, msgType)
        st_data = unpack_messages(header[3], complete_pkg)
        return len(complete_pkg), st_data, skip_flag, legacy_size
    else:
        return 0, None, skip_flag, legacy_size


def communicate_buffer_init(size):
    global buffer,buffer_size, skip_recv, legacy_Bytes_count
    buffer_size = size
    skip_recv = False
    legacy_Bytes_count = 0
    buffer = buffer.zfill(buffer_size)


if __name__ == '__main__':
    communicate_buffer_init(1024)
    cli = connect_to_motion_server("127.0.0.1", 12289)
    _inputText = 'c'
    if cli:
        motion_thread = threading.Thread(target=process_message, args=(cli,))
        motion_thread.start()
        while _inputText!= 'q':
            _inputText = input(r'please type a request:').lower()
            sz = 0
            if _inputText == '0':
                sz = send_message_to_server(cli,0)
            if _inputText == '1':
                sz = send_message_to_server(cli,1)
            if sz < 0:
                break
            
        motion_thread.join()
    

四.补充一个c++实现的UDP收发类

/***************************************udpclient.h******************************/

#ifndef UDP_CLIENT_H
#define UDP_CLIENT_H
#include <winsock.h>

class Clientudp
{
public:
	Clientudp();
	bool InitializeClient(const char* ip, int local_port, int remote_port = -1);
	void SetReadTimeout(int timeout_ms);
	int PushToWriteBuffer(const char* msg, unsigned int size);
	int PullFromReadBuffer(char* msg, unsigned int size);
	bool GetClientStatus();
    bool GetRemoteAddress(sockaddr_in* addr);
	~Clientudp();
private:
	int m_fd_;
	int m_read_timeout = -1;
	bool m_socket_avaliable_ = false;
	sockaddr_in m_remote_addr_;
	int m_remote_port_ = -1;
	sockaddr_in m_bind_addr_;
};

#endif



/*************************************udpclient.cpp***************************/

#include <iostream>
#include "UdpClient.h"

Clientudp::Clientudp()
{}

Clientudp::~Clientudp()
{
    if(m_socket_avaliable_)
        closesocket(m_fd_);
}

void Clientudp::SetReadTimeout(int timeout_ms)
{
	m_read_timeout = timeout_ms;
}

bool Clientudp::InitializeClient(const char* ip, int local_port, int remote_port)
{
	if (m_socket_avaliable_)
	{
		closesocket(m_fd_);
		m_socket_avaliable_ = false;
	}
	if ((m_fd_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
	{
		// create socket failed.
		m_socket_avaliable_ = false;
		return m_socket_avaliable_;
	}
	if (m_read_timeout > 0)
	{
		struct timeval read_timeout = { m_read_timeout, 0 };
		setsockopt(m_fd_, SOL_SOCKET, SO_RCVTIMEO, (char *)&read_timeout, sizeof(struct timeval));
	}
	memset(&m_remote_addr_, 0, sizeof(m_remote_addr_));
	m_remote_addr_.sin_family = AF_INET;
	m_remote_addr_.sin_addr.s_addr = inet_addr(ip);
	m_remote_addr_.sin_port = htons(remote_port);
	m_remote_port_ = remote_port;
	memset(&m_bind_addr_, 0, sizeof(m_bind_addr_));
	m_bind_addr_.sin_family = AF_INET;
	m_bind_addr_.sin_addr.s_addr = htonl(INADDR_ANY);
	m_bind_addr_.sin_port = htons(local_port);

	if (bind(m_fd_, (SOCKADDR*)&m_bind_addr_, sizeof(m_bind_addr_)) == SOCKET_ERROR)
	{
		int error_code = WSAGetLastError();
		if (error_code == WSAEADDRINUSE)
			printf("The port %d on this machine has been Occqupied.\n", local_port);
		printf("Bind Client to Fixed address Failed, Then you should settle a correct remote port and send data to remote machine before recv.\n");
		if (m_remote_port_ <= 0)
		{
			printf("InitializeClient Failed, Remote port = %d seems not to be a valid port number.\n", m_remote_port_);
			closesocket(m_fd_);
			m_socket_avaliable_ = false;
			return false;
		}
	}
	m_socket_avaliable_ = true;
	return m_socket_avaliable_;
}

int Clientudp::PushToWriteBuffer(const char* msg, unsigned int size)
{
	if (!m_socket_avaliable_)
	{
		return -1;
	}
	int ret = sendto(m_fd_, msg, size, 0, (sockaddr*)&m_remote_addr_, sizeof(m_remote_addr_));
	if (ret == SOCKET_ERROR)
	{
		int error_code = WSAGetLastError();
		//printf("error_code: %d\n",error_code);
		if (m_remote_port_ < 0 && error_code == WSAEINVAL)
		{
			printf("You should recv data from peer firstly before send.\n");
			return 0;
		}
		closesocket(m_fd_);
		m_socket_avaliable_ = false;
		m_remote_port_ = -1;
		return -1;
	}
	else
	{
		return ret;
	}
}

int Clientudp::PullFromReadBuffer(char* msg, unsigned int size)
{
	if (!m_socket_avaliable_)
	{
		return -1;
	}
	int addr_len = sizeof(m_remote_addr_);
	int ret = recvfrom(m_fd_, msg, size, 0, (sockaddr*)&m_remote_addr_, &addr_len);
	if (ret == SOCKET_ERROR)
	{
		int error_code = WSAGetLastError();
		if (error_code == WSAEMSGSIZE || error_code == WSAEINTR || error_code == WSAETIMEDOUT || error_code == WSAEWOULDBLOCK)
		{
            if(error_code == WSAEMSGSIZE)//datagram is too large to put into msg buffer.
                memset(msg,0x00,size);
			return 0;
		}
		else
		{
			closesocket(m_fd_);
			m_socket_avaliable_ = false;
			return -1;
		}
	}
	else
	{
		return ret;
	}
}

bool Clientudp::GetClientStatus()
{
	return m_socket_avaliable_;
}

bool Clientudp::GetRemoteAddress(sockaddr_in* addr)
{
    if(!m_socket_avaliable_)
        return false;
    if (m_remote_port_ <= 0)
    {
        int asize;
        getpeername(m_fd_, (sockaddr*)&m_remote_addr_, &asize);
        m_remote_port_ = ntohs(m_remote_addr_.sin_port);
        if (m_remote_port_ <= 0)
            return false;
    }
    *addr = m_remote_addr_;
    return true;
}

udp测试:文章来源地址https://www.toymoban.com/news/detail-524817.html

#include <iostream>
#include "udpclient.h"

int main()
{
    WSADATA ws
    WSAStartup(MAKEWORD(2,2),&ws);
    Clientudp udp;
    udp.InitializeClient("127.0.0.1", 1234, -1);
    char RxBuffer[1024];
    char TxBuffer[1024];
    memset(RxBuffer,0x00,1024);
    memset(TxBuffer,0x00,1024);
    /*如果remote_port为-1,则目的端口号未知,只能先收再发。如果知道对方端口号,则收发顺序可随便*/
    int readLen= udp.PullFromReadBuffer(RxBuffer,1024);
    printf("Read message length: %d",readLen);
    int writeLen = sprintf_s(TxBuffer,1023,"hello world");
    udp.PushToWriteBuffer(TxBuffer,writeLen);
    getchar();
    WSACleanup();   
    return 0;
}

到了这里,关于c/c++自定义通讯协议(TCP/UDP)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 网络传输层协议详解(TCP/UDP)

    目录 一、TCP协议 1.1、TCP协议段格式  1.2、TCP原理  确认应答机制 超时重传机制 (安全机制) 连接管理机制(安全机制)  滑动窗口  流量控制(安全机制)  拥塞控制  延迟应答(效率机制) 捎带应答(效率机制)  ​编辑面向字节流(粘包问题)  缓冲区  TCP异常情况  二、UDP协议

    2024年02月06日
    浏览(57)
  • 计算机网络笔记:TCP协议 和UDP协议(传输层)

    TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族。 TCP的全称是 传输控制协议 是一种 面向连接的、可靠的、基于字节流 的 传输层 通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构) TCP报文 是TCP层传输的数据单元,也称为 报文段 ,一个TCP报文段由

    2024年02月02日
    浏览(53)
  • 【JavaEE】网络原理——传输层协议:UDP和TCP

    目录 1、简单了解应用层协议 2、传输层UDP协议 3、传输层TCP协议  3.1、TCP报文介绍 3.2、TCP实现可靠传输的核心机制 3.2.1、确认应答 3.2.2、超时重传  3.3、连接管理 (三次挥手,四次握手) 3.3.1、建立连接(三次握手) 3.3.2、断开连接(四次挥手)  3.4、滑动窗口  3.5、流量

    2024年02月10日
    浏览(82)
  • 网络基础二——传输层协议UDP与TCP

    ​ 传输层协议有UDP协议、TCP协议等; ​ 两个远端机器通过 使用\\\"源IP\\\",“源端口号”,“目的IP”,“目的端口号”,\\\"协议号\\\"来标识一次通信 ; 9.1端口号的划分 ​ 0-1023:知名端口号,HTTP,HTTPS,FTP,SSH等应用层协议,他们的端口号都是固定的;如:ssh使用的是22号端口,

    2024年04月12日
    浏览(46)
  • UDP(用户数据报协议)和TCP(传输控制协议)是互联网协议(IP)中两种主要的传输层协议

    您的描述是正确的。UDP(用户数据报协议)和TCP(传输控制协议)是互联网协议(IP)中两种主要的传输层协议。他们之间有几个重要的区别,其中之一就是建立连接的方式。 连接方式: • TCP:在进行数据传输之前,需要通过三次握手(3-way handshake)建立连接。这可以确保

    2024年02月02日
    浏览(116)
  • 网络编程——TCP/IP协议族(IP协议、TCP协议和UDP协议……)

    1、IP协议简介 IP协议又称 网际协议 特指为实现在一个相互连接的网络系统上从源地址到目的地传输数据包(互联网数据包)所提供必要功能的协议,是网络层中的协议。 2、特点 不可靠 :它不能保证IP数据包能成功地到达它的目的地,仅提供尽力而为的传输服务 无连接 :IP 并不

    2024年02月13日
    浏览(77)
  • 【网络】传输层——UDP | TCP(协议格式&&确认应答&&超时重传&&连接管理)

    🐱作者:一只大喵咪1201 🐱专栏:《网络》 🔥格言: 你只管努力,剩下的交给时间! 现在是传输层,在应用层中的报文(报头 + 有效载荷)就不能被叫做报文了,而是叫做 数据段 (报头 + 有效载荷),传输层的有效载荷就是应用层的完整报文。 端口号(port):标识了一个主机上

    2024年02月13日
    浏览(46)
  • HTTP/UDP/TCP/IP网络协议

    OSI模型定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI模型各层的通信协议,大致举例如下表所示: 层次 常见协议 应用层 HTTP、SMTP、SNMP、FTP、Telnet、SIP、SS

    2024年04月11日
    浏览(43)
  • TCP/IP网络参考模型(端口号、TCP滑动窗口、TCP拥塞控制、IP协议号、以太网帧)

    目录 TCP/IP四/五层模型 应用层常见协议——传输数据PDU 传输层协议——传输数据段 端口号 TCP面向连接服务CONS  TCP报文格式 TCP数据传输——ACK确认机制 TCP会话建立——三次握手 TCP会话结束——四次握手 TCP流量控制——滑动窗口协议 TCP可靠传输机制 TCP拥塞控制 UDP无面向连

    2024年02月03日
    浏览(60)
  • 网路原理-传输层UDP,TCP/IP(确认应答,超时重传,连接管理,三次握手,四次挥手,状态转换,流量控制,滑动窗口,拥塞控制,延时应答,捎带应答,异常情况,面向字节流)-网络层(IP协议,地址管理)

    本节重点 • 理解传输层的作⽤,深⼊理解TCP的各项特性和机制 • 对整个TCP/IP协议有系统的理解 • 对TCP/IP协议体系下的其他重要协议和技术有⼀定的了解 我们之前编写完了基本的 java socket ,要知道,我们之前所写的所有代码都在应⽤层,都是为了 完成某项业务,如翻译等。

    2024年04月15日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包