erlang_mysql_driver 源码分析4

其实本来是要讲erlang如何解决tcp粘包问题的,刚好erlang_mysql_driver里面就有关于这个问题的一种解决方式,所以干脆就以erlang_mysql_driver的源码为例来探究下该问题的解决方案。

tcp 粘包问题

mysql的协议包是建立在tcp的基础上的,而tcp协议是流协议,也就是在使用的时候可以保证按顺序收到,但是并不是对方发送多少次,我们就能接收多少次。
这就是tcp粘包问题了,例如你调用两次gen_tcp:send,发了两句话,“你好”“吃饭了吗”。可是我这边在调用gen_tcp:recv接收的时候,可能是需要调用3次,分别收到 “你”“好吃”“饭了吗”。那我就根本不知道你在说什么了。
针对上述问题的解决方案是在协议自定义的时候用len+body的形式,len所占的空间固定,比如只占一个字节,那么你就可以这样发“2你好4吃饭了吗”。我这边每次先取一个字节,得到2,然后就取后面2个数据“你好”。再次循环,取一个字节4,然后取后面4个字节“吃饭了吗”。这样我们就可以正常聊天了。(当然中文不止一个字节,我随便定的,不要纠结哈)本文要讲的就是针对这种方案的erlang具体设计了。
另外关于tcp粘包问题的概念也挺多的,有一种特别的看法是把粘包问题看成传输层的问题,然后得出的结论是tcp以前会出现“粘包问题”现在不会了。这种概念的“粘包问题”是指多进程通过同一端口发送消息如何避免乱序。这里不会对这些进行讨论,在这篇文章中我理解的tcp粘包问题是在应用层的,也就是因为tcp的字节流无边界,发送方发出N个协议包,接收方收到M个协议包的问题,N可能大于M也可能小于M。


mysql_recv是怎么处理tcp粘包

erlang_mysql_driver使用mysql_recv模块来接收mysql服务器发送来的数据,那么mysql_recv进程肯定是需要处理tcp粘包的。

mysql_recv建立连接的方式

我们先看下,mysql_recv是怎么建立连接的?
这个连接建立的过程在mysql_recv:init中

init(Host, Port, LogFun, Parent) ->
    case gen_tcp:connect(Host, Port, [binary, {packet, 0}, {keepalive, true}]) of
    {ok, Sock} ->
        Parent ! {mysql_recv, self(), init, {ok, Sock}},
        State = #state{socket  = Sock,
               parent  = Parent,
               log_fun = LogFun,
               data    = <<>>
              },
        loop(State);
    E ->
        LogFun(?MODULE, ?LINE, error,
           fun() ->
               {"mysql_recv: Failed connecting to ~p:~p : ~p",
                [Host, Port, E]}
           end),
        Msg = lists:flatten(io_lib:format("connect failed : ~p", [E])),
        Parent ! {mysql_recv, self(), init, {error, Msg}}
    end.

gen_tcp:connect(Host, Port, [binary, {packet, 0}, {keepalive, true}])
这里有个很重要的默认参数就是{active, true},在不显式指定active的时候,默认为true。
true指定的是主动消息接收,也就是一旦有数据达到这个Socket,那么这个Socket的控制进程(mysql_recv)就会收到{tcp, …}, {tcp_error, …}, {tcp_closed,…}这一类的消息。

而packet这个参数,跟我们要讨论的tcp粘包问题关系就很大了,它就是erlang用来解决tcp粘包的,如{packet, 1}表明协议包的第一个bytes用来表示长度,那么我们只需要直接接收数据就行了,或者直接发送数据。erlang会自动解析协议包头的长度,但是在mysql_recv中并没有使用packet这个功能。


mysql_recv send_packet

那么mysql_recv自己是怎么解决的呢?我们看它收到数据后怎么做的?

loop(State) ->
    Sock = State#state.socket,
    receive
    {tcp, Sock, InData} ->
        NewData = list_to_binary([State#state.data, InData]),
        %% send data to parent if we have enough data
        Rest = sendpacket(State#state.parent, NewData),
        loop(State#state{data = Rest});
    {tcp_error, Sock, Reason} ->
        LogFun = State#state.log_fun,
        LogFun(?MODULE, ?LINE, error,
           fun() ->
               {"mysql_recv: Socket ~p closed : ~p",
                [Sock, Reason]}
           end),
        State#state.parent ! {mysql_recv, self(), closed, {error, Reason}},
        error;
    {tcp_closed, Sock} ->
        LogFun = State#state.log_fun,
        LogFun(?MODULE, ?LINE, debug,
           fun() ->
               {"mysql_recv: Socket ~p closed", [Sock]}
           end),
        State#state.parent ! {mysql_recv, self(), closed, normal},
        error
    end.

每次收到数据后InData后,先把之前缓存在mysql_recv state中的data取出,然后和InData合并。NewData = InData + 之前缓存的Data,然后调用send_packet。

%% send data to parent if we have enough data
sendpacket(Parent, Data) ->
    case Data of
    <<Length:24/little, Num:8, D/binary>> ->
        if
        Length =< size(D) ->
            {Packet, Rest} = split_binary(D, Length),
            Parent ! {mysql_recv, self(), data, Packet, Num},
            sendpacket(Parent, Rest);
        true ->
            Data
        end;
    _ ->
        Data
    end.

sendpacket并没有直接发送数据,sendpacket取出前面3个字节,作为这个包长度的值。为什么是3个字节呢?为什么不是2个或4个呢,因为这是mysql规定,mysql的数据包就是用前面3个字节表示长度,第4个字节表示序号,可见上面的代码匹配 Length:24, Num:8。
当我们知道这个包的长度后,就看后面的data够不够了,如果不够那就继续进入#state.data中缓存。如果够了,那就按Length截取出数据,发送给mysql_conn,剩下的data继续进入#state.data中缓存。


其他处理tcp粘包的方法

事实上erlang本身的{packet, N}这个参数就提供了,强大的tcp粘包处理。
在不考虑{packet, N}的情况下,其他处理tcp粘包的方法:

  • mysql_recv给我们提供了一种主动接收信息{active, true}时处理粘包的方式,这种方式其实就是准备了一个数据缓存区(#state.data),然后按包头定义的长度取数据,多的放回,少的继续等待。
  • 使用参数{active,false}的时候,我们是用gen_tcp:recv(Socket, Len)的方式来接收数据的,只有调用了recv的时候才会去按Len指定的长度获取数据,那么这种方式的话,其实解决的方法也比较简单。每次先gen_tcp:recv(Socket, 3),接收到数据后取出该数据的大小DataLen,然后调用gen_tcp:recv(Socket, DataLen).这次接收完数据后,继续gen_tcp:recv(Socket, 3)就行了。

当然上面说的两种方式,包括{packet, N}都是建立在我们自定义的协议包使用固定的字节数(如mysql的3),来表示该报的长度。包的格式 = Len + Body,Len所占的字节数固定。这里的Len的字节数固定后,这个包的最大值其实也就知道了,如mysql的一个包最大为16m((2^24)-1=(16M-1)字节),超过该值就要分包发送。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值