├── .gitignore ├── README.md ├── fileutil.erl ├── udpserv.erl ├── ring.erl ├── redis_test.erl ├── httputil.erl ├── proletariat.erl ├── jpeg.erl └── redis.erl /.gitignore: -------------------------------------------------------------------------------- 1 | *.dump 2 | *.beam 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Miscellaneous Erlang learnings 2 | 3 | All sorts of misc erlang code from my days of reading "Programming Erlang" and other learning material. 4 | 5 | Just to save it for future reference. 6 | 7 | **NO HIGHLY ESTEEMED DEED IS COMMEMORATED HERE... NOTHING VALUED IS HERE** 8 | -------------------------------------------------------------------------------- /fileutil.erl: -------------------------------------------------------------------------------- 1 | -module(fileutil). 2 | -export([lines/1]). 3 | 4 | %% Read text files line at a time 5 | 6 | lines(File) -> 7 | {ok, S} = file:open(File, read), 8 | Lines = read_lines(S,[]), 9 | file:close(S), 10 | Lines. 11 | 12 | read_lines(S, Lines) -> 13 | case io:get_line(S,'') of 14 | eof -> lists:reverse(Lines); 15 | Line -> read_lines(S, [Line|Lines]) 16 | end. 17 | -------------------------------------------------------------------------------- /udpserv.erl: -------------------------------------------------------------------------------- 1 | -module(udpserv). 2 | -export([listen/2, hello/1, reverser/1]). 3 | 4 | % Listen to UDP messages, run a function to get response 5 | 6 | listen(Port, Handler) -> 7 | {ok, Socket} = gen_udp:open(Port,[binary]), 8 | loop(Socket,Handler). 9 | 10 | loop(Socket,Handler)-> 11 | receive 12 | {udp, Socket, Host,Port,Bin} -> 13 | AsList = binary_to_list(Bin), 14 | io:format("[~p:~p] ~p~n", [Host,Port,AsList]), 15 | gen_udp:send(Socket, Host, Port, list_to_binary(Handler(AsList))), 16 | loop(Socket,Handler); 17 | Else -> io:format("ei yymmarra: ~p~n", [Else]) 18 | end. 19 | 20 | hello(Port) -> 21 | listen(Port, fun(X) -> "Hello, " ++ string:trim(X) ++ "!" end). 22 | 23 | 24 | reverser(Port) -> 25 | listen(Port, fun(X) -> lists:reverse(string:trim(X)) end). 26 | -------------------------------------------------------------------------------- /ring.erl: -------------------------------------------------------------------------------- 1 | -module(ring). 2 | -compile(export_all). 3 | 4 | 5 | %% Send a message around a ring of processes 6 | %% logs the time it took 7 | 8 | ring_handler() -> 9 | receive {First, NextProc} -> 10 | ring_handler(First, NextProc) 11 | end. 12 | ring_handler(First, NextProc) -> 13 | receive 14 | {Me, Time, Msg} when is_pid(Me) andalso Me =:= self() -> 15 | io:format("[~p] Got msg back around the ring ~p (~pms) ~n", 16 | [self(), Msg, (erlang:system_time()-Time)/1000000]), 17 | ring_handler(First,NextProc); 18 | Msg when First -> 19 | io:format("[~p] Start send ~p to ~p~n", [self(), Msg, NextProc]), 20 | NextProc ! {self(), erlang:system_time(), Msg}, 21 | ring_handler(First,NextProc); 22 | Msg -> 23 | %io:format("[~p] Passing thru ~p to ~p~n", [self(), Msg, NextProc]), 24 | NextProc ! Msg, 25 | ring_handler(First,NextProc) 26 | end. 27 | 28 | ring(N) -> 29 | Initial = spawn(fun ring_handler/0), 30 | Initial ! {true, ring(Initial, N-1)}, 31 | Initial. 32 | 33 | ring(Initial,N) -> 34 | Next = spawn(fun ring_handler/0), 35 | case N of 36 | 0 -> Next ! {false, Initial}; 37 | _ -> Next ! {false, ring(Initial, N-1)} 38 | end, 39 | Next. 40 | -------------------------------------------------------------------------------- /redis_test.erl: -------------------------------------------------------------------------------- 1 | -module(redis_test). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | 5 | %%%%% 6 | %% Tests for RESP read/write 7 | 8 | read(Bin) -> 9 | {ok, Val, <<>>} = redis:read(Bin), 10 | Val. 11 | 12 | read_simple_string_test() -> 13 | "Hello World" = read(<<"+Hello World\r\n">>). 14 | 15 | read_list_test() -> 16 | [420, <<"hello">>, "world"] = 17 | read(<<"*3\r\n:420\r\n$5\r\nhello\r\n+world\r\n">>). 18 | 19 | read_cont(D1, D2) -> read_cont(D1,D2,<<>>). 20 | read_cont(D1, D2, Rest) -> 21 | {incomplete,Cont} = redis:read(D1), 22 | %% Check that parsing with continuation retuns same 23 | %% data as parsing in one go 24 | {ok, Val, Rest} = Cont(D2), 25 | {ok, Val, Rest} = redis:read(iolist_to_binary([D1,D2])), 26 | Val. 27 | 28 | read_continuation_test() -> 29 | %% Reading a binary string without enough data yields 30 | %% a continuation to be called when more data is received. 31 | <<"Hello Erlang">> = read_cont(<<"$12\r\nHello ">>, <<"Erlang\r\n">>). 32 | 33 | read_continuation_arr_test() -> 34 | [420, 666, "end"] = read_cont( 35 | <<"*3\r\n:420\r\n:">>, 36 | <<"666\r\n+end\r\nREST">>, 37 | <<"REST">>). 38 | 39 | read_roundtrip(X) -> 40 | Written = iolist_to_binary(redis:write(X)), 41 | {ok, X, <<>>} = redis:read(Written), 42 | ok. 43 | 44 | read_roundtrip_test() -> 45 | read_roundtrip(["help", <<"I'm stuck in a list">>, 666]), 46 | read_roundtrip(null). 47 | -------------------------------------------------------------------------------- /httputil.erl: -------------------------------------------------------------------------------- 1 | -module(httputil). 2 | -compile(export_all). 3 | -record(url, {scheme = http :: atom(), 4 | port = 80 :: integer(), 5 | host :: string(), 6 | path = "/" :: string()}). 7 | -record(response, {status :: integer(), message :: string(), 8 | headers = #{} :: #{atom() => string()}, 9 | body :: binary()}). 10 | 11 | % This is a toy http client library, don't actually use! 12 | % Just a learning exercise. 13 | 14 | 15 | parse_url(Url) -> 16 | Part = fun({Start,Len}) -> lists:sublist(Url, Start+1, Len) end, 17 | case re:run(Url, "([^:]+)://([^/]*)(/.*)?$") of 18 | {match, [_, Scheme, Host, Path]} -> 19 | #url{scheme=list_to_atom(Part(Scheme)), 20 | host=Part(Host), 21 | path=Part(Path)}; 22 | {match, [_, Scheme, Host]} -> 23 | #url{scheme=list_to_atom(Part(Scheme)), 24 | host=Part(Host)}; 25 | _ -> {error, {badurl, Url}} 26 | end. 27 | 28 | parse_headers_and_body(Bin) -> parse_headers_and_body(Bin, #{}). 29 | parse_headers_and_body(Bin, Headers) -> 30 | [HeaderLine, Rest] = binary:split(Bin, <<"\r\n">>), 31 | case HeaderLine of 32 | % empty line, we are done with headers, return them 33 | <<>> -> {ok, Headers, Rest}; 34 | 35 | % parse one header 36 | _ -> 37 | [Name,Value] = binary:split(HeaderLine, <<": ">>), 38 | parse_headers_and_body(Rest, maps:put(binary_to_atom(Name), binary_to_list(Value), Headers)) 39 | end. 40 | 41 | parse_response(Data) -> 42 | [StatusLine, HeadersAndBody] = binary:split(Data, <<"\r\n">>), 43 | case StatusLine of 44 | <<"HTTP/1.1 ", Code:3/binary, " ", Message/binary>> -> 45 | {ok, Headers,Body} = parse_headers_and_body(HeadersAndBody), 46 | #response{status = list_to_integer(binary_to_list(Code)), 47 | message = binary_to_list(Message), 48 | headers = Headers, 49 | body = Body}; 50 | 51 | % anything else, we don't understand 52 | _ -> {error, unrecognized_response, Data} 53 | end. 54 | 55 | read_response(Socket, Read) -> 56 | receive 57 | {tcp, Socket, Data} -> 58 | read_response(Socket, [Data,Read]); 59 | {tcp_closed,Socket} -> 60 | parse_response(list_to_binary(lists:reverse(Read))) 61 | after 62 | 30000 -> {error, timeout_reading_response} 63 | end. 64 | 65 | http_get(Socket, Host, Path) -> 66 | ok = gen_tcp:send(Socket,["GET ", Path, " HTTP/1.1\r\n", 67 | "Connection: close\r\n", 68 | "Host: ", Host, "\r\n\r\n"]), 69 | %ok = gen_tcp:close(Socket), 70 | read_response(Socket, []). 71 | 72 | get(Url) -> 73 | case parse_url(Url) of 74 | #url{scheme=http, host=Host, path=Path, port=Port} -> 75 | {ok,Socket} = gen_tcp:connect(Host,Port,[binary,{packet,0}]), 76 | http_get(Socket, Host, Path); 77 | 78 | {error, Why} -> 79 | io:format("ei yhteytt",[]), 80 | Why 81 | end. 82 | -------------------------------------------------------------------------------- /proletariat.erl: -------------------------------------------------------------------------------- 1 | -module(proletariat). 2 | -compile(export_all). 3 | -record(workstate, {final_result_pid, 4 | work_collected = false, 5 | work = [], 6 | results = #{}, 7 | workers, 8 | in_progress = 0}). 9 | 10 | % a process worker experiment with silly names 11 | 12 | 13 | % bourgeoisie process is the one controlling means of production (gives out work) 14 | % 15 | % NumProles controls concurrency, how many proletariat workers are spawned. 16 | % Proletariat send results to bourgeoisie with {'RESULT', worker_pid, work_id, work_result}. 17 | % 18 | % WorkerFun is the function the proletariat will execute. It receives 19 | % the {bourgeoisie_pid, work_data} and sends back the work result. 20 | % 21 | % OnComplete will be called with the end result of doing all the work 22 | % 23 | % returns function to submit more work, which must be called with 24 | % {work_id, work_data} or 'DONE' to signal all work is done. 25 | 26 | bourgeoisie(NumProles, WorkerFun, OnComplete) -> 27 | Bourgeoisie = spawn(piiskuri(NumProles, WorkerFun)), 28 | Final = spawn(fun() -> receive Result -> OnComplete(Result) end end), 29 | Bourgeoisie ! {'FINAL', Final}, 30 | fun ({WorkId,WorkData}) -> Bourgeoisie ! {'WORK', WorkId, WorkData}; 31 | ('DONE') -> Bourgeoisie ! 'DONE' 32 | end. 33 | 34 | piiskuri(NumProles, WorkerFun) -> 35 | % spawn worker processes and spawn the 36 | Proles = lists:map( 37 | % map proletariat pid to its state (initially IDLE) 38 | fun(_) -> duunari(WorkerFun) end, 39 | lists:seq(1, NumProles)), 40 | fun () -> 41 | io:format("Started piiskuri with ~p~n", [NumProles]), 42 | piiskuri(#workstate{workers=Proles}) 43 | end. 44 | 45 | duunari(Duuni) -> 46 | spawn(fun() -> duunari_loop(Duuni) end). 47 | 48 | duunari_loop(Duuni) -> 49 | receive 50 | {PiiskuriPid, {WorkId, WorkData}} -> 51 | io:format("tehdaan hommia ~p datalla ~p~n", [WorkId, WorkData]), 52 | PiiskuriPid ! {'RESULT', self(), WorkId, Duuni(WorkData)}, 53 | duunari_loop(Duuni) 54 | end. 55 | 56 | piiskuri(State) -> 57 | receive 58 | {'FINAL', Pid} -> 59 | %% mark the Pid to send final results 60 | piiskuri(State#workstate{final_result_pid = Pid}); 61 | {'WORK', WorkId, WorkData} -> 62 | %% mark new work as found 63 | piiskuri(State#workstate{ 64 | work = 65 | [{WorkId,WorkData} | State#workstate.work]}); 66 | {'RESULT', WorkerPid, WorkId, WorkResult} -> 67 | %% mark result of a single work 68 | piiskuri(State#workstate{ 69 | %% Add result to result map 70 | results = maps:put(WorkId, WorkResult, State#workstate.results), 71 | %% Set worker as idle 72 | workers = [WorkerPid | State#workstate.workers], 73 | 74 | %% Reduce in progress count 75 | in_progress = State#workstate.in_progress - 1}); 76 | 'DONE' -> 77 | %% mark all work as collected 78 | io:format("hommat done ~p~n", [State#workstate{work_collected = true}]), 79 | piiskuri(State#workstate{work_collected = true}) 80 | after 81 | 10 -> 82 | case State of 83 | %% No more work and no in progress, send final result 84 | #workstate{work=[], work_collected=true, in_progress=0, 85 | final_result_pid = Pid, 86 | results = Results} -> 87 | Pid ! Results; 88 | 89 | %% Have some work to do 90 | #workstate{work=[Work|OtherWork], 91 | workers=[Worker|OtherWorkers], 92 | in_progress=InProgress} -> 93 | Worker ! {self(), Work}, 94 | piiskuri(State#workstate{ 95 | work = OtherWork, 96 | workers = OtherWorkers, 97 | in_progress = InProgress + 1}); 98 | %% waiting for work 99 | _ -> piiskuri(State) 100 | end 101 | end. 102 | 103 | %% 104 | %% Work = proletariat:bourgeoisie(20, fun(X) -> X * 2 end, fun(Res) -> io:format("kaikki valmista: ~p~n", [Res]) end). 105 | %% Work({kymp, 10}). 106 | %% Work({kolmekaks, 32}). 107 | %% Work('DONE'). 108 | %% -> prints results 109 | -------------------------------------------------------------------------------- /jpeg.erl: -------------------------------------------------------------------------------- 1 | -module(jpeg). 2 | -compile(export_all). 3 | -record(marker, {type, size, data}). 4 | 5 | %% See EXIF data format description: 6 | % https://www.media.mit.edu/pia/Research/deepview/exif.html 7 | 8 | read_start_of_image(<<16#FF:8, 16#D8:8, Img/binary>>) -> Img; 9 | read_start_of_image(_) -> throw(not_start_of_image). 10 | 11 | is_end_of_image(<<16#FF:8, 16#D9:8>>) -> true; 12 | is_end_of_image(_) -> false. 13 | 14 | 15 | hex_dump(Binary) -> hex_dump(Binary, 0). 16 | hex_dump(Binary, I) -> 17 | if I < size(Binary) -> 18 | io:format("~.16B ", [binary:at(Binary,I)]), 19 | hex_dump(Binary,I+1); 20 | true -> void 21 | end. 22 | 23 | read_marker(Binary) -> 24 | {Marker,Rest} = split_binary(Binary, 4), 25 | case Marker of 26 | <<16#FF:8, MarkerNum:8, DataSize:16/big>> -> 27 | % io:format("marker ~.16B with size ~p ~n", [MarkerNum, DataSize-2]), 28 | {Data, Rest1} = split_binary(Rest, DataSize-2), 29 | %hex_dump(Data), 30 | {#marker{type=MarkerNum,size=DataSize-2,data=Data}, Rest1}; 31 | _ -> throw({not_at_marker, Marker}) 32 | end. 33 | 34 | read_segments(Bin,Acc) -> 35 | case is_end_of_image(Bin) of 36 | true -> Acc; 37 | false -> 38 | {Marker, Rest} = read_marker(Bin), 39 | read_segments(Rest, [Marker | Acc]) 40 | end. 41 | 42 | 43 | read(Filename) -> 44 | {ok, Bin} = file:read_file(Filename), 45 | Img = read_start_of_image(Bin), 46 | read_segments(Img,[]). 47 | 48 | read_tiff_header(Bin) -> 49 | case Bin of 50 | <<"Exif", 0, 0, % EXIF magic 51 | _Endianness:2/binary, 0, 42, % TIFF magic 52 | IfdOffset:32, 53 | Rest/binary>> -> 54 | {_, IfdData} = split_binary(Rest, IfdOffset-8), 55 | read_ifds(IfdData); 56 | _ -> 57 | hex_dump(element(split_binary(Bin,32), 1)), 58 | {error, Bin} 59 | end. 60 | 61 | 62 | ifd_tag(16#010E) -> image_description; 63 | ifd_tag(16#010F) -> make; 64 | ifd_tag(16#0110) -> model; 65 | ifd_tag(16#0132) -> date_time; 66 | ifd_tag(16#0131) -> software; 67 | ifd_tag(16#8825) -> gps_info; 68 | ifd_tag(16#013C) -> host_computer; 69 | % EXIF 2.3 IFD1 tags 70 | ifd_tag(16#0100) -> image_width; 71 | ifd_tag(16#0101) -> image_height; 72 | ifd_tag(16#0102) -> bits_per_sample; 73 | ifd_tag(16#0103) -> compression; 74 | ifd_tag(16#0106) -> photometric_interpretation; 75 | ifd_tag(16#0111) -> strip_offsets; 76 | ifd_tag(16#0112) -> orientation; 77 | ifd_tag(16#0115) -> samples_per_pixel; 78 | ifd_tag(16#0116) -> rows_per_strip; 79 | ifd_tag(16#0117) -> stripbytecounts; 80 | ifd_tag(16#011A) -> x_resolution; 81 | ifd_tag(16#011B) -> y_resolution; 82 | ifd_tag(16#011C) -> planar_configuration; 83 | ifd_tag(16#0128) -> resolution_unit; 84 | ifd_tag(16#0201) -> jpeg_if_offset; 85 | ifd_tag(16#0202) -> jpeg_if_byte_count; 86 | ifd_tag(16#0211) -> ycbcrcoefficients; 87 | ifd_tag(16#0212) -> ycbcrsubsampling; 88 | ifd_tag(16#0213) -> ycbcrpositioning; 89 | ifd_tag(16#0214) -> reference_black_white; 90 | ifd_tag(X) -> X. 91 | 92 | ifd_val(2, Data) -> 93 | binary_to_list(binary:part(Data, 0, size(Data)-1)); 94 | ifd_val(3, <>) -> Short; 95 | ifd_val(4, <>) -> Long; 96 | ifd_val(5, <> = D) -> 97 | %io:format("rational: ~p~n", [D]), 98 | {Numerator,Denominator}; 99 | ifd_val(X, Data) -> {type,X,data,Data}. 100 | 101 | % read multiple ifd values 102 | ifd_vals(_Type,<<>>,_BytesPerComponent) -> []; 103 | ifd_vals(Type, Data, BytesPerComponent) -> 104 | {First,Rest} = split_binary(Data, BytesPerComponent), 105 | [ifd_val(Type,First) | ifd_vals(Type,Rest,BytesPerComponent)]. 106 | 107 | read_ifd_entries(_, Count, Bin, Map) when Count =:= 0 -> {Map, Bin}; 108 | read_ifd_entries(Ifd, Count, Bin, Map) -> 109 | case Bin of 110 | <> -> 111 | %io:format("Got tag: ~p, type: ~p, cnt: ~p at ~p~n", [Tag,Type,Cnt,DataOrOffset]), 112 | BytesPerComponent = case Type of 113 | 1 -> 1; % unsigned byte 114 | 2 -> 1; % string 115 | 3 -> 2; % unsigned short 116 | 4 -> 4; % unsigned long 117 | 5 -> 8; % unsigned rational 118 | 6 -> 1; % signed byte 119 | 7 -> 1; % undefined 120 | 8 -> 2; % signed short 121 | 9 -> 4; % signed long 122 | 10 -> 8; % signed rational 123 | 11 -> 4; % single float 124 | 12 -> 8 % double float 125 | end, 126 | Size = Cnt * BytesPerComponent, 127 | % If Size<4 then DataOrOffset is the actual data, 128 | % otherwise it is an offset to get the data. 129 | Data = if 130 | Size =< 4 -> DataOrOffset; 131 | true -> (case DataOrOffset of 132 | <> -> binary:part(Ifd, Offset-8, Size) 133 | end) 134 | end, 135 | TagName = ifd_tag(Tag), 136 | TagVal = if Cnt > 1 andalso Type /= 2 -> 137 | ifd_vals(Type,Data,BytesPerComponent); 138 | true -> ifd_val(Type,Data) 139 | end, 140 | Val = case TagName of 141 | % if tag is a link to another IFD, read that 142 | gps_info -> element(1, read_ifd(element(2, split_binary(Ifd, TagVal-8)))); 143 | _ -> TagVal 144 | end, 145 | read_ifd_entries(Ifd, Count-1, Rest, 146 | maps:put(TagName, Val, Map)); 147 | _ -> {error, invalid_ifd_entry, Count, Bin, Map} 148 | end. 149 | 150 | 151 | 152 | read_ifd(Bin) -> 153 | case Bin of 154 | <> -> 155 | read_ifd_entries(Bin, Count, Rest, #{}); 156 | _ -> {error, not_ifd, Bin} 157 | end. 158 | 159 | read_ifds(Bin) -> 160 | read_ifds(Bin, Bin, #{}). 161 | read_ifds(Whole, Bin, Map) -> 162 | {IfdMap, Rest} = read_ifd(Bin), 163 | NewMap = maps:merge(Map,IfdMap), 164 | case Rest of 165 | <<0:32, Img/binary>> -> {NewMap, Img}; 166 | <> -> 167 | %% offset from where?? 168 | %io:format("seuraava ~p~n", [NextIfd]), 169 | read_ifds(Whole, element(2,split_binary(Whole,NextIfd-8)), NewMap) 170 | %%{seuraava, NextIfd, Map, IfdMap} 171 | end. 172 | 173 | read_header(Filename) -> 174 | {ok, Bin} = file:read_file(Filename), 175 | Img = read_start_of_image(Bin), 176 | {App1,_Rest} = read_marker(Img), 177 | read_tiff_header(App1#marker.data). 178 | -------------------------------------------------------------------------------- /redis.erl: -------------------------------------------------------------------------------- 1 | -module(redis). 2 | -export([read/1, write/1, start/1, demo/0]). 3 | 4 | % A very minimal Redis wire protocol server that saves 5 | % values to ETS. 6 | 7 | %%%%%%% 8 | % RESP Wire format 9 | % https://redis.io/docs/reference/protocol-spec/ 10 | 11 | resp_split(Bin) -> 12 | binary:split(Bin,<<"\r\n">>). 13 | 14 | -type incomplete() :: {incomplete, function()}. 15 | -type complete() :: {ok, any(), binary()}. 16 | -type parsed() :: complete() | incomplete(). 17 | 18 | %% Read Count amount of bytes, or if there are not enough, 19 | %% return an incomplete with a continuation 20 | -spec resp_read(binary(), integer(), function()) -> parsed(). 21 | resp_read(Bin, Count, Parser) -> 22 | Received = iolist_size(Bin), 23 | if 24 | Count =< Received -> 25 | %% We have enough 26 | {Read, Rest} = split_binary(iolist_to_binary(Bin), Count), 27 | {ok, Parser(Read), Rest}; 28 | true -> 29 | %% We need more 30 | {incomplete, 31 | fun(MoreBin) -> 32 | resp_read([Bin, MoreBin], Count, Parser) 33 | end} 34 | end. 35 | 36 | -spec with_value(binary(), function()) -> parsed(). 37 | with_value(Bin, Cont) -> 38 | case resp_split(Bin) of 39 | [Bin] -> 40 | {incomplete, 41 | fun(MoreBin) -> 42 | with_value(iolist_to_binary([Bin,MoreBin]), Cont) 43 | end}; 44 | [Val,Rest] -> 45 | Cont(Val, Rest) 46 | end. 47 | 48 | -spec read(binary()) -> parsed(). 49 | read(<<$+, Str/binary>>) -> 50 | with_value(Str, 51 | fun(S,Rest) -> 52 | {ok, binary_to_list(S), Rest} 53 | end); 54 | read(<<$-, Err/binary>>) -> 55 | with_value(Err, 56 | fun(E,Rest) -> 57 | {ok, binary_to_list(E), Rest} 58 | end); 59 | read(<<$:, Int/binary>>) -> 60 | with_value(Int, 61 | fun(I,Rest) -> 62 | {ok, binary_to_integer(I), Rest} 63 | end); 64 | read(<<$*, Arr/binary>>) -> 65 | with_value( 66 | Arr, 67 | fun (CountB, Rest) -> 68 | Count = binary_to_integer(CountB), 69 | if 70 | %% array of -1 len is considered the null 71 | Count == -1 -> {ok, null, Rest}; 72 | true -> read_array(Count, Rest, []) 73 | end 74 | end); 75 | read(<<$$, Bulk/binary>>) -> 76 | %% prefixed bulk string, return as binary 77 | with_value( 78 | Bulk, 79 | fun(SizeB, Rest) -> 80 | Size = binary_to_integer(SizeB), 81 | resp_read(Rest, Size + 2, % include CRLF 82 | fun(<<"Null\r\n">>) -> null; 83 | (Bin) -> {BinStr,_} = split_binary(Bin, Size), BinStr 84 | end) 85 | end). 86 | 87 | -spec read_array(integer(), binary(), [any()]) -> [any()]. 88 | read_array(0, Rest, Arr) -> 89 | {ok, lists:reverse(Arr), Rest}; 90 | read_array(Items, Bin, Arr) -> 91 | case read(Bin) of 92 | {ok, Item, Rest} -> 93 | read_array(Items-1, Rest, [Item|Arr]); 94 | {incomplete, Cont} -> 95 | read_array_cont(Items,Arr,Cont) 96 | end. 97 | 98 | -spec read_array_cont(integer(), [any()], function()) -> incomplete(). 99 | read_array_cont(Items,Arr,Cont) -> 100 | {incomplete, 101 | fun(MoreBin) -> 102 | case Cont(MoreBin) of 103 | {incomplete, Cont1} -> 104 | %io:format("still incomplete ~p~n", [Arr]), 105 | read_array_cont(Items,Arr,Cont1); 106 | {ok, Item, Rest} -> 107 | %io:format("item complete ~p~n", [Item]), 108 | read_array(Items-1, Rest, [Item|Arr]) 109 | end 110 | end}. 111 | 112 | -spec write(any()) -> iolist(). 113 | write(null) -> 114 | <<"*-1\r\n">>; 115 | write(Int) when is_integer(Int) -> 116 | [":", integer_to_list(Int), <<"\r\n">>]; 117 | write({error, Message}) -> 118 | ["-", Message, <<"\r\n">>]; 119 | write(Bin) when is_binary(Bin) -> 120 | ["$", integer_to_list(size(Bin)), <<"\r\n">>, Bin, <<"\r\n">>]; 121 | write(StrOrList) when is_list(StrOrList) -> 122 | case io_lib:printable_unicode_list(StrOrList) of 123 | true -> ["+", StrOrList, <<"\r\n">>]; 124 | false -> ["*", integer_to_list(length(StrOrList)), <<"\r\n">>, 125 | [write(X) || X <- StrOrList]] 126 | end. 127 | 128 | -spec start(integer()) -> function(). 129 | start(Port) -> 130 | Table = ets:new(redisdata, [set,public]), 131 | {ok, Listen} = gen_tcp:listen(Port, [binary,{reuseaddr,true}]), 132 | spawn(fun() -> accept(Table, Listen) end), 133 | %% return function to stop server 134 | fun() -> gen_tcp:close(Listen) end. 135 | 136 | accept(Table,Listen) -> 137 | {ok, Socket} = gen_tcp:accept(Listen), 138 | inet:setopts(Socket,[{packet,0},binary,{active, true}]), 139 | spawn(fun() -> accept(Table,Listen) end), 140 | serve(Table,Socket). 141 | 142 | demo()-> 143 | observer:start(), 144 | start(6666). 145 | 146 | serve(Table,Socket) -> serve(Table,Socket, fun read/1). 147 | serve(Table,Socket,ReadFn) -> 148 | receive 149 | {tcp, Socket, Data} -> 150 | NextReadFn = process_cmds(Table, Data, Socket, ReadFn), 151 | serve(Table,Socket,NextReadFn); 152 | {tcp_closed, Socket} -> 153 | ok; 154 | Else -> io:format("got something weird: ~p~n", [Else]) 155 | end. 156 | 157 | process_cmds(Table, Data, Socket, ReadFn) -> 158 | case ReadFn(Data) of 159 | {incomplete, Cont} -> Cont; 160 | {ok, [Cmd | Args] = FullCmd, Rest} -> 161 | %%io:format("GOT: ~p~nRAW: ~p~n", [FullCmd, Data]), 162 | Res = try 163 | process(Table, Cmd, Args) 164 | catch 165 | throw:Msg -> {error, Msg}; 166 | error:E -> 167 | io:format("ERROR ~p~n", [E]), 168 | {error, "Internal error, see log"} 169 | end, 170 | %%io:format(" -> ~p~nRAW> ~p~n", [Res, iolist_to_binary(write(Res))]), 171 | gen_tcp:send(Socket, write(Res)), 172 | if size(Rest) == 0 -> 173 | %% Read everything, just return 174 | fun read/1; 175 | true -> 176 | %% io:format("MORE TO READ ~p\n", [size(Rest)]), 177 | %% Still more data to read, try reading next 178 | process_cmds(Table, Rest, Socket, fun read/1) 179 | end 180 | end. 181 | 182 | coerce_int(Bin) -> 183 | try binary_to_integer(Bin) 184 | catch error:badarg -> throw("Not integer") 185 | end. 186 | 187 | update(T, Key, Default, UpdateFn) -> 188 | Old = case ets:lookup(T, Key) of 189 | [{_, Val}] -> Val; 190 | _ -> Default 191 | end, 192 | {New,Ret} = UpdateFn(Old), 193 | ets:insert(T, {Key, New}), 194 | Ret. 195 | 196 | incr(T, Key, By) -> 197 | update(T, Key, <<"0">>, 198 | fun(Bin) -> 199 | New = coerce_int(Bin) + By, 200 | {integer_to_binary(New), New} 201 | end). 202 | 203 | hupdate(T, Key, Field, Default, UpdateFn) -> 204 | update(T, Key, #{}, 205 | fun(Map) when is_map(Map) -> 206 | NewVal = UpdateFn(maps:get(Field, Map, Default)), 207 | NewMap = maps:put(Field, NewVal, Map), 208 | {NewMap, NewVal}; 209 | (_) -> throw("Not a map") 210 | end). 211 | 212 | process(_, <<"COMMAND">>, [<<"DOCS">>]) -> "OK"; 213 | process(_, <<"PING">>, []) -> "PONG"; 214 | process(_, <<"PING">>, [Msg]) -> Msg; 215 | process(_, <<"CONFIG">>, [Cmd, Name]) -> 216 | [Name, 217 | case [Cmd, Name] of 218 | [<<"GET">>, <<"save">>] -> <<"3600 1 300 100 60 10000">>; 219 | [<<"GET">>, <<"appendonly">>] -> <<"no">> 220 | end]; 221 | process(T, <<"SET">>, [Key, Val]) -> ets:insert(T, {Key,Val}), "OK"; 222 | process(T, <<"GET">>, [Key]) -> 223 | case ets:lookup(T, Key) of 224 | [{_,Val}] -> Val; 225 | [] -> null 226 | end; 227 | process(T, <<"INCR">>, [Key]) -> 228 | incr(T, Key, 1); 229 | process(T, <<"INCRBY">>, [Key, By]) -> 230 | incr(T, Key, coerce_int(By)); 231 | process(T, <<"DECR">>, [Key]) -> 232 | incr(T, Key, -1); 233 | process(T, <<"DECRBY">>, [Key, By]) -> 234 | incr(T, Key, -1 * coerce_int(By)); 235 | process(T, <<"HSET">>, [Key, Field, Value]) -> 236 | hupdate(T, Key, Field, ignore, 237 | fun(_) -> Value end); 238 | process(T, <<"HGET">>, [Key, Field]) -> 239 | case ets:lookup(T, Key) of 240 | [{_,Val}] -> maps:get(Field, Val, null); 241 | [] -> null 242 | end; 243 | process(T, <<"HINCRBY">>, [Key, Field, By]) -> 244 | hupdate(T, Key, Field, <<"0">>, fun(N) -> integer_to_binary(coerce_int(N) + coerce_int(By)) end); 245 | process(T, <<"HKEYS">>, [Key]) -> 246 | case ets:lookup(T, Key) of 247 | [{_,Val}] -> maps:keys(Val); 248 | [] -> [] 249 | end; 250 | process(T, <<"DEL">>, Keys) -> 251 | lists:foldl(fun(K,A) -> 252 | A + case ets:lookup(T, K) of 253 | [] -> 0; 254 | _ -> ets:delete(T, K), 1 255 | end 256 | end, 0, Keys). 257 | --------------------------------------------------------------------------------