ret= mysql_socket_setsockopt(vio->mysql_socket, SOL_SOCKET, optname,optval, sizeof(timeout));
my_bool my_net_write(NET *net,const uchar *packet, size_t len)//将长度为packet的数据写入到net
->buffer
{
uchar buff[NET_HEADER_SIZE];// lenth 3 seq 1 4bytes
int rc;
if(unlikely(!net->vio))/* nowhere to write */
return 0;
MYSQL_NET_WRITE_START(len);
DBUG_EXECUTE_IF("simulate_net_write_failure",{
my_error(ER_NET_ERROR_ON_WRITE, MYF(0));
return 1;
};
);
/*
Big packets are handled by splitting them in packets of MAX_PACKET_LENGTH
length. The last packet is always a packet that is< MAX_PACKET_LENGTH.
(The last packet may even have a length of 0)
*/
while(len>= MAX_PACKET_LENGTH)//
如果写入MYSQL 协议包的长度大于了最大mysq NET包 就分为多个MYSQL NET包
{
const ulong z_size = MAX_PACKET_LENGTH;// 16M-
1 计为包的长度
int3store(buff, z_size);//
将长度写入到栈 buff中
buff[3]=(uchar) net->pkt_nr++;//
将buffer中的 seq
+1 当然 pkt_nr 序列也
+1
if(net_write_buff(net, buff, NET_HEADER_SIZE) //
写入MYSQL NET包头部
net_write_buff(net, packet, z_size))//
将长度为z_size的进行拆分的MYSQL 协议包一分部写入到net buffer中
{
MYSQL_NET_WRITE_DONE(1);
return 1;
}
packet += z_size;//
将packet的指针 加上z_size的大小 其实也就是16M
-1
len-= z_size;//
当然len 也就相应的减少z_size 其实也就是16M
-1
}
//
如果不是大的MYSQL 协议包或者是MYSQL协议包的最后一部分则执行下面代码
/* Write last packet */
int3store(buff,len);//
将最后的长度计入buffer 头3字节
buff[3]=(uchar) net->pkt_nr++;//
当然序号继续
+1
if(net_write_buff(net, buff, NET_HEADER_SIZE))//
写入MYSQL NET包头部
{
MYSQL_NET_WRITE_DONE(1);
return 1;
}
#ifndef DEBUG_DATA_PACKETS
DBUG_DUMP("packet_header", buff, NET_HEADER_SIZE);
#endif
rc= MY_TEST(net_write_buff(net,packet,len));//
写入 MYSQL 协议包 的最后数据写入到net buffer中
MYSQL_NET_WRITE_DONE(rc);
return rc;
}
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 阶段才会出现,后面会描述
下面我将我写的中文注释加上源码部分一同放出如下:
点击(此处
)折叠或打开
static my_bool
net_write_buff(NET *net,const uchar *packet, ulong len)
{
ulong left_length;
//
下面计算buffer
->max_packet的剩余空间
if(net->compress && net->max_packet > MAX_PACKET_LENGTH)
left_length=(ulong)(MAX_PACKET_LENGTH -(net->write_pos - net->buff));
else
left_length=(ulong)(net->buff_end - net->write_pos);
#ifdef DEBUG_DATA_PACKETS
DBUG_DUMP("data", packet,len);
#endif
if(len> left_length)//
如果长度大于剩余空间
{
if(net->write_pos != net->buff)
{
/* Fill up already used packet and write it */
memcpy(net->write_pos, packet, left_length);//
这里使用指针packet后left_lengeh长度来填满整个net buffer
if(net_write_packet(net, net->buff,
(size_t)(net->write_pos - net->buff)+ left_length))//
写满后,然后调用net_write_packet写到scoket
//(size_t)(net->write_pos - net->buff)+
left_length 为整个buffer长度
return 1;
net->write_pos= net->buff;//
这里wirte_pos指针 应该也是移动了到了wirte_pos
+left_lengeh
packet+= left_length;//
packet 指针增加
len-= left_length;//
长度相应减少
}
if(net->compress)//
压缩属性先不考虑,实际是压缩开启使用Zlib进行压缩位于Zlib/compress中
{
..................
}
if(len> net->max_packet)//
如果填满 net
->max_packet 后剩余的数据 还是大于整个net buffer 大小,则跳过缓冲区直接写scoket
(重要
)
//
实际上这里len 最大为16M
-1, 如果为16M
-1的MYSQL NET包始终会使用直接写入的方法,这点
//
和read阶段不同,read阶段会有一个合并mysql net包为MYSQL协议包过程,net buffer有着额外
//
的使命
return net_write_packet(net, packet,len);//
直接调用net_write_packet写入
/* Send out rest of the blocks as full sized blocks */
}
memcpy(net->write_pos, packet,len);//
如果长度小于 net buffer剩余的空间,只是写入net buffer 即可
net->write_pos+=len; //这里wirte_pos指针也移动相应的长度
return 0;
}
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:写入长度
这一步实际上是进行一个压缩功能,并没有进行真正的传输,所以我们不进行过多的讨论
下面我将我写的中文注释加上源码部分一同放出如下
点击(此处
)折叠或打开
my_bool
net_write_packet(NET *net,const uchar *packet, size_t length)//
函数并没有真正传输只是做了一层封装将数据压缩封装在内
//
注意这里的数据可能来自net
->buffer 可能来自net_flush
{
my_bool res;
DBUG_ENTER("net_write_packet");
#if defined(MYSQL_SERVER)&& defined(USE_QUERY_CACHE)
query_cache_insert((char*) packet, length, net->pkt_nr);
#endif
/* Socket can't be used */
if(net->error== 2)
DBUG_RETURN(TRUE);
net->reading_or_writing= 2; //设置标示表示开始写入
#ifdef HAVE_COMPRESS
//参数是否开启
const bool do_compress= net->compress;
if(do_compress)//
MYSQL自己决定是否开启压缩
{
if((packet= compress_packet(net, packet,&length))==NULL)//
压缩数据 如果内存不足报错
//{"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"},
//
压缩完成后返回一个malloc的内存空间
(压缩后数据的内存首地址
)给packet,这个时候packet已经不是形参的packet了,需要释放
{
net->error= 2;
net->last_errno= ER_OUT_OF_RESOURCES;
/*In the server, allocation failure raises a error.*/
net->reading_or_writing= 0;
DBUG_RETURN(TRUE);
}
}
#endif /* HAVE_COMPRESS */
#ifdef DEBUG_DATA_PACKETS
DBUG_DUMP("data", packet, length);
#endif
res= net_write_raw_loop(net, packet, length);//
进行真正的底层传输工作
#ifdef HAVE_COMPRESS//
参数是否开启
if(do_compress)//
mysql自己决定
my_free((void *) packet);//
如前所述这里需要释放压缩后数据的内存避免泄露
#endif
net->reading_or_writing= 0;//关闭标示
DBUG_RETURN(res);
}
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超时设置
下面我将我写的中文注释加上源码部分一同放出如下
点击(此处
)折叠或打开
static my_bool
net_write_raw_loop(NET *net,const uchar *buf, size_t count)
{
unsigned int retry_count= 0;
while(count)
{
size_t sentcnt= vio_write(net->vio, buf, count);//
成功放回写入字节数量 失败返回
-1 这里真正写max_packet buffer包
/mysql NET包
>max_packet buffer的数据到socket
/* VIO_SOCKET_ERROR (-1) indicates an error.*/
if(sentcnt == VIO_SOCKET_ERROR)//
如果写操作遇到错误下面是异常处理 总体来说就是晕倒的是EINTR就做重试,否则直接退出发送数据循环进入异常处理if语句
{
/* A recoverable I/O error occurred?*/
if(net_should_retry(net,&retry_count))
//
1、线程安全客户端如果是EINTR总是重试
//
2、非线程安全客户端或者服务器端如果是EINTR并且达到net
->retry_count就跳出循环
//
服务端MYSQLD肯定是线程安全的但是为了服务端的性能不可能在EINTR错误下面无线重试
//
非线程安全的客户端可能全局区数据已经混乱造成I
/O错误
continue;
else
break;
}
//
下面是正常情况下
count-= sentcnt;//
总数
-发送的
buf+= sentcnt;//
指针当然也就相应增加
update_statistics(thd_increment_bytes_sent(sentcnt));
}
/*On failure, propagate the error code.*/
if(count)//
如果count
>0 也就是还有未发送的数据
{
/* Socket should be closed.*/
net->error= 2;
/* Interrupted by a timeout?*/
if(vio_was_timeout(net->vio))//
是否为ETIMEOUT错误,如果是则报错Got timeout writing communication packets
net->last_errno= ER_NET_WRITE_INTERRUPTED;
else//
否则报错Got an
error writing communication packets
net->last_errno= ER_NET_ERROR_ON_WRITE;
#ifdef MYSQL_SERVER
my_error(net->last_errno, MYF(0));
#endif
}
到这里MYSQL层次对MYSQL数据包到MYSQL NET包的转换和传输准备已经完成接下来就是通过
底层TCP/IP、以太网等协议进行封装然后通过socket传输了。下面一张图对上面的说明
进行一个汇总,但是图中有些细节并没有表示出来还是最好通过源码备注了解
三、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
下面我将我写的中文注释加上源码部分一同放出如下,注意我忽略了解压操作来降低学习的难度
点击(此处
)折叠或打开
ulong
my_net_read(NET *net)//
{
size_t len, complen;
MYSQL_NET_READ_START();
#ifdef HAVE_COMPRESS
if(!net->compress)//如果没有压缩
{
#endif
len= net_read_packet(net,&complen);//
读取一个MYSQL NET包返回实际长度在len变量中,如果有压缩
//压缩前长度保存在complen变量中 这个函数还有一个
重要
//
功能就是扩张max_packet buffer的长度直到max_packet_size
//
限制,如果不能扩张就报错
,这里也指出了一个现实每个MYSQL
//
协议包必须放到一个max_packet buffer中,这也是很多packet
//
buffer 不够报错的根源
if(len== MAX_PACKET_LENGTH)//
是否为一个满包及大小为16M
-1大小
{
/* First packet of a multi-packet. Concatenate the packets */
ulong save_pos = net->where_b;
size_t total_length= 0;
do//
这里这个循环完成多个mysql net包合并为一个MYSQL 协议包的动作
{
net->where_b +=len;//
读取偏移量不断增加
total_length +=len;//
总长度不断增加
len= net_read_packet(net,&complen);//
读取动作
}while(len== MAX_PACKET_LENGTH);
if(len!= packet_error)//
packet_err被定义为
~((unsigned long)(0))
len+= total_length;//
这里要注意MYSQL协议包分为多个mysql net包后结束包的长度是0,所以也不会增加len
net->where_b = save_pos;
}
net->read_pos = net->buff + net->where_b;
if(len!= packet_error)
net->read_pos[len]=0;/* Safeguard for mysql_use_result */
MYSQL_NET_READ_DONE(0,len);
return len;//
返回读取的总长度
#ifdef HAVE_COMPRESS
}
else //不考虑压缩
{.....
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函数进行详细分析,如果有兴趣自行研究
下面我将我写的中文注释加上源码部分一同放出如下
点击(此处
)折叠或打开
static ulong net_read_packet(NET *net, size_t *complen)
{
size_t pkt_len, pkt_data_len;
*complen= 0;
net->reading_or_writing= 1;//
将读写标示设置为1,表示读取开始
/* Retrieve packet length and number.*/
if(net_read_packet_header(net))//
读取一个MYSQL net包的长度和MYSQL NET sequence
goto error;
net->compress_pkt_nr= net->pkt_nr;
#ifdef HAVE_COMPRESS
if(net->compress)//
先不考虑压缩
{
.......
}
#endif
/* The length of the packet that follows.*/
pkt_len= uint3korr(net->buff+net->where_b);//
获得本MYSQL NET包的长度
/*End of big multi-packet.*/
if(!pkt_len)//
判断是否为mysql数据包分包后的结束包
goto end;
pkt_data_len = max(pkt_len,*complen)+ net->where_b;//
获得读取此MYSQL NET包后需要的内存空间,也就是整个NET BUFFER需要多大,需要判断如果是
//
是经过压缩的需要的空间是数据压缩前的长度
/* Expand packet buffer if necessary.*/
if((pkt_data_len >= net->max_packet)&& net_realloc(net, pkt_data_len))//
这里实际的判断net buffer是否够用,如果不够用调用realloc进行内存扩充,
//
在realloc中判断是否超过max_packet_size的设置
goto error;
/* Read the packet data (payload).*/
if(net_read_raw_loop(net, pkt_len))//
开始进行实际的读取操作
goto error;
end:
net->reading_or_writing= 0;//
将读写标示设置为0,表示读取结束
return pkt_len;//
函数返回本次读取
error://
出错返回值
net->reading_or_writing= 0;
return packet_error;
}
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
static my_bool net_read_raw_loop(NET *net, size_t count)
{
bool eof= false;
unsigned int retry_count= 0;
uchar *buf= net->buff + net->where_b;
while (count)
{
size_t recvcnt= vio_read(net->vio, buf, count); //如果写操作遇到错误下面是异常处理 总体来说就是晕倒的是EINTR就做重试,否则直接退出发送数据循环进入异常处理if语句
/* VIO_SOCKET_ERROR (-1) indicates an error. */
if (recvcnt == VIO_SOCKET_ERROR) //
{
/* A recoverable I/O error occurred? */
if (net_should_retry(net, &retry_count))
//1、线程安全客户端如果是EINTR总是重试
//
2、非线程安全客户端或者服务器端如果是EINTR并且达到net
->retry_count就跳出循环
//服务端MYSQLD肯定是线程安全的但是为了服务端的性能不可能在EINTR错误下面无线重试
//
非线程安全的客户端可能全局区数据已经混乱造成I
/
O错误
continue;
else
break;
}
/* Zero indicates end of file. */
else if (!recvcnt) //recv半连接状态? LINUX man recv:The return values will be 0 when the peer has performed an orderly shutdown
{
eof= true;
break;
}
count-= recvcnt;
buf+= recvcnt;
update_statistics(thd_increment_bytes_received(recvcnt));
}
/* On failure, propagate the error code. */
if (count)//
如果count
>0 也就是没有读取到预期的数据
{
/* Socket should be closed. */
net->error= 2;
/* Interrupted by a timeout? */
if (!eof && vio_was_timeout(net->vio)) //是否为ETIMEOUT错误,如果是则报错Got timeout reading communication packets
net->last_errno= ER_NET_READ_INTERRUPTED;
else
net->last_errno= ER_NET_READ_ERROR;//否则报错Got an error reading communication packets
#ifdef MYSQL_SERVER
my_error(net->last_errno, MYF(0));
#endif
}
return MY_TEST(count);
}
这个函数和前面写阶段的时候差不多,调用底层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读取了。下面一张图对上面的说明
进行一个汇总,但是图中有些细节并没有表示出来还是最好通过源码备注了解
四、使用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 客户端程序官方文档给出的
? 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通信的地方,因为没有过多的去剖析所以
有的地方一笔带过,如果日后进行详细的分析会在以文章的方式给出。