MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析

发布时间:2020-08-11 17:52:06 作者:gaopengtttt
来源:ITPUB博客 阅读:297
原创:转载请说明出处

水平有限再加上源码的复杂性,难免出现错误,请共同研究予以纠正
本文参考源码:
Net_serv.cc(主要参考)
Mysql.h.pp
Mysql_socket.h
Violite.h
Viosocket.c
Vio.c
参考书籍:
深入理解MYSQL核心技术
MYSQL核心内幕
internals-en
MYSQL官方手册
LINUX系统编程手册

注意:
   1、本文将主要解析非压缩MYSQL NET包,而尽量不考虑压缩的MYSQL NET包来减小难度
   2、本文主要以TCP->IP->以太网为蓝本进行描述,不考虑其他协议如(UDP)
   3、本文主要以Net_serv.cc的my_net_write来描述写入socket阶段,而没有考虑net_write_command
        实际上net_write_command函数是client传递命令包的主要函数入口,调用的下层函数一致
   4、写入阶段可以达到net buffer满写入也可以调用net_flush()写入,但是这里无力研究net_flush()只研究满写入的情况

一、基本概念
在这之前我们必须明白如下的一些基本概念,否则不利于阅读

1、socket:是一种进程间通信的方式,可以用于多态计算机和本地两个进程进行通信,类似管道是双向
           通信的一种方式,在网络上主要通过绑定IP和端口和识别唯一的网络服务端,在本地通过绑
           定一个本地文件进行通信,它工作在LINUX 内核态。
2、通信协议:协议也就是客户端和服务端事先商量好的一种格式,如果格式不对则解包失败,比如TCP
                    协议格式如下,MYSQL有自己的通信协议。
                    MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析


3、MYSQL协议:MYSQL作为大型数据库系统,他有着自己的协议,至少包含如下一些数据包。
              1、握手阶段
                 --服务端到客户端 初始化握手包 
                 --客户端到服务端 客户端认证包
                 --服务端到客户端 OK包、ERROR包
              2、连接建立阶段
                --客户端到服务端 命令(command)包
                --服务端到客户端 OK包、ERROR包、结果集包            
              其中结果集包包含:
              1、包头包
              2、FILED属性包
              3、EOF包
              4、行数据包        
              FILED属性包:为列属性每个列都会有一个
              行数据包:为返回数据每行一个包
              如果一个SELECT 返回 2行3列数据
              会包含3(列)+2(行)+1(包头包)+2(EOF包)个数据包
              由于MYSQL数据包的复杂性本文并不准备解析MYSQL协议各种包,可以参考:
             MYSQL核心内幕
             internals-en
             下图是展示了MYSQL 服务端和客户端之间如何握手成功,并且进行数据传输
               
            MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析
 
                     我们约定它叫做MYSQL数据包               
4、MYSQL NET包:它是实际的传输包的大小,大小最大为16M-1,这是在源码中定义死了的,每个MYSQL NET包
               包含一个包头,至少包含4个字节(非压缩包,如果压缩包会多3个字节),如下:
               3 bytes:(压缩后)payload长度
               1 bytes:序号
         (压缩)3 bytes:压缩前payload长度
               其中payload就是实际数据
               比如这样一个MYSQL NET包:
               MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析
               
               为什么有一个序号呢?因为为了保证每个命令发送的包是有序的,比如一个结果
               集合包会包含多个包,而其中的行数据包(SERVER->CLIENT的时候每一行数据是一个MYSQL数据包)
               包很可能大于16M-1,那么我们就需要将整个结果集包分为多个MYSQL NET包进行传输,当到达
               client的时候保证他顺序。当然并不是每个MYSQL NET包都很大,比如一些MYSQL数据包如OK包,就很
               小,我们知道在以太网传输的最大帧为MTU 1500字节,那么可能出现一个以太网帧包含多个MYSQL NET
               包(如OK包),也可能一个MYSQL NET包在多个以太网帧中,同时可能出现一个MYSQL数据包在多个MYSQL 
               NET包中,但是最小一个MYSQL NET包至少包含一个MYSQL数据包(如OK包),当然TCP
               注意当一个MYSQL数据包分为多个MYSQL NET包的时候其最后会紧跟一个长度为0作为结束的标识,源码中
               /* End of big multi-packet. */
                   if (!pkt_len)
                   goto end;
             我们约定它叫做MYSQL NET包    
5、NET结构体说明
下面先来看几个截图说明:
MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析
MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析


   可以看到NET结构中封装了一个BUFFER,而这个BUFFER正是由参数net-buffer-length控制
其大小不能超过参数max-allowed-packet大小的这个buffer,本文约定将它叫做net buffer
   net-buffer-length 默认16K最大为1M
   max-allowed-packet 默认4M最大1G
   结构体还封装了2个unsigned int的变量write_timeout,read_timeout. 他们正是
net-wirte-timeout,net-read-timeout参数指定,用来表示在返回一个ETIMEDOUT错误前能够
KEEPLIVE最大的时间。
  设置超时的底层调用很有可能是
  ret= mysql_socket_setsockopt(vio->mysql_socket, SOL_SOCKET, optname,optval, sizeof(timeout)); 
  之类的调用   
  另外结构体还封装了retry_count这是在遇到EINTR错误的时候重试的次数由参数net-retry-count
控制,在后面将会讲述       

6、LINUX ETIMEDOUT、EINTR、EWOULDBLOCK、EAGAIN
  #define    ETIMEDOUT    110    /* Connection timed out */
  #define    EINTR         4    /* Interrupted system call */
  #define    EAGAIN        11    /* Try again */
  #define    EWOULDBLOCK    EAGAIN    /* Operation would block *

7、LINUX平台下MYSQL读取和写入scoket函数
位于Mysql_socket.h中
send(mysql_socket.fd, buf, IF_WIN((int),) n, flags);
recv(mysql_socket.fd, buf, IF_WIN((int),) n, flags);
当然如果是WIN_32平台send和recv函数有底层封装

8、包封装流程
如下图:
MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析

本文研究是应用层MYSQL通过自己的协议进行数据包封装后如何进行传输的
                        
二、MYSQL数据包的写入scoket阶段

1、将可能大的MYSQL数据包进行拆分
函数原型
my_bool my_net_write(NET *net, const uchar *packet, size_t len) 
net:NET结构体指针
packet:MYSQL数据包指针,MYSQL数据包由MYSQL协议栈准备好
len:MYSQL数据包长度

这个过程会将大的MYSQL数据包进行拆分打包为多个MYSQL NET包,如果是小的MYSQL数据包(如OK包)就进行
打包为MYSQL NET包调用net_write_buff下面我将我写的中文注释加上源码部分一同放出如下:

点击(此处)折叠或打开

  1. my_bool my_net_write(NET *net, const uchar *packet, size_t len) //将长度为packet的数据写入到net->buffer
  2. {
  3.   uchar buff[NET_HEADER_SIZE]; // lenth 3 seq 1 4bytes
  4.   int rc;

  5.   if (unlikely(!net->vio)) /* nowhere to write */
  6.     return 0;

  7.   MYSQL_NET_WRITE_START(len);

  8.   DBUG_EXECUTE_IF("simulate_net_write_failure", {
  9.                   my_error(ER_NET_ERROR_ON_WRITE, MYF(0));
  10.                   return 1;
  11.                   };
  12.                  );

  13.   /*
  14.     Big packets are handled by splitting them in packets of MAX_PACKET_LENGTH
  15.     length. The last packet is always a packet that is < MAX_PACKET_LENGTH.
  16.     (The last packet may even have a length of 0)
  17.   */
  18.   while (len >= MAX_PACKET_LENGTH) //如果写入MYSQL 协议包的长度大于了最大mysq NET包 就分为多个MYSQL NET包
  19.   {
  20.     const ulong z_size = MAX_PACKET_LENGTH; // 16M-1 计为包的长度
  21.     int3store(buff, z_size); //将长度写入到栈 buff中
  22.     buff[3]= (uchar) net->pkt_nr++; //将buffer中的 seq+1 当然 pkt_nr 序列也+1
  23.     if (net_write_buff(net, buff, NET_HEADER_SIZE) || //写入MYSQL NET包头部
  24.         net_write_buff(net, packet, z_size)) //将长度为z_size的进行拆分的MYSQL 协议包一分部写入到net buffer中
  25.     {
  26.       MYSQL_NET_WRITE_DONE(1);
  27.       return 1;
  28.     }
  29.     packet += z_size; //将packet的指针 加上z_size的大小 其实也就是16M-1
  30.     len-= z_size; //当然len 也就相应的减少z_size 其实也就是16M-1
  31.   }
  32.   //如果不是大的MYSQL 协议包或者是MYSQL协议包的最后一部分则执行下面代码
  33.   /* Write last packet */
  34.   int3store(buff,len); //将最后的长度计入buffer 头3字节
  35.   buff[3]= (uchar) net->pkt_nr++; //当然序号继续+1
  36.   if (net_write_buff(net, buff, NET_HEADER_SIZE)) //写入MYSQL NET包头部
  37.   {
  38.     MYSQL_NET_WRITE_DONE(1);
  39.     return 1;
  40.   }
  41. #ifndef DEBUG_DATA_PACKETS
  42.   DBUG_DUMP("packet_header", buff, NET_HEADER_SIZE);
  43. #endif
  44.   rc= MY_TEST(net_write_buff(net,packet,len));//写入 MYSQL 协议包 的最后数据写入到net buffer中
  45.   MYSQL_NET_WRITE_DONE(rc);
  46.   return rc;
  47. }
2、写入缓存阶段
函数原型
static my_bool net_write_buff(NET *net, const uchar *packet, ulong len)
net:NET结构体指针
packet:MYSQL数据包指针,注意这个指针和上面不同,由于my_net_write分包后这个指针
       也会每次相应的增加到上次写入后的位置
len:如果是拆分的大包就是16M-1,如果是小包(如OK包)就是其相应的长度,还可能是MYSQL NET包头长度
这个过程分为如下情况:
--如果MYSQL NET包大于net buffer的剩余空间
  --将MYSQL NET包一部分调用memcpy写入到剩余空间,完成后调用net_write_packet来进行一次传输,清空net buffer
  --如果MYSQL NET包的剩余部分任然大于net buffer(net-buffer-length)则直接调用net_write_packet进行传输
--如果MYSQL NET包能够存储在net buffer中
  --直接调用memcpy写入到net buffer即可
这里有几个重点
one、MYSQL这样处理实际上讲大的MYSQL NET包和小的MYSQL NET进行区分开,使用net buffer来减小传输的次数,提高
     性能
two、这里也揭示了写入阶段不会出现超过net buffer大小的情况,这和read不同,在写入阶段net buffer只是一个提高
     性能的缓存,如果大于他可以直接调用net_write_packet写入,而read阶段不同net buffer还承载了另外一个重要
     的功能将多个MYSQL NET包合并为一个MYSQL 数据包的功能,所以net buffer的大小小于一个MYSQL数据包的大小会
     直接导致报错如:Got a packet bigger than 'max_allowed_packet' bytes
three、net buffer的设置也就是net-buffer-length参数设置会直接影响到这里,同时这里并不会进行扩充到max_allowed_packet
      的操作,扩充到max_allowed_packet是在read 阶段才会出现,后面会描述
下面我将我写的中文注释加上源码部分一同放出如下:

点击(此处)折叠或打开

  1. static my_bool
  2. net_write_buff(NET *net, const uchar *packet, ulong len)
  3. {
  4.   ulong left_length;
  5.   //下面计算buffer->max_packet的剩余空间
  6.   if (net->compress && net->max_packet > MAX_PACKET_LENGTH)
  7.     left_length= (ulong) (MAX_PACKET_LENGTH - (net->write_pos - net->buff));
  8.   else
  9.     left_length= (ulong) (net->buff_end - net->write_pos);

  10. #ifdef DEBUG_DATA_PACKETS
  11.   DBUG_DUMP("data", packet, len);
  12. #endif
  13.   if (len > left_length) //如果长度大于剩余空间
  14.   {
  15.     if (net->write_pos != net->buff)
  16.     {
  17.       /* Fill up already used packet and write it */
  18.       memcpy(net->write_pos, packet, left_length); //这里使用指针packet后left_lengeh长度来填满整个net buffer
  19.       if (net_write_packet(net, net->buff,
  20.                            (size_t) (net->write_pos - net->buff) + left_length))//写满后,然后调用net_write_packet写到scoket
  21.                            //(size_t) (net->write_pos - net->buff) + left_length 为整个buffer长度
  22.         return 1;
  23.       net->write_pos= net->buff; //这里wirte_pos指针 应该也是移动了到了wirte_pos+left_lengeh
  24.       packet+= left_length; //packet 指针增加
  25.       len-= left_length; //长度相应减少
  26.     }
  27.     if (net->compress)//压缩属性先不考虑,实际是压缩开启使用Zlib进行压缩位于Zlib/compress中
  28.     {
  29.      ..................
  30.     }
  31.     if (len > net->max_packet) //如果填满 net->max_packet 后剩余的数据 还是大于整个net buffer 大小,则跳过缓冲区直接写scoket (重要)
  32.                                 //实际上这里len 最大为16M-1, 如果为16M-1的MYSQL NET包始终会使用直接写入的方法,这点
  33.                                 //和read阶段不同,read阶段会有一个合并mysql net包为MYSQL协议包过程,net buffer有着额外
  34.                                 //的使命
  35.       return net_write_packet(net, packet, len); //直接调用net_write_packet写入
  36.     /* Send out rest of the blocks as full sized blocks */
  37.   }
  38.   memcpy(net->write_pos, packet, len); //如果长度小于 net buffer剩余的空间,只是写入net buffer 即可
  39.   net->write_pos+= len; //这里wirte_pos指针也移动相应的长度
  40.   return 0;
  41. }
3、进行压缩阶段
函数原型
my_bool net_write_packet(NET *net, const uchar *packet, size_t length)
return TRUE on error, FALSE on success.
net:NET结构体指针
packet:这里的packet有2个可能的来源
       --来自net buffer
       --原始的MYSQL 数据包指针偏移后的位置如16M-1的大MYSQL NET包   
lenth:写入长度
这一步实际上是进行一个压缩功能,并没有进行真正的传输,所以我们不进行过多的讨论
下面我将我写的中文注释加上源码部分一同放出如下

点击(此处)折叠或打开

  1. my_bool
  2. net_write_packet(NET *net, const uchar *packet, size_t length) //函数并没有真正传输只是做了一层封装将数据压缩封装在内
  3. //注意这里的数据可能来自net->buffer 可能来自net_flush
  4. {
  5.   my_bool res;
  6.   DBUG_ENTER("net_write_packet");

  7. #if defined(MYSQL_SERVER) && defined(USE_QUERY_CACHE)
  8.   query_cache_insert((char*) packet, length, net->pkt_nr);
  9. #endif

  10.   /* Socket can't be used */
  11.   if (net->error == 2)
  12.     DBUG_RETURN(TRUE);

  13.   net->reading_or_writing= 2; //设置标示表示开始写入

  14. #ifdef HAVE_COMPRESS //参数是否开启
  15.   const bool do_compress= net->compress;
  16.   if (do_compress) //MYSQL自己决定是否开启压缩
  17.   {
  18.     if ((packet= compress_packet(net, packet, &length)) == NULL) //压缩数据 如果内存不足报错
  19.      //{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some other process uses all available memory; if not, you may have to use \'ulimit\' to allow mysqld to use more memory or you can add more swap space" },
  20.      //压缩完成后返回一个malloc的内存空间(压缩后数据的内存首地址)给packet,这个时候packet已经不是形参的packet了,需要释放
  21.     {
  22.       net->error= 2;
  23.       net->last_errno= ER_OUT_OF_RESOURCES;
  24.       /* In the server, allocation failure raises a error. */
  25.       net->reading_or_writing= 0;
  26.       DBUG_RETURN(TRUE);
  27.     }
  28.   }
  29. #endif /* HAVE_COMPRESS */

  30. #ifdef DEBUG_DATA_PACKETS
  31.   DBUG_DUMP("data", packet, length);
  32. #endif

  33.   res= net_write_raw_loop(net, packet, length); //进行真正的底层传输工作

  34. #ifdef HAVE_COMPRESS//参数是否开启
  35.   if (do_compress)//mysql自己决定
  36.     my_free((void *) packet);//如前所述这里需要释放压缩后数据的内存避免泄露
  37. #endif

  38.   net->reading_or_writing= 0;//关闭标示

  39.   DBUG_RETURN(res);
  40. }
4、调用vio虚拟I/O接口进行写入阶段
函数原型
static my_bool net_write_raw_loop(NET *net, const uchar *buf, size_t count)
net:NET结构体指针
packet:这里的buffer有3个可能的来源
       --来自net buffer
       --原始的MYSQL 数据包指针偏移后的位置如16M-1的大MYSQL NET包
       --经过压缩后的上面两种包
lenth:写入长度
              
这个函数调用真正的底层vio_write虚拟IO接口函数进行写入,同时如果遇到EINTR错误会进行如下的操作:
--线程安全客户端如果是EINTR总是重试
--非线程安全客户端或者服务器端如果是EINTR并且达到net->retry_count就跳出循环 
服务端MYSQLD肯定是线程安全的但是为了服务端的性能不可能在EINTR错误下面无限重试
非线程安全的客户端可能全局区数据已经混乱造成I/O错误
此外如果数据没有发送完成或者剩余了一部分会根据错误码判断抛错
--ETIMEOUT错误,如果是则报错Got timeout writing communication packets
--否则Got an error writing communication packets
注意这里的ETIMEOUT就是根据参数net-wirte-timeout设置的SOCKET超时设置
下面我将我写的中文注释加上源码部分一同放出如下

点击(此处)折叠或打开

  1. static my_bool
  2. net_write_raw_loop(NET *net, const uchar *buf, size_t count)
  3. {
  4.   unsigned int retry_count= 0;

  5.   while (count)
  6.   {
  7.     size_t sentcnt= vio_write(net->vio, buf, count);//成功放回写入字节数量 失败返回-1 这里真正写max_packet buffer包/mysql NET包>max_packet buffer的数据到socket

  8.     /* VIO_SOCKET_ERROR (-1) indicates an error. */
  9.     if (sentcnt == VIO_SOCKET_ERROR) //如果写操作遇到错误下面是异常处理 总体来说就是晕倒的是EINTR就做重试,否则直接退出发送数据循环进入异常处理if语句
  10.     {
  11.       /* A recoverable I/O error occurred? */
  12.       if (net_should_retry(net, &retry_count))
  13.      //1、线程安全客户端如果是EINTR总是重试
  14.          //2、非线程安全客户端或者服务器端如果是EINTR并且达到net->retry_count就跳出循环
  15.          //服务端MYSQLD肯定是线程安全的但是为了服务端的性能不可能在EINTR错误下面无线重试
  16.          //非线程安全的客户端可能全局区数据已经混乱造成I/O错误
  17.         continue;
  18.       else
  19.         break;
  20.     }
  21.     //下面是正常情况下
  22.     count-= sentcnt; //总数-发送的
  23.     buf+= sentcnt; //指针当然也就相应增加
  24.     update_statistics(thd_increment_bytes_sent(sentcnt));
  25.   }

  26.   /* On failure, propagate the error code. */
  27.   if (count) //如果count>0 也就是还有未发送的数据
  28.   {
  29.     /* Socket should be closed. */
  30.     net->error= 2;

  31.     /* Interrupted by a timeout? */
  32.     if (vio_was_timeout(net->vio)) //是否为ETIMEOUT错误,如果是则报错Got timeout writing communication packets
  33.       net->last_errno= ER_NET_WRITE_INTERRUPTED;
  34.     else //否则报错Got an error writing communication packets
  35.       net->last_errno= ER_NET_ERROR_ON_WRITE;
  36. #ifdef MYSQL_SERVER
  37.     my_error(net->last_errno, MYF(0));
  38. #endif
  39.   }
到这里MYSQL层次对MYSQL数据包到MYSQL NET包的转换和传输准备已经完成接下来就是通过
底层TCP/IP、以太网等协议进行封装然后通过socket传输了。下面一张图对上面的说明
进行一个汇总,但是图中有些细节并没有表示出来还是最好通过源码备注了解
MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析
三、MYSQL数据包的读取scoket阶段
1、合并多个MYSQL NET包为一个MYSQL 数据包
函数原型
ulong my_net_read(NET *net)
net:NET结构体指针,一个MYSQL 数据包存储在一个NET结构体的buffer所指向的内存
     空间中
返回值为读取到的实际一个MYSQL 数据包的长度,不包MYSQL NET包的包头字节数
这个函数调用net_read_packet来读取一个MYSQL 数据包,如果为大的MYSQL 数据包完成解压
合并操作源码注释中将大的MYSQL 数据包分为多个MYSQL NET包叫做packet of a multi-packet
下面我将我写的中文注释加上源码部分一同放出如下,注意我忽略了解压操作来降低学习的难度

点击(此处)折叠或打开

  1. ulong
  2. my_net_read(NET *net) //
  3. {
  4.   size_t len, complen;

  5.   MYSQL_NET_READ_START();

  6. #ifdef HAVE_COMPRESS
  7.   if (!net->compress)//如果没有压缩
  8.   {
  9. #endif
  10.     len= net_read_packet(net, &complen); //读取一个MYSQL NET包返回实际长度在len变量中,如果有压缩
  11.                                          //压缩前长度保存在complen变量中 这个函数还有一个重要
  12.                                          //功能就是扩张max_packet buffer的长度直到max_packet_size
  13.                                          //限制,如果不能扩张就报错,这里也指出了一个现实每个MYSQL
  14.                                          //协议包必须放到一个max_packet buffer中,这也是很多packet
  15.                                          //buffer 不够报错的根源
  16.     if (len == MAX_PACKET_LENGTH) //是否为一个满包及大小为16M-1大小
  17.     {
  18.       /* First packet of a multi-packet. Concatenate the packets */
  19.       ulong save_pos = net->where_b;
  20.       size_t total_length= 0;
  21.       do //这里这个循环完成多个mysql net包合并为一个MYSQL 协议包的动作
  22.       {
  23.         net->where_b += len; //读取偏移量不断增加
  24.         total_length += len; //总长度不断增加
  25.         len= net_read_packet(net, &complen); //读取动作
  26.       } while (len == MAX_PACKET_LENGTH);
  27.       if (len != packet_error) //packet_err被定义为 ~((unsigned long)(0))
  28.         len+= total_length; //这里要注意MYSQL协议包分为多个mysql net包后结束包的长度是0,所以也不会增加len
  29.       net->where_b = save_pos;
  30.     }
  31.     net->read_pos = net->buff + net->where_b;
  32.     if (len != packet_error)
  33.       net->read_pos[len]=0;        /* Safeguard for mysql_use_result */
  34.     MYSQL_NET_READ_DONE(0, len);
  35.     return len; //返回读取的总长度
  36. #ifdef HAVE_COMPRESS
  37.   }
  38.   else //不考虑压缩
  39.   {.....



2、获得MYSQL NET包长度阶段
函数原型
static ulong net_read_packet(NET *net, size_t *complen)
net:NET结构体指针,一个MYSQL 数据包存储在一个NET结构体的buffer所指向的内存
     空间中
complen:为输出形参,输出的是可能的压缩前的数据长度
返回值为实际读取的MYSQL NET包的长度大小( Read the packet data (payload))
失败返回packet_error
本函数主要是为了获得MYSQL NET包的长度而封装的一层函数,net_read_packet_header为获得MYSQL
NET包长度函数,并且本函数计算多个MYSQL NET包为一个MYSQL 数据包后需要的内存空间是否够用
如果不够用分为如下操作
1、如果扩充后NET BUFFER的大小不会超过参数max_packet_size设置的大小,则调用net_realloc()扩充成功
2、如果扩充后NET BUFFER的大小超过参数max_packet_size设置的大小,则调用net_realloc扩充失败报错
{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" }
   这也是非常常见的一个错误
当然如果内存不足都会引起如下错误
{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some other process uses all 
available memory; if not, you may have to use \'ulimit\' to allow mysqld to use more memory 
or you can add more swap space" }
这里不对net_realloc函数和net_read_packet_header函数进行详细分析,如果有兴趣自行研究
下面我将我写的中文注释加上源码部分一同放出如下

点击(此处)折叠或打开

  1. static ulong net_read_packet(NET *net, size_t *complen)
  2. {
  3.   size_t pkt_len, pkt_data_len;

  4.   *complen= 0;

  5.   net->reading_or_writing= 1; //将读写标示设置为1,表示读取开始

  6.   /* Retrieve packet length and number. */
  7.   if (net_read_packet_header(net)) //读取一个MYSQL net包的长度和MYSQL NET sequence
  8.     goto error;

  9.   net->compress_pkt_nr= net->pkt_nr;

  10. #ifdef HAVE_COMPRESS
  11.   if (net->compress)//先不考虑压缩
  12.   {
  13.    .......
  14.   }
  15. #endif

  16.   /* The length of the packet that follows. */
  17.   pkt_len= uint3korr(net->buff+net->where_b);//获得本MYSQL NET包的长度

  18.   /* End of big multi-packet. */
  19.   if (!pkt_len) //判断是否为mysql数据包分包后的结束包
  20.     goto end;

  21.   pkt_data_len = max(pkt_len, *complen) + net->where_b; //获得读取此MYSQL NET包后需要的内存空间,也就是整个NET BUFFER需要多大,需要判断如果是
  22.                                                         //是经过压缩的需要的空间是数据压缩前的长度

  23.   /* Expand packet buffer if necessary. */
  24.   if ((pkt_data_len >= net->max_packet) && net_realloc(net, pkt_data_len)) //这里实际的判断net buffer是否够用,如果不够用调用realloc进行内存扩充,
  25.                                                                            //在realloc中判断是否超过max_packet_size的设置
  26.     goto error;

  27.   /* Read the packet data (payload). */
  28.   if (net_read_raw_loop(net, pkt_len)) //开始进行实际的读取操作
  29.     goto error;

  30. end:
  31.   net->reading_or_writing= 0; //将读写标示设置为0,表示读取结束
  32.   return pkt_len; //函数返回本次读取

  33. error: //出错返回值
  34.   net->reading_or_writing= 0;
  35.   return packet_error;
  36. }

3、调用vio虚拟I/O接口进行读取阶段
函数原型
static my_bool net_read_raw_loop(NET *net, size_t count)
net:NET结构体指针,一个MYSQL 数据包存储在一个NET结构体的buffer所指向的内存
    空间中
count:本次读取的MYSQL NET包有多大,如果是压缩过的MYSQL NET包不是压缩前的数据而是压缩后的MYSQL NET包长度
(@return TRUE on error, FALSE on success.)
 成功返回FALSE、失败返回TURE

点击(此处)折叠或打开

  1. static my_bool net_read_raw_loop(NET *net, size_t count)
  2. {
  3.   bool eof= false;
  4.   unsigned int retry_count= 0;
  5.   uchar *buf= net->buff + net->where_b;

  6.   while (count)
  7.   {
  8.     size_t recvcnt= vio_read(net->vio, buf, count); //如果写操作遇到错误下面是异常处理 总体来说就是晕倒的是EINTR就做重试,否则直接退出发送数据循环进入异常处理if语句

  9.     /* VIO_SOCKET_ERROR (-1) indicates an error. */
  10.     if (recvcnt == VIO_SOCKET_ERROR) //
  11.     {
  12.       /* A recoverable I/O error occurred? */
  13.       if (net_should_retry(net, &retry_count))
  14.      //1、线程安全客户端如果是EINTR总是重试
  15.      //2、非线程安全客户端或者服务器端如果是EINTR并且达到net->retry_count就跳出循环
  16.      //服务端MYSQLD肯定是线程安全的但是为了服务端的性能不可能在EINTR错误下面无线重试
  17.      //非线程安全的客户端可能全局区数据已经混乱造成I/O错误
  18.          
  19.         continue;
  20.       else
  21.         break;
  22.     }
  23.     /* Zero indicates end of file. */
  24.     else if (!recvcnt) //recv半连接状态? LINUX man recv:The return values will be 0 when the peer has performed an orderly shutdown
  25.     {
  26.       eof= true;
  27.       break;
  28.     }

  29.     count-= recvcnt;
  30.     buf+= recvcnt;
  31.     update_statistics(thd_increment_bytes_received(recvcnt));
  32.   }

  33.   /* On failure, propagate the error code. */
  34.   if (count)//如果count>0 也就是没有读取到预期的数据
  35.   {
  36.     /* Socket should be closed. */
  37.     net->error= 2;

  38.     /* Interrupted by a timeout? */
  39.     if (!eof && vio_was_timeout(net->vio)) //是否为ETIMEOUT错误,如果是则报错Got timeout reading communication packets
  40.       net->last_errno= ER_NET_READ_INTERRUPTED;
  41.     else
  42.       net->last_errno= ER_NET_READ_ERROR;//否则报错Got an error reading communication packets

  43. #ifdef MYSQL_SERVER
  44.     my_error(net->last_errno, MYF(0));
  45. #endif
  46.   }

  47.   return MY_TEST(count);
  48. }
这个函数和前面写阶段的时候差不多,调用底层vio虚拟IO接口进行实际的读取
也会出现如果数据没有发送完成或者剩余了一部分会根据错误码判断抛错
--ETIMEOUT错误,如果是则报错Got timeout reading communication packets
--否则Got an error reading communication packets
注意这里的ETIMEOUT就是根据参数net-read-timeout设置的SOCKET超时设置
到这里MYSQL层次对从读取到MYSQL NET包到MYSQL数据包的转换合并过程就完成了,读取工作
成接下来就是通过底层TCP/IP、以太网等协议进行封装然后通过socket读取了。下面一张图对上面的说明
进行一个汇总,但是图中有些细节并没有表示出来还是最好通过源码备注了解

MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析




四、使用TCPDUMP抓取MYSQL NET包解析实例

虽然本文不解析MYSQL 协议但是通过tcpdump抓包来进行一下简单的客户端和服务端的连接建立后的交互情况
使用命令
tcpdump tcp port 3307  -X >log.log

1、select 模型 
客户端:select * from test.test;(命令包)
服务端:返回查询结果(结果集包)

id1  id2
1     1
2     2
3     3
4     4
5     5
6    6
7    7

   1311 12:48:29.632228 IP bogon.61796 > testmy.opsession-prxy: Flags [P.], seq 53:82, ack 19791, win 16142, length 29
   1312         0x0000:  4500 0045 0dbe 4000 4006 2f45 c0a8 be01  E..E..@.@./E....
   1313         0x0010:  c0a8 be5d f164 0ceb 097a 4b52 7e10 8b88  ...].d...zKR~...
   1314         0x0020:  5018 3f0e 9de8 0000 1900 0000 0373 656c  P.?..........sel
   1315         0x0030:  6563 7420 2a20 6672 6f6d 2074 6573 742e  ect.*.from.test.
   1316         0x0040:  7465 7374 3b                             test;
   1317 12:48:29.632651 IP testmy.opsession-prxy > bogon.61796: Flags [P.], seq 19791:19956, ack 82, win 131, length 165
   1318         0x0000:  4500 00cd f754 4000 4006 4526 c0a8 be5d  E....T@.@.E&...]
   1319         0x0010:  c0a8 be01 0ceb f164 7e10 8b88 097a 4b6f  .......d~....zKo
   1320         0x0020:  5018 0083 fe6f 0000 0100 0001 0226 0000  P....o.......&..
   1321         0x0030:  0203 6465 6604 7465 7374 0474 6573 7404  ..def.test.test.
   1322         0x0040:  7465 7374 0269 6402 6964 0c3f 000b 0000  test.id.id.?....
   1323         0x0050:  0003 0350 0000 0028 0000 0303 6465 6604  ...P...(....def.
   1324         0x0060:  7465 7374 0474 6573 7404 7465 7374 0369  test.test.test.i
   1325         0x0070:  6432 0369 6432 0c3f 000b 0000 0003 0000  d2.id2.?........
   1326         0x0080:  0000 0005 0000 04fe 0000 2200 0400 0005  ..........".....
   1327         0x0090:  0131 0131 0400 0006 0132 0132 0400 0007  .1.1.....2.2....
   1328         0x00a0:  0133 0133 0400 0008 0134 0134 0400 0009  .3.3.....4.4....
   1329         0x00b0:  0135 0135 0400 000a 0136 0136 0400 000b  .5.5.....6.6....
   1330         0x00c0:  0137 0137 0500 000c fe00 0022 00         .7.7.......".

客户端:
IP bogon.61796 > testmy.opsession-prxy 客户端端口61796端口发送给3307端口的TCP包
   1314         0x0020:                      1900 0000 0373 656c             .sel
   1315         0x0030:  6563 7420 2a20 6672 6f6d 2074 6573 742e  ect.*.from.test.
   1316         0x0040:  7465 7374 3b                             test;

我们只看这一段
1900 00  MYSQL NET包长度小端显示为0X19=25 当然数一数后面从0373开始一共25个字节
00 MYSQL NET序号
03 MYSQL 协议命令包的第一个自己0X03代表是COM_QUERY指令
后面的没什么说的携带就是select * from test.test;的strings模式

服务端:
IP testmy.opsession-prxy > bogon.61796 服务端端端口3307端口发送给客户端端口61796的TCP包
服务端我们将列属性包的分析留给读者这里从列属性过后的EOF包开始
05 0000:小端显示为0X05=05
04:这个MYSQL NET包在整个结果集包中的SEQ
fe:总是0XFE 
0000:警告数量
2200:状态标示
0400 00:小端显示为0X05=04MYSQL NET长度
05:这个MYSQL NET包在整个结果集包中的SEQ
0131:01为返回结果集第一个列数据的长度 31就是实际数据1
0131:02为返回结果集第一个列数据的长度 31就是实际数据1

以此类推可以看到全部的结果,这里也展示这样一个事实、因为一行记录为一个结果集行包,那么
当这行数据很长而导致超过客户端(如MYSQL MYSQLDUMP)max_packet_size大小的时候会报错,这点
在源码分析中已经分析这点也要非常注意

2、insert模型
客户端:insert into test.test values(100,100),(101,102),(103,103),(104,104),
        (105,105),(106,107),(108,109),(111,123);(命令包)
服务端:返回受影响的行数等(OK包)

[SQL] insert into test.test values(100,100),(101,102),(103,103),(104,104),
(105,105),(106,107),(108,109),(111,123);
受影响的行: 8
时间: 0.027s

13:07:39.121552 IP bogon.61796 > testmy.opsession-prxy: Flags [P.], seq 220:335, ack 39809, win 16140, length 115
        0x0000:  4500 009b 100f 4000 4006 2c9e c0a8 be01  E.....@.@.,.....
        0x0010:  c0a8 be5d f164 0ceb 097a 4de5 7e11 54c6  ...].d...zM.~.T.
        0x0020:  5018 3f0c 9890 0000 6f00 0000 0369 6e73  P.?.....o....ins
        0x0030:  6572 7420 696e 746f 2074 6573 742e 7465  ert.into.test.te
        0x0040:  7374 2076 616c 7565 7328 3130 302c 3130  st.values(100,10
        0x0050:  3029 2c28 3130 312c 3130 3229 2c28 3130  0),(101,102),(10
        0x0060:  332c 3130 3329 2c28 3130 342c 3130 3429  3,103),(104,104)
        0x0070:  2c0d 0a28 3130 352c 3130 3529 2c28 3130  ,..(105,105),(10
        0x0080:  362c 3130 3729 2c28 3130 382c 3130 3929  6,107),(108,109)
        0x0090:  2c28 3131 312c 3132 3329 3b              ,(111,123);
13:07:39.147808 IP testmy.opsession-prxy > bogon.61796: Flags [P.], seq 39809:39859, ack 335, win 140, length 50
        0x0000:  4500 005a f77f 4000 4006 456e c0a8 be5d  E..Z..@.@.En...]
        0x0010:  c0a8 be01 0ceb f164 7e11 54c6 097a 4e58  .......d~.T..zNX
        0x0020:  5018 008c fdfc 0000 2e00 0001 0008 0002  P...............
        0x0030:  0000 0026 5265 636f 7264 733a 2038 2020  ...&Records:.8..
        0x0040:  4475 706c 6963 6174 6573 3a20 3020 2057  Duplicates:.0..W
        0x0050:  6172 6e69 6e67 733a 2030                 arnings:.0
客户端:
 bogon.61796 > testmy.opsession-prxy 客户端端口61796端口发送给3307端口的TCP包
6f00 0000 0369 6e73从这里开始

6f00 00:MYSQL NET包长度小端显示为0X6f=111 当然数一数后面从0369开始一共111个字节
00:MYSQL NET序号
03:MYSQL 协议命令包的第一个自己0X03代表是COM_QUERY指令 这里query不止代表SELECT
   代表了INSERT\UPDATE\DELETE\SELECT
后面没什么说的就是insert into test.test values(100,100),(101,102),(103,103),(104,104),
                  (105,105),(106,107),(108,109),(111,123);
的strings acsii编码
这里也展示了这样一个事实如果客户端source导入语句的时候那么每一个INSERT语句是一个指令包
如果这个指令包大于了服务端max_packet_size的大小就会报错、或者其他错误这点需要非常注意

服务端:
IP testmy.opsession-prxy > bogon.61796 服务端端端口3307端口发送给客户端端口61796的TCP包
我们从2e00 0001 0008 0002开始解析
2e00 00 MYSQL NET包长度小端显示为0X2e=46
01 MYSQL NET序号
00 总为0
08 影响行数
00 插入ID
02 00 服务器状态
00 00 警告数量
最后就是消息的ASCII编码没什么好说的了

五、本文中提到的一些错误
1、{ "ER_OUT_OF_RESOURCES", 1041, "Out of memory; check if mysqld or some 
      other process uses all available memory; if not, you may have to use 
      \'ulimit\' to allow mysqld to use more memory or you can add more swap space" },
内存不足分配内存失败
2、{ "ER_NET_PACKET_TOO_LARGE", 1153, "Got a packet bigger than \'max_allowed_packet\' bytes" }
读取阶段由于max_allowed_packet大小的限制net buffer不能进行扩充,一个MYSQL数据包必须存放到一个
net buffer中。
3、{ "ER_NET_WRITE_INTERRUPTED", 1161, "Got timeout writing communication packets" }
   { "ER_NET_READ_INTERRUPTED", 1159, "Got timeout reading communication packets" } 
   由于net-wirte-timeout,net-read-timeout参数指定,在返回ETIMEDOUT错误前保持连接活跃的
   秒数

六、错误演示
MYSQLD服务端
MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析

MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析
MYSQL 客户端程序官方文档给出的
? max_allowed_packet
The maximum size of the buffer for client/server communication. The default is 16MB, the maximum is
1GB.
? net_buffer_length
The buffer size for TCP/IP and socket communication. (Default value is 16KB.)
源码中定义 mysql.cc
{"max_allowed_packet", OPT_MAX_ALLOWED_PACKET,
   "The maximum packet length to send to or receive from server.",
   &opt_max_allowed_packet, &opt_max_allowed_packet, 0,
   GET_ULONG, REQUIRED_ARG, 16 *1024L*1024L, 4096,
   (longlong) 2*1024L*1024L*1024L, MALLOC_OVERHEAD, 1024, 0},
{"net_buffer_length", OPT_NET_BUFFER_LENGTH,
   "The buffer size for TCP/IP and socket communication.",
   &opt_net_buffer_length, &opt_net_buffer_length, 0, GET_ULONG,
   REQUIRED_ARG, 16384, 1024, 512*1024*1024L, MALLOC_OVERHEAD, 1024, 0},


MYSQLDUMP 客户端程序官方文档给出的
? max_allowed_packet
The maximum size of the buffer for client/server communication. The default is 24MB, the maximum is
1GB.
? net_buffer_length
The initial size of the buffer for client/server communication. When creating multiple-row INSERT
statements (as with the --extended-insert or --opt option), mysqldump creates rows up
to net_buffer_length bytes long. If you increase this variable, ensure that the MySQL server
net_buffer_length system variable has a value at least this large.

源码中定义mysqldump.c
 {"max_allowed_packet", OPT_MAX_ALLOWED_PACKET, 
   "The maximum packet length to send to or receive from server.",
    &opt_max_allowed_packet, &opt_max_allowed_packet, 0,
    GET_ULONG, REQUIRED_ARG, 24*1024*1024, 4096,
   (longlong) 2L*1024L*1024L*1024L, MALLOC_OVERHEAD, 1024, 0},
  {"net_buffer_length", OPT_NET_BUFFER_LENGTH, 
   "The buffer size for TCP/IP and socket communication.",
    &opt_net_buffer_length, &opt_net_buffer_length, 0,
    GET_ULONG, REQUIRED_ARG, 1024*1024L-1025, 4096, 16*1024L*1024L,
   MALLOC_OVERHEAD-1024, 1024, 0},

我们可以看不到不管是MYSQLDUMP还是MYSQL客户端程序都有这样两个命令行参数,他的功能已经在上面源码解析中
进行了说明,在MYSQLDUMP中net_buffer_length还有一个额外注意的地方,也就是当多个结果集行包进入NET BUFFER
后需要进行输出,这里看起来他的输出就是以NET BUFFER为单位的官方文档也有说明,换句话说,当使用multiple-row 
INSERT 方式的时候一条语句的长度由他控制,注意在导入的时候服务端的max_allowed_packet一定要大于这个值,因为
导入的时候一个INSERT语句就是客户端到服务端的一个命令包,这个MYSQL服务端读取这个数据命令包必须保存在一个
NET BUFFER中

我们来验证一下MYSQLDUMP的这种说法
 /mysqldata/mysql5.7/bin/mysqldump  --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --net-buffer-length=4k test testpack2>log10.log
 /mysqldata/mysql5.7/bin/mysqldump  --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --net-buffer-length=16k test testpack2>log11.log
cat log10.log |grep INSERT  |head -n 1 >test10.log
cat log11.log |grep INSERT  |head -n 1 >test11.log
[root@testmy ~]# du -hs test1*
4.0K    test10.log
16K     test11.log

确实如我们期望了一个multiple-row 由于NET BUFFER的变动而改变了大小。

接下来我们来模拟这种服务端和客户端一个mysql数据包大于max_allowed_packet报错的情况

1、服务端报错
使用
/mysqldata/mysql5.7/bin/mysqldump  --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --net-buffer-length=5m test testpack2>log11.log

这样会生成一个大约5m的命令包
然后在服务端进行source
默认的服务端max_allowed_packet为4M,net-buffer-length 为16k
mysql> show variables like '%max_allowed_packet%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| max_allowed_packet       | 4194304    |
| slave_max_allowed_packet | 1073741824 |
+--------------------------+------------+
mysql> source /root/test11.log
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id:    63
Current database: test
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id:    64
Current database: test

ERROR 2006 (HY000): MySQL server has gone away
可以看到服务端报错了

2017-05-07T07:14:15.957486Z 58 [Note] Aborted connection 58 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)
2017-05-07T07:14:16.020153Z 63 [Note] Aborted connection 63 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)
2017-05-07T07:14:16.080146Z 64 [Note] Aborted connection 64 to db: 'test' user: 'root' host: 'localhost' (Got a packet bigger than 'max_allowed_packet' bytes)

原因在前面已经做了详细的描述,我们来修改max_allowed_packet为6M
再次source
mysql> show variables like '%max_allowed_packet%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| max_allowed_packet       | 5999616    |
| slave_max_allowed_packet | 1073741824 |
+--------------------------+------------+
mysql> use test
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> source /root/log11.log
Query OK, 349522 rows affected (3.49 sec)
Records: 349522  Duplicates: 0  Warnings: 0
Query OK, 174766 rows affected (1.77 sec)
Records: 174766  Duplicates: 0  Warnings: 0

2、客户端MYSQL

为了方便测试我构造可一行2M左右数据的行,我们知道一行数据就是一个MYSQL数据包
这里模拟了另外一个错误
mysql> insert into testpack2 values(100,repeat('a',7000000));
ERROR 1301 (HY000): Result of repeat() was larger than max_allowed_packet (5999616) - truncated

报错明显大于了我们服务端设置的max_allowed_packet (5999616),而这个命令行包虽然使用了repeat但是repeat
的个数超过了服务端的max_allowed_packet设置,导致报错.

我们改为2M左右
mysql> insert into testpack2 values(100,repeat('a',2000000));
Query OK, 1 row affected (0.06 sec)
没有问题
[root@testmy data]# /mysqldata/mysql5.7/bin/mysql --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --max-allowed-packet=1m -e 'select * from test.testpack2 where id=100' >log.log
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2020 (HY000) at line 1: Got packet bigger than 'max_allowed_packet' bytes
我们看到了预期的报错
修改--max-allowed-packet为2M
[root@testmy data]# /mysqldata/mysql5.7/bin/mysql --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --max-allowed-packet=2m -e 'select * from test.testpack2 where id=100' >log.log
mysql: [Warning] Using a password on the command line interface can be insecure.
[root@testmy data]# 
报错消失

3、客户端MYSQLDUMP
沿用上面的数据,这里出现一样的结果 不需要过多描述了
[root@testmy data]# /mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --max-allowed-packet=1m test testpack2>log2.log
mysqldump: [Warning] Using a password on the command line interface can be insecure.
Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events. 
mysqldump: Error 2020: Got packet bigger than 'max_allowed_packet' bytes when dumping table `testpack2` at row: 524288
[root@testmy data]# ^C
[root@testmy data]# /mysqldata/mysql5.7/bin/mysqldump --socket=/mysqldata/mysql5.7/mysqld3307.sock -uroot -pgelc123  --max-allowed-packet=2m test testpack2>log2.log
mysqldump: [Warning] Using a password on the command line interface can be insecure.
Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events. 
[root@testmy data]# 
这3个测试分别用来证明了在读取阶段不管是客户端还是服务端都需要将一个MYSQL数据包在NET BUFFER中进行合并,如果一个MYSQL数据包大于了--max-allowed-packet设置就会抛错,而写入阶段当然不需要,源码解析的时候已经做了详细解析。


至此整个文章从预备知识到源码解析到抓包解析到错误证明都进行了详细的描述,耗费了我大约2天半的时间基本就是整个周末多一点,因为怕上班时间少有时间研究,所以加紧完成
其中肯定有一些不严谨或者错误的地方特别是源码解析因为没有过多的资料而且要了解设计者的思想特别困难,还有就是涉及到底层SOCKET通信的地方,因为没有过多的去剖析所以
有的地方一笔带过,如果日后进行详细的分析会在以文章的方式给出。


作者微信:

               MYSQL CLENT/SERVER数据包传输及net packet buffer作用解析


推荐阅读:
  1. vscode编译c控制台输出乱码怎么办
  2. vscode中eslint插件不起作用的解决方法

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

clent 作用 包传输

上一篇:人工智能属于什么专业?

下一篇:css颜色单位表示法

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》