天天看點

轉載: Erlang Socket解析二進制資料包

轉自:http://www.itkee.com/developer/detail-318.html

1. 對于測試用例的介紹:

Erlang編寫TCP伺服器。隻做一次Accept,接收到Socket

之後開始收資料。用python編寫Client,連接配接到伺服器上;發送LEN(int)+CMD(short)+BODY(binary)格式的資料

包。用于熟悉Erlang如何做拆解包,資料讀取。

2. 編寫簡單的Erlang TCP伺服器:

Erlang裡面的TCP socket應該都是這個方式來編寫代碼。指的修改和優化的是在于可以啟動更多的程序來驅動起這個應用。

%% 檔案名:server.erl

%% 子產品定義

-module(server).

%% 導出函數

-export([start/0]).

%% 宏定義

-define( PORT, 2345 ).

-define( HEAD_SIZE, 4 ).

%% 解數字類型用到的宏

-define( UINT, 32/unsigned-little-integer).

-define( INT, 32/signed-little-integer).

-define( USHORT, 16/unsigned-little-integer).

-define( SHORT, 16/signed-little-integer).

-define( UBYTE, 8/unsigned-little-integer).

-define( BYTE, 8/signed-little-integer).

%% 對外接口

start() ->

 %% 這個地方有些有意思的東西:

 %% 1.{packet,0}這個設定,可以讓Erlang不再接管socket的封包了;

 %%   如果被Erlang接管了,在實體網絡包前面4Bytes裡面寫的東西不

 %%  是簡單的網絡包的Size.

 %% 2.{active,false}這個設定,可以讓接受到的Socket Recv指定Size

 %%   網絡包,這樣也就友善了拆解包的工作了。

    {ok, Listen}=gen_tcp:listen( ?PORT,[ binary,

                    { packet, 0 }, { reuseaddr, true }, { active, false }]),

    io:format("start listen port: ~p~n", [?PORT] ),

    {ok, Socket} = gen_tcp:accept(Listen),

 %% 接收到用戶端之後将馬上關閉Listen Socket

    gen_tcp:close( Listen ),

 %% 開始讀取資料標頭

    looph(Socket).

%% 讀出標頭

looph(Socket) ->

    case gen_tcp:recv( Socket, ?HEAD_SIZE ) of

        { ok, H } ->

            io:format("recv head binary=~p~n", [H] ) ,

            %% 比對出標頭

            << TotalSize:?UINT >> = H ,

   %% 除去標頭的SIZE

            BodySize = TotalSize - ?HEAD_SIZE,

            %% 開始收包體

            loopb(Socket,BodySize);

  %% 出異常了

        { error, closed } ->

            io:format("recv head fail." )

    end.

%% 讀出包體

loopb(Socket,BodySize) ->

    case gen_tcp:recv( Socket, BodySize ) of

        { ok, B } ->

            %% 模式比對

   %% 1.得出資料包中的CMD編号

   %% 2.将後面部分的Buffer放到Contain裡面

            << CMD:?USHORT, Contain/binary>> = B,

            io:format("recv body binary = ~p~n", [B] ),

            io:format("recv protocol CMD = ~p~n", [CMD] ),

            io:format("recv body = ~p~n", [Contain] ),

   %% 繼續讀取標頭

            looph(Socket);

  %% 異常處理

        {error,close} ->

            io:format("recv body fail.")

在編寫這個代碼過程中遇到的麻煩:

2.1. 不知道如何比對出資料標頭來:

<< TotalSize:?UINT >> = H

2.2. 不知道如何将一個binary比對出來部分,将剩餘部分binary放到别的裡面:

<< CMD:?USHORT, Contain/binary>> = B

2.3. 在多次調試之後出來這樣的錯誤:

{error,eaddrinuse}

端口被占用了,這個時候去關閉全部背景的.beam也是沒有解決這個問題。最後重新開機了機器才能讓這個問題解決。

2.4. Erlang中對于binary操作的熟悉:

term_to_binary和binary_to_term函數的功效:

用于将一個任意的Erlang值轉化成為二進制(反向操作),這個特性可能也隻有在Erlang之間打交道的時候可以用上。

list_to_binary:

這個函數非常有用,原因是它不挑食。打個比方:

1> A = "A".

"A"

2> B = list_to_binary(A).

<<"A">>

結果這個"A"字元串被好好的放在了binary裡面去了。

還有一個用處就是用來連接配接已經生成好的一些binary的對象

10> A = << 1,2,3,4 >>.

<<1,2,3,4>>

11> B = << "A" >>.

12> C = list_to_binary( [A, B] ).

<<1,2,3,4,65>>

3. 開始編寫python用戶端代碼:

這個Socket用戶端是使用的asyncore的dispatcher來做的。用起來有些像ACE裡面的reactor模型。這個代碼寫起來非常容易了。

# -*- coding: utf-8 -*-

import socket

import asyncore

# 宏定義

MAX_RECV_CACHE = 1024

CHAT_MSG = 0x101A

# 聊天用戶端

class ChatClient( asyncore.dispatcher ):

    def __init__( self, host = Host, port = Port ):

        asyncore.dispatcher.__init__( self )

        self.create_socket( socket.AF_INET, socket.SOCK_STREAM)

        self.connect( ( host, port) )

        self.buffer_ = ''

        self.recv_buf_ = ''

        pass

    # 連結成功

    def handle_connect( self ):

        print( "[SOCKET] handle_connect event." )

        self.send_message( CHAT_MSG, "hello then world." )

        self.send_message( CHAT_MSG, "this data is come from python." )

    # 讀取内容

    def handle_read( self ):

        ret = self.recv( MAX_RECV_CACHE )

    def send_message( self, _prop_cmd, _msg ):

        print( "presend size = %d"%len( _msg ) )

        total_size = len( _msg ) + 4 + 2

        self.buffer_ = self.buffer_ + struct.pack( "I", total_size )

        self.buffer_ = self.buffer_ + struct.pack( "H", _prop_cmd )

        self.buffer_ = self.buffer_ + _msg

    pass

if __name__ == "__main__":

    try:

        client = ChatClient()

        asyncore.loop()

    except KeyboardInterrupt:

        print( "退出." )

完結。下次開始學習Erlang的OTP ETS了。